Redux/Zustand와 TS: 전역 상태 관리 도구의 타입 안정성 확보

규모가 큰 리액트 애플리케이션에서 전역 상태 관리는 필수적입니다. 하지만 상태가 복잡해질수록 "어떤 데이터가 들어있는지", "이 액션이 어떤 변화를 일으키는지" 추적하기 어려워집니다.
TypeScript를 Redux Toolkit(RTK)과 Zustand에 도입하면 컴파일 타임에 에러를 잡고, 강력한 자동 완성 기능을 통해 개발 생산성을 비약적으로 높일 수 있습니다. 각 도구별 최적의 타입 적용 패턴을 정리해 보았습니다.
1. Zustand: 직관적이고 가벼운 타입 정의
Zustand는 리액트 훅을 기반으로 하며, TypeScript와의 궁합이 매우 뛰어납니다. 별도의 보일러플레이트 없이 인터페이스 하나로 상태와 액션을 모두 정의할 수 있습니다.
기초: 스토어 인터페이스 정의
Zustand의 create 함수에 제네릭을 전달하여 스토어의 전체 구조를 확정합니다.
import { create } from 'zustand';
// 1. 상태(State)와 액션(Action)을 아우르는 인터페이스 정의
interface AuthState {
user: { id: string; name: string } | null;
isLoggedIn: boolean;
login: (userData: { id: string; name: string }) => void;
logout: () => void;
}
// 2. create<T> 제네릭을 사용하여 스토어 생성
const useAuthStore = create<AuthState>((set) => ({
user: null,
isLoggedIn: false,
login: (userData) => set({ user: userData, isLoggedIn: true }),
logout: () => set({ user: null, isLoggedIn: false }),
}));
// 사용 예시
export const UserProfile = () => {
const { user, logout } = useAuthStore();
return (
<div>
{user ? (
<>
<span>{user.name}님 환영합니다.</span>
<button onClick={logout}>로그아웃</button>
</>
) : (
<span>로그인이 필요합니다.</span>
)}
</div>
);
};
고급: Middleware(Persist)와 타입 결합
로컬 스토리지에 상태를 저장하는 persist 미들웨어를 사용할 때도 타입 추론이 끊기지 않도록 주의해야 합니다.
import { persist } from 'zustand/middleware';
const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
theme: 'light',
toggleTheme: () => set((state) => ({
theme: state.theme === 'light' ? 'dark' : 'light'
})),
}),
{ name: 'app-settings' } // 로컬 스토리지 키 이름
)
);
2. Redux Toolkit (RTK): 표준화된 타입 시스템
Redux Toolkit은 구조가 정형화되어 있어, 한 번 설정해두면 프로젝트 전체에서 매우 일관된 타입 안정성을 누릴 수 있습니다.
Step 1: Slice와 PayloadAction 정의
createSlice를 작성할 때 PayloadAction 타입을 사용하여 액션의 데이터 구조를 명시합니다.
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoState {
items: Todo[];
}
const initialState: TodoState = {
items: [],
};
const todoSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// PayloadAction 제네릭으로 전달받는 데이터 타입 지정
addTodo: (state, action: PayloadAction<string>) => {
state.items.push({
id: Date.now(),
text: action.payload,
completed: false,
});
},
toggleTodo: (state, action: PayloadAction<number>) => {
const todo = state.items.find(t => t.id === action.payload);
if (todo) todo.completed = !todo.completed;
},
},
});
Step 2: Store에서 RootState와 AppDispatch 추출
이 부분이 RTK 타입 안정성의 핵심입니다. 스토어 자체에서 타입을 추출하여 수동 관리를 방지합니다.
// store.ts
import { configureStore } from '@reduxjs/toolkit';
export const store = configureStore({
reducer: {
todos: todoSlice.reducer,
},
});
// 1. 전체 상태 타입 추출
export type RootState = ReturnType<typeof store.getState>;
// 2. 디스패치 함수 타입 추출
export type AppDispatch = typeof store.dispatch;
Step 3: Typed Hooks 작성 (매우 중요)
컴포넌트에서 useSelector를 쓸 때마다 (state: RootState)를 작성하는 것은 번거롭습니다. 미리 타입이 적용된 커스텀 훅을 만들어 사용하세요.
// hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
// 타입이 이미 지정된 전용 훅들
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// 컴포넌트에서의 사용
const TodoList = () => {
const todos = useAppSelector((state) => state.todos.items);
const dispatch = useAppDispatch();
return (
<button onClick={() => dispatch(todoSlice.actions.addTodo("새 할 일"))}>
추가
</button>
);
};
3. Zustand vs Redux Toolkit: 타입 관점에서의 선택
비교 항목ZustandRedux Toolkit| 타입 정의 복잡도 | 매우 낮음 (인터페이스 하나) | 중간 (Slice, Store, Hooks 분리) |
| 자동 완성 경험 | 직관적이고 빠름 | 엄격하고 체계적임 |
| 비동기 처리 타입 | 일반 함수로 처리 가능 | createAsyncThunk 등 전용 타입 필요 |
| 추천 상황 | 중소규모, 빠른 프로토타이핑 | 대규모 팀 프로젝트, 복잡한 상태 로직 |
요약: 타입 안정성을 위한 골든 룰
- 상태를 먼저 정의하라: 코드를 짜기 전 interface나 type으로 상태의 모습을 먼저 그려보는 습관이 중요합니다.
- any를 지양하라: 전역 상태에서 any를 사용하는 것은 프로젝트 전체에 시한폭탄을 심는 것과 같습니다.
- 커스텀 훅을 활용하라: Redux의 경우 useAppSelector와 같은 커스텀 훅을 사용하는 것이 유지보수의 핵심입니다.
- 불변성을 지켜라: RTK는 Immer를 내장하고 있지만, Zustand는 스프레드 연산자(...state)를 통한 불변성 유지를 타입 시스템과 함께 꼼꼼히 체크해야 합니다.
전역 상태에 흐르는 데이터를 타입으로 묶어두는 순간, 수많은 런타임 버그는 사라지고 개발자의 확신은 견고해질 것입니다.