๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

Redux

Redux-Toolkit ์—์„œ Saga ์‚ฌ์šฉํ•˜๊ธฐ (Feat. Typescript)

์˜ค๋Š˜ ํฌ์ŠคํŠธ๋Š” Redux-Toolkit ๊ณผ Redux-saga๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋น„๋™๊ธฐ๋กœ API๋ฅผ ๋ฐ›์•„์™€๋ณด์ž ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์™€ ํ•จ๊ป˜ !

 

์šฐ์„  ๋ณธ๊ฒฉ์ ์œผ๋กœ ์ฝ”๋“œ๋ฅผ ๋ณด๊ธฐ์— ์•ž์„œ Saga์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์ž 

 

Redux-Saga ์— ๋Œ€ํ•˜์—ฌ 

redux-saga ๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ, ์˜ˆ๋ฅผ ๋“ค๋ฉด ๋ฐ์ดํ„ฐ fetching์ด๋‚˜ ๋ธŒ๋ผ์šฐ์ € ์บ์‹œ์— ์ ‘๊ทผํ•˜๋Š” ์ˆœ์ˆ˜ํ•˜์ง€ ์•Š์€ ๋น„๋™๊ธฐ ๋™์ž‘๋“ค์„, ๋” ์‰ฝ๊ณ  ์ข‹๊ฒŒ ๋งŒ๋“œ๋Š” ๊ฒƒ์„ ๋ชฉ์ ์œผ๋กœํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋‹ค.

 

saga๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๋งŒ์„ ๋‹ด๋‹นํ•˜๋Š” ๋ณ„๋„์˜ ์“ฐ๋ ˆ๋“œ๋ผ๊ณ  ๋ณด๋ฉด ๋˜๊ณ , redux-saga๋Š” ๋ฏธ๋“ค์›จ์–ด์ด๊ธฐ ๋•Œ๋ฌธ์— saga๋ผ๋Š” ์“ฐ๋ ˆ๋“œ๊ฐ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ redux ์•ก์…˜์„ ํ†ตํ•ด ์‹คํ–‰๋˜๊ณ , ๋ฉˆ์ถ”๊ณ , ์ทจ์†Œํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค. ๋˜ ๋ชจ๋“  redux ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ƒํƒœ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ณ  redux ์•ก์…˜๋„ dispatch ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค. 

 

saga๋Š” ๋น„๋™๊ธฐ ํ๋ฆ„์„ ์‰ฝ๊ฒŒ ์ฝ๊ณ , ์“ฐ๊ณ , ํ…Œ์ŠคํŠธ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋„์™€์ฃผ๋Š” ES6์˜ Generator๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. Generator๋ฅผ ์‚ฌ์šฉํ•จ์œผ๋กœ์จ, ๋น„๋™๊ธฐ ํ๋ฆ„์€ ํ‘œ์ค€ ๋™๊ธฐ์‹ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์ฝ”๋“œ์ฒ˜๋Ÿผ ๋ณด์ด๊ฒŒ ๋œ๋‹ค. (async/await์™€ ๋น„์Šทํ•œ๋ฐ, ๋” ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค)

redux-thunk์™€ ๋‹ค๋ฅด๊ฒŒ ์ฝœ๋ฐฑ ์ง€์˜ฅ์— ๋น ์ง€์ง€ ์•Š๊ณ  ๋น„๋™๊ธฐ ํ๋ฆ„๋“ค์„ ์‰ฝ๊ฒŒ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋ฉฐ ์•ก์…˜๋“ค์„ ์ˆœ์ˆ˜ํ•˜๊ฒŒ ์œ ์ง€ ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 

 

์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด์„œ ์•Œ์•„๋ณด์ž 

 

์šฐ์„  redux-toolkit์„ ์ƒ์„ฑํ•ด๋ณด์ž 

 

 

import { createSlice, PayloadAction } from '@reduxjs/toolkit';

//initialState
export const initialState: Description = {
	animal: [],
	isLoading: false,
	error: null,
};

