리덕스 사가 - redux toolkit + redux-saga 예제로 직접 사용해보기

📘 Redux-saga 개념 알아보기

redux-saga를 사용해 코드를 작성하는 방법은 아래 직접 사용해보기 부분을 참고해주세요😃


🧐 리덕스 사가란?

redux-saga는 redux의 미들웨어로, 어플리케이션의 사이드 이펙트를 더 효과적으로 관리하고자 만들어졌다.

사이드 이펙트?
API 통신이나 유저 인터렉션 등의 비동기 작업

따라서 saga는 어플리케이션에서 오로지 사이드 이펙트에만 반응하도록 만들어진 별도 스레드와 같다고 할 수 있다. 즉, 사이드 이펙트를 더 쉽게 관리하고 효과적으로 실행, 테스트, 에러처리할 수 있도록 한다.

리덕스의 action을 모니터링하고 있다가, 특정 액션이 발생하면 이에 따라 특정한 작업을 할 수 있도록 해준다. 또한 리덕스의 상태값의 접근하고 action을 dispatch할 수도 있다.



🧐 리덕스 사가, 왜 사용할까?

redux의 한계

redux에서는 action을 dispatch하면 바로 state가 변경된다.
따라서 비동기 처리가 불가능하여 별도의 라이브러리를 사용한다.
비동기 처리를 위한 라이브러리 중에는 redux-thunk, redux-saga 등이 있다.


redux-thunk vs redux-saga

redux-thunk는 함수를 디스패치 할 수 있도록 해주는 미들웨어이다.
redux-thunk가 오랜 기간 사용되어왔으나, 최근에는 redux-saga가 비동기의 다양한 상황을 처리하기 좋고 테스트나 디버깅이 쉽기 때문에 더 많이 사용되는 추세라고 한다.

조금 더 자세히 말하면...

redux-saga는 redux-thunk로 하지 못하는 다양한 작업들을 처리할 수 있는데, 예를 들면,

  • 비동기 작업 시 기존 요청을 취소가능
  • 특정 액션 발생 시 다른 액션을 디스패치하거나 js코드를 실행 가능
  • API요청 실패 시 재요청 가능

이와 같은 다양한 비동기 작업들을 처리할 수 있다.



🧐 어떻게 사용할까?

redux-saga는 generator라는 문법을 사용한다.
이 문법을 사용하면 함수의 실행을 멈추거나 원하는 시점에 함수를 다시 이어서 실행할 수 있다.

무슨 소리지?? 예시를 보자!

function weirdFunction() {
  return 1;
  return 2;
  return 3;
  return 4;
  return 5;
}

이런 함수가 있을 때, 함수는 호출하면 1만 반환한다.
하지만 generator함수를 사용하면 값을 순차적으로 여러번 반환 할 수 있다. 나아가 진행을 멈췄다가 이후에 이어서 반환하게 만들 수도 있다...!


함수 선언

function* generatorFunction() {
    console.log('안녕하세요?');
    yield 1;
    console.log('제너레이터 함수');
    yield 2;
    console.log('function*');
    yield 3;
    return 4;
}


🧐 generator 생성

함수선언: Function*

제너레이터 함수를 만들 때는 function*라는 키워드를 사용한다.

const generator = generatorFunction();

generator 객체

해당 함수를 호출하여 반환되는 generator 객체를 generator라는 변수에 할당해주었다.

이 함수를 호출했을 때 반환되는 객체, 위 예시에서 generator라는 변수에 할당된 객체를 generator라고 하며 이 객체는
{ value, done }의 속성을 갖고 있다.


이제 선언해두었던 generatorFunction은 generator가 되었다.



🧐 generator 실행

generator.next()

yield()

yield는 함수 내부에서 다음값, 동작을 제어 하는데, 뒤의 로직이나 값을 전달한 뒤 해당 함수에서 벗어나 실행을 잠시 멈춘다.
따라서 이 함수는 호출 시마다 yield한 값을 반환하고 코드의 흐름을 멈춘다.


next()

