Recoil 레시피: 소개

소개, 기본 개념, 몇 가지 API 둘러보기

Recoil은 지난 5월 14일 열린 React Europe 2020 컨퍼런스에서 페이스북이 발표한 상태 관리 라이브러리이다. 비록 실험 단계이긴 하지만 페이스북에서 "공식적으로" 작성한 상태 관리 라이브러리라서 많은 주목을 받고 있다. 공부도 할겸 간단하게 개인 프로젝트에 적용해보았는데 Redux나 MobX 같은 다른 라이브러리보다 이해하고 작성하기가 더 쉽다는 생각이 들었다.

2020년 7월 현재 최신 버전이 0.0.10이고 공식 문서 곳곳에 unstable 경고가 붙어있는 초기 단계라 프로덕션에 사용하기에는 이르지만, 사용 방법이나 기반 개념 등에는 큰 변화가 없을 것으로 예상하므로 현 단계에서 배워둬도 나쁘지 않을 듯 하다.

이 시리즈에서는 Recoil의 기본 개념을 살펴본 후 다른 상태 관리 라이브러리에서 제공하던 기능을 Recoil로 어떻게 대체할 수 있는지 알아보겠다.

들어가기 전에

Recoil은 Context API를 기반으로 구현되었으며 함수형 컴포넌트에서만 사용할 수 있다.

Recoil API 대부분은 훅(Hooks)으로 구현되어 있다. 만약 React의 훅에 익숙하지 않다면 훅부터 먼저 학습한 후 Recoil을 접하는 것이 좋다. 훅을 기반으로 하고 있으므로 클래스 컴포넌트에서는 Recoil을 사용할 수 없다. 다만 React 개발팀에서도 훅을 권장하고 있는 점을 미루어 볼 때, 장차 React는 클래스보다는 함수형 컴포넌트 중심으로 발전할 것으로 보인다.

... In the longer term, we expect Hooks to be the primary way people write React components.

장기적으로는 훅이 React 컴포넌트를 작성하는 주된 방법으로 자리 잡았으면 한다.

React 공식 문서 Hooks FAQ에서

따라서 이 제약은 큰 약점이 될 것 같지는 않다. 필요하다면 상태를 다루는 함수형 컴포넌트와 그렇지 않은 클래스 컴포넌트를 적절히 혼합해서 사용할 수도 있다.

코드 실행

직접 실행하면서 원리를 파악할 수 있도록 예제 코드는 대부분 코드 샌드박스 링크도 제공하고 있다. 일부러 에러 상황을 의도하지 않은 코드는 최소한 구글 크롬 브라우저 환경에서 실행되는 것을 모두 확인했다. 다만 링크를 클릭해서 열었을 때 애플리케이션이 실행되지 않는 경우가 있을 수 있는데, 이럴 때는 브라우저 패널의 리프레시 아이콘을 클릭하면 해결된다.

애플리케이션이 실행되지 않을 땐 리프레시 버튼을 클릭하자

기본 개념

<RecoilRoot />

앞서 말했듯이 Recoil은 내부적으로 ContextAPI를 기반으로 한다. ContextAPI에서 API를 사용할 컴포넌트는 반드시 Context의 공급자(Provider) 안에 있어야 한다. 마찬가지로 Recoil을 사용할 컴포넌트는 반드시 RecoilRoot로 감싸져야 한다. 대체로 애플리케이션 루트에 둘 것이다.

import {RecoilRoot} from 'recoil';

function AppRoot() {
  return (
    <RecoilRoot>
      <ComponentThatUsesRecoil />
    </RecoilRoot>
  );
}

한 애플리케이션에서 여러 개를 사용할 수도 있고 중첩도 가능하다. 이 때 Recoil API는 가장 가까운 조상 RecoilRoot에 접근한다.

Atom

Recoil의 단위 데이터이다. 스토어에 저장되고 추출되는 데이터는 모두 Atom을 기반으로 한다. 아톰은 다음과 같이 atom() 함수에 고유한 키(key)와 기본값(default)을 전달해서 작성한다.

import {atom} from 'recoil';

const counter = atom({
  key: 'counter',
  default: 0
});

물론 배열이나 객체를 값으로 사용하는 아톰도 작성할 수 있다.

import {atom} from 'recoil';

const user = atom({
  key: 'user',
  default: {
    firstName: 'Gildong',
    lastName: 'Hong',
    age: 30
  }
});

const todos = atom({
  key: 'todos',
  default: [
    '글 쓰기',
    '발행하기'
  ]
});

컴포넌트에서는 아톰 객체를 훅에 전달하여 값을 가져오고 설정한다. 다음을 살펴보자.

import {atom, useRecoilState} from 'recoil';

const counter = atom({
  key: 'counter',
  default: 0
});

function Counter() {
  const [count, setCount] = useRecoilState(counter);
  const incrementByOne = () => setCount(count + 1);

  return (
    <div>
      Count: {count}
      <br />
      <button onClick={incrementByOne}>Increment</button>
    </div>
  );
}

값을 가져오고 설정하는 부분은 아톰 객체를 작성하고 전달하는 것만 제외하면 Context API의 useState를 사용하는 방법과 거의 동일하다. 번거롭게 액션과 리듀서를 작성할 필요도 없다.

