TypeScript로 Redux 타입 안전하게 마이그레이션하기

배경

3년차 React 프로젝트의 Redux 코드베이스를 TypeScript로 전환하는 작업을 시작했다. 한 번에 전체를 마이그레이션할 수 없어서 점진적으로 진행하는 방식을 택했다.

Action 타입 정의

가장 먼저 Action Creator에 타입을 적용했다. string literal type을 활용해 action type을 정의했다.

// actionTypes.ts
export const FETCH_USER_REQUEST = 'FETCH_USER_REQUEST' as const;
export const FETCH_USER_SUCCESS = 'FETCH_USER_SUCCESS' as const;
export const FETCH_USER_FAILURE = 'FETCH_USER_FAILURE' as const;

// actions.ts
interface FetchUserRequestAction {
  type: typeof FETCH_USER_REQUEST;
}

interface FetchUserSuccessAction {
  type: typeof FETCH_USER_SUCCESS;
  payload: User;
}

type UserAction = FetchUserRequestAction | FetchUserSuccessAction | FetchUserFailureAction;

Reducer 타입 적용

Reducer의 state 타입을 먼저 정의하고, discriminated union을 활용해 타입 가드를 구현했다.

interface UserState {
  loading: boolean;
  data: User | null;
  error: string | null;
}

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

function userReducer(state = initialState, action: UserAction): UserState {
  switch (action.type) {
    case FETCH_USER_SUCCESS:
      // action.payload가 User 타입으로 추론됨
      return { ...state, loading: false, data: action.payload };
    default:
      return state;
  }
}

마주친 문제들

  1. 기존 JS 파일과의 호환: .ts.js 파일이 공존하면서 타입 체크가 제대로 되지 않는 부분이 있었다. allowJscheckJs 옵션을 활용해 점진적으로 전환했다.

  2. Redux의 타입 추론 한계: connect의 타입 추론이 복잡해서 mapStateToPropsmapDispatchToProps에 명시적으로 타입을 작성해야 했다.

  3. Thunk 타입 정의: redux-thunk의 타입 정의가 복잡했다. ThunkAction 타입을 제대로 이해하는 데 시간이 걸렸다.

결과

약 2주간 진행하면서 전체 Redux 코드의 40% 정도를 TypeScript로 전환했다. 컴파일 타임에 오류를 잡을 수 있게 되면서 런타임 버그가 확실히 줄어들었다. 특히 action payload의 타입 불일치로 인한 버그를 사전에 발견할 수 있었던 게 가장 큰 성과였다.

TypeScript로 Redux 타입 안전하게 마이그레이션하기