Recoil과 Recoil Selector를 이용하여 비동기 처리하기

mhlog·2023년 5월 5일
0

React

목록 보기
7/10
post-thumbnail

이 문서는 Recoil 공식문서를 참고하여 제가 이해한 방식으로 작성한 글입니다. 틀린 부분이 있으면 알려주시면 감사하겠습니다!

0. Recoil의 필요성

Recoil팀에서 말하는 React 자체에 내장된 상태 관리 기능의 한계점은 다음과 같다.

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

React에서 컴포넌트는 상태를 가지고 있을 수 있으며, 이 상태가 변경되면 해당 컴포넌트와 그 자식 컴포넌트들이 다시 렌더링 된다. 그러나, 컴포넌트들 사이에 상태를 공유하고 싶을 때는 해당 상태를 공통된 상위 요소까지 끌어올려야 한다. 이를 Lifting Up이라고 한다. 상태를 끌어올리는 과정에서 문제가 발생할 수 있는데, 상태를 끌어올린 상위 요소가 다시 렌더링되면 그 아래에 있는 모든 자식 요소들도 함께 다시 렌더링되어야 한다. 이는 컴포넌트 트리 전체의 재렌더링을 유발할 수 있으며, 큰 규모의 애플리케이션에서는 성능 이슈를 야기할 수 있다. 이런 현상을 "거대한 트리가 다시 렌더링되는 효과"라고 말하는 것이다.

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

이는 React에서 기본적으로 제공하는 Context API의 한계에 대한 언급이다. Context API를 사용하면 Provider와 Consumer를 통해 데이터를 전달하고, 컴포넌트 트리 내에서 필요한 곳에서 해당 데이터를 사용할 수 있다. 그러나 Context API는 단일 값만 저장할 수 있는 한계가 있다. 즉, Context는 단일 값 또는 객체를 저장할 수 있지만, 여러 값의 집합을 저장하기 위해 별도의 자체 소비자를 가지는 것은 불가능하다. 따라서 여러 값들을 컨텍스트에 저장하고 싶은 경우, 단일 값으로 묶거나 객체 안에 넣어야 하는 번거로움이 있을 수 있다.

1. Atoms

Atoms는 상태의 단위이며, 업데이트와 구독이 가능하다. atom이 업데이트되면 각각 구독된 컴포넌트는 새로운 값을 반영하여 다시 렌더링 된다. atoms는 런타임에서 생성될 수도 있다. Atoms는 React의 로컬 컴포넌트의 상태 대신 사용할 수 있다. 동일한 atom이 여러 컴포넌트에서 사용되는 경우 모든 컴포넌트는 상태를 공유한다.

Recoil에서는 크게 Atoms와 Selector로 이루어진다. 먼저, Atoms은 React에서 한번이라도 상태관리를 해보았다면 매우 쉽게 파악할 수 있을 것이다. 사용 예시는 다음과 같다.

// recoil/fontSizeState.ts
const fontSizeState = atom({
  key: 'fontSizeState',
  default: 14,
});

export default fontSizeState;

Atoms에는 디버깅과 지속성 등을 위해서 사용되는 Key가 필요하다. 이 Key는 전역적으로 고유해야한다. 또한, default로 기본값을 관리한다. 이는 React 컴포넌트의 상태와 매우 유사하다고 볼 수 있다. 이렇게 선언한 State는 컴포넌트에서 다음과 같이 사용될 수 있다.

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

이는 React의 useState와 매우 유사한 것을 볼 수 있다. Recoil에서 사용하는 Hook은 다음과 같다.

  • useRecoilState(state): 첫 요소가 상태의 값이며, 두번째 요소가 호출되었을 때 주어진 값을 업데이트하는 setter 함수를 리턴.
  • useRecoilValue(state): 주어진 Recoil 상태의 값을 리턴.
  • useSetRecoilState(state): 쓰기 가능한 Recoil 상태의 값을 업데이트하기 위한 setter 함수를 리턴.
  • useResetRecoilState(state): 주어진 상태를 default 값으로 리셋하는 함수를 리턴.

2. Selector

Synchronous

Selector는 파생된 상태(derived state)의 일부를 나타낸다. 파생된 상태를 어떤 방법으로든 주어진 상태를 수정하는 순수 함수에 전달된 상태의 결과물로 생각할 수 있다. -공식문서

여기서 "파생된 상태"란, 기존 상태(State)를 기반으로 계산되고 생성되는 상태를 말한다. 즉, Selector는 기존 상태를 입력으로 받아 순수 함수를 통해 파생된 상태를 반환하는 것을 의미한다. (순수함수란, 같은 입력이 들어오면, 해당 입력에 대한 출력은 항상 같은 함수라는 뜻)

