리덕스

2022-07-11

key value
참고 벨로퍼트와 함께하는 모던 리액트 리덕스
참고 VELOPERT.LOG 리덕스(Redux)를 왜 쓸까? 그리고 리덕스를 편하게 사용하기 위한 발악

1 리덕스

리덕스는 리액트 생태계에서 가장 사용률이 높은 상태관리 라이브러리입니다.

1.1 리덕스가 나오게 된 배경?

  1. Props Drilling Problem
    1. 하위에 특정 값을 전달하기 위해서는 중간 레벨에 있는 컴포넌트들은 전부 그 props를 가지고 있어야 하는 문제가 발생합니다.
    2. 중간 계층에 있는 props를 하나씩 추가하는 문제를 Props Drilling Problem이라고 부릅니다.

      데이터 전달

      위 그림은 Root에서 G까지의 데이터 전달하는 것이 쉽지 않음을 나타내고 있습니다.

    3. 글로벌 상태관리하기가 매우 까다로웠기에 사용되었었으나 최근에는 Context API 를 활용하는 것 만으로도 충분하다고 합니다.
  2. FLUX 패턴
    1. 이러한 문제를 해결하기 위해 등장한 아키텍쳐 입니다.
    2. 간단히 설명하면 View에서 Action을 호출하면 Dispatcher를 통해 Store라는 공간에 Data가 보관이 되고 다시 뷰로 전달되는 흐름이라고 합니다.
    3. FLUX 패턴을 알아들은 똑똑한 사람이 Redux를 만들어 발표하게 되었습니다.
    4. 그리고 나서 센세이션을 일으킵니다.
    5. 이후 각 프레임워크 진영에게 영감을 주어, Vuex 등의 상태관리 라이브러리들이 만들어지기 시작했습니다.
  3. 리덕스 와 Context API 와의 차이
    1. 리덕스에는 미들웨어라는 개념이 존재함
    2. connect 함수와 userSelector 함수가 최적화가 잘되어 있음
    3. 리덕스는 글로벌 상태를 하나의 상태 객체로 관리하기 때문에 매번 Context를 만들 필요가 없음
  4. ContextAPI 대신 리덕스를 써야 할 경우 써야 할때
    1. 규모가 큰 경우
    2. 비동기 작업이 잦은 경우
    3. 사용하기가 편하다고 느껴지는 경우

1.2 리덕스를 통한 상태관리 원리

리덕스를 사용하면 상태값을, 컴포넌트에 종속시키지 않고, 상태 관리를 컴포넌트의 바깥에서 관리 할 수 있게 됩니다.

  1. 설정

    리덕스를 프로젝트에 적용하게 되면 이렇게 스토어 라는 녀석이 생깁니다. 스토어 안에는 프로젝트의 상태에 관한 데이터들이 담겨있습니다.

    리덕스 설정

  2. 구독

    컴포넌트는 스토어에 구독을 합니다. 구독을 하는 과정에서, 특정 함수가 스토어한테 전달이 됩니다. 그리고 나중에 스토어의 상태값에 변동이 생긴다면 전달 받았던 함수를 호출해줍니다.

    리덕스 구독

  3. 상태 변경

    1. dispatch

      이제 B 컴포넌트에서 어떤 이벤트가 생겨서, 상태를 변화 할 일이 생겼습니다. 이 때 dispatch 라는 함수를 통하여 액션을 스토어한테 던져줍니다. 액션은 상태에 변화를 일으킬 때 참조 할 수 있는 객체입니다. 액션 객체는 필수적으로 type 라는 값을 가지고 있어야 합니다.

      예를들어 { type: 'INCREMENT' } 이런 객체를 전달 받게 된다면, 리덕스 스토어는 아~ 상태에 값을 더해야 하는구나~ 하고 액션을 참조하게 됩니다.

      추가적으로, 상태값에 2를 더해야 한다면, 이러한 액션 객체를 만들게 됩니다: { type: 'INCREMENT', diff: 2 }

      그러면, 나중에 이 diff 값을 참고해서 기존 값에 2를 더하게되겠죠. type 를 제외한 값은 선택적(optional) 인 값입니다.

      리덕스 상태 변경1

    2. 리듀서

      액션 객체를 받으면 전달받은 액션의 타입에 따라 어떻게 상태를 업데이트 해야 할지 정의를 해줘야겠죠? 이러한 업데이트 로직을 정의하는 함수를 리듀서라고 부릅니다. 이 함수는 나중에 우리가 직접 구현하게 됩니다. 예를들어 type 이 INCREMENT 라는 액션이 들어오면 숫자를 더해주고, DECREMENT 라는 액션이 들어오면 숫자를 감소시키는 그런 작업을 여기서 하면 되죠.

      리듀서 함수는 두가지의 파라미터를 받습니다.

      • state: 현재 상태
      • action: 액션 객체

      그리고, 이 두가지 파라미터를 참조하여, 새로운 상태 객체를 만들어서 이를 반환합니다.

      리덕스 상태 변경2

  4. 알림

    상태에 변화가 생기면, 이전에 컴포넌트가 스토어한테 구독 할 때 전달해줬었던 함수 listener 가 호출됩니다. 이를 통하여 컴포넌트는 새로운 상태를 받게되고, 이에 따라 컴포넌트는 리렌더링을 하게 되죠.

    리덕스 알림

