src
ㄴapi
-client.ts
-post.ts
ㄴcomponents
-PostApp.tsx
-PostForm.tsx
-PostView.tsx
ㄴmodules
-index.ts
-types.ts
-actions.ts
-reducer.ts
-saga.ts
App.tsx
index.tsx
import client from './client';
// client = axios.create({ ... });
export async function getPostById(id: string) {
const response = await client.get<responsePayloadType>(`/posts/${id}`);
return response.data;
}
export type requestPropsType = {
id: string;
};
export type responsePayloadType = {
userId: number;
id: number;
title: string;
body: string;
};
비동기 함수 getPostById()
는 axios 객체를 이용해 API 요청을 받아오고 있다.
JS와 달리 타입스크립트에서 API 요청을 할 때에는 API의 request와 response에 관한 타입을 따로 정의해야 한다.
import { ActionType } from 'typesafe-actions';
import { initialize, getPost } from './actions';
const actions = { initialize, getPost };
export type PostAction = ActionType<typeof actions>;
export type Post = {
userId: number;
id: number;
title: string;
body: string;
};
export type PostState = {
loading: boolean;
posts: Post[];
color: string;
};
리덕스 모듈에 쓰이는 상태와 액션의 타입을 정의한다.
import { createAction, createAsyncAction } from 'typesafe-actions';
import { requestPropsType, responsePayloadType } from '../api/post';
import { AxiosError } from 'axios';
export const INITIALIZE = 'INITIALIZE';
export const GET_POST = 'GET_POST';
export const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
export const GET_POST_FAILURE = 'GET_POST_FAILURE';
export const initialize = createAction(INITIALIZE)();
export const getPost = createAsyncAction(
GET_POST,
GET_POST_SUCCESS,
GET_POST_FAILURE,
)<requestPropsType, responsePayloadType, AxiosError>();
액션 객체 및 액션 생성 함수를 정의한다.
import { createReducer } from 'typesafe-actions';
import { PostAction, PostState } from './types';
import {
GET_POST,
INITIALIZE,
GET_POST_SUCCESS,
GET_POST_FAILURE,
} from './actions';
const initialState: PostState = {
loading: false,
posts: [],
color: 'white',
};
const post = createReducer<PostState, PostAction>(initialState, {
[INITIALIZE]: (state) => ({ ...state, posts: [] }),
[GET_POST]: (state) => ({ ...state, loading: true }),
[GET_POST_SUCCESS]: (state, action) => ({
...state,
loading: false,
posts: state.posts.concat(action.payload),
}),
[GET_POST_FAILURE]: (state, action) => ({
...state,
loading: false,
error: action.payload,
}),
});
export default post;
리듀서를 작성할 때에는 상태 및 액션의 타입을 필요로 한다.
리덕스의 보일러 플레이트 코드를 더욱 간편하게 작성하기 위한 라이브러리.
// 액션 타입 객체
// 뒤에 as const를 붙이지 않아도 됨
const ACTION_TYPE = 'module/action';
// 액션 생성 함수
// 반드시 뒤에 ()를 붙여서 호출해야 정상 작동
const actionCreator = createAction(ACTION_TYPE)(map props to payload)();
// 비동기 액션 생성 함수
// 액션의 요청, 성공, 실패를 한 번에 다룰 수 있음
const asyncActionCreator = createAsyncAction(
ACTION_REQUEST_TYPE,
ACTION_SUCCESS_TYPE,
ACTION_FAILURE_TYPE
)<actionRequestPayload, actionResponsePayload, actionErrorPayload>();
// 리듀서에서 사용할 전체 액션 타입
const actions = { actionCreator, ... };
type Action = ActionType<typeof actions>;
// 리듀서
const reducer = createReducer<State, Action>(initialState, {
[ACTION_TYPE]: (state, action) => ({ ... }),
...
});
또는
const reducer = createReducer<State, Action>(initialState)
.handleAction(ACTION_TYPE, (state, action) => ({ ... }))
.handleAction( ... )
...;
import { AxiosError } from 'axios';
import { call, put, takeLatest } from 'redux-saga/effects';
import * as api from '../api/post';
import { getPost } from './actions';
function* getPostByIdSaga(action: ReturnType<typeof getPost.request>) {
try {
const response: api.responsePayloadType = yield call(
[undefined, api.getPostById],
action.payload.id,
);
yield put(getPost.success(response));
} catch (e) {
yield put(getPost.failure(e as AxiosError));
}
}
export default function* postSaga() {
yield takeLatest(getPost.request, getPostByIdSaga);
}
비동기 작업을 위한 redux-saga의 각 saga는 요청 액션 타입을 인수로 받는다.
call
은 API 호출을 위해 사용되었다. 첫 번째 인수로는 Context와 API 호출 함수로 이루어진 배열을, 두 번째 인수로는 요청 시 호출 함수에 필요한 인수를 제공한다.
put
은 saga 안에서 dispatch
와 비슷한 역할을 한다. 성공/실패 시 각 상황에 맞는 액션 생성 함수를 인수와 함께 넣어 호출한다.
takeLatest
는 첫 번째 인수로 제공된 액션을 감지하면 두 번째 인수의 saga를 실행한다. 해당 액션이 동시에 여러 번 발생하면 가장 마지막으로 발생한 액션만 감지한다.
export const getPostByIdThunk =
(id: string): ThunkAction<void, PostState, null, PostAction> =>
async (dispatch) => {
dispatch(getPost.request());
try {
const response = await api.getPostById(id);
dispatch(getPost.success(response));
} catch (e) {
dispatch(getPost.failure(e as AxiosError));
}
};
export const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
dispatch(getPostByIdThunk(value)); // 함수를 디스패치
};
redux-saga 대신 redux-thunk를 이용하면 이렇게 작성한다.
import { combineReducers } from 'redux';
import { all } from 'redux-saga/effects';
import post from './reducer';
import postSaga from './saga';
const rootReducer = combineReducers({
post,
...
});
export default rootReducer;
export type RootState = ReturnType<typeof rootReducer>;
export function* rootSaga() {
yield all([postSaga()]);
...
}
리덕스 모듈의 인덱스에서는 각 리듀서와 사가를 하나로 합쳐 내보낸다. 이때 rootReducer
의 반환 타입인 RootState
도 함께 내보내준다.
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import createSagaMiddleware from 'redux-saga';
import ReduxThunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';
import App from './App';
import rootReducer, { rootSaga } from './modules';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(sagaMiddleware, ReduxThunk)),
);
sagaMiddleware.run(rootSaga); // saga 미들웨어 실행
ReactDOM.render(
<Provider store={store}>
<React.StrictMode>
<App />
</React.StrictMode>
</Provider>,
document.getElementById('root'),
);