이 데이터, 도대체 어디서 관리해야 하지?
React를 쓰다 보면 누구나 한 번쯤 이런 고민을 하는 것 같다.
버튼 클릭 횟수, 입력 값 같은 단순한 데이터는 useState를 사용해 간단히 처리할 수 있다.
하지만 서비스가 점점 커지고, 컴포넌트가 많아질수록 점점 까다로워 지는 것이 상태관리다.
예를 들어, 로그인 정보, 장바구니, 테마처럼 여러 곳에서 공유해야 하는 상태가 늘어나고,
서버에서 가져온 데이터도 관리해야 하는 상황이 생기면서 점점 머리가 아파진다.
그래서 이번 글에서는 React에서의 상태관리를
개념 → 필요성 → 종류 → 도구별 사용법과 장단점 순서로 정리해보려고 한다.
상태(State)란 무엇일까?
➡️ 상태(State)란 화면(UI)에 영향을 주는 데이터를 말한다.
예를 들어보자!
이 값들이 바뀌면 UI도 자동으로 업데이트 된다.
➡️ React의 핵심은 바로 상태가 변하면 UI가 다시 렌더링 된다는 점이다!
왜 상태관리가 중요할까?
작은 서비스에서는 단순히 useState만 써도 충분히 원하는 바를 구현할 수 있다.
하지만 규모가 커지면 다양한 문제가 발생한다.
➡️ Props Drilling
: 부모 → 자식 → 손자 → 증손자 컴포넌트로 props를 줄줄이 전달해야 하는 상황
➡️ 데이터 불일치
: 어떤 컴포넌트는 최신 데이터를, 다른 컴포넌트는 예전의 데이터를 들고 있는 상황
➡️ 유지보수 어려움
: 상태가 어디서 바뀌는지 추적하기가 힘들다.
그래서 상태관리는 단순히 값을 저장하는 것이 아니라,
서비스를 예측 가능하고 유지보수하기 쉽게 만드는 하나의 전략이라고 볼 수 있다!
상태관리의 세 가지 범주
React에서 상태는 세 가지 범주로 구분하는데,
로컬 상태(Local), 전역 상태(Global), 서버 상태(Server)로 나눌 수 있다.
컴포넌트 안에서만 쓰는 상태를 말한다.
로컬 상태 관리 시에는 useState와 useReducer를 사용해 간단하게 처리할 수 있다.
➡️ 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>
</>
);
}
여러 컴포넌트가 공유해야 하는 상태를 말한다.
전역 상태 관리 시에는 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>;
}
➡️ 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>
);
}
➡️ 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>
);
}
➡️ 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>
);
}
참고로 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>
));
서버에서 가져온 데이터(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>
);
}
➡️ 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>
);
}
분류 | 도구 | 장점 | 단점 |
---|---|---|---|
로컬 | useState | 간단, 직관적 | 범위 제한 |
로컬 | useReducer | 복잡한 로직 관리 | 단순 상태엔 과잉 |
전역 | Context API | prop drilling 해결 | 성능 최적화 한계 |
전역 | Redux | 대규모 앱 적합, DevTools | 설정 복잡 |
전역 | Zustand | 가볍고 간단 | 레퍼런스 부족 |
전역 | Jotai | atom 단위 관리 | 생태계 작음 |
전역 | 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는 가장 많이 사용되는 라이브러리 중 하나다.
잘 읽었습니다 깔끔하게 정리하셔서 한눈에 보기 좋아요!