Intro

안녕하세요! Playce Dev 팀 Frontend Engineer 강동희 입니다. 매주 팀 내에서 주최하는 Frontend Tech-day 에서 새로운 Frontend Architecture 설계를 준비하고 있습니다. 이번주 주제는 상태 관리 매니저 선정이었습니다. 현재 Playce Dev 팀 Frontend 에서는 상태관리 라이브러리로 Redux 를 사용하고 있습니다. Redux 를 사용하면서 마주치게 된 가장 큰 어려움은 커다란 보일러 플레이트였고, 이를 해결하기 위한, 혹은 이를 대체할 수 있는 라이브러리가 있을까? 에서 조사를 시작하게 되었습니다. 그럼 Recoil 에 대해서 깊지도, 얕지도 않게 조사했던 내용을 여러분들과 함께 나눠보도록 하겠습니다! 부담없이 즐겨주세요!

Recoil Overview

Recoil : React 에서 사용하는 상태관리 라이브러리
Version : 0.72 (2022년 6월 16일 기준)
Git : https://github.com/facebookexperimental/Recoil

npm Trends..

Recoil 만을 위한 글이지만, 해당 기술을 탐구하기 전에 같은 문제를 해결하기 위해 사용되고 있는 라이브러리와 비교를 하는것은 상당히 중요한 일이라고 생각합니다.

프론트엔드 개발을 하면서 state 를 관리하는 방법은 여러가지 방법이 있습니다.

첫 번째로, 라이브러리를 통해 관리 하지 않고 직접 state 를 관리하는 방법입니다. 이 방법은 작은 프로젝트에서 유효할 수 있습니다. 하지만 규모가 있는 애플리케이션에서 직접 state 를 관리하게 된다면 props drilling 이 심각하게 발생하거나, state 가 어디서 관리되고 있는지 개발자도 모르는 등, 커다란 문제에 직면할 수 있습니다

두 번째는 React 에서 자체적으로 제공하고 있는 Context API 를 사용하는 것입니다. React 팀은 16.3 버전에서 라이브러리를 사용하지 않아도 자체적으로 전역 상태를 관리할 수 있는 Context API 를 출시 했습니다. 그런데 React 팀에서 공식적으로 출시한 상태관리 API 가 있음에도 불구하고, 왜 많은 프로젝트에서 Redux 와 같은 상태 관리 라이브러리를 사용할까요? 그 이유는 Context API 를 사용할 때 익혀야 하는 기본적인 개념과 작성해야 하는 코드의 길이가 Redux 의 보일러 플레이트와 크게 차이나지 않기 때문입니다. 그렇기에 기존의 코드에서 벗어나 Context API 를 사용할 필요가 없는 것입니다.

세 번째는 상태관리 라이브러리의 사용입니다. React 에서 흔히 사용 되는 상태 관리 라이브러리는 react-redux 입니다. mobx 또한 많이 사용되지만, 이번 tech-day 에서 제외하기로 했기 때문에 조사하지 않았습니다. 마지막으로 비교적 최근에 나온 Recoil 은 페이스북에서 출시한 React 만을 위한 상태 관리 라이브러리 입니다.

npm trends
npm 트렌드

사실 비교하는 게 무의미 할 정도의 격차를 살펴볼 수 있습니다. 라이브러리 간 격차가 왜 이렇게 두드러지게 보일까요?

  1. 출시일 Recoil 은 2020년 5월에 출시된 State Management Library 이다. Redux 7년 전에 개발된 라이브러리다. (세월의 차이)
  2. 익숙함 개발을 하는데 있어서 중요한 건 익숙함이다. 익숙함이란 숙련도 라고도 이야기 할 수 있을 것 같은데, Redux 가 오래된 만큼 팬 층이 깊고 숙련도 깊은 사람들이 많기 때문이다. 즉 요즘 말로 고인물 들이 많다.

