React Redux TodoList Example

류지승·2025년 2월 15일

React

목록 보기
18/19
post-thumbnail

https://react-redux.js.org/tutorials/connect

Example Introduce

React UI Components

TodoApp

TodoAppUI Components를 감싸는 부모 컴포넌트이다. header, AddTodo, TodoList, VisibilityFilters로 구성되어 있다.

import React from "react";
import AddTodo from "./components/AddTodo";
import TodoList from "./components/TodoList";
import VisibilityFilters from "./components/VisibilityFilters";
import "./styles.css";

export default function TodoApp() {
  return (
    <div className="todo-app">
      <h1>Todo List</h1>
      <AddTodo />
      <TodoList />
      <VisibilityFilters />
    </div>
  );
}

AddTodo

AddTodo는 사용자가 할 일 항목을 입력하고 Add Todo Button을 클릭하여 목록에 추가할 수 있는 컴포넌트이다. Controlled input을 사용해 onChange 이벤트로 상태를 관리하며, 버튼 클릭시 addTodo Actiondispatch하여 새로운 Todo를 store에 추가한다.

// class component
import React from "react";
import { connect } from "react-redux";
import { addTodo } from "../redux/actions";

class AddTodo extends React.Component {
  constructor(props) {
    super(props);
    this.state = { input: "" };
  }

  updateInput = input => {
    this.setState({ input });
  };

  handleAddTodo = () => {
    this.props.addTodo(this.state.input);
    this.setState({ input: "" });
  };

  render() {
    return (
      <div>
        <input
          onChange={e => this.updateInput(e.target.value)}
          value={this.state.input}
        />
        <button className="add-todo" onClick={this.handleAddTodo}>
          Add Todo
        </button>
      </div>
    );
  }
}

export default connect(
  null,
  { addTodo }
)(AddTodo);

export default AddTodo;

// function component
import React, { useState } from "react";
import { connect } from "react-redux";
import { addTodo } from "../redux/actions";

const AddTodo = ({ addTodo }) => {
  const [input, setInput] = useState("");

  const updateInput = (input) => {
    setInput(input);
  };

  const handleAddTodo = () => {
    addTodo(input);
    setInput("");
  };

  return (
    <div>
      <input
        onChange={(e) => updateInput(e.target.value)}
        value={input}
      />
      <button className="add-todo" onClick={handleAddTodo}>
        Add Todo
      </button>
    </div>
  );
};

export default connect(
  null,
  { addTodo }
)(AddTodo);

export default AddTodo;

TodoList

TodoList는 할 일 리스트 전체를 보여주는 component로써 VisibilityFilters가 선택되었을 때, 필터된 할 일 리스트를 보여준다.

import React from "react";
import { connect } from "react-redux";
import Todo from "./Todo";
// import { getTodos } from "../redux/selectors";
import { getTodosByVisibilityFilter } from "../redux/selectors";
import { VISIBILITY_FILTERS } from "../constants";

const TodoList = ({ todos }) => (
  <ul className="todo-list">
    {todos && todos.length
      ? todos.map((todo, index) => {
          return <Todo key={`todo-${todo.id}`} todo={todo} />;
        })
      : "No todos, yay!"}
  </ul>
);

// const mapStateToProps = state => {
//   const { byIds, allIds } = state.todos || {};
//   const todos =
//     allIds && state.todos.allIds.length
//       ? allIds.map(id => (byIds ? { ...byIds[id], id } : null))
//       : null;
//   return { todos };
// };

const mapStateToProps = state => {
  const { visibilityFilter } = state;
  const todos = getTodosByVisibilityFilter(state, visibilityFilter);
  return { todos };
  //   const allTodos = getTodos(state);
  //   return {
  //     todos:
  //       visibilityFilter === VISIBILITY_FILTERS.ALL
  //         ? allTodos
  //         : visibilityFilter === VISIBILITY_FILTERS.COMPLETED
  //           ? allTodos.filter(todo => todo.completed)
  //           : allTodos.filter(todo => !todo.completed)
  //   };
};
// export default TodoList;
export default connect(mapStateToProps)(TodoList);

Todo

TodoTodoList에서의 할 일 중 하나이다. 할 일을 완료되었으면 취소선이 그어지며 onClick시 해당 할 일을 완료 상태를 토글하는 toggleTodo action을 dispatch합니다.

import React from "react";
import { connect } from "react-redux";
import cx from "classnames";
import { toggleTodo } from "../redux/actions";

