[SSR] 필터링된 데이터를 가져올 때 SSR을 활용하여 체감 로드 속도 개선
🔥 결론
홈화면을 SSR로 그리면서 쿼리 파라미터로 필터링된 데이터를 사용자에게 빠르게 제공할 수 있게 되었어요.
CSR | SSR |
---|---|
🚨 문제 상황
앞서 말했듯이 필터링된 데이터를 사용자에게 빠르게 보여줘야하는데 클라이언트에서 처리했더니 지연이 발생했어요. 사용자는 아래 3가지 상황에서 필터링된 데이터를 보게 돼요.
- 사용자가 직접 필터를 추가하는 경우
- 참여자로 로그인한 직후 또는 홈화면으로 들어올 때 참여자 로그인 정보가 있는 경우
- 필터링이 걸린 상태에서 새로고침하는 경우
위 3가지 상황 중에 1번은 상호작용에 의한 데이터 변경
, 2번과 3번은 페이지 로드 시점의 데이터 변경
이예요. 따라서 1번은 문제가 없지만, 2번과 3번 상황은 페이지가 로드될 때 노출되어야 하는 정보가 필터링에 따라 달라져야 해요.
nextjs app router에선 서버 컴포넌트가 디폴트이기 때문에, pre-rendering 되어 HTML에 컴포넌트가 그려져서 넘어와요.
이때 2번과 3번처럼 필터링 정보에 따라 다른 데이터를 노출해야 하는 상황을 클라이언트에서 처리하는 경우 깜박임이 발생해요. 전체 데이터를 노출했다가 쿼리 파라미터가 변경되고 필터링된 데이터를 노출하는거죠.
- HTML pre-rendering (
로딩 UI
)- 공고 목록 API 요청 (
전체 데이터
)- 유저 정보 API 요청 + 쿼리 파라미터 업데이트 (
전체 데이터
)- 참여자 정보 기반으로 공고 목록 API 요청 (
전체 데이터
)- 전체 페이지 로딩 완료 (
필터링된 데이터
)
그래서 잘못된 데이터를 노출하는 것보단 로딩 UI를 보여주기로 결정했어요.
이러다 보니 유저 정보 응답을 기다린 후 공고 목록 조회가 완료되어야 사용자는 필터링된 데이터를 볼 수 있어요.
- HTML pre-rendering (
로딩 UI
)- 유저 정보 API 요청 + 쿼리 파라미터 업데이트 (
로딩 UI
)- 참여자 정보 기반으로 공고 목록 API 요청 (
로딩 UI
)- 전체 페이지 로딩 완료 (
필터링된 데이터
)
✅ 문제 해결
페이지 레벨의 Home 컴포넌트를 서버 컴포넌트로 처리하고, SSR을 적용했어요.
- Home 컴포넌트에서 HTTP request URL을 파싱하여 주입받는 searchParams으로
필터 정보
를 얻고, next-auth의 session으로인증 정보
를 확인해요.- 인증 정보를 확인하여 참여자면
유저 정보
를 가져와 자동 적용될 필터값을 구하고, 이를 기반으로공고 목록을 조회
해요.- 하이드레이션 이후 추가 네트워크 요청이 발생하지 않도록
prefetchQuery
로 주입해요.
// src/app/home/page.tsx
interface HomePageProps {
searchParams: {
[k in keyof ExperimentPostListFilters]?: string;
};
}
export default async function Home({ searchParams }: HomePageProps) {
const session = await getServerSession(authOptions);
const queryClient = getQueryClient();
const fetchClient = createSSRFetchClient(session?.accessToken);
const hasQueryParams = Object.keys(searchParams).length > 0;
const initialUserInfo =
session?.role === Role.participant
? await fetchClient.get<ParticipantResponse | ResearcherResponse>(
API_URL.me(session.role.toLowerCase())
)
: null;
const initialGender =
initialUserInfo && isParticipantInfo(initialUserInfo)
? initialUserInfo.gender
: undefined;
const initialAge =
initialUserInfo && isParticipantInfo(initialUserInfo)
? calculateAgeFromBirthDate(initialUserInfo.birthDate)
: undefined;
const parsedParamsResult = URLFilterSchema().safeParse({
gender: hasQueryParams ? undefined : initialGender,
age: hasQueryParams ? undefined : initialAge,
...searchParams,
});
const filters: ExperimentPostListFilters = parsedParamsResult.success
? { ...parsedParamsResult.data, count: POST_PER_PAGE }
: { recruitStatus: "ALL", count: POST_PER_PAGE };
const queryParams = getQueryParamsToString({ ...filters });
const initialPosts = await fetchClient.get<ExperimentPostResponse>(
API_URL.postList(queryParams),
{
requireAuth: false,
next: { tags: ["experiment-posts"] },
}
);
if (session?.role) {
await queryClient.prefetchQuery({
queryKey: queryKey.userInfo(session.role),
queryFn: () => Promise.resolve(initialUserInfo),
});
}
await queryClient.prefetchInfiniteQuery({
queryKey: queryKey.post(filters),
queryFn: () => Promise.resolve(initialPosts),
initialData: {
pages: [initialPosts],
pageParams: [1],
},
initialPageParam: 1,
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<DefaultLayout>
<Banner />
<ExperimentPostContainer
initialPosts={initialPosts}
initialGender={hasQueryParams ? undefined : initialGender}
initialAge={hasQueryParams ? undefined : initialAge}
/>
</DefaultLayout>
</HydrationBoundary>
);
}
🥕 효과
Next.js에서 앱 전체를 렌더링할 때의 플로우를 다시 정리해봤어요.
- 서버에서 전체 앱에 대한 데이터를 가져온다.
- 서버에서 전체 앱을 HTML로 렌더링하고 응답으로 보낸다. (
pre-rendering
)- 클라이언트에서 전체 앱에 대한 JS 번들을 로드한다.
- 응답받은 HTML에 이벤트 핸들러가 부착되어 상호작용할 수 있는 화면이 만들어진다. (
hydration
)- 클라이언트 로직을 수행한다.
쿼리파라미터 필터링 로직을 수행할 때, SSR
을 적용하면 1번에서 필터링된 데이터를 가져오고 2번에서 필터링된 데이터를 바로 그릴 수 있어요.
반면 CSR
로 수행하면 1~4번까지 수행하고 5번에서 유저 정보 응답을 기다린 후 공고 목록 조회가 완료되어야 사용자는 필터링된 데이터를 볼 수 있어요.
SSR은 기존보다 1번 과정의 시간은 좀더 늘어나겠지만 서버에서 데이터 페칭을 수행해 더 빠르고 브라우저에 의존하지 않는 이점이 있어요.
댓글남기기