공부 및 일상기록

[React] Recoil 이란 무엇인가? 본문

개발/React

[React] Recoil 이란 무엇인가?

낚시하고싶어요 2023. 1. 19. 10:43

React의 한계

컴포넌트 상태는 공통된 상위 요소까지 끌어올려야만 공유될 수 있으며, 이 과정에서 거대한 트리가 다시 렌더링되는효과를 야기하기도 한다.

Context는 단일 값만 저장할 수 있으며, 자체 소비자(comsumer)를 가지는 여러 값들의 집합을 담을 수는 없다.

이 두 가지 특성이 트리의 최상단(state가 존재하는 곳)부터 트리의 말단(state가 사용되는 곳)까지의 코드 분할을 어렵게 한다.

 

Recoil은 방향 그래프를 정의하고 React 트리에 붙인다. 상태변화는 이 그래프의 뿌리(atoms)로 부터 순수함수(selectors)를 거쳐 컴포넌트로 흐르며, 다음과 같은 접근 방식을 따른다.

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

 

Recoil 비동기 통신 (Suspense, Loadable)

Suspense

recoil은 비동기 상태에 대한 처리를 react의 suspense를 통해 지원하고 있다.

때문에 비동기 통신을 사용하는 selector를 사용하기 위해선 해당 컴포넌트를 suspense로 비동기 상태에 대한 처리를 진행해 주어야 한다. 

Suspense를 사용하면 컴포넌트가 렌더링 되기 전까지 기다릴 수 있다.

 

 

 

 

atom

atom은 하나의 상태를 의미한다. react에서 흔히 볼 수 있는 state와 같은 개념으로 atom의 값을 변경시 해당 atom을 구독하고 있는 모든 컴포넌트들이 리렌더링 되며 해당 변경된 atom의 값을 사용하게 된다. 

atom은 다음과 같은 두가지 필수 값을 설정해야 한다.

key : 고유한 key값 (보통 해당 atom을 생성하는 변수 명으로 지정)

default : atom의 초기값을 정의한다. 정적인값, promise, 다른 atom의 값으로 설정할 수 있다.

promise 로 기본값 설정시 정적인 값을 반환하는 promise는 설정이 가능하지만 외부에서 데이터를 불러오는 비동기 통신정보는 사용할 수없다. 그 경우에는 selector를 사용해야 한다.

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

위 코드는 아톰을 생성하는 예시이다.

 

atom, selector를 사용하기 위해 지원하는 Hook

atom생성 후 컴포넌트에서 해당 atom을 사용할 땐, useState를 사용하는 것 처럼 Recoil에서 제공하는 hook을 통해 사용 할 수 있다.

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

atom 사용 예시 (count 예시)

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


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

 

아톰을 생성 했으면 아래와 같이 컴포넌트에서 사용 할 수 있다.

// 읽기 및 쓰기 컴포넌트
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를 보여주기만 한다면 useRecoilValue를 사용해 다음과 같이 만들 수 있다.

// 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;

 

Selector

Selector는 atom의 상태에 의존하는 동적인 데이터를 생성한다. selector에서는 get함수를 통해 atom 정보들을 1개이상 가져올 수 있다. 이를 통해 atom을 조합하여 간단히 새로운 데이터를 생성할 수 있다. atom의 정보가 바뀌면 해당 atom에 의존하는 selector도 자동 리렌더링 된다. 또한 한개 이상의 atom 정보를 업데이트 하도록 set함수를 받을 수 있다.

 

selector 사용해보기

위 예제로 사용했던 count atom을 이용한 selector 간단 예시 ( + input atom을 추가로 작성)

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

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

 

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

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;

 

 

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

selector에서는 set이라는 함수를 통해 여러개의 atom 정보를 동시에 수정할 수 있다.

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에 set 함수를 추가하여 값을 수정하는 코드를 위처럼 작성한다.

 

컴포넌트 코드도 수정한다.

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;

이렇게 하면 여러개의 atom 정보가 한번에 수정되는것을 볼 수 있다.

 

비동기 호출

selector에서는 비동기 호출에 대한 데이터 처리도 지원한다. React의 suspense를 지원하기 때문에 비동기 처리를 위해 별도의 큰 작업이 필요가 없다. 또한 redux대비 비동기 처리하는 별도의 미들웨어도 필요 없고 작성 코드 양도 현저히 적다.

 

비동기 recoil 작성

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

해당 셀렉터를 다음 컴포넌트에서 사용

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;

하지만 이렇게 실행하면 에러가 발생한다.

리코일의 비동기 통신은 suspense를 통해 지원이 되고 있으므로 selector로 비동기 통신을 하기 위해선 해당 컴포넌트를 Suspense로 감싼 이후 사용해야 한다.

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

 

suspense 사용 없이 비동기 제어하기 (Loadable)

Loadable 클래스는 아톰 또는 셀러터의 현재 준비 상태와 값을 알려준다. 

비동기 값을 반환하는 아톰이나 셀렉터를 useRecoilValueLoadable과 useRecoilStateLoadable 훅에 전달하면 Loadble 인스턴스를 반환한다.

 

Loadable에는 몇 가지 메소드와 프로퍼티가 있지만 아마도 현재 준비 상태를 알려주는state와 contents 프로퍼티가 가장 유용할 것이다. state는 hasValue, hasError, loading 중 하나의 값이 되고, contents는 hasValue 상태일 때는 실제값, hasError 상태일 때는 에러 객체가 되고, loading 상태일 때는 Promise객체가 된다.

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

 

비동기 아톰이나 셀렉터라 하더라도 Loadable을 사용해서 값을 가져오고, loading 상태의 contents를 사용하지 않으면 <Suspense>를 사용하지 않아도 된다. 다음 예제는 Loadable을 사용해 <Suspense> 없이 로딩 중 상태를 보여주는 카운터 컴포넌트 이다.

import {atom, useRecoilValueLoadable} from 'recoil';

const counter = atom({
  key: 'counter',
  default: new Promise(resolve => {
    setTimeout(() => resolve(0), 10000);
  })
});

function Counter() {
  const countLoadable = useRecoilValueLoadable(counter);
  let count = '';

  switch (countLoadable.state) {
    case 'hasValue':
      count = countLoadable.contents; // contents는 Number
      break;
    case 'hasError':
      count = countLoadable.contents.message; // contents는 Error
      break;
    case 'loading':
    default:
      count = '... loading ...'; // 로딩 중일 때
  }

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

 

Recoil과 Redux 비교

Redux에서는 각 state에 대한 비동기 상태를 별도로 가져야 하지만, recoil의 selector는 비동기 상태에 대한 값의 정보는 담고 있지 않아도 Loadable에서 지원해주기 때문에 훨씬 깔끔하고 코드양이 적어진다.

redux에서는 reducer 단위로 state를 구성하였으나 atom은 이런 reducer 단위가 아닌 더 잘개 쪼개진 state 단위로 상태를 관리할 수 있게 된다.

 

 

 

출처 

https://blog.woolta.com/categories/1/posts/209

 

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

woolta 블로그 ver2.0 기술 개발 블로그

blog.woolta.com

 

'개발 > React' 카테고리의 다른 글

DOM과 Virtual DOM  (0) 2023.05.03
[React] CORS에러 해결방법 (Proxy server 설정)  (0) 2023.05.03
[React] React-Query 란 무엇인가?  (0) 2023.01.19
[React] Context API와 Redux 비교  (0) 2023.01.19
[React] Flux 패턴이란?  (0) 2023.01.19