Redux만으로는 비동기적인 작업을 처리하지 못 합니다. 그래서 미들웨어를 연결해서 비동기작업 처리를 하는 코드를 짜주어야 합니다. 대표적인 미들웨어로 thunk와 saga가 있는데 saga가 더 기능이 많고 많이 쓰이는 것 같아 saga로 해보도록 합니다.
npm install redux-saga next-redux-saga
위 명령어를 입력하여 redux-saga와 next-redux-saga를 설치해줍니다. 기본적인 redux-saga를 사용하기 위한 라이브러리와 next에 redux-saga를 연결하기 위한 라이브러리입니다.
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));
앞에서 사용했던 슈퍼맨과 배트맨 TV Show 가져오기 예제를 해봅니다.
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를 만들어 볼게요.
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에 접근할 수 있게 돼요!
// 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를 액션을 만들어서 성공했을 때 처리, 실패 했을 때 에러 처리를 해주면 된다!
안녕하세요, 글 잘 읽어보고 따라해보고 있는데 에러가 나서 질문드립니다.
거의 똑같이 했는데 Provider에서 TypeError: Cannot read property 'getState' of undefined 라는 에러가 나오고 있습니다. 찾아보니까
<Provider store={store}>
의 store가 비었거나 제대로 생성되서 넘겨지지가 않는 것 같아요. 왜이런건지 혹시 알고 계신 부분이 있을까요?