안녕하세요
Redux를 좀더 편리하게 사용하기위한 방식 Ducks패턴에 대해 알아보겠습니다.
Redux로 개발하는 대부분의 경우에는 Ducks패턴이 편리합니다.
Redux 공식문서 : Tutorial
실전 리액트프로그래밍 을 참고해 작성했습니다.
리덕스의 튜토리얼을 따라 보면 액션타입, 액션 생성자 함수, 리듀서를 각각 다른 파일에 작성합니다. 이런 식으로 구조중심으로 파일들을 나누게 되면 만약 기능을 추가해야 할 때, 적어도 3개 이상의 파일을 열어 수정을 해야합니다.
이런 문제점을 완화하기 위해 Ducks패턴이 제안됐습니다.
Ducks패턴은
Redux의 Ducks패턴을 이용해서 TodoList 앱을 처음부터 만들어보겠습니다.
create-react-app [redux-test]npm i redux react-redux immerindex.html의 필요없는 코드들도 지워주세요.npm start 로 실행시켜서 화면과 콘솔에 이상없는지 확인해봅니다.먼저 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함수를 이용해 불변객체로 리턴합니다.
다음은 todos라는 폴더를 만들고 state.js파일을 만듭니다.
2번에서 만든 createReducer함수를 이용해 reducer를 만들어 export합니다.
reducer를 만들기 위해 action들을 먼저 정의해야합니다.

여기서 Ducks패턴으로 작성합니다.
Ducks패턴은
// 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를 생성할 수 있습니다.

지금은 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를 출력하는 미들웨어를 작성했습니다.
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 합니다.
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-[이름] 의 형태로 사용하는데 읽기전용 속성입니다.
-`는 참조시에는 카멜케이스로 호출합니다.
<div id="mydiv" data-my-div="wrapper div">
// document.getElementById("mydiv").dataset.myDiv 로 호출 가능
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);
import React from 'react';
import TodoMain from './container/todoMain';
function App() {
return (
<TodoMain/>
);
}
export default App;
redux의 ducks패턴을 이용한 간단한 todolist가 완성됐습니다.

지금은 추가만 가능한 상태입니다.
수정과 삭제 기능을 넣어보겠습니다.
몇가지 코드를 수정해야합니다.
// todos/state.js
...생략
const reducer = createReducer(INITIAL_STATE, {
[ADD]: (state, action) => {
state.todos.push({id: todos.length, todo: action.todo});
},
...생략
}
// 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패키지에 대해 알아보겠습니다.