Recoil

Recoil이란?

Facebook에서 만든 React 상태 관리 라이브러리 입니다. Recoil은 자신의 장점을 다음과 같이 설명하고 있습니다.

  • 리코일은 리액트처럼 일하고 생각한다. 앱에 일부를 추가하고 빠르고 유연한 공유 상태를 유지해라.
  • 파생 데이터와 비동기 쿼리는 순수한 기능과 효율적인 구독에 길들여진다.
  • 코드 분할을 손상시키지 않고 앱 전체에서 모든 상태 변경을 관찰하여 지속성, 라우팅, 시간 이동 디버깅 또는 실행 취소를 구현해라.

그렇다고 합니다.

등장 배경

복잡한 react 앱에서는 상태 관리가 개발자의 주요하고 어려운 작업이 됩니다. 하나의 상태 변경으로 인해 다른 컴포넌트들에 영향을 주기도 하기 때문인데, 그래서 Redux나 MobX같은 상태 관리 라이브러리들이 등장하게 되었습니다.
Redux나 MobX의 API는 단순하지 않고, 본 목적이 React에서 사용하기 위해 나온것이 아닙니다. 그래서 API와 동작 방식을 가능한 리액트스럽게 만들기 위해 리코일을 제작했다고 합니다.

적용기

일단 CRA로 앱 하나를 만들어 줍니다.

npx create-react-app recoil-practice

그리고 리코일을 인스톨 합니다.

cd recoil-practice
npm i recoil

or

yarn add recoil

RecoilRoot

프로젝트의 루트 컴포넌트에 RecoilRoot를 추가합니다. 리코일 루트를 설정해주어야 내부에서 리코일 사용이 가능합니다. 타 상태관리의 provider와 비슷한 역할을 하는 것 같습니다.
프로젝트의 index.js에 추가해줍니다.

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import * as serviceWorker from './serviceWorker'
import { RecoilRoot } from 'recoil'

ReactDOM.render(
  <React.StrictMode>
    <RecoilRoot>
      <App />
    </RecoilRoot>
  </React.StrictMode>,
  document.getElementById('root')
)

핵심 개념

Recoil은 Atom으로부터 시작해서 Selector를 거쳐 React 컴포넌트까지 전달되는 하나의 Data-Flow Graph를 만들게 한다. Atom은 컴포넌트들이 구독할 수 있는 단위이고, Selector는 동기적 혹은 비동기적으로 상태를 변환한다. 이 두가지의 핵심 개념으로 이루어진 라이브러리이다.

구성 요소들

  • atom 아톰은 상태의 조각을 나타내며 key와 value를 설정해준다. 기본 상태이다.

    import { atom } from 'recoil'
    
    const number = atom({
    key: 'number',
    default: 0,
    })

    배열, 객체를 값으로 사용할 수 있다.

    import { atom } from 'recoil'
    
    const todos = atom({
    key: 'todos',
    default: ['화장실 가기', '밥먹기'],
    })
  • Selector 셀렉터는 reducer와 비슷하다. 셀렉터에서 atom의 값을 가져오고, switch문 등으로 매개변수의 해당 부분을 찾아, 값을 변형시켜 반환할 수 있다. 다른 atom 또는 selector를 받아 사용하는 순수 함수. 작성할 때 함수에 고유한 키와 게터(get), 세터(set)을 전달하여 작성한다. 게터는 필수지만 세터는 필수가 아니다.
  • useRecoilState useState와 흡사하게, [value, setValue]형식으로 setValue로 값을 변경할 수 있다.
  • useRecoilValue: 오직 recoil의 값만 가져오고 싶을 때 사용한다.

todoList 컴포넌트

공식문서에 있는 투두리스트 예제를 따라해보며 적용하기 위해 todoList컴포넌트를 만듭니다.

import React from 'react'
import { atom, useRecoilValue } from 'recoil'