개인적으로 두 가지 이유를 생각해 봤습니다.

이렇게 큰 격차가 남에도 불구하고, 해당 글에서는 아키텍처에 Recoil 을 사용하도록 여러분들을 설득하기 위해서 작고 귀여운 Recoil 에 대해서 탐구하고 소개해 보도록 하겠습니다.

React 에서 데이터의 흐름

React 가 추구하는 패턴

React 에서 데이터는 단방향으로 흐르며, 위에서 아래로 즉 부모에서 자식 컴포넌트로 흐릅니다. 이러한 방식은 Flux 패턴에 의해서 적용된 방식인데, 간단하게 Flux 패턴이 왜 적용이 되었는지 알아보도록 하겠습니다.

MVC 패턴

Flux 패턴은 MVC 패턴이 가진 문제점에서 시작했습니다.

mvc-simple
출처 : Flux 공식 이미지 | 단순 MVC 패턴

위 사진은 기존의 MVC 패턴입니다. Model 에 데이터를 정의해 놓고, Controller 를 통해서 Model 의 데이터를 CRUD 작업하며, 변경된 데이터를 View 에 출력하는 식의 패턴입니다.

MVC 패턴은 Web 개발을 하는데 가장 많이 사용 되는 패턴입니다. 하지만 MVC 패턴에도 다양한 문제점이 존재합니다.

mvc-complex
출처 : Flux 공식 이미지

Web 애플리케이션이 커지면서 정의된 Model 과 이를 출력하는 View 가 다양해 졌습니다. 위 사진을 살펴보면 데이터의 흐름이 정말 많다는 것입니다. 어떤 데이터가 변경 되었을 시 해당 데이터를 사용하는 모든 곳에서 코드를 작성하고 변경해줘야 합니다. 만약 이런 과정을 생략 했을 시 예측하지 못한 부분에서 오류가 발생하거나 다르게 동작하는 등 Side Effect 가 발생할 수 있습니다.

Flux 패턴

flux
출처 : https://haruair.github.io/flux/docs/overview.html

페이스북에서는 이 같은 문제를 해결하기 위해서 Flux 패턴을 출시 했습니다.

간단하게 살펴보자면, Action 은 데이터의 상태를 변경하는 명령어입니다. Dispatcher 은 Action을 감지하여 Store 에 Action 을 전달해주는 역할을 합니다. Model 은 Store라고도 볼 수 있는데 state 가 저장되어 있는 공간입니다. Dispatcher 을 통해서 가져온 Action을 확인해 내부에 저장된 데이터를 변경합니다. 마지막으로 View 는 React 를 통해서 만드는 코드들입니다. Model 에 저장된 데이터를 가져와서 View 에 뿌려주고, View 는 해당 데이터들을 가지고 와서 화면에 렌더링 합니다.

쉽게 이해하기 위해서 보고 체계를 예시로 들어보겠습니다. 사령부에서 지시 (Action) 가 내려오고, 집배원 (Dispatcher) 이 해당 지시를 가지고 내려옵니다. 해당 지시를 통해서 병사는 창고 (Store) 에 저장된 물건 (데이터 혹은 state) 을 변경하여 꺼내서 훈련할 때 사용합니다. (View 에 렌더링 하는 과정을 비유)

Redux

Redux 는 Flux 패턴을 적용한 상태 관리 라이브러리 입니다. Store 에 상태들을 저장하고, 해당 어떠한 변화가 필요할 때 Action 을 Dispatch 하여 Reducer 에서 이를 받아 정해놓은 흐름으로 상태를 변화시키는 방식입니다.

redux
출처 : http://kimstar.kr/7600/

왜 Redux 대신 Recoil 을?

