3분 Recoil

24867 단어 RecoilRecoil

목차

  • Recoil Basics
    • atom
    • selector
    • hooks
  • Recoil 비동기 처리
    • action hooks
    • async selector
  • recoil vs redux
    • 장점
    • 단점

이글은 Recoil을 써보지 않은 사람을 독자로 설정합니다.
간단한 설명을 위해 디테일한 코드 및 설명은 생략됩니다.
3분만에 recoil을 훑어보고, 팀내에 도입된 Redux와 장단점을 비교해봅니다.

4일동안 리코일을 써보고, 공부해본게 전부입니다. 🙃
그래서 깊은 내용을 다루지는 않습니다. 이점 참고하고 읽어주세요.
주말에 공부하면서 이거 썼어요.... 꼭 읽어주세요

Recoil Basics

우선 Recoil의 기본개념부터 알아보죠
Recoil의 핵심 개념은 atoms, selectors로 나눠져요.

⚛️ atom

atom은 상태 단위로 업데이트, 구독이 가능합니다.
우선은 리액트의 state라고 생각하시면 됩니다.

const $counter = atom<number>({ // number는 아래 default값에서 추론되기때문에 생략가능합니다
   key: 'counter',
   default: 0,
})

컴포넌트에서 이를 가져다 쓰기위해서는 useRecoilState() 함수를 사용합니다.

const [counter, setCounter] = useRecoilState($counter)

React의 useState()hook과 사용성이 비슷하지만 상태를 컴포넌트간 공유한다는 차이점이 있어요
어떤 컴포넌트에서든 $counter를 구독할 수 있고 상태가 업데이트되면 react state와 마찬가지로 reRender됩니다.

☑ selector

selector는 atom 또는 selector 를 구독해서 derived value을 리턴하는 순수함수입니다.

const $doubleCounter = selector({
   key: 'doubleCounter',
   get: ({ get }) => {
      const counter = get($counter)
      return counter * 2
   },
})

위 예시를 보면 derived value에 대해서 이해가 될거에요. $counter 값으로부터 유도된 새로운 값이죠.
$doubleCounter$counter를 구독하게 됩니다. $counter가 업데이트되면 $doubleCounter도 당연히 같이 업데이트 되겠죠?

최소한의 상태만 atom에 저장하고 이로부터 유도된 값은 selector를 통해서 관리하게 됩니다.

그리고 위에서 언급했다시피 selectorselector도 구독할 수 있습니다.
$doubleCounter를 구독해서 2배값을 derive 한다면 $quadraCounter라는 selector도 만들 수 있겠죠?

🎣 hooks

다음으로 살펴볼것들은 hooks 입니다.

위에서도 한번 보여드린 useRecoilState() 같은 것들이 몇개 더 있는데요.

주로 사용되는 hook은 다음과 같습니다:

  • useRecoilState(): 상태를 읽고 쓰려고 할 때 사용. 컴포넌트는 atom을 구독함
  • useRecoilValue(): 상태를 읽기만 할 때 사용. 컴포넌트는 atom을 구독함
  • useSetRecoilState(): 상태를 쓰려고만 할 때 사용.
  • useResetRecoilState(): 초기화 함수 제공. atom선언시 넘겼던 default값으로 초기화

useSetRecoilState(), useResetRecoilState()처럼 set류 hook은 리코일 상태를 구독하지 않습니다.
따라서 리렌더링으로부터 자유롭습니다. 아래처럼 최적화를 하는것이 좋습니다.

const [menu, setMenu] = useRecoilState($menu)
setMenu({ ...menu, food: 'pizza' }) // ❌

const setMenu = useSetRecoilState($menu)
setMenu((prevMenu) => ({ ...prevMenu, food: 'pizza' })) // ✅

Recoil 비동기 처리

🎣 action hooks

공식문서에서 등장하는 비동기 처리 패턴은 아닙니다. 이름도 사실 제가 지은거에요...
명령적 프로그래밍을 해야할 경우, 쓰기 편하고 직관적입니다. 👍

명령적 프로그래밍을 해야만 하는 경우가 가끔 있는데요
이런경우는 아래의 async selector를 쓰면 문제가 복잡해집니다.
리코일 문서에서 이를 해결하는법을 알려주고, 여러 컨퍼런스 영상을 찾아봤지만 (불편하다는 의견 다수)
이런경우는 그냥 action hooks를 쓰는게 낫습니다. (개인적인 생각입니다.)

const $menu = atom({
  key: 'menu',
  default: {
    food: '',
    price: 0,
    isLoading: false,
  },
})

const useMenuAction = () => {
  const setMenu = useSetRecoilState($menu)
  const fetchMenu = async (id: number) => {
    setMenu((prev) => ({ ...prev, isLoading: true }))
    const menu = await MyAPI.getMenu(id)
    setMenu((prev) => ({ ...menu, isLoading: false }))
  }
  return {
    fetchMenu,
  }
}

const Menu = () => {
  const menu = useRecoilState($menu)
  const { fetchMenu } = useMenuAction()
  
  if (menu.isLoading) return <LoadingSkeleton />
  return (
    <div> // redux의 dispatch(fetchMenu(id)) 와 유사하죠?
      <button onClick={() => fetchMenu(id)}>
        여기를 눌러서 메뉴정보를 가져오세요
      </button>
      <div>{menu.food} : {menu.price}</div>
    </div>
  )
}

☑ async selector

공식문서에 등장하는 패턴입니다.
선언적 프로그래밍을 할 경우 편리합니다. 👍