const todoListState = atom({
  key: 'todoListState',
  default: [],
})

function TodoList() {
  const todoList = useRecoilValue(todoListState)

  return (
    <>
      {/* <TodoListStats /> */}
      {/* <TodoListFilters /> */}
      <TodoItemCreater />
      {todoList.map(todoItem => (
        <TodoItem key={todoItem.id} item={todoItem} />
      ))}
    </>
  )
}

export default TodoList

TodoItemCreator

iimport React, { useState } from 'react';
import { useSetRecoilState } from 'recoil';
import { todoListState } from '../recoilState';

function TodoItemCreator() {
  const [inputValue, setInputValue] = useState('');
  const setTodoList = useSetRecoilState(todoListState);

  const additem = () => {
    setTodoList(oldTodoList => [
      ...oldTodoList,
      {
        id: getId(),
        text: inputValue,
        isComplete: false
      }
    ]);
    setInputValue('');
  };

  const onChange = ({ target: { value } }) => {
    setInputValue(value);
  };

  return (
    <div>
      <input type="text" value={inputValue} onChange={onChange} />
      <button onClick={additem}>Add</button>
    </div>
  );
}

export default TodoItemCreator;

let id = 0;
function getId() {
  return id++;
}

useSetRecoilState훅을 사용해서 아톰으로 정의한 상태를 인자로 넣어주면 세터 함수를 가져옵니다. 세터 함수를 가져온 뒤, 인풋 상태관리를 내부상태로 관리하고 투두리스트 내용 추가 함수에 사용했습니다.

TodoItem

function TodoItem({ item }) {
  const [todoList, setTodoList] = useRecoilState(todoListState)
  const index = todoList.findIndex(listItem => listItem === item)

  const editItemText = ({ target: { value } }) => {
    const newList = replaceItemAtIndex(todoList, index, {
      ...item,
      text: value,
    })

    setTodoList(newList)
  }

  const toggleItemCompletion = () => {
    const newList = replaceItemAtIndex(todoList, index, {
      ...item,
      isComplete: !item.isComplete,
    })

    setTodoList(newList)
  }

  const deleteItem = () => {
    const newList = removeItemAtIndex(todoList, index)

    setTodoList(newList)
  }

  return (
    <div>
      <input type="text" value={item.text} onChange={editItemText} />
      <input
        type="checkbox"
        checked={item.isComplete}
        onChange={toggleItemCompletion}
      />
      <button onClick={deleteItem}>X</button>
    </div>
  )
}

export default TodoItem

function replaceItemAtIndex(arr, index, newValue) {
  return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)]
}

function removeItemAtIndex(arr, index) {
  return [...arr.slice(0, index), ...arr.slice(index + 1)]
}

TodoItem 컴포넌트에서는 useRecoilState훅을 일반 useState훅처럼 사용해 상태값과 세터함수를 가져왔습니다. 일반 투두리스트 예제 내용과 비슷한데, delete, edit방식을 편하게 ReplaceItemAtIndex와 같이 함수를 따로 빼어서 사용합니다.

export const todoListFilterState = atom({
  key: 'todoListFilterState',
  default: 'Show All',
})

목록을 필터링할 상태를 관리하기 위해 todoListFilterState를 추가합니다.

export const filteredTodoListState = selector({
  key: 'filteredTodoListState',
  get: ({ get }) => {
    const filter = get(todoListFilterState)
    const list = get(todoListState)

    switch (filter) {
      case 'Show Completed':
        return list.filter(item => item.isComplete)
      case 'Show Uncompleted':
        return list.filter(item => !item.isComplete)
      default:
        return list
    }
  },
})

filteredTodoListState라는 함수에 selector를 사용하여 키와 게터 함수를 전달하여 작성하였습니다.
todoListFilterState와 todoListState 두 가지 상태를 사용하여 가공된 데이터를 반환하는것 이 목적입니다. 가공된 데이터를 받거나 데이터를 가공하여 저장하고 싶을때 사용합니다.
해당 셀렉터에서 todoListFilterState와 todoListState를 트래킹하여, 변화가 감지되면 다시 실행됩니다.