위에서 살펴본 대로 Redux 는 React 에서 추구하는 데이터의 흐름을 그대로 구현해 놓은 라이브러리 입니다. 그렇다면 상태 관리 라이브러리를 선택할 때 Redux 대신 Recoil 을 선택할 이유를 생각해봅시다!

  1. Redux의 복잡한 코드
    Redux 를 사용하고자 할 때 마주하는 가장 큰 어려움은 복잡한 코드다. Redux 를 활용하기 위해서는 action, dispatcher, reducer, store 등 구현해야 할 기본 코드 들이 큰 편이다. 이는 보일러 플레이트를 활용해서 해결할 수 있는 문제지만, 만약 여러 개발자가 공동 작업 할 때 컨벤션을 적용하지 않고 코드를 작성할 경우 자기만 알아볼 수 있는 구조의 코드를 작성하게 된다.
  2. 간단한 Recoil 의 개념
    Redux 를 이해하고 사용하려면 공부해야 할 것들이 많다. 데이터의 흐름을 추상화 하여서 익히려고 하여도 여러가지 복잡한 흐름을 이해하는 건 쉽지 않다. 이에 비해서 Recoil 에서 state 를 관리하는 방법은 굉장히 간단해 보인다.
  3. 쉽게 사용하는 비동기 로직
    Redux 에서 비동기를 활용하기 위해서는 middleware 을 활용한다. 비동기 통신을 한다면 통신의 결과가 Success 일수도 있고 Fail 일 수도 있다. 이를 구분하여 state 관리를 해야하는데, 이를 쉽게 하기 위해서 Redux 에서는 Redux-thunk 혹은 Redux-saga 같은 미들웨어를 활용한다. 하지만 Recoil 에서는 내장된 개념인 selector 을 활용해 추가적인 미들웨어의 사용 없이 쉽게 비동기 로직을 구현할 수 있다.

크게 위의 세 가지 이유를 생각해봤습니다. 자, 그럼 이제 여러분들이 Recoil 에 대해서 느껴볼 시간입니다!

Recoil 이 자신을 소개하는 문구

작고 React 스러운
” Recoil은 React처럼 작동하고 생각합니다. 앱에 추가하여 빠르고 유연한 공유되는 상태를 사용해 보세요. “

데이터 흐름 그래프
” 파생 데이터와 비동기 쿼리는 순수 함수와 효율적인 구독으로 관리됩니다. “

교차하는 앱 관찰
” 코드 분할을 손상시키지 않고 앱 전체의 모든 상태 변경을 관찰하여 지속성, 라우팅, 시간 이동 디버깅 또는 실행 취소를 구현합니다. ”

Recoil 시작하기

Recoil 을 시작하기 위해선 index.tsx (혹은 index.jsx) 에서 렌더링 하고 있는 root 를 RecilRoot 를 통해서 감싸줘야 합니다. 마치 Redux 에서 <Provider> 를 통해서 App 에 store 을 연결해주는 것과 비슷한 과정입니다. Redux 에서는 하나의 store 를 연결해주는 과정이지만, Recoil 에서는 atom 들이 떠다니는 Root 를 설정해준다고 추상화 하면 좋을 것 같습니다.

import * as ReactDOMClient from "react-dom/client";
import { RecoilRoot } from "recoil";
import App from "./App";

const rootElement = document.getElementById("root");
const root = ReactDOMClient.createRoot(rootElement);

root.render(
  <RecoilRoot>
    <App />
  </RecoilRoot>
)
;

Recoil 의 핵심 개념

atom

Recoil 의 첫 번째 핵심 개념은 atom 입니다.

간단히 이해하고자 한다면 atom 을 비눗방울로 추상화 할 수 있습니다. 우리가 만드는 Web Application 을 구조화 한다면 그 구조의 상단에 atom 이 비눗방울 처럼 둥둥 떠다니고 있다고 추상화 할 수 있겠습니다. 만약 개발을 하다가 어떤 비눗방울 (상태) 이 필요하다면 해당하는 비눗방울만 쏙 빼서 쉽게 사용할 수 있습니다.

