Redux를 Zustand로 바꾸는 데
3주가 걸린 이유
"Zustand는 간단하잖아요" — 그 말이 얼마나 틀렸는지 마이그레이션 첫날 알았다
회의는 30분이면 끝났다. "Redux 보일러플레이트가 너무 많고, Zustand가 더 가볍고 빠르니까 바꾸자." 팀원 모두 동의했다. 예상 기간은 1주일이었다.
실제로는 3주가 걸렸다. 그 3주 동안 프로덕션 버그 2건이 났고, 팀원 한 명이 번아웃에 준하는 상태가 됐고(나였다), 스프린트 일정이 한 번 밀렸다.
"기술 스택을 바꾼다"는 말이 얼마나 가볍게 결정되고 얼마나 무겁게 실행되는지 — 이 글은 그 경험의 기록이다.

예상 기간
소요 기간
프로덕션 버그
감소
왜 바꾸기로 했나 — 진짜 이유
공식적인 이유는 "개발 생산성 향상"이었다. 그런데 솔직히 말하면 복합적이었다.
표면적: Redux Toolkit 보일러플레이트가 너무 많다. 새 기능 추가할 때마다 slice, action, selector, thunk를 전부 새로 만들어야 한다.
실질적: 신규 입사한 팀원이 Redux 패턴에 적응하는 데 2주가 걸렸다. Zustand는 러닝커브가 낮다.
솔직히: 기술 블로그에서 "Zustand가 훨씬 낫다"는 글을 보고 팀장이 먼저 꺼낸 주제였다. 다들 새 기술이 궁금했던 것도 있었다. 솔직 고백
이 결정에서 빠진 게 있었다. 지금 코드베이스가 얼마나 Redux에 의존하고 있는지 실제로 측정하지 않았다. "한 30개 파일 정도 되겠지"라고 막연히 생각했는데, 실제로 세어보니 Redux 관련 파일이 47개, Redux를 직접 import하는 컴포넌트가 63개였다.
Redux vs Zustand — 개념부터 달랐다
마이그레이션을 시작하기 전에 두 라이브러리의 철학 차이를 제대로 이해하지 못했던 것도 문제였다. 단순히 "문법이 다른 같은 것"이 아니었다.
| 항목 | Redux Toolkit | Zustand | 마이그레이션 영향 |
|---|---|---|---|
| 상태 구조 | 단일 스토어 모든 상태가 하나의 트리 |
다수 스토어 기능별로 분리 |
구조 재설계 필요 |
| 비동기 처리 | createAsyncThunk pending/fulfilled/rejected |
직접 async 함수 스토어 안에 그냥 씀 |
패턴 전환, 로딩 상태 재설계 |
| 미들웨어 | Redux middleware logger, thunk 등 |
middleware 함수 방식이 다름 |
로깅 재구축 필요 |
| DevTools | Redux DevTools 시간여행 디버깅 |
별도 설정 필요 기본은 미지원 |
디버깅 환경 재설정 |
| 셀렉터 | createSelector 메모이제이션 내장 |
커스텀 필요 기본 없음 |
성능 최적화 재검토 |
"Zustand가 간단하다는 건 사실이다. 그런데 기존 코드가 Redux 방식으로 설계돼 있으면, 그 '간단함'으로 가는 길이 간단하지 않다."
— 마이그레이션 2주차에 깨달은 것실제로 겪은 고통 5가지
userSlice, cartSlice, uiSlice가 하나의 스토어에 묶여 있었는데, 이걸 어떻게 나누느냐가 첫 번째 설계 충돌이었다. 어떤 상태를 어떤 스토어에 넣느냐를 팀이 합의하는 데만 반나절이 걸렸다. 이건 기술 문제가 아니라 설계 문제였다.createAsyncThunk가 pending / fulfilled / rejected 세 상태를 자동으로 관리해줬다. Zustand에는 이게 없다. 로딩, 에러, 성공 상태를 스토어 안에 직접 관리해야 했다. 기존에 asyncThunk를 쓰던 API 호출이 18개였는데, 전부 패턴을 새로 짜야 했다. 여기서 일주일을 썼다.// ❌ Before: createAsyncThunk
export const fetchUser = createAsyncThunk(
'user/fetch',
async (id: string) => {
return await api.getUser(id);
}
);
// extraReducers로 loading/error 자동 처리
// ✅ After: Zustand — 직접 구현
const useUserStore = create<UserStore>()(set => ({
user: null,
isLoading: false, // 직접 관리
error: null, // 직접 관리
fetchUser: async (id) => {
set({ isLoading: true, error: null });
try {
const user = await api.getUser(id);
set({ user, isLoading: false });
} catch (e) {
set({ error: e, isLoading: false });
}
},
}));
createSelector로 파생 상태를 메모이제이션했다. Zustand로 바꾸고 나서 같은 방식으로 쓰다가 특정 페이지에서 리렌더링 횟수가 6배로 늘었다. 장바구니 페이지에서 상품 하나를 추가할 때마다 전체 목록이 통째로 다시 그려졌다. DevTools로 추적하는 데만 하루가 걸렸고, useShallow를 사용하는 패턴으로 해결하는 데 또 하루가 걸렸다.// ❌ 이렇게 쓰면 매 렌더마다 새 객체 참조 → 리렌더링
const { items, total } = useCartStore();
// ✅ useShallow로 얕은 비교 적용
import { useShallow } from 'zustand/react/shallow'
const { items, total } = useCartStore(
useShallow(state => ({
items: state.items,
total: state.total,
}))
);
// 또는 파생 상태는 별도 selector 함수로 분리
const selectCartSummary = (state: CartStore) => ({
count: state.items.length,
total: state.items.reduce((sum, i) => sum + i.price, 0),
});
renderWithStore 같은 테스트 헬퍼들이 Redux 기반이었다. 테스트 코드 변환에 예상 외로 이틀이 더 걸렸다. 마이그레이션 계획에 테스트 코드를 항목으로 넣지 않은 실수였다.// ❌ Before: Redux Provider 필요
const renderWithStore = (ui, { initialState } = {}) => {
const store = configureStore({ reducer, preloadedState: initialState });
return render(<Provider store={store}>{ui}</Provider>);
};
// ✅ After: Zustand는 Provider 불필요 — 스토어 직접 초기화
const renderWithZustand = (ui, initialStoreValues = {}) => {
// 테스트 전 스토어 초기화
beforeEach(() => {
useUserStore.setState(initialStoreValues.user ?? defaultUserState);
useCartStore.setState(initialStoreValues.cart ?? defaultCartState);
});
return render(ui); // Provider 없이도 됨
};
3주 타임라인 — 실제로 어떻게 흘렀나
package.json에서 삭제했다. CI 통과. 번들 사이즈 8.2KB 감소. 팀 회고에서 "왜 예상이 이렇게 빗나갔나"를 분석했다. 다음 마이그레이션을 위한 체크리스트도 만들었다.다음 마이그레이션 때 바꿀 것들
grep -r "from 'redux'" src/로 의존 파일 수를 먼저 센다. "한 30개 되겠지"는 금물. 실제 숫자가 계획의 시작이다.번들 사이즈 8.2KB 감소, 신규 기능 개발 시 상태 추가 시간 70% 단축, 새 팀원 온보딩 시 상태 관리 이해 시간 1주 → 2일. 고통스러웠지만 결과는 분명히 좋아졌다. 다만 그 과정이 예상의 3배였을 뿐이다.
기술 마이그레이션은 항상 "이 정도면 금방이지"로 시작해서 "왜 이렇게 나왔지"로 끝난다. 그 간극이 좁혀지는 건 경험이 쌓이면서다. 이 글이 다음 마이그레이션을 앞둔 팀에게 조금이라도 그 간극을 좁혀주길 바란다.
💬 마이그레이션 경험이 있으신가요?
Redux에서 Zustand, Recoil, Jotai, React Query 등으로 전환한 경험이 있다면 가장 힘들었던 부분이 무엇이었는지 댓글로 나눠주세요. 반대로 "이 방법으로 했더니 훨씬 수월했다"는 팁도 반깁니다. 다음 마이그레이션 체크리스트에 반영할게요. 🙏
'개발자 > 기술스택 도구' 카테고리의 다른 글
| Figma와 협업하면서 생긴 프론트엔드 개발자의 고충 (0) | 2026.07.03 |
|---|---|
| 내가 지금 쓰는 개발 도구 세팅 공개 - VSCode, 터미널, 맥 환경 (1) | 2026.06.26 |
| TypeScript 도입, 현실은 달랐다 (1) | 2026.06.19 |
| Next.js 실무 도입 후 1년, 솔직하게 달라진 점들을 써봅니다 (0) | 2026.06.12 |