포스트

Redux Toolkit이 필요한 앱은 따로 있다: Zustand, RTK Query 판단법

Redux Toolkit은 공유 상태와 서버 캐시의 변경 경로를 팀 전체가 추적해야 할 때 강하지만, 단순한 로컬 UI 상태까지 모두 넣으면 여전히 과한 선택입니다.

이 글은 Redux Toolkit 공식 문서와 Immer, Redux Toolkit 토론을 기준으로 RTK를 고르는 판단법을 정리합니다. 오래된 Redux의 action type, action creator, reducer와 saga 보일러플레이트를 그대로 전제로 평가하면 현재 사용 방식과 맞지 않습니다.

createSlice가 줄인 것은 불변성 실수다

createSlice의 reducer 안에서 state.value를 직접 바꾸는 것처럼 작성할 수 있는 이유는 Immer가 draft Proxy에서 변경을 기록하고 새 불변 상태를 만들기 때문입니다. action 생성과 reducer 정의도 한곳에 모여 파일 수를 줄일 수 있습니다.

이 문법은 RTK reducer의 draft 안에서만 안전합니다. 일반 객체나 컴포넌트에서 Redux 상태를 직접 수정해도 된다는 뜻이 아닙니다. 큰 중첩 구조를 무작정 넣으면 변경 범위와 렌더 비용을 이해하기 어려우므로 상태 모양과 selector를 함께 설계해야 합니다.

RTK Query는 서버 상태를 Store 안에서 관리한다

RTK Query는 요청 결과, 구독과 캐시 수명을 Redux Store에 연결합니다. providesTags와 invalidatesTags로 목록과 상세 데이터의 관계를 표현하면 mutation 뒤 필요한 쿼리를 다시 가져올 수 있습니다. 로딩, 오류, 중복 요청을 직접 slice로 구현하는 일을 줄여 주는 것이 장점입니다.

태그를 너무 넓게 무효화하면 작은 변경마다 많은 요청이 발생하고, 너무 좁게 잡으면 오래된 화면이 남습니다. 엔드포인트별 캐시 키와 구독 수를 DevTools에서 확인하고 목록 항목 하나가 바뀔 때 실제로 어떤 쿼리가 재실행되는지 회귀 테스트해야 합니다.

낙관적 업데이트는 실패 경쟁을 시험한다

onQueryStarted에서 캐시를 먼저 바꾸고 요청이 실패하면 undo하는 패턴은 반응이 빠른 화면을 만들 수 있습니다. 하지만 같은 항목에 여러 mutation이 겹치면 먼저 실패한 요청의 롤백이 나중 성공 결과를 덮을 수 있습니다. 요청 순서, 중복 클릭과 서버 버전 충돌을 포함해 검증해야 합니다.

결제나 좌석 예약처럼 실제 확정이 중요한 상태는 화면을 먼저 성공으로 보이게 하는 것이 위험할 수 있습니다. 좋아요처럼 되돌리기 쉬운 동작과 구분하고, 실패 메시지와 재동기화 경로를 준비하세요. 원문의 코드 조각은 개념 예시이며 앱의 동시성 정책까지 완성한 구현은 아닙니다.

선택은 상태의 종류와 팀 비용으로 한다

컴포넌트 몇 개의 테마, 모달과 폼 상태라면 React 로컬 상태나 가벼운 Store가 이해하기 쉽습니다. 서버 데이터가 주이고 클라이언트 전역 상태가 거의 없다면 전용 서버 캐시 도구와의 비교도 필요합니다. 반대로 복잡한 B2B 화면, 여러 기능이 공유하는 상태, 일관된 로그와 중앙 정책이 중요하다면 RTK의 규율이 이득이 될 수 있습니다.

작은 기능 하나를 RTK와 현재 대안으로 각각 구현해 코드량만 아니라 새 개발자의 이해 시간, 캐시 버그, DevTools 추적성, 번들 영향과 테스트 비용을 비교하세요. ‘Redux는 죽었다’거나 ‘엔터프라이즈에는 무조건 RTK’라는 구호보다, 변경 경로를 예측 가능하게 만드는 비용이 실제 복잡도에 맞는지가 최종 기준입니다.

먼저 상태의 소유권을 어떻게 나눌까?

