Feature description
axios๋ฅผ ์ด์ฉํด์ ๋ฐฑ์๋์ ์ฐ๊ฒฐํ๋ ํจ์๋ค์ ๋ง๋ค๊ธฐ
ํ์ผ ๊ตฌ์กฐ
src
api
band
actions.ts
actionTypes.ts
actions.ts
actionTypes.ts
dummy.ts
...
...
๏ฟฝRedux-saga
official page
๋ฆฌ๋์ค ์ฌ๊ฐ๋ฅผ ์ฌ์ฉํ๋ฉด ๋น๋๊ธฐ ํธ์ถ์ ์ํ (loading, error, success ๋ฑ)์ ๋ฐ๋ผ ์ฌ๋ฌ๋ฒ ์ํ๋ฅผ ๋ณ๊ฒฝํ ์ ์์ด์ apiํธ์ถํ ๋ ์ํ๋ฅผ ์๋ ค์ฃผ๋ ์ฉ๋๋ก ์ฌ์ฉํ์ต๋๋ค.
TypeAction์ type ๋ค์ ์ง์ ํด ์ฃผ๋๋ฐ ๋์์ ์ฃผ๋ module์
๋๋ค.
์ฌ์ฉ๋ฒ
ํ์ฌ CoverPage์ ์ ์ฉ์์ผ ๋์ ์ํ์ด๊ณ , test์ slice๋ฅผ ๋ง๋ค๋ ์ด ๋ถ๋ถ์ ์ฐธ๊ณ ํ๋ฉด ๋ ๊ฒ ๊ฐ์ต๋๋ค.
- Action ๋ฑ๋ก
src/api/band/actionTypes.ts
ํ์ผ ์์ ์๋ก์ด asyncAction์ ๋ง๋ญ๋๋ค.
# src/api/band/actionTypes.ts
export const LOAD_COVER = asyncActionCreator('LOAD_COVER');
src/api/band/actions.ts
ํ์ผ์ ์๋ก์ด ์ก์
์ ๋ง๋ญ๋๋ค.
asyncAction์ generic type์ ์ฒซ๋ฒ์งธ๋ถํฐ [request's input type], [data type], [error type]์
๋๋ค.
# src/api/band/action.ts
export const loadCover = asyncAction<number, Cover, string>(AT.LOAD_COVER);
- slice action ๋ฑ๋ก
src/containers/<Page>/slice/index.ts
์์ type๊ณผ createSlice์ reducer๋ฅผ ์ถ๊ฐํด์ค๋๋ค.
# src/containers/CoverPage/slice/index.ts
...
/* --- STATE --- */
export interface CoverState {
name: string;
coverResponse: AsyncStateType<Cover>;
} // state ํ์ ์ ์
export const initialState: CoverState = {
name: 'cover',
coverResponse: { loading: false },
};
const slice = createSlice({
name: 'cover', // ์ด ์ด๋ฆ์ types/RootState.ts์ ์จ๋์์ผ ํจ
initialState,
reducers: {
loadingCoverResponse(state, action: PayloadAction<any>) {
state.coverResponse = { loading: true };
return state;
},
successCoverResponse(state, action: PayloadAction<Cover>) {
state.coverResponse.loading = false;
state.coverResponse.data = action.payload;
return state;
},
errorCoverResponse(state, action: PayloadAction<string>) {
state.coverResponse.loading = false;
state.coverResponse.error = action.payload;
return state;
},
},
});
...
// useCoverSlice ์ useInjectSaga์ ์ถ๊ฐํด ์ค๋๋ค.
export const useCoverSlice = () => {
...
useInjectSaga({
key: slice.name,
saga: coverPageSaga,
});
...
};
- saga.ts ๋ง๋ค๊ธฐ
container์ slice ํด๋ ์์ saga.tsํ์ผ์ ๋ง๋ญ๋๋ค.
์ค์ ์ฝ๋์ ์ฃผ์์ ๋ ํ์ต๋๋ค.
# src/containers/CoverPage/slice/sags.ts
import { put, takeEvery } from 'redux-saga/effects';
import { coverActions } from '.';
import * as AT from 'api/actionTypes';
import * as actions from 'api/actions';
import { api } from 'api/band';
import { ActionType } from 'typesafe-actions';
// sagaํจ์๋ค์ ๋ชจ์์ slice/index.ts์์ ๋ฑ๋กํ saga์
๋๋ค.
export default function* coverPageSaga(payload: any) {
// action type์ REQUEST๋ฅผ ํตํด ํธ์ถํ๋ฉด ๋ค์ ๏ฟฝsaga ํจ์์ธ getCoverResponse ๊ฐ ํธ์ถ๋ฉ๋๋ค.
yield takeEvery(AT.LOAD_COVER.REQUEST, getCoverResponse);
}
// API๋ฅผ ํธ์ถํ๋ ๋ถ๋ถ์
๋๋ค.
export function* getCoverResponse(
action: ActionType<typeof actions.loadCover.request>,
) {
// ์ฒ์ ์์ํ๋ฉด loading์ ํ์ํด ์ค๋๋ค.
yield put(coverActions.loadingCoverResponse('start load'));
try {
const coverResponse = yield api.getCoverInfo(action.payload);
// ์ฑ๊ณตํ๋ฉด ๋ฐ์ดํฐ๋ฅผ ๋ฑ๋กํด ์ค๋๋ค.
yield put(coverActions.successCoverResponse(coverResponse));
} catch (e: any) {
// ์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด ์๋ฌ๋ฅผ ์ ๋ฌํด์ค๋๋ค.
yield put(coverActions.errorCoverResponse(e));
}
}
- Container์์ ํธ์ถ
dispatch ๋ฅผ ์ด์ฉํด์ ํธ์ถํ๋๊ฑด ์ด์ ๊ณผ ๊ฐ์ต๋๋ค.
const dispatch = useDispatch();
dispatch(apiActions.loadCover.request(Number(props.match.params.id)));
Api client and testing
axios client๋ฅผ ์จ๊ฒจ์ test์ ์ฉ์ดํ๊ฒ ํ์ต๋๋ค.
src/api/band/index.ts
์์ ๋ชจ๋ api์ ๋ํ ํจ์๋ฅผ ์ ๋ฆฌํด ๋์์ต๋๋ค.
get ์์ฒญ์ ๊ฒฝ์ฐ์๋ Http Response์์ data๋ฅผ ๋ฐ์์ค์ง๋ง ๋๋จธ์ง ์์ฒญ์ Http Request๊ฒฐ๊ณผ๋ฅผ ๊ทธ๋๋ก ๋ฐํํฉ๋๋ค.
// get
getCoversBySongId: async (songId: number) => {
const response = await apiClient.get<Cover[]>(`/api/cover/${songId}/`);
return response.data;
},
// others
putCoverInfo: async (coverForm: CoverFormPut) => {
return await apiClient.put<Cover>(
`/api/cover/info/${coverForm.id}/`,
coverForm,
);
},
ํ
์คํธ ํ ๋ api์ ๋ํด์ ๊ฐ๊ฐ์ fn๋ง jest.fn
์ ํตํด mock ํจ์๋ก ๋ง๋ค๋ฉด ์ธํฐ๋ท ๋ฌธ์ ์์ด ํ
์คํธ๋ฅผ ์งํํ ์ ์์ต๋๋ค.
api.getCoverInfo = jest.fn(
(coverId: number) =>
new Promise((res, rej) => {
res(dummyCovers[coverId]);
}),
);
Dummy
ํฉ์ด์ ธ ์๋ dummy๋ค์ ํ๊ตฐ๋ฐ๋ก ๋ชจ์์ต๋๋ค.
src/api/dummy.ts
Screenshots / Additional context