Post

Redux는 죽었다? 아니, RTK로 독기를 품고 돌아왔다: 10년 차가 뜯어본 상태 관리의 끝판왕

Redux는 죽었다? 아니, RTK로 독기를 품고 돌아왔다: 10년 차가 뜯어본 상태 관리의 끝판왕

Metadata

  • 공식 문서: https://redux-toolkit.js.org/
  • Immer.js 코어 레포지토리: https://github.com/immerjs/immer
  • 관련 아키텍처 토론 (Redux vs React Query): https://github.com/reduxjs/redux-toolkit/discussions

The Hook: 우리가 Redux를 버렸던 진짜 이유

솔직히 말씀드릴게요. 불과 3~4년 전만 해도 전 후배 개발자들에게 “신규 프로젝트면 제발 Redux 쓰지 마세요”라고 입버릇처럼 말하고 다녔습니다. 현업에서 모달 창 하나 띄우는 상태를 관리하려고 ACTION_TYPES.js, actions.js, reducer.js를 만들고, 비동기 처리를 위해 redux-saga 설정 파일까지 총 5개의 파일을 넘나들어야 했던 경험, 다들 있으시죠? 그건 개발이 아니라 거의 ‘의식(Ritual)’에 가까웠습니다.

이 지독한 보일러플레이트(Boilerplate) 지옥에 지쳐갈 때쯤 Zustand, Recoil, Jotai 같은 경량화 라이브러리와 React Query라는 걸출한 서버 상태 관리 툴이 등장했습니다. Redux의 시대는 끝난 것처럼 보였죠. 하지만 수많은 대규모 엔터프라이즈 프로젝트가 여전히 Redux를 놓지 못하고 있었고, Redux 팀은 칼을 갈았습니다. 그리고 내놓은 해답이 바로 RTK(Redux Toolkit)입니다.

처음엔 그저 기존 문법을 조금 줄여주는 ‘Syntactic Sugar(문법적 설탕)’인 줄 알았습니다. 하지만 실무에서 대규모 트래픽과 복잡한 도메인 상태를 다뤄보니 알겠더라고요. RTK는 단순한 편의성 도구가 아니라, 프론트엔드 상태 관리에 대한 Redux 팀의 독기 어린 ‘철학적 선언’이었습니다.


TL;DR: 이 녀석이 진짜 무서운 이유

RTK는 클라이언트 상태와 서버 상태를 완전히 분리하고, 지독했던 보일러플레이트를 내부의 Immer.js와 자동화된 캐시 머신(RTK Query)으로 박살 낸 ‘의견이 강하게 반영된(Opinionated) 완성형 아키텍처’입니다.


Deep Dive: Under the Hood (어떻게 지옥에서 벗어났는가)

RTK가 이전의 Legacy Redux와 가장 크게 차별화되는 지점은 크게 두 가지, ‘불변성 유지의 자동화’‘서버 상태 관리의 내재화’입니다. 겉핥기식 기능 나열은 접어두고, 아키텍처 내부에서 무슨 일이 벌어지는지 뜯어봅시다.

1. Immer.js와 Proxy 객체의 마법

기존 Redux에서 가장 끔찍했던 건 상태의 깊은(depth) 복사였습니다. ...state, user: { ...state.user, name: 'Kim' } 같은 코드는 실수하기 딱 좋았죠. RTK의 createSlice는 내부적으로 Immer.js를 품고 있습니다.

우리가 state.value += 1처럼 대놓고 상태를 직접 변경(Mutate)하는 코드를 작성해도 에러가 나지 않는 이유는, 우리가 수정하는 state가 실제 상태 객체가 아니라 Proxy로 래핑된 Draft 객체이기 때문입니다. Immer의 Proxy가 우리가 수정한 내역을 가로채서(Trap) 기존 상태 트리와 비교한 뒤, 변경된 부분만 새로운 참조를 갖는 완전히 새로운 불변 객체를 ‘알아서’ 생성해 줍니다. 이 구조적 변화 덕분에 리듀서 코드가 평균 70% 이상 압축됩니다.

