[#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개의 댓글

관련 채용 정보

Powered by GraphCDN, the GraphQL CDN