Pandaman Blog

[React.js]Redux를 이용해 TodoList 만들기 본문

Front end/React

[React.js]Redux를 이용해 TodoList 만들기

oyg0420 2020. 3. 25. 21:39

TodoList 만들기

이전 포스팅에서 Redux의 3원칙을 간단하게 학습했다.
오늘은 3가지 원칙을 활용해 TodoList를 만들어 리덕스를 이해해보자.

Redux 설치

예제를 시작하기 앞서 Redux와 React-Redux 설치가 필요하다.

터미널에 아래와 같이 입력한다.

npm install --save redux

또는

yarn add redux

그리고 리액트 애플리케이션에서 Redux의 사용을 편리하게 할 react-redux도 설치하자.
react-redux는 redux와 react 컴포넌트를 연동하여 store에 저장된 상태를 props로 전달하도록 도와준다.

npm install --save react-redux

또는

yarn add react-redux

Directory 구성

index.js 

components > TodoList.js // todo 작성과 작성된 todo들을 화면에 렌더링하는 컴포넌트  
           > Todo.js // todo컴포넌트
           > TodoForm.js // todo 작성 컴포넌트

modules > index.js 
          todoList > actions > index.js // 액션 정의
                   > reducers > index.js // 액션에 따른 새로운 상태 객체를 반환하는 Reducer작성

components > TodoList.js

아래는 TodoList에 대한 코드이다.

const TodoList = props => {
  const [toggle, setToggle] = useState(false);
  const handleToggle = () => {
    setToggle(!toggle);
  };
  return (
    <div>
      <button onClick={() => {}}>글쓰기</button>
      {toggle && <TodoForm onClickAddTodo={() => {}} />}
      <TodoListStyled>
        {todoList &&
          todoList.map(todo => (
            <Todo
              key={todo.id}
              id={todo.id}
              title={todo.title}
              description={todo.description}
              onClickDeleteTodo={() => {}}
            />
          ))}
      </TodoListStyled>
    </div>
  );
};

export default TodoList;

현재 todoList는 존재하지 않는다. 우리는 todoList를 Redux를 이용해 store에서 관리하여 전달할 예정이다. '글쓰기' 버튼은 컴포넌트의 노출 여부를 결정한다. 그리고 확인할 점은 TodoForm 컴포넌트의 onClickAddTodo와 Todo 컴포넌트의 onClickDeleteTodo 함수는 아무런 작동을 하지 않는다.
onClickAddTodo함수는 store에 todoList에 새로운 todo객체를 푸시 명령을 하는 함수이다.
onClickDeleteTodo 함수는 store의 todoList에 todo객체를 제거 명령을 하는 함수이다.

스토어에 저장된 todoList를 변화시키기 위해서는 무엇이 필요할까? 이전 포스팅에서 제대로 학습했다면, 바로 눈치챘을 것이다.
액션을 통해서만 우리는 todoList를 갱신할 수 있다. 따라서 액션이 필요하다.

modules > todoList > actions > index.js

export const ADDTODO = "todoList/ADDTODO";
export const DELETETODO = "todoList/DELETETODO";

export const addTodo = ({ id, title, description }) => ({
  type: ADDTODO,
  id,
  title,
  description
});

export const deleteTodo = id => ({
  type: DELETETODO,
  id
});

ADDTODO, DELETETODO은 액션 타입에 대한 정의이다. ADDTODO는 todo를 생성하기 위한 액션 타입이고, DELETETODO는 저장된 todo를 제거하기 위한 타입이다.
addTodo, deleteTodo은 액션생성 함수에 대한 정의이다. 컴포넌트에서 이벤트가 발생했을 때 이벤트 핸들러로 액션 생성 함수를 사용하며, 컴포넌트에서의 인수를 액션 생성 함수로 전달할 수 있다.

이제 전달받은 데이터(payload)는 원하는 데이터로 가공해 저장해야 한다. 이 부분은 바로 Reducer에서 처리한다.

