본 게시물은 Medium의 Redux-Thunk vs Redux-Saga을 우리말로 번역한 글입니다. 약간의 의역이 포함되어 있습니다.
요즘에는 많은 동적 웹 애플리케이션에서 비동기 작업
을 사용합니다. 당신은 React 개발자로서 아마 경력 내내 Redux-Thunk
를 사용해왔을 것입니다. 기존 Redux-Thunk
에서 몇년 전부터 유행하고 있는 Redux-Saga
와 같은 다른 솔루션으로 이동하는 것이 불편할 수 있습니다. 맞습니다, syntax가 다르기 때문에 처음 접했을 때는 매우 혼란스럽고 이해가 되지 않을 수 있습니다.
Redux-Saga
의 이점(Redux-Thunk
와 비교하여)은 비동기 데이터 흐름을보다 쉽게 테스트 할 수 있다는 것입니다.
Redux-Thunk
는 소규모 프로젝트 및 React에 막 입문한 개발자에게는 좋을 수 있습니다. Redux-Thunk
의 로직은 모두 함수 내부에 포함되어 있으며, 더불어 Redux-Thunk
을 배우면Redux-Saga
의 그 이상하고 생소한 구문
도 배울 필요가 없습니다. 그러나 이 튜토리얼에서는 Redux-Thunk
에서 → Redux-Saga
로 쉽게 이동할 수있는 방법을 보여주고, 위의 이상하고 생소한 구문
에 대해 설명해드리겠습니다 :D
가장 먼저 디펜던시를 몇 가지를 설치하겠습니다.
npm i --save react-redux redux redux-logger redux-saga redux-thunk
다음으로 프로젝트에서 Redux를 셋업해야 합니다. Redux 폴더를 만들고 그 안에 store.js 파일을 만들어 보겠습니다.
import { createStore, applyMiddleware } from 'redux'; import logger from 'redux-logger'; import thunk from 'redux-thunk'; import rootReducer from './root-reducer'; const middlewares = [thunk]; if (process.env.NODE_ENV === 'development') { middlewares.push(logger); } export const store = createStore(rootReducer, applyMiddleware(...middlewares)); export default store;
그 다음에는 root-reducer.js 파일을 만들어줍니다.
import { combineReducers } from 'redux'; import fetchTasksReducer from './reducers/fetchTasksReducer' const rootReducer = combineReducers({ tasks: fetchTasksReducer, }); export default rootReducer;
App.js에 위의 store를 import 해주는 것도 잊지 마시고요.
import React, { Component } from 'react'; import { BrowserRouter, Route } from 'react-router-dom'; import { Provider } from 'react-redux'; import Tasks from './components/tasks'; import './App.css'; const store = require('./reducers').init(); class App extends Component { render() { return ( <Provider store={store}> <BrowserRouter> <div className='App'> <div className='container'> <Route exact path='/' component={Tasks} /> </div> </div> </BrowserRouter> </Provider> ); } } export default App;
이제 fetchTasksReducerer
를 생성할 차례입니다.
... const INITIAL_STATE = { tasks: null, isFetching: false, errorMessage: undefined }; const fetchTasksReducer = (state = INITIAL_STATE, action) => { switch (action.type) { case "FETCH_TASKS_START": return { ...state, isFetching: true }; case "FETCH_TASKS_SUCCESS": return { ...state, isFetching: false, tasks: action.payload }; case "FETCH_TASKS_ERROR": return { ...state, isFetching: false, errorMessage: action.payload }; default: return state; } }; export default fetchTasksReducer;
action
을 dispatch
할 때마다 해당 action
은 fetchTasksReducer
를 통과하고 필요에 따라 state
를 업데이트합니다. 이 세 가지 조건들이 작동하는 방식은 다음과 같습니다.
FETCH_TASKS_START
: HTTP 요청이 시작되었습니다. 예를 들어 사용자에게 진행중인 프로세스가 있음을 알리는 로딩바를 표시하기에 좋은 시간입니다.FETCH_TASKS_SUCCESS
: HTTP 요청이 성공했습니다. state
를 업데이트해야합니다.FETCH_TASKS_ERROR
: HTTP 요청이 실패했습니다. 사용자에게 문제를 알리기 위해 오류 구성 요소를 표시 할 수 있습니다.현재는 reducer
가 처리할 action
을 실행하는 액션 생성자
(의역 : 액션객체 생성함수)가 없기 때문에 앱이 원하는 대로 작동하고 있지 않습니다.
export const fetchTasksStarted = () => ({ type: "FETCH_TASKS_START" }); export const fetchTasksSuccess = tasks => ({ type: "FETCH_TASKS_SUCCESS", payload: tasks }); export const fetchTasksError = errorMessage => ({ type: "FETCH_TASKS_ERROR", payload: errorMessage }); const fetchTasks = () => async dispatch => { dispatch(fetchTasksStarted()) try{ const TaskResponse = await fetch("API URL") const task = await taskResponse.json() dispatch(fetchTasksSuccess(tasks)) }catch(exc){ dispatch(fetchTasksError(error.message)) } }
fetchTasks
가 처음에는 이상하게 보일 수 있지만, fetchTasks
는 dispatch
매개변수를 가진 함수를 반환하는 함수입니다. dispatch
가 호출되면 제어 흐름이 reducer
로 이동하여 어떤 작업을 수행할지 결정됩니다. 위의 경우에는 요청이 성공한 경우에만 애플리케이션의 state
를 업데이트합니다.
Redux-saga
는 Redux로 비동기 코드를 쉽게 구현할 수있는 redux 미들웨어입니다. Redux-Thunk
의 가장 막강한 경쟁자입니다.
시작해봅시다. 위의 코드에서 Redux-Thunk
도입 이전까지의 동일한 코드를 가지고 진행해보겠습니다. Store.js에 Redux-saga
미들웨어를 구현해보겠습니다.
import { createStore, applyMiddleware } from 'redux'; import createSagaMiddleware from 'redux-saga'; import logger from 'redux-logger'; import rootReducer from './root-reducer'; import { watchFetchTasksSaga } from './saga/fetchTasks.saga'; const sagaMiddleware = createSagaMiddleware(); const middlewares = [logger, sagaMiddleware]; export const store = createStore(rootReducer, applyMiddleware(...middlewares)); sagaMiddleware.run(watchFetchTasksSaga); export default store;
이제 actions
를 만들어볼 차례입니다.
export const fetchTasksStarted = () => ({ type: "FETCH_TASKS_START" }); export const fetchTasksSuccess = tasks => ({ type: "FETCH_TASKS_SUCCESS", payload: tasks }); export const fetchTasksError = errorMessage => ({ type: "FETCH_TASKS_ERROR", payload: errorMessage });
saga폴더를 만들고 그 안에 fetchTasks.saga
파일을 생성합니다.
import { takeLatest, put } from "redux-saga/effects"; function* fetchTasksSaga(){ try { const taskResponse = yield fetch("API URL") const tasks = yield taskResponse.json() yield put(fetchTasksSuccess(tasks)); } catch (error) { yield put(fetchTasksError(error.message)); } } export default function* watchFetchTasksSaga(){ yield takeLatest("FETCH_TASKS_START", fetchTasksSaga) }
위의 함수들을 생성기 함수
(generator functions)라고 합니다.
takeLatestand
와 takeEvery
를 모두 사용할 수 있지만 여기서는 takeLatest
를 사용했습니다.
(마지막 이벤트만 불러들이는 한)
put
함수를 호출하면 dispatch
에서 했던 것처럼 action
을 실행할 수 있습니다. put
은 reducer
가 action
을 처리하도록 조절해줍니다.
... const INITIAL_STATE = { tasks: null, isFetching: false, errorMessage: undefined }; const fetchTasksReducer = (state = INITIAL_STATE, action) => { switch (action.type) { case "FETCH_TASKS_START": return { ...state, isFetching: true }; case "FETCH_TASKS_SUCCESS": return { ...state, isFetching: false, tasks: action.payload }; case "FETCH_TASKS_ERROR": return { ...state, isFetching: false, errorMessage: action.payload }; default: return state; } }; export default fetchTasksReducer;
여기까지입니다. 이제 당신은React
와 Redux
에서 비동기 작업을 사용하는 두 가지 접근 방식에 익숙하게 됐습니다. 당신이 작업중인 프로젝트에 따라 둘 중 어느 것을 선택할지 결정할 수 있습니다.