TIL - Recoil, getting started

김수지·2020년 8월 1일
1

TILs

목록 보기
33/39
post-custom-banner

Today What I Learned

Javascript를 배우고 있습니다. 매일 배운 것을 이해한만큼 정리해봅니다.
오늘은 새로운 상태 관리 라이브러리 Recoil를 정리해봅니다.


3개월 만에 블로깅이라니... 글쓰기 버튼을 누르는 동시에 찐한 반성과 후회가 몰려온다.
입사 이후 한동안 기본기를 다지겠다며 react나 typescript를 책 위주로 보고, 커뮤니티에 올라오는 글만 쓱쓱 리뷰하고 말았던 것 같다. 나름 실전 공부 중이라 여겼지만 사실은 귀차니즘에 대한 변명이었다고 한다...

아무튼 회사에서 mvp 서비스를 하나 내놓기로 했는데 규모가 크지 않아서 React Europe 2020 컨퍼런스에서 페이스북이 세상에 내놓은 Recoil을 적용해보기로 했다. 언제나 그렇듯 getting Started 부터 리뷰 시작. 다음에는 비동기 처리를 정리해볼 예정.

1. What is Recoil and what's the motivation?

1. Recoil이란 무엇인가?

A state management library for React

  • React를 위한 상태관리 라이브러리. 홈페이지에 들어가면 간단히 그 정의가 써있다.
    너무 간단해서 좀 놀랄 수도 있지만, Copyright © 2020 Facebook, Inc.
    요렇게 보면 뭐 이 정도면 말 다 나온 것 아닌가.
    React는 페이스북에서 만든 자바스크립트 라이브러리이다. 그리고 이 React를 위한 상태 관리 라이브러리로 올해 소개된 놈이 Recoil이다.

2. Recoil이 나오기까지 동기(motivations)

  • 개인적으로 폭포수 방식의 props 전개와 이로 인한 re-rendering이 특징인 React에서 상태관리 라이브러리 없이 컴포넌트를 꾸려가기는 매우 어렵다고 생각한다. 가장 먼저 진행해보았던 프로젝트에서 이로 인해 굉장히 머리가 복잡했기 때문이다.
  • 함수형 컴포넌트 + context api 기반의 hooks를 이용한다고 가정한다면 상황은 조금 나아지겠지만 여전히 useState와 useReducer 등을 이용해 어느 컴포넌트에 state를 배치할 것인가는 여전한 고민이다.
  • 위와 같은 이유들 때문에 React 내에서 코드 스플릿 등이 쉽지 않았다. 아마도 이 문제들을 풀기 위해서 Recoil이라는 React 전용 상태관리 라이브러리를 만들기 시작한 것 같다. 클래스형을 버리고 함수형 컴포넌트로 가면서 hook까지 적용해 온 그간의 행보를 봤을 때 여전히 코드 스플릿팅에 어려움이 존재한다면 꼭 풀고 가야 하는 이슈였을 것 같기도 하다.
  • 그 결과 리액트 내에 존재하는 트리 구조에 맞붙어 상태로 관리되는 데이터인 atom과 순수 함수를 통해 atom으로부터 가공된 값을 만들어 컴포넌트에 전달하는 selector등의 개념들이 소개되었다. 그 뿐 아니라 비동기 처리에 있어도 React에서 실험적으로 소개되었던 Concurrent Mode 즉, 매번 렌더링을 하는 것이 아니라 렌더링 동작 우선순위를 정해 렌더링을 할 준비가 되면 그 때서야 렌더링을 진행하는 모드를 지원한다고 한다.

2. Recoil core concepts

  • 일단은 가장 기본이 되는 코어 컨셉을 정리해보자.

1. Atom

  • atom은 상태를 담고 있는 유닛이다. React hooks에서 useState를 보면 state에 데이터를 넣고 setState로 데이터를 변경하는 구조를 띄고 있는 것을 볼 수 있는데 atom은 state를 만들어내는 느낌이다.
  • atom 또한 useState의 state처럼 업데이트할 수 있고, subscribe 할 수 있다. 아톰에 업데이트가 발생하면 이를 subscribe하고 있는 컴포넌트들도 새로운 값과 함께 리렌더 된다. 아톰은 컴포넌트 내부 상태로도 쓰일 수 있다. 만약 여러 컴포넌트가 같은 아톰을 사용한다면 모든 컴포넌트들이 아톰의 상태를 공유한다고 볼 수 있다.
  • 아톰은 함수를 통해 값이 형성된다.