const Todo = ({ todo, toggleTodo }) => (
  <li className="todo-item" onClick={() => toggleTodo(todo.id)}>
    {todo && todo.completed ? "👌" : "👋"}{" "}
    <span
      className={cx(
        "todo-item__text",
        todo && todo.completed && "todo-item__text--completed"
      )}
    >
      {todo.content}
    </span>
  </li>
);

// export default Todo;
export default connect(
  null,
  { toggleTodo }
)(Todo);

VisibilityFilters

VisibilityFilters는 TodoList를 필터링하는 버튼을 포함하는 컴포넌트이다. all / completed / incomplete 필터 버튼을 클락하여 할 일 목록을 필터링할 수 있다. 선택한 필터를 activeFilter prop를 통해 부모 컴포넌트로부터 받으며 선택된 필터는 밑줄로 강조 표시된다. setFilter action을 dispatch하여 filter를 업데이트한다.

import React from "react";
import cx from "classnames";
import { connect } from "react-redux";
import { setFilter } from "../redux/actions";
import { VISIBILITY_FILTERS } from "../constants";

const VisibilityFilters = ({ activeFilter, setFilter }) => {
  return (
    <div className="visibility-filters">
      {Object.keys(VISIBILITY_FILTERS).map(filterKey => {
        const currentFilter = VISIBILITY_FILTERS[filterKey];
        return (
          <span
            key={`visibility-filter-${currentFilter}`}
            className={cx(
              "filter",
              currentFilter === activeFilter && "filter--active"
            )}
            onClick={() => {
              setFilter(currentFilter);
            }}
          >
            {currentFilter}
          </span>
        );
      })}
    </div>
  );
};

const mapStateToProps = state => {
  return { activeFilter: state.visibilityFilter };
};
// export default VisibilityFilters;
export default connect(
  mapStateToProps,
  { setFilter }
)(VisibilityFilters);

The Redux Store

Store

todos는 할 일 목록을 관리하는 store로써 byIdsallIds의 state가 존재한다., byIds는 할 일 내용을 담은 content와 할 일을 완료했는 지 확인하는 complete(boolean)으로 구성되어 있는 object이다. allIds는 현재 할 일 리스트에 존재하는 모든 id를 담은 array이다.

const initialState: { 
  allIds: number[], 
  byIds: { [key: number]: { content: string, completed: boolean } } 
} = {
  allIds: [],
  byIds: {}
};

visibilityFilters는 현재 어떤 filter가 focus 되었는 지 확인하는 store이다.

export const VISIBILITY_FILTERS = {
  ALL: "all",
  COMPLETED: "completed",
  INCOMPLETE: "incomplete"
};

const initialState = VISIBILITY_FILTERS.ALL;

storerootReducer에서 combineReducers를 통해서 선언된 reducer를 통합하고 이후 rootReducer를 createStore에 인자로 넣어 store를 생성한다.

// rootReducer
import { combineReducers } from "redux";
import visibilityFilter from "./visibilityFilter";
import todos from "./todos";

export default combineReducers({ todos, visibilityFilter });

// store
import { createStore } from "redux";
import rootReducer from "./reducers";

export default createStore(rootReducer);

Action Creators

// actionTypes.js
export const ADD_TODO = "ADD_TODO";
export const TOGGLE_TODO = "TOGGLE_TODO";
export const SET_FILTER = "SET_FILTER";

addTodo 새로운 할 일을 TodoList에 추가하는 action creator. 콘텐츠와 함께 id++를 payload로 추가한다.

import { ADD_TODO } from "./actionTypes";

let nextTodoId = 0;

export const addTodo = content => ({
  type: ADD_TODO,
  payload: {
    id: ++nextTodoId,
    content
  }
});

toggleTodo 특정 할 일의 완료 상태를 토글하는 action creator id를 받아서 해당 할 일을 토글한다.

import { TOGGLE_TODO } from "./actionTypes";

export const toggleTodo = id => ({
  type: TOGGLE_TODO,
  payload: { id }
});

setFilter 필터를 설정하는 action creator 필터 값(all / completed / incomplete)으로 payload를 받는다.

import { SET_FILTER } from "./actionTypes";

export const setFilter = filter => ({ type: SET_FILTER, payload: { filter } });

Reducers

todos reducer

ADD_TODO action을 dispatch로 받으면 allIds에 ID를 추가하고 byIds에 해당 ID의 할 일(content)와 완료 여부(completed = default false) 저장한다.
TOGGLE_TODO action을 dispatch로 받으면 해당 ID에 completed의 boolean을 논리 NOT 연산자를 이용하여 반전시킨다.

