[NextJS] 세션,로컬 스토리지 사용 시 Hydrate 이슈 커스텀훅으로 해결하기

hoonsbory·2023년 5월 28일
1
post-thumbnail

Hydrate 이슈란?

NextJS 서버는 렌더링된 정적 페이지를 클라이언트에게 보내고, react는 번들링된 JavaScript 코드를 클라이언트에게 보냅니다.

클라이언트는 전달받은 HTML과 JS코드를 매칭하는데, 이를 Hydrate, 수화라고 합니다.

Hydrate란 전송받은 JavaScript들이 이전에 보내진 HTML DOM 요소 위에서 한번 더 렌더링 하게 되면서 각각 자기 자리를 찾아가며 매칭되는 것인데, 이 때 사전 렌더링된 HTML과 다른 내용이 렌더링될 때 나타나는 이슈가 바로 Hydrate 이슈입니다.

hydration failed because the initial ui does not match what was rendered on the server.

왜 세션, 로컬 스토리지의 데이터를 사용할 때 이런 에러가 발생할까요?

사실 NEXTJS에서 초기 렌더링 때 직접적으로 세션 , 로컬 스토리지의 데이터를 불러와 렌더링하는 것은 불가능합니다.

사전 렌더링은 서버 (NODE) 환경에서 이루어지기 때문에 브라우저 API인 세션, 로컬 스토리지가 없어서 에러를 뱉어내기 때문이죠.


그럼 언제 이런 에러가 날까요?

바로 Recoil, Zustand 같은 전역 상태 관리 라이브러리에서 Persist 데이터를 다룰 때 일어납니다.
(물론 더 다양한 경우로 에러가 발생하기도 하는데, 결국 해결법은 하나입니다)

원래대로라면 사전 렌더링 때 Window 객체가 없기 때문에 localStroage is not defined 같은 에러가 발생해야 하지만

위 라이브러리들은 사전 렌더링 때 스토리지 객체가 없을 상황에 대한 예외 처리가 되어있기 때문에 에러가 발생하지 않습니다.


코드로 문제의 흐름을 한번 살펴보겠습니다.

UserInfo.tsx

import React from 'react';

const UserInfo = () => {
  const userName = useUserStore(store => store.userName);
  return <h1>{userName}</h1>;
};

export default UserInfo;

세션, 로컬 스토리지에서 userName을 꺼내 렌더링하는 코드입니다.

(store는 zustand 기준으로 작성했지만, hydrate 이슈를 유발하는 데이터를 받아오는 코드라고 생각하면 됩니다.)

이 코드를 실행하면,

NextJS는 사전 렌더링을 시작하고

세션, 로컬 스토리지에서 데이터를 가져오는 코드를 만나면 window 객체가 없으므로 store에 설정한 초기값이 할당됩니다.

초기값이 빈값이라고 가정하면 사전 렌더링된 html의 모습은 <h1></h1> 입니다.

사전 렌더링이 완료되면 한 번 더 코드를 훑으면서 HTML을 매칭시킵니다. 이 과정을 hydarte, 수화라고 합니다.

이 과정은 브라우저에서 진행되므로 세션,로컬 스토리지에 접근이 가능하기 때문에 Hydrate 중에 달라진 <h1>john doe</h1> 을 만나고 에러를 발생시키는겁니다.

해결방법

위 내용을 읽었다면 어느정도 유추가 가능할겁니다.

단순하게 사전 렌더링과 브라우저 렌더링의 HTML을 맞춰주면 됩니다.

위 문제는 사전 렌더때는 데이터를 불러올 수 없기 때문에 발생한 문제입니다.

그럼 해결방법은 두가지가 있겠죠.

  1. 사전렌더 때 데이터를 불러온다.
  2. hydrate (브라우저에서 첫 렌더링) 할때까지 데이터를 불러오지 않는다.

window객체가 필요하기 때문에 아무래도 1번은 불가능할 것 같습니다.

그렇다면 답은 2번인데, 아무래도 데이터를 늦게 불러오는 것보다는 데이터를 미리 불러오고 hydrate 후에 렌더링을 하는 것이 효율적으로 보이네요.

import React, { useState } from 'react';

const UserInfo = () => {
  const userName = useUserStore(store => store.userName);
  const [isMounted, setIsMounted] = useState(false);
  useEffect(() => {
    setIsMounted(true);
  }, []);

  return <h1>{isMounted && userName}</h1>;
};

export default UserInfo;

이렇게 하면 컴포넌트가 mount된 후에 userName을 렌더링하기 때문에 초기 렌더링 시<h1></h1> 으로 hydrate때 에러가 발생하지 않습니다.

하지만 hydate 이슈가 발생하는 곳마다 코드를 작성하기는 불편하기 때문에 커스텀훅으로 빼주는 것이 좋겠네요.

UseHydratedData.tsx

import { useEffect, useState } from 'react';

const useHydratedData = <T>(beforeHydrateData: T): T => {
  const [afterHydrateData, setAfterHydrateData] = useState<T | false>(false as T | false);
  useEffect(() => setAfterHydrateData(beforeHydrateData), [beforeHydrateData]);
  return afterHydrateData as T;
};

export default useHydratedData;

데이터를 받아서 hydrate 전엔 false, 후엔 데이터를 리턴하는 커스텀 훅입니다.

const UserInfo = () => {
  const userName = useUserStore(store => store.userName);
  const afterHydrateUserName = useHydrated<string>(userName);

  return <h1>{afterHydrateUserName}</h1>;
};

export default UserInfo;

이렇게 하면 hydrate전까지는 afterHydrateUserName이 false기 때문에 렌더링되지 않아서 hydrate 이슈를 피할 수 있습니다.

0개의 댓글