이번 시간에는 todos를 필터링하는 기능을 만들어 보도록 하겠습니다. 이를 위해 recoil
의 selector
와 useRecoilState
, useSetRecoilState
도 한 번 사용해보도록 하겠습니다.
todos를 3가지 상태에 따라 보여줄 수 있습니다.
done
: false
)done
: true
)1번은 그냥 모든 아이템을 뿌려주면 되고, 2, 3번은 done
값에 따라 뿌려주면 됩니다.
현재 우리는 atom
으로 만든 todos
가 있습니다. 그리고 이 todos
를 기반으로 현재의 필터링 속성에 따라 어떻게 뿌려줄지 결정하면 됩니다.
그럼 우선, 필터링 속성에 관한 상태가 필요합니다. 바로 atom
으로 정의해줍니다.
// src/state/todos.js
import { atom } from 'recoil'
let uniqId = 0
export const createTodo = text => ({
id: ++uniqId,
done: false,
text,
})
export const todos = atom({
key: 'todos',
default: [
createTodo('react 공부하기'),
createTodo('recoil 공부하기'),
],
})
// 👇👇👇
export const filterType = atom({
key: 'filterType',
default: 'all',
})
filterType
상태를 정의했습니다. 기본값은 all
이고, do
와 done
의 상태가 있다고 가정할겁니다. 이제, filterType
에 따라 todos
를 보여줘야됩니다. selector
가 등장할 차례입니다.
// src/state/todos.js
// 👇👇👇
import { atom, selector } from 'recoil'
let uniqId = 0
export const createTodo = text => ({
id: ++uniqId,
done: false,
text,
})
export const todos = atom({
key: 'todos',
default: [
createTodo('react 공부하기'),
createTodo('recoil 공부하기'),
],
})
export const filterType = atom({
key: 'filterType',
default: 'all',
})
// 👇👇👇
export const filteredTodos = selector({
key: 'filteredTodos',
get: ({ get }) => {
const items = get(todos)
const type = get(filterType)
switch (type) {
case 'do':
return items.filter(todo => !todo.done)
case 'done':
return items.filter(todo => todo.done)
default:
return items
}
}
})
우선 selector
를 import
했습니다. 그리고 이를 이용해 atom
과 비슷하게 filteredTodos
상태를 정의했습니다.
여기서 atom
과는 다르게, key
다음에 get
을 정의하였습니다. 즉 getter
를 정의했습니다.
Vue를 해보신분들이라면 selector
의 개념을 아주 쉽게 이해할 수 있는데, selector
는 Vue의 computed
라고 생각하시면 됩니다. selector
는 atom
과 같이 상태를 표현하지만, 어떤 다른 상태에 의존하고 있습니다. 이 의존관계는 ({ get })
으로 전달받은 get
함수로 연결됩니다. 즉, 다른 상태를 구독합니다.
해당 상태가 변경될 때마다 selector
는 동작합니다. 해당 상태가 변경되지 않으면 이전의 값을 그대로 리턴합니다. 즉, 캐시가 됩니다. Vue의 computed가 바로 정확하게 이런식으로 동작하지요.
filteredTodos
는 todos
와 filterType
에 의존하고 있습니다. 둘 중 어느 하나의 값이 변경되면 selector
는 다시 동작하여 계산된 값을 리턴할겁니다.
이제 filterType
과 filteredTodos
를 사용하면 됩니다. 우선은 filter 상태를 변경하는 Footer
컴포넌트를 먼저 가보겠습니다.
useRecoilState
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
기존에 사용했던 useRecoilValue
의 경우, 리턴되는 값은 하나였습니다. 말 그대로 Value
만을 사용하기 때문에 read-only
훅 함수를 사용했던 것이죠.
하지만 상태는 변화합니다. useRecoilState
는 상태와 그 상태를 변화시키는 함수까지 리턴합니다. react hook의 useState
와 똑같습니다.
지금까지의 결과는 아래와 같습니다.
상태를 바꿔도 아이템의 목록이 바뀌지는 않습니다. 왜냐하면, 현재 Main
컴포넌트에서는 filteredTodos
가 아닌 todos
를 뿌려주고 있기 때문입니다. 이제 todos
를 filterTodos
로 바꿔줍니다.
// src/components/Main/index.js
import { useRecoilValue } from 'recoil'
import * as state from '../../state/todos'
import Todo from './Todo'
function Main() {
// 👇👇👇
const todos = useRecoilValue(state.filteredTodos)
const Todos = todos.map(todo => <Todo key={todo.id} todo={todo} />)
return (
<section className="main">
<input id="toggle-all" className="toggle-all" type="checkbox" />
<label htmlFor="toggle-all">Mark all as complete</label>
<ul className="todo-list">
{ Todos }
</ul>
</section>
)
}
export default Main
기존 state.todos
를 state.filteredTodos
로 변경만 해주었습니다. 결과는 아래와 같습니다.
현재 아이템들은 모두 done: false
상태이기 때문에, All과 Active에서는 보이지만 Completed에서는 보이지 않습니다. 제대로 구현된 것 같습니다!
이제 현재 아이템의 완료 상태를 바꾸는 구현을 마지막으로 마무리하겠습니다.
useSetRecoilState
현재 아이템의 상태는 todos
state가 갖고 있습니다. 그리고, 이 상태를 바꾸는 동작은 Todo
컴포넌트에서 이루어집니다. 다음과 같은 로직을 구현하면 됩니다.
상태변경 감지는 change
이벤트를 통해서 할 수 있고, 변경된 상태를 적용하는 것은 todos
의 상태를 변화시키는 것이므로 useRecoilState(todos)
를 이용하여 할 수 있습니다.
하지만 Todo
컴포넌트는 props
를 통해 아이템의 정보를 받습니다. 따라서, useRecoilState
로 todos
의 값을 가져올(getter) 필요는 없습니다. 새로 설정(setter)만 하면 됩니다. 이를 위해 useSetRecoilState
를 사용하겠습니다.
// src/components/Main/Todo.js
// 👇👇👇
import { useSetRecoilState } from 'recoil'
import { todos } from '../../state/todos'
function Todo(props) {
const { id, done, text } = props.todo
// 👇👇👇 #1
const setTodos = useSetRecoilState(todos)
// 👇👇👇 #2
const toggleTodo = checked => {
setTodos(todos => todos.map(todo => {
return todo.id === id
? { ...todo, done: checked }
: todo
}))
}
// 👇👇👇 #3
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
하나하나 설명해보겠습니다.
#1: useSetRecoilState
로 todos
를 변경시킬 수 있는 setter를 얻어옵니다.
#2: 기존에 filterType
을 useRecoilState
로 가져왔을 때, 상태를 바꿀때 단순히 setFilterType('all')
과 같이 매개변수를 함수가 아닌 값으로 호출했었습니다. 이렇게 함수가 아닌 값으로 호출하면 그 값으로 적용이 되어버립니다. 이와 다르게 콜백으로 넘겨주면, 콜백의 첫번째 매개변수로 현재 state의 값을 받아올 수 있습니다. 그리고 이 콜백이 리턴하는 값이 새로운 state가 됩니다.
우리는 많은 todo 아이템 중 특정 아이템의 상태를 바꿔야 합니다. 그 특정 아이템은 todos
어딘가에 들어있습니다. 그러면 다른 todo들은 건들이지 않고, 특정 todo의 done
값만 바꾸기 위해서는 특정 todo를 식별할 값이 필요한데, 우린 각 todo가 고유하게 갖고 있는 id
값이 있으므로 이를 이용하여 바꿔주면 됩니다.
todos.map(todo => {
return todo.id === id
? { ...todo, done: checked }
: todo
})
현재 state의 todos
를 map
으로 새로운 todos
를 만듭니다. 이 때, id
값을 이용해 타겟 todo인 경우 done
을 체크된 값으로 바꾸고, 그 이외에는 그대로 값을 사용하도록 합니다.
toggleTodo
함수는 현재 컴포넌트의 체크박스 상태인 checked
를 매개변수로 받고, 이를 setter
인 setTodos
를 이용하여 todos
의 state를 변경하는 역할을 합니다.
#3: 체크박스의 이벤트 핸들러입니다.
그러면 이제 상태가 변경되는 모습을 볼 수 있습니다.
상태에 따라 필터링 역시 적용됩니다.
오늘은 selector
와 useRecoilState
, useSetRecoilState
에 대해 알아보았습니다. 간단히 정리해보자면
selector
: 다른 state들을 구독하는 state. 원래 값에서 변경된 값들을 보여줄 때 사용useRecoilState
: useState
와 같이 value와 setter를 얻을 수 있는 훅 함수useSetRecoilState
: setter만을 얻을 수 있는 훅 함수위와 같이 정리할 수 있겠습니다.
다음 시간에는 추가, 삭제, Clear Completed를 구현해보도록 하겠습니다.