본문 바로가기
학습 내용/Front-End

[Redux] 리덕스 모듈 만들기 - 덕스 패턴(Ducks pattern)

by yein 2021. 9. 23.

리덕스 모듈

  • 리덕스 모듈이란?
    • 액션 타입(Actions), 액션 생성 함수(Action Creators), 리듀서(Reducer)가 모두 들어있는 자바스크립트 파일을 의미함.
    • 이렇게 하나의 파일에 작성하는 것을 덕스(Ducks) 패턴이라고 함.
  • 리덕스 모듈에서 리듀서default export하고, 액션 생성 함수는 그냥 export함.


"덕스 패턴"이 뭐고 이걸 쓰면 뭐가 좋은걸까?

  • 덕스 패턴은 Erik Rasmussen님이 리덕스 사용 방법과 관련하여 제안한 패턴이다. (참고)
  • 액션 타입, 액션 생성 함수, 리듀서를 각각 별도의 파일(심지어는 별도의 폴더)에 분리하여 작성하기 보다는, 그 셋을 하나의 모듈처럼 한 파일 안에 작성하자는 제안이다.
    이렇게 말고! (사진 출처: https://medium.com/swlh/the-good-the-bad-of-react-redux-and-why-ducks-might-be-the-solution-1567d5bdc698)
    • 사실 나는 학원에서 리덕스를 처음 배울 때부터 덕스 패턴으로 작성하는 방식으로 배워서, 이게 덕스 패턴인지도 모르고 그냥 그렇게 작성하는 게 관례(?)인 줄 알고 썼었다.
    • 만약 덕스 패턴에 따라 리덕스를 사용하지 않았더라면, 스토어를 만드는 과정이 약간 더 정신없게 느껴지지 않았을까.
  • 이렇게 리덕스를 모듈화를 하는 것의 이점은 코드를 작성하는 이도 왔다갔다 하지 않고 하나의 파일 안에서 "1. 액션 타입~ 2. 액션 생성 함수~ 3. 리듀서~" 이런 식으로 순서대로 작성하기만 하면 되니 코드 작성하기가 좀 더 용이할테고, 다른 사람들이 보기에도 코드가 깔끔 명료하고 가독성이 좋다는 것 정도인 것 같다.
  • 덕스 패턴의 규칙은 아래와 같다.
    1. MUST export default a function called reducer()
      반드시 리듀서 함수를 default export해야 한다.
    2. MUST export its action creators as functions
      반드시 액션 생성 함수를 export해야 한다.
    3. MUST have action types in the form npm-module-or-app/reducer/ACTION_TYPE
      반드시 접두사를 붙인 형태로 액션 타입을 정의해야 한다. (아래 예제 코드 참고)
    4. MAY export its action types as UPPER_SNAKE_CASE, if an external reducer needs to listen for them, or if it is a published reusable library
      (필수는 아닌데) 외부 리듀서가 모듈 내 액션 타입을 바라보고 있거나, 모듈이 재사용 가능한 라이브러리로 쓰이는 것이라면 액션 타입을 UPPER_SNAKE_CASE 형태로 이름 짓고 export 하면 된다.

 

예제

  • src 디렉토리 하위에 modules 디렉토리를 만들고, counter 모듈과 todos 모듈을 만들어보자.
    • counter 모듈(src/modules/counter.js)
      /* ----------------- 액션 타입 ------------------ */
      const SET_DIFF =  'counter/SET_DIFF'; // 얼마만큼 더하거나 뺄지
      const INCREASE = 'counter/INCREASE';
      const DECREASE = 'counter/DECREASE';
      // 덕스 패턴에서는 액션 타입을 정의할 때 이와 같이 접두사를 붙임.
      // 다른 모듈과 이름이 중복되지 않게 하기 위함.
      
      /* ----------------- 액션 생성 함수 ------------------ */
      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 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;
        }
      }
    • todos 모듈(src/modules/todos.js)
      /* ----------------- 액션 타입 ------------------ */
      const ADD_TODO =  'todos/ADD_TODO';
      const TOGGLE_TODO = 'todos/TOGGLE_TODO';
      
      /* ----------------- 액션 생성 함수 ------------------ */
      export const addTodo = (id, text) => ({
        type: ADD_TODO,
        todo: {
          id,
          text,
          done: false,
        },
      });
      export const toggleTodo = id => ({
        type: TOGGLE_TODO,
        id,
      });
      
      /* ----------------- 모듈의 초기 상태 ------------------ */
      const initialState = []; // todo list
      // 아래와 같은 객체가 상태(배열)에 추가될 예정
      /**
        {
          id: 1,
          text: '청소하기',
          done: false,
        }
       */
      
      
      /* ----------------- 리듀서 ------------------ */
      export default function todos(state = initialState, action) {
        switch (action.type) {
          case ADD_TODO:
            return state.concat(action.todo);
          case TOGGLE_TODO:
            return state.map(todo =>
              todo.id === action.id ? { ...todo, done: !todo.done } : todo
            );
          default:
            return state;
        }
      }
  • 앞서 만든 두 모듈을 하나로 합쳐서 root reducer 만들기
    • modules 디렉토리에 index.js 파일 생성 후 아래와 같이 작성
      import { combineReducers } from 'redux';
      import counter from './counter';
      import todos from './todos';
      
      const rootReducer = combineReducers({
        counter,
        todos,
      });
      
      export default rootReducer;
  • 리액트 프로젝트에 리덕스 적용하기
    • 리액트 프로젝트에 리덕스를 적용하려면 redux와 react-redux를 설치해야 함.
      # If you use npm:
      npm install redux react-redux
      
      # Or if you use Yarn:
      yarn add redux react-redux
      *참고로 TypeScript를 사용하는 경우 react-redux 타입은 이미 react-redux 패키지에 포함이 되어 있지만, 수동으로 설치를 해야 할 필요가 있다면 아래와 같이 타입을 추가로 설치해주면 됨.
      npm install @types/react-redux
    • 설치가 완료되었다면, src/index.js로 가서 Provider, createStore, rootReducer를 불러온다.
      import ReactDOM from 'react-dom';
      import './index.css';
      import App from './App';
      import reportWebVitals from './reportWebVitals';
      
      // 리덕스 적용하기
      import { Provider } from 'react-redux';
      import { createStore } from 'redux';
      import rootReducer from './modules';
      
      // 스토어 생성
      const store = createStore(rootReducer);
      
      ReactDOM.render( // App을 Provider로 감싸주면 적용 완료!
        <Provider store={store}>
          <App />
        </Provider>,
        document.getElementById('root')
      );
      
      reportWebVitals();
      이제, 스토어에서 관리되고 있는 애플리케이션 상태를 getStore()로 조회해보자.

      초기값이 찍히는 것을 확인할 수 있다.

참고

- 벨로퍼트와 함께하는 모던 리액트 : Redux

- https://github.com/erikras/ducks-modular-redux

 

GitHub - erikras/ducks-modular-redux: A proposal for bundling reducers, action types and actions when using Redux

A proposal for bundling reducers, action types and actions when using Redux - GitHub - erikras/ducks-modular-redux: A proposal for bundling reducers, action types and actions when using Redux

github.com

- https://medium.com/@matthew.holman/what-is-redux-ducks-46bcb1ad04b7

 

What is Redux Ducks?

I was in the process of implementing Redux on a large React SPA at work. I had already setup the file structure as I had seen and used…

medium.com

- https://medium.com/swlh/the-good-the-bad-of-react-redux-and-why-ducks-might-be-the-solution-1567d5bdc698

 

The Good/The Bad of React & Redux: And Why Ducks Might Be The Solution

A deep dive on how to add Redux to a React app and how to utilize the Ducks architecture to your advantage

medium.com