TIL_62_Recoil

JIEUN·2021년 4월 12일
0
post-thumbnail

오늘은 기업협업의 첫 날.
대표님께서 리덕스보다 리코일을 더 추천한다는 얘길 듣고, 개념이 궁금해져서 찾아보았다.
정말 공부할게 많구나 새삼 느꼈고
공부하는 만큼 계속 성장할 수 있다는 것에 설렘도 느꼈다.

리코일을 보기 전, 리덕스에 관해서도 이해해야할 것 같아서 리덕스 관련 벨로그도 첨부해본다.

Redux

Recoil — 새로운 리액트 상태 관리 라이브러리?

Recoil - 또 다른 React 상태 관리 라이브러리?

위 링크의 내용을 토대로 작성한 내용들이다.

Recoil

복잡한 UI를 대상으로 전역 상태관리를 위한 최적화된 방법을 찾으려고 했으나 성능 및 효율성이라는 장벽에 부딪히게 됨

그래서 Redux 나 Mobx의 잘못이다?

아니다.
기존의 상태관리 라이브러리들은 문제가 없다.

But.

  • 이 상태관리 라이브러리들이 React 라이브러리가 아니라는 점.
  • store는 "외부요인"으로 취급되어 React의 내부 스케줄러에 접근할 수 없다. 사실 지금까지는 이것이 중요한 문제는 아니었음.

But.

  • 동시성 모드가 등장하며 React와 동시성 모드를 손쉽게 사용할 수 있는 해결방안이 필요하였을 것.
    -> Recoil은 내부적으로 React의 상태를 사용하고 있으며 동시성 모드에 대한 지원도 곧 추가될 예정.

Redux는 강력한 기능을 제공하긴 하지만, 기본적인 store 구성을 위해 많은 보일러 플레이트와 장황한 코드를 작성해야 한다.

그리고 비동기 데이터 처리 또는 계산된 값 캐시와 같은 중요한 기능은 라이브러리 기능이 아니며,
이를 해결하기 위해서는 또 다른 라이브러리를 사용해야한다.

그리고 만약 selector가 동적인 prop을 받는 경우 이 값을 정확하게 기억하는 것은 어려운 일이다.

그럼 Context API를 사용하면 안돼?

React가 가진 상태 공유 솔루션인 Context API도 한계가 있다.
반복적이고 복잡한 업데이트에 사용할 경우 비효율적.

Sebastian Markbage의 말을 인용하면:

개인적으로 새로운 Context는 locale/theme와 같은 낮은 빈도의 업데이트에 사용 가능하다고 생각한다. 또한 이전에 Context를 사용했던 방법으로 사용해도 좋다.(즉 정적인 값, 구독을 통해 업데이트를 전파하는 것) 하지만 Context는 Flux와 같은 상태 관리 시스템을 대체할 수 없다.

Context API는 데이터 서브셋을 대상으로 변경을 감지하고 업데이트 할 수 없다.

