woolta

react의 새로운 상태관리 라이브러리 recoil 에 대해 알아보기 - atom, selector

wooltaUserImgb00032 | React | 2021-01-03

recoil - 새로운 react 상태관리 라이브러리

recoil은 2020년 5월 React EU 컨퍼런스에서 발표된 React 의 새로운 전역상태 관리 라이브러리 이며 다음과 같은 큰 장점이 존재합니다.

react 자체 라이브러리

recoil 은 기존의 redux, mobx 와 달리 react를 지원하는 전용 상태관리 이기 때문에 react 내부에 대한 접근이 가능하여 React의 동시성 모드, Suspense 등을 손쉽게 지원 가능 하다는 장점이 존재합니다. 또한 러닝커브가 상당히 적고 hook 을 사용하는 방식이 익숙한 react 개발자들에게 훨씬 편하게 다가올 수 있습니다. :)

현저히 적은 코드양과 보일러 플레이트

기존의 redux를 쓸때 항상 느낀 단점으로는 간단한 상태 한개만 처리하려고 해도 수많은 보일러 플레이트 코드가 필요했으나 recoil은 이런 부분에서 훨씬 적은 코드로 작업이 가능하다는 장점이 존재합니다.

현재 글 작성 시점(21. 01. 03) 을 기준으로 아직 0.1.2 버전이라 실제 프로덕트 의 도입은 무리이나 추후 react 쪽에서 떠오르는 상태관리 라이브러리가 될것이라 생각합니다. 이번에는 atom, selector 에 대해 알아보도록 하겠습니다.

본 글의 코드 예시는 해당 github 주소에서 받아 보실 수 있습니다.

https://github.com/sunginHwang/recoil-todo-practice

recoil 사용을 위한 세팅

recoil을 사용하기 위해선 redux 에서 provider를 통해 store을 세팅하는것처럼 프로젝트 최상단 위치에 recoilRoot 를 다음과 같이 설정해 주도록 하겠습니다. (본 예시는 cra를 기반으로 작성되었습니다.) 아래 코드를 보면 기존 redux 에서는 store 설정을 해주는 부분이 있었으나 (reducer등등 정의) 훨씬 간결하게 설정할 수 있게 변경되었습니다.

recoil 사용을 위한 세팅

import React from 'react';
import ReactDOM from 'react-dom';
import { RecoilRoot } from 'recoil'; // recoil 추가
import './index.css';
import App from './App';

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

atom

atom 은 하나의 상태 를 의미 합니다. 이는 React 에서 흔히 볼수 있는 State 와 같은 개념으로 atom 의 값을 변경시 해당 atom 을 구독하고 있는 모든 컴포넌트들이 리렌더링 되며 해당 변경된 atom의 값을 사용하게 됩니다. redux 에서는 reducer 단위로 state 를 구성하였으나 atom 은 이런 reducer 단위가 아닌 더 잘게 쪼개진 state 단위로 상태로 관리할 수 있게 됩니다.
atom 을 생성하기 위해서는 다음과 같은 2가지의 값을 필수로 설정 해주어야 합니다.

  • key : 고유한 key 값 (보통 해당 atom을 생성하는 변수 명으로 지정합니다.)
  • default : atom 의 초기값을 정의합니다. 정적인 값(int, string...), promise, 다른 atom 의 값으로 설정할 수 있습니다.

위의 필수값을 사용하여 다음과 같이 atom 을 생성할 수 있습니다. 이때 기본값에 promise 설정시 정적인 값 혹은 동일한 유형의 값을 반환하는 Promise 는 설정이 가능하지만 외부에서 데이터를 불러오는 작업의 비동기 통신정보는 사용할 수 없습니다. 이런 비동기 통신 데이터는 다음에 설명할 selector 를 통해 사용하면 됩니다.

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

1. atom, selector 를 사용하기 위해 지원하는 hook 함수