const todoListFilterState = atom({
  key: 'todoListFilterState',
  default: 'Show All',
});

const filteredTodoListState = selector({
  key: 'filteredTodoListState',
  get: ({get}) => {
    const filter = get(todoListFilterState);
    const list = get(todoListState);

    switch (filter) {
      case 'Show Completed':
        return list.filter((item) => item.isComplete);
      case 'Show Uncompleted':
        return list.filter((item) => !item.isComplete);
      default:
        return list;
    }
  },
});

위의 코드를 보면 Selector는 get으로 todoListState와 todoListFilterState를 구독한다. 구독한 todoListState, todoListFilterState 둘 중 하나라도 Update되면 자동으로 todoList가 업데이트 되는 구조이다.

function TodoList() {
  // changed from todoListState to filteredTodoListState
  const todoList = useRecoilValue(filteredTodoListState);

  return (
    <>
      <TodoListStats />
      <TodoListFilters />
      <TodoItemCreator />

      {todoList.map((todoItem) => (
        <TodoItem item={todoItem} key={todoItem.id} />
      ))}
    </>
  );
}

useRecoilValue를 통해서 fliter된 todoList에 접근 할 수 있고, 하위 컴포넌트에서 todoListState, todoListFilterState 둘 중 하나라도 Update되면 자동으로 todoList가 업데이트 되는 구조이다.

Asynchronous (비동기처리)

Selector에서 비동기처리를 하려면 Suspense와 Loadable을 이용하여 비동기 데이터가 도착하기 이전의 Fallback (Loading창)을 보여주어야한다. 이에 대해서는 추후에 글을 쓰도록 하고 이번 포스팅에서는 Selector로 비동기처리를 하는 방법에 대해서만 알아보도록 하겠다.

위에서 언급하지는 않았지만 위에서 처리한 작업은 동기적으로 Selector를 사용한 것이다. Atoms를 이용해서 서버로부터 데이터를 받아와서 받아온 data를 Atoms에 저장하는 방법도 가능하겠지만, 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>;
}

위와 같이 DB로 부터 User의 이름을 비동기적으로 받아와야한다고 할 때 get을 통해서 currentUserIDState를 구독하고 async await를 이용해서 DB로 부터 유저의 이름을 받아온 뒤 return 한다.

지금까지는 get함수만 소개를 했지만 Selector는 set함수도 가질 수 있다.

공식문서에서 소개된 Selector의 구조는 다음과 같은데,

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함수는 writeable 한 state 값을 변경할 수 있는 함수를 return 하는 곳. 여기서 주의할 점은, 자기 자신 selector를 set 하려고 하면, 스스로를 해당 set function에서 set 하는 것이므로 무한루프가 돌게 되니 반드시 다른 selector와 atom을 set 하는 로직을 구성하여야 한다. 또한 애초에 selector는 read-only 한 return 값(RecoilValue)만 가지기 때문에 set으로는 writeable 한 atom 의 RecoilState 만 설정할 수 있다.

3. 프로젝트에서 Selector로 구현한 비동기처리.

export default selector<TResponseData>({
  key: 'initialOrderState',
  get: async ({ get }) => {
    // queryData가 수정될때마다 아래 코드를 실행
    const queryData = get(QueryDataState);
    if (
      queryData === undefined ||
      window.location.pathname !== `/${QUIZ_PAGENAME}`
    )
      return undefined;

    const { amount, team } = queryData;
    const response = await axios({
      url: `${process.env.REACT_APP_SERVER_URL}/quiz/get`,
      method: "GET",
      params: {
        amount: amount,
        team: team,
      }
    })
    
    if(response.error) {
      throw response.error
    }
    ... 생략
    
    return response.data;
  },
  set: ({ get, set }) => {
    const amount = get(QuizNumbersState);
    const team = get(QuizTeamState);

    set(QueryDataState, { amount, team });
    set(QuizNumbersState, DEFAULT_NUMBERS);
  },
});

set 속성에서는 get을 통해서 QuizNumberState와 QuizTeamState를 구독한 뒤, set 함수를 통해서 두 State가 Update 될 때마다 QueryDataState가 Update된다.

그런데, get 속성에서 queryData를 get함수를 통해서 구독하고 있기 때문에 QueryDataState가 Update될 때마다 get 속성에 있는 함수가 실행되는 것을 확인할 수 있다.

axios로 서버와 통신을 해주었는데, 만약 서버가 죽어서 response에 error를 담아서 온다면 에러를 던지게 된다. 여기서 던진 에러는 React ErrorBoundary(보통 React Suspense와 함께 사용)를 통해서 잡을 수 있게 된다.

0개의 댓글