thumbnail
Redux와 React로 To Do List(투두리스트) 만들기
Mar 19, 2023

리덕스로 투두리스트 앱을 만들어보자!

기능 설계

To do list app image

간단한 투두리스트이므로 항목 추가와 삭제, 체크 표시, 필터 기능 정도만 구현해볼 것이다.

check list

todo 항목 추가

todo 항목 삭제

체크 토글 버튼으로 완료 표시

완료 항목 또는 완료되지 않은 항목과 같은 필터별 항목 표시 기능


기본 세팅

리덕스 설치

아래 명령어로 redux와 관련된 패키지들을 설치하자.

$ yarn add redux react-redux @reduxjs/toolkit

앱 구성

파일 구성은 아래와 같다.

📂 to-do-list
├── src
│ └── components
│ │ ├── filters
│ │ │ ├── Filters.js
│ │ │ └── filtersSlice.js
│ │ ├── todos
│ │ │ ├── Todo.js
│ │ │ ├── TodoList.js
│ │ │ └── todosSlice.js
│ │ ├── App.js
│ │ └── Input.js
│ ├── index.js
│ └── store.js

기능 구현

Slice

todo 항목에 대한 slice와 필터 slice를 분리해 생성해줄 것이다. 먼저 todosSlice에는 todo 항목을 추가, 삭제, 완료하는 세 가지의 액션이 필요하다.

초기 state는 빈 배열로 생성해준다.

/* todosSlice.js */ const initialState = { todos: [], };

액션 함수

  • addTodos는 새 할 일을 추가하면 text 속성에 해당 입력값을, completed 속성에 false 값을 저장한다.
  • checkTodos는 항목의 완료 표시를 토글 버튼을 누를 때마다 변경할 것이므로 completed 속성을 반대 불린값으로 업데이트한다.
  • deleteTodos는 삭제할 항목을 잘라낸 배열을 업데이트한다.
import { createSlice } from '@reduxjs/toolkit'; //... const todosSlice = createSlice({ name: 'todos', initialState, reducers: { addTodos(state, action) { state.todos.push({ text: action.payload, completed: false, }); }, checkTodos(state, action) { const todo = state.todos[action.payload]; todo.completed = !todo.completed; }, deleteTodos(state, action) { state.todos = [ ...state.todos.slice(0, action.payload), ...state.todos.slice(action.payload + 1), ]; }, }, });

todosSlice에 대한 액션과 리듀서를 내보낸다.

//... export const { addTodos, checkTodos, deleteTodos } = todosSlice.actions; export default todosSlice.reducer;

다음은 filtersSlice에 관한 내용이다. selectFilters는 필터 종류를 모아놓은 객체이며, 초기 state는 모든 항목을 보여주는 SHOW_ALL로 설정한다.

/* filtersSlice.js */ export const selectFilters = { SHOW_ALL: 'SHOW_ALL', SHOW_COMPLETED: 'SHOW_COMPLETED', SHOW_ACTIVE: 'SHOW_ACTIVE', }; const initialState = selectFilters.SHOW_ALL;

filterTodos는 인자로 받은 필터 문자열을 리턴한다.

import { createSlice } from '@reduxjs/toolkit'; //... const filtersSlice = createSlice({ name: 'filters', initialState, reducers: { filterTodos(state, action) { return action.payload; }, }, }); export const { filterTodos } = filtersSlice.actions; export default filtersSlice.reducer;

store(스토어)

store.js 파일에는 위에서 만들어준 todosSlicefiltersSlice를 reducer 속성에 넣어준다. configureStore가 알아서 합쳐줄 것이다.

/* store.js */ import { configureStore } from '@reduxjs/toolkit'; import todosSlice from './components/todos/todosSlice'; import filtersSlice from './components/filters/filtersSlice'; const store = configureStore({ reducer: { todosSlice: todosSlice, filtersSlice: filtersSlice, }, }); export default store;

store를 리액트 앱에 연동시키기 위해 index.js 파일에 <Provider> 컴포넌트를 추가하고 store를 전달한다.

//... import { Provider } from 'react-redux'; import store from './store'; import App from './components/App'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <Provider store={store}> <App /> </Provider>, );

메인 컴포넌트

app component

메인 컴포넌트인 App.js에 다음과 같이 기본 틀을 만들어준다. 크게 입력 폼, 할 일 목록, 필터 컴포넌트로 나눌 수 있다.

/* App.js */ export default function App() { return ( <div className="app"> <div className="todo-container"> <h1>To Do List</h1> <Input /> <TodoList /> <Filters /> </div> </div> ); }

액션 함수들을 불러와 이벤트 핸들러 함수를 각각 만들어 준 다음 해당 컴포넌트에 연결해준다.