export const slice = createSlice({
	name: 'animal',
	initialState,
	reducers: {
		getDataSuccess: (state, action: PayloadAction<DescriptionParams>) => {
			state.isLoading = true;
			state.animal.length = 0;
			const newState = state.animal.concat(action.payload);
			state.animal = newState;
		},
		getDataFailure: (state, { payload: error }) => {
			state.isLoading = false;
			state.error = error;
		},
		getData: (state, action: PayloadAction<ParamType>) => {
			state.isLoading = false;
		},
	},
});

export const animal = slice.name;
export const animalReducer = slice.reducer;
export const animalAction = slice.actions;

 

 

์šฐ์„  createSlice๋ฅผ ์‚ฌ์šฉํ•  ๊ฒƒ ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋”ฐ๋กœ ์•ก์…˜์˜ ํƒ€์ž…์„ ์ง€์ •ํ•ด์ฃผ๊ฑฐ๋‚˜ ์•ก์…˜์ƒ์„ฑํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด ์ฃผ๋Š” ์ผ์„ ํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค

 

initialState๋ฅผ ์„ ์–ธํ•œ ๋’ค Slice์•ˆ์— ๋„ฃ๊ณ   reducer: {} ์•ˆ์— ๋ฆฌ๋“€์„œ๋“ค์„ ๋„ฃ์–ด์ฃผ๋ฉด ๋œ๋‹ค. name์€ ์•ก์…˜์˜ ํƒ€์ž…์ด๋‹ค.

 

์—ฌ๊ธฐ์„œ ์•ก์…˜์˜ Payload ํƒ€์ž…์„ PayloadAction์œผ๋กœ ์ง€์ •ํ•ด์ฃผ์—ˆ๋Š”๋ฐ ๋ฆฌ๋“€์„œ์—์„œ ์•ก์…˜์˜ ํƒ€์ž…์„ ์„ ์–ธ ํ•  ๋•Œ ์‚ฌ์šฉํ•  ๊ธฐ๋ณธ ์œ ํ˜•์€ PayloadAction <PayloadType> ์ด๋‹ค

 

 

interface๋กœ ํƒ€์ž…์ง€์ •์„ ํ•ด๋ณด๋ฉด 

 

 

// ๋ฐ›์•„์˜ค๋Š” api Data์˜ type
interface DescriptionParams {
	age: number;
	careAddr: string;
	careNm: string;
	careTel: string;
	chargeNm: string;
	colorCd: string;
	desertionNo: number;
	filename: string;
	happenDt: number;
	happenPlace: string;
}

// state์˜ ํƒ€์ž…
interface Description {
	animal: DescriptionParams[];
	isLoading: boolean;
	error: null;
}

// api์˜ param ํƒ€์ž…
interface ParamType {
	city: number;
	kind: number;
}

 

 

๋ฐ›์•„์˜ค๋Š” api์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๊ฐ์ฒด๋ฐฐ์—ด ํ˜•์‹์ด๊ธฐ ๋•Œ๋ฌธ์— api ๋ฐ์ดํ„ฐ์˜ interface๋ฅผ initialState์˜ animal ์ด๋ผ๋Š” state๋Š” ๊ทธ interface๋ฅผ

ํƒ€์ž…์œผ๋กœ ๊ฐ€์ง„๋‹ค 

 

 

 

์ด์ œ Saga์˜ ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด๊ธฐ ์ „์— ๋ช‡๊ฐ€์ง€ ์‚ดํŽด๋ณด์ž

yield ๋ž€ ? 

Promise ๊ฐ€ ๋ฏธ๋“ค์›จ์–ด์— yield ๋  ๋•Œ, ๋ฏธ๋“ค์›จ์–ด๋Š” Promise ๊ฐ€ ๋๋‚ ๋•Œ ๊นŒ์ง€ Saga ๋ฅผ ์ผ์‹œ์ •์ง€ ์‹œํ‚ค๋Š” ๊ฒƒ

 

