Recoil에 대해서 가볍게 톺아보기

adultlee·2023년 11월 19일
4

NDD

목록 보기
8/16

NDD 프로젝트의 FE에서는 recoil을 도입하게 되었습니다!!
저는 지금까지 mobx와 redux만을 사용해 왔기 때문에, recoil에 대해서는 새롭게 학습이 필요했습니다.
그에 따라 (정말 정말) 가볍게 톺아보려 합니다!

Recoil이란?

Recoil은 React 애플리케이션을 위한 상태 관리 라이브러리입니다. 이는 React의 기본적인 상태관리 영역에서 확장되어, 특히 복잡하고 대규모의 서비스에서의 효율적인 상태관리를 가능하게 합니다. Recoil은 Facebook에서 개발되었으며, React의 기존 개념과 잘 통합되도록 설계되었습니다!

바로 공식문서를 들어가 보도록 하겠습니다!

들어가자 마자 하단의 세 요소가 눈에 띕니다!

공식문서 홈의 세 요소가 아무래도 recoil의 테마를 결정하는 주요한 기능인듯합니다. 이를 중심으로 학습하며 기존에 제가 학습해오던 mobx와 비교하면 좋을듯 합니다!

작고 React스러운 (Small and React-ish)

요 귀여운 애니메이션이 떠오르는 제목인듯합니다...!

해당 제목의 주된 내용은 크게 두가지라고 생각합니다. 먼저 "작고"라는 부분은 recoil이 가볍게 도입할 수 있으며, 큰 러닝 커브를 요하지 않음을 의미하는듯 합니다.
두번째로 "React스러운" 이라는 부분은 당연히 Facebook에서 만든 상태관리 라이브러리이다 보니, 그 연결을 곤고히 하기 위해 도입되었다고 생각할 수있는데 아마도 이를 강조하는듯 합니다.

React의 철학을 따르는 상태 관리

Recoil은 React의 주요 철학인 선언적 UI, 컴포넌트 중심의 설계, 그리고 훅스(Hooks)를 사용하는 방식을 기반으로 합니다.
React에서 상태 관리를 위해 훅스를 사용하는 방식은 매우 직관적이고 간결합니다. Recoil도 이러한 패턴을 따릅니다.
아래는 정말 간단하게 recoil을 통하여 상태관리를 진행하는 컴포넌트를 보여줍니다.

import React from 'react';
import { atom, useRecoilState } from 'recoil';

// Atom 정의: 전역 상태를 나타내는 단위
const textState = atom({
  key: 'textState', // 고유한 키
  default: '', // 기본값
});

function TextInput() {
  const [text, setText] = useRecoilState(textState); // 사실상 useState와 동일한 역할

  const onChange = (event) => {
    setText(event.target.value);
  };

  return <input type="text" value={text} onChange={onChange} />;
}

위 예시에서 textState는 전역적으로 공유되는 상태(atom)이며, TextInput 컴포넌트는 이 상태를 사용하여 데이터를 렌더링하고 업데이트합니다. 이 과정은 React의 useState 훅스를 사용하는 것과 매우 유사하며, Recoil은 이러한 일관성을 통해 React 개발자들에게 친숙한 경험을 제공합니다.

간결함과 효율성

Recoil은 상태 관리를 위한 정말 최소한의 API만을 제공함으로써 러닝커브를 낮추고, 효율적인 개발 경험을 가능하게 합니다.

Recoil의 Selector는 Atom에서의 파생 상태를 생성하기 위한 간결하고 강력한 방법을 제공합니다. 이는 React의 컴포넌트 렌더링 최적화와 유사한 개념을 상태 관리에 적용합니다.

import { selector } from 'recoil';

const charCountState = selector({
  key: 'charCountState',
  get: ({ get }) => {
    const text = get(textState);
    return text.length;
  },
});

function CharacterCount() {
  const count = useRecoilValue(charCountState);

  return <>Character Count: {count}</>;
}

위 예시에서 charCountState는 textState의 변경에 의존하는 파생 상태(selector)입니다. 이는 순수 함수로 정의되며, textState의 값이 변경될 때만 재계산됩니다. 이 방식은 React의 선언적 프로그래밍 패러다임을 따르면서, 상태 관리에서도 불필요한 계산을 최소화합니다.

데이터 흐름 그래프(Data-Flow Graph)


"데이터 흐름 그래프 (Data-Flow Graph)"는 서비스의 상태와 그 상태(state)에 의존하는 파생 데이터 간의 관계를 나타냅니다. 이는 데이터의 흐름과 업데이트를 명확하게 관리하고 감시할수 있으며, 이를 효율적으로 처리할 수 있게 합니다.

