Next.js SSR 전환 과정에서 만난 인증 에러와 중복 페칭 문제 해결기
Next.js SSR 사용이유
프로젝트 메인 페이지에는 사용자 자산 데이터와 이미지가 포함된 카드가 20~30장씩 한 번에 렌더링됩니다. 단순히 React CSR만으로는 첫 로딩이 느려지고, SEO 측면에서도 불리하다고 판단했습니다.
Next.js는 이미지 최적화 기능을 자동 제공하고, 페이지 대부분이 정적 데이터 기반이어서 SSR(Server Side Rendering) 전환이 적합하다고 생각했습니다.
SSR 전환 과정에서 두 가지 문제가 발생했습니다:
- 인증 쿠키가 서버에서 전달되지 않아 발생한 인증 에러
- 서버와 클라이언트가 동일 데이터를 중복 요청하는 중복 페칭 문제
아래에서는 문제 원인과 해결 과정을 기록합니다.
React Query 도입 배경
일단, SSR 환경에서도 React Query의 상태 관리 기능이 필요한 이유는 크게 세 가지였습니다.
- 낙관적 업데이트(좋아요 기능) 필요
- 카드에는 좋아요 기능이 포함되어 있어, 사용자가 클릭 시 즉시 UI에 반영해야 합니다.
- React Query의 캐시와 낙관적 업데이트 기능을 활용하면 SSR에서 내려준 데이터도 클라이언트 캐시와 자연스럽게 이어지고, 좋아요 클릭 시 캐시를 즉시 반영한 뒤 서버 응답과 동기화할 수 있습니다.
- 캐싱 및 동기화 관리
- 사용자 자산 데이터는 단일 페이지에 여러 컴포넌트에서 반복적으로 사용됩니다.
- React Query 캐시를 사용하면 동일 데이터를 여러 컴포넌트에서 재활용할 수 있고, 서버에서 변경된 내용은 자동으로 동기화되어 최신 상태를 유지할 수 있습니다.
- 개발 경험 및 코드 통일성
- 이미 다른 CSR 영역에서는 React Query를 사용하고 있었기 때문에, SSR 환경에서도 React Query를 도입하면 데이터 패칭/캐싱 로직을 일관되게 관리할 수 있습니다.
1. SSR 전환 시 인증 에러
원인
Next.js SSR 환경에서는 브라우저 전용 customFetch 유틸을 그대로 사용했습니다.
- 서버는 상대 URL 사용 불가 → full URL 필요
- 쿠키 자동 전달 불가 → SSR에서는 직접 헤더에 쿠키를 심어야 함
그 결과, SSR 렌더링 단계에서 “인증되지 않은 사용자입니다” 오류가 발생했고, 페이지가 클라이언트 렌더링으로 강제 전환되었습니다.
해결 과정
- 기존 fetch 유틸 함수에 SSR 분기 추가
- SSR 환경에서는 next/headers에서 쿠키를 읽어와 직접 헤더에 추가하도록 수정했습니다.
// customFetch.ts
if (typeof window === "undefined") {
const { cookies } = await import("next/headers");
const cookieStore = await cookies();
const cookieHeader = cookieStore.toString();
if (cookieHeader) {
headers.Cookie = cookieHeader;
}
}
2. 중복 페칭 문제
원인
SSR에서 부모 서버 컴포넌트가 데이터를 props로 내려주고, 클라이언트 자식 컴포넌트가 useQuery로 같은 데이터를 다시 호출했습니다.
React Query 입장에서는 캐시가 비어 있으므로 마운트 직후 다시 네트워크 요청 → 중복 페칭 발생.
방식 선정
- initialData 옵션: 각 쿼리에 props를 직접 주입해야 하고, 메타데이터 복원이 불완전
- Hydration(dehydrate + HydrationBoundary): 서버에서 쿼리 캐시를 직렬화해 클라이언트에서 복원 가능, 메타·상태까지 일관성 유지
앱 전체에서 일관된 데이터 캐시 관리가 필요했기 때문에 Hydration 방식을 선택했습니다.
해결 과정
HomeLayout (서버 컴포넌트)
export default async function HomeLayout({ children }: { children: React.ReactNode }) {
const queryClient = getQueryClient();
await queryClient.prefetchQuery(cardListOptions());
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<Suspense fallback={<div>Loading profile...</div>}>
{children}
</Suspense>
</HydrationBoundary>
);
}
HomePage (클라이언트 컴포넌트)
export default function HomePage() {
const { data } = useSuspenseQuery(cardListOptions());
// ...
}
- prefetchQuery: 서버에서 데이터 미리 가져와 QueryClient에 캐시 저장
- dehydrate: 서버 캐시를 JSON으로 직렬화해 클라이언트로 전달
- HydrationBoundary: 직렬화된 캐시를 클라이언트에서 복원
- useSuspenseQuery: 복원된 캐시를 즉시 사용, 네트워크 재요청 방지
이번 경험을 통해 SSR 환경에서의 인증 흐름과 쿠키 처리, CSR과 SSR의 경계에서 발생하는 중복 페칭 문제 React Query의 Hydration 패턴에 대해 학습해보게 되었습니다.
'FE' 카테고리의 다른 글
| [Next.js] customFetch로 효율적으로 API 호출하기 (0) | 2025.08.17 |
|---|---|
| [FE] FSD 적용 전 고려해봐야 할 것들 (0) | 2024.07.07 |