이 데이터, 도대체 어디서 관리해야 하지?

React를 쓰다 보면 누구나 한 번쯤 이런 고민을 하는 것 같다.
버튼 클릭 횟수, 입력 값 같은 단순한 데이터는 useState를 사용해 간단히 처리할 수 있다.

하지만 서비스가 점점 커지고, 컴포넌트가 많아질수록 점점 까다로워 지는 것이 상태관리다.

예를 들어, 로그인 정보, 장바구니, 테마처럼 여러 곳에서 공유해야 하는 상태가 늘어나고,
서버에서 가져온 데이터도 관리해야 하는 상황이 생기면서 점점 머리가 아파진다.

그래서 이번 글에서는 React에서의 상태관리를
개념 → 필요성 → 종류 → 도구별 사용법과 장단점 순서로 정리해보려고 한다.

상태(State)란 무엇일까?

➡️ 상태(State)란 화면(UI)에 영향을 주는 데이터를 말한다.

예를 들어보자!

  • 입력창에 입력된 글자
  • 로그인 여부 (true/false)
  • 장바구니에 담긴 상품 목록
  • 좋아요 버튼을 누른 횟수

이 값들이 바뀌면 UI도 자동으로 업데이트 된다.
➡️ React의 핵심은 바로 상태가 변하면 UI가 다시 렌더링 된다는 점이다!

왜 상태관리가 중요할까?

작은 서비스에서는 단순히 useState만 써도 충분히 원하는 바를 구현할 수 있다.
하지만 규모가 커지면 다양한 문제가 발생한다.

➡️ Props Drilling
: 부모 → 자식 → 손자 → 증손자 컴포넌트로 props를 줄줄이 전달해야 하는 상황

➡️ 데이터 불일치
: 어떤 컴포넌트는 최신 데이터를, 다른 컴포넌트는 예전의 데이터를 들고 있는 상황

➡️ 유지보수 어려움
: 상태가 어디서 바뀌는지 추적하기가 힘들다.

그래서 상태관리는 단순히 값을 저장하는 것이 아니라,
서비스를 예측 가능하고 유지보수하기 쉽게 만드는 하나의 전략이라고 볼 수 있다!

상태관리의 세 가지 범주

React에서 상태는 세 가지 범주로 구분하는데,
로컬 상태(Local), 전역 상태(Global), 서버 상태(Server)로 나눌 수 있다.

로컬 상태 (Local State)

컴포넌트 안에서만 쓰는 상태를 말한다.
로컬 상태 관리 시에는 useStateuseReducer를 사용해 간단하게 처리할 수 있다.

➡️ useState

import React, { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      클릭 횟수: {count}
    </button>
  );
}
  • 장점: 가장 직관적이고 빠르게 사용 가능하다.
  • 단점: 컴포넌트 범위를 벗어나면 공유가 불가능하다.

➡️ useReducer

import React, { useReducer } from "react";

function reducer(state, action) {
  switch (action.type) {
    case "increment": return { count: state.count + 1 };
    case "decrement": return { count: state.count - 1 };
    default: return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return (
    <>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      {state.count}
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
    </>
  );
}
  • 장점: 복잡한 상태 로직을 깔끔하게 관리할 수 있다.
  • 단점: 단순 상태에는 오히려 과잉 설계로 여겨질 수 있다.

전역 상태 (Global State)

여러 컴포넌트가 공유해야 하는 상태를 말한다.
전역 상태 관리 시에는 Context API, Redux, Zustand, Jotai, Recoil, MobX 같은 툴을 사용할 수 있다.

➡️ Context API

import React, { createContext, useContext } from "react";

const ThemeContext = createContext("light");

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  const theme = useContext(ThemeContext);
  return <p>현재 테마: {theme}</p>;
}
  • 장점: props drilling 문제를 해결할 수 있고, 추가 라이브러리가 필요 없다.
  • 단점: 상태가 많아지면 성능 최적화가 어렵다.

➡️ Redux

import { configureStore, createSlice } from "@reduxjs/toolkit";
import { Provider, useDispatch, useSelector } from "react-redux";

const counterSlice = createSlice({
  name: "counter",
  initialState: { value: 0 },
  reducers: {
    increment: state => { state.value += 1; },
    decrement: state => { state.value -= 1; }
  }
});

const store = configureStore({ reducer: { counter: counterSlice.reducer } });
export const { increment, decrement } = counterSlice.actions;

function Counter() {
  const dispatch = useDispatch();
  const value = useSelector(state => state.counter.value);
  return (
    <>
      <button onClick={() => dispatch(decrement())}>-</button>
      {value}
      <button onClick={() => dispatch(increment())}>+</button>
    </>
  );
}

export default function App() {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  );
}
  • 장점: 대규모 서비스에서 예측 가능한 상태 관리, DevTools 지원, 생태계가 풍부하다.
  • 단점: 설정이 복잡하고, 코드가 길어진다.

➡️ Zustand

import create from "zustand";

const useStore = create(set => ({
  count: 0,
  increase: () => set(state => ({ count: state.count + 1 }))
}));

function Counter() {
  const { count, increase } = useStore();
  return <button onClick={increase}>클릭: {count}</button>;
}
  • 장점: 코드가 간단하고, 러닝커브가 낮고, 리렌더링을 최소화 할 수 있다.
  • 단점: 대규모 서비스에서는 구조화 고민이 필요하고, 레퍼런스가 적다.

