Front-End

vsoghlv@naver.com

React.js 타입스크립트에서 redux Thunk 사용

깃 api 를 통해 특정 사용자의 정보를 받아오는 작업을 typescript 로 해보려고 한다.

깃헙 사용자 정보는 아래와 같이 요청하면 된다.

GET https://api.github.com/users/:username

먼저 axios 와 redux-thunk 를 설치하자.

yarn add axios redux-thunk

두 라이브러리다 공식적으로 typescript 를 지원한다. 그 후 thunk 를 적용해주자.

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { createStore, applyMiddleware } from "redux";
import rootReducer from "./modules";
import { Provider } from "react-redux";
import Thunk from "redux-thunk";

const store = createStore(rootReducer, applyMiddleware(Thunk));

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

reportWebVitals();

깃헙에서 유저정보를 요청하면 매우 많은 정보가 있는데 응답한 내용의 타입을 매우 쉽게 정리할 수 있는 도구가 있다.

quicktype 를 사용하면 된다.

좌측에 응답내용을 적어주면 아래와 같이 변환해준다.

export interface GithubProfile {
    login:               string;
    id:                  number;
    node_id:             string;
    avatar_url:          string;
    gravatar_id:         string;
    url:                 string;
    html_url:            string;
    followers_url:       string;
    following_url:       string;
    gists_url:           string;
    starred_url:         string;
    subscriptions_url:   string;
    organizations_url:   string;
    repos_url:           string;
    events_url:          string;
    received_events_url: string;
    type:                string;
    site_admin:          boolean;
    name:                null;
    company:             null;
    blog:                string;
    location:            null;
    email:               null;
    hireable:            null;
    bio:                 null;
    twitter_username:    null;
    public_repos:        number;
    public_gists:        number;
    followers:           number;
    following:           number;
    created_at:          Date;
    updated_at:          Date;
}

이제 위의 타입들을 이용해 데이터를 요청하는 api 요청 함수를 만들자

//api/github.ts
import axios from "axios";

export async function getUserPeofile(username: string) {
  //response 의 데이터가 GithubProfile 인 경우 get 뒤에서 제네릭으로 설정해주면 된다.
  const response = await axios.get<GithubProfile>(
    `https://api.github.com/users/${username}`
  );

  return response.data;
}

export type GithubProfile = {
  login: string;
  id: number;
  node_id: string;
  avatar_url: string;
  gravatar_id: string;
  url: string;
  html_url: string;
  followers_url: string;
  following_url: string;
  gists_url: string;
  starred_url: string;
  subscriptions_url: string;
  organizations_url: string;
  repos_url: string;
  events_url: string;
  received_events_url: string;
  type: string;
  site_admin: boolean;
  name: null;
  company: null;
  blog: string;
  location: null;
  email: null;
  hireable: null;
  bio: null;
  twitter_username: null;
  public_repos: number;
  public_gists: number;
  followers: number;
  following: number;
  created_at: Date;
  updated_at: Date;
};

받아오는 데이터의 타입을 지정할 경우 get 뒤에 제네릭으로 설정해주면 된다.

이제 깃헙에 관련된 리덕스 코드들을 작성하자.

액션 생성함수를 만들때 typesafe 의 createAsyncAction 을 사용해주면 더욱 쉽게 만들 수 있다. 파라미터로 각 액션타입들을 넣어주고 제너릭으로 각 타입들의 페이로드 타입들을 넣어주면 된다. 만약 없는 경우 undefined 를 넣어주면 된다.

//moudle/github/actions.ts
import { AxiosError } from "axios";
import { createAction, createAsyncAction } from "typesafe-actions";
import { GithubProfile } from "../../api/guthub";

//액션타입
export const GET_USER_PROFILE = "github/GET_USER_PROFILE";
export const GET_USER_PROFILE_SUCCESS = "github/GET_USER_PROFILE_SUCCESS";
export const GET_USER_PROFILE_ERROR = "github/GET_USER_PROFILE_ERROR";

//액션 생성함수
//AxiosError = axios 에서 에러객체를 담는 타입
export const getUserProfileAsync = createAsyncAction(
  GET_USER_PROFILE,
  GET_USER_PROFILE_SUCCESS,
  GET_USER_PROFILE_ERROR
)<undefined, GithubProfile, AxiosError>();

이제 액션에 대한 타입, 리듀서에서 사용할 타입등, 타입들을 만들어주자.

//modules/github/types.ts
import * as actions from "./actions";
import { ActionType } from "typesafe-actions";
import { GithubProfile } from "../../api/guthub";

export type GithubAction = ActionType<typeof actions>;
export type GithubState = {
  userProfile: {
    loading: boolean;
    data: GithubProfile | null;
    error: Error | null;
  };
};

이제 덩크함수와 리듀서를 만들자

//modules/github/thunks.ts
import { Dispatch } from "redux";
import { getUserPeofile } from "../../api/guthub";
import { getUserProfileAsync } from "./actions";

export function getUserProfileThunk(username: string) {
  return async (dispatch: Dispatch) => {
    const { request, success, failure } = getUserProfileAsync;
    dispatch(request());
    try {
      const userProfile = await getUserPeofile(username);
      dispatch(success(userProfile));
    } catch (e) {
      dispatch(failure(e));
    }
  };
}

//modules/github/reducer.ts
import { createReducer } from "typesafe-actions";
import {
  GET_USER_PROFILE,
  GET_USER_PROFILE_ERROR,
  GET_USER_PROFILE_SUCCESS,
} from "./actions";
import { GithubAction, GithubState } from "./types";

//기본상태
const initialState: GithubState = {
  userProfile: {
    loading: false,
    data: null,
    error: null,
  },
};

const github = createReducer<GithubState, GithubAction>(initialState, {
  [GET_USER_PROFILE]: (state) => ({
    ...state,
    userProfile: {
      loading: true,
      error: null,
      data: null,
    },
  }),
  [GET_USER_PROFILE_SUCCESS]: (state, action) => ({
    ...state,
    userProfile: {
      loading: false,
      error: null,
      data: action.payload,
    },
  }),
  [GET_USER_PROFILE_ERROR]: (state, action) => ({
    ...state,
    userProfile: {
      loading: false,
      error: action.payload,
      data: null,
    },
  }),
});

//modeules/github/index.ts
export { default } from "./reducer";
export * from "./actions";
export * from "./thunk";

이제 준비는 끝났으니 루트의 index.ts 의 루트리듀서에 넣어주면 된다.

//index.ts
import { combineReducers } from "redux";
import counter from "./counter";
import todos from "./todos";
import github from "./github";

const rootReducer = combineReducers({
  counter,
  todos,
  github,
});

export default rootReducer;
//리덕스에서 관리하는 상태에 대한 타입
export type RootState = ReturnType<typeof rootReducer>;