Next.js에 Redux-Saga 연결 해보기

IT공부중·2020년 2월 26일
4

Next

목록 보기
8/10
post-thumbnail

Redux-Saga란

Redux만으로는 비동기적인 작업을 처리하지 못 합니다. 그래서 미들웨어를 연결해서 비동기작업 처리를 하는 코드를 짜주어야 합니다. 대표적인 미들웨어로 thunk와 saga가 있는데 saga가 더 기능이 많고 많이 쓰이는 것 같아 saga로 해보도록 합니다.

npm install redux-saga next-redux-saga

위 명령어를 입력하여 redux-saga와 next-redux-saga를 설치해줍니다. 기본적인 redux-saga를 사용하기 위한 라이브러리와 next에 redux-saga를 연결하기 위한 라이브러리입니다.

Redux-Saga 연결 해보기

/pages/_app.js

import React from "react";
import withRedux from 'next-redux-wrapper';
import { Provider } from 'react-redux'; 
import { createStore, compose, applyMiddleware } from 'redux';
import reducer from '../reducers';
import { composeWithDevTools } from 'redux-devtools-extension';
import createSagaMiddleware from "redux-saga"; // redux-saga를 생성하기 위한 라이브러리
import withReduxSaga from 'next-redux-saga'; // next와 redux-saga를 연결하기 위한 라이브러리
import rootSaga from '../sagas'; // sagas의 index.js를 가지고온다.

const Test = ({ Component, store }) => {
  return (
    <Provider store={store}>
      <Component/>
    </Provider>
  );
};

const configureStore = (initialState, options) => {
  const sagaMiddleware = createSagaMiddleware(); // 리덕스 사가 생성
  const middlewares = [sagaMiddleware]; // 미들웨어 연결
  const enhancer = process.env.NODE_ENV === 'production' ? 
    compose(applyMiddleware(...middlewares)) : 
        composeWithDevTools(
          applyMiddleware(...middlewares)
        );
  const store = createStore(reducer, initialState, enhancer); // enhancer에 넣어서 saga가 적용된 store 생성
  store.sagaTask = sagaMiddleware.run(rootSaga); // store에 rootSaga를 넣은 sagaMiddleware를 실행시켜준다.
  return store;

}
// export default withRedux(configureStore)(Test);를 아래와 같이 변경
// next가 redux 와 redux-saga가 적용되어 돌아가게 해준다.
export default withRedux(configureStore)(withReduxSaga(Test));

Redux-Saga 사용해보기 간단한 예제

앞에서 사용했던 슈퍼맨과 배트맨 TV Show 가져오기 예제를 해봅니다.

/pages/index.js

import React, { useCallback } from 'react'
import { useDispatch, useSelector } from 'react-redux';
import { loadTvShowRequestAction } from '../reducers/tvShow'; // tvShow 리듀서에서 만든 액션

