24. RTK Query - 데이터 페칭의 혁명
10편에서 “서버 상태는 클라이언트 상태와 성격이 다르다"고 하며 전용 도구의 필요성을 언급했습니다. RTK Query가 그 공식 해답입니다. 이 편은 19편의 createAsyncThunk 방식과 RTK Query를 나란히 비교하며, 캐싱·자동 재요청·뮤테이션이 어떻게 선언적으로 처리되는지 다룹니다.
학습 목표
createApi로 API 엔드포인트를 정의하고, 자동 생성된 Hook으로 데이터를 조회할 수 있다.- RTK Query가 캐싱과 로딩 상태를 자동으로 관리하는 원리를 설명할 수 있다.
tagTypes를 이용한 캐시 무효화로, 뮤테이션 후 관련 데이터를 자동 재요청할 수 있다.
19편 복습: createAsyncThunk로 만든 수동 캐싱
19~20편에서 fetchTodos를 createAsyncThunk로 만들고, status 필드로 로딩 상태를 직접 관리했습니다.
| |
이 모든 것이 틀린 방식은 아니지만, “서버 데이터를 가져오고, 캐시하고, 필요할 때 갱신한다"는 패턴은 프로젝트마다 반복해서 나타납니다. RTK Query는 이 반복을 표준화합니다.
createApi: 엔드포인트를 선언적으로 정의한다
| |
endpoints에 정의한 getTodos, addTodo로부터 useGetTodosQuery, useAddTodoMutation이라는 React Hook이 자동으로 생성됩니다. 이 명명 규칙(use + 엔드포인트 이름 + Query/Mutation)은 RTK Query가 항상 동일하게 따릅니다.
컴포넌트에서 사용하기: 19~20편과의 비교
| |
20편의 TodoList.jsx와 비교하면, useEffect로 마운트 시점에 fetchTodos()를 dispatch하는 코드, status를 직접 확인하는 조건문, 상태 구조(items/status/error)를 설계하는 과정이 모두 사라졌습니다. useGetTodosQuery() Hook 하나가 요청 시점 결정, 캐싱, 로딩/에러 상태를 전부 처리합니다.
캐싱이 자동으로 일어난다는 것의 의미
같은 쿼리를 여러 컴포넌트에서 호출해도, RTK Query는 네트워크 요청을 한 번만 보내고 결과를 공유합니다.
| |
TodoList와 TodoCount가 동시에 마운트되어도 /api/todos 요청은 한 번만 발생합니다. 22편에서 thunk로 이 최적화를 하려면 getState로 “이미 요청 중인지” 확인하는 가드를 직접 작성해야 했지만, RTK Query는 이를 기본 동작으로 제공합니다.
invalidatesTags/providesTags: 뮤테이션 후 자동 재요청
addTodo("새 할 일")가 성공하면, todosApi는 invalidatesTags: ["Todo"] 설정에 따라 "Todo" 태그를 가진 모든 쿼리(getTodos)를 자동으로 무효화하고 다시 요청합니다.
sequenceDiagram
participant UI as 컴포넌트
participant RTK as RTK Query
participant Server as 서버
UI->>RTK: addTodo("새 할 일") 호출
RTK->>Server: POST /api/todos
Server-->>RTK: 201 Created
RTK->>RTK: invalidatesTags: ["Todo"] 감지
RTK->>Server: GET /api/todos (자동 재요청)
Server-->>RTK: 최신 목록
RTK-->>UI: useGetTodosQuery()가 새 데이터로 자동 리렌더
20편에서는 Todo를 추가한 뒤 화면을 갱신하려면, todoAdded 리듀서가 로컬 상태를 직접 업데이트하거나 fetchTodos()를 수동으로 재dispatch해야 했습니다. RTK Query는 **“이 데이터를 바꾸는 작업이 끝나면, 그 데이터에 의존하는 화면은 자동으로 최신 상태가 된다”**는 규칙을 태그 시스템으로 선언적으로 표현합니다.
폴링과 자동 재요청
RTK Query는 캐싱 외에도 실무에서 자주 필요한 기능을 옵션으로 제공합니다.
| |
캐시 수명과 조건부 요청: keepUnusedDataFor, skip, useLazyQuery
캐시가 언제까지 살아있는지, 그리고 항상 즉시 요청을 보내야 하는지도 상황에 따라 조정이 필요합니다.
| |
컴포넌트가 언마운트되어도 keepUnusedDataFor에 지정한 시간 동안은 캐시가 남아있어서, 사용자가 화면을 잠깐 벗어났다 돌아와도 재요청 없이 즉시 이전 데이터를 보여줄 수 있습니다. 조건에 따라 요청 자체를 보내지 않아야 할 때는 skip 옵션을 씁니다.
| |
지금까지 본 useGetTodosQuery()는 컴포넌트가 마운트되자마자 자동으로 요청을 보냅니다. 반면 “버튼을 클릭해야만” 요청을 보내고 싶다면 useLazyQuery 버전을 씁니다.
| |
useLazyGetTodosQuery는 데이터를 담은 결과 대신 trigger 함수를 반환하고, 이 함수를 호출한 시점에만 실제로 요청이 나갑니다.
RTK Query와 React Query/SWR의 관계
RTK Query는 아이디어를 처음부터 새로 고안한 것이 아니라, 이미 널리 쓰이던 **React Query(TanStack Query)**와 SWR 같은 서버 상태 전용 라이브러리의 설계(캐싱, 자동 재요청, stale-while-revalidate 전략)에서 직접 영향을 받아 만들어졌습니다. “이미 React Query가 있는데 왜 RTK Query를 쓰는가"라는 질문에 대한 답은 생태계 통합에 있습니다.
| 구분 | RTK Query | React Query / SWR |
|---|---|---|
| 상태 저장 위치 | 기존 Redux Store 안(같은 DevTools로 관찰 가능) | 별도의 자체 캐시 저장소 |
| 클라이언트 상태와의 통합 | 같은 Store 안에서 useSelector로 함께 조합하기 쉬움 | 별도 라이브러리라 조합에 추가 작업 필요 |
| 이미 Redux를 쓰는 프로젝트 | 새 의존성 없이 바로 도입 가능 | 상태 관리 계층이 두 개로 나뉨 |
| Redux를 쓰지 않는 프로젝트 | Redux 전체를 들여와야 해 과함 | 가볍게 바로 도입 가능 |
이미 Redux(Toolkit)를 쓰고 있고 클라이언트 상태와 서버 상태를 같은 DevTools·같은 Store 안에서 함께 다루고 싶다면 RTK Query가 자연스러운 선택이고, Redux를 쓰지 않는(또는 쓸 계획이 없는) 프로젝트라면 React Query나 SWR을 단독으로 쓰는 것이 더 가볍습니다.
RTK Query vs Thunk/Saga: 언제 무엇을 쓰는가
| 상황 | 적합한 도구 |
|---|---|
| 서버 데이터를 조회·캐싱·자동 갱신해야 함(대부분의 CRUD) | RTK Query(24편) |
| 단순한 비동기 로직, 커스텀 흐름 제어가 필요함 | Thunk(22편) |
| 여러 비동기 작업의 복잡한 순서 제어·요청 취소가 핵심임 | Saga(23편) |
세 가지는 서로 배타적이지 않습니다. 실무 프로젝트에서는 **서버 데이터 페칭은 RTK Query, 그 외의 클라이언트 로직(폼 검증 흐름, 복잡한 UI 상태 전이)은 thunk나 일반 createSlice**로 나눠 쓰는 조합이 흔합니다. 21편에서 확인했듯 RTK Query도 내부적으로는 미들웨어 위에서 동작하므로, configureStore에 todosApi.middleware를 추가해야 합니다.
| |
실무 체크리스트
- 서버 데이터를 다루는 코드에서
createAsyncThunk+ 수동status관리를 반복하고 있다면, RTK Query로 전환할 여지가 있는지 검토했는가? - 뮤테이션 후 관련 쿼리가 자동으로 최신화되도록
tagTypes/providesTags/invalidatesTags를 올바르게 연결했는가? - 실시간성이 필요한 데이터에 폴링(
pollingInterval)이나 포커스 재요청(refetchOnFocus)을 고려했는가? - 조건에 따라 요청을 건너뛰어야 하는 곳에
skip을, 사용자 액션에만 반응해야 하는 곳에useLazyQuery를 쓰고 있는가? - Redux를 쓰지 않는 프로젝트인데도 관성적으로 RTK Query를 도입하려 하지 않는가(그런 경우 React Query/SWR이 더 가벼울 수 있다)?
연습 과제
기초(★☆☆)
getTodos,addTodo엔드포인트를 가진todosApi를 만들고,useGetTodosQuery로 목록을 렌더링해보세요.
중급(★★☆)
deleteTodo뮤테이션을 추가하고invalidatesTags: ["Todo"]를 설정해, 삭제 후 목록이 자동으로 갱신되는지 확인해보세요.
고급(★★★)
getTodoById(id)쿼리를 추가하고, 특정 Todo 하나만 무효화하는 세밀한 태그({ type: "Todo", id }) 구조로 리팩터링해보세요.useLazyGetTodosQuery로 “검색” 버튼을 눌렀을 때만 요청이 나가는 컴포넌트를 만들고,skip옵션으로 특정 조건에서useGetTodoByIdQuery가 아예 요청을 보내지 않는지 확인해보세요.
요약
- RTK Query는 서버 상태의 조회·캐싱·로딩 상태 관리·자동 갱신을
createApi선언 하나로 표준화한다. - 같은 쿼리를 여러 컴포넌트가 호출해도 네트워크 요청은 한 번만 발생하고 결과가 공유된다.
tagTypes/providesTags/invalidatesTags로 뮤테이션 후 관련 쿼리의 자동 재요청을 선언적으로 연결하고,keepUnusedDataFor/skip/useLazyQuery로 캐시 수명과 요청 시점을 세밀하게 제어한다.- RTK Query는 React Query/SWR의 설계에서 영향을 받았으며, 이미 Redux를 쓰는 프로젝트에서는 같은 Store·DevTools로 통합된다는 점이 차별점이다.
참고 문헌 및 출처(추천)
- Redux Toolkit 공식 문서, “RTK Query Overview”
- Redux Toolkit 공식 문서, “RTK Query, Automated Re-fetching”
- Redux 공식 문서, “Redux Essentials, Part 7: RTK Query Basics”
- Redux Toolkit 공식 문서, “RTK Query vs React Query” — 비교 관점의 공식 안내
![Featured image of post [Redux] 24. RTK Query - 데이터 페칭의 혁명](/post/redux/rtk-query/wordcloud_hu_96f1bd1a325ea372.webp)
![[Redux] 22. Redux Thunk - 가장 간단한 비동기 처리](/post/redux/redux-thunk/wordcloud_hu_b4cddaa0144503a8.webp)
![[Redux] 23. Redux Saga - 강력한 사이드 이펙트 관리](/post/redux/redux-saga/wordcloud_hu_61e6fe9d8596c675.webp)
![[Redux] 24. RTK Query - 데이터 페칭의 혁명](/post/redux/rtk-query/wordcloud_hu_1807a958ecd17a19.webp)
![[Redux] 25. 실습: RTK Query로 블로그 앱 만들기](/post/redux/practice-blog-app/wordcloud_hu_1c4aa9e1677f5dbb.webp)
![[Redux] 26. Redux 프로젝트 구조 - 확장 가능한 설계](/post/redux/redux-project-structure/wordcloud_hu_379134b467bc38d9.webp)
![[Redux] 19. createAsyncThunk - 비동기 로직 단순화](/post/redux/create-async-thunk/wordcloud_hu_37ab180eb6db5e9b.webp)
![[Redux] 17. createSlice - 간결한 리듀서 작성](/post/redux/create-slice/wordcloud_hu_835b90d6ba21615f.webp)
![[Redux] 20. 실습: RTK로 앱 리팩터링](/post/redux/practice-rtk-app/wordcloud_hu_97bfaa8516f2360e.webp)
![[Redux] 10. Redux를 사용하는 이유와 적절한 사용 시기](/post/redux/when-to-use-redux/wordcloud_hu_5206c8759804db52.webp)