[Redux] Redux(4) - Ducks패턴 Todolist 만들기 (react-redux, immer)

권준혁·2020년 11월 1일
0

Redux

목록 보기
3/3
post-thumbnail

안녕하세요
Redux를 좀더 편리하게 사용하기위한 방식 Ducks패턴에 대해 알아보겠습니다.
Redux로 개발하는 대부분의 경우에는 Ducks패턴이 편리합니다.

Redux 공식문서 : Tutorial
실전 리액트프로그래밍 을 참고해 작성했습니다.

리덕스의 튜토리얼을 따라 보면 액션타입, 액션 생성자 함수, 리듀서를 각각 다른 파일에 작성합니다. 이런 식으로 구조중심으로 파일들을 나누게 되면 만약 기능을 추가해야 할 때, 적어도 3개 이상의 파일을 열어 수정을 해야합니다.
이런 문제점을 완화하기 위해 Ducks패턴이 제안됐습니다.

Ducks패턴은

  • 액션타입, 액션생성자함수, 리듀서를 하나의 파일로 작성합니다.
  • 리듀서 함수는 export default 키보드로 내보냅니다.
  • 액션 생성자 함수는 export 키워드로 내보냅니다.
  • 액션 타입은 접두사와 액션 이름을 조합해서 만듭니다.

Redux의 Ducks패턴을 이용해서 TodoList 앱을 처음부터 만들어보겠습니다.

1. 먼저 create-react-app으로 빠르게 프로젝트를 생성합니다.

  • create-react-app [redux-test]
  • npm i redux react-redux immer

설치가 완료되면 사용하지 않는 파일들은 다 지워주세요.

  • PWA를 위한 serviceWorker 들이나 manifest파일들 css파일 logo이미지 등은 지워도 됩니다.
  • index.html의 필요없는 코드들도 지워주세요.
  • npm start 로 실행시켜서 화면과 콘솔에 이상없는지 확인해봅니다.
    준비가 다 됐습니다.

2. Reducer 생성자 함수 - creteReducer

먼저 Reducer를 생성하는 함수 createReducer를 작성해보겠습니다.
Ducks패턴은 기능중심의 패턴입니다. 공통기능을 관리할 폴더를 따로 만들고 createReducer.js 파일을 작성합니다.

// createReducer.js
import produce from 'immer';
// ▼reducer를 반환하는 함수
export default function createReducer (initialState, handlerMap) {  
  // ▼ reducer
    return function (state=initialState, action) {  
      // ▼ 객체대신 immer의 produce함수 리턴, 불변객체로 관리
        return produce(state,draft=>{  
          // ▼ handlerMap은 action들을 담고있음 action.type의 key값과 일치하는 요소로 초기화
            const handler = handlerMap[action.type]; 
          // ▼ 미리 등록한 일치하는 action.type이 있을 경우
            if(handler) {  
                handler(draft, action); 
            }
        })
    }
}

createReducer는 동적으로 reducer를 생성하는 함수입니다. reducer를 생성하는데 필요한 초기값과 action들을 담고있는 handlerMap을 가지고 있습니다.
handlerMap은 action.type을 key값으로 가지며 일치하는 action.type이 있을 경우에는 그 action을 실행하고 immer패키지의 produce함수를 이용해 불변객체로 리턴합니다.


3. Reducer 작성하기

다음은 todos라는 폴더를 만들고 state.js파일을 만듭니다.
2번에서 만든 createReducer함수를 이용해 reducer를 만들어 export합니다.
reducer를 만들기 위해 action들을 먼저 정의해야합니다.

여기서 Ducks패턴으로 작성합니다.

Ducks패턴은

  • 액션타입, 액션생성자함수, 리듀서를 하나의 파일로 작성합니다.
  • 리듀서 함수는 export default 키보드로 내보냅니다.
  • 액션 생성자 함수는 export 키워드로 내보냅니다.
  • 액션 타입은 접두사와 액션 이름을 조합해서 만듭니다.
// state.js
import createReducer from "../common/createReducer";

// ▼action.types
const ADD = 'todo/ADD';
const REMOVE = 'todo/REMOVE';
const EDIT = 'todo/EDIT';
const DONE = 'todo/DONE';

// ▼action
export const addTodo = todo => ({ type: ADD, todo });
export const removeTodo = todo => ({ type: REMOVE, todo });
export const editTodo = todo => ({ type: EDIT, todo });

// ▼initialState & reducer
const INITIAL_STATE = { todos: []};
const reducer = createReducer(INITIAL_STATE, {
    [ADD]: (state, action) => {
        state.todos.push(action.todo)
    },
    [REMOVE]: (state, action) => {
        state.todos.splice(state.todos.findIndex(todo=> todo.id === action.todo.id),1);
    },
    [EDIT]: (state,action) => {
        const index = state.todos.findIndex(todo => todo.id === action.todo.id);
        if (index >= 0) {
            state.todos[index] = action.todo;
        }
    },
});

export default reducer;

reducer라는 이름의 상수에 createReducer가 반환하는 reducer로 초기화시킵니다.
createReducer는 인수로 초기값과 handlerMap을 받습니다.
handlerMap은 객체형태로 계산된 속성명을 이용해 동적으로 reducer를 생성할 수 있습니다.


4. store 작성하기

