Zustand와 slice를 활용한 상태 관리 리팩토링 (1)

gangmin·2025년 9월 21일

React

목록 보기
5/6
post-thumbnail

Why I Refactored?

이번 리팩토링은 기능이 점차 확장되면서 생긴 문제에서 시작됐다. 처음 이 페이지에는 탭(Tab) 개념이 없었기 때문에, 각 컴포넌트가 자기 상태를 로컬 상태(useState) 로 관리해도 전혀 문제가 없었다.

하지만 나중에 두 개의 탭 기능이 추가되면서 상황이 바뀌었다. 사용자가 탭을 이동해도 입력값이나 선택값이 유지되어야 했기 때문에, 자연스럽게 전역적인 상태 관리가 필요해졌다.

당시 나는 Zustand를 적극적으로 활용하지 않고, 단순히 모든 상태를 페이지 최상단 컴포넌트로 끌어올리는 방식(lifting state up) 을 택했다. 이 접근은 동작 자체에는 문제가 없었지만, 곧 코드가 복잡해지기 시작했다.

  • props drilling이 심해져서 컴포넌트 트리 전반에 데이터가 흩어짐
    ⇒ 실제로 한 값이 세 단계 이상 내려가다 보니, 중간 컴포넌트는 그 값을 쓰지 않는데도 단순히 전달만 하는 ‘통로 역할’ 이 되어버렸다.
    ⇒ 상태 변경 함수(setter) 또한 같이 내려가야 했기 때문에, props 목록이 길어지고 가독성이 떨어졌다.
  • 상태와 로직이 뒤섞여 가독성이 떨어짐
  • 새로운 요구사항을 반영할 때 변경 범위가 불필요하게 커짐
    ⇒ 새로운 상태를 추가할 때마다 최상단에서 선언 → 관련 props를 전부 수정 → 여러 컴포넌트에 반영해야 해서, 작은 요구사항 변경에도 수정 범위가 넓어졌다.

상태를 최상단으로 올리면서 관리 포인트가 배로 늘어나버렸다. 결국 이 한계를 해결하기 위해 Zustand + 슬라이스(Slice) 기반 리팩토링을 시도하게 되었다.

What Is the Slice Pattern?

Zustand에서 Slice 패턴은 큰 전역 스토어를 기능 단위로 쪼개서 관리하는 방식이다.
한 파일에 모든 상태와 로직을 몰아넣는 대신, 탭별·기능별로 상태를 나눠서 정의하고 최종적으로 하나의 스토어에 합치는 구조다.

What Are the Alternatives?

Zustand는 가볍고 직관적인 상태 관리 도구지만, 전역 상태를 관리하는 방법은 이것 하나만 있는 게 아니다. 상황에 따라 다양한 대안들이 존재한다.

1. React Context + useReducer

React에 내장된 기능만으로도 전역 상태를 흉내낼 수 있다. Context로 전역 공급을 하고, useReducer로 상태 전환 로직을 명확히 관리할 수 있다.

  • 장점 : 별도 라이브러리 필요 없음. 작은 프로젝트에서는 충분히 단순하고 직관적임.

  • 단점 : Context 값이 변경될 때마다 해당 Context를 구독하는 모든 컴포넌트가 리렌더링. 규모가 커지면 성능 이슈와 코드 복잡성이 생김.

2. Redux (Redux Toolkit 포함)

가장 전통적인 상태 관리 도구. 중앙 집중형 스토어를 기반으로 엄격한 패턴을 따름.

  • 장점 : 대규모 애플리케이션에서도 예측 가능성과 일관성을 제공. 미들웨어, 개발자 도구 등 생태계가 풍부함.

  • 단점 : 보일러플레이트 코드가 많음. 작은 규모에서는 오히려 과하다 느껴질 수 있음.

3. Recoil

Facebook이 만든 실험적 상태 관리 라이브러리. “원자(atom)” 단위로 상태를 쪼개고 조합 가능.

  • 장점 : 파생 상태(derived state)를 선언적으로 쉽게 만들 수 있음. React의 Suspense와 잘 어울림.

  • 단점 : 커뮤니티와 생태계가 Redux, Zustand보다는 작음.

