Redux-saga in Next.js

채희태·2022년 9월 22일
0

Redux-saga?

redux-saga는 미들웨어이다.
미들웨어란 최종적인 동작 처리 이전에 특수한 동작을 만들어내는 역할을 하며
리덕스에서 최종적인 처리는 상태값에 대한 어떤 변화를 의미한다. 이 때 중간에서 api를 호출하거나, fetch를 하는 등의 동작을
redux-saga를 통해 하게 된다.
이를 통해 컴포넌트를 매우 깨끗하고 통일감이 있는 코드로 작성할 수 있다.
but, 코드량은 길어진다는 단점이 있다.


saga 구현하기 이전에

제너레이터

리덕스 사가는 자바스크립트의 제너레이터 문법을 이용한다.
function* 형태의 특수한 함수로 생성되는 제너레이터 객체는 { value, done } 속성을 가지고 있는 특수한 객체이다.
제너레이터 함수를 이용해 saga를 구현할 수 있다.
일반 함수와 달리 제너레이터 함수는 중단점을 가지는 특수한 함수이다.



제너레이터는 yield라고 하는 키워드를 중단점으로 가지며 다음 값, 동작을 제어한다. (동작이 멈춘다.)
.next()라고 하는 메서드를 받았을 때만 다음 동작을 처리하기 때문에 비동기적인 처리를 제어하기 좋다.
리듀서에 정의된 특정한 액션을 기다리다가 액션이 발생하는 시점에서 yield에 등록된 함수나 로직이 동작하게 하는 것이다.
value에 yield 이전 값이 담기고 제너레이터가 아직 끝나지 않았다면(중단점이 다음에 존재하면) done에 false를 담는다.

사가 작동 flow

store/sagas/index.js

//코드는 아래에서 위로 작성한다.
import { takeLatest, fork, all, put } from "redux-saga/effects";
import { 
  LOG_IN_REQUEST,
  LOG_IN_SUCCESS,
  LOG_IN_FAILURE,
} from "../modules/login.js"

function logInApi(data) {
  return axios.post('/api/login', data);
}

function* logIn(action) {
  const userData = yield call(logInApi, action.data)
  try {
    yield put({ type: "LOG_IN_SUCCESS", data: userData })
  } catch(err) {
    yield put({ type: "LOG_IN_FAILURE", err })	
  }
}

function* watchLogIn() {
  yield takeLatest("LOG_IN_REQUEST", logIn)
}

export default function* rootSaga() {
  yield all([
    fork(watchLogIn),
  ])
}

사가의 flow는 REQETS, SUCCESS, FAILURE로 구분된다.
flow를 단계적으로 살펴보면,

fork로 해당 제너레이터 함수(watchLogIn)를 실행한다.
all은 동시에 fork를 실행하도록 해준다.

컴포넌트에서 LOG_IN_REQUEST액션이 호출 되었다.
takeLatest는 해당 제너레이터 함수(watchLogIn)가 실행 될 때 까지 기다렸다가 실행되면 인자로 담긴 제너레이터 함수(logIn)를 실행한다.
takeLatest말고도 다른 take관련 선택지가 있다.
take: 호출을 기다렸다가 실행되면 사라진다.
takeLatest: 복수 번 빠르게 눌렀을 때 마지막 1번만 실행되도록 한다.
throttle: 시간을 설정해 그 시간 안에 1번만 클릭할 수 있도록 한다. (1000 -> 1초)

call은 첫 번째 인자로 담긴 api호출 함수를 동기적으로 실행한다.
또한, 두 번째 인자로 담긴 액션 데이터를 api호출 함수의 인자로 넣어줄 수 있다.
api호출 함수의 리턴 값을 변수(userData)에 넣고 put으로 액션 함수를 실행시킨다.(dispatch와 같은 역할)

사가 나누기

사가는 어렵지 않게 나눌 수 있다.
보통 index.js파일에 rootSaga를 둬서 합친다.

store/saga/index.js

import { all, fork } from "redux-saga/effects";
import loginSaga from "./loginSaga";

export default function* rootSaga() {
  yield all([fork(loginSaga)]);
}

store/saga/userSaga.js

import { takeLatest, fork, all, put, call, delay } from "redux-saga/effects";
import {
  LOG_IN_FAILURE,
  LOG_IN_SUCCESS,
  LOG_IN_REQUEST,
} from "../modules/login";

