React스러운 상태관리 라이브러리, Recoil을 알아보자
Front-End/React

React스러운 상태관리 라이브러리, Recoil을 알아보자

 

 

이제까지의 프로젝트에는 상태 관리 라이브러리로 항상 Redux를 사용했었는데, 그 이유는 단순하게 대부분의 사용자들이 redux를 사용했고, 사용자들이 가장 많이 사용하는 라이브러리 = 좋은 라이브러리라는 생각이 있었기 때문이다. Redux는 한때 너무 많은 양의 보일러 플레이트 코드를 작성해야 한다는 단점을 가지고 있었지만, Redux-toolkit의 등장으로 그 단점을 많이 보완되었다. 그럼에도 불구하고, 어떤 프로젝트에서는 전역 상태 관리를 위해 Redux를 설치하는 것이 너무 과하다고 느껴질 때가 있다.

 

 

 

 

 

다른 대안, context API

 

라이브러리를 추가적으로 설치하지 않고 context API를 사용하는것은 어떨까?

context API는 일일히 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있는 리액트의 내장 툴이다.

const MyContext = React.createContext(defaultValue);

context를 생성해서

<MyContext.Provider value={/* 어떤 값 */}>
        {children} //value가 전달되는 곳.
</MyContext.Provider>

위와 같이 컴포넌트를 Provider로 감싸주면, 그 하위에 있는 모든 컴포넌트들은 이 React Context에 저장되어 있는 전역 데이터에 접근할 수 있다.

 

다만 context에서는 Provider 하위에서 context를 구독하는 모든 컴포넌트는 Provider의 value prop가 바뀔 때마다 다시 렌더링 된다. 따라서 context를 관심사에 따라 나눠서 관리하거나 따로 최적화를 해줘야한다.

 

공식 홈페이지의 언제 context를 써야 할까 를 보면 context를 현재 로그인한 유저, 테마, 선호하는 언어 등의 데이터 공유 시에 사용할 것을 추천한다. 엄밀히 말하면 context전역적으로 데이터를 제공해주는 툴이지 전역 상태를 관리하는 툴이 아니다. redux의 대체 대상으로 고려하기에는 그 역할이 다르다.

 

 

 

 

recoil을 써보자

 

recoil은 이러한 전역 상태 관리 문제에 대하여 react 팀에서 리액트다운 방법으로 개선하기 위하여 만든 상태 관리 라이브러리이다.

주의할 점은 아직 알파버전만 나온 상태라는 것이다. 이 글을 작성한 시점을 기준으로 현재 0.7.4 버전까지 나와있다. 따라서 실무나 규모가 큰 프로젝트에서 사용하기에는 위험부담이 있으므로 만약 도입하고자 한다면 신중하게 고민해야 한다.

 

 

https://github.com/facebookexperimental/Recoil/issues/1495

 

What's happening in the future of recoil? · Issue #1495 · facebookexperimental/Recoil

Switched over one of my projects to recoil and its so much better than redux its absurd. I will never use redux again if I don't have to. But I was curious about what features are planned for f...

github.com

recoil의 메이저 버전이 언제 나오는지에 대한 질문에 "we keep the project in an experimental status today until we are confident in a solution compatible with all upcoming React features." 라고 답한 것을 봐서는 아마도 당분간은 알파버전을 유지할 것 같다.

 

 

 

 

recoil의 개발 동기

recoil 공식페이지에서는 recoil를 개발하게 된 계기를 다음과 같이 설명했다.

 

호환성 및 단순함을 이유로 외부의 글로벌 상태 관리 라이브러리보다는 React 자체에 내장된 상태 관리 기능을 사용하는 것이 가장 좋다. 그러나 React는 다음과 같은 한계가 있다.

* 컴포넌트의 상태는 공통된 상위 요소까지 끌어올려야만 공유될 수 있으며, 이 과정에서 거대한 트리가 다시 렌더링 되는 효과를 야기하기도 한다.
* Context는 단일 값만 저장할 수 있으며, 자체 소비자(consumer)를 가지는 여러 값들의 집합을 담을 수는 없다. 이 두 가지 특성이 트리의 최상단(state가 존재하는 곳)부터 트리의 말단(state가 사용되는 곳)까지의 코드 분할을 어렵게 한다.

우리는 API와 의미 및 동작을 가능한 React 답게 유지하면서 이것을 개선하고자 한다.

 

recoil의 개념은 매우 단순하다. 가능한 React답게 동작하는 것을 목표로 한 만큼 react hook과 사용법이 매우 닮아있다. 따라서 기존의 react hook을 사용한 개발자라면 recoil도 곧 잘 다룰 수 있을 것이다.

 

 

초기 셋팅

먼저 초기 셋팅을 해보자.

npm install recoil

recoil을 설치해준 후,

import React from 'react';
import { RecoilRoot } from 'recoil';

function Root() {
  return (
    <RecoilRoot>
      <App />
    </RecoilRoot>
  );
}

최상위 컴포넌트를 RecoilRoot 로 감싸주면 모든 준비가 끝난다.

redux를 사용할 때처럼 <Provider store={store}> store를 props로 넘겨주지 않아도 된다.

 

이제 atomsselector 의 개념을 이해해보자.

 

 

 

 

atoms

 

atoms 은 상태의 단위로 업데이트와 구독이 가능하다. atom이 업데이트되면 해당 atom을 구독한 컴포넌트는 모두 다시 렌더링 된다.