์šฐ์„  ์ œ์ผ ์•„๋ž˜์˜ animalSaga ํ•จ์ˆ˜๋ถ€ํ„ฐ ์‚ดํŽด๋ณด์ž ์ด ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์–ด๋– ํ•œ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด apiํ˜ธ์ถœ์„ ์œ„ํ•œ param๊ณผ ํ•จ๊ป˜ getData๋ผ๋Š” ์•ก์…˜์ด dispatch ๋˜๊ฒŒํ•˜์˜€๋‹ค.

 

์•„๋ž˜์™€ ๊ฐ™์€ ํ˜•์‹์œผ๋กœ dispatch ๋œ๋‹ค

 

const param = {
	city: 411400,
	kind: 210020,
};
			
dispatch(getData(param));

 

 

์ด์ œ saga์˜ ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด์ž

 

 

import { animalAction } from './animal';
import { call, put, takeEvery } from '@redux-saga/core/effects';
import * as API from '../service/get-data';

interface DescriptionParams {
	age: number;
	careAddr: string;
	careNm: string;
	careTel: string;
	chargeNm: string;
	colorCd: string;
	desertionNo: number;
	filename: string;
	happenDt: number;
	happenPlace: string;
}

interface ParamType {
	city: number;
	kind: number;
}

// get Saga
export function* getDataSaga(action: { payload: ParamType }) {
	const { getDataSuccess, getDataFailure } = animalAction;
	const param = action.payload;
	try {
		const response: DescriptionParams = yield call(API.getAnimal, param); 
        // call์€ ๋ฏธ๋“ค์›จ์–ด์—๊ฒŒ ํ•จ์ˆ˜์™€ ์ธ์ž๋“ค์„ ์‹คํ–‰ํ•˜๋ผ๋Š” ๋ช…๋ น
		yield put(getDataSuccess(response));
		// put์€ dispatch ๋ฅผ ๋œปํ•œ๋‹ค.
	} catch (err) {
		yield put(getDataFailure(err));
	}
}

// Main Saga
export function* animalSaga() {
	const { getData } = animalAction;
	yield takeEvery(getData, getDataSaga);
}

 

 

์œ„์—์„œ exportํ•œ animalAction์˜ getData๋ฅผ ์„ ์–ธํ•˜๊ณ  takeEvery์— ๋„ฃ์–ด์ค€๋‹ค 

 

์—ฌ๊ธฐ์„œ takeEvery ๋Š” ์—ฌ๋Ÿฌ๊ฐœ์˜ fetchData ์ธ์Šคํ„ด์Šค๋ฅผ ๋™์‹œ์— ์‹œ์ž‘ํ•˜๊ฒŒํ•œ๋‹ค. 

 

getData๋ผ๋Š” ์•ก์…˜์ด ํ˜ธ์ถœ์ด ๋œ๋‹ค๋ฉด getDataSaga๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ take ํ•œ๋‹ค ๋ผ๊ณ  ๋ณด๋ฉด๋˜๊ฒ ๋‹ค !

์ด์ œ getDataSaga์—์„œ๋Š” animalAction์—์„œ getDataSuccess์™€ getDataFailure ์ด๋ผ๋Š” ์•ก์…˜ ๋‘๊ฐœ๋ฅผ ๋ฐ›์•„์˜ค๊ณ  api์˜ param๋„ ๋ฐ›์•„์™€ try {} ์•ˆ์—์„œ param๊ณผ ํ•จ๊ป˜ api๋ฅผ call (ํ•จ์ˆ˜ ์‹คํ–‰) ํ•˜๊ณ   ์„ฑ๊ณต์ ์œผ๋กœ ๋ฐ›์•„์™€์ง€๋ฉด getDataSuccess ์•ก์…˜์„ put(dispatch ํ•˜๋Š”๊ฒƒ) ํ•˜๊ณ  ์—๋Ÿฌ๊ฐ€ ์ƒ๊ธธ ๊ฒฝ์šฐ์—๋Š” getDataFailure๋ฅผ put ํ•˜๊ฒŒ๋œ๋‹ค 

 