/* App.js */ export default function App() { const dispatch = useDispatch(); const handleAddBtn = (text) => { dispatch(addTodos(text)); }; const handleToggle = (id) => { dispatch(checkTodos(id)); }; const handleDelBtn = (id) => { dispatch(deleteTodos(id)); }; const handleFilterChange = (filter) => { dispatch(filterTodos(filter)); }; return ( <div className="app"> <div className="todo-container"> <h1>To Do List</h1> <Input onAddClick={handleAddBtn} /> <TodoList onToggleClick={handleToggle} onDelClick={handleDelBtn} /> <Filters onFilterChange={handleFilterChange} /> </div> </div> ); }

selectTodos는 todos와 filter를 매개변수로 받아 해당 필터에 해당하는 할 일 목록을 출력한다. 필터는 모두 보여주는 ALL, 완료된 항목을 보여주는 Completed, 완료되지 않은 항목을 보여주는 active가 있다.

/* App.js */ // 필터별 todo 출력 const selectTodos = (todos, filter) => { switch (filter) { case selectFilters.SHOW_ALL: return todos; case selectFilters.SHOW_COMPLETED: return todos.filter((todo) => todo.completed); case selectFilters.SHOW_ACTIVE: return todos.filter((todo) => !todo.completed); default: return todos; } }; export default function App() { // ... const todos = useSelector((state) => state.todosSlice.todos); const filters = useSelector((state) => state.filtersSlice); const selectedTodos = selectTodos(todos, filters); return ( //... <Input onAddClick={handleAddBtn} /> <TodoList selectedTodos={selectedTodos} // selectedTodos 연결 onToggleClick={handleToggle} onDelClick={handleDelBtn} /> <Filters filters={filters} // filters 연결 onFilterChange={handleFilterChange} /> //... ); }

입력 폼

입력 폼은 Input 컴포넌트에서 담당한다. 입력한 텍스트의 상태는 useState로 관리해줄 것이다.

handleFormSubmit는 입력값이 빈 값이 아닌 경우 props로 받은 onAddClick에 text state를 전달한다.

/* Input.js */ export default function Input({ onAddClick }) { const [text, setText] = useState(''); const handleFormSubmit = (e) => { e.preventDefault(); if (text !== '') { onAddClick(text); } setText(''); }; const handleInputChange = (e) => { setText(e.target.value); }; return ( <div className="form-wrap"> <form onSubmit={handleFormSubmit}> <input type="text" placeholder="할 일을 추가해보세요." value={text} onChange={handleInputChange} /> <button>+</button> </form> </div> ); }

todo 목록

selectedTodos를 순회하며 개별 항목 컴포넌트인 Todo에 전개 연산자로 모든 속성을 props로 전달하고 이벤트 리스너에는 인덱스(=id)를 넘겨준다.

/* TodoList.js */ export default function TodoList({ selectedTodos, onToggleClick, onDelClick }) { return ( <ul> {selectedTodos.map((todo, index) => ( <Todo {...todo} key={index} onToggleClick={() => onToggleClick(index)} onDelClick={() => onDelClick(index)} /> ))} </ul> ); }

Todo 컴포넌트는 아래와 같이 작성한다. completed의 불린값을 이용해서 스타일을 지정할 수도 있다.

/* Todo.js */ export default function Todo({ text, completed, onToggleClick, onDelClick }) { return ( <li style={{ color: completed ? '#d1d1d1' : '#000', textDecoration: completed ? 'line-through' : 'none', }} > <button className="check-btn" onClick={onToggleClick}> ✓ </button> <p>{text}</p> <button className="delete-btn" onClick={onDelClick}> ✕ </button> </li> ); }

필터 기능

마지막으로 필터별로 항목을 보여주는 기능만 구현하면 된다. filterList에 보여줄 필터값을 배열로 저장해주고 순회하면서 renderFilter 인자로 배열 값을 전달한다.

renderFilter는 filter와 필터명 name을 매개변수로 받아 onFilterChange에 필터값을 전달함으로써 필터링된 할 일 목록을 띄워준다.

/* Filter.js */ export default function Filters({ filters, onFilterChange }) { const filterList = [ ['SHOW_ALL', 'ALL'], ['SHOW_COMPLETED', 'Completed'], ['SHOW_ACTIVE', 'Active'], ]; const renderFilter = (filter, name) => { const className = filters === filter ? 'selected' : ''; const handleFilterChange = (e) => { e.preventDefault(); onFilterChange(filter); }; return ( <button key={name} className={`filter-item ${className}`} onClick={handleFilterChange} > {name} </button> ); }; return ( <div className="filter-wrap"> <h3>filter</h3> <div className="filter-list"> {filterList.map((filter) => renderFilter(filter[0], filter[1]))} </div> </div> ); }

To do list app


References

Table Of Contents
nxnaxx blog © 2022-2024 Powered By Gatsby.