프론트엔드/React

[React] Recoil에 대해

s_omi 2025. 4. 1. 21:04

오늘은 다양한 React 상태 관리 라이브러리 중 Recoil에 대해 알아보겠습니다.

 

[React] 상태 관리 라이브러리, 어떤 것을 선택해야 할까?

React로 애플리케이션을 개발하다 보면 상태 관리는 필수적인 과제입니다. 현재 사용되고 있는 React 상태 관리 라이브러리는 제일 많이 사용하는 Redux를 시작으로, Zustand, Jotai, Recoil, Context API, Rea

mi-dairy.tistory.com

 

 

1. Recoil이란?

Recoil은 페이스북에서 개발한 React 상태 관리 라이브러리로,

기존의 전역 상태 관리 라이브러리에 비해 간단하고 직관적인 API를 제공하면서도,

React Suspense와의 호환성, 동시성 모드 지원 등의 특징을 갖추고 있습니다.

 

여러 컴포넌트가 공유해야 하는 데이터, 즉 전역 상태를 다룰 때 유용하며

React로 작성된 애플리케이션에서 조금 더 선언적이면서 간편하게 상태를 관리할 수 있게 도와줍니다.

 

Recoil

 

2. 왜 사용할까?

Recoil는 React 훅과 유사한 사용법으로 진입 장벽이 낮고

React Suspense와 동시성 모드를 지원해 최신 React 패턴과 잘 맞습니다.

 

Atom과 Selector를 사용하여 전역 상태와 파생 상태를 명확하게 구분할 수 있어 코드 가독성이 높으며,

상태가 변경될 때 그 상태를 사용하는 컴포넌트만 렌더링되므로 불필요한 렌더링을 줄일 수 있습니다.

 

비동기 데이터를 쉽게 관리할 수 있어 서버 데이터와 클라이언트 상태를 자연스럽게 연결할 수 있습니다.

Redux와 달리 별도의 액션, 리듀서, 미들웨어 설정 없이 상태를 간단하게 정의하고 사용할 수 있어

복잡한 전역 상태 관리가 필요한 React 프로젝트에서 간단하고 직관적인 대안을 제공하기 위해 사용됩니다.

 

 

3. 기본 사용법

Recoil은 npm 혹은 yarn으로 설치할 수 있습니다.

npm install recoil
# 또는
yarn add recoil

 

React 애플리케이션에서 Recoil을 사용하려면, 전체 컴포넌트를 <RecoilRoot>로 감싸주어야 합니다.

// App.tsx
import React from 'react';
import { RecoilRoot } from 'recoil';
import Counter from './Counter';

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

export default App;

 

Recoil에서 Atom은 가장 기본이 되는 상태 단위입니다. atom 함수를 사용해 정의합니다.

key는 전역적으로 유일해야 하며, default는 초기값을 의미합니다.

 

// atoms/counterAtom.ts
import { atom } from 'recoil';

export const counterAtom = atom<number>({
  key: 'counterAtom',
  default: 0,
});

 

Atom을 사용하는 가장 간단한 방법은 useRecoilState() 훅으로 읽고 쓰는 것입니다.

useRecoilState(atom)라고 작성하면 useState와 유사하게 [값, setter 함수]를 반환합니다.

 

// Counter.tsx
import React from 'react';
import { useRecoilState } from 'recoil';
import { counterAtom } from './atoms/counterAtom';

function Counter() {
  const [count, setCount] = useRecoilState(counterAtom);

  const increase = () => setCount(count + 1);
  const decrease = () => setCount(count - 1);

  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={increase}>증가</button>
      <button onClick={decrease}>감소</button>
    </div>
  );
}

export default Counter;

 

 

4. 왜 사용할까?

// App.js
import React, { useState } from 'react';

const Display = ({ count }) => {
  return <h1>Count: {count}</h1>;
};