atom youtube
페이스북에서 소개하는 리코일 영상 보러가기>

페이스북에서 Recoil 을 설명해주는 유튜브 영상에서 캡쳐해 온 화면 입니다. 위의 1,2,3,4,5,6 방울들이 atom 이며 아래 구조화 되어있는 우리의 Web 에서 해당하는 atom 을 사용하고 있습니다.

“오케이, 그럼 atom 은 알았는데 atom 에 무엇을 담는데?”

atom 에는 우리가 사용할 상태 (state) 를 담습니다. 쉽게 말하면, 우리가 전역적으로 사용하길 원하는 state 를 atom 이라는 비눗방울로 띄어서 어디서든지 사용할 수 있도록 만드는 것 입니다.

import { atom } from "recoil"

export const user = atom({
  key: "user",
  default: {
    id: "Admin",
    pwd: "Admin",
  },
});

export const counting = atom({
  key: "counting",
  default: 0,

});

위 코드를 살펴보면, recoil 에서 atom 을 import 해왔고, atom 이라는 함수에 key 와 default 로 이루어진 객체를 넣어줌으로써 atom 을 만들었습니다.

여기서 key 는 atom 을 구분해줄 고유의 값이며, default 는 해당 key 값을 가진 atom 의 기본값으로 설정해줄 value 를 넣어주면 됩니다.

이렇게 간단한 코드로 하나의 전역 상태를 만들었습니다.

“오케이, 그럼 atom 을 어떻게 가져다 쓸 수 있는데?”

다섯 줄의 코드로 전역 상태를 만든 혁신적인 방법에 깜짝 놀랄 여러분을 위해 바로 코드를 통해 살펴보도록 하겠습니다.

import { useRecoilState } from "recoil";
import { counting } from "./store";

export function Example() {
  const [count, setCount] = useRecoilState(counting);
  const handleIncrease = () => {
    setCount((prev) => prev + 1);
  }
  return (
    <div>
      <span>{count}</span>
      <button onClick={handleIncrease}>increase</button>
    </div>
  );

}

위 코드를 살펴봤을 때 어떻게 atom 을 사용한 지 확인해보세요! 코드를 살펴봤을 때 useState 를 통해서 state 를 사용하는 모습과 비슷한 느낌입니다!

useRecoilState 라는 hook 을 recoil 라이브러리에서 가져와, 위에 정의한 atom 을 넣어주면서 값을 추적해 사용합니다.

이렇게 recoil 은 React 만을 위한 상태관리 라이브러리로서 가장 React 다운 상태관리 툴을 제공하고 있습니다. 그렇기에 useRecoilState 라는 간단한 hook 을 통해서 atom 을 가져와서 값을 추적하고, 값을 변경할 수 있는 것 입니다.

이렇게 짧은 과정을 통해서 전역 상태 관리를 하게되니, Redux 에서 사용했던 방식 (store 에 저장된 값을 가져오고, action 을 dispatch 해 reducer 를 통해서 state 를 변경 했던 과정) 에 아쉬움을 느끼게 됩니다.

import { useSetRecoilState, useRecoilValue } from "recoil";
import { counting } from "./store";

export function Example() {
  const setCount = useSetRecoilState(counting);
  const count = useRecoilValue(counting);
  const handleIncrease = () => {
    setCount((prev) => prev + 1);
  }
  return (
    <div>
      <span>{count}</span>
      <button onClick={handleIncrease}>increase</button>
    </div>
  );
}

위 컴포넌트와 별 다른 구조는 없지만 atom 과 atom 의 modifier 를 분리해서 사용할 수 있다는 것을 소개하고자 예제 코드를 작성했습니다. 각각 useRcoilValue, useSetRecoilState 를 활용하면 이들을 분리해서 사용할 수 있게 됩니다.

atom with typescript

타입스크립트를 적용한 간단한 예제를 살펴보도록 하겠습니다!