atom을 생성한 뒤 component 에서 해당 atom 을 사용할땐 useState 를 통해 hook 방식으로 사용하는것처럼 Recoil 에서 제공하는 hook 함수를 통해 사용할 수 있습니다. 아래와 같이 각종 조건에 대응할수 있게 여러 케이스로 사용하도록 여러 함수들을 지원하고 있습니다. 또한 해당 hook 함수들은 아래에서 설명할 selector 에서도 사용하는 함수들 입니다.

  • useRecoilState : 기존 useState 와 같이 변경되는 값과 해당 값을 변경하는 함수를 반환합니다.
  • useRecoilValue : 구독하는 값만 반환하는 함수입니다. 값의 update 함수가 필요없을 경우 사용합니다.
  • useSetRecoilState : 구독하는 값을 변경하는 함수만 반환합니다.
  • useResetRecoilState: 값을 기본값으로 reset 시키는 함수를 반환합니다.

2. atom 사용 예시 (count 예시 만들기)

atom 을 사용하는 예시로 count 를 변경하는 예시를 만들어 보도록 하겠습니다. 우선 recoil 작성을 위해 다음과 같이 count.js 파일을 생성해 count상태를 관리하는 atom 을 생성해 주도록 하겠습니다.

recoil count Atom 세팅

//count.js recoil 세팅
import { atom } from 'recoil';


export const countState = atom({
    key: 'countState', // 해당 atom의 고유 key
    default: 0, // 기본값 
});

그다음 아래와 같이 2개의 컴포넌트를 만들어서 실행해 보도록 하겠습니다.

count 를 보여주고 변경하는 컴포넌트 작용

// 읽기 및 쓰기 컴포넌트
import { useRecoilState, useSetRecoilState, useResetRecoilState } from 'recoil';
import { countState } from '../../recoil/count';

function ReadWriteCount() {
    const [ count, setCount ] = useRecoilState(countState); // useRecoilState 을 통한 value, setter 반환
    const setCountUseSetRecoilState = useSetRecoilState(countState); // 값을 변경하는 함수만 반환
    const resetCount = useResetRecoilState(countState); // 설정된 기본값으로 리셋
    return (
        <div>
            <h2>읽기 쓰기 카운트 컴포넌트</h2>
            <p>카운트 {count}</p>
            <button onClick={() => setCount(count + 1)}>숫자 증가</button>
            <button onClick={() => setCount(count - 1)}>숫자 감소</button>
            <button onClick={() => setCountUseSetRecoilState(count + 1)}>숫자 증가 (useSetRecoilState 사용)</button>
            <button onClick={() => setCountUseSetRecoilState(count - 1)}>숫자 감소 (useSetRecoilState 사용)</button>
            <button onClick={resetCount}>카운트 리셋</button>
        </div>
    );
}

export default ReadWriteCount;

count를 보여주기만 하는 컴포넌트 작성

// atom 을 읽기만 하는 컴포넌트
import { useRecoilValue } from 'recoil';
import { countState } from '../../recoil/count';

function ReadOnlyCount() {
    const count = useRecoilValue(countState); // 구독하는 atom 의 값만 반환

    return (
        <div>
            <h2>읽기 전용 컴포넌트</h2>
            <p>카운트 {count}</p>
        </div>
    );
}

export default ReadOnlyCount;

3. 예제 실행 결과

https://image.woolta.com/recoilCount2.gif

성공적으로 각기 다른 컴포넌트가 count라는 값을 구독하여 변경되는 부분과 recoil 에서 제공되는 hook 함수가 정상적으로 실행되는것을 확인하실 수 있습니다. :)

selector

selector 는 atom 의 상태에 의존하는 동적인 데이터를 생성 생성합니다. selector 에서는 get 함수(필수항목)를 통해 atom 정보들을 1개이상 가져올 수 있습니다. 이를 통해 atom을 조합하여 간단히 새로운 데이터를 생성할수 있습니다. 물론 atom 의 정보가 바뀌면 해당 atom 을 의존하는 selector 도 자동으로 리랜더링이 됩니다. 또한 한개 이상의 atom정보를 업데이트 하도록 set 함수(선택항목)를 받을 수 있습니다.

1. selector 사용해보기