Provider 하위의 모든 consumer들은 Provider 속성이 변경될 때마다 다시 렌더링된다. (https://reactjs.org/docs/context.html#before-you-use-context)

만약 Provider의 값이 배열이나 객체인 경우 구조가 조금이라도 변경된다면, 그 Context를 구독하고 있는 하위의 모든 것(컴포넌트가 그 값의 일부분만 사용할지라도) 이 다시 렌더링될 것이다.

그렇다면 각각의 이미지가 각각의 Context를 가지고 있다면?

정확한 이미지 수를 알고있다면 아무 문제가 되지 않는다.

But.
이미지를 동적으로 변경할 수 있고, 추가할 수 있다면?
새로운 이미지에 Context Provider를 추가하여 컴포넌트 트리를 다시 구성하고 전체 서브 트리를 다시 마운트 해야한다. 더 좋지 않은 방법이다.
-> 동적으로 Provider를 추가하려면 전체 하위 트리가 다시 마운트 됨.

그래서 Recoil은 뭐가 다른데?

일단.
API가 단순하고 이미 Hook을 사용하고 있는 사람들에게 익숙할 것이다.
Recoil을 시작하기 위해서는 어플리케이션을 RecoilRoot로 감싸고, 데이터를 atom이라는 단위로 선언하여 useState를 Recoil의 useRecoilState로 대체해야한다.

그리고. 컴포넌트가 사용하는 데이터 조각만 사용할 수 있고, 계산된 selector를 선언할 수 있으며, 비동기 데이터 흐름을 위한 내장 솔루션까지 제공한다.

동적 키로 atom을 만들고, selector에 인자를 보내는 등 모두 간단하게 할 수 있다.

그리고 곧 React 동시성 모드에 대한 지원도 될 것.

Recoil의 기본?

Atom : atom은 하나의 상태라고 볼 수 있다. 컴포넌트가 구독할 수 있는 React state라고 생각하면 된다. atom의 값을 변경하면 그것을 구독하고 있는 컴포넌트들이 모두 다시 렌더링 된다. atom을 생성하기 위해 어플리케이션에서 고유한 키 값과 디폴트 값을 설정해야 한다. 디폴트 값은 정적인 값, 함수 또는 심지어 비동기 함수가 될 수 있다.

export const nameState = atom({
  key: 'nameState',
  default: 'Jane Doe'
});

useRecoilState: atom의 값을 구독하여 업데이트할 수 있는 hook. useState와 동일한 방식으로 사용할 수 있다.

useRecoilValue: setter 함수 없이 atom의 값을 반환만 한다.

useSetRecoilState: setter 함수만 반환한다.

import {nameState} from './someplace'
// useRecoilState
const NameInput = () => {
  const [name, setName] = useRecoilState(nameState);
  const onChange = (event) => {
    setName(event.target.value);
  };
  return <>
    <input type="text" value={name} onChange={onChange} />
    <div>Name: {name}</div>
  </>;
}
// useRecoilValue
const SomeOtherComponentWithName = () => {
  const name = useRecoilValue(nameState);
  return <div>{name}</div>;
}
// useSetRecoilState
const SomeOtherComponentThatSetName = () => {
  const setName = useSetRecoilState(nameStae);
  return <button onClick={() => setName('Jon Doe')}>Set Name</button>;
}

selector: selector는 상태에서 파생된 데이터로, 다른 atom에 의존하는 동적인 데이터를 만들 수 있게 해준다. Recoil의 selector는 기존에 우리가 알던 selector의 개념과는 조금 다르다. Redux의 reselector 와 MobX의 @computed 처럼 동작하는 "get"함수를 가지고 있다. 하지만 하나 이상의 atom을 업데이트 할 수 있는 "set" 함수를 옵션으로 받을 수 있다.

// 동물 목록 상태
const animalsState = atom({
  key: 'animalsState',
  default: [{
    name: 'Rexy',
    type: 'Dog',
   }, {
    name: 'Oscar',
    type: 'Cat',
   }],
  });
// 필터링 동물 상태
const animalFilterState = atom({
  key: 'animalFilterState',
  default: 'dog',
  });
// 파생된 동물 필터링 목록
const filteredAnimalsState = selector({
  key: 'animalListState',
  get: ({get}) => {
    const filter = get(animalFilterState);
    const animals = get(animalsState);

    return animals.filter(animal => animal.type === filter);
 }
});
// 필터링된 동물 목록을 사용하는 컴포넌트
const Animals = () => {
  const animals = useRecoilValue(filteredAnimalsState);
  return animals.map(animal => (<div>{ animal.name }, { animal.type }</div>));
}

어플리케이션의 요구사항에 맞게 Recoil로 만들어보자.

  1. 이미지를 동적으로 추가할 수 있어야 한다.
  2. 이미지의 이름을 변경할 때는 선택한 이미지와 메타데이터 컴포넌트만 다시 렌더링 해야한다.
  3. 이미지와 데이터는 비동기적으로 로딩되어야 한다.

1번 2번 요구사항을 위해 각각의 이미지를 자체 atom에 저장하려고 한다. 이를 위해 atomFamily를 사용할 수 있다. atomFamilyatom과 동일하지만, 다른 인스턴스와 구분이 가능한 매개변수를 받을 수 있다. 아래의 두 코드는 같은 의미이다.

// atom
const itemWithId = memoize(id => atom({
  key: `item-${id}`,
  default: ...
}))

// atomFamily
const itemWithId = atomFamily({
  key: 'item',
  default: ...
});

atom과 유일한 차이점은 atomFamily는 내부적으로 memoization을 할 것이기 때문에, 각 인스턴스마다 고유한 키를 만들 필요가 없다는 것이다.

atomatomFamilydefault 값을 만들기 위해 다른 함수를 호출할 수도 있다. 여기서도 마찬가지로, atomFamily에서는 내부적으로 생성한 고유한 id를 넘겨줄 수 있다는 차이가 있다.

export const imageState = atomFamily({
  key: "imageState",
  default: id => getImage(id)
});

컴포넌트가 imageState를 최초로 호출할 때, default 값을 만드는 함수 (getImage(id)) 가 호출될 것이다.

이 함수는 비동기 함수 일 수 있으며, Recoil이 React Suspense의 도움을 받아 처리할 것이다.

store의 코드:

const getImage = async id => {
  return new Promise(resolve => {
    const url = `http://someplace.com/${id}.png`;
    let image = new Image();
    image.onload = () =>
      resolve({
        id,
        name: `Image ${id}`,
        url,
        metadata: {
          width: `${image.width}px`,
          height: `${image.height}px`
        }
      });
    image.src = url;
  });
};
export const imageState = atomFamily({
  key: "imageState",
  default: async id => getImage(id)
});

컴포넌트의 코드:

// 이미지 목록
const Images = () => {
  const imageList = useRecoilValue(imageListState);
  return (
    <div className="images">
      {imageList.map(id => (
        <Suspense key={id} fallback="Loading...">
          <Image id={id} />
        </Suspense>
      ))}
    </div>
  );
};
// 단일 이미지
const Image = ({ id }) => {
  const { name, url } = useRecoilValue(imageState(id));
  return (
    <div className="image">
      <div className="name">{name}</div>
      <img src={url} alt={name} />
    </div>
  );
};

뭐? Selector가 데이터를 SET 할 수 있다고?

위에서 selector에 대해 이야기 할 때 setter 함수를 selector에 전달할 수 있다고 언급하였다. 이상해보이지만, 단지 네이밍 때문에 혼란스러운 것이다 (그리고 바뀌길 바란다). selector를 하나의 상태이지만 파생된 것으로 생각해보자. selectoratom로부터 계산된 값을 얻을 수 있고, 또한 복수의 atom에게 영향을 줄 수도 있다.

아래 예시에서 selector는 파생된 상태(특정 색상 상자의 카운터 객체)를 반환한다. 이 setter 함수는 box atomFamily로 생성된 모든 box들에 영향을 줄 수 있으며 값을 재설정할 수 있다.

const colorCounterState = selector({
  key: "colorCounterState",
  get: ({ get }) => {
    let counter = { [COLORS.RED]: 0, [COLORS.BLUE]: 0, [COLORS.WHITE]: 0 };
    for (let i = 0; i < BOX_NUM; i++) {
      const box = get(boxState(i));
      counter[box] = counter[box] + 1;
    }
    return counter;
  },
  set: ({ set }) => {
    for (let i = 0; i < BOX_NUM; i++) {
      set(boxState(i), COLORS.WHITE);
    }
  }
});

0개의 댓글