//atom.ts
import { atom } from "recoil";

export interface IUser {
  id: string;
  pwd: string;
  name: string;
}

export const user = atom<IUser>({
  key: "user",
  default: {
    id: "admin",
    pwd: "admin",
    name: "관리자"
  }
});

먼저 atom 을 타이핑 하기 위해서 IUser 이라는 interface 를 선언하였고, 해당 interface 를 user 이라는 atom 에 typing 했습니다.

import { useRecoilState } from "recoil";
import { IUser, user } from "./atom";

export default function App() {
  const [LoginUser, setLoginUser] = useRecoilState<IUser>(user);
  return (
    <div>
      <p>userName: {LoginUser.name}</p>
      <p>userId: {LoginUser.id}</p>
      <p>userPwd: {LoginUser.pwd}</p>
    </div>
  );
}

컴포넌트에서 atom 을 사용하는데 있어서도 마찬가지로 IUser 인터페이스와 타이핑 했습니다. 이는 React 에서 typescript 를 적용해 useState 를 활용하는 방법과 거의 유사합니다.

selector

Recoil 의 두 번째 핵심 개념은 selector 입니다.

저는 처음 Recoil 을 공부 했을 때 atom 은 정말 쉽게 이해했지만, selector 라는 개념은 이해하기가 어려웠습니다. 그 이유는 atom 에 비해서 조금은 머릿속에 추상화 하기 어려웠기 때문이지 않을까 라는 생각을 해봅니다. 결국 selector 을 이해하기 위해서 추상화 했던 방법은 SQL 의 select 질의문을 활용해 봤습니다.

selector 은 atom 을 활용해 개발자가 원하는 대로 값을 뽑아서 사용할 수 있는 API 입니다. Recoil 의 공식 문서에서 selector 을 소개하는 문구를 살펴보겠습니다.

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

즉 “ atom 을 원하는 대로 변형해 값을 리턴받는다. ” 라고 생각할 수 있겠습니다. 이 과정을 마치 데이터베이스에서 저장된 데이터를 Select 을 통해 원하는 결과를 뽑아오는 과정으로도 유추 해볼 수 있겠다 라는 생각을 하였고, 이를 Select 과 연결하여 추상화 하니 이해하기 편했습니다.

또한 selector 은 readonly 한 값 만을 반환합니다. 따라서 Recoil 을 활용할 때 수정 가능한 값을 반환 받고자 한다면 반드시 atom 을 활용해야 합니다.

selector 의 구조

function selector<T>({
  key: string,
  get: ({
    get: GetRecoilValue,
    getCallback: GetCallback,
  }) => T | Promise<T> | RecoilValue<T>, // 타입 T에 해당하는 값, T를 리턴하는 Promise,
  set?: (
    {
      get: GetRecoilValue,
      set: SetRecoilState,
      reset: ResetRecoilState,
    },
    newValue: T | DefaultValue, // setter로 전달하는 값은 T 타입 값이어야 한다.
  ) => void,
}
)

간단한 예제를 통해서 selector 을 확인해보자.

구조
  1. toDo 는 “DOING”, “DONE” 두 가지 상태를 가지고 있다. 현재 노출하기 원하는 상태는 atom 으로 관리한다.
  2. 생성된 toDo 는 현재 atom 에 배열로 담겨있다.
  3. 우리는 selector 을 통해서 status atom 을 추적하고, 원하는 상태의 값 만을 쏙쏙 골라올 것이다.
// atom.ts
import { atom, selector } from "recoil";

export type status = "DONE" | "DOING";
interface toDo {
  status: status;
  contents: string;
}

export const selectStatus = atom<status>({
  key: "nowStatus",
  default: "DOING"
});

export const toDos = atom<toDo[]>({
  key: "toDos",
  default: [
    { status: "DOING", contents: "default 1" },
    { status: "DONE", contents: "default 2" },
    { status: "DONE", contents: "default 3" },
    { status: "DOING", contents: "default 4" },
    { status: "DOING", contents: "default 5" }
  ]
});

