[번역] UI 상태, 서버 상태, 애플리케이션 상태의 차이
Soshy·

원문: The Difference Between UI State, Server State, and Application State
React 애플리케이션을 유지보수하기 어렵게 만드는 가장 쉬운 방법 중 하나는 모든 데이터를 같은 종류의 상태(state)로 취급하는 것이다.
대시보드에는 다음과 같은 상태가 있을 수 있다.
- 열려 있거나 닫혀 있는 사이드바
- 선택한 날짜 범위
- 로그인한 사용자
- API에서 가져온 주문 목록
- 로딩 표시
- 알림 개수
- 선택한 대시보드 필터
- 현재 열려 있는 모달
- 사용자 권한
- 캐시된 API 응답
이 모든 것은 "상태"다. 하지만 모두 같은 종류의 상태는 아니다. 그리고 이 차이는 중요하다.
개발자가 이런 상태의 종류를 구분하지 않으면 모든 것을 Redux에 넣거나, API 데이터를 전역 상태에 중복해서 저장하거나, 여러 컴포넌트를 거쳐 props를 전달하거나, 복사본끼리 동기화하기 위해 복잡한 로직을 만드는 경우가 많다.
더 나은 접근법은 먼저 이렇게 묻는 것이다.
지금 다루고 있는 상태는 어떤 종류인가?
대부분의 React 애플리케이션에서 상태는 크게 세 가지로 나눌 수 있다.
UI State
↓
현재 인터페이스와 관련된 상태
Server State
↓
API/백엔드가 소유하는 상태
Application State
↓
애플리케이션 전체에서 공유되는 상태
이 세 가지를 이해하면 상태를 어디에 두어야 하는지, 어떤 도구로 관리해야 하는지를 훨씬 쉽게 판단할 수 있다.
1. 실제 사례: React 대시보드
관리자 대시보드를 만든다고 생각해 보자.
+------------------------------------------------+
| Sidebar | Dashboard |
| | |
| Users | Total Users: 12,450 |
| Orders | Total Orders: 8,231 |
| Reports | |
| Settings | [This Month ▼] |
| | |
| | Sales Chart |
| | |
| | Recent Orders |
+------------------------------------------------+
대시보드는 GET /api/dashboard API에서 데이터를 가져오며, 응답은 다음과 같을 수 있다.
{
"totalUsers": 12450,
"totalOrders": 8231,
"revenue": 125000,
"recentOrders": []
}
동시에 프론트엔드에서는 sidebarOpen, selectedDateRange, isFilterModalOpen, selectedOrder 같은 값도 관리해야 한다. 애플리케이션 전체에서는 currentUser, permissions, theme, notificationCount가 필요할 수도 있다.
이 모두가 상태다. 하지만 이 모든 상태를 하나의 상태 관리 솔루션에 넣는 것은 대개 잘못된 선택이다. 각각을 나눠 보자.
2. UI 상태
UI 상태는 인터페이스의 일시적인 상태를 나타낸다. 다음과 같은 질문에 답하는 상태다.
- 사이드바가 열려 있는가?
- 모달이 보이는가?
- 어떤 탭이 선택되어 있는가?
- 드롭다운이 열려 있는가?
- 현재 어떤 행이 선택되어 있는가?
- 사용자가 폼에 어떤 값을 입력했는가?
const [sidebarOpen, setSidebarOpen] = useState(false);
const [selectedTab, setSelectedTab] = useState("overview");
const [isModalOpen, setIsModalOpen] = useState(false);
중요한 특징은 이 상태를 프론트엔드가 소유한다는 점이다.
백엔드는 사이드바가 열려 있는지, 사용자가 어떤 탭을 선택했는지, 현재 모달이 보이는지에 관심이 없다. 이런 상태는 UI에 속한다.
3. UI 상태는 대개 컴포넌트 가까이에 두는 것이 좋다
React 애플리케이션에서 흔히 볼 수 있는 실수 중 하나는 단순한 UI 상태까지 Redux에 넣는 것이다.
dispatch(setSidebarOpen(true));
컴포넌트 하나만 사용하는 상태인데도 이렇게 처리하면 불필요한 복잡성이 생긴다.
대신 다음 정도면 충분한 경우가 많다.
const [sidebarOpen, setSidebarOpen] = useState(true);
유용한 기준은 다음과 같다. 하나의 컴포넌트만 필요로 하는 상태라면 로컬에 둔다.
function Dashboard() {
const [isFilterOpen, setIsFilterOpen] = useState(false);
return (
<>
<button onClick={() => setIsFilterOpen(true)}>
Filters
</button>
{isFilterOpen && <FilterModal />}
</>
);
}
이런 경우 Redux를 도입할 이유는 없다.
4. 서버 상태는 다르다
이번에는 대시보드 API에서 가져오는 데이터를 생각해 보자.
API 응답 데이터는 React가 직접 소유하는 상태가 아니다. 데이터의 실제 원본은 백엔드에 있다. 이런 데이터를 서버 상태(server state) 라고 한다.
서버 상태는 UI 상태와 성격이 다르다. 프론트엔드 내부에서만 관리되는 것이 아니라 서버를 기준으로 계속 변할 수 있기 때문이다.
서버 상태는 다음과 같은 특성을 가진다.
- 서버에서 가져와야 한다
- 캐시(cache)할 수 있다
- 시간이 지나면 오래된 데이터가 될 수 있다
- 필요할 때 다시 가져와야 한다
- 캐시를 무효화해야 할 수 있다
- 다른 사용자가 변경할 수 있다
- 다른 애플리케이션에서 변경될 수 있다
- 일시적으로 사용할 수 없을 수 있다
- 데이터를 가져오는 동안 로딩 상태가 발생한다
- 요청이 실패할 수 있다
예를 들어 다른 관리자가 새로운 주문을 생성했다고 해보자.
서버의 데이터는 이미 바뀌었지만, 현재 실행 중인 React 애플리케이션이 그 사실을 자동으로 알 수 있는 것은 아니다. 필요한 시점에 서버의 최신 데이터를 다시 가져와야 한다.
즉, 서버 상태는 React 컴포넌트 트리와는 별개로 자신의 생명주기를 가진다.
5. 서버 상태를 무조건 Redux에 넣으면 안 되는 이유
모든 것을 Redux로 관리한다고 해보자.
dashboardSlice를 만들고 다음과 같은 상태를 저장할 수 있다.
{
totalUsers: 12450,
totalOrders: 8231,
revenue: 125000,
loading: false,
error: null
}
처음에는 합리적으로 보인다.
하지만 곧 상황이 복잡해진다.
요청 → 로딩 → 성공/실패 → 저장 → 무효화 → 다시 가져오기 → 오래된 데이터 → 캐시까지 전체 생명주기를 직접 처리해야 한다.
그러다 다른 페이지에서 같은 데이터를 요청한다.
그 페이지도 같은 Redux 상태를 사용해야 할까?
데이터가 오래되면 어떻게 해야 할까?
언제 다시 가져와야 할까?
데이터를 수정한 뒤에는 기존 데이터를 직접 지워야 할까?
이쯤 되면 Redux 스토어가 점점 직접 만든 API 캐시처럼 변한다.
하지만 Redux는 원래 그런 목적으로 설계된 도구가 아니다.
6. 서버 상태에는 서버 상태 전용 도구가 필요하다
서버 상태에는 이를 위해 설계된 라이브러리를 사용하는 편이 일반적으로 더 적합하다.
2026년 기준으로는 TanStack Query(이전 이름 React Query)가 대표적인 선택지다.
const { data, isLoading, error } = useQuery({
queryKey: ["dashboard"],
queryFn: fetchDashboard
});
이제 데이터를 가져오는 것부터 캐시, 로딩 상태, 에러, 다시 가져오기, 오래된 데이터, 무효화, 재시도까지 라이브러리가 처리한다.
Redux 안에서 이 모든 기능을 직접 다시 구현할 필요가 없다.
덕분에 훨씬 깔끔하게 역할을 나눌 수 있다.
React Query
|
+---- API data
+---- Cache
+---- Loading
+---- Errors
+---- Refetching
Redux / Zustand
|
+---- Application state
useState
|
+---- UI state
이런 구분은 React 애플리케이션을 설계할 때 가장 유용한 사고방식 중 하나다.
7. 애플리케이션 상태
세 번째 종류를 살펴보자.
애플리케이션에 currentUser, permissions, selectedWorkspace, theme, notificationPreferences가 있다고 가정해 보자.
이 값들은 단순히 UI에서만 사용하는 값이라고 보기 어렵다. 서로 관련 없는 여러 컴포넌트에서 필요할 수 있기 때문이다.
Header → currentUser
Sidebar → permissions
Dashboard → permissions
Settings → currentUser
이 값들을 모든 컴포넌트를 거쳐 전달하기 시작하면 금세 관리하기 어려워진다.
이럴 때 애플리케이션 상태(application state) 가 유용하다.
애플리케이션 상태는 서로 직접적인 관계가 없는 여러 영역에서 접근하거나 수정해야 하는 정보를 의미한다.
8. 이럴 때 전역 스토어가 적합하다
애플리케이션 전체에서 사용하는 상태라면 전역 상태 관리자를 사용하는 것이 합리적이다.
어떤 도구를 선택할지에 대해서도 간단히 짚고 넘어가자.
2026년에는 보일러플레이트가 적다는 장점 덕분에 소규모에서 중간 규모 애플리케이션의 공유 클라이언트 상태를 관리할 때 Zustand가 일반적인 기본 선택지로 자리 잡았다.
반면 Redux Toolkit은 규모가 큰 팀이나, 클라이언트 상태가 실제로 복잡하게 연결되어 있어 엄격한 규칙과 타임 트래블 디버깅의 이점을 얻을 수 있는 애플리케이션에 더 적합하다.
어떤 도구를 선택하더라도 구조는 비슷하다.
Global Store (Zustand / Redux Toolkit)
|
+------+------+
| | |
Header Sidebar Dashboard
| | |
User info Permissions Workspace
중요한 것은 "Redux가 좋다" 거나 "Zustand가 좋다" 는 이야기가 아니다.
핵심은 상태를 실제로 공유하고 중앙에서 관리해야 할 때 전역 스토어가 유용하다는 것이다.
프로젝트에서 전역 스토어를 사용하고 있다는 이유만으로 모든 useState 변수를 전역 스토어에 넣는다고 해서 애플리케이션의 확장성이 좋아지는 것은 아니다.
대부분은 오히려 더 복잡해진다.
9. 하나의 대시보드에서 세 가지 상태 구분하기
지금까지의 내용을 하나로 모으면 대시보드는 다음과 같은 상태를 가지고 있을 수 있다.
UI 상태 — sidebarOpen, isFilterModalOpen, selectedTab, expandedRow
적합한 도구: useState / useReducer
서버 상태 — 대시보드 통계, 최근 주문, 사용자 목록, 매출 리포트, 모든 API 응답
적합한 도구: TanStack Query
애플리케이션 상태 — currentUser, permissions, selectedWorkspace, 전역 알림 개수, 애플리케이션 설정
적합한 도구: Zustand, Redux Toolkit 또는 다른 전역 상태 관리자
React Application
|
+----------------+----------------+
| | |
v v v
UI State Server State Application State
| | |
useState TanStack Query Zustand / Redux
useReducer | |
| API / Backend |
| |
+---------------+-----------------+
|
Components
모든 상태를 하나의 전역 스토어에 집어넣는 것보다 훨씬 깔끔한 구조다.
10. 흔한 실수: 서버 상태를 전역 스토어에 복사하기
다음 코드를 생각해 보자.
const { data } = useQuery({
queryKey: ["users"],
queryFn: fetchUsers
});
useEffect(() => {
dispatch(setUsers(data));
}, [data]);
이제 동일한 서버 상태를 중복해서 저장하고 있다.
API → TanStack Query → Redux → Component
TanStack Query가 이미 API 데이터를 관리하고 있는데 굳이 전역 스토어에 다시 복사해야 할 이유가 있을까?
이제 진실의 원천(source of truth)이 두 개가 된다.
TanStack Query: users = [A, B, C]
Redux/Zustand: users = [A, B]
어느 쪽이 맞는 데이터일까?
좋은 상태 아키텍처가 방지하려는 문제가 바로 이런 것이다.
더 나은 방법은 각각의 도구가 자신이 관리하도록 설계된 종류의 상태를 맡도록 하는 것이다.
11. 그래도 API 데이터를 전역 스토어에 넣어야 한다면?
애플리케이션이 의도적으로 서버에서 가져온 데이터를 전역 스토어에 저장해야 하는 경우도 있다.
데이터를 크게 변형해 애플리케이션 고유의 상태로 사용할 수도 있고, 아키텍처상의 제약 때문에 중앙 집중식 관리가 필요할 수도 있다.
핵심은 "API 데이터를 Redux나 Zustand에 절대로 넣지 마라" 가 아니다.
더 나은 원칙은 다음과 같다.
명확한 이유 없이 서버 상태를 중복하지 마라. 전역 스토어가 그 상태를 소유한다면 왜 그래야 하는지 알아야 한다. TanStack Query가 소유한다면 TanStack Query가 그대로 관리하도록 두어라.
문제는 라이브러리가 아니다.
문제는 상태의 소유권이 명확하지 않다는 데 있다.
12. 상태 관리보다 상태의 소유권이 더 중요하다
아마 이 글 전체에서 가장 중요한 내용일 것이다.
"Redux를 써야 할까, Zustand를 써야 할까?" 라고 묻기 전에 먼저 "이 상태는 누가 소유하는가?" 를 물어야 한다.
사이드바가 열려 있는가? → UI가 소유
서버에 어떤 사용자가 존재하는가? → 백엔드가 소유
현재 로그인한 사용자는 누구인가? → 애플리케이션에서 공유해야 함
이 사용자는 어떤 권한을 가지고 있는가? → 애플리케이션 전체 상태
사용자가 이 입력창에 무엇을 입력했는가? → UI가 소유
소유권이 명확해지면 어떤 도구를 선택할지도 훨씬 쉽게 결정할 수 있다.
13. 실용적인 의사결정 트리
새로운 상태를 만나면 다음 질문을 해보자.
백엔드가 이 데이터를 소유하는가?
→ 서버 상태. TanStack Query를 고려한다.
예: 사용자, 주문, 상품, 대시보드 통계, 리포트.
현재 UI에만 관련된 상태인가?
→ UI 상태. useState / useReducer를 고려한다.
예: 모달 표시 여부, 선택된 탭, 드롭다운 상태, 폼 입력값, 사이드바 표시 여부.
서로 관련 없는 여러 컴포넌트가 이 상태를 필요로 하는가?
→ 애플리케이션 상태. Zustand, Redux Toolkit, Context 또는 다른 전역 상태 관리자를 고려한다.
예: 현재 사용자, 권한, 워크스페이스, 전역 설정, 애플리케이션 전체 알림.
14. 전역 스토어가 있다는 이유만으로 사용하지 마라
규모가 큰 React 애플리케이션에서 흔히 볼 수 있는 패턴이다.
새로운 요구사항이 생긴다.
"이 값을 저장해야 합니다."
그러면 개발자는 곧바로 이렇게 생각한다.
slice를 만들고, action을 만들고, reducer를 만들고, dispatch하고, selector로 가져오자.
하지만 실제 요구사항은 다음 한 줄이면 충분했을 수도 있다.
const [isOpen, setIsOpen] = useState(false);
끝이다.
프로젝트의 규모가 크다고 해서 모든 상태 변수를 전역으로 관리해야 하는 것은 아니다.
큰 애플리케이션에도 로컬 상태는 얼마든지 많을 수 있다.
오히려 좋은 아키텍처는 실제로 공유해야 할 이유가 생기기 전까지 상태를 가능한 한 로컬에 유지하는 경우가 많다.
15. "모든 것이 전역"인 아키텍처를 피하라
user, dashboard, sidebar, modal, filters, forms, orders, products, notifications, API 응답, 로딩 상태, 일시적인 UI 값이 하나의 스토어에 모두 섞여 있다고 상상해 보자.
어느 순간 전역 스토어는 모든 것을 집어넣는 공간이 된다.
애플리케이션은 기술적으로 동작하겠지만 상태의 소유권을 이해하기는 점점 어려워진다.
단순히 로컬 변수 하나로 처리할 수도 있었던 모달을 수정하기 위해 개발자가 action, reducer, selector, middleware, component, effect까지 모두 살펴봐야 할 수도 있다.
이런 상황은 상태의 경계가 명확하지 않다는 신호다.
16. 더 나은 대시보드 아키텍처
앞에서 본 대시보드를 더 깔끔하게 구성하면 다음과 같다.
Dashboard
│
├── UI State
│ ├── selectedTab
│ ├── filterModalOpen
│ └── sidebarOpen
│
├── Server State
│ ├── dashboardStats
│ ├── recentOrders
│ └── salesReport
│
└── Application State
├── currentUser
├── permissions
└── selectedWorkspace
각 상태를 도구와 연결하면 다음과 같다.
UI 상태 → useState / useReducer
서버 상태 → TanStack Query
애플리케이션 상태 → Zustand 또는 Redux Toolkit
소유권이 명확하면 복잡성도 줄어든다.
17. 상태 관리는 하나의 라이브러리를 선택하는 문제가 아니다
흔히 이런 질문을 한다.
"Redux, Context, Zustand, TanStack Query 중 무엇을 사용해야 할까?"
하지만 출발점부터 잘못된 질문이다.
하나의 라이브러리가 모든 것을 관리해야 한다는 규칙은 없다.
실제 애플리케이션에서는 여러 도구를 동시에 사용할 수 있다.
작은 로컬 UI 상태에는 useState, 애플리케이션 상태에는 전역 스토어, 서버 상태에는 TanStack Query, 복잡한 폼 상태에는 폼 라이브러리를 사용할 수 있다.
목표는 사용하는 라이브러리의 수를 최소화하는 것이 아니다.
각 종류의 상태에 올바른 소유자를 지정하는 것이 목표다.
18. 간단한 기준
Who owns the data?
|
+-------------+-------------+
| | |
v v v
Frontend Backend Shared App
| | |
v v v
UI State Server State Application State
| | |
v v v
useState TanStack Query Zustand / Redux Toolkit
모든 아키텍처에 적용되는 절대적인 규칙은 아니지만, 좋은 출발점으로 사용할 수 있다.
19. 진짜 문제는 대개 라이브러리가 아니다
상태 관리가 복잡해지면 개발자들은 종종 라이브러리를 탓한다.
하지만 많은 상태 관리 문제는 사실 상태 분류의 문제다.
-
문제: 대시보드 API 데이터가 오래됐다.
잘못된 대응: Redux action을 하나 더 추가한다.
실제 문제: 서버 상태를 애플리케이션 상태처럼 다루고 있다. -
문제: 모달 상태 하나를 바꾸는 데 여러 전역 스토어 action이 필요하다.
잘못된 대응: 추상화 계층을 더 만든다.
실제 문제: 로컬 UI 상태를 불필요하게 전역 상태로 옮겼다. -
문제: 두 컴포넌트가 서로 다른 사용자 데이터 복사본을 가지고 있다.
실제 문제: 진실의 원천이 여러 개다.
항상 라이브러리가 문제인 것은 아니다.
때로는 아키텍처가 문제다.
20. 실용적인 체크리스트
새로운 상태 변수를 추가하기 전에 다음을 확인해 보자.
소유권(Ownership) — 이 데이터는 누가 소유하는가? 프론트엔드인가, 백엔드인가, 애플리케이션인가?
범위(Scope) — 하나의 컴포넌트만 필요한가? 부모와 자식 컴포넌트가 필요한가? 애플리케이션 전체에서 필요한가?
수명(Lifetime) — 컴포넌트가 마운트되어 있는 동안에만 존재하는가? 페이지를 이동해도 유지되어야 하는가? 영속화(persist)가 필요한가?
출처(Source) — API에서 가져온 데이터인가? 다른 사용자가 변경할 수 있는가? 오래된 데이터가 될 수 있는가?
도구(Tool) — useState면 충분한가? TanStack Query가 관리해야 하는가? 정말 전역 스토어가 필요한가? Context만으로 충분한가?
라이브러리부터 정하는 것보다 이런 질문에서 시작하는 편이 대개 더 나은 결정을 이끌어낸다.
결론
UI 상태, 서버 상태, 애플리케이션 상태는 모두 "상태"라고 부르지만 서로 매우 다르게 동작한다.
그리고 가장 중요한 차이는 소유권이다.
- UI 상태 → 프론트엔드가 제어한다.
- 서버 상태 → 백엔드가 소유한다.
- 애플리케이션 상태 → 애플리케이션의 여러 영역에서 필요로 한다.
이 상태들은 서로 다른 특성을 가지고 있기 때문에 자동으로 같은 방식으로 관리해서는 안 된다.
일반적인 React 대시보드라면 다음과 같이 나눌 수 있다.
UI 상태 → useState / useReducer
서버 상태 → TanStack Query
애플리케이션 상태 → Zustand, Redux Toolkit 또는 비슷한 전역 상태 관리자
가장 큰 실수는 "잘못된" 라이브러리를 선택하는 것이 아니다.
더 큰 실수는 자신이 어떤 종류의 상태를 관리하고 있는지 모르는 것이다.
reducer를 추가하거나, slice를 만들거나, 또 다른 상태 관리 라이브러리를 설치하기 전에 한 가지 질문부터 해보자.
이 상태는 누가 소유하는가?
그 답을 알게 되면 올바른 도구를 선택하는 일도 훨씬 쉬워진다.
좋은 React 아키텍처는 모든 것을 한곳에 모으는 것이 아니다.
적절한 상태를 적절한 위치에 두는 것이다.