1.3 리덕스 키워드

  1. 액션
  2. 액션 생성함수
  3. 리듀서
  4. 스토어
  5. 디스패치
  6. 구독

1.4 리덕스 3가지 규칙

  1. 하나의 애플리케이션 안에는 하나의 스토어가 존재
    1. 여러개도 허용은 하나, 권장하진 않음
  2. 상태는 읽기전용
    1. 만약 배열이라면 값을 직접 건드리지 않고, 새로운 배열을 만들어 교체해야 함
  3. 변화를 일으키는 함수, 리듀서는 순수함수여야 함
    1. 여기서 순수 함수란?
      1. 항상 같은 값이 나와야 함
         function add(a, b) {
            return a + b;
         }
        
      2. 부수적 효과가 일어나면 안됨
         let c = 20;
         function add3(a, b) {
           c = b;
           return a + b;
         }
        
      3. return 값으로만 소통
         let obj1 = { val: 10 };
         function add4(obj, b) {
           obj.val += b;
         }
        
      4. 평가시점은 중요하지 않음

        1.5 사용 준비

  4. 라이브러리 설치
        npm i redux
    
  5. 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 리덕스 사용해보기

  1. 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;
       }
     }
    
  2. src/modules/index.js
     import { combineReducers } from 'redux';
     import counter from './counter';
     import todos from './todos';
        
     const rootReducer = combineReducers({
       counter,
       todos
     });
        
     export default rootReducer;
    
  3. 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();
    
  4. 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;
    
  5. 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;
    

1.7 개발자 도구 적용

개발자 도구

  • 어떤 액션들이 디스패치 되고, 상태가 어떻게 편했는지 확인 가능
  • 심지어 직접 디스패치도 가능
  1. redux-devtools-extension 설치
     npm i redux-devtools-extension
    
  2. 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. 리덕스 미들웨어

  1. 리덕스 순서
    1. 액션 -> 미들웨어 -> 리듀서 -> 스토어
  2. 리듀서에서 추가 작업이 가능하도록 할 수 있음
    1. 특정 조건에 따라 액션이 무시
    2. 액션을 콘솔에 출력하거나, 서버쪽에 로깅
    3. 액션이 디스패치 됐을 때 이를 수정해서 리듀서에게 전달
    4. 특정 액션이 발생했을 때 이에 기반하여 다른 액션이 발생
    5. 특정 액션이 발생했을 때 특정 자바스크립트 함수를 실행

2.1 리덕스 프로젝트 준비

  1. 설치
     npm i redux react-redux
    