데이터 흐름 그래프는 상태(Atoms)와 이 상태에 의존하는 파생 데이터(Selectors) 간의 관계를 나타냅니다. 각 Atom은 그래프의 노드로, Selector는 Atom 노드에 의존하는 다른 노드로 표현됩니다. Atom의 상태가 변경되면, 이 변경은 의존하는 모든 Selector에 전파됩니다. Selector는 Atom의 새로운 상태를 기반으로 다시 계산되며, 이를 통해 관련된 컴포넌트가 업데이트됩니다. Selector는 필요할 때만 재계산됩니다. Atom의 상태가 변경되어도, 해당 Selector에 영향을 주지 않는 변경인 경우 Selector는 재계산되지 않습니다. 이는 불필요한 계산을 방지하고 성능을 최적화합니다.

// atom 은 text의 상태를 의미합니다. 
const textState = atom({
  key: 'textState',
  default: '',
});
// 이 Atom은 사용자가 입력하는 텍스트를 저장합니다. 이는 애플리케이션의 상태를 나타내는 기본 단위입니다.

// selector는 atom으로 변경되는 파생 값을 의미합니다.

const charCountState = selector({
  key: 'charCountState',
  get: ({ get }) => {
    const text = get(textState);
    return text.length;
  },
});

이 Selector는 textState Atom에 의존합니다. 사용자가 텍스트를 입력할 때마다 textState의 상태가 변경되고, charCountState는 이 변경을 감지하여 문자 수를 다시 계산합니다.

컴포넌트에서의 상호작용

function TextInput() {
  const [text, setText] = useRecoilState(textState);
  // ... 입력 처리 로직
  return <InputArea value={text} onChange(setText)>
} // 여기서 TextInput은 text를 구독합니다. 구독에 대한 내용은 아래 후술합니다.
 
function CharacterCount() {
  const count = useRecoilValue(charCountState);
  // ... 문자 수 표시 로직
  return <div>{count}</div>
}

TextInput 컴포넌트는 textState를 업데이트하고, CharacterCount 컴포넌트는 charCountState를 사용하여 계산된 문자 수를 표시합니다. 이는 Recoil이 데이터 흐름을 어떻게 관리하는지 보여줍니다.

recoil에서의 구독이란

Recoil에서의 "구독(subscription)"은 애플리케이션의 상태(atom) 또는 파생 데이터(selector)의 변경을 감지하고, 이러한 변경에 반응하는 개념을 말합니다. Recoil의 핵심 기능 중 하나는 상태의 변경을 구독하는 컴포넌트에게 효율적으로 알리는 것입니다. 이 구독 메커니즘은 React의 컴포넌트 렌더링 최적화와 상호 작용합니다.

Recoil에서 atom 또는 selector의 상태가 변경되면, Recoil은 이 상태를 구독하고 있는 모든 컴포넌트에게 알립니다. 상태를 구독하는 컴포넌트는 상태의 변경을 감지하고, 이에 따라 ReRendering됩니다. 이는 상태가 변경될 때마다 UI가 최신 상태를 반영하도록 보장합니다. Recoil은 필요한 컴포넌트만 업데이트되도록 최적화하여, 불필요한 Rendering을 방지합니다. 예를 들어, 특정 atom에 대한 변경이 일부 컴포넌트에만 영향을 미친다면, Recoil은 해당 컴포넌트만을 ReRendering합니다.

function TextInput() {
  // textState atom을 구독하고, 상태 변경 시 재렌더링됩니다.
  const [text, setText] = useRecoilState(textState);

  // ... 컴포넌트 로직
}

위 코드에서 TextInput의 textState를 구독합니다. 사용자가 인터페이스를 통해 textState를 변경하면, TextInput는 새로운 text 값으로 재렌더링됩니다.

Recoil에서의 구독 개념은 상태 관리의 핵심으로, 애플리케이션의 상태가 변경될 때 필요한 부분만을 효율적으로 업데이트하는 데 중요한 역할을 합니다. 이를 통해 개발자는 상태 변화에 따라 동적으로 반응하는 반응형 UI를 쉽게 구현할 수 있습니다.

Recoil의 다양한 hook들

1.useRecoilState

useRecoilState 훅은 Recoil의 atom에 저장된 상태를 읽고 쓰는 데 사용됩니다. 이는 React의 useState 훅과 유사한 API를 가집니다.

import { atom, useRecoilState } from 'recoil';

// Atom 정의
const textState = atom({
  key: 'textState', // 고유한 키
  default: '', // 초기값
});

function TextInput() {
  const [text, setText] = useRecoilState(textState);

  const onChange = (event) => {
    setText(event.target.value);
  };

  return <input type="text" value={text} onChange={onChange} />;
}

2. useRecoilValue