➡️ Jotai

import { atom, useAtom } from "jotai";

const countAtom = atom(0);

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  return (
    <button onClick={() => setCount(count + 1)}>
      클릭: {count}
    </button>
  );
}
  • 장점: 원자(atom) 단위로 잘게 쪼개서 관리가 가능하고 React 친화적이다.
  • 생태계가 작고, 대규모 서비스에서는 설계 고민이 필요하다.

➡️ Recoil

import { RecoilRoot, atom, useRecoilState } from "recoil";

const countState = atom({
  key: "countState",
  default: 0
});

function Counter() {
  const [count, setCount] = useRecoilState(countState);
  return (
    <button onClick={() => setCount(count + 1)}>
      클릭: {count}
    </button>
  );
}

export default function App() {
  return (
    <RecoilRoot>
      <Counter />
    </RecoilRoot>
  );
}
  • 장점: Redux보다 훨씬 간단하고 atom 단위 최적화가 가능하다.
  • 단점: 레퍼런스와 사례가 부족하고, 대규모 서비스에서 안정성 검증이 부족하다.

참고로 Recoil은 2025년 1월 12일에 공식적으로 개발이 종료 선언되었으며,
더 이상 지원되지 않는다..

➡️ MobX

import { makeAutoObservable } from "mobx";
import { observer } from "mobx-react-lite";

class CounterStore {
  count = 0;
  constructor() {
    makeAutoObservable(this);
  }
  increment() { this.count++; }
}

const counterStore = new CounterStore();

const Counter = observer(() => (
  <button onClick={() => counterStore.increment()}>
    클릭: {counterStore.count}
  </button>
));
  • 장점: 상태를 직접 변경하면 UI 자동 반영 (직관적), 코드량이 적고 러닝커브가 낮다.
  • 단점: 상태 흐름이 불투명할 수 있고, Redux에 비해 생태계/디버깅 도구가 부족하다.

서버 상태 (Server State)

서버에서 가져온 데이터(API 호출 결과 등)를 관리하는 상태를 말한다.
단순 fetch를 넘어서 캐싱, 동기화, 로딩·에러 처리까지 담당한다.

서버 상태 관리 시에는 React Query, SWR 과 같은 툴을 사용할 수 있다.

➡️ React Query

import { useQuery } from "@tanstack/react-query";

async function fetchUsers() {
  const res = await fetch("/api/users");
  return res.json();
}

function Users() {
  const { data, isLoading, error } = useQuery(["users"], fetchUsers);

  if (isLoading) return <p>로딩 중...</p>;
  if (error) return <p>에러 발생!</p>;

  return (
    <ul>
      {data.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}
  • 장점: API 캐싱, 자동 refetch, 로딩·에러 처리가 내장되어 있다.
  • 단점: 클라이언트 전역 상태와는 별도로 관리해야 한다.

➡️ SWR

import useSWR from "swr";

const fetcher = url => fetch(url).then(res => res.json());

function Users() {
  const { data, error } = useSWR("/api/users", fetcher);

  if (!data) return <p>로딩 중...</p>;
  if (error) return <p>에러 발생!</p>;

  return (
    <ul>
      {data.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}
  • 장점: 데이터 가져오기 단순화, React Hooks와 자연스럽게 어울린다.
  • 단점: React Query에 비해 기능이 단순하다.

분류도구장점단점
로컬useState간단, 직관적범위 제한
로컬useReducer복잡한 로직 관리단순 상태엔 과잉
전역Context APIprop drilling 해결성능 최적화 한계
전역Redux대규모 앱 적합, DevTools설정 복잡
전역Zustand가볍고 간단레퍼런스 부족
전역Jotaiatom 단위 관리생태계 작음
전역Recoil단순, 성능 최적화사례 부족
전역MobX직관적, 코드 간단상태 흐름 불투명, 생태계 적음
서버React Query강력한 서버 상태 관리전역 상태와 별도
서버SWR간단, 직관적기능 단순

그럼 어떤 상태관리 도구를 써야 하는가?

이건 사람마다 다르겠지만
React 상태관리의 핵심은 필요에 따라 가장 단순한 도구부터 시작하는 것이다!

➡️ 작은 서비스 → useState, useReducer
➡️ 전역 공유 조금 필요 → Context API
➡️ 중간 규모의 프로젝트 → Zustand, Jotai, Recoil, MobX
➡️ 대규모 팀 프로젝트 → Redux
➡️ 서버 데이터가 많은 경우 → React Query, SWR

상태관리는 단순히 데이터를 보관하는 게 아니라,
서비스를 예측 가능하고 유지보수하기 쉽게 만드는 핵심 전략이라는 것을 명심하자!

💡 2025년 React 상태 관리 라이브러리 순위는 명확하게 발표되지 않았으나, Zustand, Jotai, Recoil과 같은 경량 라이브러리들이 주목받고 있으며, React Query와 같은 데이터 페칭 라이브러리도 중요한 역할을 한다. 이와 함께 Context API는 여전히 사용되고 있으며, Redux는 가장 많이 사용되는 라이브러리 중 하나다.

profile
front-end developer

1개의 댓글

comment-user-thumbnail
2025년 9월 16일

잘 읽었습니다 깔끔하게 정리하셔서 한눈에 보기 좋아요!

답글 달기