[TYPESCRIPT] Redux Toolkit Query(RTK Query) + TypeScript - API 상태를 “표준 방식”으로 관리하는 실무 패턴

 

Redux를 쓰는 프로젝트에서 API 호출과 캐싱, 로딩/에러 상태 관리는 늘 골칫거리입니다. 직접 thunk를 만들고, 로딩 플래그를 관리하고, 캐시 무효화까지 처리하다 보면 코드가 금방 복잡해지고 일관성도 깨지기 쉽습니다.

이 문제를 구조적으로 해결하는 도구가 Redux Toolkit Query(RTK Query)입니다. RTK Query는 API 통신을 “Redux의 한 파트”로 보고, 데이터 패칭 + 캐싱 + 무효화 + 로딩/에러 상태를 표준화해 줍니다.

 

 

RTK Query를 쓰면 뭐가 달라지나?

  • API 호출 로직이 slice/thunk에서 분리되고 표준화
  • 캐싱, 자동 refetch, 중복 요청 방지
  • 쿼리/뮤테이션 분리로 의도가 명확
  • invalidateTags로 캐시 무효화 일관성 확보
  • TypeScript 타입이 자연스럽게 전파

특히 Redux를 이미 사용 중인 프로젝트라면 React Query를 도입하는 것보다 RTK Query가 더 자연스럽게 맞는 경우가 많습니다.

 

설치

RTK Query는 Redux Toolkit에 포함되어 있습니다. 이미 RTK를 설치했다면 추가 설치가 필요 없는 경우가 많습니다.

npm install @reduxjs/toolkit react-redux
npm install -D @types/react-redux

기존 Redux/RTK 설정이 되어있다면 그대로 이어서 진행하시면 됩니다.

 

API 타입부터 정의하기 (실무 핵심)

RTK Query를 TypeScript로 쓸 때 가장 중요한 원칙은 “API 계약 타입부터 고정”하는 것입니다.

export type User = {
  id: number;
  name: string;
};

export type ApiResponse<T> =
  | { ok: true; data: T }
  | { ok: false; errorCode: string; message: string };

프로젝트 전체에서 응답 형식을 통일해두면 쿼리/뮤테이션 모두 예측 가능한 패턴으로 유지됩니다.

 

createApi로 API 모듈 만들기

RTK Query의 핵심은 createApi입니다.

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import type { ApiResponse, User } from "../types";

export const api = createApi({
  reducerPath: "api",
  baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
  tagTypes: ["User"],
  endpoints: (builder) => ({
    getUser: builder.query<ApiResponse<User>, number>({
      query: (id) => `/users/${id}`,
      providesTags: (result, error, id) => [{ type: "User", id }],
    }),
  }),
});

여기서 중요한 부분:

  • builder.query<응답타입, 파라미터타입>
  • tagTypesprovidesTags로 캐싱 단위를 정의

 

Store에 RTK Query 리듀서/미들웨어 등록

RTK Query는 store에 reducer와 middleware 등록이 필요합니다.

import { configureStore } from "@reduxjs/toolkit";
import { api } from "./api";

export const store = configureStore({
  reducer: {
    [api.reducerPath]: api.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(api.middleware),
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

이 설정이 누락되면 RTK Query가 정상 동작하지 않습니다.

 

자동 생성 훅 사용하기

RTK Query는 endpoint를 정의하면 React 훅을 자동으로 생성합니다.

export const { useGetUserQuery } = api;
function Profile({ userId }: { userId: number }) {
  const { data, isLoading, error } = useGetUserQuery(userId);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error</div>;

  if (!data) return null;

  if (!data.ok) {
    return <div>{data.message}</div>;
  }

  return <div>{data.data.name}</div>;
}

응답 타입이 고정되어 있으니, 컴포넌트는 “무슨 데이터가 올지”를 추측할 필요가 없습니다.

 

Mutation(변경) 정의하기

조회는 query, 변경은 mutation으로 정의합니다.

type UpdateUserRequest = {
  id: number;
  name: string;
};

export const api = createApi({
  reducerPath: "api",
  baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
  tagTypes: ["User"],
  endpoints: (builder) => ({
    updateUser: builder.mutation<ApiResponse<User>, UpdateUserRequest>({
      query: (body) => ({
        url: `/users/${body.id}`,
        method: "PUT",
        body,
      }),
      invalidatesTags: (result, error, body) => [{ type: "User", id: body.id }],
    }),
  }),
});

핵심 포인트:

  • builder.mutation<응답타입, 요청타입>
  • invalidatesTags로 관련 캐시 자동 무효화

 

Mutation 사용하기

export const { useUpdateUserMutation } = api;

function EditName({ userId }: { userId: number }) {
  const [updateUser, { isLoading }] = useUpdateUserMutation();

  const onClick = async () => {
    const res = await updateUser({ id: userId, name: "New Name" }).unwrap();

    if (!res.ok) {
      alert(res.message);
      return;
    }
    alert("Updated");
  };

  return (
    <button onClick={onClick} disabled={isLoading}>
      Save
    </button>
  );
}

unwrap()을 쓰면 Promise 결과로 응답을 직접 받을 수 있어 try/catch 흐름이 깔끔해집니다.

 

캐싱/무효화(tag) 설계 팁

RTK Query에서 성능과 일관성을 결정하는 것은 tag 설계입니다.

  • 리소스 단위로 tagTypes 정의 (User, Post, Order 등)
  • 조회 query는 providesTags
  • 변경 mutation은 invalidatesTags
  • id 기반으로 세분화하면 불필요한 refetch 감소

tag 설계가 잘 되면 API 상태 관리는 “거의 자동”이 됩니다.

 

실무에서 자주 하는 실수

  • store에 api.middleware 등록을 빼먹음
  • 응답 타입을 대충 any로 두고 시작
  • tagTypes/invalidatesTags 설계를 안 해서 캐시가 꼬임
  • query와 mutation을 thunk처럼 섞어 사용

RTK Query는 “규칙을 지킬수록 편해지는 도구”입니다.

 

실무 권장 체크리스트

  • 응답 형식(ApiResponse<T>)을 프로젝트 표준으로 통일
  • createApi는 도메인 단위로 분리 가능(사용자/주문/결제 등)
  • tagTypes는 리소스 단위로 설계
  • query는 providesTags, mutation은 invalidatesTags
  • 컴포넌트는 자동 생성 훅만 사용

 


 

 

RTK Query + TypeScript 조합은 “API 상태 관리의 표준화”를 목표로 합니다. 설정을 한 번 제대로 해두면, 이후부터는 API 호출이 단순히 “훅을 호출하는 행위”가 되고 로딩/에러/캐싱/무효화는 프레임워크가 책임지게 됩니다.

  • 응답 타입부터 고정
  • query/mutation을 명확히 분리
  • tag 기반 캐시 무효화로 일관성 유지