2.2 미들웨어 만들기

  1. 리덕스 미들웨어의 템플릿
     const middleware = store => next => action => {
       // 하고 싶은 작업...
     }
    

    첫번째 store는 리덕스 스토어 인스턴스입니다. 이 안에 dispatch, getState, subscribe 내장함수들이 들어있죠.

    두번째 next 는 액션을 다음 미들웨어에게 전달하는 함수입니다. next(action) 이런 형태로 사용합니다. 만약 다음 미들웨어가 없다면 리듀서에게 액션을 전달해줍니다. 만약에 next 를 호출하지 않게 된다면 액션이 무시처리되어 리듀서에게로 전달되지 않습니다.

    세번째 action 은 현재 처리하고 있는 액션 객체입니다.

    미들웨어 구조 미들웨어 구조

  2. src/middlewares/myLogger.js
    1. 미들웨어 작성

       const myLogger = store => next => action => {
         console.log(action); // 먼저 액션을 출력합니다.
         const result = next(action); // 다음 미들웨어 (또는 리듀서) 에게 액션을 전달합니다.
         return result; // 여기서 반환하는 값은 dispatch(action)의 결과물이 됩니다. 기본: undefined
       };
          
       export default myLogger;
      
  3. index.js

    1. 미들웨어를 스토어에 적용

    2. 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();
      

    콘솔로그가 잘 찍힘을 볼수 있음

    미들웨어 콘솔로그

  4. src/middlewares/myLogger.js
    1. 미들웨어 수정
     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 사용

  1. redux-logger 사용하기

    1. 라이브러리 설명

      • 콘솔 로그를 예쁘게 찍어줌
    2. 설치

       npm i redux-logger
      
    3. 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();
      
    4. 결과 확인
      • 예쁘게 출력함

        redux logger

    5. 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();
        
    6. DevTools 동작확인

      logger DevTool

2.4 redux-thunk

  • 비동기 작업을 처리 할 때 가장 많이 사용하는 미들웨어

  • 액션 객체가 아닌 함수를 디스패치 가능

  1. 설치

     npm i redux-thunk
    
  2. 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();
    
  3. 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;
          }
        }
      
  4. 컨테이너 컴포넌트에서 사용

    • 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초간 딜레이가 되는 것을 볼 수 있습니다.

      thunk 딜레이

2.5 redux-thunk 프로미스

  1. 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 로 찾아서 반환
          };
      
  2. 리덕스 모듈 (vuex)
    • 프로미스를 다루는 리덕스 모듈을 다룰 땐 다음과 같은 사항을 고려사항
      1. 프로미스가 시작, 성공, 실패했을때 다른 액션을 디스패치해야합니다.
      2. 각 프로미스마다 thunk 함수를 만들어주어야 합니다.
      3. 리듀서에서 액션에 따라 로딩중, 결과, 에러 상태를 변경해주어야 합니다.
    • 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) {
          ...
        }
      
  3. 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 }); // 실패
            }
          };
        };
      
  4. 리덕스 모듈 수정

    • 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);
        };
      
  5. 루트 리듀서에 등록

    • src/modules/index.js

        import { combineReducers } from 'redux';
        import counter from './counter';
        import posts from './posts';
              
        const rootReducer = combineReducers({ counter, posts });
              
        export default rootReducer;
      
  6. 컨테이너
    • 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;
      

    비동기 작업

    Edit learn-redux-middleware

2.6 redux-saga

  • 특정 액션이 발생하면 이에 따라 특정 작업을 하는 방식으로 사용

  • redux-saga로 할 수 있는 것들

    1. 비동기 작업을 할 때 기존 요청을 취소 처리 할 수 있습니다
    2. 특정 액션이 발생했을 때 이에 따라 다른 액션이 디스패치되게끔 하거나, 자바스크립트 코드를 실행 할 수 있습니다.
    3. 웹소켓을 사용하는 경우 Channel 이라는 기능을 사용하여 더욱 효율적으로 코드를 관리 할 수 있습니다 (참고)
    4. API 요청이 실패했을 때 재요청하는 작업을 할 수 있습니다.
  • Generator 문법을 사용함

    1. 제너레이터 함수 생성 (function* 키워드 사용)
       function* generatorFunction() {
           console.log('안녕하세요?');
           yield 1;
           console.log('제너레이터 함수');
           yield 2;
           console.log('function*');
           yield 3;
           return 4;
       }
      
    2. 제너레이터를 생성

       const generator = generatorFunction();
      
    3. 제너레이터 코드가 실행 (generator.next())

      generator

  1. 사가 설치

     npm i redux-saga
    
  2. 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 액션만을 처리
     }
    
  3. 루트 사가

    • 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;
    
  4. 리덕스 스토어에 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();
      

    saga

    Edit learn-redux-middleware

results matching ""

    No results matching ""

    99 other / uml

    04 react / JSX