export const selectToDo = selector<toDo[]>({
  key: "selectToDos",
  get: ({ get }) => {
    const originalToDos = get(toDos);
    const nowStatus = get(selectStatus);
    return originalToDos.filter((toDo) => toDo.status === nowStatus);
  }
});

위 코드를 살펴보면 toDos 라는 atom 에 toDo 배열을 담아놨고, selector 을 통해서 변화된 값을 리턴받아 사용하고 있습니다.

selector 의 구조를 살펴보면, atom 과 다른 부분이 있습니다. 위 코드에서 바로 get 이라는 코드를 살펴볼 수 있는데, selector 은 내부적으로 함수에서 get 을 반환 해주며 get 메서드를 활용해 현재 저장된 atom 이나 다른 selector 의 값을 받아올 수 있습니다. 이를 통해서 atom 을 input 받고 원하는 결과를 위해 배열을 변형해 output 해줍니다.

// App.tsx
import React from "react";
import { useRecoilState, useRecoilValue } from "recoil";
import { selectStatus, selectToDo, user } from "./atom";

export default function App() {
  const [status, setStatus] = useRecoilState(selectStatus);
  const selectToDos = useRecoilValue(selectToDo);
  const handleStatus = (event: React.ChangeEvent<HTMLSelectElement>) => {
    setStatus(event.currentTarget.value as any);
  };
  return (
    <>
      <div>
        <select value={status} onChange={handleStatus}>
          <option value="DOING">DOING</option>
          <option value="DONE">DONE</option>
        </select>
        <ul>
          {selectToDos.map((toDo, index) => {
            return (
              <li key={index}>
                <span>status: {toDo.status}</span>
                <br />
                <span>content: {toDo.contents}</span>
              </li>
            );
          })}
        </ul>
      </div>
    </>
  );
}

UI 컴포넌트 입니다. <select> 태그를 활용해 status atom 을 변경해주고 있으며, selector 을 통해서 toDo 배열을 화면에 뿌려주고 있습니다. 

selector 의 또 다른 기능 set

selector 은 get 뿐만 아니라 set 을 활용해서 atom 의 값을 변경해줄 수 있습니다. 이를 통해서 비동기 통신 처리를 할 수도 있습니다. 간단히 위 toDo 예제를 활용해 set 을 활용한 모습을 살펴보겠습니다.

// atom.ts
export const selectToDo = selector<toDo[]>({
  key: "selectToDos",
  get: ({ get }) => {
    const originalToDos = get(toDos);
    const nowStatus = get(selectStatus);
    return originalToDos.filter((toDo) => toDo.status === nowStatus);
  },
  set: ({ set }, newToDo) => {
    set(toDos, newToDo);
  }
}
);

atom.ts 에서 변화한 코드는 selectToDo 의 set 부분입니다. get 을 활용 했을때와 마찬가지로 set 메서드를 인자로 받아와 사용합니다. 또한 인자로는 컴포넌트에서 넣어줄 newValue (여기서는 newToDo) 를 받을 수 있습니다. set 메서드의 첫 번째 매개변수로 변경할 atom 을 넣어주고 두 번째 인자로는 변경해 줄 값을 넣어줍니다.

import React, { useState } from "react";
import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
import { selectStatus, selectToDo, toDo, toDos } from "./atom";