도구를 선택하기 전에 상태를 네 종류로 분류하면 과도한 전역화를 피할 수 있습니다. 버튼 열림, 입력 포커스처럼 컴포넌트 생명주기에 묶인 UI 상태는 로컬에 둡니다. URL로 공유돼야 하는 필터와 페이지는 라우터가 소유합니다. 서버에서 다시 가져올 수 있는 데이터는 서버 캐시가, 여러 화면의 편집 초안이나 권한처럼 클라이언트 업무 규칙은 전역 상태가 맡을 수 있습니다.

같은 값을 두 곳에 복사하면 동기화 비용이 생깁니다. RTK Query 응답을 다시 slice에 통째로 저장하기보다 query 결과를 selector로 조합하고, 사용자가 편집을 시작할 때 필요한 필드만 초안으로 복사하세요. 서버 원본, 로컬 초안과 저장 상태를 명확히 이름 붙이지 않으면 refetch가 사용자의 입력을 덮거나 오래된 초안이 새 서버 값을 가릴 수 있습니다.

Redux Store에 넣었다는 이유로 영속 저장이 필요한 것도 아닙니다. 새로고침 뒤 복구할 상태, 로그아웃 때 지울 상태와 서버에서 재생성할 캐시를 구분합니다. 민감 정보는 DevTools와 영속화 플러그인에 노출될 수 있으므로 상태 모양뿐 아니라 관측 범위도 검토해야 합니다.

state 모양과 selector는 어떻게 설계하는가?

큰 중첩 객체를 한 slice에 넣고 상위 객체를 매번 교체하면 관련 없는 컴포넌트도 변경으로 인식할 수 있습니다. 엔티티를 ID 기준으로 정규화하고 관계와 화면 순서를 분리하면 항목 하나의 변경 경로가 선명해집니다. 하지만 데이터가 작고 한 화면에서만 쓰인다면 정규화 코드가 오히려 복잡할 수 있으므로 실제 업데이트 패턴을 봅니다.

selector는 저장 구조를 UI에서 숨기는 계약입니다. 컴포넌트가 state.feature.deep.items를 직접 읽게 두면 구조 변경이 화면 전체에 번집니다. 도메인 의미를 가진 selector를 만들고 입력 reference가 같을 때 결과를 재사용하도록 합니다. 매 호출마다 새 배열이나 객체를 만드는 selector는 불필요한 렌더를 만들 수 있으니 React Profiler와 Redux DevTools로 확인하세요.

Immer도 공짜 추상화는 아닙니다. draft 안에서 넓은 트리를 순회하거나 한 action으로 큰 배열을 모두 바꾸면 복사와 selector 재계산 비용이 커질 수 있습니다. 실제 느린 업데이트를 프로파일링한 뒤 상태를 나누거나 서버에서 페이지 단위로 가져오세요. 미리 ‘Redux는 느리다’고 단정하거나 반대로 추적 없이 모든 것을 slice로 옮기는 선택 모두 피해야 합니다.

RTK Query 캐시 키와 태그는 어떻게 검증할까?

캐시 키는 endpoint와 정규화된 인수에서 만들어지므로 정렬되지 않은 객체, 불필요한 UI 값과 불안정한 인수를 넘기면 같은 요청이 여러 캐시로 나뉠 수 있습니다. API가 실제 결과를 바꾸는 파라미터만 인수에 포함하고, 목록 필터와 권한 주체가 캐시에 정확히 반영되는지 확인합니다. 서로 다른 사용자나 테넌트의 데이터가 같은 키를 공유해서는 안 됩니다.

태그는 ‘모두 무효화’와 ‘아무것도 갱신되지 않음’ 사이를 조절합니다. 목록 전체 태그와 항목 ID 태그를 구분하고 mutation이 바꾸는 범위만 무효화합니다. 생성, 삭제 때 목록이, 수정 때 상세와 해당 항목을 포함한 목록이 어떻게 갱신되는지 테스트하세요. 페이지가 여러 개면 현재 화면만 새로 받아 다른 페이지가 오래된 상태로 남는지도 봅니다.

구독이 사라진 뒤 캐시를 얼마나 유지할지도 제품 행동입니다. 화면을 오갈 때 매번 refetch하면 네트워크가 늘고, 너무 오래 유지하면 다른 사용자의 변경을 늦게 봅니다. 데이터 변동성, 네트워크 비용과 사용자가 허용하는 신선도에 맞춰 정책을 정하고 포커스 복귀, 재연결 시 동작을 시험합니다.

