![thumbnail](/static/025749714b4e520137b5ed095828db94/ab134/230319_redux-thumbnail.png)
리덕스로 투두리스트 앱을 만들어보자!
기능 설계
![To do list app image To do list app image](/static/025749714b4e520137b5ed095828db94/fe486/230319_redux-thumbnail.png)
간단한 투두리스트이므로 항목 추가와 삭제, 체크 표시, 필터 기능 정도만 구현해볼 것이다.
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
파일에는 위에서 만들어준 todosSlice
와 filtersSlice
를 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 component](/static/13430675258c7f6cbad65d6ded4e9a41/f6e13/230319_app.png)
메인 컴포넌트인 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>
);
}
References