velog 에 이에 관한 포스트가 조만간 올라올 예정입니다.
타입스크립트를 사용하는 리액트 프로젝트에서 리덕스를 프로처럼 사용해봅시다!
이 프로젝트는 총 10개의 브랜치로 나뉘어져있습니다.
- basics/counter: 가장 기본적인 카운터 예시입니다
- basics/todos: 조금 더 다양한 액션을 다루는 투두리스트 예시입니다
- refactor/counter: 카운터 리덕스 모듈을 typesafe-actions 를 사용하여 리팩토링하는 예시입니다.
- refactor/counter2: 카운터 리듀서를 typesafe-actions의
createReducer함수를 사용하여 메서드 체이닝 방식을 사용하여 리팩토링하는 예시입니다. - refactor/todos: 투두리스트 리덕스 모듈을 typesafe-actions 로 리팩토링한 예시입니다.
- refactor/todos2: 투두리스트 리덕스 모듈을 여러개의 파일로 분리하여 리팩토링한 예시입니다.
- middleware/thunk: redux-thunk 미들웨어를 사용하는 예시입니다.
- middleware/thunk-refactor: 비동기 작업을 관리하는 thunk 함수와, 비동기 관련 액션에 따라 상태를 업데이트하는 리듀서를 조금 더 짧은 코드로 구현 할 수 있도록
createAsyncThunk와createAsyncReducer유틸 함수를 만들어서 코드를 리팩토링하는 예시입니다. - middleware/saga: redux-saga 미들웨어를 사용하는 예시입니다
- middleware/saga-refactor: Promise 를 기반으로 액션을 발생시키는 saga 를 쉽게 만들어주는
createAsyncSaga유틸 함수를 만들어서 코드를 리팩토링하는 예시입니다.
주목할만한 코드는 다음과 같습니다.
액션 타입을 선언 할 때에는 다음과 같이 선언합니다.
constINCREASE_BY='counter/INCREASE_BY'asconst;as const 를 함으로써 추후 액션 객체의 타입을 유추 할 수 있게 됩니다.
액션 생성 함수를 만들 떄에는 다음과 같이 선언합니다.
exportconstincreaseBy=(diff: number)=>({type: INCREASE_BY,payload: diff});그리고, 리듀서를 구현하기 전에 모든 액션들에 대한 타입을 하나의 type alias 에 담아주어야 되는데요 이는 이렇게 작성합니다.
typeCounterAction=|ReturnType<typeofincrease>|ReturnType<typeofdecrease>|ReturnType<typeofincreaseBy>;다음, 리듀서에서 관리 할 상태와 초깃값은 다음과 같이 선언합니다.
// 이 리덕스 모듈에서 관리 할 상태의 타입을 선언합니다typeCounterState={count: number};// 초기상태를 선언합니다.constinitialState: CounterState={count: 0};리듀서는 다음과 같이 구현합니다.
functioncounter(state: CounterState=initialState,action: CounterAction): CounterState{switch(action.type){caseINCREASE: // case 라고 입력하고 Ctrl + Space 를 누르면 어떤 종류의 action.type들이 있는지 확인 할 수 있습니다.return{count: state.count+1};caseDECREASE:
return{count: state.count-1};caseINCREASE_BY:
return{count: state.count+action.payload};default:
returnstate;}}그리고 루트 리듀서와 루트 상태는 다음과 같이 만듭니다.
import{combineReducers}from'redux';importcounterfrom'./counter';constrootReducer=combineReducers({
counter
});// 루트 리듀서를 내보내주세요.exportdefaultrootReducer;// 루트 리듀서의 반환값를 유추해줍니다// 추후 이 타입을 컨테이너 컴포넌트에서 불러와서 사용해야 하므로 내보내줍니다.exporttypeRootState=ReturnType<typeofrootReducer>;컨테이너는 다음과 같이 만듭니다.
importReactfrom'react';import{useSelector,useDispatch}from'react-redux';import{RootState}from'../modules';import{increase,decrease,increaseBy}from'../modules/counter';importCounterfrom'../components/Counter';exportinterfaceCounterContainerProps{}constCounterContainer=(props: CounterContainerProps)=>{// 상태를 조회합니다. 상태를 조회 할 때에는 state 의 타입을 RootState 로 지정해야합니다.constcount=useSelector((state: RootState)=>state.counter.count);constdispatch=useDispatch();// 디스패치 함수를 가져옵니다// 각 액션들을 디스패치하는 함수들을 만들어줍니다constonIncrease=()=>{dispatch(increase());};constonDecrease=()=>{dispatch(decrease());};constonIncreaseBy=(diff: number)=>{dispatch(increaseBy(diff));};return<Countercount={count}onIncrease={onIncrease}onDecrease={onDecrease}onIncreaseBy={onIncreaseBy}/>;};exportdefaultCounterContainer;여기 까지의 코드들은 basics/counter 브랜치에서 볼 수 있습니다.
typesafe-actions](https://github.com/piotrwitek/typesafe-actions) 를 사용 할 때는 액션 타입과 액션 생성 함수를 다음과 같이 작성합니다.
// 액션 type 선언constINCREASE='counter/INCREASE';constDECREASE='counter/DECREASE';constINCREASE_BY='counter/INCREASE_BY';// 액션 생성함수를 선언합니다exportconstincrease=createStandardAction(INCREASE)();exportconstdecrease=createStandardAction(DECREASE)();exportconstincreaseBy=createStandardAction(INCREASE_BY)<number>();// payload 타입을 Generics 로 설정해주세요.액션 타입을 선언 할 때 as const 를 빼셔도 됩니다.
모든 액션들에 대한 타입은 이렇게 구현 할 수 있습니다.
// 액션 객체 타입 준비constactions={ increase, decrease, increaseBy };// 모든 액션 생성함수들을 actions 객체에 넣습니다typeCounterAction=ActionType<typeofactions>;// ActionType 를 사용하여 모든 액션 객체들의 타입을 준비해줄 수 있습니다리듀서는 이렇게 작성 할 수 있습니다.
constcounter=createReducer<CounterState,CounterAction>(initialState,{[INCREASE]: state=>({count: state.count+1}),// 액션을 참조 할 필요 없으면 파라미터로 state 만 받아와도 됩니다[DECREASE]: state=>({count: state.count-1}),[INCREASE_BY]: (state,action)=>({count: state.count+action.payload})// 액션의 타입을 유추 할 수 있습니다.});여기까지의 코드는 refactor/counter 브랜치에서 볼 수 있습니다.
리듀서를 메서드 체이닝 방식을 사용한다면 다음과 같이 구현 할 수 있습니다.
constcounter=createReducer(initialState).handleAction(increase,state=>({count: state.count+1})).handleAction(decrease,state=>({count: state.count-1})).handleAction(increaseBy,(state,action)=>({count: state.count+action.payload}));이 코드는 refactor/counter2 에서 확인 할 수 있습니다.
리덕스 관련 코드를 Ducks 패턴으로 작성하다가 액션의 갯수가 너무 많아져서 코드가 길어지면 한 디렉터리에 actions, reducer, types 를 분리해서 작성하시는 것도 좋은 방법입니다.
modules/
todos/
index.ts
actions.ts
reducer.ts
types.ts
이에 대한 코드는 refactor/todos2 에서 확인 할 수 있습니다.
index.ts 는 다음과 같이 작성합니다.
export{default}from'./reducer';// reducer 를 불러와서 default로 내보내겠다는 의미export*from'./actions';// 모든 액션 생성함수들을 불러와서 같은 이름들로 내보내겠다는 의미export*from'./types';// 모든 타입들을 불러와서 같은 이름들로 내보내겠다는 의미비동기 작업 관련 액션 생성 함수는 typesafe-actions의 createAsyncAction를 사용하면 편하게 구현 할 수 있습니다.
import{createAsyncAction}from'typesafe-actions';import{GithubProfile}from'../../api/github';import{AxiosError}from'axios';exportconstGET_USER_PROFILE='github/GET_USER_PROFILE';exportconstGET_USER_PROFILE_SUCCESS='github/GET_USER_PROFILE_SUCCESS';exportconstGET_USER_PROFILE_ERROR='github/GET_USER_PROFILE_ERROR';exportconstgetUserProfileAsync=createAsyncAction(GET_USER_PROFILE,GET_USER_PROFILE_SUCCESS,GET_USER_PROFILE_ERROR)<undefined,GithubProfile,AxiosError>();Thunk 함수를 만들 때는 다음과 같이 작성합니다.
import{ThunkAction}from'redux-thunk';import{RootState}from'..';import{GithubAction}from'./types';import{getUserProfile}from'../../api/github';import{getUserProfileAsync}from'./actions';// ThunkAction 의 Generics 에는 다음 값들을 순서대로 넣어줍니다./* 1. thunk 함수에서 반환하는 값의 타입 2. 리덕스 스토어의 상태 타입 3. Extra Argument (https://github.com/reduxjs/redux-thunk#injecting-a-custom-argument) 4. thunk 함수 내부에서 디스패치 할 수 있는 액션들의 타입*/
export functiongetUserProfileThunk(username: string): ThunkAction<void,RootState,null,GithubAction>{returnasyncdispatch=>{const{ request, success, failure }=getUserProfileAsync;dispatch(request());try{const userProfile =awaitgetUserProfile(username);dispatch(success(userProfile));}catch(e){dispatch(failure(e));}};}thunk 관련 코드는 middleware/thunk 브랜치에서 확인 할 수 있습니다.
createAsyncThunk 와 reducerUtils 유틸 함수들을 만들어서 사용하게 된다면,
thunk 함수는 다음과 같이 작성 할 수 있고
exportconstgetUserProfileThunk=createAsyncThunk(getUserProfileAsync,getUserProfile);리듀서는 다음과 같이 작성 할 수 있습니다.
constgithub=createReducer<GithubState,GithubAction>(initialState).handleAction(transformToArray(getUserProfileAsync),createAsyncReducer(getUserProfileAsync,'userProfile'));이에 대한 코드는 middleware/thunk-refactor 에서 확인 하실 수 있습니다.
saga 는 다음과 같이 작성합니다.
import{getUserProfileAsync,GET_USER_PROFILE}from'./actions';import{getUserProfile,GithubProfile}from'../../api/github';import{call,put,takeEvery}from'redux-saga/effects';function*getUserProfileSaga(action: ReturnType<typeofgetUserProfileAsync.request>){try{constuserProfile: GithubProfile=yieldcall(getUserProfile,action.payload);yieldput(getUserProfileAsync.success(userProfile));}catch(e){yieldput(getUserProfileAsync.failure(e));}}exportfunction*githubSaga(){yieldtakeEvery(GET_USER_PROFILE,getUserProfileSaga);}saga 를 적용한 코드는 middleware/saga 브랜치에서 확인 할 수 있습니다.
프로미스를 다루는 saga 를 쉽게 만들어주는 함수 createAsyncSaga를 만들면
API 요청에 대한 saga 를 만들 때 다음과 같이 한 줄로 작성 할 수 있습니다.
constgetUserProfileSaga=createAsyncSaga(getUserProfileAsync,getUserProfile);이에 대한 코드는 saga-refactor 브랜치에서 보실 수 있습니다.