React 학습 _ recoil이란?

박서연·2022년 10월 13일
0

React학습

목록 보기
1/1

0. recoil이란?

todo list를 만들기 전 recoil을 모르기 때문에 recoil을 먼저 검색해 보았습니다.
옆에 있는 친구에게 물어보니 자기도 atom과 selector의 차이를 모르겠다고 하더군요.
하지만, 전 atom과 selector도 모르는 상태였기 때문에 무슨말이지 싶었습니다.

결론 : 열공하자!!

recoil이 나오게 된 배경

기존 사용하는 react 자체에 내장된 상태 관리 기능들에는 한계가 있는데,

  • 컴포넌트의 상태를 공통된 상위 요소까지 끌어올려야만 공유되며, 이 과정에서 거대 트리가 다시 렌더링된다.
  • context는 단일 값만 저장할 수 있으며, 자체 소비자를 가지는 여러 값들의 집합을 담을 수는 없다.
    -> 1, 2로 인해 트리의 최상단인 state가 존재하는 곳 부터, 말단인 state가 사용되는 곳까지의 코드 분할을 어렵게 하게됩니다.

위의 문제점을 개선하기 위한 recoil

recoil을 간단히 정리하자면 트리구조로 되어있는 dom이, 먼 친척한테 영향을 미칠 때 조상부터 계속 내려가지 않고도 사용할 수 있게 해주는 아이이다.

  • 리코일 장점
  1. 상태 정의는 점진적이고 분산되어 있기 때문에, 코드 분할이 가능하다.
  2. 파생된 데이터를 사용하는 컴포넌트를 수정하지 않고도 상태를 파생된 데이터로 대체가능하다.
  3. 링크에서 상태 전환을 인코딩할 수 있다.
  4. 전체 어플리케이션 상태를 하위호환되는 방식으로 유지하기가 쉬워, 유지된 상태에서 어플리케이션에 변동이 있어도 살아남을 수 있다.

recoil 살펴보기

recoil을 어렵게 말하면 atoms(공유 상태)에서 selectors(순수 함수)를 거쳐 React 컴포넌트로 내려가는 data-flow graph를 만들 수 있다고 한다.

  • atoms : 컴포넌트가 구독할 수 있는 상태의 단위.
  • selectors: atoms 상태값을 동기 or 비동기 방식을 통해 변환.

1. atoms

atoms 사용해보기

const fontSizeState = atom({
 key: 'fontSizeState',
 default : 14,
 });

atoms는 고유한 key가 필요합니다. 키 값은 전역적으로 고유하도록 되어야하며, 같은 키를 갖게 되면 오류가 발생합니다.
defalut는 값의 기본 값을 의미합니다.

컴포넌트에서 atom을 읽고 쓰기 위해서는 useRecoilState라는 hook을 사용합니다.

  • useRecoilState vs useState
    : useRecoilState는 useState와 비슷하지만, state가 컴포넌트 간에 공유될 수 있다는 차이가 있습니다.

    function FontButton() {
     const [fontSize, setFontSize] = useRecoilState(fontSizeState);
     return (
     <button onClick = {() => setFontSize((size) => size + 1)} style = {{fontSize}}>
      Click to Enlarge
      </button>
      );
      }

    위와 같이 사용되며, 다른 컴포넌트에서 fontSizeState를 사용하는 곳이 있다면

    ex)

    funtion Text() {
    	const [fontSize, setFontSize] = useRecoilState(fontSizeState);
       return <p style = {{fontSize}}> This is hi </p>;
    }

    이런 컴포넌트에서도 동일하게 값이 변경됩니다.

useState만 써봤던 저에게는 엄청 재미있는 이야기였습니다. 값을 함수의 인자로 넘겨주거나 하는 번거로움이 없어졌네요!!

그렇다면 selector는 어떻게 사용할까요?

2. selector

selector 사용해보기

