[TS]리덕스 사가로 Fetch 하기
서론
리덕스는 전역 상태 관리를 도와주는 라이브러리입니다. 리액트와 궁합이 잘 맞기 때문에 리액트 하면 꼭 따라오는 상태 관리 라이브러리 중 하나죠. 하지만 요즈음에는 Context API나 Apollo로 대체 되기도 하고 또 다른 상태 관리 라이브러리인 MobX도 많이 사용하지만 그래도 리덕스는 여전히 쓸만 하다고 생각합니다. 리덕스의 문제점은 제가 생각하기엔 러닝 커브 단 하나입니다. 사용하는 방법만 잘 알고 있다면 다른 대체제에 비할 때 전혀 뒤질것 없는 상태 관리 라이브러리입니다. 그리고 이번엔 리덕스-사가로 비동기 요청을 하는 방법에 대해 알아 보겠습니다.
보기 전에
일단 기본적으로 Typescript로 모든 코드를 작성할 것이고 리덕스의 기본에 대해 알아야 합니다. 그리고 자바스크립트의 제너레이터에 대해 알아야합니다.
- Typescript
- Basic Of Redux
- Generator
리덕스-사가의 필요성과 흐름
그럼 리덕스-사가를 왜 사용할까요? 일단 리덕스의 기본을 이해했다면 리덕스는 이러한 플로우로 실행이 됩니다.
- 디스패치를 통해 액션 함수를 실행
- 액션 함수가 리턴하는 값을 확인
- 액션 함수에 포함된 payload를 확인
- 확인한 값에 따라 스토어의 상태를 변경
리덕스의 모든 실행 과정은 동기적으로 실행됩니다. 따라서 만약 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 버튼을 누른 후 리덕스-사가의 흐름은 다음과 같습니다.
- requestData 액션을 dispatch
- index.ts에서 설정한 미들웨어에 의해 saga의 takeLatest가 실행
- takeLatest가 들어온 요청인 REQUEST_DATA에 의해 getApiData를 실행
- yield call에 의한 비동기 통신 후 데이터를 가져옴
- yield put에 의해 그 데이터에 따른 receiveData 액션이 실행됨
- receiveData가 스토어에 그 데이터를 저장
- useSelector를 통해 스토어의 데이터를 확인
조금 복잡할 수 있지만 위 과정을 거쳐서 스토어의 데이터가 정해집니다.
결론
지금까지 리덕스-사가를 통해 비동기 함수를 리덕스를 통해 실행해 보았습니다. 지금까지 작성한 포스트중에 가장 러닝 커브 및 진입 장벽이 높은 부분이 아니었나 싶습니다. 하지만 리덕스도 그러했듯이 리덕스-사가도 이해하고 나면 응용하기는 어렵지 않고 대체제에 비했을 때 이해한 그 순간부터 굳이 사용안할 이유가 없는 부분이기 때문에 이번에 조금 익숙해지기 위해 예제를 작성해보았습니다.