next()메서드는 yield()메서드로 멈춘 함수실행을 이어서 다음 동작을 다시 처리하도록 해준다.
따라서 다시 generator.next()를 호출하면 이어서 호출한다

정리하면, generator 함수는 코드 진행중에 yield 키워드를 만나면 일단 멈춘다. 그리고 계속 진행하라는 의미를 담은 next()메서드가 호출되면 다음 yield키워드를 만날때까지 코드를 실행시킨다.
이처럼 리덕스 사가가 Generator 문법 기반이기 때문에 비동기적 처리에 적합하다.


정리

위에서 살펴본 메서드를 사용하기 때문에 while(true)같은 코드 안에서도 비동기 동작을 잘 제어할 수 있다.

이제 기본적인 문법은 이해했다.
그럼 이 문법들이 어떻게 리덕스 사가에서 비동기 처리에 사용되는걸까?

리듀서에 정의된 특정한 액션을 기다리다가 액션이 발생하는 시점에서 yield에 등록된 함수나 로직을 동작시킨다.
이런 처리들은 리덕스 사가에 미리 정의된 여러 부수효과(effects) 함수들로 동작하게 된다.

특정한 액션을 기다리다가 발생했을 때 함수를 동작시키는 건 어떻게 할까? generator가 어떻게 액션을 모니터링 하는지는 다음 예시를 참고하자.


generator로 액션 모니터링하기

function* watchGenerator() {
    console.log('모니터링 시작!');
    while(true) {
        const action = yield;
        if (action.type === 'HELLO') {
            console.log('안녕하세요?');
        }
        if (action.type === 'BYE') {
            console.log('안녕히가세요.');
        }
    }
}
const watch = watchGenerator();

watch.next({ type: 'HELLO' });// 안녕하세요?

아직 감이 오지 않는다면, 아래 직접 사용해보기에서 코드와 설명을 확인해보자!



🧐 리덕스 사가 세팅하기

리덕스 사가를 사용하기 위해서는 첫번째로,

  • 스토어를 만들 때 미들웨어로 연결한 다음
  • 동작하는 구문을 넣어주어야 한다.

예시 코드 출처

import { createStore, combineReducers, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import user from './user';
import rootSaga from './saga';

// 여러 상태값을 변경하는 리듀서들을 하나의 리듀서 함수로 함친다.
const rootReducer = combineReducers({ user });

// 사가 미들웨어를 생성해서 스토어에 연결해준다.
const sagaMiddleware = createSagaMiddleware();

// store 생성
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));

// 사가 미들웨어에서 통합 사가 함수를 실행시킨다.
sagaMiddleware.run(rootSaga);

export default store;
  1. 리덕스 사가에서 createSagaMiddleWare()라는 메서드를 불러와 사가 미들웨어 객체를 생성한다.
  2. 생성된 사가 미들웨어 객체를 리덕스 스토어에 applyMiddleWare함수의 인자로 넘겨준다.

등록된 스토어 상태값을 변경할 때 사가 함수들을 인식할 수 있도록 하기 위함이다.

  1. 사가 미들웨어 객체에 있는 run 메서드에 통합적인 rootSaga함수를 연결해준다.

rootSaga는 이제 별도로 작성해주면 된다.



🧐 Generator 대표 문법 알아보기

주로 사용되는 문법을 정리했다.
이 문법들은 아래와 같이 모두 yield라는 키워드와 함께 사용할 수 있다.

const res = yield call(getMyInfo, action.payload)

all

  • generator 함수를 배열의 형태로 인자로 전달받는다.
  • 전달받은 generator 함수들을 동시에 실행하고 전부 resolve될 때까지 기다린다.
  • 예시
    - yield all([testSaga1(), testSaga2()])
    • testSaga1()과 testSaga2()가 동시에 실행되고 모두 resolve될 때가지 기다린다.

call

  • 비동기 함수 호출
  • 첫번째 파라미터는 함수, 나머지 파라미터는 해당 함수에 넣을 니수
    - 예시: call(delay, 1000)
    - delay(1000) 함수를 비동기적으로 호출하는 것

