본문 바로가기

개발 공부/React

React Recoil

단순한 컴포넌트 내부 상태는 useState로 충분하지만, 전역적으로 공유해야 하는 데이터가 많아질수록 관리가 복잡해집니다.

2020년 페이스북(메타)이 공개한 상태관리 라이브러리입니다.

초반에는 React 생태계에서 Redux 대안으로 큰 관심을 받았지만, 지금 기준(2025년)으로는 예전 기술로 보는 게 맞습니다.

 

 

Recoil의 현재 상황

  • 2020~2021년
    "React 전용 전역 상태 관리"로 주목받음. atom/selector 개념 덕분에 러닝커브가 Redux보다 쉽다는 평가.
  • 2022~2023년
    React 18(Concurrent Mode)과 잘 맞긴 했지만, React Query, Zustand, Jotai 같은 가볍고 단순한 라이브러리가 많이 나오면서 점점 밀리기 시작.
  • 2024년 말
    메타가 사실상 개발을 중단했고, 2025년 1월 1일부로 공식 GitHub 저장소가 아카이브 처리됨 → 신규 기능/버그 수정 없음.
  • React 19 (Next.js 15 기본)과 호환성이 깨짐 → ReactCurrentDispatcher 오류 다수 발생.
 

 

Recoil

 

Recoil은 Facebook이 만든 React 전용 전역 상태 관리 라이브러리입니다.

  • Hooks 기반 API: useRecoilState, useRecoilValue, useSetRecoilState 등 React 훅과 유사한 사용성
  • Atom + Selector: 최소 상태 단위와 파생 상태의 명확한 분리
  • Concurrent Features 친화적: Suspense/동시성 기능과 자연스럽게 연동
  • 비동기 흐름 내장: Selector로 동기/비동기 파생 상태 모두 간단히 표현

 


 

핵심 개념: Atom & Selector

 

Atom

  • 전역 상태의 최소 단위입니다.
  • 여러 컴포넌트가 동시에 구독할 수 있고, 값이 바뀌면 해당 값을 구독 중인 컴포넌트만 리렌더링됩니다.
// src/state/todo.ts
import { atom } from 'recoil';

export const todoListState = atom<string[]>({
  key: 'todoListState', // 전역적으로 고유해야 함
  default: [],
});

 


 

Selector

  • 파생 상태(Derived state)를 정의합니다.
  • 하나 이상의 Atom/Selector를 입력으로 받아 계산된 값을 반환합니다.
  • 동기/비동기 모두 가능하며, 캐싱/메모이제이션이 기본 제공됩니다.
// src/state/todo.ts
import { selector } from 'recoil';
import { todoListState } from './todo';

export const todoStatsState = selector<{ total: number; completed: number }>({
  key: 'todoStatsState',
  get: ({ get }) => {
    const todos = get(todoListState);
    const completed = todos.filter((t) => t.startsWith('[완료]')).length;
    return { total: todos.length, completed };
  },
});

 

 

 

셋업

npm install recoil
# 또는
pnpm add recoil
  • Recoil을 사용하려면 최상위에 RecoilRoot를 감쌉니다.
// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { RecoilRoot } from 'recoil';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <RecoilRoot>
      <App />
    </RecoilRoot>
  </React.StrictMode>
);

 

 


예제: Todo List 

Atom 정의

// src/state/todo.ts
import { atom } from 'recoil';

export const todoListState = atom<string[]>({
  key: 'todoListState',
  default: [],
});

// 완료/전체 개수 파생 상태
export const todoStatsState = selector<{ total: number; completed: number }>({
  key: "todoStatsState",
  get: ({ get }) => {
    const todos = get(todoListState);
    const completed = todos.filter((t) => t.startsWith("[완료]")).length;
    return { total: todos.length, completed };
  },
});

쓰기/읽기: useRecoilState

// src/components/TodoInput.tsx
import { useRecoilState } from 'recoil';
import { todoListState } from '../state/todo';
import { useState } from 'react';

export default function TodoInput() {
  const [todos, setTodos] = useRecoilState(todoListState);
  const [text, setText] = useState('');

  const add = () => {
    if (!text.trim()) return;
    setTodos((prev) => [...prev, text.trim()]);
    setText('');
  };

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} placeholder="할 일을 입력" />
      <button onClick={add}>추가</button>
    </div>
  );
}

 

