[TS]리덕스 사가로 Fetch 하기

peppermint100
11 min readMay 21, 2020

--

서론

리덕스는 전역 상태 관리를 도와주는 라이브러리입니다. 리액트와 궁합이 잘 맞기 때문에 리액트 하면 꼭 따라오는 상태 관리 라이브러리 중 하나죠. 하지만 요즈음에는 Context API나 Apollo로 대체 되기도 하고 또 다른 상태 관리 라이브러리인 MobX도 많이 사용하지만 그래도 리덕스는 여전히 쓸만 하다고 생각합니다. 리덕스의 문제점은 제가 생각하기엔 러닝 커브 단 하나입니다. 사용하는 방법만 잘 알고 있다면 다른 대체제에 비할 때 전혀 뒤질것 없는 상태 관리 라이브러리입니다. 그리고 이번엔 리덕스-사가로 비동기 요청을 하는 방법에 대해 알아 보겠습니다.

보기 전에

일단 기본적으로 Typescript로 모든 코드를 작성할 것이고 리덕스의 기본에 대해 알아야 합니다. 그리고 자바스크립트의 제너레이터에 대해 알아야합니다.

  1. Typescript
  2. Basic Of Redux
  3. Generator

리덕스-사가의 필요성과 흐름

그럼 리덕스-사가를 왜 사용할까요? 일단 리덕스의 기본을 이해했다면 리덕스는 이러한 플로우로 실행이 됩니다.

  1. 디스패치를 통해 액션 함수를 실행
  2. 액션 함수가 리턴하는 값을 확인
  3. 액션 함수에 포함된 payload를 확인
  4. 확인한 값에 따라 스토어의 상태를 변경

리덕스의 모든 실행 과정은 동기적으로 실행됩니다. 따라서 만약 API 콜과 같은 비동기 통신은 그냥 리덕스로는 진행이 불가능합니다. 따라서 redux-saga를 통하여 이러한 문제를 해결할 수 있습니다. (redux-thunk라는 더 간단한 방법이 있다고 합니다.)

리덕스-사가의 해결 방법은 액션 객체가 스토어의 상태를 변경하는 중에 비동기 통신을 중간에 끼워 넣어서 문제를 해결합니다. 여기서 우리는 리덕스-사가로 간단한 open api에서 데이터를 가져오도록 하겠습니다.

설정

npx create-react-app <YOUR_APP_NAME> --typescript

를 통해 리액트 앱을 생성해줍니다.

그리고 필요 라이브러리들을 다운 받습니다. (yarn 기준으로 작성하겠습니다.)

yarn add redux react-redux redux-saga
yarn add -D @types/react-redux

API Call

우리가 여기서 사용할 API는

입니다. get 메소드를 통해 데이터를 가져오면

{"quote": "My dad got me a drone for Christmas"}

이러한 형태로 매번 다른 간단한 데이터를 가져옵니다.(참고로 칸예 웨스트라는 힙합 아티스트가 SNS에 올린 글을 랜덤으로 가져오는 API라고 합니다.)

아래와 같이 api.ts를 작성합니다.

const fetchData = async () => {
try {
const res = await fetch("https://api.kanye.rest/")
const data = await res.json();
return data;
}
catch (err) {
console.log(err)
}
}
export default fetchData;

비동기 메소드인 fetch를 통해 데이터를 가져와서 json 형태로 반환하는 함수입니다.

Store 설정

리액트의 index.ts 파일을 아래와 같이 작성합니다.

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { createStore, applyMiddleware } from "redux";
import { rootReducer } from './reducers/rootReducer';
import { Provider } from "react-redux";
import mySaga from './reducers/saga';
import createSagaMiddleware from "redux-saga";
const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(mySaga);ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);

비동기 통신을 중간에 끼워넣는 방법은 미들웨어를 사용하는 것입니다. redux-saga에서 createSagaMiddleware를 통해 미들웨어를 설정해줍니다.

sagaMiddleware.run(mySaga);

여기서 mySaga는 이후에 우리가 직접 설정할 것입니다.

import { rootReducer } from './reducers/rootReducer';

이 부분은 이 후에 리듀서 코드를 작성할 때 추가합니다.

액션 객체

다음은 action 함수를 작성합니다. 저는 action이라는 디렉토리를 만들고 그 안에 index.ts라는 이름으로 파일을 만들었습니다.

export const REQUEST_DATA = "REQUEST_DATA" as const;
export const RECEIVE_DATA = "RECEIVE_DATA" as const;
export const requestData = () => ({
type: REQUEST_DATA,
})
export const receiveData = (data: { quote: string }) => ({
type: RECEIVE_DATA,
data
})

위와 같이 두 가지 액션 객체를 만듭니다. 하나(requestData)는 데이터를 요청할 때 리액트 컴포넌트에서 직접 디스패치 되고 receiveData는 requestData의 요청에 의해 사가에서 실행되어 스토어를 건드리는 액션이 됩니다.