export default function App() {
  const [status, setStatus] = useRecoilState(selectStatus);
  const selectToDos = useRecoilValue(selectToDo);
  const toDoAtom = useRecoilValue(toDos);
  // 아래가 selector 의 set 을 위해 추가된 코드
  const [contents, setContents] = useState("");
  const setNewToDos = useSetRecoilState(selectToDo);
  const handleStatus = (event: React.ChangeEvent<HTMLSelectElement>) => {
    setStatus(event.currentTarget.value as any);
  };
  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setContents(event.currentTarget.value);
  };
  const handleSubmit = (event: React.ChangeEvent<HTMLFormElement>) => {
    event.preventDefault();
    if (contents === "") {
      return;
    } else {
      const newToDoList: toDo[] = [
        ...toDoAtom,
        {
          contents,
          status: "DOING"
        }
      ];
      setNewToDos(newToDoList);
      setContents("");
    }
  };
  return (
    <>
      <form onSubmit={handleSubmit}>
        <input value={contents} onChange={handleInputChange} />
        <button>Submit</button>
      </form>
      <br />
      <div>
        <select value={status} onChange={handleStatus}>
          <option value="DOING">DOING</option>
          <option value="DONE">DONE</option>
        </select>
        <ul>
          {selectToDos.map((toDo, index) => {
            return (
              <li key={index}>
                <span>status: {toDo.status}</span>
                <br />
                <span>content: {toDo.contents}</span>
              </li>
            );
          })}
        </ul>
      </div>
    </>
  );
}

컴포넌트 예제 코드입니다. 윗 예제의 코드들과 겹쳐서 긴 코드가 들어왔습니다. useSetRecoilState 훅을 통해서 selector 의 set 을 활용할 수 있는데, set 을 활용할 때는 인자로 변경할 값을 넣어주면 됩니다. <form> 의 submit 이벤트 내부에서 selector 의 set 을 통해서 toDos 라는 atom 을 변경해 줬습니다.

아래에서 이를 활용한 비동기 통신 예제를 살펴보도록 하겠습니다!

selector 을 활용한 비동기 통신

Recoil 을 활용해 비동기 통신을 하기 위해서는 selector 을 사용해야 합니다. 간단하게 코드를 통해서 알아보겠습니다.

export const selectId = atom({
  key: "selectId",
  default: 1
});

export const selectingUser = selector({
  key: "selectingUser",
  get: async ({ get }) => {
    const id = get(selectId);
    const user = await fetch(
      `https://jsonplaceholder.typicode.com/users/${id}`
    ).then((res) => res.json());
    return user;
  },
  set: ({ set }, newValue) => {
    set(nowUser as any, newValue);
  }
});

위 코드를 살펴보면 get 메서드를 통해서 API 콜을 하여서 데이터를 가져옵니다.

위 코드에서는 동적 parameter 을 위해서 id 라는 atom 을 사용합니다. 그럼 atom 을 사용하지 않고 컴포넌트에서 직접 파라미터를 넘겨주는 방법은 없을까요?

export const selectUser = selectorFamily({
  key: "selectOne",
  get: (id: number) => async () => {
    const user = fetch(
      `https://jsonplaceholder.typicode.com/users/${id}`
    ).then((res) => res.json());
    return user;
  }
});

// 컴포넌트에서 사용 시 
const user = useRecoilValue<IUser>(selectUser(id));

위 코드를 살펴보면 selectorFamily 를 활용해 비동기 통신을 하고 있는 것을 확인할 수 있습니다. 컴포넌트에서 동적 parameter 을 위해서 직접 매개변수를 넘겨주고 싶다면 selectorFamily 를 활용해 get 메서드 내부에서 직접 파라미터를 받아줄 수 있습니다.

selector 의 강력한 기능

selector 을 활용해 비동기 통신을 하였을 때 체감되는 강력한 기능은 캐싱 입니다. selector 을 활용해 비동기 통신을 해온다면, 내부적으로 알아서 값을 캐싱 해주기 때문에 이미 한번 비동기 통신을 하여 값이 캐싱되어 있다면 매번 같은 비동기 통신을 하지 않고 캐싱 된 값을 추적하여 사용합니다.

예제

selector 을 활용한 비동기 통신을 작성하며 간단한 예제를 만들어 봤습니다.