const Increment = ({ setCount }) => {
  return <button onClick={() => setCount((prev) => prev + 1)}>Increment</button>;
};

const App = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <Display count={count} />
      <Increment setCount={setCount} />
    </div>
  );
};

export default App;

 

위와 같이 useState를 활용한 상태 관리를 하면 count와 setCount는 각각 Display와 Increment 컴포넌트에서만 사용되지만,
상태를 사용하지 않는 App 컴포넌트에 두어 props 형태로 전달해야 합니다.

 

이는 useState의 상태 관리 범위가 컴포넌트 내부에 한정되어 있기 때문에,
상태를 공통으로 사용하려면 상위 컴포넌트인 App 내부에 상태를 정의해야 합니다.

또한 상태가 변경될 때, App, Display, Increment 컴포넌트가 모두 리렌더링되어 불필요한 리렌더링이 발생합니다.


결과적으로 이와 같은 구조는 계층이 깊어질수록 Prop Drilling 문제를 유발하며,
여러 컴포넌트에서 동일한 상태를 사용하려면 공통 부모 컴포넌트를 찾아야 하는 번거로움이 생깁니다.

 

똑같은 코드에 대해 Recoil를 활용한다면,

// recoilState.js
import { atom } from 'recoil';

export const countState = atom({
  key: 'countState',
  default: 0,
});
// App.js
import React from 'react';
import { RecoilRoot, useRecoilState, useRecoilValue } from 'recoil';
import { countState } from './recoilState';

const Display = () => {
  const count = useRecoilValue(countState);
  return <h1>Count: {count}</h1>;
};

const Increment = () => {
  const [count, setCount] = useRecoilState(countState);
  return <button onClick={() => setCount(count + 1)}>Increment</button>;
};

const App = () => (
  <RecoilRoot>
    <Display />
    <Increment />
  </RecoilRoot>
);

export default App;

 

전역 상태로 관리하기 때문에 필요한 컴포넌트들만 Recoil Atom을 import해 사용할 수 있습니다.

덕분에 App 컴포넌트는 상태가 변해도 불필요한 리렌더링을 하지 않게 됩니다.

 

 

 

5. 주요 함수 / 훅

Recoil 자체에는 메소드라고 할 수 있는 기능이 따로 있는 것은 아니지만, 상태를 관리하고 조작하는 데 사용하는 주요 함수들이 있습니다.
Recoil의 상태 관리 구조는 Atom과 Selector라는 개념을 중심으로 이루어져 있으며, React 훅을 사용하여 상태를 읽고 수정합니다.

 

5.1 함수

  • atom(): 전역 상태를 정의하는 기본 단위, 고유한 키와 기본값을 설정하여 상태를 만들 때 사용
import { atom } from 'recoil';

export const counterAtom = atom({
  key: 'counterAtom',
  default: 0,
});
  • selector(): Atom을 기반으로 파생 상태를 만들 때 사용, 다른 Atom의 상태를 읽거나 가공하여 새로운 상태 반환
import { selector } from 'recoil';
import { counterAtom } from './atoms';

export const doubledCounter = selector({
  key: 'doubledCounter',
  get: ({ get }) => {
    const count = get(counterAtom);
    return count * 2;
  },
});

 

5.2 훅

  • useRecoilState(): 상태를 읽고 수정할 수 있는 훅, useState()와 유사하게 [상태, setter] 형태로 반환
import { atom } from 'recoil';

export const counterAtom = atom({
  key: 'counterAtom',
  default: 0,
});
  • useRecoilValue(): 상태를 읽기 전용으로 가져오는 훅, 상태 값만 반환
const count = useRecoilValue(counterAtom);
  • useSetRecoilState(): 상태를 수정하기만 할 때 사용하는 훅, setter 함수만 반환
const setCount = useSetRecoilState(counterAtom);
setCount(10);
  • useResetRecoilState(): 상태를 초기값으로 리셋할 때 사용하는 훅
