리액트 상태 관리 심화 #1 클라이언트 상태와 서버 상태의 차이

6 분 소요

리액트 기초 강좌(#1~#15)에서 useStateuseReducer, 상태 끌어올리기, useContext까지 다뤘습니다. 작은 앱이라면 이 빌트인 도구만으로 충분합니다. 그런데 앱이 커지면 어느 순간 useStateuseContext만으로는 버거워지는 지점이 옵니다. 이 시리즈는 바로 그 지점에서 시작합니다.

총 6편으로 구성됩니다.

  • #1 클라이언트 상태와 서버 상태의 차이 ← 이번 글
  • #2 TanStack Query로 다루는 서버 상태
  • #3 Zustand로 다루는 가벼운 클라이언트 상태
  • #4 Jotai와 원자(atom) 모델
  • #5 Redux Toolkit과 레거시 컨텍스트
  • #6 어떤 도구를 언제 — 결정 가이드

이번 글은 코드가 거의 없습니다. 도구를 고르기 전에 상태에는 성질이 다른 두 종류가 있다는 사실을 먼저 정리해 두는 것이 나머지 다섯 편을 이해하는 데 결정적이기 때문입니다.

빌트인 도구가 버거워지는 순간 #

리액트가 기본으로 제공하는 상태 도구는 세 가지입니다.

  • useState — 컴포넌트 하나의 지역 상태
  • useReducer — 전이 규칙이 복잡한 지역 상태
  • useContext — 컴포넌트 트리를 가로지르는 값 전달

앱이 커지면 이 도구들로 두 가지 다른 문제를 동시에 풀려고 하게 됩니다. 그리고 두 문제는 성질이 전혀 다릅니다.

문제 A. 다크 모드 설정, 사이드바 열림 여부, 모달 표시 상태, 장바구니 담긴 항목처럼 브라우저 안에서 생기고 브라우저 안에서만 의미 있는 값을 여러 컴포넌트가 공유해야 합니다. useContext로 풀 수 있지만, 값이 바뀔 때마다 Provider 아래 전체가 리렌더되거나, Context가 여러 개로 늘어나면서 Provider 중첩이 깊어집니다.

문제 B. 사용자 목록, 게시글, 주문 내역처럼 서버 데이터베이스에 진짜 원본이 있고, 화면은 그 사본을 보여줄 뿐인 값을 다뤄야 합니다. useEffect 안에서 fetch하고 useState에 담는 익숙한 패턴인데, 로딩 표시, 에러 처리, 재요청, 캐싱, 다른 화면과의 동기화를 전부 손으로 짜다 보면 코드가 금세 복잡해집니다.

핵심은 이것입니다. 문제 A와 문제 B는 같은 useState로 다루지만, 사실 완전히 다른 종류의 상태입니다. 이 둘을 구분하지 못하면 도구 선택이 계속 어긋납니다.

클라이언트 상태와 서버 상태 #

상태를 다음 두 종류로 나눠 보겠습니다.

구분클라이언트 상태서버 상태
원본이 있는 곳브라우저 (이 상태가 곧 원본)서버 데이터베이스 (화면은 사본)
예시다크 모드, 모달 열림, 폼 입력값, 선택된 탭사용자 목록, 게시글, 주문 내역, 검색 결과
동기화 대상없음 (내 브라우저 안에서 완결)서버와 계속 맞춰야 함
신선도항상 최신 (내가 바꾸니까)시간이 지나면 낡음 (남이 바꿀 수 있음)
주된 고민누가 공유하고 어떻게 갱신하나언제 다시 가져오고 어떻게 캐싱하나

클라이언트 상태는 브라우저 안에서 태어나고 브라우저 안에서 죽습니다. 다크 모드를 켰다는 사실은 서버에 물어볼 필요가 없습니다. 이 상태 자체가 원본입니다.

서버 상태는 다릅니다. 화면에 보이는 게시글 목록은 진짜가 아니라 서버에 있는 원본의 사본입니다. 내가 보고 있는 동안 다른 사용자가 글을 추가했을 수도 있습니다. 즉 서버 상태는 가만히 두면 낡습니다. 그래서 “언제 다시 가져올까”, “가져온 걸 얼마나 캐싱할까”, “여러 화면이 같은 데이터를 보고 있을 때 어떻게 한꺼번에 갱신할까” 같은 고민이 따라옵니다. 클라이언트 상태에는 없는 고민입니다.

노트
이 구분은 TanStack Query를 만든 Tanner Linsley가 널리 퍼뜨린 관점입니다. “서버 상태"라는 이름이 처음에는 어색할 수 있지만, 한 번 이 렌즈로 코드를 보면 그동안 useEffect + useState로 힘들게 짜던 것의 정체가 분명해집니다.

왜 이 구분이 도구 선택을 바꾸는가 #

두 상태의 성질이 다르므로, 잘 맞는 도구도 다릅니다.

서버 상태를 위한 도구는 캐싱, 자동 재요청, 로딩과 에러 상태, 백그라운드 갱신을 기본으로 제공해야 합니다. 이 영역의 표준이 TanStack Query(과거 이름 React Query)입니다. #2에서 다룹니다.

클라이언트 상태를 위한 도구는 가볍게 전역 값을 공유하고, 꼭 필요한 컴포넌트만 리렌더되게 해야 합니다. 이 영역에는 선택지가 여럿입니다. Zustand(#3), Jotai(#4), 그리고 전통의 Redux Toolkit(#5)을 차례로 보겠습니다.

여기서 가장 흔한 실수를 짚고 넘어가겠습니다. 서버 데이터를 Redux나 Zustand 같은 전역 클라이언트 상태 저장소에 통째로 넣는 것입니다. 한동안 표준처럼 쓰였지만, 이렇게 하면 캐싱, 재요청, 신선도 관리를 전부 손으로 다시 만들어야 합니다. 서버 상태는 서버 상태 도구에 맡기는 편이 훨씬 단순합니다.

흔한 안티패턴 vs 권장 구조
[흔한 안티패턴]
모든 상태 → 하나의 거대한 전역 저장소(Redux 등)
  - 다크 모드도 여기에
  - 서버에서 받은 게시글 목록도 여기에
  - 캐싱,재요청,로딩 처리를 전부 손으로 구현

[권장 구조]
서버 상태   → TanStack Query (캐싱,재요청,신선도 자동)
클라이언트 상태 → Zustand / Jotai / Context (가벼운 전역 공유)
지역 상태   → useState / useReducer (그대로)

그럼 useState는 언제 쓰나 #

오해를 막기 위해 분명히 해두겠습니다. 이 시리즈는 useState를 버리자는 이야기가 아닙니다. 한 컴포넌트 안에서만 쓰이는 상태, 예를 들어 입력 폼의 값이나 토글 하나는 여전히 useState가 정답입니다. 새 도구는 다음 조건이 겹칠 때 필요해집니다.

  • 여러 컴포넌트가 같은 상태를 공유해야 한다.
  • 그 컴포넌트들이 트리에서 멀리 떨어져 있어 props로 넘기기 번거롭다.
  • (서버 상태라면) 캐싱과 재요청까지 신경 써야 한다.

이 조건에 해당하지 않으면 빌트인 도구로 충분합니다. 도구를 늘리는 것 자체가 비용이라는 점을 기억해 두면 좋습니다.

이 시리즈에서 배우게 될 것 #

각 편은 도구 하나를 같은 기준으로 들여다봅니다. 설치, 기본 사용법, 리렌더 동작, 어떤 상황에 잘 맞는지를 일관되게 비교할 수 있도록 구성했습니다.

  • #2 TanStack Query — 서버 상태의 캐싱과 재요청을 자동화
  • #3 Zustand — 최소한의 보일러플레이트로 전역 클라이언트 상태
  • #4 Jotai — 상태를 작은 원자로 쪼개는 상향식 모델
  • #5 Redux Toolkit — 여전히 많은 코드베이스에 남아 있는 표준의 현재 모습
  • #6 결정 가이드 — 상황별로 무엇을 고를지 정리

마무리 #

이번 글의 결론은 한 문장입니다. 상태에는 클라이언트 상태와 서버 상태가 있고, 둘은 성질이 달라 잘 맞는 도구도 다릅니다.

  • 클라이언트 상태 — 브라우저 안에서 완결, 그 자체가 원본
  • 서버 상태 — 서버에 원본이 있는 사본, 가만히 두면 낡음

새 화면을 만들 때마다 먼저 묻는 습관을 들이면 좋습니다. “이 값은 브라우저 안에서 완결되나, 아니면 서버 데이터의 사본인가?” 이 한 가지 질문이 나머지 편에서 어떤 도구를 꺼낼지를 결정합니다.

다음 글인 “리액트 상태 관리 심화 #2 TanStack Query로 다루는 서버 상태"에서는 가장 먼저 서버 상태부터 다룹니다. useEffect + useState로 짜던 데이터 페칭이 어떻게 몇 줄로 줄어드는지 직접 보겠습니다.

X