이번 시간에는 새로운 todo 추가, 삭제, Clear Completed 기능을 구현해보도록 하겠습니다.
참고로 이번 TodoApp의 경우 atom
, selector
, useRecoilState
3가지만 있으면 모두 구현할 수 있기 때문에 더 이상 새로운 recoil
의 기능이 나오지는 않을 것입니다.
이 TodoApp을 완성한 이후에 약간 실전에 가까운 예제(ex: API 통신하는 예제)를 적당히 생각해보고, 따로 포스팅을 하면서 다른 recoil
기능들도 한 번 탐방 해보도록 하겠습니다.
새로운 todo를 추가하는 기능은 간단합니다. todos
state에 새로운 todo
를 추가하면 됩니다. 이를 위해 todos
를 useSetRecoilState
로 설정하면 될 것 같습니다. 새로운 아이템을 추가할 수 있는 input
을 가진 Header
컴포넌트에서 구현하면 되겠습니다.
현재 Header
컴포넌트는 아래와 같습니다.
// src/components/Header/index.js
function Header() {
return (
<header className="header">
<h1>todos</h1>
<input className="new-todo" placeholder="What needs to be done?" autofocus />
</header>
)
}
export default Header
우선 input
폼을 리액트 컴포넌트와 동기화해야 할 것 같습니다. useState
를 이용해 input
폼을 연동해봅시다.
import { useState } from 'react'
function Header() {
const [value, setValue] = useState('')
const handleInput = e => {
setValue(e.target.value)
}
return (
<header className="header">
<h1>todos</h1>
<input className="new-todo" placeholder="What needs to be done?" value={ value } onInput={ handleInput } />
</header>
)
}
export default Header
이제 input
창에 정상적으로 글씨가 입력됩니다. 아시다시피 리액트에서는 onChange
류 이벤트를 이용해 value
바인딩을 해줘야 합니다.
새로 입력된 값을 todos
state에 추가해주기 위해 createTodo
, todos
, useSetRecoilState
를 import
하고 구현합니다.
import { useState } from 'react'
import { useSetRecoilState } from 'recoil'
import { createTodo, todos } from '../../state/todos'
function Header() {
const [value, setValue] = useState('')
const handleInput = e => {
setValue(e.target.value)
}
const setTodos = useSetRecoilState(todos)
const handleAddTodo = e => {
// 엔터키로 새로운 아이템을 입력한다.
// 엔터키가 아니면 종료
if (!(e.key === 'Enter' || e.keyCode === 13)) {
return
}
const text = value.trim()
// 공백 문자열이면 따로 추가하지 않고
// 현재 input 창도 공백으로 만들고 종료
if (text === '') {
setValue('')
return
}
// 현재 input창 공백으로 만들고
// todos를 새로 추가
setValue('')
setTodos(todos => [
...todos,
createTodo(text),
])
}
return (
<header className="header">
<h1>todos</h1>
<input className="new-todo" placeholder="What needs to be done?" value={ value } onInput={ handleInput } onKeyDown={ handleAddTodo }/>
</header>
)
}
export default Header
특정 아이템을 삭제하기 위해서는 특정 아이템의 상태를 토글했을 때와 비슷하게 구현할 수 있습니다. Todo
컴포넌트에서 바로 구현하도록 합니다. 구현 전, 현재까지의 코드는 아래와 같습니다.
// src/components/Main/Todo.js
import { useSetRecoilState } from 'recoil'
import { todos } from '../../state/todos'
function Todo(props) {
const { id, done, text } = props.todo
const setTodos = useSetRecoilState(todos)
const toggleTodo = checked => {
setTodos(todos => todos.map(todo => {
return todo.id === id
? { ...todo, done: checked, }
: todo
}))
}
const handleToggle = e => {
const { checked } = e.target
toggleTodo(checked)
}
return (
<li className={ done ? 'completed' : '' }>
<div className="view">
<input className="toggle" type="checkbox" checked={ done } onChange={ handleToggle }/>
<label>{ text }</label>
<button className="destroy" />
</div>
<input className="edit" value="Create a TodoMVC template" />
</li>
)
}
export default Todo
button
태그 클릭 이벤트를 이용하면 되겠습니다. setTodos
가 이미 존재하기 때문에, toggleTodo
와 비슷하게 구현하면 됩니다. 단, 이번엔 대상 아이템만 필터링한 새로운 todos
를 등록해주면 되겠습니다.
import { useSetRecoilState } from 'recoil'
import { todos } from '../../state/todos'
function Todo(props) {
// ... 생략
// 👇👇👇
const handleDestroy = () => {
setTodos(todos => todos.filter(todo => todo.id !== id))
}
return (
<li className={ done ? 'completed' : '' }>
<div className="view">
<input className="toggle" type="checkbox" checked={ done } onChange={ handleToggle } />
<label>{ text }</label>
{/* 👇👇👇 */}
<button className="destroy" onClick={ handleDestroy }/>
</div>
<input className="edit" value="Create a TodoMVC template" />
</li>
)
}
export default Todo
완료된 모든 아이템을 삭제하는 기능입니다. 마찬가지로 todos
state에서 done: true
인 항목들을 제외한 아이템들을 필터링하면 됩니다. 현재 Footer
컴포넌트는 아래와 같습니다.
// src/components/Footer/index.js
import { useRecoilState } from 'recoil'
import * as state from '../../state/todos'
function Footer() {
const [filterType, setFilterType] = useRecoilState(state.filterType)
return (
<footer className="footer">
<span className="todo-count"><strong>0</strong> item left</span>
<ul className="filters">
<li>
<a className={ filterType === 'all' ? 'selected' : '' } href="#/" onClick={ () => setFilterType('all') }>All</a>
</li>
<li>
<a className={ filterType === 'do' ? 'selected' : '' } href="#/active" onClick={ () => setFilterType('do') }>Active</a>
</li>
<li>
<a className={ filterType === 'done' ? 'selected' : '' } href="#/completed" onClick={ () => setFilterType('done') }>Completed</a>
</li>
</ul>
<button className="clear-completed">Clear completed</button>
</footer>
)
}
export default Footer
todos
state를 바꿀 수 있도록 useSetRecoilState
를 추가적으로 import
하면 될 것 같습니다.
물론
useRecoilState
로도 할 수 있습니다만, 지금은todos
값 자체가 필요한게 아니므로setter
에 충실한useSetRecoilState
를 사용하도록 합니다. 이렇게getter
와setter
를 구분해서 사용해서 얻을 수 있는 이익이 있는데, 위와 같이setter
만 사용한다면todos
의 state가 바뀌었을 때 현재Footer
컴포넌트는todos
의 값을 사용하고 있지 않기 때문에 렌더링이 발생하지 않는다는 점입니다.
// 👇👇👇
import { useRecoilState, useSetRecoilState } from 'recoil'
import * as state from '../../state/todos'
function Footer() {
const [filterType, setFilterType] = useRecoilState(state.filterType)
// 👇👇👇
const setTodos = useSetRecoilState(state.todos)
// 👇👇👇
const handleClearCompleted = () => {
setTodos(todos => todos.filter(todo => !todo.done))
}
return (
<footer className="footer">
<span className="todo-count"><strong>0</strong> item left</span>
<ul className="filters">
<li>
<a className={ filterType === 'all' ? 'selected' : '' } href="#/" onClick={ () => setFilterType('all') }>All</a>
</li>
<li>
<a className={ filterType === 'do' ? 'selected' : '' } href="#/active" onClick={ () => setFilterType('do') }>Active</a>
</li>
<li>
<a className={ filterType === 'done' ? 'selected' : '' } href="#/completed" onClick={ () => setFilterType('done') }>Completed</a>
</li>
</ul>
{/* 👇👇👇 */}
<button className="clear-completed" onClick={ handleClearCompleted }>Clear completed</button>
</footer>
)
}
export default Footer
오늘은 새로운 recoil
API를 따로 알아보진 않았지만, getter
와 setter
함수를 왜 따로 사용하는지 알아보았습니다.
핵심은 setter
기능만 있는 경우, useSetRecoilState
훅 함수를 사용해야 불필요한 렌더링을 줄일 수 있다는 점입니다.
이제 마무리를 향해 달려가고 있습니다. 현재 남은 기능인 모든 체크 토글, 남은 아이템 갯수 확인, 아이템 내용 수정은 다음 포스팅에서 알아보도록 하겠습니다.