현대 프론트엔드 애플리케이션에서의 상태 관리
Ethan Miller
Product Engineer · Leapcell

소개
끊임없이 진화하는 프론트엔드 개발 환경에서 애플리케이션 상태를 효과적으로 관리하는 것은 강력하고 확장 가능하며 유지 관리 가능한 사용자 인터페이스를 구축하는 데 매우 중요합니다. 애플리케이션의 복잡성이 증가함에 따라 데이터 일관성, 예측 가능한 업데이트 및 직관적인 개발자 경험을 보장하는 과제도 커집니다. 수년에 걸쳐 수많은 상태 관리 솔루션이 등장했으며, 각각 이러한 복잡성을 해결하기 위한 고유한 접근 방식을 제공합니다. 이 글에서는 React 생태계의 세 가지 주요 경쟁자인 Redux Toolkit, Zustand 및 Jotai를 자세히 살펴봅니다. 각 라이브러리의 고유한 패러다임을 살펴보고, 기본 메커니즘을 이해하고, 장단점을 파악하여 궁극적으로 다음 프로젝트에 가장 적합한 솔루션을 선택할 수 있는 지식을 갖추게 될 것입니다.
프론트엔드 상태 관리의 핵심 개념
각 라이브러리의 구체적인 내용을 살펴보기 전에 논의의 기초가 될 몇 가지 기본 개념에 대한 공통된 이해를 확립해 보겠습니다.
- 상태(State): 애플리케이션의 UI와 로직을 구동하는 데이터입니다. 사용자 입력, 가져온 데이터, UI 기본 설정 등을 포함할 수 있습니다.
- 상태 관리(State Management): 애플리케이션의 상태를 예측 가능하고 효율적인 방식으로 구성, 저장 및 업데이트하는 프로세스입니다.
- 중앙 집중식 vs. 분산식 상태(Centralized vs. Decentralized State):
- 중앙 집중식: 모든 애플리케이션 상태가 단일 전역 스토어에 상주하며 어디서든 액세스할 수 있습니다. 이는 종종 단일 진실 공급원을 만듭니다.
- 분산식: 상태는 다양한 컴포넌트 또는 작고 독립적인 스토어에 분산됩니다.
- 불변성(Immutability): 상태를 직접 수정하지 않는 원칙입니다. 대신 원하는 변경 사항이 포함된 새 상태 객체가 생성됩니다. 이는 예상치 못한 부작용을 방지하고 상태 변경을 추적하고 디버깅하기 쉽게 만드는 데 도움이 됩니다.
- 액션/이벤트(Actions/Events): 상태 변경 의도를 설명하는 객체 또는 함수입니다.
- 리듀서(Reducers): 현재 상태와 액션을 입력으로 받아 새 상태를 반환하는 순수 함수입니다. 이는 많은 중앙 집중식 솔루션에서 상태를 변경하는 유일한 메커니즘입니다.
- 셀렉터(Selectors): 전역 상태에서 특정 데이터 조각을 추출하는 함수로, 불필요한 리렌더링을 방지하기 위한 최적화에 자주 사용됩니다.
- 훅(Hooks): React 16.8에 도입된 훅을 통해 함수형 컴포넌트에서 상태와 부작용을 관리할 수 있습니다. 이를 통해 컴포넌트 내에서 상태 관리가 더 쉽게 액세스하고 구성할 수 있습니다.
Redux Toolkit - 포괄적인 중앙 집중식 솔루션
Redux Toolkit(RTK)은 효율적인 Redux 개발을 위한 공식적인 의견 기반 솔루션입니다. 일반적인 Redux 패턴을 단순화하고, 상용구를 줄이며, 더 나은 개발자 경험을 제공하기 위해 만들어졌습니다. RTK는 중앙 집중식 불변 상태 관리 접근 방식을 채택하여 강력한 개발 도구와 예측 가능한 상태 흐름을 제공합니다.
Redux Toolkit의 원칙:
RTK는 Redux의 핵심 원칙을 기반으로 합니다. 즉, 단일 진실 공급원(Redux 스토어), 순수 리듀서 함수를 통한 상태 변경, 무슨 일이 일어났는지 설명하는 액션입니다. RTK의 주요 기능은 다음과 같습니다.
configureStore: 합리적인 기본값으로 스토어 설정을 단순화합니다.createSlice: 특정 상태 조각에 대한 액션과 리듀서의 생성을 자동화하여 상용구를 크게 줄입니다.createAsyncThunk: API 호출과 같은 비동기 로직 처리를 간소화합니다.createSelector: 성능을 최적화하기 위해 셀렉터 함수를 메모이제이션합니다.
실제 예제: 카운터와 할 일 목록 관리.
// store.js import { configureStore, createSlice } from '@reduxjs/toolkit'; const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { increment: (state) => { state.value += 1; }, decrement: (state) => { state.value -= 1; }, incrementByAmount: (state, action) => { state.value += action.payload; }, }, }); const todosSlice = createSlice({ name: 'todos', initialState: [], reducers: { addTodo: (state, action) => { state.push({ id: Date.now(), text: action.payload, completed: false }); }, toggleTodo: (state, action) => { const todo = state.find((t) => t.id === action.payload); if (todo) { todo.completed = !todo.completed; } }, }, }); export const { increment, decrement, incrementByAmount } = counterSlice.actions; export const { addTodo, toggleTodo } = todosSlice.actions; export const store = configureStore({ reducer: { counter: counterSlice.reducer, todos: todosSlice.reducer, }, });
// Counter.js (React Component) import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { increment, decrement, incrementByAmount } from './store'; function Counter() { const count = useSelector((state) => state.counter.value); const dispatch = useDispatch(); return ( <div> <h2>Counter: {count}</h2> <button onClick={() => dispatch(increment())}>Increment</button> <button onClick={() => dispatch(decrement())}>Decrement</button> <button onClick={() => dispatch(incrementByAmount(5))}>Increment by 5</button> </div> ); } export default Counter;
적용 시나리오: RTK는 상호 작용하는 컴포넌트가 많고 예측 가능한 상태 업데이트, 디버깅 기능(예: Redux DevTools) 및 단일 진실 공급원이 필요한 대규모 복잡한 애플리케이션에 탁월합니다. 특히 유지 관리 가능성과 장기적인 확장성이 중요한 엔터프라이즈급 애플리케이션에 매우 적합합니다.
Zustand - 미니멀하고 성능이 뛰어난 접근 방식
Zustand는 상태 관리에 대한 미니멀하고 훅 기반이며 성능이 뛰어난 접근 방식으로 신선한 바람을 선사합니다. Redux와 달리 Zustand는 리듀서나 Immer와 같은 라이브러리를 통한 불변성 강제에 의존하지 않습니다(물론 함께 사용할 수 있습니다). 대신 간단한 함수형 API를 사용하여 스토어를 생성하고 상태를 직접 수정하지만, 여전히 리렌더링이 최적화되도록 보장합니다.
Zustand의 원칙:
Zustand의 철학은 단순함과 직접성에 있습니다. React 훅을 활용하여 컴포넌트를 스토어에 연결하고 핵심 API가 매우 작습니다.
create함수: 스토어를 정의하는 주요 방법입니다. 상태와 업데이트 함수를 포함하는 객체를 반환하는 함수를 인수로 받습니다.- 직접 수정: Zustand는 업데이트 함수 내에서 상태 객체를 직접 수정할 수 있도록 하여 많은 개발자에게 상태 업데이트가 더 자연스럽게 느껴지도록 합니다. 실제 상태 변경 사항을 얕게 비교하여 최적화된 리렌더링을 달성합니다.
- 메모이제이션 없는 셀렉터: 기본적으로 Zustand의 셀렉터는 명시적인 메모이제이션이 필요하지 않습니다. 선택된 상태 조각이 실제로 변경된 경우에만 컴포넌트가 리렌더링됩니다.
실제 예제: Zustand로 동일한 카운터와 할 일 목록 구축.
// useStore.js import { create } from 'zustand'; // Counter store const createCounterSlice = (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 })), }); // Todos store const createTodosSlice = (set) => ({ todos: [], addTodo: (text) => set((state) => ({ todos: [...state.todos, { id: Date.now(), text, completed: false }], })), toggleTodo: (id) => set((state) => ({ todos: state.todos.map((todo) => todo.id === id ? { ...todo, completed: !todo.completed } : todo ), })), }); // slices 결합 (선택 사항, 별도의 스토어로도 가능) export const useBoundStore = create((...a) => ({ ...createCounterSlice(...a), ...createTodosSlice(...a), })); // 더 나은 모듈화를 위해 별도의 스토어 사용 export const useCounterStore = create(createCounterSlice); export const useTodosStore = create(createTodosSlice);
// Counter.js (React Component) import React from 'react'; import { useCounterStore } from './useStore'; // 또는 useBoundStore function Counter() { const count = useCounterStore((state) => state.count); const increment = useCounterStore((state) => state.increment); const decrement = useCounterStore((state) => state.decrement); const incrementByAmount = useCounterStore((state) => state.incrementByAmount); return ( <div> <h2>Counter: {count}</h2> <button onClick={increment}>Increment</button> <button onClick={decrement}>Decrement</button> <button onClick={() => incrementByAmount(5)}>Increment by 5</button> </div> ); } export default Counter;
적용 시나리오: Zustand는 소규모에서 중규모 애플리케이션, 또는 Redux의 오버헤드와 구조화된 상용구 없이 성능이 뛰어나고 가벼운 상태 솔루션이 필요한 시나리오에 이상적입니다. 특히 몇 개의 컴포넌트에서 공유해야 하는 로컬 컴포넌트 상태, 개인 프로젝트 또는 useState에서 전역 솔루션으로 마이그레이션할 때 특히 적합합니다. 단순함 덕분에 빠른 프로토타이핑에도 훌륭합니다.
Jotai - 본질적으로 강력한 접근 방식
Jotai는 Recoil에서 영감을 받은 독특한 원자 기반 접근 방식으로 상태 관리를 합니다. 단일 전역 스토어 대신, Jotai는 "원자(atom)"라고 불리는 작고 독립적인 상태 조각을 정의할 수 있도록 합니다. 이러한 원자는 서로 결합하고 파생될 수 있어 모듈성이 뛰어나고 유연한 상태 그래프를 생성할 수 있습니다. Jotai는 전역 수준에서 React의 useState와 더 가까운 개념을 채택합니다.
Jotai의 원칙:
Jotai는 본질적으로 반응형 상태의 단위인 원자라는 개념을 중심으로 합니다.
- 원자(Atoms): 읽고 쓸 수 있는 상태의 기본 단위입니다. 원자는 어떤 값이든 보유할 수 있습니다.
- 파생 원자(Derived Atoms): 원자는 다른 원자에서 값을 파생시켜 종속성이 변경될 때 자동으로 업데이트되는 계산된 상태를 생성할 수 있습니다. 이는 복잡한 셀렉터를 만드는 데 강력합니다.
useAtomHook: React 컴포넌트 내에서 원자와 상호 작용하는 주요 훅입니다.useState와 마찬가지로 원자의 값과 설정 함수를 반환합니다.- 디자인상 분산: 상태는 단일 스토어에 수집되는 대신 수많은 작은 원자에 분산됩니다.
실제 예제: Jotai로 카운터와 할 일 목록 구현.
// atoms.js import { atom } from 'jotai'; // Counter atoms export const countAtom = atom(0); export const incrementAtom = atom( null, // setter (get, set) => set(countAtom, get(countAtom) + 1) ); export const decrementAtom = atom( null, (get, set) => set(countAtom, get(countAtom) - 1) ); export const incrementByAmountAtom = atom( null, (get, set, amount) => set(countAtom, get(countAtom) + amount) ); // Todos atoms export const todosAtom = atom([]); export const addTodoAtom = atom( null, (get, set, text) => set(todosAtom, [...get(todosAtom), { id: Date.now(), text, completed: false }]) ); export const toggleTodoAtom = atom( null, (get, set, id) => set( todosAtom, get(todosAtom).map((todo) => todo.id === id ? { ...todo, completed: !todo.completed } : todo ) ) );
// Counter.js (React Component) import React from 'react'; import { useAtom } from 'jotai'; import { countAtom, incrementAtom, decrementAtom, incrementByAmountAtom } from './atoms'; function Counter() { const [count] = useAtom(countAtom); const [, increment] = useAtom(incrementAtom); const [, decrement] = useAtom(decrementAtom); const [, incrementByAmount] = useAtom(incrementByAmountAtom); return ( <div> <h2>Counter: {count}</h2> <button onClick={increment}>Increment</button> <button onClick={decrement}>Decrement</button> <button onClick={() => incrementByAmount(5)}>Increment by 5</button> </div> ); } export default Counter;
적용 시나리오: Jotai는 매우 모듈화되고 세분화된 상태에 이점을 제공하는 애플리케이션에서 빛을 발합니다. 애플리케이션의 특정 부분에 국한되지만 필요한 경우 전역적으로 액세스할 수 있어야 하는 UI 상태에 탁월합니다. "원시적"이라는 특성은 고유한 상태 그래프 요구 사항과 매우 세밀한 수준에서 렌더링을 최적화하는 데 매우 유연합니다. 또한 useState의 단순함을 좋아하지만 전역 솔루션이 필요한 경우 또는 리듀서와 같은 의견 기반 패턴을 피하려는 프로젝트에도 좋은 선택입니다.
결론
Redux Toolkit, Zustand 및 Jotai는 각각 React에서 상태 관리에 대한 강력한 솔루션을 제공하지만, 서로 다른 요구 사항과 선호도를 충족합니다. Redux Toolkit은 명확하고 중앙 집중식 상태 흐름과 디버깅 가능성을 요구하는 대규모 애플리케이션에 이상적인 포괄적이고 의견 기반이며 고도로 구조화된 프레임워크를 제공합니다. 미니멀한 API와 직접적인 상태 조작을 특징으로 하는 Zustand는 작거나 덜 의견 기반인 접근 방식을 선호하는 사람들을 위한 성능이 뛰어나고 가벼운 대안을 제공합니다. 원자 기반의 분산 모델을 활용하는 Jotai는 매우 모듈화된 애플리케이션 및 고급 상태 파생에 탁월한 세밀한 제어와 탁월한 유연성을 제공합니다. 궁극적으로 최상의 선택은 프로젝트의 규모, 팀의 숙련도 및 애플리케이션 상태를 구성하고 상호 작용하는 데 필요한 특정 요구 사항에 따라 달라집니다.

