[에러 로그] Next.js에서 페이지 이동 시 상태 초기화 문제

D uuu·2025년 1월 18일
0

에러해결

목록 보기
4/7
post-thumbnail

문제 상황

서비스 운영 중 처음으로 유저가 실제로 에러를 발생시키면서 에러 로그를 메일로 받았다. 에러 메시지는 다음과 같았다.

An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.

에러 메시지를 보면 서버 컴포넌트 렌더링 중 문제가 발생했지만, 자세한 내용은 민감한 정보로 인해 생략되었다는 내용이었다. 이를 해결하기 위해 로그를 분석하고 테스트를 진행하며 문제를 파악하기 시작했다.

문제 파악

문제 재현

테스트를 진행해 보니 유저가 정보를 입력한 후 페이지를 이동할 때 Context API 값이 초기화되는 문제가 핵심이라는 것을 확인했다. 흥미로웠던 점은 로컬 환경에서는 문제가 발생하지 않았지만, 운영 환경에서만 에러가 발생한다는 점이었다. 에러가 발생한 경로는 다음과 같다.

  1. 유저는 정보를 단계별로 입력한다.
  2. Context API를 사용해 상태를 관리하며 페이지 전환 시 이전 데이터를 유지해야 한다.
  3. 하지만 페이지 이동 시 Context 값이 초기화되어 데이터가 유실된다.

디버깅 과정

첫번째 시도) layout.tsx 로 Provider 를 이동

기존에 Provider 컴포넌트를 페이지 레벨에 선언했었는데 페이지 전환 시 컴포넌트가 다시 렌더링되면서 Context 값이 초기화되는게 아닐까 하는 생각이 들었다. 그래서 Provider 를 layout.tsx 으로 이동시켜봤다. layout 페이지는 여러 페이지간에 공유되는 UI 를 설정할 수 있는 컴포넌트로 리렌더링 되지 않는 특징이 있다.

하지만 결과는.. 여전히 페이지 이동 시 context 값이 초기화됐다.

app/goals/layout.tsx 

const GoalsLayout = ({ children }: { children: ReactNode }) => {
    return <Provider>{children}</Provider>;
};

export default GoalsLayout;

두번째 시도) RootLayout 으로 Provider 를 이동

다음으로 혹시 최상위 RootLayout에 Provider를 선언해서 상태를 글로벌하게 유지하려 했으나 이 역시 페이지 전환시 상태가 계속 초기화되었다.

세번째 시도) Context API 를 Zustand 로 변경

Context API 대신 Zustand를 사용하여 상태를 관리하도록 변경해봤다. Zustand는 컴포넌트 트리와 독립적으로 상태를 관리하므로, 페이지 전환 시에도 데이터 유지가 가능하리라 예상했지만, 동일한 문제가 발생했다🤔

💡페이지 이동 시 State가 초기화 되는 문제

해결방법

페이지 이동 시 상태(State)가 초기화되는 문제로 라우팅과 관련이 있을 것으로 생각하여 Next.js 공식 문서를 참고하던 중, Native History API에 대해 알게 됐다. 이 API는 브라우저에서 기본적으로 제공되며, 브라우저 히스토리 스택을 관리할 수 있게 해준다.

https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#windowhistoryreplacestate

window.history.pushState

나의 경우에는 페이지를 새로 렌더링하지 않고 URL만 변경되기를 원했다. 따라서 router.push 대신 window.history.pushState 를 사용하는 것이 더 적합했다. window.history.pushState는 페이지를 새로 렌더링하지 않고 URL만 변경해준다. 또한, 히스토리를 관리하여 뒤로가기를 눌렀을 때 이전 상태를 그대로 확인할 수 있다.

그래서 기존 router.push() 로직을 window.history.pushState 로 변경하니 상태를 유지할 수 있었다!

사용법

history.pushState(state, unused, ur;);
  • state : 브라우저 이동 시 넘겨줄 데이터
  • unused : 사실상 사용하지 않는 옵션, 빈 문자열을 넣어준다.
  • url : 변경할 브라우저 주소

useSearchParams

window.history.pushState 로 페이지를 리로드 없이 URL을 변경한 후, 변경된 URL에 접근하기 위해 useSearchParams 를 사용했다. 공식 문서에서는 pushState 와 replaceState 가 Next.js 라우터에 통합되어 있어 usePathname과 useSearchParams 을 통해 동기화된다고 나와있다.

그런데 useSearchParams를 이용하니 사용하고 있던 그래프 라이브러리에서 Could not find a suitable point for the given distance 에러가 발생했다. url 변경과 그래프 라이브러리가 렌더링되는 타이밍 불일치로 발생하는 문제 같아 보였다. 이 문제는 useSearchParams 대신 popstate 이벤트 리스너로 직접 변경 사항을 체크하고 업데이트하니 해결되었다.

