[TYPSCRIPT] Redux / Redux Toolkit(RTK) + TypeScript 설정하기 -전역 상태를 타입으로 통제하는 실무 기준

 

Redux는 여전히 대규모 React 애플리케이션에서 중요한 역할을 합니다. 다만 순수 Redux + TypeScript 조합은 설정과 보일러플레이트가 많아 실무에서 피로도가 큽니다. 이 문제를 해결하기 위해 등장한 것이 Redux Toolkit(RTK)이며, RTK는 TypeScript와 함께 사용할 때 가장 큰 효과를 발휘합니다.

 

 

Redux Toolkit + TypeScript를 쓰는 이유

RTK를 쓰면 다음 문제들이 한 번에 해결됩니다.

  • 액션 타입 문자열 관리 불필요
  • 불변성 코드 작성 부담 감소 (Immer 내장)
  • 타입 추론이 잘 동작하는 slice 구조
  • 비동기 로직 표준화(createAsyncThunk)

결론적으로 RTK + TypeScript는 현재 Redux의 사실상 표준 조합입니다.

 

패키지 설치

React 프로젝트(Vite 기준)에 필요한 패키지를 설치합니다.

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

RTK는 TypeScript 타입을 자체 포함하고 있으므로 별도의 Redux 타입 패키지는 필요하지 않습니다.

 

기본 폴더 구조 (실무 권장)

Redux 관련 코드는 한 곳에 모아두는 것이 관리에 유리합니다.

src/
  store/
    index.ts
    hooks.ts
    userSlice.ts

 

Slice 작성하기 (상태 + 리듀서 + 타입)

RTK의 핵심은 createSlice입니다.

import { createSlice, PayloadAction } from "@reduxjs/toolkit";

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

type UserState = {
  user: User | null;
  loading: boolean;
};

const initialState: UserState = {
  user: null,
  loading: false,
};

const userSlice = createSlice({
  name: "user",
  initialState,
  reducers: {
    setUser(state, action: PayloadAction) {
      state.user = action.payload;
    },
    clearUser(state) {
      state.user = null;
    },
  },
});

export const { setUser, clearUser } = userSlice.actions;
export default userSlice.reducer;

핵심 포인트:

  • State 타입을 먼저 정의
  • PayloadAction으로 액션 payload 타입 명시
  • Immer 덕분에 state 직접 변경 가능

 

Store 설정하기

모든 slice를 하나의 store로 묶습니다.

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

export const store = configureStore({
  reducer: {
    user: userReducer,
  },
});

 

RootState와 AppDispatch 타입 추출

이 단계가 TypeScript + Redux의 핵심입니다.

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

이 두 타입은 이후 전체 애플리케이션에서 재사용됩니다.

 

타입 안전한 커스텀 훅 만들기

react-redux 기본 훅 대신, 타입이 고정된 커스텀 훅을 만드는 것이 실무 표준입니다.

import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "./index";

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

이제 컴포넌트에서는 타입을 신경 쓸 필요가 없습니다.

 

React에 Store 연결하기

import { Provider } from "react-redux";
import { store } from "./store";

<Provider store={store}>
  <App />
</Provider>

 

컴포넌트에서 사용하기

function Profile() {
  const user = useAppSelector(state => state.user.user);
  const dispatch = useAppDispatch();

  if (!user) {
    return (
      <button onClick={() => dispatch(setUser({ id: 1, name: "Alice" }))}>
        Login
      </button>
    );
  }

  return (
    <div>
      {user.name}
      <button onClick={() => dispatch(clearUser())}>Logout</button>
    </div>
  );
}

state, dispatch 모두 타입이 자동으로 보장됩니다.

 

비동기 처리(createAsyncThunk)와 타입

RTK의 비동기 표준은 createAsyncThunk입니다.

import { createAsyncThunk } from "@reduxjs/toolkit";

export const fetchUser = createAsyncThunk<
  User,
  number
>("user/fetch", async (id) => {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
});

제네릭 의미:

  • 첫 번째: 성공 시 반환 타입
  • 두 번째: 인자 타입

 

extraReducers로 비동기 상태 처리

extraReducers: (builder) => {
  builder
    .addCase(fetchUser.pending, (state) => {
      state.loading = true;
    })
    .addCase(fetchUser.fulfilled, (state, action) => {
      state.loading = false;
      state.user = action.payload;
    })
    .addCase(fetchUser.rejected, (state) => {
      state.loading = false;
    });
}

비동기 액션의 각 단계도 타입 안전하게 처리됩니다.

 

실무에서 자주 하는 실수

  • RootState / AppDispatch 타입을 만들지 않음
  • useSelector / useDispatch를 직접 사용
  • Slice State 타입을 명확히 정의하지 않음
  • Redux에 UI 전용 상태까지 모두 넣음

Redux는 “전역 비즈니스 상태”에만 사용하는 것이 이상적입니다.

 

실무 권장 체크리스트

  • Redux Toolkit 사용
  • Slice 단위로 상태 분리
  • RootState / AppDispatch 타입 필수
  • 커스텀 훅으로 타입 은닉
  • 비동기는 createAsyncThunk 사용

 


 

 

Redux / RTK + TypeScript의 핵심은 “전역 상태를 타입 계약으로 고정하는 것”입니다. 설정만 제대로 해두면 이후 개발자는 상태 구조를 외울 필요 없이 타입에 따라 자연스럽게 코드를 작성할 수 있습니다.

  • RTK는 기본 선택
  • 타입은 중앙에서 정의
  • 컴포넌트는 타입을 의식하지 않게