[#4] react-recoil-todo

undefcat·2021년 4월 28일
2

react-recoil-todo

목록 보기
4/5
post-thumbnail

react-recoil-todo

이번 시간에는 새로운 todo 추가, 삭제, Clear Completed 기능을 구현해보도록 하겠습니다.

참고로 이번 TodoApp의 경우 atom, selector, useRecoilState 3가지만 있으면 모두 구현할 수 있기 때문에 더 이상 새로운 recoil의 기능이 나오지는 않을 것입니다.

이 TodoApp을 완성한 이후에 약간 실전에 가까운 예제(ex: API 통신하는 예제)를 적당히 생각해보고, 따로 포스팅을 하면서 다른 recoil 기능들도 한 번 탐방 해보도록 하겠습니다.

추가

새로운 todo를 추가하는 기능은 간단합니다. todos state에 새로운 todo를 추가하면 됩니다. 이를 위해 todosuseSetRecoilState로 설정하면 될 것 같습니다. 새로운 아이템을 추가할 수 있는 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, useSetRecoilStateimport하고 구현합니다.

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

Clear Completed

완료된 모든 아이템을 삭제하는 기능입니다. 마찬가지로 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를 사용하도록 합니다. 이렇게 gettersetter를 구분해서 사용해서 얻을 수 있는 이익이 있는데, 위와 같이 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를 따로 알아보진 않았지만, gettersetter 함수를 왜 따로 사용하는지 알아보았습니다.

핵심은 setter 기능만 있는 경우, useSetRecoilState 훅 함수를 사용해야 불필요한 렌더링을 줄일 수 있다는 점입니다.

이제 마무리를 향해 달려가고 있습니다. 현재 남은 기능인 모든 체크 토글, 남은 아이템 갯수 확인, 아이템 내용 수정은 다음 포스팅에서 알아보도록 하겠습니다.

profile
undefined cat

0개의 댓글