const fontSizeState = atom({
  key: 'fontSizeState',
  default: 14,
});
  • 아톰은 디버깅, 영속성 그리고 모든 아톰의 구조를 보여주는 특정 api에서 사용하기 위한 고유 키값이 필요하다. 만약 2개 이상의 아톰이 같은 이름의 키값을 가지고 있다면 에러를 발생시킨다. 아톰의 키 값은 전역에서 고유해야 한다.
  • 아톰을 컴포넌트 안에서 읽고 활용하기 위해서는 useRecoilState라는 hook을 사용한다. 리액트의 useState와 같은 기능을 한다고 볼 수 있지만, 그와 다르게 컴포넌트들 사이에서 상태를 공유할 수 있다. (우와!)
function FontButton() {
  const [fontSize, setFontSize] = useRecoilState(fontSizeState);
  return (
    <button onClick={() => setFontSize((size) => size + 1)} style={{fontSize}}>
      Click to Enlarge
    </button>
  );
}
  • useState보다 조금 더 꼬아놓은 듯한 느낌이 들긴 하지만 useState가 defaultState를 가지고 선언 된다면 useRecoilState에서는 atom 함수를 안고 있다고 생각하면 되겠다. 아톰 내부는 객체처럼 고유 key를 부르면 해당하는 default value을 뱉어내는 구조로 선언된다고 생각했다.
  • 같은 아톰을 사용하는 다른 컴포넌트에서도 value가 변경되면 적용된다.
function Text() {
  const [fontSize, setFontSize] = useRecoilState(fontSizeState);
  return <p style={{fontSize}}>This text will increase in size too.</p>;
}

2. Selector

  • selector는 atom 혹은 다른 selector를 인풋으로 받는 순수 함수이다. 인풋이 업데이트되면 셀렉터 함수도 재평가된다. 아톰처럼 컴포넌트가 셀렉터를 subscribe할 수 있고, 셀렉터의 변경이 있을 때 컴포넌트들이 리렌더딩 한다.
  • 셀렉터는 상태를 기준으로 해서 파생된 데이터를 계산하는데 사용한다. 셀렉터의 방식을 따르면 중복 상태를 나열하는 것을 피할 수 있고, 리듀서를 이용해 동기적이면서 유효한 값을 유지하는 작업을 진행하지 않아도 된다.(굳이 리듀서까지 불러올 이유가 없다는 이야기) 대신 아톰이라는 최소한의 정보의 셋을 유지하면서 다른 상태값들은 함수의 형태로 존재하다가 필요 시 효율적으로 값을 계산해낸다.
  • 아톰처럼 셀렉터도 selector 함수를 통해서 정의된다.
const fontSizeLabelState = selector({
  key: 'fontSizeLabelState',
  get: ({get}) => {
    const fontSize = get(fontSizeState);
    const unit = 'px';

    return `${fontSize}${unit}`;
  },
});
  • selector로도 state를 꾸릴 수 있는데 이때는 get, set 설정이 필요하고 state의 가공이 필요한 경우에 사용한다고 보면 된다.(굳이 가공이 필요 없는 것들도 selector로 get, set을 마련해둘 필요는 없다) 이렇게 보니 mobx의 getter, setter와 매우 흡사한 느낌이다.
  • get은 아톰 혹은 다른 셀렉터들을 인자로 받아서 계산하는 함수이다. 위에 나온 fontSizeLabelStatefontSizeState라는 아톰에 의존적이다. fontSizeState를 인자로 받아서 가공된 결과(fontSizeLabelState)를 뱉는 순수함수인 것이다.
  • 셀렉터는 useRecoilValue()라는 훅을 이용해 사용할 수 있다. 셀렉터에는 useRecoilState를 사용하지 않는데 이는 셀렉터 자체는 writable하지 않기 때문이다.
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>
    </>
  );
}
  • 이제 이 컴포넌트에서 버튼을 클릭하면 폰트사이즈도 커지고(아톰) 그로 인해 폰트사이즈의 라벨(셀렉터의 get을 통해 산출된 결과)도 그 영향을 받게 될 것이다.
profile
선한 변화와 사회적 가치를 만들고 싶은 체인지 메이커+개발자입니다.
post-custom-banner

0개의 댓글