[Redux, Redux-Saga] Redux-Saga를 이용한 API 통신
지금까지 개발해 오면서 API 통신을 useEffect만 사용하거나, 아니면 react-Query만 사용해 봐서
이번기회에 Redux, Redux-saga 사용해 보면서 공부했던 내용을 정리해보려고 합니다.
https://github.com/HyeokjaeLee/korea-webtoon-api
GitHub - HyeokjaeLee/korea-webtoon-api: ✨ 한국의 웹툰 정보를 제공하는 API
✨ 한국의 웹툰 정보를 제공하는 API. Contribute to HyeokjaeLee/korea-webtoon-api development by creating an account on GitHub.
github.com
여기서 제공하는 웹툰 정보 API를 사용하여 Redux-Saga를 사용해 보겠습니다.
미들웨어 (middleware)란?
중간자 역할을 둬서 그 역할을 대신 수행하는 것
- 중간자 역할 : action을 dispatch 함으로써 reducer에 전달되는 데, 그 사이의 역할을 의미 (= action과 reducer의 중간)
Redux-Thunk vs Redux-Saga
Redux에서 비동기 작업을 처리할 때 가장 많이 사용하는 미들웨어 라이브러리
흔히 redux의 비동기 처리 로직 혹은 사이드 이펙트를 처리하기 위한 용도로 redux-thunk나 redux-saga를 많이 사용한다.
redux-Thunk : 액션 객체가 아닌 함수를 디스패치 할 수 있다.
redux-Saga : 액션을 모니터링하고 있다가, 특정 액션이 발생하면 이에 따라 특정 작업을 하는 방식으로 사용합니다.
- 특정 작업 : 특정 자바스크립트 실행, 다른 액션을 디스패치, 현재 상태 불러오기
Redux-Thunk
- 작은 boilerplate code
- redux-saga와 비교했을 때 상대적으로 낮은 러닝커브
- 중소규모의 프로젝트 때 사용하면 좋음
Redux-Saga
- 많은 boilerplate code
- redux-thunk와 비교했을 때 상대적으로 높은 러닝커브
- effect(yield, call, takeLatest 등)를 이용해서 좀 더 간결한 코드 유지 가능
- 대규모 프로젝트에 사용하면 좋음(애플리케이션의 상태 변화를 더 세밀하게 제어할 수 있음으로,
복잡한 비동기 흐름이 필요한 경우 적합
이렇다는데 솔직히 Redux-Thunk, Redux-Saga 둘 다 어려워서 둘 중 뭐가 더 쉽다? 이런 생각은 안 들었던 것 같습니다.
그리고 Redux-Thunk보다는 redux-Saga가 비동기 작업을 직관적으로 이해하기 편하다 생각이 듭니다.
Redux-Saga를 이용한 API 통신
순서
- redux, redux-saga, redux-logger, axios 설치
- 통신할 API 관련 코드 작성 및 세팅
- redux폴더 정의
- actionType 작성
- action 생성 함수 작성
- reducer 작성
- sagas 작성
- store 작성
- index.js에 redux 세팅하기
- App.js에 redux를 사용해서 웹툰 관련 정보 불러오기 및 화면 렌더링
1. Redux, Redux-saga, Redux-logger, Axios 설치
npm install redux react-redux redux-saga redux-logger axios
react에 redux를 설치하려면 redux 말고도 react-redux도 설치해야 됩니다.
redux-logger : redux를 통해 바뀔 이전 state, dispatch 실행으로 인해 바뀐 state가 콘솔에 찍혀 디버깅 쉽게 해주는 라이브러리
2. 통신할 API 관련 코드 작성 및 세팅
src
|- api
|- webtoon.js
api/webtoon.js
import axios from "axios";
const instance = axios.create({
baseURL: "https://korea-webtoon-api.herokuapp.com",
headers: { "X-Custom-Header": "foobar" },
});
export const getWebtoons = async (
page = 1,
perPage = 10,
service,
updateDay
) => {
const result = await instance.get("/", {
params: {
page,
perPage,
service,
updateDay,
},
});
return result;
};
3. redux폴더 정의
src
|- redux
|- actionType.js : 액션 타입 정의
|- action.js : 액션 생성 함수 생성
|- reducers.js : 리듀서 생성
|- sagas.js : worker saga, watcher saga, root saga 생성
|- store.js : redux saga 미들웨어 생성, store 생성
액션 (Action) :
상태에 어떠한 변화가 필요하게 될 때 액션이란 것을 발생시키며, 하나의 객체로 표현
액션 객체는 type 필드를 필수적으로 가져야 된다.
액션 생성 함수 (Action Creator) :
액션을 만드는 함수, 파라미터를 받아와서 액션을 객체 형태로 만들어 준다.
리듀서 (Reducer) :
변화를 일으키는 함수, 리듀서는 state, action 이 두 가지의 파라미터를 받아온다.
리듀서는 현재의 상태, 전달받은 액션을 참고하여 새로운 상태를 만들어서 반환 (useReducer와 똑같은 형태)
스토어 (Store) :
리덕스에서는 한 애플리케이션당 하나의 스토어를 만들게 된다.
스토어 안에는 현재의 앱의 상태, 리듀서, 추가적으로 몇 가지 내장 함수들이 있다.
디스패치 (dispatch) :
스토어의 내장함수 중 하나이며, 액션을 발생시키는 것이라고 이해하면 된다.
dispatch라는 함수에는 액션을 파라미터로 전달한다. dispatch(action) 이런 식으로
redux로 프로미스를 다룰 경우 주의해야 될 점
- 프로미스가 시작, 성공, 실패했을 때에 따른 액션을 디스패치해야 된다.
- 리듀서에는 액션에 따라 로딩 중, 결과, 에러 상태를 변경해주어야 한다.
4. actionType 작성
액션 (Action) :
상태에 어떠한 변화가 필요하게 될 때 액션이란 것을 발생시키며, 하나의 객체로 표현
액션 객체는 type 필드를 필수적으로 가져야 된다.
redux/actionType.js
// 액션 타입 정의
export const GET_WEBTOONS_REQUEST = "GET_WEBTOONS_REQUEST"; // 요청 시작
export const GET_WEBTOONS_SUCCESS = "GET_WEBTOONS_SUCCESS"; // 요청 성공
export const GET_WEBTOONS_FAILURE = "GET_WEBTOONS_FAILURE"; // 요청 실패
액션 타입은 주로 대문자로 작성합니다.
5. action 생성 함수 작성
액션 생성 함수 (Action Creator) :
액션을 만드는 함수, 파라미터를 받아와서 액션을 객체 형태로 만들어 준다.
redux/actions.js
import * as types from "./actionTypes";
// 액션 생성 함수
export const getWebtoonsRequest = (page, perPage, service, updateDay) => ({
type: types.GET_WEBTOONS_REQUEST,
payload: { page, perPage, service, updateDay },
});
export const getWebtoonsSuccess = (webtoons) => ({
type: types.GET_WEBTOONS_SUCCESS,
payload: webtoons,
});
export const getWebtoonsFailure = (error) => ({
type: types.GET_WEBTOONS_FAILURE,
payload: error,
});
6. reducer 작성
리듀서 (Reducer) :
변화를 일으키는 함수, 리듀서는 state, action 이 두 가지의 파라미터를 받아온다.
리듀서는 현재의 상태, 전달받은 액션을 참고하여 새로운 상태를 만들어서 반환 (useReducer와 똑같은 형태)
redux/reducers.js
import * as types from "./actionTypes";
// 초기 상태 정의
const initialState = {
loading: false,
data: null,
error: null,
};
// 리듀서 정의
const webtoonsReducer = (state = initialState, action) => {
switch (action.type) {
case types.GET_WEBTOONS_REQUEST:
return { ...state, loading: true, error: null };
case types.GET_WEBTOONS_SUCCESS:
return { ...state, loading: false, data: action.payload, error: null };
case types.GET_WEBTOONS_FAILURE:
return { ...state, loading: false, data: null, error: action.payload };
default:
return state;
}
};
export default webtoonsReducer;
리듀서에는 액션에 따라 로딩 중, 결과, 에러 상태를 변경해주어야 한다.
- 요청 : {loading: true, data: null, error: null}
- 성공 : {loading: false, data: action.payload, error: null}
- 실패 : {loading: false, data: null, error: action.payload}
7. sagas 작성
Redux-Saga에서 Generator 함수를 사용하여 비동기 작업을 관리합니다.
제너레이터 함수를 만들 때에는 function*이라는 키워드를 사용합니다.
제너레이터 함수에서는 yield를 통해 액션을 모니터링하고, 비동기 작업을 수행하고, 성공 또는 실패에 따른 액션을 디스패치하여 애플리케이션의 비동기 흐름을 제어합니다.
redux-saga effect
effect는 yield뒤에 오는 함수로 각자의 역할을 수행한다.
- call : 함수를 동기적으로 실행시켜 준다. (함수를 실행시켜 주는 이펙트)
- fork : 함수를 비동기로 실행시켜 준다. (함수를 실행시켜 주는 이펙트)
- put : 특정 action을 dispatch 시켜준다.(파라미터로는 액션 객체가 들어간다.)
- take : 해당 액션이 dispatch 되면 제너레이터를 next 한다. (단점 : 1회용)
- takeEvery : 모든 액션에 대하여 동작한다. (비동기)
- takeLatest : 같은 종류의 액션이 여러 번 요청된다면 가장 마지막으로 dispatch 된 액션을 동작을 실행한다. (특정 버튼을 여러 번 클릭하는 경우)
- throttle : 마지막 함수가 호출된 후 일정 시간이 지나기 전에 다시 호출되지 않도록 하는 것(특수한 경우에만 사용, 보통 take Latest사용)
- delay : 정해 놓은 시간 동안 비동기적인 효과를 준다.
- all : 여러 사가를 합쳐주는 역할을 한다. (all은 파라미터로 배열을 받으며 배열 내에 있는 것을 모두 실행)
redux/sagas.js
import { all, call, put, takeEvery } from "redux-saga/effects";
import * as types from "./actionTypes";
import * as actions from "./actions";
import * as webtoonAPI from "../api/webtoon";
// API 호출을 담당하는 worker saga
// 액션이 지니고 있는 값을 조회 싶다면 action을 파라미터로 받아와서 사용 가능
function* getWebtoonsSaga(action) {
try {
// API 함수에 넣어주고 싶은 인자는 call 함수의 두번째 인자부터 순서대로 넣어주면 된다.
// 액션 생성 함수에서 넘겨줬기 때문에 사용가능
const { page, perPage, service, updateDay } = action.payload;
const response = yield call(
webtoonAPI.getWebtoons,
page,
perPage,
service,
updateDay
);
yield put(actions.getWebtoonsSuccess(response.data));
} catch (error) {
yield put(actions.getWebtoonsFailure(error.message));
}
}
// Watcher saga: 특정 액션을 감시하고 특정 액션이 발생할 때 worker saga를 호출
function* watchGetWebtoons() {
yield takeEvery(types.GET_WEBTOONS_REQUEST, getWebtoonsSaga);
}
// Root saga: 여러 watcher saga들을 결합
export default function* rootSaga() {
yield all([watchGetWebtoons()]);
}
8. store 작성
스토어 (Store) :
리덕스에서는 한 애플리케이션당 하나의 스토어를 만들게 된다.
스토어 안에는 현재의 앱의 상태, 리듀서, 추가적으로 몇 가지 내장 함수들이 있다.
redux/store.js
import { createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
import rootSaga from "./sagas";
import rootReducer from "./reducers";
import logger from "redux-logger";
// Redux Saga 미들웨어 생성
const sagaMiddleware = createSagaMiddleware();
// Store 생성
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware, logger));
// Saga middleware 실행
sagaMiddleware.run(rootSaga);
export default store;
9. index.js에 redux 세팅하기
index.js
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
/* redux */
import { Provider } from "react-redux";
import store from "./redux/store";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
reportWebVitals();
10. App.js에 redux를 사용해서 웹툰 관련 정보 불러오기 및 화면 렌더링
App.js
useSelecotr : 리덕스 스토어의 상태를 조회하는 Hook
useDispatch : 리덕스 스토어의 dispatch를 함수에서 사용할 수 있게 해주는 Hook
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getWebtoonsRequest } from "./redux/actions";
const App = () => {
const dispatch = useDispatch();
const { loading, data, error } = useSelector((state) => state);
const [requestParams, setRequestParams] = useState({
page: 1,
perPage: 10,
service: "",
updateDay: "",
});
useEffect(() => {
dispatch(getWebtoonsRequest(requestParams));
}, [dispatch, requestParams]);
const handleServiceButtonClick = (service) => {
setRequestParams((prevParams) => ({
...prevParams,
service,
}));
};
const handleDayButtonClick = (updateDay) => {
setRequestParams((prevParams) => ({
...prevParams,
updateDay,
}));
};
if (loading === undefined) {
return <p>Loading...</p>;
}
if (error) {
return <p>Error: {error}</p>;
}
return (
<div>
<h1>Webtoons</h1>
<div>
<h2>Service: {requestParams.service}</h2>
<button onClick={() => handleServiceButtonClick("naver")}>Naver</button>
<button onClick={() => handleServiceButtonClick("kakao")}>Kakao</button>
<button onClick={() => handleServiceButtonClick("kakaoPage")}>
KakaoPage
</button>
</div>
<div>
<h2>Update Day: {requestParams.updateDay}</h2>
<button onClick={() => handleDayButtonClick("mon")}>Monday</button>
<button onClick={() => handleDayButtonClick("tue")}>Tuesday</button>
<button onClick={() => handleDayButtonClick("wed")}>Wednesday</button>
<button onClick={() => handleDayButtonClick("thu")}>Thursday</button>
<button onClick={() => handleDayButtonClick("fri")}>Friday</button>
<button onClick={() => handleDayButtonClick("sat")}>Saturday</button>
<button onClick={() => handleDayButtonClick("sun")}>Sunday</button>
</div>
<ul>
{data &&
data.webtoons.map((webtoon) => (
<li key={webtoon._id}>
<a href={webtoon.url}>
<img src={webtoon.img} alt={webtoon.title} />
</a>
{webtoon.title} - {webtoon.author}
</li>
))}
</ul>
</div>
);
};
export default App;
css 작업을 따로 안 해서 지금은 이렇습니다.
참고자료
https://react.vlpt.us/redux-middleware/
7장. 리덕스 미들웨어 · GitBook
리덕스 미들웨어 리덕스 미들웨어는 리덕스가 지니고 있는 핵심 기능입니다. Context API 또는 MobX를 사용하는것과 차별화가 되는 부분이죠. 리덕스 미들웨어를 사용하면 액션이 디스패치 된 다음,
react.vlpt.us
https://velog.io/@0715yk/FE-Redux-Thunk-RTK-vs-Redux-Saga
[FE] Redux-Thunk vs Redux-Saga
: 흔히 redux의 비동기 처리 로직 혹은 사이드 이펙트를 처리하기 위한 용도로 redux-thunk나 redux-saga를 많이 사용한다. 이번 포스팅에서는 앞서 말한 redux와 연관되는 라이브러리가 필요한 이유부터
velog.io
https://loosie.tistory.com/109
[React/ Next.js] Redux-saga 설치 및 알아보기 (vs thunk / generator, effect)
1. redux-thunk vs redux-saga thunk는 리덕스에서 비동기 작업을 처리할 때 많이 사용한다. 아래와 같이 9줄정도 되는 코드로 1만5천개정도의 git Star를 받았다. function createThunkMiddleware(extraArgument) { return ({
loosie.tistory.com
[React] redux-saga 시작하기! 기본적인 effects 활용
💥 redux-saga 비동기 작업을 하는 미들웨어가 필요할 때 사용한다. 특정 액션이 발생했을 때 상태 값이나 응답 상태 등에 따라 다른 액션을 디스패치 하거나 추가적인 로직을 적용 해야될 때 사용
juhi.tistory.com