라이브러리

[Redux, Redux-Saga] Redux-Saga를 이용한 API 통신

개발조각 2024. 1. 23. 18:22
728x90
반응형

지금까지 개발해 오면서 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 통신

순서

  1. redux, redux-saga, redux-logger, axios 설치
  2. 통신할 API 관련 코드 작성 및 세팅
  3. redux폴더 정의
  4. actionType 작성
  5. action 생성 함수 작성
  6. reducer 작성
  7. sagas 작성
  8. store 작성
  9. index.js에 redux 세팅하기
  10. 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가 콘솔에 찍혀 디버깅 쉽게 해주는 라이브러리

redux-logger 사용시

 

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

https://juhi.tistory.com/25

 

[React] redux-saga 시작하기! 기본적인 effects 활용

💥 redux-saga 비동기 작업을 하는 미들웨어가 필요할 때 사용한다. 특정 액션이 발생했을 때 상태 값이나 응답 상태 등에 따라 다른 액션을 디스패치 하거나 추가적인 로직을 적용 해야될 때 사용

juhi.tistory.com

 

728x90
반응형