우선 위에서 작성한 count atom 을 이용한 selector 의 간단한 예시를 작성해보도록 하겠습니다.

recoil 추가

기존 count 외에 input atom 을 추가해 count, input 정보를 조합한 countInputState selector 를 생성하도록 하겠습니다.

// count.js
export const inputState = atom({
    key: 'inputState',
    default: '',
});

export const countInputState = selector({
    key: 'countTitleState',
    get: ({ get }) => {
        return `현재 카운트는 ${get(countState)} 이고 입력값은 ${get(inputState)} 입니다.`;
    },
});

component 작성

atom 의 카운트는 그대로 두고 input 작성부분만 추가 한 뒤 이를 나타내도록 selector 의 값을 보여주도록 합니다. selector 는 값을 보여주기만 할것이기때문에 useRecoilValue 를 통해 작성하도록 하겠습니다.

import { useRecoilState, useRecoilValue } from 'recoil';
import { countState, inputState, countInputState } from '../../../recoil/count';

function SelectorCount() {
    const [ count, setCount ] = useRecoilState(countState); // useRecoilState 을 통한 value, setter 반환
    const [ input, setInput ] = useRecoilState(inputState); // useRecoilState 을 통한 value, setter 반환
    const countInput = useRecoilValue(countInputState);  // useRecoilValue 을 통한 selector 의 get value 반환

    return (
        <div>
            <h2>읽기 쓰기 카운트 컴포넌트</h2>
            <p>카운트 {count}</p>
            <p>selector {countInput}</p>
            <input value={input} onChange={(e) => setInput(e.target.value)} />
            <button onClick={() => setCount(count + 1)}>숫자 증가</button>
            <button onClick={() => setCount(count - 1)}>숫자 감소</button>
        </div>
    );
}

export default SelectorCount;

실행결과

https://image.woolta.com/recoilSelctor1.gif 성공적으로 input, count 의 정보를 조합한 selector 의 정보가 보이고 각 atom들의 정보 변경시점마다 동일하게 변경되는것을 확인하실 수 있습니다. :)

2. set 을 통한 selector 에서의 복수개의 atom 수정

selector 에서는 set 이라는 함수를 통해 여러개의 atom 정보를 동시에 수정할 수 있습니다. 해당 옵션은 필수는 아니고 필요한 경우에만 사용하면 됩니다.
사용하는 방법을 익히기 위해 input 에서 숫자를 적으면 동시에 count, input 정보를 9999 로 변경되도록 하는 예시를 작성해 보도록 하겠습니다.

recoil Selector 에 set 추가

우선 위에서 작성한 countInputState Selector 에 다음과 같이 set 함수를 추가해 주도록 하겠습니다.

export const countInputState = selector({
    key: 'countTitleState',
    get: ({ get }) => {
        return `현재 카운트는 ${get(countState)} 이고 입력값은 ${get(inputState)} 입니다.`;
    },
    set: ({ set }, newValue) => { // 2번째 파라미터 에는 추가로 받을 인자를 나타냅니다.
        set(countState, Number(newValue)); // count atom 수정
        set(inputState, newValue + ''); // input atom 수정
    },
});

컴포넌트 코드 수정

기존의 selector 에는 값을 읽기만 하기 위해 useRecoilValue 를 사용하였으나 set 함수 사용을 위해 useRecoilState 로 변경 9999로 바꾸기 위한 버튼을 작성하도록 하겠습니다.

import { useRecoilState } from 'recoil';
import { countState, inputState, countInputState } from '../../../recoil/count';

function SelectorCount() {
    const [ count, setCount ] = useRecoilState(countState); // useRecoilState 을 통한 value, setter 반환
    const [ input, setInput ] = useRecoilState(inputState); // useRecoilState 을 통한 value, setter 반환
    const [ countInput, setCountInput ] = useRecoilState(countInputState);

    return (
        <div>
            <h2>읽기 쓰기 카운트 컴포넌트</h2>
            <p>카운트 {count}</p>
            <p>selector {countInput}</p>
            <input value={input} onChange={(e) => setInput(e.target.value)} />
            <button onClick={() => setCount(count + 1)}>숫자 증가</button>
            <button onClick={() => setCount(count - 1)}>숫자 감소</button>
            <button onClick={() => setCountInput('9999')}>selector 값 9999로 변경</button>
        </div>
    );
}