const resetCount = useResetRecoilState(counterAtom);
resetCount();
  • useRecoilCallback(): Recoil 상태를 한꺼번에 업데이트하거나 비동기 로직을 처리할 때 사용
const updateCount = useRecoilCallback(({ set }) => (value) => {
  set(counterAtom, value);
});
updateCount(5);
  • useRecoilTransaction_UNSTABLE(): 여러 상태를 한꺼번에 업데이트할 때 사용하며, 트랜잭션 단위로 여러 Atom 수정 가능
const updateMultipleAtoms = useRecoilTransaction_UNSTABLE(({ set }) => () => {
  set(counterAtom, 100);
  set(otherAtom, 'newValue');
});

updateMultipleAtoms();

 

 

6. Selector

Selector는 Atom을 이용해 가공된 값을 반환합니다.

비동기 Selector도 간편하게 만들 수 있어, 백엔드 API로부터 데이터를 받아오는 로직을 Suspense와 결합해 구성 가능합니다.

 

6.1 동기 Selector

// selectors/counterSelector.ts
import { selector } from 'recoil';
import { counterAtom } from '../atoms/counterAtom';

export const isEvenSelector = selector<boolean>({
  key: 'isEvenSelector',
  get: ({ get }) => {
    const count = get(counterAtom);
    return count % 2 === 0;
  },
});
// IsEvenStatus.tsx
import React from 'react';
import { useRecoilValue } from 'recoil';
import { isEvenSelector } from './selectors/counterSelector';

function IsEvenStatus() {
  const isEven = useRecoilValue(isEvenSelector);
  return <p>{isEven ? '짝수입니다' : '홀수입니다'}</p>;
}

export default IsEvenStatus;

 

위의 코드와 같이 Atom counterAtom의 값이 짝수인지 여부를 판별해주는 Selector를 만들 수 있습니다.

 

 

6.2 비동기 Selectors와 React Suspense

Recoil Selector에서는 비동기 함수를 사용할 수 있습니다.

비동기 Selector가 Promise를 반환하면, 컴포넌트에서 useRecoilValue()로 값을 읽을 때 해당 Promise가 resolve될 때까지 Suspense가 동작합니다.

// selectors/asyncDataSelector.ts
import { selector } from 'recoil';

export const asyncDataSelector = selector({
  key: 'asyncDataSelector',
  get: async () => {
    const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
    const data = await response.json();
    return data;
  },
});
// AsyncData.tsx
import React, { Suspense } from 'react';
import { useRecoilValue } from 'recoil';
import { asyncDataSelector } from './selectors/asyncDataSelector';

function AsyncData() {
  const data = useRecoilValue(asyncDataSelector);
  return <div>{JSON.stringify(data)}</div>;
}

export default function AsyncDataContainer() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <AsyncData />
    </Suspense>
  );
}
 

이처럼 Recoil + Suspense 조합을 사용하면 비동기 데이터가 로딩 중일 때

React 컴포넌트에서 자동으로 로딩 상태를 보여주도록 간단히 구현할 수 있습니다.

 

Recoil은 React 애플리케이션에서 가벼운 학습 비용, 직관적인 개발 흐름, 뛰어난 성능 최적화를 제공하는 신생 라이브러리입니다.

 

간단한 전역 상태 관리부터 복잡한 비동기 상태 관리까지 효율적으로 처리할 수 있으며, React Suspense와의 연동이 핵심 장점입니다.

규모가 큰 프로젝트에서도 Atom/Selector를 잘 설계하면 데이터 흐름이 깔끔해지고,

특정 상태만 구독하는 형태라 불필요한 렌더링을 줄일 수 있습니다.

 

아직 안정성과 커뮤니티 지원 측면에서 개선의 여지가 있지만,

React 공식 문서에서도 언급될 정도로 주목 받고 있으니, React 프로젝트에서 Redux 대안으로 고려해 볼 만합니다.