put

  • reducer에 특정 action 함수 전달

call과 put의 차이?

  • put은 스토어에 인자로 들어온 action을 dispatch한다.
  • call은 주어진 함수를 실행한다.

take

  • action 을 감지해 함수를 실행

takeEvery

  • action 함수를 감지해 지정된 작업을 처리
  • take를 무한하게 실행하는 것으로 while(true)문 안에서 take를 실행하는 것과 같다.
    - 예시: takeEvery(INCREASE_ASYNC, increaseSaga)
    - 전달받는 모든 INCREASE_ASYNC액션에 대해 increaseSaga함수 실행
  • 작업이 완료되기 전 다시 감지될 경우 그에 맞는 또 다른 작업 처리

takeLatest

  • action 함수를 감지해 지정된 작업 처리
  • 작업 완료 전 다시 감지될 시 기존 진행되던 작업은 종료하고 새로운 작업 처리
  • 예시
    - takeLatest(DECREASE_ASYNC, decreaseSaga)
    • DECREASE_ASYNC 액션에 대해서 기존에 진행 중이던 작업이 있다면 취소 처리하고 가장 마지막으로 실행된 작업에 대해서만 decreaseSaga함수를 실행한다.


📘 직접 사용해보기

코드 출처: https://www.youtube.com/watch?v=9MMSRn5NoFY

👉 store 생성

전체 코드

//index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";

import { Provider } from "react-redux";
import createSagaMiddleware from "redux-saga";
import { configureStore } from "@reduxjs/toolkit";
import catsReducer from "./catState";
import catSaga from "./catSaga";

const saga = createSagaMiddleware();
const store = configureStore({
  reducer: {
    cats: catsReducer,
  },
  middleware: [saga],
});
saga.run(catSaga);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

하나하나 살펴보자.

1️⃣ store 생성

configureStore()

configureStore로 리듀서와 미들웨어를 전달해 스토어를 생성한다.

const store = configureStore({
  reducer: {
    cats: catsReducer,
  },
  middleware: [saga],
});

reducer와 미들웨어로 사용할 redux-saga 객체는 다음단계에서 생성해줄 것이다.

Provider로 store 접근할 수 있도록 하기

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

2️⃣ saga 미들웨어 생성

createSagaMiddleWare()

redux

const saga = createSagaMiddleware();

run()

생성한 sagaMiddleWare 인 saga에 내장되어 있는 run 메서드를 사용해 catSaga를 실행한다.
catSaga는 별도로 정의해둔 generator 함수로, 다음단계에서 확인해보자.

saga.run(catSaga);


👉 redux 세팅

1️⃣ action, reducer 선언

전체 코드

import { createSlice } from "@reduxjs/toolkit";

export const catSlice = createSlice({
  name: "cats",
  initialState: {
    cats: [],
    isLoading: false,
  },
  reducers: {
    getCatsFetch: (state) => {
      state.isLoading = true;
    },
    getCatsSuccess: (state, action) => {
      state.cats = action.payload;
      state.isLoading = false;
    },
    getCatsFailure: (state) => {
      state.isLoading = false;
    },
  },
});

export const { getCatsFetch, getCatsSuccess, getCatsFailure } =
  catSlice.actions;
export default catSlice.reducer;

이 부분에 대해서는 다음 포스팅에 정리해두었다
👉 포스팅 [리덕스 툴킷 사용하기] 보러가기



👉 사가 함수 정의

전체코드

import { call, put, takeEvery } from "redux-saga/effects";
import { getCatsSuccess } from "./catState";

function* workGetCatsFetch() {
  const cats = yield call(() => fetch("https://api.thecatapi.com/v1/breeds"));
  const formattedCats = yield cats.json();
  const formattedCatsShortened = formattedCats.slice(0, 10);
  yield put(getCatsSuccess(formattedCatsShortened));
}

function* catSaga() {
  yield takeEvery("cats/getCatsFetch", workGetCatsFetch); 
  //cats라는 name의 slice에서 getCatsFetch리듀서
}

export default catSaga;