const Home = () => {
  const dispatch = useDispatch();
  const { tvShowTitle, tvShowContents } = useSelector(state => state.tvShow); // tvShow redux의 state들을 불러온다.
  
  const onClickHero = useCallback((hero) => () => {
    dispatch(loadTvShowRequestAction(hero));
  }, []); // hero 넣어서 동적으로 data를 변경해주는 action 생성함수를 시행한다. 

  return ( // 고차함수라 한번 실행한 함수를 onClick에 넣어준다.
    <div>
      <button onClick={onClickHero('superman')}>슈퍼맨</button>
      <button onClick={onClickHero('batman')}>배트맨</button>
      {tvShowTitle && <div>{tvShowTitle}</div>}
      <br/>
      {tvShowContents && (
        <div>
          {tvShowContents.map(show => (
            <div key={show.id}>
              <a href={show.url}>{show.name}</a>
              <div>점수 : {show.score}</div>
              <div>타입 : {show.type}</div>
              <div>언어 : {show.language}</div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

export default Home

기본적인 화면을 세팅해줍니다. tvShowContents와 tvShowTitle이 있을 때 화면에 렌더링되게 해주었습니다.

슈퍼맨을 누르면 슈퍼맨 tvshow Request, 배트맨을 누르면 batman tvshow를 Request를 하게 reducer와 saga를 만들어 볼게요.

/reducer/index.js

import { combineReducers } from 'redux';
import count from './count';
import tvShow from './tvShow';

const rootReducer = combineReducers({
    count,
    tvShow
});

export default rootReducer;

저번에 했던 /reducer/index.js에 tvShow를 연결해줍니다. 그러면 useSelector에서 state => state.tvShow로 state에 접근할 수 있게 돼요!

/reducer/tvShow.js

// tvShow 전체 제목, 각각의 tvShow 내용들 에러 났을 때 담을 state들을 만들어줍니다.
export const initialState = {
    tvShowTitle : '',
    tvShowContents : [],
    loadTvShowError : '',
}

// 비동기 적인 작업을 해야하므로 요청, 성공, 실패로 액션 타입들을 만들어 줘요.
export const LOAD_TVSHOW_REQUEST = 'LOAD_TVSHOW_REQUEST';
export const LOAD_TVSHOW_SUCCESS = 'LOAD_TVSHOW_SUCCESS';
export const LOAD_TVSHOW_FAILURE = 'LOAD_TVSHOW_FAILURE';

// 액션 생성 함수입니다. data 부분이 동적으로 바뀔 수 있게 설정 해주었습니다.
export const loadTvShowRequestAction = (data) => ({
    type : LOAD_TVSHOW_REQUEST,
    data,
});

export const loadTvShowSuccessAction = (data) => ({
    type : LOAD_TVSHOW_SUCCESS,
    data,
});

export const loadTvShowFailureAction = (error) => ({
    type : LOAD_TVSHOW_FAILURE,
    error
});

// ...은 spread 문법으로 불변성을 지키기 위해 사용 됩니다.
// 성공시에는 받아온 배열 데이터에서 필요한 부분만 새로운 배열로 만들어서 tvShowContents에 넣어주었습니다.

const reducer = (state=initialState, action) => {
    switch (action.type) {
        case LOAD_TVSHOW_REQUEST:
            return {...state, tvShowTitle : action.data};
        case LOAD_TVSHOW_SUCCESS:
            const tvShows = action.data.map(tvShow => ({
                id : tvShow.show.id,
                score : tvShow.score,
                url : tvShow.show.url,
                name: tvShow.show.name,
                type : tvShow.show.type,
                language: tvShow.show.language            
            }))
            return {...state, tvShowContents : tvShows};
        case LOAD_TVSHOW_FAILURE:
            return {...state, loadTvShowError : action.error};
        default:
            return state;
    }
};

export default reducer;

이제 saga 부분을 세팅해 봅니다!

import { all, call } from 'redux-saga/effects';
import tvShow from './tvShow';

export default function* rootSaga() {
    yield all([
        call(tvShow),
    ])
}

*과 yield 은 제네레이터 문법입니다. 비동기를 처리 할 수 있게 합니다. all과 call은 saga 문법으로 all 안에 적힌 모든 saga들을 call(동기) 적으로 실행하겠다~ 이런 뜻으로 이해하면 될 것 같아요. fork는 비동기적으로 실행돼요.

/sagas/tvShow.js

import { all, fork, takeLatest, call, put } from 'redux-saga/effects';
import axios from 'axios';
import { LOAD_TVSHOW_REQUEST, loadTvShowSuccessAction, loadTvShowFailureAction } from '../reducers/tvShow';

function loadTvShowAPI(data) { //게시글 업로드
    return axios.get(`https://api.tvmaze.com/search/shows?q=${data}`); // data에 따라 다른 요청을 합니다.
};

function* loadTvShow(action) { 
    try { // call로 loadTvShowAPI 를 실행합니다. 인자로 action.data를 넘깁니다. call대신 fork를 쓰면 비동기적으로 지나가버려서 result에 값이 없어서 에러가 납니다.
        const result = yield call(loadTvShowAPI, action.data);
        yield put(loadTvShowSuccessAction(result.data));
    }  // put은 dispatch와 같은 역할을 합니다. 결과의 data를 Success로 보내줍니다.
    catch (e) {
        console.error(e);
        yield put(loadTvShowFailureAction(e));
    }
};

function* watchLoadTvShow() { // takeLatest : 한번에 많은 LOAD_TVSHOW_REQUEST가  들어오면 마지막 요청일 때만 loadTvShow 함수를 실행합니다.
    yield takeLatest(LOAD_TVSHOW_REQUEST, loadTvShow);
};


export default function* tvShowSaga() {
    yield all([ // watchLoadTvShow를 비동기적으로 실행합니다. 밑에 더 많은 함수들을 적을 수 있어요.
        fork(watchLoadTvShow),
    ]);
};

이렇게 세팅을 해주시면 완성입니다! 이제 npm run dev를 통해 localhost:3000으로 실행을 해주시고 확인해 보면 아래와 같은 결과가 나옵니다.

css로 꾸미진 않아서 예쁘진 않지만 비동기작업을 할 수 없던 redux를 saga를 통해 비동기작업을 하고 그 결과를 redux state로 저장 할 수 있게 됐습니다.

정리

redux-saga는 비동기작업을 못 하는 redux를 비동기 작업을 할 수 있게 해주는 미들웨어이다.
next에서 redux-saga를 연결하려면 next-redux-saga를 설치해주고 세팅해주면 된다.
saga에서 REQUEST, SUCCESS, FAILURE를 액션을 만들어서 성공했을 때 처리, 실패 했을 때 에러 처리를 해주면 된다!

profile
3년차 프론트엔드 개발자 문건우입니다.

4개의 댓글

comment-user-thumbnail
2020년 9월 17일

안녕하세요, 글 잘 읽어보고 따라해보고 있는데 에러가 나서 질문드립니다.
거의 똑같이 했는데 Provider에서 TypeError: Cannot read property 'getState' of undefined 라는 에러가 나오고 있습니다. 찾아보니까 <Provider store={store}> 의 store가 비었거나 제대로 생성되서 넘겨지지가 않는 것 같아요. 왜이런건지 혹시 알고 계신 부분이 있을까요?

1개의 답글
comment-user-thumbnail
2021년 9월 5일

오래된 글이지만 질문 하나 남겨봅니다!
Next.js를 공부하고 있는 학생입니다. Next.js를 쓰는 이유는 SSR을 위하여 사용하는 것으로 알고 있는데
getServerSideProps 대신 useDispatch를 이용하여 리덕스 사가를 사용하는 경우 Next.js를 사용하는 의미를 잃는거 아닌가요? 혹시 serverSideProps에서 사가를 사용할 수 있는지 알고 싶습니다.

답글 달기