2. RTK Query: 단순 페칭 툴이 아닌 ‘의존성 기반 캐시 머신’

많은 분들이 RTK를 쓰면서도 RTK Query를 React Query의 하위 호환 정도로 생각하시더라고요. 하지만 내부 작동 원리를 보면 전혀 다릅니다. RTK Query는 철저하게 Redux Store의 생명주기와 엮여 있습니다.

데이터를 가져오면 Redux Store 내부에 엄청나게 거대한 캐시 딕셔너리가 생성됩니다. 이때 providesTagsinvalidatesTags를 통해 데이터 간의 관계를 그래프 형태로 맵핑하죠. 즉, 특정 API가 호출되어 User 태그가 무효화(Invalidate)되면, 이 태그를 구독(Subscribe)하고 있던 컴포넌트들의 트리거가 자동으로 당겨지며 백그라운드 리페칭이 일어나는 완벽한 반응형 캐싱 시스템입니다.

비교 항목Legacy Redux + SagaRedux Toolkit (RTK) + RTK Query
상태 불변성 유지개발자가 수동으로 Spread Operator 사용내부 Immer.js가 Proxy 객체로 자동 불변성 보장
비동기 처리 구조Generator 함수 기반 복잡한 Saga 흐름 제어RTK Query의 Hook(useGet...Query)으로 단일화
서버 상태 캐싱직접 캐시 만료 로직과 스토어 초기화 구현providesTags 기반의 자동화된 캐시 무효화 메커니즘
보일러플레이트Action, Reducer, Saga 등 최소 3~5개 파일createSlice, createApi 단 1~2개 파일로 압축

Pragmatic Use Cases: 실무 적용 시나리오와 트러블슈팅

단순한 CRUD 예제는 공식 문서에 널려 있으니 치워버리겠습니다. 제가 작년 티켓 예매 시스템 개편 프로젝트에서 마주했던 ‘대규모 트래픽 스파이크 시의 낙관적 업데이트(Optimistic Updates)’ 시나리오를 살펴볼까요?

서버 응답 속도가 느려질 때, 사용자가 ‘좋아요’나 ‘예약’ 버튼을 눌렀는데 화면이 1초 이상 멈춰있으면 사용자 경험은 박살 납니다. 이때 RTK Query의 onQueryStarted를 활용하면 서버 응답을 기다리지 않고 Redux Store의 캐시를 먼저 강제로 덮어써버릴 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// RTK Query를 활용한 하드코어 낙관적 업데이트 예시
updateTicketStatus: build.mutation({
  query: ({ ticketId, status }) => ({
    url: `/tickets/${ticketId}`,
    method: 'PATCH',
    body: { status },
  }),
  async onQueryStarted({ ticketId, status }, { dispatch, queryFulfilled }) {
    // 1. 서버 요청이 시작되자마자 Store 캐시를 즉시 업데이트 (Optimistic)
    const patchResult = dispatch(
      api.util.updateQueryData('getTicketDetails', ticketId, (draft) => {
        draft.status = status; // Immer 덕분에 직접 수정 가능!
      })
    );
    try {
      // 2. 실제 서버 응답 대기
      await queryFulfilled;
    } catch {
      // 3. 서버 응답이 실패하면 즉시 이전 캐시 상태로 롤백
      patchResult.undo();
      console.error('앗, 서버가 터졌네요. 상태를 원복합니다.');
    }
  },
});

이 코드가 진짜 강력한 이유는, 개발자가 별도의 ‘임시 상태’를 만들 필요 없이 RTK Query가 관리하는 중앙 캐시 스토어를 직접 조작하고 롤백할 수 있다는 점입니다. 기존 Saga로 이 로직을 구현하려면 코드가 5배는 길어졌을 겁니다.