modules > todoList > reducers > index.js

import { ADDTODO, DELETETODO } from "../actions";

export default function todoList(state = [], action) {
  switch (action.type) {
    case ADDTODO:
      return [
        ...state,
        {
          id: action.id,
          title: action.title,
          description: action.description
        }
      ];
    case DELETETODO:
      const nextState = state.filter(todo => todo.id !== action.id);
      return nextState;
    default:
      return state;
  }
}

todoList 함수의 첫 번째 인자는 초기 state를 정의한다. 두번째 인자는 action 객체이다. action 객체에는 우리가 액션 생성 함수에서 반환하는 객체를 말한다.
switch 구문을 통해서 액션타입에 따라 데이터를 가공하고 객체를 반환한다.
Redux의 3가지 원칙에서 Reducer(리듀서)는 이전 상태와 액션을 인자로 받아 새로운 상태를 리턴하는 순수 함수라고 정의했다. 위 코드를 확인해보면 기존의 상태를 변경하지 않고, 새로운 객체를 반환하는 것을 확인하자.

이제 리듀서가 생성되었으니 스토어에 저장해야 한다. 그전에 해야 할 일이 있다. Reducer가 굉장히 많은 경우를 대비해 Reducer을 하나로 통합시켜주는 녀석이 존재한다.

modules > todoList > index.js

import { combineReducers } from "redux";
import todoList from "./togoList/reducers";

export default combineReducers({
  todoList
});

combineReducers는 정의한 Reducer들을 하나로 통합시켜주는 역할을 한다. 물론 현재는 하나의 리듀서만 존재하지만 프로젝트가 커질수록 리듀서는 늘어날 수밖에 없다.

이제 리듀서에서 반환한 객체를 스토어에 저장해야 한다.

index.js

import React from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";
import rootReducer from "./modules";
import TodoList from "./components/TodoList";
// 리덕스 개발자 도구
const devTools =
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();

const store = createStore(rootReducer, devTools);

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

rootReducermodules > todoList > index.js를 말한다.
createStore함수를 통해서 rootReducer에서 반환한 상태 객체를 저장한다. 그리고 반환된 store를 다시 화면에 보여주어야 한다.
이때 등장하는 것이 바로 Provider이다. Provider는 하위 컴포넌트에게 store에 접근할 수 있도록 한다.
일반적인 컴포넌트와 다른 점은 parent-child 구조로 store를 전달하는 것이 아닌 connect를 이용하여 하위 컴포넌트들은 store에 접근할 수 있다.

이제 store를 위에 작성한 TodoList 컴포넌트와 connect 해주어야 한다.

components > TodoList.js

const TodoList = props => {
  const { todoList, handleAddTodo, handleDeleteTodo } = props;
  const [toggle, setToggle] = useState(false);
  const handleToggle = () => {
    setToggle(!toggle);
  };
  return (
    <div>
      <button onClick={handleToggle}>글쓰기</button>
      {toggle && <TodoForm onClickAddTodo={handleAddTodo} />}
      <TodoListStyled>
        {todoList &&
          todoList.map(todo => (
            <Todo
              key={todo.id}
              id={todo.id}
              title={todo.title}
              description={todo.description}
              onClickDeleteTodo={handleDeleteTodo}
            />
          ))}
      </TodoListStyled>
    </div>
  );
};

const mapStateToProps = state => {
  return {
    todoList: state.todoList
  };
};

const mapDispatchToProps = dispatch => ({
  handleAddTodo: ({ id, title, description }) =>
    dispatch(addTodo({ id, title, description })),
  handleDeleteTodo: id => dispatch(deleteTodo(id))
});

export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

TodoList가 store와 연동하기 위해서는 connect를 사용한다.

 

