Next.js + Zustand: Hydration 문제 해결하기

서예림·2025년 6월 19일
13
post-thumbnail

이번에 새 프로젝트를 시작하면서 SEO를 어느정도 고려해야했기 때문에 next.js를 도입하게 되었습니다. 클라이언트 상태 관리는 직관적인 Zustand를 활용했고 API 통신은 익숙한 axios를 이용해서 호출했습니다.

개발을 진행하면서 서버와 클라이언트 컴포넌트 간의 상태 불일치로 인한 헤더 깜빡임 이슈를 겪게 되었고 이를 해결 해나간 과정을 정리해보려고 합니다!

1. 새로고침 시 헤더가 깜빡 거리는 현상 발생

처음 문제를 인식한건 로그인 상태에서 새로고침 했을 때 였습니다.
로그인 상태임에도 불구하고,

  • 헤더가 잠깐 비로그인 상태로 깜빡임
  • 다시 로그인 상태로 전환되는 현상이 발생했습니다.

브라우저 새로고침 시에 항상 발생하는 문제였고, CSR 환경에서는 문제가 없던 문제였기 때문에 SSR 동작 과정에서의 상태 처리 문제라고 생각하게 되었습니다.

2. Next.js의 렌더링 방식과 Hydration

💡 Next.js는 곧 SSR이 아니다.

많은 사람들이 착각하는 부분인데, Next.js ≠ SSR입니다.
Next.js는 CSR, SSR, SSG를 자유롭게 혼합할 수 있는 하이브리드 프레임워크입니다.

Next.js App Router의 컴포넌트 구분

app/디렉토리를 기준으로 컴포넌트는 아래처럼 나뉩니다.

  • 서버 컴포넌트 (기본): 서버에서 HTML을 렌더링하고 클라이언트에는 JS가 전송되지 않음
  • 클라이언트 컴포넌트 ("use client" 필요): 상태관리, 이벤트 핸들링 등 클라이언트 환경에서만 동작

Hydration 과정에서 상태 불일치 문제

이런 구조로 인해 서버와 클라이언트에서의 상태가 일치하지 않으면 Hydration시 깜빡임 또는 UI 불일치가 발생할 수 있습니다.

3. 문제 해결 시도

3-1) 첫 번째 시도: 클라이언트에서 세션을 재 동기화

처음에는 클라이언트에서 Zustand 상태를 동기화하는 sessionHydration 컴포넌트를 만들었습니다.

"use client"

import { useEffect } from "react"
import { useUserStore } from "@/store/user"
import { getSession } from "@/lib/auth"

export function SessionHydration() {
  const setUser = useUserStore((state) => state.setUser);

  useEffect(() => {
    getSession().then((session) => {
      if (session) {
      	setUser(session.user);
      }
    });
  }, []);

  return null;
}

Header 컴포넌트는 이 상태를 바라보고 렌더링 하도록 처리해보았지만,

  • 페이지가 처음 서버에서 렌더링 될 때는 상태를 알지 못함
  • 클라이언트에서 Zustand가 업데이트가 되기 전 까지는 비로그인 상태로 보이는 깜빡임이 여전히 발생했습니다.

3-2) 두 번째 시도: 서버 컴포넌트에서 세션 상태 가져오기

서버에서 세션 상태를 먼저 가져오고 그 결과를 Header에 props로 넘기면 어떨까 생각해서 시도해보았습니다.

// layout.tsx
import { fetchSession } from "@/lib/auth"

export default async function Layout({ children }: PropsWithChildren) {
  const session = await fetchSession();

  return (
    <>
      <Header session={session} />
      {children}
    </>
  );
}

이렇게 하면 SSR 단계에서 로그인 상태를 미리 알고 있기 때문에 상태 불일치에 따른 깜빡임이 더이상 일어나지 않았습니다. 👏🏻

그렇지만....

4. 실서버에 배포 후, 예상치 못한 문제 발생

실제로 서버에 배포 후 사내에서 테스트 해보니 로그인 호출은 정상적으로 동작했는데, 로그인 상태로 전환되지 않는 문제가 발생했습니다.

근본 원인 분석: Next.js 서버에서의 API 호출 위험성

왜 그런지 원인을 찾는데 헤매던 와중, 백엔드 개발자 동료의 조언이 있었습니다.

Next 서버에서 API를 호출하는 방식이면, 다중 사용자 환경에서 세션이 꼬일 수도 있습니다.
예를 들면, 쇼핑몰에서 내 계정으로 로그인했는데 다른 사람 장바구니가 보이는 문제와 비슷해요.
세션 상태는 항상 클라이언트에서 판단하는 게 안전합니다.

왜 이런 문제가 발생할까?

