게시글

5만명이 선택한 평균 별점 4.9의 제로초 프로그래밍 강좌! 로드맵만 따라오면 됩니다! 클릭
강좌4 - TypeScript - 6년 전 등록 / 4년 전 수정

블로그 타입스크립트 전환 후기 - 리덕스

2020.11.07 최신 요약 - redux toolkit 쓰시면 타입스크립트 한 방에 해결됩니다.

------------------------------------------------------------------------------------------------

안녕하세요. 이번 시간에는 리덕스를 타입스크립트로 전환한 후기입니다. 리덕스는 액션이 많은 만큼 상당히 코드량이 길어지더군요.

코드가 너무 기니까 두 개 액션만 예시를 들어보겠습니다. 비동기 액션이기 때문에 한 액션 당 3~4개의 타입이 들어갑니다(기본, 요청, 성공, 실패). 각각 방명록과 댓글을 가져오는 비동기 액션입니다.

export const CHECK_GUESTBOOK = 'CHECK_GUESTBOOK';
export const CHECK_GUESTBOOK_REQUEST = 'CHECK_GUESTBOOK_REQUEST';
export const CHECK_GUESTBOOK_SUCCESS = 'CHECK_GUESTBOOK_SUCCESS';
export const CHECK_GUESTBOOK_FAIL = 'CHECK_GUESTBOOK_FAIL';

export const CHECK_COMMENTS = 'CHECK_COMMENTS';
export const CHECK_COMMENTS_REQUEST = 'CHECK_COMMENTS_REQUEST';
export const CHECK_COMMENTS_SUCCESS = 'CHECK_COMMENTS_SUCCESS';
export const CHECK_COMMENTS_FAIL = 'CHECK_COMMENTS_FAIL';

typescript는 const를 이해하므로(typeof CHECK_GUESTBOOK은 string이 아니라 'CHECK_GUESTBOOK'입니다) 액션 이름을 그대로 쓰셔도 됩니다.

export를 하는 이유는 타입이나 인터페이스, 그리고 액션 이름은 앱 전체에 자주 쓰이기 때문입니다.

export interface ILoadCommentsAction {
  type: typeof CHECK_COMMENTS;
  promise: (client: IApiClient) => PromiseLike<void>;
}

export function loadComments(id: string, limit: number | 'undefined') { 
  return {
    type: typeof CHECK_COMMENTS,
    promise: (client: IApiClient) => client.get(`/comment/${id}?limit=${limit}`),
  };
}

export interface ILoadCommentsRequestAction {
  type: typeof CHECK_COMMENTS_REQUEST;
}
export interface ILoadCommentsSuccessAction {
  type: typeof CHECK_COMMENTS_SUCCESS;
  result: IComment[];
}
export interface ILoadCommentsFailureAction {
  type: typeof CHECK_COMMENTS_FAIL;
  error: Error;
}

export interface ILoadGuestBookAction {
  type: typeof CHECK_GUESTBOOK;
  promise: (client: IApiClient) => PromiseLike<void>;
}
export function loadGuestBook(): ILoadGuestBookAction {
  return {
    type: typeof CHECK_GUESTBOOK,
    promise: (client: IApiClient) => client.get(`/comment/${GUESTBOOK_ID}`),
  };
}

export interface ILoadGuestBookRequestAction {
  type: typeof CHECK_GUESTBOOK_REQUEST;
}
export interface ILoadGuestBookSuccessAction {
  type: typeof CHECK_GUESTBOOK_SUCCESS;
  result: IComment[];
}
export interface ILoadGuestBookFailureAction {
  type: typeof CHECK_GUESTBOOK_FAIL;
  error: Error;
}

후... 정말 깁니다. 각 액션마다 인터페이스를 만들어서 액션 안에 어떤 타입이 들어갈 지 적어야합니다. 참고로 저는 redux-thunk나 redux-saga같은 미들웨어를 쓰지 않고 제가 미들웨어를 만들어서 쓰기 때문에 액션 안에 promise라는 속성이 들어 있는 것입니다. 이것은 여러분의 액션에 맞게 수정하시면 됩니다.

또한 저는 CHECK_COMMENT를 하는 순간 REQUEST, SUCCESS, FAIL는 리덕스 미들웨어에서 자동으로 생성해주기 때문에 실제 액션 객체가 없이 인터페이스만 있습니다. 여러분의 미들웨어 선택에 따라 실제 액션 객체가 필요할 수 있습니다(코드가 더 길어질 수 있다는 얘기! ㅎㅎ)

액션 인터페이스는 다음과 같이 액션을 리덕스로부터 상속받아 쓸 수도 있습니다. 나중에 모든 코드를 아래처럼 바꾸었습니다.

import { Action } from 'redux';

interface ILoadGuestBookAction extends Action<typeof CHECK_GUESTBOOK> {
  promise: (client: IApiClient) => PromiseLike<void>;
}

다음은 나머지 인터페이스들입니다. 액션 인터페이스에서 쓰였던 IComment와, 리듀서의 defaultState와 그 인터페이스인 ICommentsReducerState, 그리고 위의 액션들을 모두 묶은 타입인 TCommentsReducerAction입니다.

export interface IWriter {
  emailVerified: boolean;
  point: number;
  medals: 0 | 1 | 2 | 3 | 5;
  isSubscribed: boolean;
  _id: string;
  email: string;
  displayName: string;
}

