리덕스
2022-07-11
key | value |
---|---|
참고 | 벨로퍼트와 함께하는 모던 리액트 리덕스 |
참고 | VELOPERT.LOG 리덕스(Redux)를 왜 쓸까? 그리고 리덕스를 편하게 사용하기 위한 발악 |
1 리덕스
리덕스는 리액트 생태계에서 가장 사용률이 높은 상태관리 라이브러리입니다.
1.1 리덕스가 나오게 된 배경?
- Props Drilling Problem
- 하위에 특정 값을 전달하기 위해서는 중간 레벨에 있는 컴포넌트들은 전부 그 props를 가지고 있어야 하는 문제가 발생합니다.
-
중간 계층에 있는 props를 하나씩 추가하는 문제를 Props Drilling Problem이라고 부릅니다.
위 그림은 Root에서 G까지의 데이터 전달하는 것이 쉽지 않음을 나타내고 있습니다.
- 글로벌 상태관리하기가 매우 까다로웠기에 사용되었었으나 최근에는 Context API 를 활용하는 것 만으로도 충분하다고 합니다.
- FLUX 패턴
- 이러한 문제를 해결하기 위해 등장한 아키텍쳐 입니다.
- 간단히 설명하면 View에서 Action을 호출하면 Dispatcher를 통해 Store라는 공간에 Data가 보관이 되고 다시 뷰로 전달되는 흐름이라고 합니다.
- FLUX 패턴을 알아들은 똑똑한 사람이 Redux를 만들어 발표하게 되었습니다.
- 그리고 나서 센세이션을 일으킵니다.
- 이후 각 프레임워크 진영에게 영감을 주어, Vuex 등의 상태관리 라이브러리들이 만들어지기 시작했습니다.
- 리덕스 와 Context API 와의 차이
- 리덕스에는 미들웨어라는 개념이 존재함
- connect 함수와 userSelector 함수가 최적화가 잘되어 있음
- 리덕스는 글로벌 상태를 하나의 상태 객체로 관리하기 때문에 매번 Context를 만들 필요가 없음
- ContextAPI 대신 리덕스를 써야 할 경우 써야 할때
- 규모가 큰 경우
- 비동기 작업이 잦은 경우
- 사용하기가 편하다고 느껴지는 경우
1.2 리덕스를 통한 상태관리 원리
리덕스를 사용하면 상태값을, 컴포넌트에 종속시키지 않고, 상태 관리를 컴포넌트의 바깥에서 관리 할 수 있게 됩니다.
-
설정
리덕스를 프로젝트에 적용하게 되면 이렇게 스토어 라는 녀석이 생깁니다. 스토어 안에는 프로젝트의 상태에 관한 데이터들이 담겨있습니다.
-
구독
컴포넌트는 스토어에 구독을 합니다. 구독을 하는 과정에서, 특정 함수가 스토어한테 전달이 됩니다. 그리고 나중에 스토어의 상태값에 변동이 생긴다면 전달 받았던 함수를 호출해줍니다.
-
상태 변경
-
dispatch
이제 B 컴포넌트에서 어떤 이벤트가 생겨서, 상태를 변화 할 일이 생겼습니다. 이 때 dispatch 라는 함수를 통하여 액션을 스토어한테 던져줍니다. 액션은 상태에 변화를 일으킬 때 참조 할 수 있는 객체입니다. 액션 객체는 필수적으로 type 라는 값을 가지고 있어야 합니다.
예를들어
{ type: 'INCREMENT' }
이런 객체를 전달 받게 된다면, 리덕스 스토어는 아~ 상태에 값을 더해야 하는구나~ 하고 액션을 참조하게 됩니다.추가적으로, 상태값에 2를 더해야 한다면, 이러한 액션 객체를 만들게 됩니다:
{ type: 'INCREMENT', diff: 2 }
그러면, 나중에 이 diff 값을 참고해서 기존 값에 2를 더하게되겠죠. type 를 제외한 값은 선택적(optional) 인 값입니다.
-
리듀서
액션 객체를 받으면 전달받은 액션의 타입에 따라 어떻게 상태를 업데이트 해야 할지 정의를 해줘야겠죠? 이러한 업데이트 로직을 정의하는 함수를 리듀서라고 부릅니다. 이 함수는 나중에 우리가 직접 구현하게 됩니다. 예를들어 type 이 INCREMENT 라는 액션이 들어오면 숫자를 더해주고, DECREMENT 라는 액션이 들어오면 숫자를 감소시키는 그런 작업을 여기서 하면 되죠.
리듀서 함수는 두가지의 파라미터를 받습니다.
- state: 현재 상태
- action: 액션 객체
그리고, 이 두가지 파라미터를 참조하여, 새로운 상태 객체를 만들어서 이를 반환합니다.
-
-
알림
상태에 변화가 생기면, 이전에 컴포넌트가 스토어한테 구독 할 때 전달해줬었던 함수 listener 가 호출됩니다. 이를 통하여 컴포넌트는 새로운 상태를 받게되고, 이에 따라 컴포넌트는 리렌더링을 하게 되죠.
1.3 리덕스 키워드
- 액션
- 액션 생성함수
- 리듀서
- 스토어
- 디스패치
- 구독
1.4 리덕스 3가지 규칙
- 하나의 애플리케이션 안에는 하나의 스토어가 존재
- 여러개도 허용은 하나, 권장하진 않음
- 상태는 읽기전용
- 만약 배열이라면 값을 직접 건드리지 않고, 새로운 배열을 만들어 교체해야 함
- 변화를 일으키는 함수, 리듀서는 순수함수여야 함
- 여기서 순수 함수란?
- 항상 같은 값이 나와야 함
function add(a, b) { return a + b; }
- 부수적 효과가 일어나면 안됨
let c = 20; function add3(a, b) { c = b; return a + b; }
- return 값으로만 소통
let obj1 = { val: 10 }; function add4(obj, b) { obj.val += b; }
- 평가시점은 중요하지 않음
1.5 사용 준비
- 항상 같은 값이 나와야 함
- 여기서 순수 함수란?
- 라이브러리 설치
npm i redux
- exercise.js 작성
import { createStore } from 'redux'; // createStore는 스토어를 만들어주는 함수입니다. // 리액트 프로젝트에서는 단 하나의 스토어를 만듭니다. /* 리덕스에서 관리 할 상태 정의 */ const initialState = { counter: 0, text: '', list: [] }; /* 액션 타입 정의 */ // 액션 타입은 주로 대문자로 작성합니다. const INCREASE = 'INCREASE'; const DECREASE = 'DECREASE'; const CHANGE_TEXT = 'CHANGE_TEXT'; const ADD_TO_LIST = 'ADD_TO_LIST'; /* 액션 생성함수 정의 */ // 액션 생성함수는 주로 camelCase 로 작성합니다. function increase() { return { type: INCREASE // 액션 객체에는 type 값이 필수입니다. }; } // 화살표 함수로 작성하는 것이 더욱 코드가 간단하기에, // 이렇게 쓰는 것을 추천합니다. const decrease = () => ({ type: DECREASE }); const changeText = text => ({ type: CHANGE_TEXT, text // 액션안에는 type 외에 추가적인 필드를 마음대로 넣을 수 있습니다. }); const addToList = item => ({ type: ADD_TO_LIST, item }); /* 리듀서 만들기 */ // 위 액션 생성함수들을 통해 만들어진 객체들을 참조하여 // 새로운 상태를 만드는 함수를 만들어봅시다. // 주의: 리듀서에서는 불변성을 꼭 지켜줘야 합니다! function reducer(state = initialState, action) { // state 의 초깃값을 initialState 로 지정했습니다. switch (action.type) { case INCREASE: return { ...state, counter: state.counter + 1 }; case DECREASE: return { ...state, counter: state.counter - 1 }; case CHANGE_TEXT: return { ...state, text: action.text }; case ADD_TO_LIST: return { ...state, list: state.list.concat(action.item) }; default: return state; } } /* 스토어 만들기 */ const store = createStore(reducer); console.log(store.getState()); // 현재 store 안에 들어있는 상태를 조회합니다. // 스토어안에 들어있는 상태가 바뀔 때 마다 호출되는 listener 함수 const listener = () => { const state = store.getState(); console.log(state); }; const unsubscribe = store.subscribe(listener); // 구독을 해제하고 싶을 때는 unsubscribe() 를 호출하면 됩니다. // 액션들을 디스패치 해봅시다. store.dispatch(increase()); store.dispatch(decrease()); store.dispatch(changeText('안녕하세요')); store.dispatch(addToList({ id: 1, text: '와우' }));
1.6 리덕스 사용해보기
- src/modules/counter.js
- 리듀스 선언
/* 액션 타입 만들기 */ // Ducks 패턴을 따를땐 액션의 이름에 접두사를 넣어주세요. // 이렇게 하면 다른 모듈과 액션 이름이 중복되는 것을 방지 할 수 있습니다. const SET_DIFF = 'counter/SET_DIFF'; const INCREASE = 'counter/INCREASE'; const DECREASE = 'counter/DECREASE'; /* 액션 생성함수 만들기 */ // 액션 생성함수를 만들고 export 키워드를 사용해서 내보내주세요. export const setDiff = diff => ({ type: SET_DIFF, diff }); export const increase = () => ({ type: INCREASE }); export const decrease = () => ({ type: DECREASE }); /* 초기 상태 선언 */ const initialState = { number: 0, diff: 1 }; /* 리듀서 선언 */ // 리듀서는 export default 로 내보내주세요. export default function counter(state = initialState, action) { switch (action.type) { case SET_DIFF: return { ...state, diff: action.diff }; case INCREASE: return { ...state, number: state.number + state.diff }; case DECREASE: return { ...state, number: state.number - state.diff }; default: return state; } }
- src/modules/index.js
import { combineReducers } from 'redux'; import counter from './counter'; import todos from './todos'; const rootReducer = combineReducers({ counter, todos }); export default rootReducer;
- src/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 } from 'redux'; import rootReducer from './modules'; const store = createStore(rootReducer); // 스토어를 만듭니다. console.log(store.getState()); // 스토어의 상태를 확인해봅시다. ReactDOM.render(<App />, document.getElementById('root')); serviceWorker.unregister();
- src/components/Counter.js
import React from 'react'; function Counter({ number, diff, onIncrease, onDecrease, onSetDiff }) { const onChange = e => { // e.target.value 의 타입은 문자열이기 때문에 숫자로 변환해주어야 합니다. onSetDiff(parseInt(e.target.value, 10)); }; return ( <div> <h1>{number}</h1> <div> <input type="number" value={diff} min="1" onChange={onChange} /> <button onClick={onIncrease}>+</button> <button onClick={onDecrease}>-</button> </div> </div> ); } export default Counter;
-
src/components/CounterContainer.js
- dispatch 및 subscribe
- subscribe를 직접 쓰진 않고, 제공하는 useSelector Hook을 사용하여 구독을 함
import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import Counter from '../components/Counter'; import { increase, decrease, setDiff } from '../modules/counter'; function CounterContainer() { // useSelector는 리덕스 스토어의 상태를 조회하는 Hook입니다. // state의 값은 store.getState() 함수를 호출했을 때 나타나는 결과물과 동일합니다. const { number, diff } = useSelector(state => ({ number: state.counter.number, diff: state.counter.diff })); // useDispatch 는 리덕스 스토어의 dispatch 를 함수에서 사용 할 수 있게 해주는 Hook 입니다. const dispatch = useDispatch(); // 각 액션들을 디스패치하는 함수들을 만드세요 const onIncrease = () => dispatch(increase()); const onDecrease = () => dispatch(decrease()); const onSetDiff = diff => dispatch(setDiff(diff)); return ( <Counter // 상태와 number={number} diff={diff} // 액션을 디스패치 하는 함수들을 props로 넣어줍니다. onIncrease={onIncrease} onDecrease={onDecrease} onSetDiff={onSetDiff} /> ); } export default CounterContainer;
- dispatch 및 subscribe
1.7 개발자 도구 적용
- 어떤 액션들이 디스패치 되고, 상태가 어떻게 편했는지 확인 가능
- 심지어 직접 디스패치도 가능
- redux-devtools-extension 설치
npm i redux-devtools-extension
- 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 } from 'redux'; import { Provider } from 'react-redux'; import rootReducer from './modules'; import { composeWithDevTools } from 'redux-devtools-extension'; // 리덕스 개발자 도구 const store = createStore(rootReducer, composeWithDevTools()); // 스토어를 만듭니다. // composeWithDevTools 를 사용하여 리덕스 개발자 도구 활성화 ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') ); serviceWorker.unregister();
1.8 useSelector hook을 사용할 경우 구독해제(unsubscribe)는 어떻게 할까?
useSelector 에 따라 마운트 해제될 때 저장소에서 명시적으로 구독을 취소함
useIsomorphicLayoutEffect(() => {
function checkForUpdates() {
try {
const newSelectedState = latestSelector.current(store.getState())
if (equalityFn(newSelectedState, latestSelectedState.current)) {
return
}
latestSelectedState.current = newSelectedState
} catch (err) {
// we ignore all errors here, since when the component
// is re-rendered, the selectors are called again, and
// will throw again, if neither props nor store state
// changed
latestSubscriptionCallbackError.current = err
}
forceRender()
}
subscription.onStateChange = checkForUpdates
subscription.trySubscribe()
checkForUpdates()
return () => subscription.tryUnsubscribe() // unsubscribes here
}, [store, subscription])
2. 리덕스 미들웨어
- 리덕스 순서
액션
->미들웨어
->리듀서
->스토어
- 리듀서에서 추가 작업이 가능하도록 할 수 있음
- 특정 조건에 따라 액션이 무시
- 액션을 콘솔에 출력하거나, 서버쪽에 로깅
- 액션이 디스패치 됐을 때 이를 수정해서 리듀서에게 전달
- 특정 액션이 발생했을 때 이에 기반하여 다른 액션이 발생
- 특정 액션이 발생했을 때 특정 자바스크립트 함수를 실행
2.1 리덕스 프로젝트 준비
- 설치
npm i redux react-redux
2.2 미들웨어 만들기
- 리덕스 미들웨어의 템플릿
const middleware = store => next => action => { // 하고 싶은 작업... }
첫번째
store
는 리덕스 스토어 인스턴스입니다. 이 안에 dispatch, getState, subscribe 내장함수들이 들어있죠.두번째
next
는 액션을 다음 미들웨어에게 전달하는 함수입니다. next(action) 이런 형태로 사용합니다. 만약 다음 미들웨어가 없다면 리듀서에게 액션을 전달해줍니다. 만약에 next 를 호출하지 않게 된다면 액션이 무시처리되어 리듀서에게로 전달되지 않습니다.세번째
action
은 현재 처리하고 있는 액션 객체입니다.미들웨어 구조
- src/middlewares/myLogger.js
-
미들웨어 작성
const myLogger = store => next => action => { console.log(action); // 먼저 액션을 출력합니다. const result = next(action); // 다음 미들웨어 (또는 리듀서) 에게 액션을 전달합니다. return result; // 여기서 반환하는 값은 dispatch(action)의 결과물이 됩니다. 기본: undefined }; export default myLogger;
-
-
index.js
-
미들웨어를 스토어에 적용
-
applyMiddleware 사용
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 from './modules'; import myLogger from './middlewares/myLogger'; const store = createStore(rootReducer, applyMiddleware(myLogger)); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') ); serviceWorker.unregister();
콘솔로그가 잘 찍힘을 볼수 있음
-
- src/middlewares/myLogger.js
- 미들웨어 수정
const myLogger = store => next => action => { console.log(action); // 먼저 액션을 출력합니다. const result = next(action); // 다음 미들웨어 (또는 리듀서) 에게 액션을 전달합니다. // 업데이트 이후의 상태를 조회합니다. console.log('\t', store.getState()); // '\t' 는 탭 문자 입니다. return result; // 여기서 반환하는 값은 dispatch(action)의 결과물이 됩니다. 기본: undefined }; export default myLogger;
콘솔로그에 상태도 찍히는것을 볼 수 있음
2.3 redux-logger 사용
-
redux-logger 사용하기
-
라이브러리 설명
- 콘솔 로그를 예쁘게 찍어줌
-
설치
npm i redux-logger
-
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 from './modules'; import myLogger from './middlewares/myLogger'; import logger from 'redux-logger'; // 미들웨어 const store = createStore(rootReducer, applyMiddleware(myLogger, logger)); // 여러개의 미들웨어를 적용 할 수 있습니다. ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') ); serviceWorker.unregister();
- 결과 확인
-
예쁘게 출력함
-
-
Redux DevTools 와 함께 미들웨어 사용
-
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 from './modules'; import logger from 'redux-logger'; import { composeWithDevTools } from 'redux-devtools-extension'; // DevTools const store = createStore( rootReducer, composeWithDevTools(applyMiddleware(logger)) ); // 여러개의 미들웨어를 적용 할 수 있습니다. ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') ); serviceWorker.unregister();
-
-
DevTools 동작확인
-
2.4 redux-thunk
-
비동기 작업을 처리 할 때 가장 많이 사용하는 미들웨어
-
액션 객체가 아닌 함수를 디스패치 가능
-
설치
npm i redux-thunk
-
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 from './modules'; import logger from 'redux-logger'; import { composeWithDevTools } from 'redux-devtools-extension'; import ReduxThunk from 'redux-thunk'; const store = createStore( rootReducer, // logger 를 사용하는 경우, logger가 가장 마지막에 와야합니다. composeWithDevTools(applyMiddleware(ReduxThunk, logger)) ); // 여러개의 미들웨어를 적용 할 수 있습니다. ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') ); serviceWorker.unregister();
-
setTimeout를 사용하여 액션이 디스패치되는 것을 1초씩 딜레이
-
src/modules/counter.js
// 액션 타입 const INCREASE = 'INCREASE'; const DECREASE = 'DECREASE'; // 액션 생성 함수 export const increase = () => ({ type: INCREASE }); export const decrease = () => ({ type: DECREASE }); // getState를 쓰지 않는다면 굳이 파라미터로 받아올 필요 없습니다. export const increaseAsync = () => dispatch => { setTimeout(() => dispatch(increase()), 1000); }; export const decreaseAsync = () => dispatch => { setTimeout(() => dispatch(decrease()), 1000); }; // 초깃값 (상태가 객체가 아니라 그냥 숫자여도 상관 없습니다.) 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; } }
-
-
컨테이너 컴포넌트에서 사용
-
src/containers/CounterContainer.js
import React from 'react'; import Counter from '../components/Counter'; import { useSelector, useDispatch } from 'react-redux'; import { increaseAsync, decreaseAsync } from '../modules/counter'; function CounterContainer() { const number = useSelector(state => state.counter); const dispatch = useDispatch(); const onIncrease = () => { dispatch(increaseAsync()); }; const onDecrease = () => { dispatch(decreaseAsync()); }; return ( <Counter number={number} onIncrease={onIncrease} onDecrease={onDecrease} /> ); } export default CounterContainer;
아래와 같이 1초간 딜레이가 되는 것을 볼 수 있습니다.
-
2.5 redux-thunk 프로미스
-
api
- src/api/posts.js
// 포스트 목록을 가져오는 비동기 함수 export const getPosts = async () => { await sleep(500); // 0.5초 쉬고 return posts; // posts 배열 }; // ID로 포스트를 조회하는 비동기 함수 export const getPostById = async id => { await sleep(500); // 0.5초 쉬고 return posts.find(post => post.id === id); // id 로 찾아서 반환 };
- src/api/posts.js
- 리덕스 모듈 (vuex)
- 프로미스를 다루는 리덕스 모듈을 다룰 땐 다음과 같은 사항을 고려사항
- 프로미스가 시작, 성공, 실패했을때 다른 액션을 디스패치해야합니다.
- 각 프로미스마다 thunk 함수를 만들어주어야 합니다.
- 리듀서에서 액션에 따라 로딩중, 결과, 에러 상태를 변경해주어야 합니다.
-
src/modules/posts.js
import * as postsAPI from '../api/posts'; // api/posts 안의 함수 모두 불러오기 /* 액션 타입 */ // 포스트 여러개 조회하기 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'; // 액션 함수 // thunk 를 사용 할 때, 꼭 모든 액션들에 대하여 액션 생성함수를 만들 필요는 없습니다. // 그냥 thunk 함수에서 바로 액션 객체를 만들어주어도 괜찮습니다. export const getPosts = () => async dispatch => { dispatch({ type: GET_POSTS }); // 요청이 시작됨 try { const posts = await postsAPI.getPosts(); // API 호출 dispatch({ type: GET_POSTS_SUCCESS, posts }); // 성공 } catch (e) { dispatch({ type: GET_POSTS_ERROR, error: e }); // 실패 } }; // thunk 함수에서도 파라미터를 받아와서 사용 할 수 있습니다. export const getPost = id => async dispatch => { dispatch({ type: GET_POST }); // 요청이 시작됨 try { const post = await postsAPI.getPostById(id); // API 호출 dispatch({ type: GET_POST_SUCCESS, post }); // 성공 } catch (e) { dispatch({ type: GET_POST_ERROR, error: e }); // 실패 } }; const initialState = { posts: { loading: false, data: null, error: null }, post: { loading: false, data: null, error: null } }; // 리듀스 export default function posts(state = initialState, action) { ... }
- 프로미스를 다루는 리덕스 모듈을 다룰 땐 다음과 같은 사항을 고려사항
-
Promise에 기반한 Thunk를 만들기 위한 Utils
-
src/lib/asyncUtils.js
// Promise에 기반한 Thunk를 만들어주는 함수입니다. export const createPromiseThunk = (type, promiseCreator) => { const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`]; // 이 함수는 promiseCreator가 단 하나의 파라미터만 받는다는 전제하에 작성되었습니다. // 만약 여러 종류의 파라미터를 전달해야하는 상황에서는 객체 타입의 파라미터를 받아오도록 하면 됩니다. // 예: writeComment({ postId: 1, text: '댓글 내용' }); return param => async dispatch => { // 요청 시작 dispatch({ type, param }); try { // 결과물의 이름을 payload 라는 이름으로 통일시킵니다. const payload = await promiseCreator(param); dispatch({ type: SUCCESS, payload }); // 성공 } catch (e) { dispatch({ type: ERROR, payload: e, error: true }); // 실패 } }; };
-
-
리덕스 모듈 수정
-
src/modules/posts.js
import { createPromiseThunk, reducerUtils } from '../lib/asyncUtils'; // 아주 쉽게 thunk 함수를 만들 수 있게 되었습니다. export const getPosts = createPromiseThunk(GET_POSTS, postsAPI.getPosts); export const getPost = createPromiseThunk(GET_POST, postsAPI.getPostById);
-
이전 코드들
// 리덕스 기본 export const increase = () => ({ type: INCREASE }); // thuck export const increaseAsync = () => dispatch => { setTimeout(() => dispatch(increase()), 1000); };
-
-
루트 리듀서에 등록
-
src/modules/index.js
import { combineReducers } from 'redux'; import counter from './counter'; import posts from './posts'; const rootReducer = combineReducers({ counter, posts }); export default rootReducer;
-
- 컨테이너
- src/components/PostList.js
import React from 'react'; function PostList({ posts }) { return ( <ul> {posts.map(post => ( <li key={post.id}> {post.title} </li> ))} </ul> ); } export default PostList;
-
src/containers/PostListContainer.js
import React, { useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import PostList from '../components/PostList'; import { getPosts } from '../modules/posts'; function PostListContainer() { const { data, loading, error } = useSelector(state => state.posts.posts); const dispatch = useDispatch(); // 컴포넌트 마운트 후 포스트 목록 요청 useEffect(() => { dispatch(getPosts()); }, [dispatch]); if (loading) return <div>로딩중...</div>; if (error) return <div>에러 발생!</div>; if (!data) return null; return <PostList posts={data} />; } export default PostListContainer;
- src/components/PostList.js
2.6 redux-saga
-
특정 액션이 발생하면 이에 따라 특정 작업을 하는 방식으로 사용
-
redux-saga로 할 수 있는 것들
- 비동기 작업을 할 때 기존 요청을 취소 처리 할 수 있습니다
- 특정 액션이 발생했을 때 이에 따라 다른 액션이 디스패치되게끔 하거나, 자바스크립트 코드를 실행 할 수 있습니다.
- 웹소켓을 사용하는 경우 Channel 이라는 기능을 사용하여 더욱 효율적으로 코드를 관리 할 수 있습니다 (참고)
- API 요청이 실패했을 때 재요청하는 작업을 할 수 있습니다.
-
Generator 문법을 사용함
- 제너레이터 함수 생성 (function* 키워드 사용)
function* generatorFunction() { console.log('안녕하세요?'); yield 1; console.log('제너레이터 함수'); yield 2; console.log('function*'); yield 3; return 4; }
-
제너레이터를 생성
const generator = generatorFunction();
-
제너레이터 코드가 실행 (generator.next())
- 제너레이터 함수 생성 (function* 키워드 사용)
-
사가 설치
npm i redux-saga
-
delay, put takeEvery, takeLatest
takeEvery
: 특정 액션 타입에 대하여 디스패치되는 모든 액션들을 처리takeLatest
: 특정 액션 타입에 대하여 디스패치된 가장 마지막 액션만을 처리
import { delay, put, takeEvery, takeLatest } from 'redux-saga/effects'; // 액션 생성 함수 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 액션만을 처리 }
-
루트 사가
- src/modules/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;
-
리덕스 스토어에 redux-saga 미들웨어를 적용
-
src/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();
-