하나하나 살펴보자.

1️⃣ generator 함수 생성

workGetCatsFetch()

이 함수는 api를 호출해 json 문자열로 결과를 변환한뒤, 성공하면 getCatsSuccess라는 액션을 디스패치한다.

function* workGetCatsFetch() {
  const cats = yield call(() => fetch("https://api.thecatapi.com/v1/breeds"));
  const formattedCats = yield cats.json();
  const formattedCatsShortened = formattedCats.slice(0, 10);
  yield put(getCatsSuccess(formattedCatsShortened));
}

이 때 formattedCatsShortened는 fetch한 데이터를 json객체로 변환한 뒤 10개만 가져온 것이다. 이 데이터를 getCatsSuccess에 인자로 전달해주었다.

위에서 Redux의 createSlice로 선언한 slice의 리듀서 중 하나인 getCatsSuccess의 내용은 다음과 같았다.

    getCatsSuccess: (state, action) => {
      state.cats = action.payload;
      state.isLoading = false;
    },

즉, 데이터 fetch가 성공했을 경우 action.payload로 전달받은 formattedCatsSortened라는 10개의 데이터를 state의 cats에 할당해준 것이다.

참고로 state.cats는

export const catSlice = createSlice({
  name: "cats",
  initialState: {
    cats: [],
    isLoading: false,
  },

이 부분에서 slice의 initialState 내부에 선언해주었었다.

catSaga()

getCatsFetch액션이 실행되면 workGetCatsFetch함수를 호출해 원하는 데이터를 fetch 해올 수 있도록 한다.

function* catSaga() {
  yield takeEvery("cats/getCatsFetch", workGetCatsFetch); //cats라는 name의 slice에서 getCatsFetch리듀서
}

그럼 이 getCatsFetch는 어디에서 디스패치 되는 것일까?



👉 사가 함수 사용하기

전체코드


import { useEffect } from "react";
import { useDispatch } from "react-redux";
import { useSelector } from "react-redux";
import "./App.css";
import { getCatsFetch } from "./catState";

function App() {
  const cats = useSelector((state) => state.cats.cats);
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(getCatsFetch());
  }, [dispatch]);
  console.log(cats);
  return <div className="App">dd</div>;
}

export default App;

하나하나 살펴보자.

1️⃣ 초기 액션 디스패치하기

getCatsFetch()호출

페이지가 처음 렌더링 될 때 원하는 데이터를 불러와야 하므로,
useEffect를 사용해 getCatsFetch()를 호출한다.

  useEffect(() => {
    dispatch(getCatsFetch());
  }, [dispatch]);

2️⃣ saga에서 workGetCatsFetch()호출

그러면 위에서 작성한 catSaga generator 함수가 이것을 감지하고 workGetCatsFetch를 호출한다.

function* catSaga() {
  yield takeEvery("cats/getCatsFetch", workGetCatsFetch); //cats라는 name의 slice에서 getCatsFetch리듀서
}


✔️ 정리

즉, 기존 리덕스의 로직에 비동기 로직을 추가했다고 보면 된다.
정리하면,

리덕스 로직

  • getCatsFetch : isLoading을 true로 업데이트 한다.
  • getCatsSuccess: cats 라는 state에 action.payload를 할당한다. 로드를 완료했으므로 isLoading은 false로 변경한다.
  • getCatsFailure: isLoading을 false로 변경한다.

saga 로직

  • getCatsFetch가 실행되면 workGetCatsFetch()를 호출한다.
  • workGetCatsFetch()내부에서는 call메서드를 활용해 api를 fetch하는 함수를 호출했다. 그후 받아온 결과를 put메서드를 사용해 getCatsSuccess 액션으로 10개까지의 데이터를 넣어주었다.
  • getCatsSuccess는 action.payload, 즉 인자로 전달된 10개의 데이터를 Redux store의 cats라는 전역 state에 넣어준다.
  • 따라서 useSelector를 사용해 출력해보면 데이터가 잘 출력된다.



Reference

좋은 웹페이지 즐겨찾기