읽기 전용: useRecoilValue

// src/components/TodoList.tsx
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { todoListState } from '../state/todo';

export default function TodoList() {
  const todos = useRecoilValue(todoListState); // 읽기 전용
  const setTodos = useSetRecoilState(todoListState); // setter만

  const toggleComplete = (idx: number) => {
    setTodos((prev) =>
      prev.map((t, i) => (i === idx ? (t.startsWith('[완료]') ? t.replace('[완료] ', '') : `[완료] ${t}`) : t))
    );
  };

  return (
    <ul>
      {todos.map((t, i) => (
        <li key={i}>
          <button onClick={() => toggleComplete(i)}>완료토글</button> {t}
        </li>
      ))}
    </ul>
  );
}

 

파생 상태: Selector

// src/components/TodoStats.tsx
import { useRecoilValue } from 'recoil';
import { todoStatsState } from '../state/todo';

export default function TodoStats() {
  const { total, completed } = useRecoilValue(todoStatsState);
  return <p>총 {total}개 / 완료 {completed}개</p>;
}

 

조립

// src/App.tsx
import TodoInput from './components/TodoInput';
import TodoList from './components/TodoList';
import TodoStats from './components/TodoStats';

export default function App() {
  return (
    <main>
      <h1>Recoil Todo</h1>
      <TodoInput />
      <TodoStats />
      <TodoList />
    </main>
  );
}

 

 


 

비동기 : Selector로 API 호출하기

Recoil은 별도 미들웨어 없이 Selector만으로도 비동기 로직을 표현할 수 있습니다.

// src/state/user.ts
import { selector } from 'recoil';

export type User = { id: number; name: string };

export const currentUserQuery = selector<User>({
  key: 'currentUserQuery',
  get: async () => {
    const res = await fetch('/api/user');
    if (!res.ok) throw new Error('Failed to fetch user');
    return (await res.json()) as User;
  },
});
// src/components/UserProfile.tsx
import { useRecoilValue } from 'recoil';
import { currentUserQuery } from '../state/user';

export default function UserProfile() {
  const user = useRecoilValue(currentUserQuery);
  return <div>{user.name}님 환영합니다!</div>;
}

 

 


Recoil 체크리스트

key는 전역 고유 문자열

  • Atom/Selector의 key는 앱 전체에서 유일해야 합니다.
  • 동적 생성 시 충돌 방지에 주의하세요. (2편에서 atomFamily/selectorFamily 소개)

 상태는 쪼개서 설계

  • 큰 객체 하나로 모든 것을 담기보다 관심사별로 atom을 분리하세요.
  • 리렌더링 범위를 최소화하여 성능을 확보합니다.

읽기/쓰기 훅 적절히 분리

  • 읽기 전용은 useRecoilValue, 쓰기 전용은 useSetRecoilState로 구분하면 불필요한 리렌더를 줄일 수 있습니다.

전역 상태 vs 서버 상태 구분

  • 서버 상태(fetch/캐싱/리패칭)는 React Query 같은 도구가 더 적합한 경우가 많습니다.
  • 반면 클라이언트 전역 UI 상태(모달, 필터, 권한 플래그 등)는 Recoil이 간결합니다.

언제 Recoil을 쓰면 좋은가?

  • 작은~중간 규모 앱에서 빠르게 전역 상태를 도입하고 싶을 때
  • 파생 상태가 많고, 계산/조합으로 값이 자주 만들어질 때
  • Redux의 보일러플레이트가 부담스럽고, Context로는 리렌더링 관리가 어려울 때
  • 실무 포인트: 서버 상태는 React Query, 클라이언트 전역/파생 상태는 Recoil 혼합 전략이 가장 현실적입니다.

 

'개발 공부 > React' 카테고리의 다른 글

useOptimistic  (0) 2025.08.28
Zustand (1)  (2) 2025.08.17
React Query, Redux Toolkit Query  (0) 2025.06.22
TanStack Query  (0) 2025.05.22
Next.js - 동적 라우팅  (0) 2025.05.14