export default SelectorCount;

실행 결과

https://image.woolta.com/recoilSelctor1.gif 이처럼 복수개의 atom 정보를 한번에 수정하는것을 확인할 수 있습니다.

3. 비동기 호출

selector 에서는 비동기 호출에 대한 데이터 처리도 지원하고 있습니다. 또한 React의 suspense 를 지원하기 때문에 비동기 처리를 위해 별도의 작업이 없는 큰 장점이 존재합니다. 또한 기존의 redux 대비 비동기를 처리하는 별도의 미들웨어도 필요없고 작성 코드양도 현저히 적어 개인적으로 아주 좋아하는 부분입니다. :)
관련 설명을 위한 예시로 github에서 recoil 의 star 갯수를 가져오는 비동기 예시를 작성해 보도록 하겠습니다.

비동기 recoil 작성

우선 비동기통신을 위한 recoil 작성을 위해 기존의 count.js 가 아닌 recoilStar.js 를 만들어 새로운 selector 를 작성해보도록 하겠습니다.

// recoil/recoilStar.js
import { selector } from 'recoil';

// 비동기 처리 셀렉터
export const recoilStarCountState = selector({
    key: 'asyncState',
    get: async () => {
        const response = await fetch('https://api.github.com/repos/facebookexperimental/Recoil');
        const recoilProjectInfo = await response.json();
        // stargazers_count 반환
        return recoilProjectInfo['stargazers_count'];
    },
});

컴포넌트 생성

이제 해당 selector의 값을 보여주는 컴포넌트를 작성해보도록 하겠습니다.

import { useRecoilValue } from 'recoil';
import { recoilStarSelector } from '../../../recoil/count';

function RecoilStarCount() {
    const recoilStarCount = useRecoilValue(recoilStarSelector);

    return (
        <>
            <p>recoil gitbub star 갯수 </p>
            <p>{recoilStarCount}개</p>
        </>
    );
}

export default RecoilStarCount;

실행결과 (에러발생?!!)

https://image.woolta.com/3fe5fef0d279b3b1.png ??! 비동기 처리된 값이 보일것으로 예상되었으나 실행해보니 에러만 가득 나와있습니다.. 이건 왜 그런 걸까요?..

비동기 처리를 위한 Suspense 지원

recoil 은 비동기 상태에 대한 처리를 React의 Suspense 를 통해 지원하고 있습니다. 때문에 해당 비동기통신을 사용하는 selector 를 사용하기 위해선 해당 컴포넌트를 Suspense 로 비동기 상태에 대한 처리를 진행해 주어야 합니다. 이제 다음과 같이 Suspense 로 감싼 이후 다시 실행해 보도록 하겠습니다.

      <React.Suspense fallback={<div>로딩중입니다.</div>} > // suspense 를 통한 비동기 처리
          <RecoilStarCount />
      </React.Suspense> 

Suspense 사용 후 실행 결과

https://image.woolta.com/recoilSelector3.gif Suspense 를 통한 비동기의 로딩 처리가 정상적으로 되는것을 확인 하실 수 있습니다.

4. suspense 를 사용 안하고 비동기 제어 하기 (useRecoilValueLoadable, useRecoilStateLoadable)

위의 suspense 를 사용하지 않고 비동기를 제어 해야 하는 경우도 생기기 때문에 이를 위해 Recoil 에서는 useRecoilValueLoadableuseRecoilStateLoadable 함수를 지원합니다. 둘의 차이는 위에서 설명드린 useRecoilState, useRecoilValue 와 같이 setter 포함 유무의 차이입니다. 해당 함수로 호출하면 다음과 같이 2개의 값을 반환하게 됩니다.

Loadable 을 통한 상태

  • state : 비동기 상태를 나타내며 hasValue(값이 존재하는 상태), loading(로딩중), hasError(에러발생) 3가지 상태가 존재합니다.
  • contents : 비동기 통신의 결과값입니다.

