Recoil이란?
Recoil은 리액트를 위한 상태관리 라이브러리이다. NPM Trends를 살펴봤을 때 (2023.07 기준) 다른 라이브러리들에 비해 다운로드 수가 다소 낮은 수준이지만, 그럼에도 불구하고 편리한 사용성으로 적지 않은 관심을 받고 있는 라이브러리이다.
이번 글은 Recoil을 소개하는 글이 아니니 자세한 설명은 생략하겠다.
Next.js와 Recoil
Next.js와 Recoil 라이브러리 자체는 잘 어울리는 편이다. Magic의 영역이 다소 있지만 사용법 자체가 워낙 직관적이고, Next.js에서의 일반적인 개발 흐름에 잘 맞는다. SSR 환경에서도 잘 동작하고...
그런데 만약에 이 state를 클라이언트에 저장하고싶다면? 다시 말해 웹 스토리지에 저장하고 싶다면? 그 때는 recoil-persist 라이브러리를 사용하면 된다.
recoil-persist 라이브러리는 local storage 혹은 session storage에 데이터를 저장한다. 일단 최초에 로딩이 되면 지정한 스토리지에서 데이터를 불러온다. 그리고 불러온 데이터를 recoil 내의 state와 동기화시키고, 실제 상태를 읽고 쓰는 행위 자체는 recoil이 담당하게 된다. recoil-persist는 atom에 업데이트가 발생할 때마다 이를 storage에 넣어 동기화시킨다.
SSR에서의 이슈
1) 서버에는 window.localStorage 객체가 없다
바로 여기서 문제가 발생한다. SSR 구조에서 서버는 미리 렌더링할 데이터를 만들어서 정적으로 내려줘야하는데, 서버에는 웹 스토리지가 없으므로 오류가 발생한다. 이거는 localStorage를 일단 undefined로 선언해서, 서버에서 window.localStorage 객체를 로드하지 않도록 처리하면 된다.
import { recoilPersist } from "recoil-persist";
const localStorage =
typeof window !== "undefined" ? window.localStorage : undefined;
const { persistAtom } = recoilPersist({
key: "recoil-states",
storage: localStorage,
});
2) State를 읽어올 때 발생하는 Hydration 이슈
이제 각 컴포넌트에서 Recoil State를 불러오는 상황을 가정해보자. 일단 Recoil에서 제공해주는 훅으로 State를 불러올 것이다.
import { useRecoilState } from "recoil";
import { ExampleState } from "@/states";
function ExampleComponent() {
const [exampleState, setExampleState] = useRecoilState(ExampleState);
...
}
그렇게 실행해보면! Hydration 오류가 발생한다. 왜일까?
일단 서버에서는 localStorage에서 데이터를 불러오도록 설정했는데, localStorage가 일단 undefined로 들어갔으니까, atom에서 선언한 기본 값이 state로 들어갈꺼다. 그리고 클라이언트에서 Hydration을 실행할 때는 localStorage에서 실제 들어갈 값을 불러오게 된다. 이 때 기본 값과 실제 값이 달라지게 된다. 바로 여기서 Hydration 오류가 발생하는 것이다.
그렇다면 이를 해결하는 방법은? Hydration 자체를 하지 않도록, 다시 말해 서버에서 값을 가져오지 않도록 useEffect를 쓰면 된다.
이제 다시 코드를 조금 수정해보자.
import { useRecoilState } from "recoil";
import { ExampleState } from "@/states";
import { useState, useEffect } from "react";
function ExampleComponent() {
const [exampleState, setClientExampleState] = useState("");
const [recoilExampleState, setExampleState] = useRecoilState(ExampleState);
useEffect(() => {
setClientExampleState(recoilExampleState);
}, [recoilExampleState]);
// state를 변경할 때
setExampleState("변경하고 싶은 값");
...
}
일단 useState로 클라이언트에서 동작하는 state를 하나 만들었다. 그리고 마찬가지로 Recoil에서도 state를 불러온다. 근데 이름이 뭔가 다르다.
원래라면 [exampleState, setExampleState] 였는데, 아래와 같이 이름이 배치되었다.
const [exampleState, setClientExampleState] = useState("");
const [recoilExampleState, setExampleState] = useRecoilState(exampleState);
그리고 useEffect 구문을 보면, 색깔로 칠한 변수는 보이지 않는다.
useEffect(() => {
setClientExampleState(recoilExampleState);
}, [recoilExampleState]);
그렇다면 색깔로 칠한 변수는 언제 쓰냐? 초록색은 값을 불러올 때, 주황색은 값을 설정할 때 사용한다. 다시 말해 일반적인 state 사용할 때 쓰는 컨벤션과 동일하게 맞춘 것이다. 바로 아래 코드처럼 사용하면 된다.
<div>{exampleState}</div>
<button onClick={() => { setExampleState("눌렀다!"); }}>눌러보세요</button>
구조를 다시 한 번 살펴보자.
실제 화면에 표시되는 exampleState는 useState로 선언되어 일단 기본으로 빈 값이 들어간다. 서버에서도 빈 값으로 렌더링하고, Hydration 과정에서도 빈 값이 유지된다. 그 이후에 useEffect가 실행되면서 Recoil State로부터 값을 불러오고, 불러온 값을 exampleState에 넣어주게 된다.
만일 state 변경이 발생하면 setExampleState를 호출하는데, setExampleState는 Recoil의 상태를 변경하고, Recoil의 상태가 변경되면 useEffect가 실행되면서 화면에 표시되는 exampleState도 변경되게 된다.
확실히 방식이 다소 복잡하다. 다만 기본적으로 상태 관리 툴들은 클라이언트 사이드에서의 상태관리를 염두에 뒀기 때문에 서버가 함께 돌아가는 환경에서는 복잡성이 높아질 수밖에 없는 것 같다.
라이브러리 공식 문서에서 설명하는 SSR 대응법
2023년 5월에 README에 SSR Section이 추가되었다. 위에서 사용한 방법은 문서 업데이트 이전에 작업했던거라 컴포넌트 내에서 직접 state들을 관리해줬는데, 내용을 보니 문서대로 Custom Hook을 만드는 방법도 좋아 보인다. 본인의 상황에 맞는 적절한 방법을 선택하면 될 것 같다.
https://github.com/polemius/recoil-persist#server-side-rendering
'Frontend' 카테고리의 다른 글
Next.js SSR 환경에서 Mantine 사용하기 (0) | 2023.07.05 |
---|---|
Cloudflare Pages는 게임 체인져가 될 수 있을까? (0) | 2022.06.18 |
[해결] 2022-04-03 CRA로 Chakra UI 매뉴얼 세팅했을 때 오류 발생하는 이슈 (0) | 2022.04.03 |
Chakra UI 간단 사용기 (0) | 2022.03.09 |