์ด๊ฒƒ์ด saga์˜ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ผ๊ณ  ๋ณด๋ฉด ๋˜๊ฒ ๋‹ค 

 

์ด์ œ rootReducer๋ฅผ ์‚ดํŽด๋ณด์ž 

 

 

import { animalReducer } from './animal';
import { combineReducers } from 'redux';
import { all } from 'redux-saga/effects';
import { animalSaga } from './saga';

// animalReducer ๋ฅผ rootReducer ๋กœ ํ•ฉ์ณ ๋‚ด๋ณด๋ƒ„
const rootReducer = combineReducers({
	animalReducer,
});

export function* rootSaga() {
	// all ์€ ์—ฌ๋Ÿฌ ์‚ฌ๊ฐ€๋ฅผ ๋™์‹œ์— ์‹คํ–‰์‹œ์ผœ์ค€๋‹ค. ํ˜„์žฌ๋Š” animalSaga ํ•˜๋‚˜.
	yield all([animalSaga()]);
}

export type ReducerType = ReturnType<typeof rootReducer>;
export default rootReducer;

 

 

rootReducer์— combineReducer๋กœ ๋ฆฌ๋“€์„œ ํŒŒ์ผ์—์„œ exportํ•œ animalReducer๋ฅผ ๋‹ด์•„์ค€๋‹ค

 

rootSagaํ•จ์ˆ˜๋Š” animalSaga๋ผ๋Š” Sagaํ•จ์ˆ˜๋ฅผ ์‹คํ–‰์‹œ์ผœ์ค€๋‹ค 

 

์ด ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰ ๋˜์–ด์ ธ ์žˆ์–ด์•ผ ๋ฏธ๋“ค์›จ์–ด๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•œ๋‹ค 

 

๋งˆ์ง€๋ง‰์œผ๋กœ ReturnType์ด๋ผ๋Š” type์„ ์ง€์ •ํ•ด ์ฃผ์—ˆ๋Š”๋ฐ TypeScript ํ™˜๊ฒฝ์—์„œ ๋ฆฌ๋•์Šค๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ํ•„์ˆ˜์ ์œผ๋กœ ํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.

 

์ด์œ ๋Š” useSelector๋ฅผ ์ด์šฉํ•˜์—ฌ state๋ฅผ ๋ฐ›์•„์˜ฌ ๋•Œ ํƒ€์ž…์„ ์ง€์ •ํ•ด์ฃผ์ง€ ์•Š๋Š”๋‹ค๋ฉด ์ •์ƒ์ ์œผ๋กœ state๋ฅผ ๋ฐ›์•„์˜ค์ง€ ๋ชปํ•œ๋‹ค 

์•„๋ž˜์™€ ๊ฐ™์ด ํ•˜๋ฉด๋œ๋‹ค.

 

 

const { animal } = useSelector<ReducerType, Description>(
		(state) => state.animalReducer
	);

 

 

index ํŒŒ์ผ

 

import App from './App';
import React from 'react';
import ReactDOM from 'react-dom';
import GlobalStyle from './assets/styles/global-styles';
import { configureStore } from '@reduxjs/toolkit';
import rootReducer, { rootSaga } from './modules/rootReducer';
import { Provider } from 'react-redux';
import createSagaMiddleware from 'redux-saga';

const sagaMiddleware = createSagaMiddleware();
const store = configureStore({
	reducer: rootReducer,
	middleware: [sagaMiddleware],
});

// saga๋ฅผ ์‹คํ–‰
sagaMiddleware.run(rootSaga);

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

 

 


 

์ด๋ ‡๊ฒŒ Redux-toolkit๊ณผ Saga๋ฅผ TypeScript ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๊ฐ„๋‹จํ•˜๊ฒŒ ์•Œ์•„๋ณด์•˜๋‹ค 

๋” ํ•™์Šตํ•ด์„œ ๋Šฅ์ˆ™ํ•˜๊ฒŒ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๋„๋ก ํ•ด๋ณด์ž 

 

 

 

reference

https://im-developer.tistory.com/195