내용이 안 보인다면 쿠키/캐시를 지우고 새로고침 하세요!
이 블로그는 광고 클릭 수익으로 운영됩니다!
괜찮으시다면 광고 차단을 풀어주세요 ㅠㅠ

게시글

강좌4 - TypeScript - 한 달 전 등록

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

조회수:
0
이 블로그는 광고 클릭 수익으로 운영됩니다!
괜찮으시다면 광고 차단을 풀어주세요 ㅠㅠ
이 블로그는 광고 클릭 수익으로 운영됩니다!
괜찮으시다면 광고 차단을 풀어주세요 ㅠㅠ

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

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

export type CHECK_GUESTBOOK = 'CHECK_GUESTBOOK';
export const CHECK_GUESTBOOK: CHECK_GUESTBOOK = 'CHECK_GUESTBOOK';

export type CHECK_GUESTBOOK_REQUEST = 'CHECK_GUESTBOOK_REQUEST';
export const CHECK_GUESTBOOK_REQUEST: CHECK_GUESTBOOK_REQUEST = 'CHECK_GUESTBOOK_REQUEST';

export type CHECK_GUESTBOOK_SUCCESS = 'CHECK_GUESTBOOK_SUCCESS';
export const CHECK_GUESTBOOK_SUCCESS: CHECK_GUESTBOOK_SUCCESS = 'CHECK_GUESTBOOK_SUCCESS';

export type CHECK_GUESTBOOK_FAIL = 'CHECK_GUESTBOOK_FAIL';
export const CHECK_GUESTBOOK_FAIL: CHECK_GUESTBOOK_FAIL = 'CHECK_GUESTBOOK_FAIL';

export type CHECK_COMMENTS = 'CHECK_COMMENTS';
export const CHECK_COMMENTS: CHECK_COMMENTS = 'CHECK_COMMENTS';

export type CHECK_COMMENTS_REQUEST = 'CHECK_COMMENTS_REQUEST';
export const CHECK_COMMENTS_REQUEST: CHECK_COMMENTS_REQUEST = 'CHECK_COMMENTS_REQUEST';

export type CHECK_COMMENTS_SUCCESS = 'CHECK_COMMENTS_SUCCESS';
export const CHECK_COMMENTS_SUCCESS: CHECK_COMMENTS_SUCCESS = 'CHECK_COMMENTS_SUCCESS';

export type CHECK_COMMENTS_FAIL = 'CHECK_COMMENTS_FAIL';
export const CHECK_COMMENTS_FAIL: CHECK_COMMENTS_FAIL = 'CHECK_COMMENTS_FAIL';

일단 타입의 이름을 이렇게 정의해줍니다. 타입과 변수를 중복해서 만들었습니다. 이게 무슨 짓인가 싶지만, 나중에 리듀서 안의 switch 문에서 타입 가드를 적용하려면 타입을 일일이 만들어줘야 합니다. 타입 가드는 뒤에 리듀서에서 설명합니다.

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

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

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

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

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

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

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

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

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

import { Action } from 'redux';

interface ILoadGuestBookAction extends Action<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입니다).

투표로 게시글에 관해 피드백을 해주시면 많은 도움이 됩니다. 오류가 있다면 어떤 부분에 오류가 있는지도 알려주세요! 잘못된 정보가 퍼져나가지 않도록 도와주세요.
Copyright © 2016- 무단 전재 및 재배포 금지

댓글

2개의 댓글이 있습니다.
한 달 전
수정하는 중에 답글이 달렸다 ㄷㄷㄷ
한 달 전
하도 고생해서 눈물이 나신 거에요? 웹프로그래밍 경험 하나도 없는데 리액트 공부한답시고 책보고 있는데.. 자바스크립트에 함수형 프로그래밍 너무 어색해요. 타입스크립트가 끌리네요.
한 달 전
코드 양이 너무 많아져서요 ㅠㅠ