Recoil

Khan·2022년 7월 11일
0

TIL

목록 보기
1/19
post-thumbnail

Recoil 이란

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

Atoms는 컴포넌트가 구독할 수 있는 상태의 단위이며 Selectors는 atoms 상태 값을 동기 또는 비동기 방식을 통해 변환해줍니다.

왜 Recoil을 사용할까?

각 상태관리 라이브러리 단점

Context

  • 불필요한 렌더링이 발생한다. 왜냐하면 context의 상태 값이 달라지면 Provider 밑에 있는 하위 컴포넌트가 모두 렌더링 되기 때문이다. 그래서 context api는 단순히 props drilling 해결이 목적이고, 렌더링 최적화가 필요 없는 상태라면 Context를 사용해서 해결할 수 있다.

Redux

  • Redux는 사용자가 가장 많은 게 장점이지만 확실한 단점이 존재한다. Redux에의 단점으로 가장 많이 언급된 것은 보일러 플레이트 코드가 너무 많다는 것이다. 하나의 상태를 위해 액션 타입, 액션 생성함수, 리듀서를 선언하는 것은 매우 피곤한 일이다. 그리고 러닝 커브가 있다는 단점도 있다. 위에 말한 것처럼 Redux에서 파생된 여러 라이브러리까지 익히기 위해서는 많은 시간을 투자해야 한다.

Recoil

  • 아직 Recoil은 정식 버전을 출시하지 않았다.

Recoil 장점

  1. 적은 코드양, 쉬운 러닝 커브
    Recoil은 상태 하나를 atom으로 정의하고, 그것을 구독하는 패턴이다. 또한 이런 atom 들로부터 파생된 상태를 selector로 선언한다. 이 selector 들은 구독한 atom이 변화하면 자동으로 변화한다. 그리고 페이스북에서 만든 만큼 리액트와도 매우 잘 어울려서 훅을 사용해보았다면 쉽게 배울 수 있다.

  2. 렌더링 최적화
    Context에서는 상태 변경 시 구독한 하위 컴포넌트들이 모두 리렌더링 되는 문제가 있었는데, Recoil은 atom, selector를 구독하면 구독한 컴포넌트만 리렌더링이 일어난다.

  3. 간편한 비동기 처리
    Recoil에서는 자체적으로 selector에서 지원한다. 에러처리도 리액트의 suspense를 사용할 수 있고, useRecoilValueLoadable을 사용한다면, hasValue, loading, hasError 상태를 제공해서, 로딩 완료 시, 로딩 시, 에러 시 분기 처리가 손쉽게 가능하다.

사용예시

RecoilRoot

리코일 state를 사용하는 컴포넌트들은 <RecoilRoot>를 필요로 한다. <RecoilRoot>를 사용하는 가장 좋은곳은 root component이다.

// app.js
import React from 'react';
import {RecoilRoot} from 'recoil';    
import Counter from './Counter';

const App = () => {
  return (
    <RecoilRoot>
      <Counter />
    </RecoilRoot>
  );
};

Atom

아톰은 상태를 말하며 어떠한 컴포넌트에서 씌여지고 읽혀질 수 있다. 아톰의 value를 읽는 컴포넌트들은 암묵적으로 그 아톰을 구독하고 있다. 그래서 아톰을 업데이트하면 모든 컴포넌트가 리렌더링 되는게 아니라 아톰을 참고하고 있는 컴포넌트만 리렌더링 된다.

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

selector

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

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

export const countInputValueState = selector({
    key: 'countInputValueState',
    get: ({ get }) => {
          const countValue = get(countState);
       return `현재 카운트는 ${countValue} 입니다.`;
    },
});

컴포넌트 사용

  • useRecoilState

    • 기존 useState 와 같이 변경되는 값과 해당 값을 변경하는 함수를 반환합니다.
    • 아톰의 상태를 설정할 수 있고, 변경 시킬 수 도 있다.
  • useRecoilValue

    • 구독하는 값만 반환하는 함수입니다. 값의 update 함수가 필요없을 경우 사용합니다.
    • 아톰을 조회할 때만 사용한다
  • useSetRecoilState

    • 구독하는 값을 변경하는 함수만 반환합니다.
  • useResetRecoilState

    • 아톰 값을 디폴트 값으로 변경시켜준다.
  • useRecoilValueLoadable

    • 비동기로 처리된 selector를 실행시켜준다.

