티스토리 뷰

reactjs

Redux와 Ducks Pattern

__songji__ 2022. 3. 25. 00:06

Redux

 

react는 상태를 자식 컴포넌트로 전달, 전달하는 방식으로,

전달의 단계가 많아진다면 상당히 복잡한 구조가 되고, 리소스 낭비가 되곤한다.

 

props-driliing

 

이런 전달의 구조가 깊어지는 것을 props-drilling이라고 부른다.

 

이런 props-driliing을 해소하기 위해, 전역에서 상태관리가 가능한 여러방법들이 존재한다.

context api, redux, mobx ,, 등 여러가지 방법이 있는데, 

이 방법 중, 우리나라에서 가장 사용률이 높은 redux에 관해 살펴보자.

 

redux는 전역 상태 저장소(store)를 제공한다.

react의 모든 컴포넌트는 이 store에 접근 가능하다.

without Redux vs with Redux

위의 그림과 같이 redux를 사용하게 되면, Store를 통해 상태의 관계를 맺게 되기 때문에, 좀 더 편한 상태전달이 가능하다.

 

 

Redux 개념

Store

앱의 전역 상태를 갖는 저장소

store는 독립적이고 무결성을 보장해야 한다.

store의 상태를 변경하는 요소는 action 뿐이다.

import { createStore } from 'redux';
import todoApp from './reducers';

// Reducer들을 store에 넘겨 연결하기
let store = createStore(todoApp);

 

Action

상태 변화에 대한 정보를 담은 객체 (순수 자바스크립트의 object 타입)

액션 객체는 상태에 행위를 나타내는  type 문자열 필수

{ type: "ADD_TODO", todo: 'to do list item' }

 

Action Creater 

Dispatch에 인자로 전달하기 위해, Action을 함수로 감싼 형태 

즉, Action을 Return하는 함수로, 최종적으로 해당 액션이 호출됨

function addTodo(todo) {
  return {
    type: ADD_TODO,
    todo
  }
}

 

Reducer

액션에 대한 함수를 정의하고, 함수를 실행하여 상태를 업데이트

상태의 Data를 변화시키는 개념이 아닌, 상태변화한 Data를 새로운 상태로 Store에 반환하는 것이 상태 업데이트라고 생각하자.

export default function reducer(state, action){
	...
}

 

Dispatch

액션을 Reducer에 전달

store.dispatch(addTodo('Learn about Redux'));

 

 

Redux 원칙
  1. 앱의 전역상태는 단일 저장소 내의 트리에 저장된다. (단일 Redux Store)
  2. 상태는 읽기 전용 (Read-Only)
  3. 상태는 순수 함수에 의해서 변경되어야 한다.( Action & Action Creator)

 

Redux Flow

Redux Flow

  1. Component에서 이벤트 발생
  2. 이벤트에 해당하는 Action Creator를  Dispatch에  전달
  3. Dispatch가 ActionCreator에서 생성된 ActionReducer에 전달 및 실행
  4. Reducer는 Action에 타입에 따라 정의된 함수를 실행하여, 새로운 상태로 업데이트 
      reducer(prevState, action)  
  5. Component 렌더링

 

Redux sample code

아주 간단한 redux 예제를 만들어보자. (to do list)

 

react & redux 셋팅

npx create-react-app my-project  
npm install --save react-redux
* 파일구조 *
- src
      - actions / index.js
      - reducers / index.js
      - index.js
      - App.js

 

src/actions/index.js

export const ADD_TODO = 'ADD_TODO';
export const addTodo = (todo) => {
    return {
        type: ADD_TODO,
        todo
    }
}

 

src/reducers/index.js

import {combineReducers} from 'redux';
import {ADD_TODO} from "../actions";


const todoState = {
    todos : []
}

const todoReducer = (state = todoState, action) => {
    switch (action.type){
        case ADD_TODO:
            return {...state, todos: state.todos.concat(action.todo)}
        default:
            return state
    }
}

// combineReducers : 여러개의 리듀서를 하나로 합쳐주는 메서드
const AppReducer = combineReducers({
    todoReducer
})

export default AppReducer;

 

 

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {createStore} from 'redux'
import reducers from "./reducers";
import {Provider} from "react-redux";

const store = createStore(reducers);

ReactDOM.render(
    <React.StrictMode>
        <Provider store={store}>
            <App/>
        </Provider>
    </React.StrictMode>,
    document.getElementById('root')
);

 

src/App.js

import {useDispatch, useSelector} from "react-redux";
import {useState} from "react";
import {addTodo} from "./actions";


function App() {
    const [text, setText] = useState("");

    // store에 접근하여 state 가져오기
    const { todos } = useSelector(state => state.todoReducer);

    // dispatch를 사용하기 위한 준비
    const dispatch = useDispatch();
    const add = () => {
        // store에 있는 state 바꾸는 함수 실행
        dispatch(addTodo(text));
    };
  return (
    <div className="App">
      <h1 className="App-header">
          To Do List
      </h1>
        <ul>
            {todos.map((item, idx)=>(
                <li key={idx}>{item}</li>
            ))}
        </ul>
        <div>
            <input type="text" onChange={(e)=>setText(e.currentTarget.value)} value={text}/>
            <button onClick={()=>add()}>할일 추가하기</button>
        </div>
    </div>
  );
}

export default App;

 

이렇게 아주 간단한  To do List에 추가하는 기능을 Redux로 구현해보았다.

 