selector는 atoms나 다른 selectors를 입력으로 받아들이는 함수입니다. recoil에서 함수나 파생된 상태를 나타내며, 주어진 종속성 값 집합에 대해 항상 동일한 값을 반환하는 부작용이 없는 순수함수입니다.

  • 상위 atoms 또는 selectors가 업데이트되면, 하위 selector 함수도 다시 실행됩니다.
  • 컴포넌트들은 selectors를 subscribe할 수 있고, selectors가 변경되면 컴포넌트들도 다시 렌더링됩니다.

그럼 언제 selector를 사용할까요?

  • selectors는 상태를 기반으로 하는 파생 데이터를 계산하는 경우 사용됩니다.
  • atoms에는 최소한의 상태 집합만 저장하고, 다른 데이터들은 selectors에 명시한 함수를 통해 효율적으로 계산할 수 있습니다.
  • selectors는 어떤 컴포넌트가 자신을 필요로 하거나 어떤 상태에 의존하는지를 추적하기 때문에 함수적인 접근방식을 매우 효율적으로 만들어줍니다.

ex)

const fontSizeLabelState = selector({
key: 'fontSizeLabelState',
get: ({get}) => {
  const fontSize = get(fontSizeState);
  const unit = 'px';
  return `${fontSize}${unit}`;
},
});

위 함수가 어떤 역할을 하는지 먼저 살펴보겠습니다.
이 예시에서 selector는 fontSizeState라는 하나의 atom에 의존성을 가지게 됩니다. 즉, fontSizeLabelState selector는 fontSizeState를 입력으로 사용하며 형식화된 글꼴 크기 레이블을 출력으로 반환하는 순수 함수처럼 동작하게 됩니다.

selector함수에 대해 자세히 보면,

key는 atom과 동일하게 고유한 문자열이어야 하며, 내부적으로는 atom을 식별하는데 사용되는 고유한 문자열입니다.
get 함수는 다른 atom이나 selector로 부터 값을 찾는데 사용되는 함수입니다. 전달되는 get 인자를 통해 atoms와 다른 selectors에 접근이 가능합니다. 자동으로 종속 관계가 생성되므로, 참조했던 다른 atoms나 selectors가 업데이트 되는 경우 이 함수도 다시 실행되게 됩니다.
useRecoilValue()를 사용하여 읽을 수 있으며, 이는 하나의 atom이나 selector를 인자로 받아 대응값을 반환합니다.

data 읽는 selectors 간단한 예시

function FontButton() {
 const [fontSize, setFontSize] = useRecoilState(fontSizeState);
 const fontSizeLabel = useRecoilValue(fontSizeLabelState);

 return (
   <>
     <div>Current font size: ${fontSizeLabel}</div>

     <button onClick={setFontSize(fontSize + 1)} style={{fontSize}}>
       Click to Enlarge
     </button>
   </>
 );
}

동작을 간단히 요약하면, 버튼을 클릭하면 버튼의 글꼴 크기가 증가하는 동시에 현재 글꼴 크기가 업데이트되어 나오는 두 가지 작업이 수행됩니다.
세부 동작 (default font size를 14로 둠)
1. current font size: 14px로 출력된다.
2. button이 클릭되어 setFontSize(fontSize + 1)이 동작
3. useRecoilState(fontSizeState)인 atom이 동작하여 fontSizeState인 atom을 사용하는 모든 곳에서 fontSize(14) + 1이 됨.
4. fontSizeState atom에 의존성을 가지던 fontSizeLabelState selecotr도 자동으로 업데이트되어 useRecoilBalue가 작동하게 된다.
5. 새 값은 useRecoilVaule()를 통해 읽어 와 fontSizeLabel에 return된다.
6. Current font size: 15px로 출력된다.

get 함수만 제공되면 읽기만 가능한 RecoilValueReadOnly 객체를 반환하며, set 함수도 제공되면 selector는 쓰기 가능한 RecoilState 객체를 반환합니다.
ex)