4. Jotai

최소한의 API로 상태를 atom 단위로 관리. Recoil과 비슷하지만 훨씬 더 단순함.

  • 장점 : 작은 단위의 상태 관리에 강함. 러닝 커브가 낮음.

  • 단점 : 프로젝트가 커지면 구조화가 필요함. 대규모 사례나 참고 자료는 상대적으로 적음.

5. Query 라이브러리 (예: TanStack Query)

원래는 서버 상태(server state) 관리에 특화된 도구. 클라이언트 전역 상태를 관리하는 도구는 아니지만, 서버와 동기화되는 상태는 대부분 Query로 관리하는 게 권장됨.

  • 장점 : 캐싱, 리페치, 에러 핸들링 등 서버 상태 관리에 최적화. 전역 상태 라이브러리와 역할을 나눠 쓰면 깔끔함.

  • 단점 : 로컬/클라이언트 상태 관리와는 다른 영역이므로 혼동되면 복잡해질 수 있음.

👉 정리하자면, 전역 상태 관리에는 정답이 없다.

  • 작은 규모라면 Context + useReducer 만으로도 충분하다.
  • 규칙성과 확장성을 원한다면 Redux.
  • 단순하면서 가볍게 쓰고 싶다면 Zustand.
  • 원자 단위로 세밀한 제어를 원한다면 Recoil/Jotai.
  • 서버에서 내려오는 데이터는 TanStack Query로 처리하는 것이 권장된다.

Props Drilling vs Global State

리팩토링을 하며 고민했던 지점은 바로 이것이다.
“프롭스 드릴링보다 전역 상태 관리가 무조건 좋은 걸까?”

[ Props 기반 관리 ]
부모 컴포넌트가 상태를 갖고 있으면, 그 상태가 바뀔 때마다 해당 부모와 모든 자식이 리렌더링된다. 불필요하게 깊은 컴포넌트까지 렌더링이 전파되는 게 문제다.

  • 장점: 데이터 흐름이 명확하다.
  • 단점: 깊은 컴포넌트 트리에서는 가독성이 떨어지고 유지보수가 힘들다.

[ Zustand 기반 관리 ]
원하는 컴포넌트에서 직접 상태를 구독할 수 있다. 즉, “필요한 곳만 다시 렌더링”할 수 있기 때문에 props drilling보다 효율적일 수 있다. 하지만 selector를 제대로 설정하지 않으면, 스토어 전체가 리렌더링 트리거가 되어 오히려 성능이 떨어질 수 있다.

  • 장점: 어디서든 필요한 상태를 바로 가져올 수 있고, props drilling을 줄인다.
  • 단점: 상태 구조를 잘못 설계하면 전역이 불필요하게 비대해지고, 디버깅이 어려워질 수 있다.

👉 따라서 전역 상태는 잠재적으로 성능 개선 여지가 있지만, 구독 범위를 얼마나 잘 좁히느냐에 따라 결과가 달라진다.

즉, 전역 상태가 “편하다”는 건 맞지만, 곧바로 성능 최적화나 코드 품질 향상으로 이어지는 것은 아니다.

잘못 설계하면 오히려 불필요한 리렌더링이 늘어난다. 전역 상태는 구독 범위가 넓기 때문에, slice/selector를 제대로 안 쓰면 전체가 리렌더링될 수 있다. 또한, 전역에 몰아넣기만 하면 상태 트리가 비대해지고, 오히려 관리 포인트가 늘어난다. 결국 어떻게 설계하고 분리했느냐가 품질을 좌우한다.

Performance Perspective

많은 사람들이 “전역 상태 관리 → 성능 최적화” 라고 생각하지만, 실제로는 측정없이는 알 수 없다. Zustand 같은 라이브러리는 React Context 대비 렌더링 최소화에 유리하지만, 전역 상태를 남발하면 오히려 모든 컴포넌트가 다시 렌더링될 수도 있다.

Profiler 같은 도구를 활용해 “리팩토링 전/후가 얼마나 달라졌는지”를 수치로 검증하는 과정이 중요하다.
(이 부분은 다음에 이어서 다루겠다.)

0개의 댓글