Next.js 서버에서 API 호출 시 발생할 수 있는 문제들

  • 쿠키 전달 문제: Next.js 서버에서 백엔드 API를 호출할 때 브라우저의 쿠키가 제대로 전달되지 않을 수 있음
  • 서버 인스턴스 공유: 여러 사용자의 요청이 같은 서버 인스턴스에서 처리되면서 상태가 꼬일 가능성
  • 네트워크 지연: 서버-서버 통신에서의 타임아웃이나 지연으로 인한 상태 불일치

다중 사용자 환경에서의 위험성

이런 구조에서는 사용자 A의 세션 정보가 사용자 B에게 잘못 전달될 위험이 있습니다.

layout.tsx에서의 과도한 API 호출 문제

또한, layout.tsx에서 세션 상태를 확인하면 모든 페이지 요청마다 API가 호출되어

  • 불필요한 서버 부하 증가
  • 응답 지연으로 인한 사용자 경험 저하
  • API 호출 실패 시 전체 페이지 렌더링 지연

위와 같은 문제들이 발생할 수 있었습니다.

5. 최종 해결 방법: 쿠키 기반 SSR + 클라이언트 동기화

그래서 다시 구조를 변경해보았습니다.

  • layout.tsx에서 Next.js의 cookies()를 사용해 sid 쿠키가 있는지만 확인
  • 이 정보로 isLoggedIn props만 Header에 내려줌
  • 클라이언트에서 Zustand는 알아서 hydration 되도록 설정
import { cookies } from "next/headers"

export default function Layout({ children }: PropsWithChildren) {
  const hasSid = cookies().has("sid");

  return (
    <>
      <Header isLoggedIn={hasSid} />
      {children}
    </>
  );
}

Zustand 설정

store에서 skipHydration옵션을 설정하거나 서버에서 초기화 되지 않도록 설정해주었습니다.

persist(
  (set) => ({
    user: null,
    setUser: (u) => set({ user: u }),
  }),
  {
    name: "user-store",
    skipHydration: true,
  }
)

마무리

이번 문제 해결을 통해서

  • Next.js는 하이브리드 프레임워크입니다. SSR만의 프레임워크가 아니라 CSR/SSR/SSG를 상황에 맞게 조합할 수 있습니다.
  • 서버와 클라이언트의 역할을 명확히 분리해야 합니다. 서버에서는 최소한의 정보만 판단하고, 상세한 상태 관리는 클라이언트에서 처리하는 것이 안전합니다.
  • 다중 사용자 환경에서의 보안을 항상 고려해야 합니다. 서버에서 사용자별 API를 호출할 때는 세션 꼬임 위험을 인지해야 합니다.

🔗 참고자료

혹시 더 좋은 해결 방법이나 다른 접근법을 알고 계시다면 언제든지 알려주세요.
잘못된 내용이나 개선할 점이 있다면 언제든 피드백 부탁드립니다. 감사합니다. 🙇🏻‍♀️

6개의 댓글

comment-user-thumbnail
2025년 6월 20일

저도 이번에 사이드 프로젝트로 Next를 사용하는데, 이 글이 너무 도움이 될 것 같아요. 원인 파악에만 오랜 시간을 쏟을 뻔했는데 유익한 글 감사합니다~!

답글 달기
comment-user-thumbnail
2025년 6월 21일

결국 잘 해결하셨네요!!!
문제 상황, 해결 시도, 원인, 마무리까지.. 트러블 슈팅관련 글 정말 잘쓰시는거 같아요!
앞으로도 화이팅입니다~~!!

답글 달기
comment-user-thumbnail
2025년 6월 22일

이전의 hydration 이슈 경험에서 고민했던 흐름과 유사해서 공감하며 읽었습니다. 그땐 적당히 정리하고 넘어갔었는데, 이렇게 배경과 적용했던 방법들을 자세히 남겨놨으면 좋았을 것 같네요. 잘 보고 갑니다!

답글 달기
comment-user-thumbnail
2025년 6월 22일

next 는 참.. 좋으면서도 이런 어려운점이 많은거 같습니다. 저도 최근에 hydration 관련 부분 해결하는 발표를 들은적이 있는데요. 그 내용에서는 hook 관련 회피방법이였는데, 라이브러리 사용하면서 이렇게도 해결을 할 수 있군요.
저도 이번주 부터는 예림님처럼 유익한 글을 쓰도록 노력해보겠습니다. !

답글 달기
comment-user-thumbnail
2025년 6월 22일

이슈 상황부터 해결 과정까지 잘 이해되는 글인 것 같아요!!
앞으로도 화이팅임니다 💪💪💪

답글 달기
comment-user-thumbnail
2025년 7월 6일

문제점을 파악하고 해결을 위해 여러 방식으로 접근하는 모습이 흥미로웠습니다..!
Next.js와 SSR에 대해서 다시 한번 생각해보게 하는 글이었습니다!
귀한 경험에 대한 글 잘 읽고 갑니다!

답글 달기