function logInApi(data) {
  return axios.post('/api/login', data);
}

function* logIn() {
  const userData = yield call(logInApi, action.data)
  try {
    yield put({ type: "LOG_IN_SUCCESS", data: userData })
  } catch(err) {
    yield put({ type: "LOG_IN_FAILURE", err })	
  }
}

function* watchLogIn() {
  yield takeLatest(LOG_IN_REQUEST, logIn);
}

export default function* loginSaga() {
  yield all([fork(watchLogIn)]);
}

리덕스 사가 구현하기

사가 설치
npm i redux-saga

사가 추가하기

store/configureStore.js

import { createStore, applyMiddleware, compose } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension';
import { createWrapper } from "next-redux-wrapper";
import rootReducer from "./modules";
//사가 미들웨어 import
import createSagaMiddleware from "redux-saga";
import rootSaga from "./sagas/index";

//배포 모드일 때
const isProduction = proccess.env.NODE === 'production'

const configureStore = () => {
  //sagaMiddleware 불러오기
  const sagaMiddleware = createSagaMiddleware();
  //미들웨어에 sagaMiddleware추가
  const middlewares = [sagaMiddleware];
  const enhancer = isProduction 
    ? compose(applyMiddleware(...middlewares))
  	: composeWithDevTools(applyMiddleware(...middlewares))
  const store = createStore(rootReducer, enhancer);
  //사가의 추가적인 기능 사용 사가 설계할 때 생성할 것.
  store.sagaTask = sagaMiddleware.run(rootSaga);
  return store;
};

const wrapper = createWrapper(configureStore, { debug: !isProduction });

export default wrapper;

wrapper

wrapper로 감싸야 getInitialProps, getServerSideProps, getStaticProps등에서 리덕스 스토어에 접근이 가능해진다.
또한 리덕스 사가의 완료를 위해 END를 사용해야 한다.

import { END } from 'redux-saga'

export async function wrapper.getServerSideProps(async (context) => {
  console.log(context);
  context.store.dispatch({
    type: "LOG_IN_REQEUST",
  });
  context.store.dispatch(END);
  await context.store.sagaTask.toPromise();
})

사가 설계하기

로그인과 로그아웃 기능을 리덕스 사가로 구현해보았다.

store/sagas/index.js

import { all, fork } from "redux-saga/effects";
import loginSaga from "./loginSaga";

export default function* rootSaga() {
  yield all([fork(loginSaga)]);
}

store/saga/loginSaga.js

import { takeLatest, fork, all, put, call } from "redux-saga/effects";

import {
  LOG_IN_FAILURE,
  LOG_IN_SUCCESS,
  LOG_IN_REQUEST,
  LOG_OUT_FAILURE,
  LOG_OUT_REQUEST,
  LOG_OUT_SUCCESS,
} from "../modules/login";

function logInApi(data) {
  return axios.post('/api/login', data);
}

function* logIn(action) {
  //action.data로 컴포넌트에서 호출된 액션 생성자 함수의 data가 action.data로 들어옴.
  const userData = yield call(logInApi, action.data)
  try {
    //리듀서 LOG_IN_SUCCESS 호출
    yield put({ type: LOG_IN_SUCCESS, data: userData })
  } catch(err) {
    //리듀서 LOG_IN_FAILURE 호출
    yield put({ type: LOG_IN_FAILURE, err })	
  }
}
function* logOut() {
  try {
    //리듀서 LOG_OUT_SUCCESS 호출
    yield put({ type: LOG_OUT_SUCCESS });
  } catch (err) {
    //리듀서 LOG_OUT_FAILURE 호출
    yield put({ type: LOG_OUT_FAILURE });
  }
}

function* watchLogIn() {
  //리듀서 LOG_IN_REQUEST와 동시 호출
  yield takeLatest(LOG_IN_REQUEST, logIn);
}
function* watchLogOut() {
  //리듀서 LOG_OUT_REQUEST와 동시 호출
  yield takeLatest(LOG_OUT_REQUEST, logOut);
}

export default function* loginSaga() {
  yield all([fork(watchLogIn), fork(watchLogOut)]);
}

