안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

리액트 (React)

[React] 전역 상태 관리를 위한 Recoil 핵심 개념 익히기

피그브라더 2022. 2. 3. 21:07

1. Recoil 소개

작지 않은 규모의 웹사이트를 React로 개발해본 사람이라면 전역 상태가 얼마나 중요한지 알고 있을 것이다. 전역 상태가 없다면, 여러 컴포넌트가 특정 상태를 공유해야 할 때 컴포넌트 트리의 props 전달 구조가 매우 복잡해지기 때문이다. 그래서 보통 React로 개발할 때는 Redux나 MobX 등의 전역 상태 관리 라이브러리를 함께 사용하는 경우가 많다.

그러나 React를 배우기 위해서 또 다른 라이브러리도 배워야 한다는 것은 다소 부담스럽다. 해당 라이브러리를 사용하기 위해 추가로 작성해야 하는 보일러 플레이트 코드도 그다지 마음에 들지 않는다. 웬만하면 React가 자체적으로 전역 상태 관리 기능을 제공했으면 좋겠다. 이러한 맥락에서 등장한 React 자체의 기능이 바로 Context API이다. 그러나 Context API도 여전히 한계점이 명확하다. 단일 값만 저장할 수 있기 때문에 여러 값을 관리하려면 코드가 다소 복잡해진다는 것이다.

 

Recoil

A state management library for React.

recoiljs.org

이때 등장한 라이브러리가 바로 Recoil이다. 이는 Facebook이 아예 React 전용으로 사용할 수 있도록 구현한 상태 관리 라이브러리인데, 이 또한 앞서 소개한 것들과 마찬가지로 서드 파티 라이브러리이긴 하지만 완전히 React 의존적으로 구현했기 때문에 React에 익숙한 사람이라면 매우 금방 익힐 수 있다는 특징이 있다.

Hook을 포함한 기본적인 React 개념만 익숙하다면 Recoil은 정말 쉽다(믿어도 된다). Recoil은 상태를 의미하는 Atom과 파생 상태를 의미하는 Selector만 익히면 된다. 고급 개념으로 들어가면 조금 더 공부할 게 생기겠지만, 기본적인 사용 방법은 아래에서 설명하는 내용이 전부이다. 핵심만 정리해보도록 하자.

 

2. 설치 및 사용 준비

Recoil은 다음과 같은 방법으로 설치한다.

# If you use `npm`
npm install recoil

# If you use `yarn`
yarn add recoil

설치가 완료되면, 다음과 같이 컴포넌트 트리의 최상위에 <RecoilRoot> 컴포넌트를 배치하자. 그러면 사용 준비는 끝이다.

import React from 'react';
import { RecoilRoot } from 'recoil';

function App() {
  return (
    <RecoilRoot>
      ...
    </RecoilRoot>
  );
}

 

3. Atom (= State)

Atom은 여러 컴포넌트가 공유할 수 있는 상태를 의미한다. 일반적으로 최소한의 상태만이 Atom으로 정의되고, 나머지 상태는 그러한 최소한의 상태로부터 계산하여 얻어지도록 Selector로 정의된다(Selector에 대한 설명은 아래 참조). Atom을 참조하는 컴포넌트는 자동으로 해당 Atom을 구독하게 되고, Atom의 값이 변경되면 이를 구독하는 컴포넌트들도 다시 렌더링 된다.

 

Atom은 다음과 같이 atom() 함수를 통해 정의된다. 일반적으로 별도의 JavaScript 파일에서 정의되고 컴포넌트에서 이를 불러와서 사용한다. key 프로퍼티에는 해당 Atom의 고윳값을 지정하며, default 프로퍼티에는 해당 Atom의 디폴트 값을 지정한다.

import { atom } from 'recoil';

// Atom is defined as follows. (usually written in a separate file)
const todoListState = atom({
  key: 'todoListState',  // Atom's unique key
  default: []  // Atom's default value
});

그리고 그렇게 정의된 Atom은 컴포넌트에서 다음과 같이 Hook을 통해 사용된다.

import { useRecoilValue, useSetRecoilState, useRecoilState } from 'recoil';
import { todoListState } from '...';

// For reading
todoList = useRecoilValue(todoListState);

// For writing
setTodoList = useSetRecoilState(todoListState);

// For reading and writing
[todoList, setTodoList] = useRecoilState(todoListState);

 

4. Selector (= Derived State)

Selector는 여러 컴포넌트가 공유할 수 있는 파생 상태를 의미한다. 일반적으로 다른 Atom이나 Selector의 값으로부터 새로운 값을 계산하는 순수 함수로 정의된다. Selector를 참조하는 컴포넌트는 자동으로 해당 Selector를 구독하게 되고, Selector의 값이 변경되면 이를 구독하는 컴포넌트들도 다시 렌더링 된다. Selector는 자신이 구독하는 Atom이나 Selector의 값이 변경될 때 자신의 값도 다시 계산한다. 이로 인해 연쇄적으로 해당 Selector를 구독하는 컴포넌트들도 다시 렌더링 된다.

 

Selector는 다음과 같이 selector() 함수를 통해 정의된다. 일반적으로 별도의 JavaScript 파일에서 정의되고 컴포넌트에서 이를 불러와서 사용한다. key 프로퍼티에는 해당 Selector의 고윳값을 지정하며, get 프로퍼티에는 해당 Selector의 값을 계산하는 함수를 지정한다. 이때 이 함수의 인자로 전달되는 get() 함수는 해당 Selector의 값을 계산할 때 사용하는 다른 Atom이나 Selector의 값을 참조 및 구독하는 역할을 수행한다.

import { selector } from 'recoil';

// Selector is used as follows. (usually written in a separate file)
const filteredTodoListState = selector({
  // Selector's unique key
  key: 'filteredTodoListState',

  // Selector's logic
  get: ({ get }) => {
    const filter = get(todoListFilterState);  // Subscribe todoListFilterState
    const todoList = get(todoListState);  // Subscribe todoListState
        
    switch (filter) {
      case 'Show Completed':
        return todoList.filter((todoItem) => todoItem.isCompleted);
      case 'Show Uncompleted':
        return todoList.filter((todoItem) => !todoItem.isCompleted);
      default:
        return todoList;
    }
  }
});

그리고 그렇게 정의된 Selector은 컴포넌트에서 다음과 같이 Hook을 통해 사용된다. 이때 Atom에서 사용한 useSetRecoilState(), useRecoilState() Hook을 사용할 수 없음에 주목하자. 이는 Writeable Selector(set 프로퍼티를 가진 객체로 정의되는 Selector)가 아니기 때문이다. (이번 포스팅에서는 해당 내용까지 다루지 않는다.)

import { useRecoilValue } from 'recoil';
import { filteredTodoListState } from '...';

// For reading
filteredTodoList = useRecoilValue(filteredTodoListState);