Recoil 을 활용 했으며, 초기 데이터 로딩에서는 react-query 를 활용해 봤습니다.

바로가기 >

Suspense Trouble Shooting

selector 을 활용해 비동기 통신을 연습하는 과정에서 해당 에러를 확인했습니다.

suspense error

위 에러는 비동기 통신을 할 때 데이터가 아직 도착하지 않았을 경우 (isLoading 상태) 대체해서 보여줄 UI 가 없다는 뜻입니다. 이를 해결하기 위해서는 아래 세 가지 방법이 있습니다.

  1. <React.Suspense> 활용
    가장 보편적인 방법입니다. index.tsx 에서 <App> 컴포넌트를 <Suspense /> 태그로 감싸줬습니다.
    root.render( <RecoilRoot> <Suspense fallback={<div>loading..</div>}> <App /> </Suspense> </RecoilRoot> );
  2. Recoil 의 Loadable 활용
    Recoil 에서는 atom 이나 selector 의 현 상태를 나타낼 수 있는 hook 을 제공합니다.
    const userLoadable = useRecoilValueLoadable(getUser);
    Loadable 을 객체는 hasValue , hasError , loading 세 가지 상태를 가지고 있습니다. 이에 따라서 컴포넌트의 상태를 표현해 줄 수 있습니다.
  3. startTransition 활용
    이번 React 18 버전에서 Transition 이라는 기능이 나왔습니다. Transition 에 관한 내용은 아래 블로그 글에서 확인하시면 좋을 것 같습니다.
    바로가기 > 

Recoil 을 활용했을 때 느낀점

Recoil 을 사용해 보고 난 후 Recoil 을 출시하면서 Recoil 팀이 의도 했던 내용을 느껴볼 수 있었습니다.

  1. 가장 React 스러운 상태관리 라이브러리
    Recoil 의 핵심 기능은 hook 을 활용하여 100% React 답게 활용할 수 있었습니다.
  2. 낮은 진입장벽
    Recoil 을 처음 배울 때 느꼈던 어려움은 거의 없었습니다. 아마 현재 사용하는 상태 관리 라이브러리 중 제일 쉽게 배울 수 있지 않을까 생각합니다.
  3. DevTool 의 부재
    많은 사람들이 꼽는 가장 아쉬운 점은 DevTool 의 부재입니다. Redux 에서는 DevTool 을 제공하고 있기 때문에 편리하게 state 를 시각화 하여 관리할 수 있습니다. 현재 Devtool 관련 부분은 Recoil 팀에서 테스트 하며 출시를 준비하고 있는 듯 합니다. 

마무리하며

긴 글을 통해서 Recoil 의 전반적인 내용을 살펴 봤습니다. 100% 에 가까운 내용을 살펴볼 순 없었지만, 대부분의 내용을 담았다고 생각합니다. 공식 문서도 한글화가 잘 되어있고, 출시 팀이 Facebook 이기 때문에 앞으로의 발전될 기능을 기대해보며 아키텍처에 담아봐도 좋을 것 같다는 생각을 합니다.

이어지는 글 살펴보기

  1. Recoil With Storage (feat. effects)
  2. Recoil 정확하게 사용하기! (feat. selector)
  3. React-Query 도입을 위한 고민 (feat. Recoil)
강동희
Developer

오픈소스컨설팅 Playce Dev Team 소속으로 오늘을 바탕으로 내일이 더 기대되는 프론트엔드 개발자 강동희입니다. 빠르게 변화하는 기술들에 관심이 많으며 지식 공유에 큰 가치를 두고 있습니다.

Leave a Reply

Your email address will not be published. Required fields are marked *

One reply on “Recoil, 리액트의 상태관리 라이브러리”

  • 박준우
    2022년 06월 21일 at 8:51 am

    Recoil을 막 도입하고 있는 입장에서 무척 도움이 되었습니다 감사합니다.