사가에서 각 제너레이터 함수 마다 리덕스의 액션 타입을 호출 -> 실행한다.
이에 대응하기 위해 이에 맞게 리덕스의 리듀서를 설계 해준다.
*각 제너레이터 함수마다 delay를 주어 1초의 딜레이를 시켜줬다. (동작 확인용)

리듀서 설계하기

store/modules/login.js

//액션 타입
export const LOG_IN_REQUEST = "LOG_IN_REQUEST";
export const LOG_IN_SUCCESS = "LOG_IN_SUCCESS";
export const LOG_IN_FAILURE = "LOG_IN_FAILURE";
export const LOG_OUT_REQUEST = "LOG_OUT_REQUEST";
export const LOG_OUT_SUCCESS = "LOG_OUT_SUCCESS";
export const LOG_OUT_FAILURE = "LOG_OUT_FAILURE";

//액션 생성자 함수 -> 컴포넌트에서 호출 
export const logInRequest = (data) => {
  return {
    type: LOG_IN_REQUEST,
    data
  }
}
export const logOutRequest = () => {
  return {
    type: LOG_OUT_REQUEST,
  }
}

//초기 상태
const initialState = {
  isLogedIn: false,
  isLoggingIn: false,
  isLoggingOut: false,
  me: {}
}

//리듀서
const reducer = (state = initialState, action) => {
  switch(action.type) {
    //컴포넌트에서 호출됨.
    case LOG_IN_REQUEST:
      return {
        ...state,
        isLogginIn: true,
      }
    //컴포넌트에서 호출됨.
    case LOG_OUT_REQUEST: 
      return {
        ...state,
        isLogginOut: true,
      }
    //사가에서 호출됨.
    case LOG_IN_SUCCESS:
      return {
        ...state,
        isLoggedIn: true,
        isLogginIn: false,
        me: { ...action.data, nickname: '채희태' },
      }
    //사가에서 호출됨.
    case LOG_OUT_SUCCESS:
      return {
        ...state,
        isLoggedIn: false,
        isLoggingOut: false,
        me: {},
      }
    //사가에서 호출됨.
    case LOG_IN_FAILURE:
      return {
        ...state,
        isLogginIn: false,
        isLoggedIn: false,
      }
     //사가에서 호출됨.
     case LOG_OUT_FAILURE:
      return {
        ...state,
        isLogginOut: false,
      }
    default: 
      return state
  }  
}

export default reducer

로그인 flow

  1. 컴포넌트에서 액션 생성자 함수 logInRequest 호출. => dispatch(logInRequest(data))
  2. logInRequest의 액션 타입 LOG_IN_REQUEST가 리듀서와 사가에서 동시에 호출.
  3. LOG_IN_REQUEST가 호출 되었으므로 사가에서 설계한 대로 logIn 제너레이터 함수가 호출.
  4. logIn 제너레이터 함수가 호출 되었으므로 사가에서 설계한 대로 API함수를 호출하고 그 리턴 값과 액션타입 LOG_IN_SUCCESS 또는 LOG_IN_FAILURE를 put으로 리듀서 호출.

state에 로그인, 로그아웃이 request -> success 되기 전까지를 상태로서 처리하기 위해 isLoggingIn, isLoggingOut을 추가했다.
isLoggingIn, isLoggingOut이 true면 로딩창을 띄우도록 한다.

사가에서 각 제너레이터 함수 마다 호출된 액션 타입의 리턴 값을 리듀서에서 알맞게 정의해준다.

LOG_IN_SUCCESS 액션 타입에서 보내진 액션 데이터(action.data)는 사가에서 put으로 loginApiFetch 함수의 리턴 값으로 put으로 보내졌다.
=> put({ type: LOG_IN_SUCCESS, data })

리덕스 사가는 REQUEST-SUCCESS-FAILURE의 구조를 갖는다.
REQUEST는 컴포넌트에서 호출되고, SUCCESS와 FAILURE는 사가에서 호출된다.

리덕스-사가 컴포넌트에서 사용하기

  1. 로그인 버튼 클릭 -> logInRequest 액션 디스패치 -> 사가에서 LOG_IN_REQUEST가 호출됨. 동시에 리듀서에서 해당 케이스 실행.
  2. 1초 뒤 사가에서 LOG_IN_SUCCESS타입의 값을 put으로 리듀서로 디스패치함. -> isLoggedIn이 true가 됨.
profile
기록, 공부, 활용

0개의 댓글