function selector<T>({
 key: string,

 get: ({
   get: GetRecoilValue
 }) => T | Promise<T> | RecoilValue<T>,

 set?: (
   {
     get: GetRecoilValue,
     set: SetRecoilState,
     reset: ResetRecoilState,
   },
   newValue: T | DefaultValue,
 ) => void,

 dangerouslyAllowMutability?: boolean,
})

set이 설정되면, selector는 쓰기 가능한 상태를 반환하며, 첫번째 매개변수로 콜백 객체와 새로운 입력 값이 전달됩니다. 사용자가 selecotr를 재설정하는 경우 새로운 입력 값은 T타입 또는 DefaultValue 타입의 객체일 수 있습니다.
콜백 객체>
1. get : 다른 atom이나 selector로 부터 값을 찾는데 사용되며, 이 함수는 주어진 atom이나 selector를 subscribe하지 않는다.
2. set : recoil상태의 값을 설정할 때 사용되는 함수. 첫 번째 매개변수는 recoil 상태, 두번째 매개변수는 새로운 값.

dangerouslyAllowMutability : selecotr는 파생된 상태의 순수 함수를 나타내며 항상 동일한 의존성 입력 값 집합에 대해 동일한 값을 반환해줍니다. 이를 위해 selector에 저장된 모든 값은 기본적으로 고정되어 있고, 경우에 따라 dangerouslyAllowMutability을 사용해 재정의할 수 있습니다.

set 예시 & 동기/비동기

먼저 동기와 비동기를 언제 사용하는지 무슨 차이가 있는지 헷갈릴때가 있는데요!

  • 동기 : 데이터의 요청과 결과가 한 자리에서 동시에 일어난다.
    요청을 하는 경우 시간이 얼마나 걸리던지 요청한 자리에서 결과가 주어져야하며 기다려야합니다.
  • 비동기 : 비동기는 요청한 결과는 동시에 일어나지 않을 것이라는 약속입니다.

비동기의 경우 원하는 순서로 결과값을 예상하기 힘들기 때문에, callback을 사용할 수 있습니다. callback을 사용하면 callback의 유무를 확인하여 순서대로 출력이 가능한데, 계속 중첩되는 callback은 코드가 길어져 가독성이 떨어집니다. 이러한 콜백지옥에 빠지지 않도록 promise와 async/await을 사용하여 해결할 수 있습니다.
promise는 .then을 사용하여 다음 작업을 처리하기 때문에 콜백지옥 없이 진행되는데, promise를 더 쉽게 사용할 수 있도록 도와주는게 async/await입니다. 함수의 앞부분에 async를 넣고, 해당 함수 내부에서 promise부분에 await을 사용하여 promise가 끝날때 까지 기다리고 해당 값을 특정 변수에 담을 수 있습니다.

selector 비동기 사용 예시

const currentUserNameQuery = selector({
  key: 'CurrentUserName',
  get: async ({get}) => {
    const response = await myDBQuery({
      userID: get(currentUserIDState),
    });
    return response.name;
  },
});

function CurrentUserInfo() {
  const userName = useRecoilValue(currentUserNameQuery);
  return <div>{userName}</div>;
}

function MyApp() {
  return (
    <RecoilRoot>
      <React.Suspense fallback={<div>Loading...</div>}>
        <CurrentUserInfo />
      </React.Suspense>
    </RecoilRoot>
  );
}

비동기의 경우에는 promise를 리턴하거나 async 함수를 사용하기만 하면 됩니다.
위의 경우에는 react의 렌더 함수가 동기로 사용되고 있습니다. 이때는 promise가 해결되기 전에 렌더되는 아이가 없게 됩니다. 이러한 경우를 위해 Recoil은 React Suspense와 함께 동작하도록 디자인 되어있습니다. 위의 MyApp()을 보면 React.Suspense로 CurrentUserInfo가 감싸져있는데, 아직 보류중인 하위 항목들(promise가 해결되기 전)을 잡아내고 이를 대체하기 위한 UI를 렌더합니다. 위의 예시에서는 Loading...이 뜨는 것으로 대체되는걸 볼 수 있습니다.