export interface IComment {
  parents: string[];
  _id: string;
  writer?: IWriter;
  content: string;
  email?: string;
  password?: string;
  postId: string;
  category: string;
  createdAt: string;
  updatedAt: string;
  __v: number;
}

export interface ICommentsReducerState {
  fetchingCommentUpdate: boolean;
  fetchingGuestbook: boolean;
  latest: IComment[];
  comments: IComment[];
  guestbook: IComment[];
  message: null;
  error: null | Error;
}

export const defaultStartState = {
  fetchingCommentUpdate: false,
  fetchingGuestbook: false,
  latest: [],
  comments: [],
  guestbook: [],
  message: null,
  error: null,
};

type TCommentsReducerAction =
  ILoadGuestBookAction | ILoadGuestBookRequestAction | ILoadGuestBookSuccessAction | ILoadGuestBookFailureAction |
  ILoadCommentsAction | ILoadCommentsRequestAction | ILoadCommentsSuccessAction | ILoadCommentsFailureAction;

마지막으로 리듀서입니다. 리듀서의 매개변수에 인터페이스와 타입을 심어줍시다.

export default (state: ICommentsReducerState = defaultStartState, action: TCommentsReducerAction) => {
  switch (action.type) {
    case CHECK_COMMENTS_REQUEST:
    case CHECK_GUESTBOOK_REQUEST:
      return {
        ...state,
        fetchingGuestbook: true,
        message: null,
        error: null,
      };
    case CHECK_COMMENTS_SUCCESS:
      if (action.result) {
        return {
          ...state,
          fetchingCommentUpdate: false,
          comments: action.result,
        };
      }
      return { ...state };
    case CHECK_GUESTBOOK_SUCCESS:
      if (action.result) {
        return {
          ...state,
          fetchingGuestbook: false,
          guestbook: action.result,
        };
      }
      return { ...state };
    case CHECK_COMMENTS_FAIL:
      return {
        ...state,
        fetchingCommentUpdate: false,
        error: action.error,
      };
    case CHECK_GUESTBOOK_FAIL:
      return {
        ...state,
        fetchingGuestbook: false,
        error: action.error,
      };
    default:
      return state;
  }
};

action에 TCommentsReducerAction이 들어갔기 때문에 switch (action.type)에서 타입스크립트가 action.type의 타입에 따라 action이 어떤 인터페이스에 속해 있는지 파악해줍니다. 처음에 액션의 타입마다 모두 type을 선언했던 덕을 이제 보고 있는 것입니다. 이렇게 타입스크립트가 if나 switch같은 분기문에 따라 타입을 찾아주는 기능을 타입 가드(type guard)라고 부릅니다.

컴포넌트에서는 mapStateToProps나 mapDispatchToProps 같은 것에 이 타입과 인터페이스들을 가져와서 쓰면 됩니다.

// Comments 컴포넌트는 생략

interface IStateToProps {
  commentsReducer: ICommentsReducerState;
}

const mapStateToProps = (state: IReducerState) => ({
  commentsReducer: state.commentsReducer,
});

interface IDispatchToProps {
  checkComments: () => void;
}

const mapDispatchToProps = (dispatch: Dispatch) => ({
  checkComments() {
    return dispatch({ type: CHECK_COMMENTS });
  },  
});

export default connect(mapStateToProps, mapDispatchProps)(Comments);

인터페이스와 타입 정의 때문에 코드가 매우 길어졌습니다. 이렇게 타입 정의를 해놓고 tsc를 돌려보면 코드에서 많은 에러가 나게 됩니다. 특히 잠재적으로 발생할 수 있는 런타임 에러를 많이 잡아줍니다. 저도 미처 몰랐던 부분이 많아 깜짝 놀랐습니다. 덕분에 제 블로그는 한 층 더 견고해졌습니다(근데 왜 눈물이 나는지 모르겠습니다 ㅠㅠ 타입스크립트는 타입 정의 시간과 소프트웨어 견고성의 trade-off입니다).

조회수:
0
목록
투표로 게시글에 관해 피드백을 해주시면 게시글 수정 시 반영됩니다. 오류가 있다면 어떤 부분에 오류가 있는지도 알려주세요! 잘못된 정보가 퍼져나가지 않도록 도와주세요.
Copyright 2016- . 무단 전재 및 재배포 금지. 출처 표기 시 인용 가능.
5만명이 선택한 평균 별점 4.9의 제로초 프로그래밍 강좌! 로드맵만 따라오면 됩니다! 클릭

댓글

3개의 댓글이 있습니다.
5년 전
액션 타입쪽에 질문이 있는데요~ 타입스크립트 3.4 이상 이용할 때는 const assertions 이용하면 되지 않나요??
일부러 하위호환성을 위해서 타입 선언을 개별로 해주신건가요~?
5년 전
이 글이 언제 쓰였는지를 보시면 될 것 같습니다.
5년 전
앗 그러네요 민망하네요...ㅋㅋㅋ;;;
6년 전
수정하는 중에 답글이 달렸다 ㄷㄷㄷ
6년 전
하도 고생해서 눈물이 나신 거에요? 웹프로그래밍 경험 하나도 없는데 리액트 공부한답시고 책보고 있는데.. 자바스크립트에 함수형 프로그래밍 너무 어색해요. 타입스크립트가 끌리네요.
6년 전
코드 양이 너무 많아져서요 ㅠㅠ