📑 목차
- 현대 React 애플리케이션의 상태 관리, 무엇을 선택해야 할까?
- Redux Toolkit: Redux의 현대적인 진화
- RTK의 주요 특징과 장점
- RTK의 단점
- Zustand: 간결함과 성능의 조화
- Zustand의 주요 특징과 장점
- Zustand의 단점
- Jotai: 아톰 기반의 미니멀리스트 접근
- Jotai의 주요 특징과 장점
- Jotai의 단점
- Recoil: React스러운 상태 관리의 재해석
- Recoil의 주요 특징과 장점
- Recoil의 단점
- 주요 라이브러리 심층 비교: 어떤 상황에 어떤 라이브러리가 적합할까?
- 각 라이브러리의 활용 시나리오
- 결론 및 최종 선택 가이드
Image by Olga_Fil on Pixabay
현대 React 애플리케이션의 상태 관리, 무엇을 선택해야 할까?
React 애플리케이션의 규모가 커지고 복잡해질수록, 상태 관리는 개발의 핵심 과제로 떠오릅니다. 컴포넌트 간의 데이터 공유, 비동기 데이터 처리, 전역 상태의 일관성 유지 등 다양한 요구사항을 충족시키기 위해 우리는 여러 상태 관리 라이브러리 중 하나를 선택해야 합니다. 선택지는 다양하며, 각각의 라이브러리는 고유한 철학과 장단점을 가지고 있습니다.
오랫동안 React 생태계의 표준처럼 여겨졌던 Redux는 강력한 기능과 거대한 생태계를 자랑했지만, 상당한 양의 보일러플레이트 코드와 높은 학습 곡선으로 인해 많은 개발자에게 부담으로 작용했습니다. 그러나 Redux Toolkit의 등장으로 이러한 단점은 상당 부분 해소되었습니다. 이와 더불어, Redux의 복잡성에 대한 대안으로 등장한 Zustand, Jotai, 그리고 Facebook에서 개발한 Recoil과 같은 경량의 혁신적인 라이브러리들이 주목받고 있습니다.
이 글에서는 현대 React 애플리케이션의 상태 관리를 위한 네 가지 주요 라이브러리인 Redux Toolkit, Zustand, Jotai, Recoil을 심층적으로 비교 분석합니다. 각 라이브러리의 핵심 특징, 장단점, 그리고 실질적인 코드 예시를 통해 여러분의 프로젝트에 가장 적합한 선택을 돕는 가이드가 되고자 합니다.
Redux Toolkit: Redux의 현대적인 진화
Redux Toolkit (RTK)은 기존 Redux의 단점들을 보완하고 개발 편의성을 극대화하기 위해 공식적으로 권장되는 도구 세트입니다. Redux의 강력함은 그대로 유지하면서도, 복잡한 설정과 보일러플레이트를 줄여 개발자가 핵심 로직에 집중할 수 있도록 돕습니다.
RTK의 주요 특징과 장점
- 보일러플레이트 감소:
createSlice를 통해 리듀서, 액션 타입, 액션 생성자를 한 번에 정의할 수 있어 코드 양이 크게 줄어듭니다. Immer 라이브러리가 내장되어 있어 불변성 관리를 더욱 쉽게 할 수 있습니다. - 쉬운 설정:
configureStore함수는 Redux DevTools, Redux Thunk 등 권장 미들웨어를 기본적으로 포함하여 스토어 설정을 간소화합니다. - 강력한 비동기 처리:
createAsyncThunk는 비동기 액션을 처리하는 표준화된 방법을 제공하여 로딩, 성공, 실패 상태를 효과적으로 관리할 수 있습니다. - 데이터 캐싱 및 패칭: RTK Query는 서버 상태 관리를 위한 강력한 도구로, 캐싱, 데이터 재요청, 로딩 상태 관리 등을 자동화하여 클라이언트와 서버 간의 데이터 동기화를 효율적으로 처리합니다. 이는 React Query나 SWR과 유사한 기능을 제공하며, Redux 생태계와 완벽하게 통합됩니다.
- 성숙한 생태계와 커뮤니티: Redux는 오랜 기간 사용되어 온 만큼 방대한 자료, 문서, 커뮤니티 지원을 받을 수 있습니다. RTK는 이러한 Redux의 장점을 그대로 물려받습니다.
RTK의 단점
- 여전히 존재하는 학습 곡선: 기존 Redux에 비하면 훨씬 쉽지만, 여전히 액션, 리듀서, 스토어, 미들웨어 등 Redux의 개념을 이해해야 합니다. 경량 라이브러리에 비하면 여전히 복잡하게 느껴질 수 있습니다.
- 상대적으로 큰 번들 크기: Redux 자체의 기능과 RTK의 추가 도구들로 인해 번들 크기가 다른 경량 라이브러리에 비해 클 수 있습니다.
- 엄격한 구조: RTK는 Redux의 철학을 따르므로, 특정 구조와 패턴을 강제하는 경향이 있습니다. 이는 대규모 팀 프로젝트에서는 장점이 될 수 있지만, 소규모 프로젝트나 자유로운 구조를 선호하는 개발자에게는 제약이 될 수 있습니다.
// counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface CounterState {
value: number;
}
const initialState: CounterState = {
value: 0,
};
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += 1; // Immer 덕분에 불변성 걱정 없이 직접 수정 가능
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
// store.ts
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
위 예시에서 createSlice 하나로 액션 타입, 액션 생성자, 리듀서가 모두 정의되며, Immer 덕분에 불변성 로직이 간결해집니다. 이는 기존 Redux 대비 약 50% 이상의 코드량 감소 효과를 가져올 수 있습니다.
Zustand: 간결함과 성능의 조화
Zustand는 React 훅 기반의 상태 관리 라이브러리로, 매우 작고 빠르며, 간결한 API를 제공합니다. Redux의 개념 없이도 쉽게 전역 상태를 관리할 수 있도록 설계되었습니다.
Zustand의 주요 특징과 장점
- 극도로 간결한 API: 스토어를 생성하고 사용하는 과정이 매우 직관적입니다. 프로바이더(Provider) 컴포넌트가 필요 없으며, 어디서든 훅처럼 스토어에 접근할 수 있습니다.
- 작은 번들 크기: 약 1KB 미만의 매우 작은 번들 크기를 자랑하여, 성능에 민감한 애플리케이션에 적합합니다.
- 뛰어난 성능: 오직 구독하는 컴포넌트만 리렌더링되도록 최적화되어 있습니다. 상태의 특정 부분만 선택하여 구독할 수 있어 불필요한 리렌더링을 방지합니다.
- 유연한 비동기 처리: 별도의 미들웨어 없이 스토어 내부에서
async/await를 사용하여 비동기 로직을 쉽게 처리할 수 있습니다. - Typescript 친화적: Typescript와의 통합이 매우 용이하여 타입 안정성을 확보하기 좋습니다.
Zustand의 단점
- 덜 정형화된 구조: Redux와 같이 엄격한 구조를 강제하지 않기 때문에, 대규모 프로젝트에서 일관된 상태 관리 패턴을 유지하기 위해 팀 내에서 자체적인 규칙을 정해야 할 수 있습니다.
- 제한적인 개발 도구: Redux DevTools를 연동할 수는 있지만, Redux만큼 풍부하고 상세한 디버깅 기능을 기본적으로 제공하지는 않습니다.
- 미들웨어 기능의 직접 구현: 로깅이나 특정 액션에 대한 반응과 같은 미들웨어 기능을 구현하려면 직접 작성하거나 외부 라이브러리를 활용해야 합니다.
// useCounterStore.ts
import { create } from 'zustand';
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
incrementByAmount: (amount: number) => void;
}
const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
incrementByAmount: (amount) => set((state) => ({ count: state.count + amount })),
}));
export default useCounterStore;
// CounterComponent.tsx
import useCounterStore from './useCounterStore';
function CounterComponent() {
const { count, increment, decrement } = useCounterStore(
(state) => ({
count: state.count,
increment: state.increment,
decrement: state.decrement,
}),
(oldState, newState) => oldState.count === newState.count // 선택적 얕은 비교
);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
create 함수를 사용하여 스토어를 정의하고, useCounterStore 훅을 통해 상태와 액션을 직접 가져와 사용할 수 있습니다. 이는 기존 Redux의 설정 과정 대비 80% 이상의 코드 간소화 효과를 가져옵니다.
Image by C1ri on Pixabay
Jotai: 아톰 기반의 미니멀리스트 접근
Jotai는 React의 useState와 useCallback처럼 작동하는 아톰(Atom) 기반의 상태 관리 라이브러리입니다. Recoil과 유사한 아톰 모델을 사용하지만, 더욱 작고 간결하며, 더 적은 개념을 가지고 있습니다.
Jotai의 주요 특징과 장점
- 아톰 기반의 미니멀리즘: 상태의 최소 단위인 아톰을 정의하고, 이 아톰들을 조합하여 복잡한 상태를 만듭니다. 아톰은 React의 로컬 상태처럼 작동하여 직관적입니다.
- 뛰어난 성능과 세밀한 렌더링 최적화: 오직 구독하는 아톰이 변경되었을 때만 해당 아톰을 사용하는 컴포넌트가 리렌더링됩니다. 이는 불필요한 리렌더링을 최소화하여 애플리케이션 성능을 극대화합니다.
- 작은 번들 크기: Recoil보다 훨씬 작은 번들 크기를 자랑하며, Zustand와 유사하게 매우 경량입니다.
- 쉬운 학습 곡선: React 훅에 익숙하다면 빠르게 적응할 수 있습니다.
useState와useEffect의 확장판처럼 느껴집니다. - 유연한 비동기 처리: 아톰 내부에서 비동기 로직을 처리하거나, 비동기 데이터를 가져오는 아톰을 정의할 수 있습니다. React의
Suspense와도 잘 통합됩니다.
Jotai의 단점
- 낯선 아톰 개념: Redux나 Context API에 익숙한 개발자에게는 아톰이라는 새로운 개념이 다소 낯설게 느껴질 수 있습니다.
- 상대적으로 작은 생태계: Redux나 Recoil에 비해 커뮤니티나 자료가 적을 수 있습니다.
- 디버깅의 복잡성: 복잡한 아톰 의존성 그래프에서 문제가 발생했을 때 디버깅이 어려울 수 있습니다.
// atoms.ts
import { atom } from 'jotai';
// 기본 아톰
export const countAtom = atom(0);
// 파생된 아톰 (derived atom)
export const doubledCountAtom = atom((get) => get(countAtom) * 2);
// 비동기 아톰 예시 (데이터 패칭)
export const fetchDataAtom = atom(async () => {
const response = await fetch('/api/data');
return response.json();
});
// useData.ts (데이터 패칭 예시)
// import { useAtom } from 'jotai';
// import { fetchDataAtom } from './atoms';
//
// function DataDisplay() {
// const [data] = useAtom(fetchDataAtom); // Suspense 사용
//
// return <div>{JSON.stringify(data)}</div>;
// }
// CounterComponent.tsx
import { useAtom } from 'jotai';
import { countAtom, doubledCountAtom } from './atoms';
function CounterComponent() {
const [count, setCount] = useAtom(countAtom);
const [doubledCount] = useAtom(doubledCountAtom); // 읽기 전용
return (
<div>
<p>Count: {count}</p>
<p>Doubled Count: {doubledCount}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
<button onClick={() => setCount((c) => c - 1)}>Decrement</button>
</div>
);
}
atom 함수로 상태 단위를 정의하고, useAtom 훅으로 이를 사용합니다. 파생된 아톰을 통해 다른 아톰의 값을 기반으로 새로운 값을 계산할 수 있으며, 이는 Redux의 셀렉터와 유사한 역할을 합니다. 상태 업데이트 시, 해당 아톰을 구독하는 컴포넌트만 리렌더링되므로 매우 효율적입니다.
Recoil: React스러운 상태 관리의 재해석
Recoil은 Facebook에서 개발한 React 상태 관리 라이브러리로, React의 동시성 모드(Concurrent Mode)를 염두에 두고 설계되었습니다. 아톰(Atom)과 셀렉터(Selector)라는 개념을 통해 React 컴포넌트 로컬 상태처럼 자연스럽게 전역 상태를 관리할 수 있도록 돕습니다.
Recoil의 주요 특징과 장점
- React스러운 접근 방식:
useState와 유사한 방식으로 전역 상태를 사용할 수 있어 React 개발자에게 매우 친숙합니다. - 아톰과 셀렉터: 아톰은 React 로컬 상태와 유사하게 개별적으로 업데이트 가능한 상태의 단위이며, 셀렉터는 아톰 또는 다른 셀렉터의 값을 기반으로 계산된 파생 상태를 정의합니다. 이는 React 컴포넌트의 계산 로직과 유사하게 동작합니다.
- 최적화된 렌더링: 아톰이 변경되면 해당 아톰을 구독하는 컴포넌트만 효율적으로 리렌더링됩니다. 셀렉터는 필요한 경우에만 재계산되어 성능 이점을 제공합니다.
- 비동기 데이터 처리: 셀렉터 내에서 비동기 작업을 쉽게 처리할 수 있으며, React의
Suspense와Error Boundary와 완벽하게 통합됩니다. - 강력한 개발 도구: Recoil DevTools는 상태 변화 추적, 스냅샷, 타임 트래블 디버깅 등 강력한 개발자 경험을 제공합니다.
- Facebook의 지원: Facebook에서 개발하고 지원한다는 점은 장기적인 안정성과 발전 가능성에 대한 신뢰를 줍니다.
Recoil의 단점
- 상대적으로 큰 번들 크기: Jotai나 Zustand에 비해서는 번들 크기가 큰 편입니다.
- 학습 곡선: 아톰과 셀렉터 개념 자체는 React스럽지만, 이들 간의 의존성 관리 및 복잡한 시나리오에서의 활용은 일정 수준의 학습이 필요합니다.
- 새로운 개념 도입: 아톰의 키(key) 관리 등 새로운 개념과 패턴을 익혀야 합니다.
// atoms.ts
import { atom, selector } from 'recoil';
// 기본 아톰
export const countState = atom({
key: 'countState', // 전역적으로 고유한 키
default: 0,
});
// 파생된 상태 (셀렉터)
export const doubledCountState = selector({
key: 'doubledCountState',
get: ({ get }) => {
const count = get(countState);
return count * 2;
},
});
// 비동기 셀렉터 예시 (데이터 패칭)
export const userDataState = selector({
key: 'userDataState',
get: async () => {
const response = await fetch('/api/user');
return response.json();
},
});
// UserProfile.tsx (데이터 패칭 예시)
// import { useRecoilValue } from 'recoil';
// import { userDataState } from './atoms';
// import React, { Suspense } from 'react';
//
// function UserProfile() {
// const userData = useRecoilValue(userDataState);
// return (
// <div>
// <h3>User Profile</h3>
// <p>Name: {userData.name}</p>
// <p>Email: {userData.email}</p>
// </div>
// );
// }
//
// function App() {
// return (
// <Suspense fallback="<div>Loading user data...</div>">
// <UserProfile />
// </Suspense>
// );
// }
// CounterComponent.tsx
import { useRecoilState, useRecoilValue } from 'recoil';
import { countState, doubledCountState } from './atoms';
function CounterComponent() {
const [count, setCount] = useRecoilState(countState);
const doubledCount = useRecoilValue(doubledCountState); // 읽기 전용
return (
<div>
<p>Count: {count}</p>
<p>Doubled Count: {doubledCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}
atom으로 기본 상태를 정의하고, selector로 파생된 상태를 정의합니다. useRecoilState 훅은 useState처럼 상태와 상태 업데이트 함수를 반환하며, useRecoilValue는 읽기 전용 상태를 가져옵니다. 비동기 셀렉터는 Suspense와 함께 사용하여 로딩 상태를 선언적으로 처리할 수 있습니다.
Image by WikiImages on Pixabay
주요 라이브러리 심층 비교: 어떤 상황에 어떤 라이브러리가 적합할까?
네 가지 라이브러리는 각각 다른 강점과 약점을 가지고 있습니다. 다음 표를 통해 주요 특징들을 한눈에 비교하고, 각 라이브러리가 어떤 프로젝트에 적합한지 살펴보겠습니다.
| 특징 | Redux Toolkit | Zustand | Jotai | Recoil |
|---|---|---|---|---|
| 핵심 철학 | 단일 스토어, 예측 가능한 상태 변경, 불변성 | 간결한 훅 기반, 최소한의 보일러플레이트 | 아톰 기반, React useState 확장, 극도로 미니멀 |
아톰 및 셀렉터 기반, React 동시성 모드 호환 |
| 학습 곡선 | 중 (기존 Redux 대비 낮음) | 하 (매우 낮음) | 하~중 (아톰 개념 익숙해지면 쉬움) | 중 (아톰/셀렉터 개념 익숙해져야 함) |
| 보일러플레이트 | 중 (기존 Redux 대비 대폭 감소) | 매우 낮음 | 매우 낮음 | 낮음 |
| 번들 크기 | 대 (Redux + RTK Query 포함 시) | 소 (~1KB) | 소 (~2KB) | 중 (~20KB) |
| 성능 및 렌더링 | 최적화 필요 (useSelector 메모이제이션) |
매우 빠름 (선택적 구독, 얕은 비교) | 매우 빠름 (아톰 단위 구독, 세밀한 렌더링) | 매우 빠름 (아톰 단위 구독, 셀렉터 최적화) |
| 비동기 처리 | createAsyncThunk, RTK Query |
스토어 내부 async/await |
비동기 아톰, Suspense 통합 |
비동기 셀렉터, Suspense 통합 |
| 개발 도구 | Redux DevTools (매우 강력) | Redux DevTools 연동 가능 (기본 제공X) | Redux DevTools 연동 가능 (기본 제공X) | Recoil DevTools (강력) |
| 주요 특징 | 공식 권장, 강력한 데이터 패칭 (RTK Query), 엔터프라이즈급 | 프로바이더 불필요, 훅 기반, 매우 유연 | 프리미티브 기반, 가벼움, Recoil 대체재 | Facebook 개발, React 동시성 모드 최적화, React 친화적 |
| 적합한 프로젝트 | 대규모, 복잡한 비즈니스 로직, 팀 규모가 큰 엔터프라이즈 앱 | 중소규모, 빠른 프로토타이핑, 성능 민감한 앱 | 중소규모, 매우 높은 성능 요구, React 훅에 익숙한 개발자 | 중대규모, React 동시성 모드 활용, Facebook 생태계 선호 |
각 라이브러리의 활용 시나리오
- Redux Toolkit:
- 대규모 엔터프라이즈 애플리케이션: 예측 가능한 상태 관리와 강력한 개발 도구가 필수적인 경우.
- 복잡한 비즈니스 로직과 비동기 데이터 흐름:
createAsyncThunk와 RTK Query를 통해 서버 상태까지 효율적으로 관리할 수 있습니다. - 기존 Redux 프로젝트 마이그레이션: 점진적으로 RTK로 전환하여 개발 생산성을 높일 수 있습니다.
- 엄격한 코드 구조와 팀 협업: 일관된 패턴을 제공하여 대규모 팀의 협업 효율을 높입니다.
- Zustand:
- 중소규모 애플리케이션 또는 마이크로 프론트엔드: 가볍고 빠르게 상태 관리를 구현해야 할 때.
- 빠른 프로토타이핑: 적은 보일러플레이트로 아이디어를 빠르게 구현하고 검증해야 할 때.
- 성능에 민감한 애플리케이션: 번들 크기와 렌더링 성능이 중요한 경우.
- React 훅에 익숙한 개발자: 기존 훅 사용 경험을 살려 직관적으로 상태를 관리하고 싶을 때.
- Jotai:
- 컴포넌트 단위의 세밀한 상태 관리: 전역 상태를 작은 아톰 단위로 나누어 관리하고, 불필요한 리렌더링을 최소화해야 할 때.
- Recoil의 대안을 찾을 때: Recoil과 유사한 아톰 모델을 선호하지만, 더 작은 번들 크기와 더 적은 개념을 원할 때.
- React의
useState를 확장하는 느낌으로 상태를 관리하고 싶을 때: 익숙한 React 훅의 패러다임을 전역 상태에 적용하고자 할 때.
- Recoil:
- React 동시성 모드를 적극적으로 활용할 애플리케이션: React의 미래 지향적인 기능들과 함께 상태를 관리하고자 할 때.
- 아톰-셀렉터 패턴을 선호할 때: 파생된 상태를 효율적으로 관리하고 싶을 때.
- Facebook 생태계에 대한 신뢰: Facebook의 지원을 받는 라이브러리를 선호할 때.
- Suspense 기반의 데이터 패칭: 비동기 데이터 처리를 Suspense와 함께 선언적으로 관리하고자 할 때.
결론 및 최종 선택 가이드
현대 React 상태 관리 라이브러리들은 각기 다른 장점을 가지고 있으며, 어떤 라이브러리가 '최고'라고 단정하기는 어렵습니다. 핵심은 여러분의 프로젝트 요구사항, 팀의 숙련도, 그리고 선호하는 개발 패러다임에 가장 잘 맞는 도구를 선택하는 것입니다.
- 이미 Redux에 익숙하거나 대규모 엔터프라이즈 프로젝트를 진행한다면: Redux Toolkit은 강력한 기능, 풍부한 생태계, 그리고 Redux의 명확한 구조를 유지하면서도 개발 생산성을 크게 향상시킬 수 있는 최적의 선택입니다. 특히 RTK Query는 서버 상태 관리의 복잡성을 크게 줄여줍니다.
- 간결함, 빠른 개발, 그리고 뛰어난 성능이 최우선이라면: Zustand는 가장 적은 보일러플레이트와 작은 번들 크기로 즉각적인 효과를 볼 수 있습니다. 중소규모 프로젝트나 성능에 민감한 컴포넌트 단위 상태 관리에 매우 적합합니다.
- React 훅의 확장 개념으로 세밀한 상태 관리를 원하고, Recoil보다 가벼운 대안을 찾는다면: Jotai는 아톰 기반의 미니멀리즘으로 최고의 렌더링 성능과 유연성을 제공합니다. React의 기본 상태 관리 방식에 가깝게 느껴질 것입니다.
- React의 미래인 동시성 모드와 통합된 강력한 아톰 기반 솔루션을 원한다면: Recoil은 Facebook의 지원을 받으며, React스러운 접근 방식과 강력한 개발 도구로 복잡한 파생 상태 관리까지 효율적으로 처리할 수 있습니다.
궁극적으로는 한두 가지 라이브러리를 직접 사용해보면서 각자의 장단점을 체감하고, 프로젝트의 특성을 고려하여 현명한 결정을 내리는 것이 중요합니다. 이 글이 여러분의 React 상태 관리 여정에 도움이 되기를 바랍니다.
여러분은 어떤 상태 관리 라이브러리를 선호하시나요? 혹은 특정 상황에서 어떤 라이브러리가 가장 효과적이었다고 생각하시나요? 댓글로 여러분의 경험과 의견을 공유해주세요!