Recoil 알아보기

최형용·2023년 3월 14일
0
post-thumbnail

Recoil이란?

Recoil는 React를 위한 상태 관리 라이브러리다.Recoil을 사용하면 atoms (공유 상태)에서 selectors (순수 함수)를 거쳐 React 컴포넌트로 내려가는 data-flow graph를 만들 수 있다.

React의 한계
컴포넌트의 상태는 공통된 상위요소까지 끌어올려야만 공유될 수 있으며, 이 과정에서 거대한 트리가 다시 렌더링되는 효과를 야기하기도 한다.
Context는 단일 값만 저장할 수 있으며, 자체 소비자(consumer)를 가지는 여러 값들의 집합을 담을 수는 없다.
이 두 가지 특성이 트리의 최상단(state가 존재하는 곳)부터 트리의 말단(state가 사용되는 곳)까지의 코드 분할을 어렵게 한다.

Recoil 특징

  • 공유상태(shared state)도 React의 내부상태(local state)처럼 간단한 get/set 인터페이스로 사용할 수 있도록 boilerplate-free API를 제공한다. (필요한 경우 reducers 등으로 캡슐화할 수도 있다.)
  • 동시성 모드(Concurrent Mode)를 비롯한 다른 새로운 React의 기능들과의 호환 가능성도 갖는다.
  • 상태 정의는 점진적이고(incremental) 분산되어 있기 때문에, 코드 분할이 가능하다.
  • 상태를 사용하는 컴포넌트를 수정하지 않고도 상태를 파생된 데이터로 대체할 수 있다.
  • 파생된 데이터를 사용하는 컴포넌트를 수정하지 않고도 파생된 데이터는 동기식과 비동기식 간에 이동할 수 있다.
  • 전체 애플리케이션 상태를 하위 호환되는 방식으로 유지하기가 쉬우므로, 유지된 상태는 애플리케이션 변경에도 살아남을 수 있다.

Atoms

Atoms는 상태의 단위이며, 업데이트와 구독이 가능하다. atom이 업데이트되면 각각의 구독된 컴포넌트는 새로운 값을 반영하여 다시 렌더링 된다. atoms는 런타임에서 생성될 수도 있다. Atoms는 React의 로컬 컴포넌트의 상태 대신 사용할 수 있다. 동일한 atom이 여러 컴포넌트에서 사용되는 경우 모든 컴포넌트는 상태를 공유한다.
컴포넌트에서 atom을 읽고 쓰려면 useRecoilState라는 훅을 사용한다. React의 useState와 비슷하지만 상태가 컴포넌트 간에 공유될 수 있다는 차이가 있다.

const fontSizeState = atom({
  key: 'fontSizeState',
  default: 14,
});

function FontButton() {
  const [fontSize, setFontSize] = useRecoilState(fontSizeState);
  return (
    <button onClick={() => setFontSize((size) => size + 1)} style={{fontSize}}>
      Click to Enlarge
    </button>
  );
}

Selectors

Selector는 atoms나 다른 selectors를 입력으로 받아들이는 순수 함수(pure function)다. 상위의 atoms 또는 selectors가 업데이트되면 하위의 selector 함수도 다시 실행된다. 컴포넌트들은 selectors를 atoms처럼 구독할 수 있으며 selectors가 변경되면 컴포넌트들도 다시 렌더링된다.
Selectors는 상태를 기반으로 하는 파생 데이터를 계산하는 데 사용된다. 최소한의 상태 집합만 atoms에 저장하고 다른 모든 파생되는 데이터는 selectors에 명시한 함수를 통해 효율적으로 계산함으로써 쓸모없는 상태의 보존을 방지한다.
Selectors는 어떤 컴포넌트가 자신을 필요로하는지, 또 자신은 어떤 상태에 의존하는지를 추적하기 때문에 이러한 함수적인 접근방식을 매우 효율적으로 만든다.
Selectors는 useRecoilValue()를 사용해 읽을 수 있다. useRecoilValue()는 하나의 atom이나 selector를 인자로 받아 대응하는 값을 반환한다.

const fontSizeLabelState = selector({
  key: 'fontSizeLabelState',
  get: ({get}) => {
    const fontSize = get(fontSizeState);
    const unit = 'px';

    return `${fontSize}${unit}`;
  },
});

function FontButton() {
  const [fontSize, setFontSize] = useRecoilState(fontSizeState);
  const fontSizeLabel = useRecoilValue(fontSizeLabelState);

  return (
    <>
      <div>Current font size: ${fontSizeLabel}</div>

      <button onClick={setFontSize(fontSize + 1)} style={{fontSize}}>
        Click to Enlarge
      </button>
    </>
  );
}

공식문서로 todolist 만들어보기

파일구조

todolist

//TodoStore.js
import { atom, selector } from "recoil";

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

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

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;
      }
    },
  });

  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,
      };
    },
  });
//TodoList
import React from 'react';
import { useRecoilValue } from 'recoil';
import { TodoItem } from './TodoItem';
import { TodoListCreator } from './TodoListCreator';
import { TodoListFilters } from './TodoListFilter';
import { TodoListState } from './TodoListState';
import { filteredTodoListState } from './TodoStore';

const TodoList = () => {
    const todoList = useRecoilValue(filteredTodoListState);

    return (
        <div>
            <TodoListState/>
            <TodoListFilters/>
            <TodoListCreator/>
            {todoList.map((item)=>(
                <TodoItem key={item.id} item={item} />
            ))}
        </div>
    );
};

export default TodoList;
//TodoListState
import React from 'react';
import { useRecoilValue } from 'recoil';
import { todoListStatsState } from './TodoStore';

export const TodoListState = () => {
    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>
      );
};
// TodoListFilters
import React from 'react';
import { useRecoilState } from 'recoil';
import { todoListFilterState } from './TodoStore';

export const 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>
      </>
    );
  }
import React, { useState } from 'react';
import { useSetRecoilState } from 'recoil';
import { todoListState } from './TodoStore';


export const TodoListCreator = ()  =>{
    const [inputValue, setInputValue] = useState('');
    const setTodoList = useSetRecoilState(todoListState);
  
    const addItem = () => {
      setTodoList((TodoList) => [
        ...TodoList,
        {
          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>
    );
  }
  
  // 고유한 Id 생성을 위한 유틸리티
  let id = 0;
  function getId() {
    return id++;
  }
//TodoItem
import React from 'react';
import { useRecoilState } from 'recoil';
import { todoListState } from './TodoStore';

export const 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>
    );
};

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)];
  }


출처

https://recoiljs.org/ko/

profile
꾸준한 개발자가 되고 싶습니다

0개의 댓글