리액트를 위한 가벼운 상태 관리 라이브러리
2024-09-01
347
9분
soaple
안녕하세요, 소플입니다.
우리는 웹 애플리케이션을 개발할 때 어떤 상태 관리 라이브러리를 사용할지 고민하게 됩니다.
워낙 상태 관리 라이브러리의 종류가 다양하기 때문이죠.
이번 매거진에서는 리액트를 위한 가벼운 상태 관리 라이브러리 중 하나인 Jotai에 대해 알아보겠습니다.
Jotai는 아래와 같이 한 문장으로 자신을 소개하고 있습니다.
Primitive and flexible state management for React
이 문장을 번역해보면 "리액트를 위한 단순하고 유연한 상태 관리" 가 됩니다.
즉, Jotai를 한 문장으로 정의하면 "리액트를 위한 가벼운 상태 관리 라이브러리" 라고 정의할 수 있습니다.
아래 화면은 Jotai의 공식 웹사이트 첫 화면입니다.
한 가지 재밌는 점은 Jotai라는 단어는 일본어로 "상태(状態)" 라는 뜻을 갖고 있다는 점입니다.
또 다른 상태 관리 라이브러리인 Zustand가 독일어로 "상태"라는 뜻을 갖고 있다는 것과 일맥상통하는 부분이죠.
'왜 이렇게 이름을 짓는 방식이 비슷할까?' 라고 의문을 가지게 될 수 있는데, 여기서 알 수 있는 한 가지 놀라운 사실이 있습니다.
그것은 바로 Jotai와 Zustand, 그리고 Valtio라는 상태 관리 라이브러리까지 모두 한 사람이 개발했기 때문입니다.
그 사람은 바로 Daishi Kato라는 일본인입니다.
한 사람이 유명한 상태 관리 라이브러리들을 3개씩이나 개발했다는 사실이 놀랍고 굉장히 대단한 것 같습니다.
그렇다면 상태 관리 라이브러리의 트렌드 관점에서 Jotai는 어떤 포지션일까요?
아래 화면은 npm trends에서 대표적인 상태 관리 라이브러리들을 비교해본 것입니다.
가장 상위에 범접할 수 없는 Redux가 위치하고 있고,
그 아래로 순서대로 Zustand, Jotai, Recoil, Valtio가 등장하고 있습니다.
현재 시점에서는 Jotai가 3위라고 할 수 있는데, 1위인 Redux와 2위인 Zustand와의 격차는 상당히 큰 편입니다.
단순히 이러한 트렌드를 따라간다면 Jotai보다는 Redux나 Zustand를 사용하는 것이 맞을 겁니다.
하지만 각 상태 관리 라이브러리마다 특징이 있고 사용하기 적합한 환경이 있기 때문에,
개발하려는 애플리케이션에 따라서 잘 맞는 상태 관리 라이브러리를 선택하는 것이 중요합니다.
그리고 그러한 관점에서 생각해볼 때, Jotai는 규모가 작고 단순한 애플리케이션을 위해 사용하기 적합하다고 할 수 있습니다.
결과적으로 Jotai를 사용하면 보다 간단하고 효율적으로 리액트에서 상태를 관리하고 공유할 수 있습니다.
지금부터 Jotai의 주요 특징에 대해 살펴보겠습니다.
Jotai에서는 원자(Atom) 라는 개념을 기반으로 하여 상태를 관리합니다.
Atom은 상태의 최소 단위로 독립적인 상태 단위를 나타내며,
Atom을 사용해서 상태 간의 의존성과 업데이트 흐름을 명확하게 표현할 수 있습니다.
Jotai의 주요 특징을 정리해보면 아래와 같습니다.
아래 코드는 Jotai를 사용하는 예시 코드입니다.
먼저 Atom을 생성하고, 생성한 Atom을 사용하여 count
값을 관리하는 단순한 코드입니다.
import React from 'react';
import { atom, useAtom } from 'jotai';
// 1. Atom 생성
const countAtom = atom(0);
function Counter() {
// 2. Atom 사용
const [count, setCount] = useAtom(countAtom);
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount((c) => c + 1)}>Increase</button>
<button onClick={() => setCount((c) => c - 1)}>Decrease</button>
</div>
);
}
export default Counter;
그렇다면 Jotai의 장점과 단점으로는 어떤 것들이 있을까요?
아래는 Jotai의 장점과 단점을 정리한 것입니다.
Jotai는 가볍고 큰 러닝커브 없이 사용할 수 있다는 것이 대표적인 장점이고,
그러한 장점으로 인해 복잡한 상태 관리에는 적합하지 않다는 것이 대표적인 단점이라고 할 수 있습니다.
지금부터는 Jotai를 사용하는 방법에 대해 살펴보도록 하겠습니다.
먼저 아래와 같이 패키지 매니저를 사용해서 Jotai를 설치합니다.
# npm
npm install jotai
# yarn
yarn add jotai
# pnpm
pnpm add jotai
이후 Atom을 생성해야 하는데 가장 기초가 되는 Primitive Atom을 먼저 생성해야 합니다.
Primitive Atom은 booleans, numbers, strings, objects, arrays, sets, maps 등 어떤 타입으로든 만들 수 있습니다.
아래 코드는 다양한 타입의 Primitive Atom을 생성하는 예시 코드입니다.
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 }
]);
Primitive Atom을 생성한 이후에는 여러 개의 Primitive Atom을 조합한 Derived Atom을 생성해서 사용할 수 있습니다.
Derived Atom에서는 아래 코드와 같이 다른 Atom으로부터 값을 읽고 그 값을 사용해서 만든 값을 리턴할 수 있습니다.
const progressAtom = atom((get) => {
const anime = get(animeAtom);
return anime.filter((item) => item.watched).length / anime.length;
});
Primitive Atom과 Derived Atom 등을 생성했다면,
다음으로는 Atom으로부터 값을 읽어오거나 쓰는 식으로 사용하면 됩니다.
같은 컴포넌트에서 값을 읽어오거나 쓰기 위해서는 useAtom()
훅을 사용하면 됩니다.
아래 코드는 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>
<>
)
}
코드가 굉장히 간단한데, 우리가 평소에 사용하던 리액트의 useState()
훅처럼 사용한다고 생각하면 됩니다.
그리고 서로 다른 컴포넌트에서 Atom의 값을 읽고 쓰기 위해서는,
각각 useAtomValue()
훅과 useSetAtom()
훅을 사용하면 됩니다.
아래 코드는 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 />
</>
)
}
그렇다면 Next.js와 같이 SSR(Server Side Rendering)을 사용하는 프레임워크에서는 Jotai를 어떻게 적용해야 할까요?
먼저 App Router에서는 모든 컴포넌트가 기본적으로 서버 컴포넌트이기 때문에,
아래와 같이 별도의 클라이언트 컴포넌트로 Provider를 만들고 서버 컴포넌트에서 import
해서 사용해야 합니다.
'use client'
import { Provider } from 'jotai';
export const JotaiProvider = ({ children }) => {
return (
<Provider>
{children}
</Provider>
)
}
import { JotaiProvider } from '../components/providers';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<JotaiProvider>
{children}
</JotaiProvider>
</body>
</html>
)
}
그리고 기존의 Pages Router의 경우에는 _app.js
파일에서 Provider
로 감싸면 됩니다.
import { Provider } from 'jotai'
export default function App({ Component, pageProps }) {
return (
<Provider>
<Component {...pageProps} />
</Provider>
)
}
이렇게 하면 Next.js에서 Jotai를 사용할 수 있습니다.
지금부터는 Jotai API를 살펴보도록 하겠습니다.
먼저 가장 핵심이 되는 API로는 Atom을 생성하기 위한 atom()
함수와,
리액트 컴포넌트에서 Atom을 사용하기 위한 useAtom()
훅이 있습니다.
import { atom, useAtom } from 'jotai';
// Create your atoms and derivatives
const textAtom = atom('hello');
const uppercaseAtom = atom((get) => get(textAtom).toUpperCase());
// Use them anywhere in your app
const Input = () => {
const [text, setText] = useAtom(textAtom);
const handleChange = (e) => setText(e.target.value);
return (
<input
value={text}
onChange={handleChange}
/>
);
};
const Uppercase = () => {
const [uppercase] = useAtom(uppercaseAtom);
return <div>Uppercase: {uppercase}</div>;
};
// Now you have the components
const App = () => {
return (
<>
<Input />
<Uppercase />
</>
);
};
다음으로 유틸리티 API로는 atomWithStorage()
함수가 있습니다.
atomWithStorage()
를 사용하면 Atom의 데이터를 스토리지에 저장함으로써 페이지가 새로고침 되어도 값이 유지되도록 할 수 있습니다.
import { useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
// Set the string key and the initial value
const darkModeAtom = atomWithStorage('darkMode', false);
const Page = () => {
// Consume persisted state like any other atom
const [darkMode, setDarkMode] = useAtom(darkModeAtom);
const toggleDarkMode = () => setDarkMode(!darkMode);
return (
<>
<h1>Welcome to {darkMode ? 'dark' : 'light'} mode!</h1>
<button onClick={toggleDarkMode}>toggle theme</button>
</>
);
};
마지막으로 Jotai의 기능을 확장시켜주는 대표적인 패키지로는 jotai-immer
가 있습니다.
jotai-immer
에서 제공하는 atomWithImmer()
함수를 사용하면,
Atom의 값을 업데이트 할 때 Immutable Update를 사용할 수 있습니다.
import { useAtom } from 'jotai';
import { atomWithImmer } from 'jotai-immer';
// Create a new atom with an immer-based write function
const countAtom = atomWithImmer(0);
const Counter = () => {
const [count] = useAtom(countAtom);
return <div>count: {count}</div>;
};
const Controls = () => {
// setCount === update: (draft: Draft<Value>) => void
const [, setCount] = useAtom(countAtom);
const increment = () => setCount((c) => (c = c + 1));
return <button onClick={increment}>+1</button>;
};
그밖에도 다양한 확장 패키지들이 있는데 여기서는 따로 다루지 않도록 하겠습니다.
Jotai의 다양한 확장 패키지에 대해 관심있는 분들은 아래 링크를 참고하기 바랍니다.
이번 매거진에서는 리액트를 위한 가벼운 상태 관리 라이브러리인 Jotai에 대해서 살펴보았습니다.
그럼 저는 다음에 또 유익한 글로 찾아뵙겠습니다!
지금까지 소플이었습니다. 감사합니다 😀
지금 가입하고 프론트엔드 개발 관련 매거진을 이메일로 받아보세요!