export default function(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO: {
      const { id, content } = action.payload;
      return {
        ...state,
        allIds: [...state.allIds, id],
        byIds: {
          ...state.byIds,
          [id]: {
            content,
            completed: false
          }
        }
      };
    }
    case TOGGLE_TODO: {
      const { id } = action.payload;
      return {
        ...state,
        byIds: {
          ...state.byIds,
          [id]: {
            ...state.byIds[id],
            completed: !state.byIds[id].completed
          }
        }
      };
    }
    default:
      return state;
  }
}

visibilityFilters reducer

SET_FILTER action을 dispatch하면 action.payload에 있는 filter를 새로운 값으로 설정한다.

import { SET_FILTER } from "../actionTypes";

const visibilityFilter = (state = initialState, action) => {
  switch (action.type) {
    case SET_FILTER: {
      return action.payload.filter;
    }
    default: {
      return state;
    }
  }
};

export default visibilityFilter;

Action Types

`actionType.js``파일에 액션 타입을 상수로 정의해두고 이를 사용하여 action을 dispatch한다.

export const ADD_TODO = "ADD_TODO";
export const TOGGLE_TODO = "TOGGLE_TODO";
export const SET_FILTER = "SET_FILTER";

ActionType.js vs constant.js

공통점으로 둘다 문자열을 변수화 시킨다는 점이다.
하지만 파일명으로 보시다시피 둘 파일은 차이가 분명히 존재한다.
ActionType.js같은 경우 Redux Action 에서의 type을 하드코딩 하지 않고 문자열 상수화 시켜 추후 중복코드를 방지하고 유지보수성을 높이는데 사용한다.
constant.js 같은 경우 값들의 집합이다. 예를 들면 VISIBILITY_FILTERS라고 가정해보자.

export const VISIBILITY_FILTERS = {
  ALL: "all",
  COMPLETED: "completed",
  INCOMPLETE: "incomplete"
};

해당 객체는 filter의 값을 모아둔 객체이다. 하드코딩하지 않고 변수화를 시키면 마찬가지로 유지보수성이든, 오타 방지할 수 있다.
결론적으로 constant 는 단순 상수의 집합 / actionType은 Redux Action 관련된 상수

Selectors

getTodoListtodos store에allIds: Array<number>를 받아온다.

export const getTodosState = store => store.todos;

// getTodoState에서 객체가 존재하면 allIds를 반환 아니면 배열
export const getTodoList = store =>
  getTodosState(store) ? getTodosState(store).allIds : [];

getTodoById id를 통해 todo를 찾음. 반환값으로 { content: string, complete: boolean, id: number }

// getTodoState(store)로 todo에 대한 store를 불러오고
// byIds[id]로 접근하여 객체 { content: string, complete: boolean }을 가져오고
// 스프레드 연산자를 이용하여 기존 객체에 id를 추가한다.
export const getTodoById = (store, id) =>
  getTodosState(store) ? { ...getTodosState(store).byIds[id], id } : {};

getTodos getTodoList를 통해서 allIds를 받아오고, getTodosState를 통해 store에 존재하는 모든 byIds에 있는 객체를 반환한다.

export const getTodos = store =>
  getTodoList(store).map(id => getTodoById(store, id));

// 반환값 예시
// [
//  { content: "Learn Redux", completed: false, id: 1 },
//  { content: "Build a project", completed: true, id: 2 },
//  { content: "Deploy the app", completed: false, id: 3 }
// ]

getTodosByVisibilityFilter는 마지막 단계로써 getTodos로 todoList를 불러오고 filter의 값에 따라 todo를 필터해주는 함수이다.

export const getTodosByVisibilityFilter = (store, visibilityFilter) => {
  const allTodos = getTodos(store);
  switch (visibilityFilter) {
    case VISIBILITY_FILTERS.COMPLETED:
      return allTodos.filter(todo => todo.completed);
    case VISIBILITY_FILTERS.INCOMPLETE:
      return allTodos.filter(todo => !todo.completed);
    case VISIBILITY_FILTERS.ALL:
    default:
      return allTodos;
  }
};

Providing the Store

처음에는 store를 react에 제공하기 위해서 최상단에 <Provide />를 감싸준 후, props에 store를 !
등록해줘야한다.

// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import TodoApp from './TodoApp'

import { Provider } from 'react-redux'
import store from './redux/store'

// As of React 18
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
  <Provider store={store}>
    <TodoApp />
  </Provider>,
)

Connecting the Components

React Redux는 store에 있는 state를 읽거나 state를 업데이트할 때 다시 읽기 위해 function connect를 제공한다. connect에는 opional인 2개의 argument를 제공한다.

mapStateToProps / mapDispatchToProps
mapStateToProps는 store에 state가 변경될 때마다 호출되며 전체 Redux Store의 state를 받아와서 필요한 데이터만 객체 형태로 반환한다. 즉, 필요한 state를 컴포넌트의 props로 매핑해준다.

mapDispathchToProps는 redux의 dispatch를 props로 전달하는 역할을 하며, function / object 형태로 전달한다.function 이면 컴포넌트가 생성될 때 한 번만 호출된다. 이 function은 dispatch를 인자로 받고 dispatch를 사용하여 액션을 호출하는 함수들이 포함된 객체를 반환해야한다.

onst mapStateToProps = (state, ownProps) => ({
  // ... computed data from state and optionally ownProps
})
>
const mapDispatchToProps = {
  // ... normally is an object full of action creators
}
>
// `connect` returns a new function that accepts the component to wrap:
const connectToStore = connect(mapStateToProps, mapDispatchToProps)
// and that function returns the connected, wrapper component:
const ConnectedComponent = connectToStore(Component)
>
// We normally do both in one step, like this:
connect(mapStateToProps, mapDispatchToProps)(Component)

mapDispatchToProps

// AddTodo Action
import { ADD_TODO } from './actionTypes'

let nextTodoId = 0
export const addTodo = (content) => ({
  type: ADD_TODO,
  payload: {
    id: ++nextTodoId,
    content,
  },
})

다음은 TodoList에 추가하는 addTodo action creator function이다. 이를 <AddTodo/> Component와 연결하기 위해서는 connectmapDispatchToProps addTodo를 넘겨주면 된다.

// components/AddTodo.js

// ... other imports
import { connect } from 'react-redux'
import { addTodo } from '../redux/actions'

class AddTodo extends React.Component {
  // ... component implementation
}

export default connect(null, { addTodo })(AddTodo)

이렇게 되면 AddTodo Component는 props로 addTodo function을 받게 된다. class component에서의 props를 전달받으려면 this.props.addTodo(state) 이런형식으로 props를 전달받을 수 있다. 또한 addTododispatch하고, input을 reset할 handleAddTodo가 필요하다.

// 최종 AddTodo Component
// components/AddTodo.js

import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../redux/actions'

class AddTodo extends React.Component {
  // ...
  /* 
  handleAddTodo
  => props로 전달받은 addTodo로 입력된 state를 dispatch하고
  state input value를 reset하기
  */
  handleAddTodo = () => {
    // dispatches actions to add todo
    this.props.addTodo(this.state.input)

    // sets state back to empty string
    this.setState({ input: '' })
  }

  render() {
    return (
      <div>
        <input
          onChange={(e) => this.updateInput(e.target.value)}
          value={this.state.input}
        />
        <button className="add-todo" onClick={this.handleAddTodo}>
          Add Todo
        </button>
      </div>
    )
  }
}

export default connect(null, { addTodo })(AddTodo)

mapStateToProps

<TodoList /> Componenttodo가 변경될 때마다 리렌더를 진행해야하기 때문에, connectmapStateToProps를 넣어줘야한다.

// components/TodoList.js

// ...other imports
import { connect } from "react-redux";

const TodoList = // ... UI component implementation

const mapStateToProps = state => {
  const { byIds, allIds } = state.todos || {};
  const todos =
    allIds && allIds.length
      ? allIds.map(id => (byIds ? { ...byIds[id], id } : null))
      : null;
  return { todos };
};

export default connect(mapStateToProps)(TodoList);

// filter logic
const mapStateToProps = state => {
  const { visibilityFilter } = state;
  const todos = getTodosByVisibilityFilter(state, visibilityFilter);
  return { todos };
};

Common ways of calling connect

Do Not Subscribe to the StoreSubscribe to the Store
Do Not Inject Action Creatorsconnect()(Component)connect(mapStateToProps)(Component)
Inject Action Creatorsconnect(null, mapDispatchToProps)(Component)connect(mapStateToProps, mapDispatchToProps)(Component)
profile
성실(誠實)한 사람만이 목표를 성실(成實)한다

0개의 댓글