connect에 인자에 들어가는 녀석들이 무엇인지 확인해보자.
mapStateToProps는 store가 가진 state를 props로 지정된 컴포넌트에게 어떻게 전달할지를 정하는 함수이다.
mapDispatchToProps는 reducer에 action을 알리는 dispatch를 props로 지정된 컴포넌트에게 어떻게 전달할지 정하는 함수이다.
connect에 mapStateToProps, mapDispatchToProps을 지정하고 props를 받을 컴포넌트를 지정한다.
store와 reducer가 연결된 컴포넌트가 반환된다.

 

따라서 TodoList 컴포넌트에는 props로 todoList, 액션을 알리는 함수 handleAddTodo, handleDeleteTodo가 전달된다.

코드에서 todoList []의 속성들을 Todo컴포넌트에 전달하며, 삭제 액션을 요청하는 함수 handleDeleteTodo도 역시 전달한다.

TodoForm에는 추가 액션을 요청하는 함수 handleAddTodo를 전달한다.

components > Todo.js

Todo 컴포넌트에 대해 작성한 코드이다.

const Todo = ({ id, title, description, onClickDeleteTodo }) => {
  return (
    <TodoStyled>
      <TodoTitleStyled>{title}</TodoTitleStyled>
      <TodoDescriptionStyled>{description}</TodoDescriptionStyled>
      <button onClick={() => onClickDeleteTodo(id)}>삭제</button>
    </TodoStyled>
  );
};

export default Todo;

Todo컴포넌트는 props로 id, title, description, onClickDeleteTodo를 부모 컴포넌트로부터 전달받아 그대로 화면에 뿌려준다.
onClickDeleteTodo는 삭제 액션을 요청하는 함수이다.

components > TodoForm.js

todo를 작성하기 위한 폼이다.

const TodoForm = ({ onClickAddTodo }) => {
  const [title, setTitle] = useState("");
  const [description, setDescription] = useState("");

  const onChangeTitleHandler = event => {
    setTitle(event.target.value);
  };

  const onChangeDescriptionHandler = event => {
    setDescription(event.target.value);
  };

  return (
    <TodoStyled>
      <input onChange={onChangeTitleHandler} value={title} />
      <textarea onChange={onChangeDescriptionHandler} value={description} />
      <button
        onClick={() =>  onClickAddTodo({ id: Math.random() * 10, title, description })}
      >
        추가
      </button>
    </TodoStyled>
  );
};

export default TodoForm;

onClickAddTodo는 생성 액션 요청과 함께 id와 title, description 속성으로 이루어진 객체를 전달한다.

 

 

이제 구현된 애플리케이션을 실행해보자.

 

글쓰기 버튼을 클릭하고 글을 작성하고 추가하면 Chrome dev tool > redux > action 탭에는 액션 객체에 대해 보여준다.

액션 타입과 payload을 한눈에 확인할 수 있다. 신기하다.

todo 추가화면과 액션객체

삭제 버튼을 클릭해보자.

todo 삭제화면과 액션객체

지금까지 Redux를 사용하여 간단한 TodoList를 만들어보았다.

사실 이번 예제로 Redux가 어떻게 작동하는지에 대해 알아본 것이고, 실제로 유용한 지 확인하기 위해서는 직접 경험이 필요하다고 생각한다. 필자는 프로젝트 규모에 커질수록 상태 관리가 어려워진다는 것을 몸으로 느끼고 있다.. 

원본코드는 https://github.com/oyg0420/redux-todo-list 에서 확인하세요.

 

다음 포스팅은 middleware인 redux-saga에 대해 포스팅하겠습니다.

'Front end > React' 카테고리의 다른 글

[React.js]React Hook  (8) 2020.06.01
[React.js]Redux Toolkit를 이용해 TodoList만들기  (2) 2020.04.21
[React.js] Redux의 3가지 원칙  (0) 2020.03.24
[React.js] Component lifecycle  (7) 2020.03.17
[React.js] 리액트 state와 setState  (0) 2020.03.12
Comments