Saga

mySaga.ts라는 이름으로 파일을 만들고

import { call, put, takeLatest } from "redux-saga/effects";
import { REQUEST_DATA, receiveData } from '../actions/index';
import fetchData from '../api';
function* getApiData() {
try {
const data = yield call(fetchData);
yield put(receiveData(data))
}
catch (err) {
console.log(err)
}
}
export default function* mySaga() {
yield takeLatest(REQUEST_DATA, getApiData);
}

위와 같이 작성합니다. *는 제너레이터 함수의 특징입니다. 여기서 익숙하지 않은 메소드들이 있는데 call은 일반 함수를 실행시킵니다. 만약 프로미스를 반환하는 메소드, 즉 우리가 API Call에서 작성한 비동기 통신 함수가 결과를 반환할 때 까지 기다려줍니다.

그리고 put은 액션을 dispatch해주는 역할을 합니다. 그리고 takeLatest는 들어온 요청 중 가장 마지막 요청에 대해 어떤 함수를 실행시킬지 정해줍니다.(이외 에도 take, takeEvery 등이 있습니다.) 여기서는 REQUEST_DATA라는 액션 객체가 들어오면 getApiData를 실행하도록 합니다. 그리고 saga 미들웨어 설정에서는 이 mySaga라는 객체를 export하고 리덕스 스토어에 설정해주었던 것입니다.

리듀서 작성

dataReducer.ts라는 이름으로 파일을 만들고

import { RECEIVE_DATA, receiveData } from '../actions/index';type dataActionType = ReturnType<typeof receiveData>
// 이 부분은 리덕스에 존재하는 메소드의 타입을 지정해줍니다.
const initialState = {
quote: "jesus"
}
export const dataReducer = (state = initialState, action: dataActionType) => {
switch (action.type) {
case RECEIVE_DATA:
return action.data;
default:
return state;
}
}

이렇게 작성합니다. receiveData에 대한 부분만 존재합니다. 데이터를 받아서 store에 저장해주는 역할만 합니다.

그리고 rootReducer도 작성해줍니다. rootReducer는 리덕스의 combineReducers를 통해 여러가지 리듀서를 합쳐줍니다. 우리는 하나의 리듀서만 가지고 있지만 이 방법에 익숙해지는 것이 좋기 때문에 combineReducers를 사용해 봅니다.

import { combineReducers } from "redux";
import { dataReducer } from './dataReducer';
export const rootReducer = combineReducers({
dataReducer
})
export type RootReducerType = ReturnType<typeof rootReducer>
// 타입스크립트에서는 역시 이 부분이 필요합니다.

Dispatch와 전체적인 흐름

이제 App.tsx에 아래와 같이 작성합니다.

import React, { useEffect } from 'react';
import { requestData } from './actions/index';
import { useSelector, useDispatch } from "react-redux";
import { RootReducerType } from './reducers/rootReducer';
function App() {
const dispatch = useDispatch();
const sagaData = useSelector((state: RootReducerType) => state.dataReducer);
useEffect(() => {
dispatch(requestData());
console.log(sagaData);
}, [])
return (
<div className="App">
<button onClick={() => {
dispatch(requestData())
}} >fetch</button>
<div>
{sagaData.quote}
</div>
</div>
);
}
export default App;

사용 방법은 일반적인 리덕스의 사용방법과 비슷합니다. requestData 액션을 dispatch해주고 리듀서를 selector를 통해 확인해주면 됩니다.

여기서 우리가 fetch 버튼을 누른 후 리덕스-사가의 흐름은 다음과 같습니다.

  1. requestData 액션을 dispatch
  2. index.ts에서 설정한 미들웨어에 의해 saga의 takeLatest가 실행
  3. takeLatest가 들어온 요청인 REQUEST_DATA에 의해 getApiData를 실행
  4. yield call에 의한 비동기 통신 후 데이터를 가져옴
  5. yield put에 의해 그 데이터에 따른 receiveData 액션이 실행됨
  6. receiveData가 스토어에 그 데이터를 저장
  7. useSelector를 통해 스토어의 데이터를 확인

조금 복잡할 수 있지만 위 과정을 거쳐서 스토어의 데이터가 정해집니다.

결론

지금까지 리덕스-사가를 통해 비동기 함수를 리덕스를 통해 실행해 보았습니다. 지금까지 작성한 포스트중에 가장 러닝 커브 및 진입 장벽이 높은 부분이 아니었나 싶습니다. 하지만 리덕스도 그러했듯이 리덕스-사가도 이해하고 나면 응용하기는 어렵지 않고 대체제에 비했을 때 이해한 그 순간부터 굳이 사용안할 이유가 없는 부분이기 때문에 이번에 조금 익숙해지기 위해 예제를 작성해보았습니다.

--

--

peppermint100
peppermint100

Written by peppermint100

기억하기 위해 또는 잊어버리기 위해 작성하는 블로그입니다.