위 코드는 아래 링크에서 실행할 수 있다.

https://codesandbox.io/s/jovial-franklin-bmem5 (JavaScript)
https://codesandbox.io/s/amazing-sammet-x3duk (TypeScript)

Selector

실제로 스토어에 저장되고 스토어에서 가져오는 데이터는 아톰 기반이지만, 때로는 가공된 데이터를 받거나 데이터를 가공하여 저장하고 싶을 때가 있다. 이 때 사용하는 게 셀렉터(selector)이다. 작성할 때는 selector 함수에 고유한 키(key)와 게터(get)와 세터(set)를 전달하여 작성한다. 게터는 필수지만 세터는 사용하지 않아도 상관없다.

Note

사실 셀렉터는 데이터 가공 외에도 기능의 실행이라는 목적이 있다. 이에 관해서는 다음 글 "비동기 액션"에서 더 다루어 볼 것이다.

위에서 보았던 사용자 정보를 예로 들어보자. 아톰에서는 이름과 성을 분리해서 저장했지만 성과 이름을 합한 문자열을 자주 사용하게 될 수 있다. 이 때는 다음과 같이 성과 이름을 연결해서 반환하는 셀렉터를 만들어 사용하면 편리할 것이다.

import {atom, selector, useRecoilState} from 'recoil';

const userState = atom({
  key: 'user',
  default: {
    firstName: 'Gildong',
    lastName: 'Hong',
    age: 30
  }
});

const userNameSelector = selector({
  key: 'userName',
  get: ({get}) => {
    const user = get(userState);
    return user.firstName +  ' ' + user.lastName;
  },
  set: ({set}, name) => {
    const names = name.split(' ');
    set(
      userState,
      (prevState) => ({
        ...prevState,
        firstName: names[0],
        lastName: names[1] || ''
      })
    );
  }
});

function User() {
  const [userName, setUserName] = useRecoilState(userNameSelector);
  const inputHandler = (event) => setUserName(event.target.value);

  return (
    <div>
      Full name: {userName}
      <br />
      <input type="text" onInput={inputHandler} />
    </div>
  );
}

마찬가지로 위 코드는 Code Sandbox에서 직접 실행해 볼 수 있다.

게터 내에 있는 get 함수에 전달된 아톰과 셀렉터는 자동으로 의존성으로 인식되고, 해당 셀렉터는 의존성으로 연결된 값이 변경될 때만 값을 갱신한다. 예를 들어, 위 코드에서 userNameSelector의 값은 userState 아톰의 값이 변경될 때와 컴포넌트가 마운트 될 때 값이 업데이트 된다.

API 세 가지

상태를 설정하지 않고 값만 가져오고 싶을 때는 useRecoilState 대신 useRecoilValue를 사용할 수 있다.

import {atom, useRecoilValue} from 'recoil';

const counter = atom({
  key: 'counter',
  default: 0
});

function Counter() {
  const count = useRecoilValue(counter);

  return (
    <div>
      Count: {count}
    </div>
  );
}

혹은 값을 불러오지 않고 상태를 설정하는 함수만 필요할 때는 useSetRecoilValue를 사용할 수 있다.

import {atom, useSetRecoilValue} from 'recoil';

const counter = atom({
  key: 'counter',
  default: 0
});

function Counter() {
  const setCount = useSetRecoilValue(counter);
  const incrementByOne = () => setCount(count + 1);

  return (
    <div>
      <button onClick={incrementByOne}>Increment</button>
    </div>
  );
}

useResetRecoilValue을 사용하면 아톰이 정의하는 상태를 초기값으로 재설정하는 리셋 함수를 구할 수 있다.

function Counter() {
  const resetCount = useResetRecoilValue(counter);
  const reset = () => resetCount();

  return (
    <div>
      <button onClick={reset}>Reset</button>
    </div>
  ); 
}

다음 글

지금까지는 기본적인 개념과 사용법만 살펴보았다. 아직은 다른 상태 관리 라이브러리에 비해 Recoil의 장점이 무엇인지 찾기 어려웠으리라 본다. 다음 글에서는 웹 애플리케이션을 개발하면서 많이 필요해지는 비동기 처리에 대해 알아볼텐데 이 시점부터 Recoil의 간결함이 빛난다고 생각한다.

  1. Recoil 공식문서 끙끙거리면서 보고있었는데, 빛과 같은 글이 여기에.....

  2. 꼼꼼한 게시글 정말 감사합니다. 덕분에 recoil 과 selector개념이 이해되었네요 .

  3. 혹시 하버드 리코일학과 나오셨나요? 이해하기 쉬운 글 정말 감사합니다

    1. 나름 쉽게 풀어쓰려고 쓴 글인데 잘 봐주셨다니 기쁘네요.
      칭찬에 사용하신 표현도 재밌습니다. 😉

  4. 오우 이해하기 쉽네요
    그나저너 리코일을 이용한 패턴(프로젝트 폴더 구조)를 찾고 있는데 아직 레퍼런스가 없네요 ㅠ 알아서 써야할듯

Leave a Reply to taggonCancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.