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;
}
}
마주친 문제들
-
기존 JS 파일과의 호환:
.ts와.js파일이 공존하면서 타입 체크가 제대로 되지 않는 부분이 있었다.allowJs와checkJs옵션을 활용해 점진적으로 전환했다. -
Redux의 타입 추론 한계:
connect의 타입 추론이 복잡해서mapStateToProps와mapDispatchToProps에 명시적으로 타입을 작성해야 했다. -
Thunk 타입 정의:
redux-thunk의 타입 정의가 복잡했다.ThunkAction타입을 제대로 이해하는 데 시간이 걸렸다.
결과
약 2주간 진행하면서 전체 Redux 코드의 40% 정도를 TypeScript로 전환했다. 컴파일 타임에 오류를 잡을 수 있게 되면서 런타임 버그가 확실히 줄어들었다. 특히 action payload의 타입 불일치로 인한 버그를 사전에 발견할 수 있었던 게 가장 큰 성과였다.