예제

  1. 버튼 클릭 시 count 증가 & 감소, 결과 값 출력
// App.jsx
import { RecoilRoot } from 'recoil';
import Counter from './Counter';

const App = () => {
  return (
    <RecoilRoot>
      <Counter />
    </RecoilRoot>
  );
};

export default App;
// countAtom.js 
import { atom, selector } from 'recoil';

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

export const countInputValueState = selector({
  key: 'countInputValueState',
  get: ({ get }) => {
    const countValue = get(countState);
    return `현재 카운트는 ${countValue}입니다.
   `;
  },
});
// Counter.jsx
const Counter = () => {
  const [countValue, setCounterValue] = useRecoilState(countState);
  const resetCountValue = useResetRecoilState(countState);
  const resultValue = useRecoilValue(countInputValueState);
  
  const plusCount = () => setCounterValue((prev) => prev + 1);
  
  const minusCount = () => setCounterValue((prev) => prev - 1);
  
  return (
    <div>
      <div>{countValue}</div>

      <button onClick={plusCount}>+</button>
      <button onClick={minusCount}>-</button>
      <button onClick={resetCountValue}>reset</button>
    </div>
  );
};

export default Counter;

  1. 버튼 클릭 외 input으로 입력 값 더하기
// App.jsx

import { RecoilRoot } from 'recoil';
import Counter from './Counter';

const App = () => {
  return (
    <RecoilRoot>
      <Counter />
    </RecoilRoot>
  );
};

export default App;
// countAtom.js 
import { atom, selector } from 'recoil';

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

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

export const countInputValueState = selector({
  key: 'countInputValueState',
  get: ({ get }) => {
    const countValue = get(countState);
    const inputValue = get(inputCountState);
    return `현재 카운트는 ${countValue}입니다, 입력 카운트는 ${inputValue} 입니다`;
  },
});
// Counter.jsx
import { countState, inputCountState, countInputValueState } from './countAtom';
import { useRecoilState, useRecoilValue, useSetRecoilState, useResetRecoilState } from 'recoil';

const Counter = () => {
  const [countValue, setCounterValue] = useRecoilState(countState);
  const resetCountValue = useResetRecoilState(countState);

  const inputCountValue = useRecoilValue(inputCountState);
  const setInputCountValue = useSetRecoilState(inputCountState);

  const resultValue = useRecoilValue(countInputValueState);

  const plusCount = () => setCounterValue((prev) => prev + 1);

  const minusCount = () => setCounterValue((prev) => prev - 1);

  const inputHandler = ({ target }) => setInputCountValue(target.value);

  const submitCount = () => setCounterValue((prev) => prev + Number(inputCountValue));

  return (
    <div>
      <div>{countValue}</div>

      <div>
        <button onClick={plusCount}>+</button>
        <button onClick={minusCount}>-</button>
        <button onClick={resetCountValue}>reset</button>
      </div>

      <div>
        <input type="text" placeholder={inputCountValue} onChange={inputHandler} />
        <button onClick={submitCount}>입력값 더하기</button>
      </div>

      <div>{resultValue}</div>
    </div>
  );
};

export default Counter;

  1. 비동기 처리
// App.jsx

import { RecoilRoot } from 'recoil';
import SelectorCount from './StarCounter';

const App = () => {
  return (
    <RecoilRoot>
      <SelectorCount />
    </RecoilRoot>
  );
};

export default App;
// starAtom.js

import { selector } from 'recoil';

export const starCountState = selector({
  key: 'starCountState',
  get: async () => {
    const response = await fetch('https://api.github.com/repos/facebookexperimental/Recoil');
    const recoilProjectInfo = await response.json();
    // stargazers_count 반환
    return recoilProjectInfo['stargazers_count'];
  },
});
// SelectorCount.jsx

import { useRecoilValueLoadable } from 'recoil';
import { starCountState } from './starAtom';

const SelectorCount = () => {
  const starCount = useRecoilValueLoadable(starCountState);

  // 로딩 상태 처리
  if (starCount.state === 'loading') return <div>loading</div>;
  

  return (
    <>
      <p>recoil github star 갯수 </p>
      <p>{starCount.contents}</p>
    </>
  );
};

export default SelectorCount;

0개의 댓글