· React· Zustand· Redux· State Management· Migration

Redux에서 Zustand로, 1년째 진행 중인 마이그레이션

서비스 중단 없이 대규모 Redux 코드베이스를 Zustand로 점진적으로 전환하는 과정

프로젝트의 전역 상태 관리는 Redux로 시작했다. 3년 넘게 운영하다 보니 slice 파일만 수십 개, action type은 셀 수 없을 정도로 늘어났다. 새로운 기능 하나 추가하려면 reducer 만들고, action 정의하고, selector 작성하고, connect하고… 보일러플레이트가 실제 로직보다 많은 상황이 됐다.

팀에 새로 합류한 개발자가 “이 상태 어디서 바뀌는 거예요?”라고 물으면, 답변하기가 점점 어려워졌다. action이 dispatch되는 곳, reducer에서 처리하는 곳, selector로 가져오는 곳이 다 다르니까.

Zustand로 바꾸고 싶었지만, 서비스는 계속 돌아가고 있었다. 전면 교체? 현실적으로 불가능했다. 그래서 1년 이상의 장기 마이그레이션을 계획했고, 지금 절반 정도 진행된 상태다.

왜 Zustand인가

Redux가 나쁜 건 아니다. 다만 우리 프로젝트 규모와 팀 상황에서는 오버헤드가 컸다.

Zustand를 선택한 이유는 단순했다.

// Redux 패턴
// userSlice.ts
const userSlice = createSlice({
  name: 'user',
  initialState: { data: null, loading: false, error: null },
  reducers: {
    setUser(state, action) { state.data = action.payload },
    setLoading(state, action) { state.loading = action.payload },
    setError(state, action) { state.error = action.payload },
  },
});
export const { setUser, setLoading, setError } = userSlice.actions;

// 사용하는 곳
const dispatch = useDispatch();
const user = useSelector(state => state.user.data);
dispatch(setUser(userData));
// Zustand 패턴
// useUserStore.ts
export const useUserStore = create((set) => ({
  user: null,
  loading: false,
  error: null,
  setUser: (data) => set({ user: data }),
}));

// 사용하는 곳
const { user, setUser } = useUserStore();
setUser(userData);

코드량 차이가 보인다. 상태 정의와 사용이 한 곳에서 끝난다. 새로 합류한 개발자도 5분이면 패턴을 이해한다.

SSR 환경에서도 이점이 있었다. Redux는 서버에서 store를 생성하고, 직렬화해서 클라이언트로 넘기고, 다시 hydrate하는 과정이 복잡했다. Zustand는 이 과정이 훨씬 단순하다.

마이그레이션 원칙

전면 교체는 위험했다. 서비스 규모가 크고, Redux에 의존하는 코드가 너무 많았다. 한 번에 바꾸다가 장애 나면 롤백도 쉽지 않다.

원칙을 세웠다.

  1. 신규 코드는 Zustand로 - 새로 만드는 기능은 무조건 Zustand
  2. 기존 Redux는 건드리지 않음 - 잘 돌아가는 건 일단 놔둠
  3. 공존 허용 - Redux store와 Zustand store가 동시에 존재해도 됨
  4. 여유 있을 때 이전 - 급하게 안 함. 리팩토링 시간이 날 때 하나씩

처음엔 이게 가능한가 싶었다. Redux와 Zustand가 같은 프로젝트에서 공존한다고? 근데 실제로 해보니 문제없었다. 둘 다 그냥 상태 관리 라이브러리일 뿐이다.

단계별 진행

1단계: 신규 영역 분리

먼저 신규 기능에만 Zustand를 적용했다. 새로 만드는 페이지, 새로 추가하는 기능은 전부 Zustand로. 이 시점에서 Redux 코드는 하나도 안 건드렸다.

팀 내 합의도 필요했다. PR 리뷰에서 “신규 기능인데 왜 Redux 쓰셨어요?”라는 코멘트가 달리기 시작했다. 점점 Zustand가 기본값이 되어갔다.

2단계: 핵심 상태 이전 (지금 여기)

신규 기능이 어느 정도 쌓이고 팀이 Zustand에 익숙해진 후, 기존 Redux 상태를 하나씩 옮기기 시작했다.

우선순위는 이랬다.

  • 사용자 인증 상태 → 거의 모든 페이지에서 쓰니까 먼저
  • 전역 설정 (테마, 언어 등) → 자주 바뀌지 않아서 이전하기 쉬움
  • UI 상태 (모달, 사이드바 등) → 독립적이라 분리하기 쉬움

이전할 때마다 기존 Redux selector를 쓰던 컴포넌트들을 찾아서 Zustand hook으로 교체했다. 한 상태를 완전히 옮긴 후에야 해당 Redux slice를 삭제했다.

3단계: 레거시 정리 (앞으로)

아직 남은 Redux 코드가 있다. 복잡하게 얽혀있는 상태들, 손대기 무서운 레거시들. 이것들은 천천히 정리할 예정이다. 급하지 않다.

최종 목표는 Redux 완전 제거지만, 언제가 될지는 모르겠다. 중요한 건 방향이 정해졌다는 것.

지금까지 느낀 점

절반 정도 전환한 시점에서 체감되는 변화가 있다.

코드 리뷰가 빨라졌다. 상태 관련 코드가 짧아지니까 리뷰할 양 자체가 줄었다. “이 action은 어디서 dispatch 되나요?” 같은 질문도 사라졌다.

신규 기능 개발 속도도 올랐다. 보일러플레이트 작성 시간이 줄어든 만큼 실제 로직에 집중할 수 있다.

상태 관련 버그도 줄었다. 상태가 어디서 바뀌는지 추적하기 쉬워지니까 디버깅도 빨라졌다.

물론 마이그레이션 자체가 공짜는 아니다. 시간 들고, 중간에 공존하는 구조가 지저분하게 느껴질 때도 있다. 그래도 장기적으로는 맞는 방향이라고 생각한다. 1년 뒤에는 Redux가 완전히 사라진 코드베이스를 볼 수 있지 않을까.