useRecoilValue 훅은 Recoil의 atom 또는 selector의 값을 읽는 데 사용됩니다. 이 훅은 상태를 읽기 전용으로 사용할 때 유용합니다.

import { useRecoilValue } from 'recoil';

function TextDisplay() {
  const text = useRecoilValue(textState);

  return <div>{text}</div>;
}

3. useSetRecoilState

useSetRecoilState 훅은 Recoil 상태를 업데이트하는 함수만을 제공합니다. 이 훅은 상태를 변경할 수 있지만, 현재 상태 값을 읽을 수는 없습니다.

import { useSetRecoilState } from 'recoil';

function TextUpdater() {
  const setText = useSetRecoilState(textState);

  const onClick = () => {
    setText('Hello, Recoil!');
  };

  return <button onClick={onClick}>Update Text</button>;
}

4. useResetRecoilState

useResetRecoilState 훅은 Recoil의 atom 상태를 초기값으로 재설정하는 데 사용됩니다.

import { useResetRecoilState } from 'recoil';

function ResetButton() {
  const resetText = useResetRecoilState(textState);

  return <button onClick={resetText}>Reset</button>;
}

5. useRecoilValueLoadable

useRecoilValueLoadable 훅은 비동기 selector의 상태를 관리하는 데 사용됩니다. 이 훅은 데이터의 로딩, 에러, 완료 상태를 쉽게 처리할 수 있게 해줍니다.

import { selector, useRecoilValueLoadable } from 'recoil';

// 비동기 Selector 정의
const asyncDataState = selector({
  key: 'asyncDataState',
  get: async () => {
    const response = await fetch('https://api.example.com/data');
    return response.json();
  },
});

function AsyncDataComponent() {
  const loadable = useRecoilValueLoadable(asyncDataState);

  switch (loadable.state) {
    case 'hasValue':
      return <div>{loadable.contents}</div>;
    case 'loading':
      return <div>Loading...</div>;
    case 'hasError':
      return <div>Error: {loadable.contents.message}</div>;
  }
}

교차하는 앱 관찰(Cross-App Observation)

"교차하는 앱 관찰 (Cross-App Observation)"은 Recoil에서 제공하는 고급 기능으로 기본적으로 서비스 내부의 상세한 상태변화를 체크해 가며 모두 확인할 수 있는 관찰기능을 제공하는 듯합니다.

About time trabel debuggin

Recoil을 사용하면, 애플리케이션의 모든 상태 변화를 실시간으로 관찰할 수 있습니다. 이는 애플리케이션의 모든 부분에서 일어나는 상태 변화를 정확히 파악하고, 필요한 반응을 취할 수 있게 해줍니다. 상태 변화를 관찰함으로써 개발자는 애플리케이션의 지속성을 관리하거나, 라우팅 로직을 구현하거나, 복잡한 디버깅 기능(예: 시간 여행 디버깅)을 손쉽게 구현할 수 있습니다.

useRecoilTransactionObserver_UNSTABLE(({ snapshot }) => {
  snapshot.getNodes_UNSTABLE({isModified: true}).forEach(modifiedNode => {
    const atomLoadable = snapshot.getLoadable(modifiedNode);
    if (atomLoadable.state === 'hasValue') {
      console.log(`Atom ${modifiedNode.key}의 새로운 값: `, atomLoadable.contents);
    }
  });
});

위 코드 예시에서는 recoil에서 제공하는 hooks입니다. 이는 Recoil의 상태 변화를 실시간으로 감지하고, 콘솔에 로깅하는 방식을 보여줍니다. 이와 같은 방법으로 애플리케이션의 상태 변화를 관찰하고, 필요한 로직을 구현할 수 있습니다.

맺음말

정말 간단하게 Recoil에 대해서 살펴보았습니다.
간단하게 찾아 사용했기 때문에, 그 속의 내부적인 동작이나 장단에 대해서 모두 이해하지 못한점이 다소 아쉽습니다만,
우선은 당장 프로젝트에서 사용가능한 상태로 진입하는것이 우선이기 때문에 불가피하게 여기서 학습을 잠시 마무리하는것이 옳다고 판단했습니다.

간단하게 프로젝트에서 사용해보니, 소개에서 언급한것과 같이 정말 간단하게 react 프로젝트에서 사용할 수 있는듯합니다. 또한 Mobx에서완 다르게 객체 지향적인 느낌보다는 react 와 어울리도록 함수형과 hooks를 통한 기능 분리를 지향하는듯 합니다.

해당 PR에서 코드를 확인할 수 있습니다.

1개의 댓글

comment-user-thumbnail
2023년 11월 20일

간단하게 알아보셨다고 했지만, 꽤 자세히 recoil에 대해 알아갑니다!

답글 달기