인자를 받는 경우는 selectorFamily, 인자가 없는 경우는 selector를 사용합니다.
지면관계상 selectorFamily만 다루겠습니다. selector도 궁금하다면 recoil:selector 공식 문서를 참고하세요.

const fetchMenu = selectorFamily({
  key: 'fetchMenu',
  get: (id: number) => async ({}) => {
    const menu = await MyAPI.getMenu(id)
    return menu
  },
})

const Menu = () => {
  const menu = useRecoilValue(fetchMenu(10))

  return (
    <div>
      <div>{menu.food} : {menu.price}</div>
    </div>
  )
}

뭔가 이상함을 눈치 채셨을텐데요, menu는 비동기적으로 결정되는 값입니다.
하지만 menu를 동기적으로 결정된 값으로 취급해서 렌더링을 하는데요.
이는 react에 실험적으로 도입된 Suspense API와 깊은 연관이 있습니다.
프로그래머가 비동기적인 값을 동기값으과 동일하게 취급해서 다루고, 리액트의 Suspense API가 이를 마법처럼 해결해주는 방식입니다.
물론 Suspense API를 위한 추가적인 코드 작성이 필요합니다.

const App = () => {
  return ( // menu값이 resolve 되기 전까지 LoadingSkeleton을 렌더링
    <Suspense fallback={<LoadingSkeleton />}>
      <Menu />
    </Suspense>
  )
}

위 예시처럼 프로그래머가 loading 처리를 if (loading)처럼 명령형으로 표현할 필요가 없습니다.
Suspense를 통해 선언형으로 로딩상태를 표현할 수 있다는것이 장점이죠.

리액트팀은 Suspense API를 통해서 다음과같은 목표를 이루려합니다: 👇🏻

  • 로딩상태와 에러상태는 외부에 위임하여 컴포넌트에서 동기적인 코드로 만든다.
  • 비동기를 쉽게 처리. 컴포넌트에서는 동기값을 처리하듯 코드작성
  • 간단하고 읽기 편한 컴포넌트

Suspense사용 코드를 살펴보면 async/await의 try, catch문과 유사한 구조를 찾을 수 있습니다.

<ErrorBoundary fallback={<ErrorView />}>
  <Suspense fallback={<LoadingSkeleton />}>
    <Menu />
  </Suspense>
</ErrorBoundary>
try {
   await fetchMenu()
} catch (error) {
  // 에러 처리
}

우리가 모든 실패할 수 있는 함수에 try, catch문을 감싸지 않는것처럼
Suspense를 일으키는 모든 컴포넌트에 Suspense, ErrorBoundary를 붙이기보다는
적당한 부분단위로 에러와 로딩상태를 한번에 처리하게 됩니다.
또한 Suspense대신 useRecoilValueLoadable()함수를 통해서 명령적으로 처리할 수 있는 옵션도 제공합니다.

리코일팀은 문서에서 동시성 모드(Concurrent Mode)를 비롯한 다른 새로운 React의 기능들과의 호환을 강조하고 있습니다.
Suspense는 며칠전까지는 실험적기능이였지만 React 18이 릴리즈됨에 따라서 리액트의 정식 스펙이 되었습니다.
이런것을 보며 페이스북에서 개발하기 때문에 리액트의 개발 방향성을 따라가는것도 리코일의 장점이라고 느꼈습니다.

Reoil vs Redux

Recoil 장점

  • 비동기문제를 깔끔하게 처리할 수 있다.
    • 동기상태를 다루듯이 처리하고, 귀찮은 비동기상태 처리는 Suspense에 위임한다.
    • 코드의 복잡도를 줄일 수 있다.
  • 단순하다
    • 리덕스의 action, dispatch, reducer, store, 비동기를 위한 thunk, saga... 복잡하다 👎
    • 리코일은 atom, selector 두개가 사실상 끝이다. 단순하다 👍
    • 과연 리덕스 초심자에게 리덕스를 소개할때도 이 글처럼 짧게 설명할 수 있을까? 🤔
  • 리액트와 개발 방향성이 같다
    • 2년반 전에 등장한 리액트의 hooks가 처음 나왔을때를 생각해보라
    • 처음에는 Class기반 컴포넌트에 익숙해서 거부감이 있었다.
    • 하지만 hooks가 선언적인 API로 코드를 얼마나 단순하게 바꿔주었는지 생각해보라
    • recoil은 또한번 선언적인 API로 코드를 단순하게 바꾸려 하고있다.
  • 선언적 프로그래밍
    • 비동기처리를 위임받아 처리하는 컴포넌트를 JSX로 선언함으로써 모든 비동기 문제를 해결한다.
  • 내용이 길어져 글에 담지 못한 장점들
    • 캐싱기능으로 비동기 데이터 처리를 빠르게 함
    • 상태를 분산적으로 둘 수 있기 때문에 코드 스플리팅이 가능함

Recoil 단점

  • 페이스북이 중간에 버리지 않을까 하는 걱정
    • 레포지토리가 facebook/Recoil이 아니고 facebookexperimental/Recoil이다.
  • 실험적인 API들
    • recoil의 api중에는 아직도 _UNSAFE surfix가 붙은것이 많다
    • 하지만 빠른 속도로 UNSAFE 딱지가 떼지고 있다.
  • devtools의 부재
  • 관련 오픈소스 생태계가 redux에 비해서 부족하다
    • 아무래도 redux가 오래되고 사용기업이 많다보니 관련 오픈소스가 활발하다
    • localStorage와 연동하여 상태를 유지시키는 persist 관련 라이브러리를 보자
    • redux-persist ⭐️ 12K
    • recoil-persist ⭐️ 213
      • 기능이 좀 딸린다. migration이 없다

좋은 웹페이지 즐겨찾기