낙관적 업데이트의 경쟁 상태를 어떻게 재현할까?

항목 A를 빠르게 두 번 수정한 뒤 첫 요청이 늦게 실패하는 시나리오를 만듭니다. 각 요청이 이전 캐시 상태를 기준으로 undo하면 첫 롤백이 두 번째 성공을 덮을 수 있습니다. 요청 ID나 서버 버전을 비교해 현재 값이 해당 optimistic patch에서 비롯됐을 때만 되돌리거나, 충돌 시 특정 캐시를 무효화해 서버 상태로 재동기화합니다.

오프라인, 탭 두 개, 중복 클릭과 서버의 409 응답도 포함합니다. UI에서 버튼을 잠그는 것은 중복을 줄이지만 네트워크 재시도와 다른 클라이언트 변경까지 막지는 못합니다. 서버가 idempotency와 버전 충돌을 지원해야 하며, 클라이언트는 실패한 작업과 현재 확정 상태를 사용자에게 구분해 보여 줍니다.

좋아요 수처럼 작은 오차가 잠시 허용되는 상태는 낙관적 반영이 유용합니다. 재고, 결제, 권한처럼 잘못된 성공 표시의 피해가 크면 pending 상태를 명확히 보이고 서버 확인 뒤 확정하는 편이 낫습니다. 패턴 선택은 구현 편의보다 실패했을 때 사용자가 어떤 잘못된 행동을 할 수 있는지로 결정합니다.

RTK가 과한지 확인하는 비교 실험은?

실제 공유 상태, 서버 mutation과 오류 처리가 들어간 대표 기능을 고릅니다. RTK 기준안과 현재 사용하는 대안을 같은 요구로 구현하고 코드 줄 수뿐 아니라 상태 변경을 추적하는 시간, 새 개발자가 수정 위치를 찾는 시간, 테스트 수와 캐시 결함을 비교합니다. DevTools에서 원인 action과 영향을 받은 selector를 설명할 수 있는지도 중요한 장점입니다.

팀이 중앙 규칙을 유지할 역량도 봅니다. slice마다 제각각 비동기 패턴을 만들거나 태그 규칙을 문서화하지 않으면 도구의 일관성이 사라집니다. 폴더 구조, endpoint 명명, error 변환과 selector 경계를 작은 예제로 합의하고 코드 리뷰에서 확인하세요.

합격 조건은 앱 크기가 아니라 복잡도를 줄였는지입니다. 전역 상태가 거의 없고 캐시 요구가 단순한데 boilerplate와 교육 시간이 늘었다면 채택하지 않을 수 있습니다. 반대로 여러 팀이 같은 업무 상태를 바꾸고 장애를 action 단위로 재현해야 한다면 중앙화 비용을 감수할 이유가 있습니다.

원문과 버전 확인

함께 읽으면 이해가 이어지는 글

자주 묻는 질문

React 앱이면 Redux Toolkit을 기본으로 써야 하나요?

아닙니다. 로컬 UI 상태와 단순한 서버 조회가 대부분이면 더 작은 도구가 이해하기 쉽습니다. 여러 기능이 공유하는 변경 규칙과 추적성이 필요할 때 RTK의 이점이 커집니다.

RTK Query를 쓰면 서버 데이터 slice가 필요 없나요?

일반적인 요청, 캐시, 로딩 상태는 RTK Query가 맡을 수 있습니다. 다만 편집 중 초안이나 여러 엔드포인트를 합친 업무 상태는 소유권을 구분해 별도로 설계해야 합니다.

낙관적 업데이트는 언제 피해야 하나요?

결제, 예약처럼 성공을 잘못 표시하는 피해가 크거나 동시 변경 충돌을 복구하기 어렵다면 서버 확인 뒤 반영하는 편이 안전합니다. 적용할 때는 롤백과 재동기화를 시험해야 합니다.

THE END / OPSOAI

여기까지 읽었습니다

핵심 장면을 한 번 더 떠올려 보세요. 이해가 남았다면 이 책은 제 역할을 다했습니다.

다른 책 고르기
표지 1 —

←→ 키와 좌우 스와이프를 지원합니다. 읽던 페이지는 이 기기에 저장됩니다.

CONTENTS

이 책의 목차

    12개 장 20 분읽는 시간