Zustand Persist

이번에 찾아보다가 알게된 부분인데 새로고침을 하거나 브라우저를 껐다가 켜도 데이터를 유지하고 싶다면 persist 를 사용하는 방법도 있다. 이름 그대로 Zustand 에 영속성을 부여하는 모듈로, 로컬스토리지나 세션스토리지 같은 브라우저 저장소에 데이터를 저장하기 때문에 새로고침을 하거나 페이지를 이동해도 데이터를 유지할 수 있다. 찾아보니 redux 에도 비슷한 목적으로 persist 모듈이 존재한다.

왜 운영환경에서만 문제가 발생할까?

여러 방법을 시도해본 결과, 개발 환경과 운영 환경에서의 렌더링 차이로 인한 문제인 것으로 보인다. 개발환경에서는 빠른 디버깅과 피드백을 위해 컴포넌트가 실시간으로 렌더링되며, 상태 변화가 즉시 반영된다. 반면, 운영환경에서는 Next.js 가 페이지를 사전 렌더링하는 방식으로 동작한다. 모든 페이지는 미리 정적 HTML 로 생성되고 클라이언트에서 네비게이션할 때도 서버 렌더링된 HTML이 우선적으로 렌더링된다.

이로인해 페이지가 전환될 때 서버에서 처음 렌더링되는 컴포넌트들은 현재 context 값에 접근할 수 없는 것이다. 따라서 첫 번째 렌더링은 기본값(default value)으로 이루어지고, 이후 Hydration 과정에서 클라이언트가 상태를 반영하여 다시 렌더링된다.

디버깅을 하던 중, 페이지 전환 후 상태에 바로 접근하면 상태가 초기화된 것처럼 보이다가 몇 번의 재시도 후 상태가 제대로 반영되는 현상을 확인할 수 있었다. 이 과정은 아마도 hydration이 완료된 후 상태가 반영되는 시점과 관련이 있지 않나 생각된다.

추가 (+2025/1/31)

Next.js 12에서는 router.push('url', { shallow: true }) 를 명시적으로 사용해야만 페이지 이동 시 전체 리로드 없이 상태를 유지할 수 있었다. 하지만 최신 버전에서는 shallow routing 이 기본적으로 적용되어, 페이지 이동 시에도 전체 리로드가 발생하지 않는다고 공식문서에 명시되어 있다!

그런데 내 경우, router.push() 로 페이지를 이동할 때마다 리로드가 발생하며, 서버에서 새로운 HTML을 다시 가져오는 현상이 발생했다. 이로 인해 상태가 유지되지 않는 문제가 있었고.. 분명 shallow 로 동작하지 않는 거 같은데 공식문서의 설명과는 맞지 않는 듯해 문제를 해결하고 나서도 찝찝한 마음이 컸었다.

그러다 동일한 이슈를 발견!

나와 동일한 문제가 제기된 사례를 찾았다.
router.push()를 사용해 search param 을 변경하면 페이지가 다시 로드되는 현상이 발생한다는 것이다. 나 역시 기존 코드에서 router.push 로 URL의 파라미터를 변경하고 있었다. 아래 설명을 보면 나와 완전히 똑같은 문제를 겪고 있다는 걸 확인할 수 있었다.

이로써 정리가 되었다. router.push 는 공식문서대로 shallow routing 을 지원한다. 그러나 search param 을 변경할경우 hard reload 가 진행된다. 이 문제에 대한 해결책으로는 내가 위에서 해결했던 것과 동일하게 window.history.pushState() 를 사용하면 된다.


관련 이슈 https://github.com/vercel/next.js/discussions/49540

마무리

이번 문제의 근본 원인은 Next.js 의 렌더링 방식에 대한 이해가 부족했던 데 있었다고 생각한다. Next.js는 많은 기능을 제공해줘 사용할땐 편하다고 느끼지만 이를 올바르게 이해하고서 사용해야지 그렇지 않으면 예상치 못한 동작이 발생하고 그 원인을 파악하는 것도 어려워진다는 걸 알게됐다. 역시 정확히 이해하고 사용해야 좋은 기능을 잘 활용할 수 있는듯하다.

이번 기회에 Next.js에 대한 근본적인 공부를 다시 해야겠다는 필요성을 많이 느끼게 되었다!

참고글
https://little-coder-dongtony.tistory.com/28?category=967615
https://medium.com/@awayyao/keep-page-state-between-page-navigation-in-nextjs-13-using-app-router-schema-f6eacd0300ad
https://github.com/vercel/next.js/discussions/18072
https://github.com/vercel/next.js/discussions/48154

profile
배우고 느낀 걸 기록하는 공간

0개의 댓글

관련 채용 정보