selector 동기 사용 예시 (with. set 함수)

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

const tempFahrenheit = atom({
  key: 'tempFahrenheit',
  default: 32,
});

const tempCelcius = selector({
  key: 'tempCelcius',
  get: ({get}) => ((get(tempFahrenheit) - 32) * 5) / 9,
  set: ({set}, newValue) =>
    set(
      tempFahrenheit,
      newValue instanceof DefaultValue ? newValue : (newValue * 9) / 5 + 32,
    ),
});

function TempCelcius() {
  const [tempF, setTempF] = useRecoilState(tempFahrenheit);
  const [tempC, setTempC] = useRecoilState(tempCelcius);
  const resetTemp = useResetRecoilState(tempCelcius);

  const addTenCelcius = () => setTempC(tempC + 10);
  const addTenFahrenheit = () => setTempF(tempF + 10);
  const reset = () => resetTemp();

  return (
    <div>
      Temp (Celcius): {tempC}
      <br />
      Temp (Fahrenheit): {tempF}
      <br />
      <button onClick={addTenCelcius}>Add 10 Celcius</button>
      <br />
      <button onClick={addTenFahrenheit}>Add 10 Fahrenheit</button>
      <br />
      <button onClick={reset}>Reset</button>
    </div>
  );
}

조금 더 복잡해진 selectors를 들고 왔습니다. 동기이면서 set이 사용되는 상태인데요!
동기의 경우 지금까지 했던 방식처럼 사용하면 됩니다.
그럼 위 코드의 작동을 살펴볼까요?
1. Temp (Celcius) : 0
Temp (Fahrenheit): 32 인 상태가 기본으로 출력 됩니다.
2. 첫번째 버튼이 클릭되어 addTemCelcius 함수가 실행되면, setTempC(tempC + 10)이 호출됩니다.
3. 이는 useRecoilState(tempCelcius)를 통해 tempCelcius selector가 호출되고, set에서 newValue는 0+10인 10의 값이 들어와 (newValue 9) / 5 + 32 = 50로 tempFahrenheit 값이 업데이트됩니다.
4. 해당 selectors는 tempFahrenheit에 의존하기 때문에 get도 업데이트되어 10을 반환하게 됩니다.
5. 반환된 값은 tempC에 저장되고, tempF는 useRecoilState(tempFahrenheit)를 통해 50로 업데이트 된 값을 가지게 됩니다.
6. 따라서 Temp (Celcius) : 10
Temp (Fahrenheit): 50의 값이 출력되게 됩니다.
7. 두번째 버튼이 클릭되면 addTenFahrenheit함수가 실행되어 setTempF(tempF + 10)가 호출됩니다. 이는 tempFahrenheit인 atom의 값에 10을 더해주어 tempF에는 60이 들어가게 됩니다.
8. 역시 tempFahrenheit가 업데이트 되었으니, 의존성을 가진 tempCelcius도 업데이트 되어 get에서 (60 - 32)
5) / 9의 값을 리턴해 tempC가 바뀌게 됩니다.
9. Temp (Celcius) : 15.55555555555556
Temp (Fahrenheit): 60 출력
10. reset을 누르게 되면 처음의 상태로 돌아가 Temp (Celcius) : 0
Temp (Fahrenheit): 32가 출력됩니다.

코딩하다가 생긴 갑작스러운 useEffect Tip.

useEffect 사용 시 렌더링 한번에 두번씩 실행되는 경우가 생기는데!
이는 index.tsx에서 React.StrictMode를 없애면 해결됨

next: to-do-list crud 코드

profile
개발자

0개의 댓글