여기서 생각해볼 점이,

현재 디렉터리 구조로 redux를 계속 추가할 경우, 각 actions와 reducers 폴더를 나누어서 관리를 해주어야 하는 번거로움이 생긴다.

현재 샘플코드와 맞지 않는 조금 앞서나가는 생각이긴 하지만,

위의 구조와 같이 실제 리덕스를 작업하다보면,

redux 관련한 코드가 수정이 될 경우 import를 타고가서 actions와 reducers 폴더 모두 확인해보고 수정해야하거나,

리듀서가 많아지면 리듀서가 꼬였을때 어디가 문제인지 파악하는데 어려움이 생기는 등의 여러 불편한 점을 흔히들 경험하게 된다.

 

이러한 리덕스의 복잡도를 해소하기 위해, ducks 패턴이란 것이 생겨났다.

 

 

 

Ducks Pattern

 

ducks pattern은 자바스크립트 개발자인 Erik Rasmussen이 제안한, 리덕스 사용 방법이다. ( ducks pattern 원문 )

ducsk pattern은 파일중심이 아닌 모듈 중심으로 파일을 나눈다.

 

일반적으로 redux를 사용하기 위해서는 actionType, actions, reducer 가 함께 추가되어야 하고, action과 reducer가 짝을 이뤄 하나의 redux flow를 사용할 수 있다.

그래서, ducks pattern은 actionType, actions, reducer를 하나의 파일 안에서 모듈형식으로 관리하자는 제안을 하고 있다.

 

Ducks Pattern 규칙
  1. 항상 reducer()란 이름의 함수를 export default 해야합니다.
  2. 항상 모듈의 action 생성자들을 함수형태로 export 해야합니다.
  3. 항상 npm-module-or-app/reducer/ACTION_TYPE 형태의 action 타입을 가져야합니다.
  4. 경우에 따라, action 타입들을 UPPER_SNAKE_CASE로 export 할 수 있습니다.
    만약, 외부 reducer가 해당 action들이 발생하는지 계속 기다리거나, 재사용할 수 있는 라이브러리로 퍼블리싱할 경우에 말이죠.

재사용가능한 Redux 라이브러리 형태로 공유하는 {actionType, action, reducer} 묶음에도 위 규칙을 추천합니다.

 

Redux sample code -> Ducks Pattern sample code

앞에서 작성해보았던 Redux sample code를 Ducks Pattern으로 변경해보자.

 

* 파일구조 *
- src
      - store
              - index.js
              - todo.js
      - index.js
      - App.js

✅ ducks pattern에서 리덕스 디렉터리는 store 또는 modules 로 많이 사용된다.

 

src/store/todo.js


// 1. 액션타입
export const ADD_TODO = 'todo/ADD_TODO';

// 2. 액션 creator
export const addTodo = (todo) => {
    return {
        type: ADD_TODO,
        todo
    }
}

// 3. 모듈의 상태 초기값
const initTodoState = {
    todos : []
};

// 4. 리듀서
export default function todoReducer(state = initTodoState, action) {
    switch (action.type){
        case ADD_TODO:
            return {...state, todos: state.todos.concat(action.todo)}
        default:
            return state
    }
}

 

src/store/index.js

import { combineReducers } from 'redux';
import todoReducer from "./todo";

const AppReducer = combineReducers({
    todoReducer,
});


export default AppReducer;

 

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {createStore} from 'redux'
import reducers from "./store"; // * 경로만 바뀜
import {Provider} from "react-redux";
import { composeWithDevTools } from 'redux-devtools-extension';


const store = createStore(reducers);

ReactDOM.render(
    <React.StrictMode>
        <Provider store={store}>
            <App/>
        </Provider>
    </React.StrictMode>,
    document.getElementById('root')
);

 

src/App.js

import {useDispatch, useSelector} from "react-redux";
import {useState} from "react";
import {addTodo} from "./store/todo"; // * 경로만 바뀜


function App() {
    const [text, setText] = useState("");

    const { todos } = useSelector(state => state.todoReducer);

    const dispatch = useDispatch();
    const add = () => {
        dispatch(addTodo(text));
    };
  return (
    <div className="App">
      <h1 className="App-header">
          To Do List
      </h1>
        <ul>
            {todos.map((item, idx)=>(
                <li key={idx}>{item}</li>
            ))}
        </ul>
        <div>
            <input type="text" onChange={(e)=>setText(e.currentTarget.value)} value={text}/>
            <button onClick={()=>add()}>할일 추가하기</button>
        </div>
    </div>
  );
}

export default App;

 

 

이렇게 ducks pattern을 사용하다보면, 

코드 작성도 용이하고 가독성도 좋아진는 등의 훨씬 더 많은 이점이 있다.

특별한 이유가 없다면 ducks pattern을 활용하자 !

 

 


참고

https://dinn.github.io/web/redux-ducks-pattern/

https://ahnanne.tistory.com/34

https://velog.io/@gyrbs22/React-Redux-flow-Architecture-%EA%B0%9C%EB%85%90

https://eatnug.github.io/frontend/react-redux/

https://velog.io/@mokyoungg/Redux-Redux%EC%9D%98-%EA%B8%B0%EC%B4%88-%EA%B0%9C%EB%85%90

 

 

'reactjs' 카테고리의 다른 글

React Context  (0) 2022.04.15
Class Component vs UseEffect LifeCycle  (0) 2022.03.06
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함