10. redux-saga : "μ‘μ μ λͺ¨λν°λ§νκ³ μλ€κ°, νΉμ μ‘μ μ΄ λ°μνλ©΄ μ΄μ λ°λΌ νΉμ μμ μ νλ€."
μλ₯Ό λ€μ΄,,,
- λΉλκΈ° μμ μ ν λ κΈ°μ‘΄ μμ²μ μ·¨μ μ²λ¦¬ ν μ μμ΅λλ€
- νΉμ μ‘μ μ΄ λ°μνμ λ μ΄μ λ°λΌ λ€λ₯Έ μ‘μ μ΄ λμ€ν¨μΉλκ²λ νκ±°λ, μλ°μ€ν¬λ¦½νΈ μ½λλ₯Ό μ€ν ν μ μμ΅λλ€.
- μΉμμΌμ μ¬μ©νλ κ²½μ° Channel μ΄λΌλ κΈ°λ₯μ μ¬μ©νμ¬ λμ± ν¨μ¨μ μΌλ‘ μ½λλ₯Ό κ΄λ¦¬ ν μ μμ΅λλ€ (μ°Έκ³ )
- API μμ²μ΄ μ€ν¨νμ λ μ¬μμ²νλ μμ μ ν μ μμ΅λλ€.
Generator λ¬Έλ² λ°°μ°κΈ°
=> ν¨μλ₯Ό μμ±ν λ ν¨μλ₯Ό νΉμ ꡬκ°μ λ©μΆ° λμ μλ μκ³ , μν λ λ€μ λμκ°κ² ν μλ μλ€.
=> κ²°κ³Όκ°μ μ¬λ¬λ² λ°νν μλ μλ€.
function* sumGenerator() {
console.log('sumGeneratorμ΄ μμλμ΅λλ€.');
let a = yield;
console.log('aκ°μ λ°μμ΅λλ€.');
let b = yield;
console.log('bκ°μ λ°μμ΅λλ€.');
yield a + b;
}
Generatorλ‘ μ‘μ λͺ¨λν°λ§νκΈ°
function* watchGenerator() {
console.log('λͺ¨λν°λ§ μμ!');
while(true) {
const action = yield;
if (action.type === 'HELLO') {
console.log('μλ
νμΈμ?');
}
if (action.type === 'BYE') {
console.log('μλ
νκ°μΈμ.');
}
}
}
리λμ€ μ¬κ° μ€μΉ λ° λΉλκΈ° μΉ΄μ΄ν° λ§λ€κΈ°
$ yarn add redux-saga
import createSagaMiddleware from 'redux-saga';
<moduels/counter.js>
import { delay, put, takeEvery, takeLatest } from 'redux-saga/effects';
// μ‘μ
νμ
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
const INCREASE_ASYNC = 'INCREASE_ASYNC';
const DECREASE_ASYNC = 'DECREASE_ASYNC';
// μ‘μ
μμ± ν¨μ
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
export const increaseAsync = () => ({ type: INCREASE_ASYNC });
export const decreaseAsync = () => ({ type: DECREASE_ASYNC });
function* increaseSaga() {
yield delay(1000); // 1μ΄λ₯Ό κΈ°λ€λ¦½λλ€.
yield put(increase()); // putμ νΉμ μ‘μ
μ λμ€ν¨μΉ ν΄μ€λλ€.
}
function* decreaseSaga() {
yield delay(1000); // 1μ΄λ₯Ό κΈ°λ€λ¦½λλ€.
yield put(decrease()); // putμ νΉμ μ‘μ
μ λμ€ν¨μΉ ν΄μ€λλ€.
}
export function* counterSaga() {
yield takeEvery(INCREASE_ASYNC, increaseSaga); // λͺ¨λ INCREASE_ASYNC μ‘μ
μ μ²λ¦¬
yield takeLatest(DECREASE_ASYNC, decreaseSaga); // κ°μ₯ λ§μ§λ§μΌλ‘ λμ€ν¨μΉλ DECREASE_ASYNC μ‘μ
λ§μ μ²λ¦¬
}
// μ΄κΉκ° (μνκ° κ°μ²΄κ° μλλΌ κ·Έλ₯ μ«μμ¬λ μκ΄ μμ΅λλ€.)
const initialState = 0;
export default function counter(state = initialState, action) {
switch (action.type) {
case INCREASE:
return state + 1;
case DECREASE:
return state - 1;
default:
return state;
}
}
takeEvery: νΉμ μ‘μ νμ μ λνμ¬ λμ€ν¨μΉλλ λͺ¨λ μ‘μ λ€μ μ²λ¦¬
takeLatest: νΉμ μ‘μ νμ μ λνμ¬ λμ€ν¨μΉλ κ°μ₯ λ§μ§λ§ μ‘μ λ§μ μ²λ¦¬
<moduels/index.js> λ£¨νΈ μ¬κ° λ§λ€κΈ°
import { combineReducers } from 'redux';
import counter, { counterSaga } from './counter';
import posts from './posts';
import { all } from 'redux-saga/effects';
const rootReducer = combineReducers({ counter, posts });
export function* rootSaga() {
yield all([counterSaga()]); // all μ λ°°μ΄ μμ μ¬λ¬ μ¬κ°λ₯Ό λμμ μ€νμμΌμ€λλ€.
}
export default rootReducer;
<index.js>
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import rootReducer, { rootSaga } from './modules';
import logger from 'redux-logger';
import { composeWithDevTools } from 'redux-devtools-extension';
import ReduxThunk from 'redux-thunk';
import { Router } from 'react-router-dom';
import { createBrowserHistory } from 'history';
import createSagaMiddleware from 'redux-saga';
const customHistory = createBrowserHistory();
const sagaMiddleware = createSagaMiddleware(); // μ¬κ° λ―Έλ€μ¨μ΄λ₯Ό λ§λλλ€.
const store = createStore(
rootReducer,
// logger λ₯Ό μ¬μ©νλ κ²½μ°, loggerκ° κ°μ₯ λ§μ§λ§μ μμΌν©λλ€.
composeWithDevTools(
applyMiddleware(
ReduxThunk.withExtraArgument({ history: customHistory }),
sagaMiddleware, // μ¬κ° λ―Έλ€μ¨μ΄λ₯Ό μ μ©νκ³
logger
)
)
); // μ¬λ¬κ°μ λ―Έλ€μ¨μ΄λ₯Ό μ μ© ν μ μμ΅λλ€.
sagaMiddleware.run(rootSaga); // λ£¨νΈ μ¬κ°λ₯Ό μ€νν΄μ€λλ€.
// μ£Όμ: μ€ν μ΄ μμ±μ΄ λ λ€μμ μ μ½λλ₯Ό μ€νν΄μΌν©λλ€.
ReactDOM.render(
<Router history={customHistory}>
<Provider store={store}>
<App />
</Provider>
</Router>,
document.getElementById('root')
);
serviceWorker.unregister();
<App.js>
import React from 'react';
import { Route } from 'react-router-dom';
import PostListPage from './pages/PostListPage';
import PostPage from './pages/PostPage';
import CounterContainer from './containers/CounterContainer';
function App() {
return (
<>
<CounterContainer />
<Route path="/" component={PostListPage} exact={true} />
<Route path="/:id" component={PostPage} />
</>
);
}
export default App;
11. redux-sagaλ‘ νλ‘λ―Έμ€ λ€λ£¨κΈ°
<modules/posts.js>
import * as postsAPI from '../api/posts'; // api/posts μμ ν¨μ λͺ¨λ λΆλ¬μ€κΈ°
import {
reducerUtils,
handleAsyncActions,
handleAsyncActionsById
} from '../lib/asyncUtils';
import { call, put, takeEvery } from 'redux-saga/effects';
/* μ‘μ
νμ
*/
// ν¬μ€νΈ μ¬λ¬κ° μ‘°ννκΈ°
const GET_POSTS = 'GET_POSTS'; // μμ² μμ
const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS'; // μμ² μ±κ³΅
const GET_POSTS_ERROR = 'GET_POSTS_ERROR'; // μμ² μ€ν¨
// ν¬μ€νΈ νλ μ‘°ννκΈ°
const GET_POST = 'GET_POST';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_ERROR = 'GET_POST_ERROR';
export const getPosts = () => ({ type: GET_POSTS });
// payloadλ νλΌλ―Έν° μ©λ, metaλ 리λμμμ idλ₯Ό μκΈ°μν μ©λ
export const getPost = id => ({ type: GET_POST, payload: id, meta: id });
function* getPostsSaga() {
try {
const posts = yield call(postsAPI.getPosts); // call μ μ¬μ©νλ©΄ νΉμ ν¨μλ₯Ό νΈμΆνκ³ , κ²°κ³Όλ¬Όμ΄ λ°ν λ λκΉμ§ κΈ°λ€λ €μ€ μ μμ΅λλ€.
yield put({
type: GET_POSTS_SUCCESS,
payload: posts
}); // μ±κ³΅ μ‘μ
λμ€ν¨μΉ
} catch (e) {
yield put({
type: GET_POSTS_ERROR,
error: true,
payload: e
}); // μ€ν¨ μ‘μ
λμ€ν¨μΉ
}
}
// μ‘μ
μ΄ μ§λκ³ μλ κ°μ μ‘°ννκ³ μΆλ€λ©΄ actionμ νλΌλ―Έν°λ‘ λ°μμμ μ¬μ© ν μ μμ΅λλ€.
function* getPostSaga(action) {
const param = action.payload;
const id = action.meta;
try {
const post = yield call(postsAPI.getPostById, param); // API ν¨μμ λ£μ΄μ£Όκ³ μΆμ μΈμλ call ν¨μμ λλ²μ§Έ μΈμλΆν° μμλλ‘ λ£μ΄μ£Όλ©΄ λ©λλ€.
yield put({
type: GET_POST_SUCCESS,
payload: post,
meta: id
});
} catch (e) {
yield put({
type: GET_POST_ERROR,
error: true,
payload: e,
meta: id
});
}
}
// μ¬κ°λ€μ ν©μΉκΈ°
export function* postsSaga() {
yield takeEvery(GET_POSTS, getPostsSaga);
yield takeEvery(GET_POST, getPostSaga);
}
// 3λ²μ§Έ μΈμλ₯Ό μ¬μ©νλ©΄ withExtraArgument μμ λ£μ΄μ€ κ°λ€μ μ¬μ© ν μ μμ΅λλ€.
export const goToHome = () => (dispatch, getState, { history }) => {
history.push('/');
};
// initialState μͺ½λ λ°λ³΅λλ μ½λλ₯Ό initial() ν¨μλ₯Ό μ¬μ©ν΄μ 리ν©ν λ§ νμ΅λλ€.
const initialState = {
posts: reducerUtils.initial(),
post: reducerUtils.initial()
};
export default function posts(state = initialState, action) {
switch (action.type) {
case GET_POSTS:
case GET_POSTS_SUCCESS:
case GET_POSTS_ERROR:
return handleAsyncActions(GET_POSTS, 'posts', true)(state, action);
case GET_POST:
case GET_POST_SUCCESS:
case GET_POST_ERROR:
return handleAsyncActionsById(GET_POST, 'post', true)(state, action);
default:
return state;
}
}
<modules/index.js> rootSaga
import { combineReducers } from 'redux';
import counter, { counterSaga } from './counter';
import posts, { postsSaga } from './posts';
import { all } from 'redux-saga/effects';
const rootReducer = combineReducers({ counter, posts });
export function* rootSaga() {
yield all([counterSaga(), postsSaga()]); // all μ λ°°μ΄ μμ μ¬λ¬ μ¬κ°λ₯Ό λμμ μ€νμμΌμ€λλ€.
}
export default rootReducer;
νλ‘λ―Έμ€λ₯Ό μ²λ¦¬νλ μ¬κ° 리ν©ν λ§
<lib/asyncUtils.js> createPromiseSaga/createPromiseSagaById λ§λ€κΈ°
import { call, put } from 'redux-saga/effects';
// νλ‘λ―Έμ€λ₯Ό κΈ°λ€λ Έλ€κ° κ²°κ³Όλ₯Ό λμ€ν¨μΉνλ μ¬κ°
export const createPromiseSaga = (type, promiseCreator) => {
const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];
return function* saga(action) {
try {
// μ¬μ¬μ©μ±μ μνμ¬ promiseCreator μ νλΌλ―Έν°μ action.payload κ°μ λ£λλ‘ μ€μ ν©λλ€.
const payload = yield call(promiseCreator, action.payload);
yield put({ type: SUCCESS, payload });
} catch (e) {
yield put({ type: ERROR, error: true, payload: e });
}
};
};
// νΉμ idμ λ°μ΄ν°λ₯Ό μ‘°ννλ μ©λλ‘ μ¬μ©νλ μ¬κ°
// APIλ₯Ό νΈμΆ ν λ νλΌλ―Έν°λ action.payloadλ₯Ό λ£κ³ ,
// id κ°μ action.metaλ‘ μ€μ ν©λλ€.
export const createPromiseSagaById = (type, promiseCreator) => {
const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];
return function* saga(action) {
const id = action.meta;
try {
const payload = yield call(promiseCreator, action.payload);
yield put({ type: SUCCESS, payload, meta: id });
} catch (e) {
yield put({ type: ERROR, error: e, meta: id });
}
};
};
// 리λμμμ μ¬μ© ν μ μλ μ¬λ¬ μ νΈ ν¨μλ€μ
λλ€.
export const reducerUtils = {
// μ΄κΈ° μν. μ΄κΈ° data κ°μ κΈ°λ³Έμ μΌλ‘ null μ΄μ§λ§
// λ°κΏ μλ μμ΅λλ€.
initial: (initialData = null) => ({
loading: false,
data: initialData,
error: null
}),
// λ‘λ©μ€ μν. prevStateμ κ²½μ°μ κΈ°λ³Έκ°μ null μ΄μ§λ§
// λ°λ‘ κ°μ μ§μ νλ©΄ null λ‘ λ°κΎΈμ§ μκ³ λ€λ₯Έ κ°μ μ μ§μν¬ μ μμ΅λλ€.
loading: (prevState = null) => ({
loading: true,
data: prevState,
error: null
}),
// μ±κ³΅ μν
success: payload => ({
loading: false,
data: payload,
error: null
}),
// μ€ν¨ μν
error: error => ({
loading: false,
data: null,
error: error
})
};
// λΉλκΈ° κ΄λ ¨ μ‘μ
λ€μ μ²λ¦¬νλ 리λμλ₯Ό λ§λ€μ΄μ€λλ€.
// type μ μ‘μ
μ νμ
, key λ μνμ key (μ: posts, post) μ
λλ€.
export const handleAsyncActions = (type, key, keepData = false) => {
const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];
return (state, action) => {
switch (action.type) {
case type:
return {
...state,
[key]: reducerUtils.loading(keepData ? state[key].data : null)
};
case SUCCESS:
return {
...state,
[key]: reducerUtils.success(action.payload)
};
case ERROR:
return {
...state,
[key]: reducerUtils.error(action.payload)
};
default:
return state;
}
};
};
// idλ³λ‘ μ²λ¦¬νλ μ νΈν¨μ
export const handleAsyncActionsById = (type, key, keepData = false) => {
const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];
return (state, action) => {
const id = action.meta;
switch (action.type) {
case type:
return {
...state,
[key]: {
...state[key],
[id]: reducerUtils.loading(
// state[key][id]κ° λ§λ€μ΄μ Έμμ§ μμ μλ μμΌλκΉ μ ν¨μ±μ λ¨Όμ κ²μ¬ ν data μ‘°ν
keepData ? state[key][id] && state[key][id].data : null
)
}
};
case SUCCESS:
return {
...state,
[key]: {
...state[key],
[id]: reducerUtils.success(action.payload)
}
};
case ERROR:
return {
...state,
[key]: {
...state[key],
[id]: reducerUtils.error(action.payload)
}
};
default:
return state;
}
};
};
<modules/posts.js>
import * as postsAPI from '../api/posts'; // api/posts μμ ν¨μ λͺ¨λ λΆλ¬μ€κΈ°
import {
reducerUtils,
handleAsyncActions,
handleAsyncActionsById,
createPromiseSaga,
createPromiseSagaById
} from '../lib/asyncUtils';
import { takeEvery } from 'redux-saga/effects';
/* μ‘μ
νμ
*/
// ν¬μ€νΈ μ¬λ¬κ° μ‘°ννκΈ°
const GET_POSTS = 'GET_POSTS'; // μμ² μμ
const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS'; // μμ² μ±κ³΅
const GET_POSTS_ERROR = 'GET_POSTS_ERROR'; // μμ² μ€ν¨
// ν¬μ€νΈ νλ μ‘°ννκΈ°
const GET_POST = 'GET_POST';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_ERROR = 'GET_POST_ERROR';
export const getPosts = () => ({ type: GET_POSTS });
export const getPost = id => ({ type: GET_POST, payload: id, meta: id });
const getPostsSaga = createPromiseSaga(GET_POSTS, postsAPI.getPosts);
const getPostSaga = createPromiseSagaById(GET_POST, postsAPI.getPostById);
// μ¬κ°λ€μ ν©μΉκΈ°
export function* postsSaga() {
yield takeEvery(GET_POSTS, getPostsSaga);
yield takeEvery(GET_POST, getPostSaga);
}
// 3λ²μ§Έ μΈμλ₯Ό μ¬μ©νλ©΄ withExtraArgument μμ λ£μ΄μ€ κ°λ€μ μ¬μ© ν μ μμ΅λλ€.
export const goToHome = () => (dispatch, getState, { history }) => {
history.push('/');
};
// initialState μͺ½λ λ°λ³΅λλ μ½λλ₯Ό initial() ν¨μλ₯Ό μ¬μ©ν΄μ 리ν©ν λ§ νμ΅λλ€.
const initialState = {
posts: reducerUtils.initial(),
post: reducerUtils.initial()
};
export default function posts(state = initialState, action) {
switch (action.type) {
case GET_POSTS:
case GET_POSTS_SUCCESS:
case GET_POSTS_ERROR:
return handleAsyncActions(GET_POSTS, 'posts', true)(state, action);
case GET_POST:
case GET_POST_SUCCESS:
case GET_POST_ERROR:
return handleAsyncActionsById(GET_POST, 'post', true)(state, action);
default:
return state;
}
}
12. sagaμμ λΌμ°ν° μ°λνκΈ°
import { createBrowserHistory } from 'history';
<index.js>
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import rootReducer, { rootSaga } from './modules';
import logger from 'redux-logger';
import { composeWithDevTools } from 'redux-devtools-extension';
import ReduxThunk from 'redux-thunk';
import { Router } from 'react-router-dom';
import { createBrowserHistory } from 'history';
import createSagaMiddleware from 'redux-saga';
const customHistory = createBrowserHistory();
const sagaMiddleware = createSagaMiddleware({
context: {
history: customHistory
}
}); // μ¬κ° λ―Έλ€μ¨μ΄λ₯Ό λ§λλλ€.
const store = createStore(
rootReducer,
// logger λ₯Ό μ¬μ©νλ κ²½μ°, loggerκ° κ°μ₯ λ§μ§λ§μ μμΌν©λλ€.
composeWithDevTools(
applyMiddleware(
ReduxThunk.withExtraArgument({ history: customHistory }),
sagaMiddleware, // μ¬κ° λ―Έλ€μ¨μ΄λ₯Ό μ μ©νκ³
logger
)
)
); // μ¬λ¬κ°μ λ―Έλ€μ¨μ΄λ₯Ό μ μ© ν μ μμ΅λλ€.
sagaMiddleware.run(rootSaga); // λ£¨νΈ μ¬κ°λ₯Ό μ€νν΄μ€λλ€.
// μ£Όμ: μ€ν μ΄ μμ±μ΄ λ λ€μμ μ μ½λλ₯Ό μ€νν΄μΌν©λλ€.
ReactDOM.render(
<Router history={customHistory}>
<Provider store={store}>
<App />
</Provider>
</Router>,
document.getElementById('root')
);
serviceWorker.unregister();
<modules/posts.js> GOTO_HOME
import * as postsAPI from '../api/posts'; // api/posts μμ ν¨μ λͺ¨λ λΆλ¬μ€κΈ°
import {
reducerUtils,
handleAsyncActions,
handleAsyncActionsById,
createPromiseSaga,
createPromiseSagaById
} from '../lib/asyncUtils';
import { takeEvery, getContext } from 'redux-saga/effects';
/* μ‘μ
νμ
*/
// ν¬μ€νΈ μ¬λ¬κ° μ‘°ννκΈ°
const GET_POSTS = 'GET_POSTS'; // μμ² μμ
const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS'; // μμ² μ±κ³΅
const GET_POSTS_ERROR = 'GET_POSTS_ERROR'; // μμ² μ€ν¨
// ν¬μ€νΈ νλ μ‘°ννκΈ°
const GET_POST = 'GET_POST';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_ERROR = 'GET_POST_ERROR';
const GO_TO_HOME = 'GO_TO_HOME';
export const getPosts = () => ({ type: GET_POSTS });
export const getPost = id => ({ type: GET_POST, payload: id, meta: id });
export const goToHome = () => ({ type: GO_TO_HOME });
const getPostsSaga = createPromiseSaga(GET_POSTS, postsAPI.getPosts);
const getPostSaga = createPromiseSagaById(GET_POST, postsAPI.getPostById);
function* goToHomeSaga() {
const history = yield getContext('history');
history.push('/');
}
// μ¬κ°λ€μ ν©μΉκΈ°
export function* postsSaga() {
yield takeEvery(GET_POSTS, getPostsSaga);
yield takeEvery(GET_POST, getPostSaga);
yield takeEvery(GO_TO_HOME, goToHomeSaga);
}
(...)
'βοΈ > React' μΉ΄ν κ³ λ¦¬μ λ€λ₯Έ κΈ
React 곡λΆνκΈ°(8) - μν κ΄λ¦¬, Context API of 리μ‘νΈ νμ μ€ν¬λ¦½νΈ (0) | 2021.11.09 |
---|---|
React 곡λΆνκΈ°(8) - 리μ‘νΈ νλ‘μ νΈμμ νμ μ€ν¬λ¦½νΈ μ¬μ©νκΈ° (0) | 2021.11.09 |
React 곡λΆνκΈ°(7) - redux-thunk of 리λμ€ λ―Έλ€μ¨μ΄ (0) | 2021.11.09 |
React 곡λΆνκΈ°(7) - 리λμ€ λ―Έλ€μ¨μ΄ (0) | 2021.11.09 |
React 곡λΆνκΈ°(6) - 리λμ€(Redux) (0) | 2021.11.05 |