또 다른 팁 하나 드릴까요? 대시보드 화면에서 실시간 데이터를 폴링(Polling)해야 할 때, 브라우저 탭이 백그라운드로 가면 굳이 API를 쏠 필요가 없죠. 컴포넌트에서 useGetMetricsQuery(undefined, { pollingInterval: 3000, skip: document.hidden }) 처럼 한 줄만 추가해 보세요. 브라우저 Visibility API와 결합해 불필요한 네트워크 트래픽을 드라마틱하게 줄일 수 있습니다.


Honest Review & Trade-offs: 환상에서 벗어나기

자, 찬양은 이쯤 하고 시니어의 깐깐한 시선으로 진짜 한계를 짚어보겠습니다. 아무리 포장을 잘해도 RTK는 여전히 무겁고, 만능이 아닙니다.

1. 지독한 블랙박스(Black-box) 현상 RTK Query의 자동화된 캐싱은 양날의 검입니다. providesTags 설정을 조금만 잘못 꼬아놓으면, 왜 특정 화면에서 API가 리페칭 안 되는지 원인을 찾느라 밤을 새워야 합니다. 내부 캐시 라이프사이클이 철저히 추상화되어 있어서, 문제가 발생했을 때 Redux DevTools를 열어봐도 주니어 개발자들은 멘붕에 빠지기 십상입니다.

2. 벤더 락인(Vendor Lock-in) 리스크 Zustand와 React Query 조합은 클라이언트 상태와 서버 상태 라이브러리가 완전히 분리되어 있어 언제든 하나를 걷어낼 수 있습니다. 하지만 RTK Query를 도입하는 순간, 당신의 프로젝트는 뼛속까지 Redux 생태계에 종속됩니다. Redux Store 없이는 RTK Query가 단독으로 동작할 수 없으니까요. 이는 향후 마이크로 프론트엔드(MFA) 전환이나 점진적 마이그레이션 시 꽤 큰 골칫거리가 됩니다.

3. 러닝 커브와 번들 사이즈 문법이 줄었다고는 하나, Redux 특유의 Flux 패턴과 Immer의 동작 원리, RTK Query의 캐시 무효화 그래프 생태계를 완벽히 이해하는 데는 여전히 상당한 시간이 필요합니다. 게다가 번들 사이즈 측면에서도 가벼운 상태 관리 도구들에 비해 초기 로딩 최적화에서 페널티를 안고 시작해야 합니다.


Closing Thoughts: 결국, 우리는 무엇을 선택해야 하는가?

“그래서 Zustand 쓸까요, RTK 쓸까요?” 현업에서 가장 많이 듣는 질문입니다. 제 대답은 항상 같습니다.

만약 당신이 만들고 있는 프로덕트가 빠른 이터레이션이 필요한 스타트업의 서비스이거나 상태의 복잡도가 낮다면, 고민 없이 Zustand나 Jotai를 쓰십시오. 하지만 결제 시스템, 복잡한 B2B 어드민, 또는 수십 명의 프론트엔드 개발자가 동시에 코드를 만지는 ‘규격화된 톱니바퀴’ 같은 엔터프라이즈 환경이라면 이야기가 다릅니다.

RTK는 자유도를 억압합니다. 하지만 그 억압이 곧 ‘예측 가능성’이라는 거대한 안정성을 가져다줍니다. 개발자의 개인기를 배제하고, 아키텍처 자체가 강제하는 규율 속에서 안전하게 코드를 짜고 싶다면, 2026년 현재 RTK만큼 견고한 녀석은 아직 존재하지 않습니다. 기술의 트렌드는 돌고 돕니다. 보일러플레이트의 지옥에서 살아 돌아온 Redux의 이 독기 어린 진화는, 우리가 상태 관리의 본질이 무엇인지 다시 한번 생각하게 만듭니다.

References

  • https://redux-toolkit.js.org/
  • https://github.com/immerjs/immer
  • https://github.com/reduxjs/redux-toolkit/discussions
This post is licensed under CC BY 4.0 by the author.