[#3] react-recoil-todo

undefcat·2021년 4월 28일
1

react-recoil-todo

목록 보기
3/5
post-thumbnail

react-recoil-todo

이번 시간에는 todos를 필터링하는 기능을 만들어 보도록 하겠습니다. 이를 위해 recoilselectoruseRecoilState, useSetRecoilState도 한 번 사용해보도록 하겠습니다.

selector

todos를 3가지 상태에 따라 보여줄 수 있습니다.

  1. 모든 아이템 보여주기
  2. 해야될 일 보여주기(done: false)
  3. 끝난 일 보여주기(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이고, dodone의 상태가 있다고 가정할겁니다. 이제, 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
    }
  }
})

우선 selectorimport했습니다. 그리고 이를 이용해 atom과 비슷하게 filteredTodos 상태를 정의했습니다.

여기서 atom과는 다르게, key 다음에 get을 정의하였습니다. 즉 getter를 정의했습니다.

Vue를 해보신분들이라면 selector의 개념을 아주 쉽게 이해할 수 있는데, selector는 Vue의 computed라고 생각하시면 됩니다. selectoratom과 같이 상태를 표현하지만, 어떤 다른 상태에 의존하고 있습니다. 이 의존관계는 ({ get })으로 전달받은 get 함수로 연결됩니다. 즉, 다른 상태를 구독합니다.

해당 상태가 변경될 때마다 selector는 동작합니다. 해당 상태가 변경되지 않으면 이전의 값을 그대로 리턴합니다. 즉, 캐시가 됩니다. Vue의 computed가 바로 정확하게 이런식으로 동작하지요.

filteredTodostodosfilterType에 의존하고 있습니다. 둘 중 어느 하나의 값이 변경되면 selector는 다시 동작하여 계산된 값을 리턴할겁니다.

이제 filterTypefilteredTodos를 사용하면 됩니다. 우선은 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를 뿌려주고 있기 때문입니다. 이제 todosfilterTodos로 바꿔줍니다.

// 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.todosstate.filteredTodos로 변경만 해주었습니다. 결과는 아래와 같습니다.

현재 아이템들은 모두 done: false 상태이기 때문에, All과 Active에서는 보이지만 Completed에서는 보이지 않습니다. 제대로 구현된 것 같습니다!

이제 현재 아이템의 완료 상태를 바꾸는 구현을 마지막으로 마무리하겠습니다.

useSetRecoilState

현재 아이템의 상태는 todos state가 갖고 있습니다. 그리고, 이 상태를 바꾸는 동작은 Todo 컴포넌트에서 이루어집니다. 다음과 같은 로직을 구현하면 됩니다.

  1. 현재 아이템의 상태를 변경을 감지한다.
  2. 변경된 상태를 todos에 적용한다.

상태변경 감지는 change 이벤트를 통해서 할 수 있고, 변경된 상태를 적용하는 것은 todos의 상태를 변화시키는 것이므로 useRecoilState(todos)를 이용하여 할 수 있습니다.

하지만 Todo 컴포넌트는 props를 통해 아이템의 정보를 받습니다. 따라서, useRecoilStatetodos의 값을 가져올(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: useSetRecoilStatetodos를 변경시킬 수 있는 setter를 얻어옵니다.

#2: 기존에 filterTypeuseRecoilState로 가져왔을 때, 상태를 바꿀때 단순히 setFilterType('all')과 같이 매개변수를 함수가 아닌 값으로 호출했었습니다. 이렇게 함수가 아닌 값으로 호출하면 그 값으로 적용이 되어버립니다. 이와 다르게 콜백으로 넘겨주면, 콜백의 첫번째 매개변수로 현재 state의 값을 받아올 수 있습니다. 그리고 이 콜백이 리턴하는 값이 새로운 state가 됩니다.

우리는 많은 todo 아이템 중 특정 아이템의 상태를 바꿔야 합니다. 그 특정 아이템은 todos 어딘가에 들어있습니다. 그러면 다른 todo들은 건들이지 않고, 특정 todo의 done값만 바꾸기 위해서는 특정 todo를 식별할 값이 필요한데, 우린 각 todo가 고유하게 갖고 있는 id값이 있으므로 이를 이용하여 바꿔주면 됩니다.

todos.map(todo => {
  return todo.id === id
  	? { ...todo, done: checked }
  	: todo
})

현재 state의 todosmap으로 새로운 todos를 만듭니다. 이 때, id값을 이용해 타겟 todo인 경우 done을 체크된 값으로 바꾸고, 그 이외에는 그대로 값을 사용하도록 합니다.

toggleTodo 함수는 현재 컴포넌트의 체크박스 상태인 checked를 매개변수로 받고, 이를 settersetTodos를 이용하여 todos의 state를 변경하는 역할을 합니다.

#3: 체크박스의 이벤트 핸들러입니다.

그러면 이제 상태가 변경되는 모습을 볼 수 있습니다.

상태에 따라 필터링 역시 적용됩니다.

정리

오늘은 selectoruseRecoilState, useSetRecoilState에 대해 알아보았습니다. 간단히 정리해보자면

  • selector: 다른 state들을 구독하는 state. 원래 값에서 변경된 값들을 보여줄 때 사용
  • useRecoilState: useState와 같이 value와 setter를 얻을 수 있는 훅 함수
  • useSetRecoilState: setter만을 얻을 수 있는 훅 함수

위와 같이 정리할 수 있겠습니다.

다음 시간에는 추가, 삭제, Clear Completed를 구현해보도록 하겠습니다.

profile
undefined cat

0개의 댓글