function TodoList() {
  const todoList = useRecoilValue(filteredTodoListState)

  return (
    <>
      <TodoListStats />
      <TodoListFilters />
      <TodoItemCreator />
      {todoList.map(todoItem => (
        <TodoItem key={todoItem.id} item={todoItem} />
      ))}
    </>
  )
}

export default TodoList

투두 리스트 컴포넌트의 useRecoilValue에서 todolistState상태에서 가져오던 값을 filteredTodoListState상태에서 가져오도록 바꿔줍니다.

TodoListFilters

import React from 'react'
import { useRecoilState } from 'recoil'
import { todoListFilterState } from '../recoilState'

function TodoListFilters() {
  const [filter, setFilter] = useRecoilState(todoListFilterState)

  const updateFilter = ({ target: { value } }) => {
    setFilter(value)
  }

  return (
    <>
      Filter:
      <select value={filter} onChange={updateFilter}>
        <option value="Show All">All</option>
        <option value="Show Completed">Completed</option>
        <option value="Show Uncompleted">Uncompleted</option>
      </select>
    </>
  )
}

export default TodoListFilters

필터링을 선택할 컴포넌트를 넣어줍니다. 셀렉트가 선택되면 해당 값으로 todoListFilterState의 상태를 바꿉니다.

export const todoListStatsState = selector({
  key: 'todoListStatsState',
  get: ({ get }) => {
    const todoList = get(todoListState)
    const totalNum = todoList.length
    const totalCompletedNum = todoList.filter(item => item.isComplete).length
    const totalUncompletedNum = totalNum - totalCompletedNum
    const percentCompleted = totalNum === 0 ? 0 : totalCompletedNum / totalNum

    return {
      totalNum,
      totalCompletedNum,
      totalUncompletedNum,
      percentCompleted,
    }
  },
})

리스트의 토탈 수, 퍼센티지를 표현하기위해 todoListState의 상태를 가공해 반환하는 셀렉터 하나를 작성해줍니다.

TodoListStats

import React from 'react'
import { useRecoilValue } from 'recoil'
import { todoListStatsState } from '../recoilState'

function TodoListStats() {
  const {
    totalNum,
    totalCompletedNum,
    totalUncompletedNum,
    percentCompleted,
  } = useRecoilValue(todoListStatsState)

  const formattedPercentCompleted = Math.round(percentCompleted * 100)

  return (
    <ul>
      <li>Total items: {totalNum}</li>
      <li>Items completed: {totalCompletedNum}</li>
      <li>Items not completed: {totalUncompletedNum}</li>
      <li>Percent completed: {formattedPercentCompleted}</li>
    </ul>
  )
}

export default TodoListStats

위에서 정의한 스탯 상태를 가져와서 사용하기 위해 TodoListStats 컴포넌트를 만듭니다. 여기에서도 useRecoilValue를 사용해 리코일 삳태의 값을 가져왔습니다.

이제 npm run start를 해서 실행시켜 보면, 각 투두리스트의 합계가 나오고, 필터링이 있으며 할밀목록들의 내용이 잘 나오는것을 확인할 수 있습니다.
기본적인 할밀목록 예제를 따라해보며 어느 정도 Recoil의 기본적인 사용방법에 대해선 알겠으나, 특히 어떤점에서 좋은지는 아직 잘 모르겠습니다. 일단 사용법이 리덕스에 비해 간편하고 쉬운 것 같으며 훅처럼 사용하기에 익숙한 점도 있습니다. 이것만으로도 큰 장점이 되겠지만… 다음 비동기 데이터 다루기도 한번 진행해보겠습니다.


Written by@jaeyoung-son
배운것을 기록하는 공간입니다.

GitHub