React에서 상태(State)란, 컴포넌트 내에 관리되는 변수 즉, 변하는 데이터를 의미한다. 서비스의 규모가 커질수록 관리해야 할 상태(State)가 많아지고 Prop Drilling이라는 현상이 발생된다. Prop Drilling이란, 하위 컴포넌트로 데이터를 전달하는 과정에서 컴포넌트의 트리 구조 상 중간 과정에 있는 컴포넌트를 거쳐 프로퍼티를 내려주는 것을 의미한다. 중간 과정에 있는 컴포넌트는 전달을 위해 불필요하게 복잡성을 증대시키거나, 프로퍼티 누락 및 변경 복잡성 등 불편함을 야기한다. 이를 체계적이고 효율적으로 관리하기 위해 상태 관리 라이브러리를 사용한다.
Redux, MobX, Zustand, Recoil, Jotai 등 다양한 라이브러리가 존재하는데 상태 관리 패턴은 크게 3가지로 존재한다.
- Flux
- Store라는 상태 저장소를 기반으로 Action 타입을 Reducer에 전달하면 해당 타입에 맞는 동작에 따라 상태값을 갱신하는 방식. 컴포넌트는 Selector를 사용해 Store에서 하향식(Top-down)으로 필요한 상태값을 구독하는 형태.
- Redux, Zustand
- Proxy
- 전체 상태를 모아놓고 액세스를 제공하며, 컴포넌트에서 사용되는 일부 상태를 자동으로 감지하고 업데이트를 인지하는 방식.
- MobX, Valtio
- Atomic
- 리액트의 State와 비슷하게 컴포넌트 트리 안에 상태들이 존재하며 이들이 상향식(Bottom-up)으로 수집 및 공유되는 방식. 각 상태는 atom이라고 불리는 객체에서 설정하며 값의 참조와 조작은 React.useState와 유사하게 [state, setState] 튜플로 수행함.
- Recoil, Jotai
이중에 우리 팀은 프로젝트를 앞두고 Recoil을 선택했으나, 최근 meta 내부 해당 부서 해고 소식 및 업데이트 중단 이슈를 이유로 Recoil과 같은 패턴을 사용하는 Jotai를 채택하기로 결정했다. Jotai에 대해 알아보자.
Jotai
Jotai는 Daishi Kato라는 일본 개발자분이 개발한 라이브러리로, 글로벌 리액트 상태 관리를 아토믹 패턴 방식으로 접근한다.
Atom의 결합을 통해 상태를 빌드하고, atom 종속성에 따라 렌더링이 자동으로 최적화 되는 특징을 갖고 있다. 이를 통해 리액트 컨텍스트의 리렌더링 이슈를 해결하고, 메모이제이션할 필요성을 없애주는 등 문제를 해결해준다.
설치
# npm
npm i jotai
# yarn
yarn add jotai
# pnpm
pnpm add jotai
리액트 프로젝트에 명령어로 jotai를 설치해주고, 사용 환경에 따라 아래와 같이 설정을 마치면 설치 완료.
// Next.js (SWC)
# npm
npm install --save-dev @swc-jotai/react-refresh
# next.config.js
experimental: {
swcPlugins: [['@swc-jotai/react-refresh', {}]],
}
// Next.js (Babel)
# .babelrc
{
"presets": ["next/babel"],
"plugins": ["jotai/babel/plugin-react-refresh"]
}
atom 생성하기
atom은 작은 상태 조각으로 데이터를 지니며, Primitive atom과 Derived atom 두 가지로 나뉜다.
Primitive Atom : booleans, numbers, strings, objects, arrays, sets, maps, ... 모든 타입으로 생성이 가능하다.
import { atom } from 'jotai'
const countAtom = atom(0)
const countryAtom = atom('Japan')
const citiesAtom = atom(['Tokyo', 'Kyoto', 'Osaka'])
const animeAtom = atom([
{
title: 'Ghost in the Shell',
year: 1995,
watched: true
},
{
title: 'Serial Experiments Lain',
year: 1998,
watched: false
}
])
Derived Atom : 파생된 atom은 자신의 값을 반환하기 전에 다른 atom으로부터 읽을 수 있다.
const progressAtom = atom((get) => {
const anime = get(animeAtom)
return anime.filter((item) => item.watched).length / anime.length
})
atom 사용하기
생성 완료했다면 이제 리액트 컴포넌트 내에서 atom을 사용하여 상태를 읽거나 쓸 수 있다.
동일한 컴포넌트 내에서 사용
동일한 컴포넌트 내에서 아톰을 읽거나 쓰는 경우, 단순화를 위해 useAtom 훅을 함께 사용할 수 있다.
import { useAtom } from 'jotai'
const AnimeApp = () => {
const [anime, setAnime] = useAtom(animeAtom)
return (
<>
<ul>
{anime.map((item) => (
<li key={item.title}>{item.title}</li>
))}
</ul>
<button onClick={() => {
setAnime((anime) => [
...anime,
{
title: 'Cowboy Bebop',
year: 1998,
watched: false
}
])
}}>
Add Cowboy Bebop
</button>
<>
)
}
별개의 컴포넌트에서 사용
atom 값을 읽기만 하거나 쓰기만 할 경우, 리렌더링을 최적화하기 위해 useAtomValue와 useSetAtom 을 사용하면 된다.
import { useAtomValue, useSetAtom } from 'jotai'
const AnimeList = () => {
const anime = useAtomValue(animeAtom)
return (
<ul>
{anime.map((item) => (
<li key={item.title}>{item.title}</li>
))}
</ul>
)
}
const AddAnime = () => {
const setAnime = useSetAtom(animeAtom)
return (
<button onClick={() => {
setAnime((anime) => [
...anime,
{
title: 'Cowboy Bebop',
year: 1998,
watched: false
}
])
}}>
Add Cowboy Bebop
</button>
)
}
const ProgressTracker = () => {
const progress = useAtomValue(progressAtom)
return (
<div>{Math.trunc(progress * 100)}% watched</div>
)
}
const AnimeApp = () => {
return (
<>
<AnimeList />
<AddAnime />
<ProgressTracker />
</>
)
}
Server Side Rendering
Next.js나 개츠비 같은 프레임워크로 서버 사이드 렌더링을 하는 경우, root에서 하나 이상의 Provider 컴포넌트를 사용해야 한다.
import { Provider } from 'jotai'
// Placement is framework-specific (see below)
<Provider>
{...}
</Provider>
Next.js (app directory)
별도의 클라이언트 컴포넌트에서 Provider를 생성한다. 그 다음 Provider를 루트 layout.js 서버 컴포넌트로 가져온다.
// providers.js (app directory)
'use client'
import { Provider } from 'jotai'
export default function Providers({ children }) {
return (
<Provider>
{children}
</Provider>
)
}
// layout.js (app directory)
import Providers from './providers'
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<Providers>
{children}
</Providers>
</body>
</html>
)
}
Next.js (pages directory)
Provider를 _app.js 에 생성한다.
// _app.js (pages directory)
import { Provider } from 'jotai'
export default function App({ Component, pageProps }) {
return (
<Provider>
<Component {...pageProps} />
</Provider>
)
}
참고
공식 문서 / 심플한 tutorial을 통해 직접 개념을 이해하기 좋다.
Jotai tutorial
tutorial.jotai.org
상품 리스트 노출 상태 관리 예시와 함께 이해하기 쉬운 영상
'Today I Learned' 카테고리의 다른 글
두 번째 팀 프로젝트 회고 (0) | 2024.01.30 |
---|---|
리액트 쿼리(React Query) (0) | 2024.01.30 |
CORS 에러 (0) | 2023.12.12 |
Next.js의 서버 사이드 렌더링(SSR) (0) | 2023.11.29 |
TypeScript의 동작 원리 (1) | 2023.11.26 |