useRecoilValueLoadable 을 통한 비동기 통신 변경 예제

이제 위에 Suspense 로 비동기 처리된 부분을 useRecoilValueLoadable 을 사용해 만들어 보도록 하겠습니다. 우선 Suspense로 감싼 부분을 삭제하고 RecoilStarCount 컴포넌트를 다음과 같이 작성하도록 하겠습니다.

import { useRecoilValueLoadable } from 'recoil';
import { recoilStarSelector } from '../../../recoil/count';

function RecoilStarCount() {
    const recoilStarCount = useRecoilValueLoadable(recoilStarSelector);

    // 로딩 상태 처리
    if (recoilStarCount.state === 'loading') {
        return <div>loading</div>
    }

    return (
        <>
            <p>recoil gitbub star 갯수 </p>
            <p>{recoilStarCount.contents}개</p>
        </>
    );
}

export default RecoilStarCount;

위의 코드로 변경후 실행하면 suspense로 작업한 결과와 동일한 실행결과를 보실 수 있습니다. :) recoil을 통한 비동기 통신의 큰 장점으로는 rudux 에서는 각 state 에 대한 비동기 상태를 별도로 가져야 하지만 recoil 의 selector 는 비동기 상태에 대한 값의 정보는 담고 있지 않아도 useRecoilXXXLoadable 에서 지원해주기 때문에 훨씬 깔끔하고 코드양이 적어지는 큰 장점이 있습니다. :)

5. 캐싱 지원

selector 를 통해 비동기통신시 가장 큰 장점중 하나로 자체적으로 캐싱을 지원하기 때문에 같은 입력값에 있어서 이전에 캐싱된 결과를 바로 보여주기 때문에 퍼포먼스 면에서도 훨씬 유리한 장점이 존재합니다. 정상적으로 캐싱을 지원하는지 확인하기 위해 간단한 예시를 만들어 확인해보도록 하겠습니다.

예시 작성

캐시 테스트를 위해 특정 atom을 구독하는 비동기통신 selector 를 만든 후 atom의 값을 변경시키는 예시를 만들어 보도록 하겠습니다. 우선 recoil 부터 작성해 보도록 하겠습니다.

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

// 지정한 ms 만큼 지연시키는 util 입니다.
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

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

export const delay1SecSelector = selector<string>({
    key: 'delay1SecSelector',
    get: async ({ get }) => {
        const result = `delayCountState 는 ${get(delayCountState)} 입니다.`;
        await delay(1000); // 1초씩 일부러 지연 시키도롭 합니다. (비동기 캐싱 확인 위함)
        return result;
    },
})

이제 해당 delayCount 를 변경하는 버튼과 비동기 Selector 인 delay1SecSelector (1초 뒤 delayCount 값에 문자열 추가하여 반환) 을 보여주는 컴포넌트를 작성하도록 하겠습니다.

import { useRecoilState, useRecoilValueLoadable } from 'recoil';
import { delayCountState, delay1SecSelector } from '../../../recoil/delayCount';

function DelayCount() {
    const delay1Sec = useRecoilValueLoadable(delay1SecSelector);
    const [ delayCount, setDelayCountState ] = useRecoilState(delayCountState);

    if (delay1Sec.state === 'loading') {
        return <div>로딩중...</div>;
    }

    return (
        <>
            <h3>캐싱된 selector 값</h3>
            <p>{delay1Sec.contents}</p>
            <button onClick={() => setDelayCountState(delayCount + 1)}>캐싱 카운트 업</button>
            <button onClick={() => setDelayCountState(delayCount - 1)}>캐싱 카운트 다운</button>
        </>
    );
}

export default DelayCount;

실행결과

https://image.woolta.com/recoilSelector4.gif

처음에는 1초씩 기다리며 비동기 통신이 되었으나 다시 이전에 값으로 변경하니 기억해둔 캐싱 정보로 바로 호출하는것을 확인하실 수 있습니다.

참고

Copyright © 2018 woolta.com

gommpo111@gmail.com