const wordState = atom<string>({
  key: "wordState",
  default: "",
});

atom 은 고유의 key값을 가진다. 상태를 컴포넌트에서 사용할 때 이 key값으로 식별한다.

default 는 말 그대로 초기값이다.

 

 

const [word, setWord] = useRecoilState(wordState);

사용법도 간단하다. useState 와 동일한 방식으로 사용해주면 된다.

 

 

const onClick = (value: string) => {
      if (word.length >= 5) {
        return;
      }
      setWord(word + value);
    };

word 는 상태값, setWord 는 setter함수이다.

 

 

이밖에도 값만을, 또는 setter만을 반환하는 hook이 있으니 상황에 따라 적절하게 사용하면 된다.

useRecoilState() atom의 값을 구독하여 업데이트할 수 있는 hook. useState와 동일한 형식을 가진다.

useRecoilValue() atom의 값만을 반환한다.

useSetRecoilState() setter 함수만을 반환한다.

useResetRecoilState() default값으로 초기화해주는 함수이다.

 

 

 

 

selector

Selector는 atom이나 다른 selector를 기반으로 파생된 상태를 만드는 순수 함수로 redux의 reselect와 닮아있다.

 

의존하는 atom 또는 selector가 업데이트되면 하위의 selector 함수도 다시 실행된다. 컴포넌트들은 selector를 atom처럼 구독할 수 있으며 selector가 변경되면 컴포넌트들도 다시 렌더링 된다.

 

 

selector를 사용해 전체 todo리스트에서 완료된 항목만을 필터링한 todo리스트를 반환해보자.

import { atom, selector } from 'recoil';

type Todo = { 
    id: number; 
    text: string; 
    isComplete: boolean 
};

const todoListState = atom<Todo[]>({
  key: 'todoListState',
  default: [],
});

const completedTodosSelector = selector({
  key: 'completedTodosSelector',
  get: ({ get }) => {
    const todoList = get(todoListState);
        return todoList.filter((todo) => todo.isComplete);
  },
});

atom과 마찬가지로 고유 key 값이 필요하며, get 매서드를 통해 사용할 값을 반환한다.

completedTodosSelector 내부에서 todoListState를 의존하고 있기 때문에 todoListState의 값이 변하면 해당 selector도 재실행된다.

 

 

➕ 쓰기 가능한 selector

 

recoil select의 특이한 점은 set 매서드도 제공해준다는 점이다. set 속성이 설정되면 select 는 쓰기 가능한 상태를 반환해준다.

 

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,
})

selector의 구조를 살펴보면, set의 새로운 값은 두 번째 매개변수인 newValue를 통해 받아올 수 있다. 이 newValueT | DefaultValue로 정의되어있는 것을 볼 수 있는데, 타입이 제너럴<T>일때는 setter함수를 통해 값을 업데이트하는 상황이며 DefaultValue일 때는 useResetRecoilState를 통해 값을 초기화하는 상황이다.

 

 

따라서, selector를 통해 데이터를 변환해야 하는 상황일 때는 아래와 같이 현재 값을 업데이트하는 상황인지 초기화하는 상황인지를 판단하여 로직을 적용해야 한다.

const transformSelector = selector({
  key: 'TransformSelector',
  get: ({get}) => get(myAtom) * 100,
  set: ({set}, newValue) =>
    set(myAtom, newValue instanceof DefaultValue ? newValue : newValue / 100),
});

 

 

 

 

마치며

 

recoil의 핵심 개념 두 개를 알아보았다. atomselector 가 어떤 건지 이해했다면 recoil을 사용할 준비가 다 된 것이다. 개인적으로는 recoil을 사용하면서 단순한 구성과 사용법이 바로 체감되어 매우 만족스러웠다.

redux를 사용하면서 불필요한 보일러 플레이트 코드를 줄이고 싶다는 생각이 들었다면, redux가 아닌 새로운 상태 관리 라이브러리를 다뤄보고 싶었다면 recoil을 도입해볼 것을 추천한다.

 

 

 

 

 

Recoil은 동시성 모드(Concurrent Mode)를 비롯한 다른 새로운 React의 기능들과의 호환 가능성도 갖습니다.

 

더 나아가서, 지난 3월 말 React 18 버전이 발표되었다. React 18 버전에서의 핵심 키워드는 concurrent(동시성) 이다. 이 concurrent mode는 사용자 경험 향상을 위해 react가 제시하는 새로운 개발 패러다임이며 아마도 개발 프로세스에 큰 변동을 야기할 것이라고 생각한다. concurrent mode와 관련해 v18에서 정식으로 추가된 기능이 Suspense이다. recoil은 데이터 처리 시 이 Suspense와 함께 동작하도록 디자인되어있다.

이 부분에 관해서는 “recoil과 비동기 데이터 호출” 포스팅에서 보다 자세하게 다뤄보도록 하겠다. (사실 포스팅 하나에 전부 넣고 싶었지만 생각보다 길어졌다🙄)

 

 

 

 

 

 

 

+  이 글은 다음 포스팅과 이어집니다

 

recoil과 비동기 데이터 호출 (+ 선언적 프로그래밍)

💡 이 글은 'React스러운 상태관리 라이브러리, Recoil을 알아보자' 에서 이어집니다. React스러운 상태관리 라이브러리, Recoil을 알아보자 이제까지의 프로젝트에는 상태 관리 라이브러리로 항상 Red

leego.tistory.com