지금은 reducer가 하나밖에 없어 store파일을 따로 작성하는 것이 번거로울 수 있지만,
나중에 여러개의 reducer를 합쳐 하나의 store를 만들어야 하거나, redux-saga등 라이브러리를 사용하는 경우를 대비해 미리 만들어 둘겁니다.

import { createStore, applyMiddleware } from 'redux';
import todoReducer from '../todos/state';

// ▼ redux의 combineReducer 메서드를 사용해 reducer를 추가하기 위해 reducer에 초기화
const reducer = todoReducer;

// ▼ logging Middleware
const logggingMiddleware = store => next => action => {
    console.log('BEFORE STATE : ');
    console.log(store.getState());
    const result = next(action);
    console.log('NEXT STATE : ');
    console.log(store.getState());
    return result;
}
// ▼ 미들웨어 적용
const store = createStore(reducer, applyMiddleware(logggingMiddleware));
export default store;

action 전후 state를 출력하는 미들웨어를 작성했습니다.


5. index.js 수정하기

react-redux를 적용하기 위해 index.js를 수정합니다.

전체 파일구조입니다.

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

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

ContextAPI를 이용해 Provider래퍼 컴포넌트가 store를 하위 컴포넌트들에게 전달해주고 있습니다. react-redux가 상탯값이 변경되면 하위 컴포넌트들에게 변경된 상탯값을 ContextAPI를 이용해 전달하므로 자동으로 re-rendering 합니다.


5. 화면작성

state에 담겨있는 todos배열을 화면에 리스트로 출력합니다.

import React from 'react';

function TodoList ( {todos} ) {
    return (
        <ul>
            {todos.map((todo,index)=>(
                <li key={index} data-id={todo.id}>{todo}</li>
            ))}
        </ul>
    )
}
export default TodoList;

data-id html5의 dataset을 사용하고있습니다.
data-[이름] 의 형태로 사용하는데 읽기전용 속성입니다.
-`는 참조시에는 카멜케이스로 호출합니다.

MDN : dataset

<div id="mydiv" data-my-div="wrapper div">
// document.getElementById("mydiv").dataset.myDiv 로 호출 가능

6. Container 컴포넌트 작성

Container컴포넌트는 react-redux의 connect 함수를 이용해 하위 컴포넌트에게 store의 dispatch함수나 상탯값등을 전달할 수 있습니다.

이전 게시글을 참고하면 좋습니다.
<< 이전글 React-Redux 사용하기

// container/todoMain.js
import React from 'react';
import TodoList from '../component/todoList';
import {connect} from 'react-redux';
import { addTodo, removeTodo, editTodo } from "../todos/state";

function TodoMain (props) {
    const {addTodo, editTodo, removeTodo, todos} = props;
    const [input, setInput] = React.useState('');
    const inputRef = React.useRef();

    const onChangeInput = () => {
        setInput(inputRef.current.value);
    }
    const onClickHandler = () => {
        addTodo(input);
        setInput('');
    }

    return (
    <React.Fragment>
        <input type='text' placeholder='할 일' ref={inputRef} onChange={onChangeInput} value={input}></input>
        <button onClick={onClickHandler}> 추가하기 </button>
        <TodoList todos={todos} editTodo={editTodo} removeTodo={removeTodo}/>
    </React.Fragment>
    )
}

const mapStateToProps = state => {
    return ({
        todos: state.todos
    });
}
const mapDispatchToProps = dispatch => {
    return {
        addTodo: todo => dispatch(addTodo(todo)),
        editTodo: todo => dispatch(editTodo(todo)),
        removeTodo: todo =>dispatch(removeTodo(todo))
    }
}
export default connect(mapStateToProps, mapDispatchToProps)(TodoMain);

7. App.js 수정하기

import React from 'react';
import TodoMain from './container/todoMain';

function App() {
  return (
    <TodoMain/>
  );
}
export default App;

redux의 ducks패턴을 이용한 간단한 todolist가 완성됐습니다.


8. 수정삭제기능 추가하기

지금은 추가만 가능한 상태입니다.
수정과 삭제 기능을 넣어보겠습니다.
몇가지 코드를 수정해야합니다.

  • reducer의 추가하는 부분을 id값이 있는 객체형태의 요소로 수정했습니다.
    // todos/state.js
    ...생략
    const reducer = createReducer(INITIAL_STATE, {
      [ADD]: (state, action) => {
          state.todos.push({id: todos.length, todo: action.todo});
      },
      ...생략
    }
  • 상태값이 변경되었으니 출력하는 화면도 수정해야합니다. map함수의 출력하는 부분과 props를 가져오는 부분이 수정됐습니다.
// component/todoList.js
import React from 'react';  

function TodoList ( props ) {  
  const {todos, editTodo, removeTodo} = props;  
  const \[input, setInput\] = React.useState('');  
  const handleOnChange = e => {  
    setInput(e.target.value);  
  }  
  return (
    {todos.map((todo,index)=>(
    <button onClick={()=>{editTodo({id:todo.id, todo:input})}}>  
      EDIT
      <button onClick={()=>removeTodo(todo)}>REMOVE
   ))}
  )  
}  
export default TodoList;

완성됐습니다!

감사합니다.
다음 포스팅에서는 redux-saga와 reselect패키지에 대해 알아보겠습니다.

profile
웹 프론트엔드, RN앱 개발자입니다.

0개의 댓글