React 라이브러리 조합 선택 핵심 요약
React 프로젝트의 도구 선택은 유행 순위가 아니라 상태의 소유권, 서버 동기화, UI 접근성, 폼 검증을 분리하는 일입니다. 클라이언트 공유 상태는 Zustand나 Redux Toolkit, 서버 원본 데이터는 TanStack Query, 소유 가능한 UI 코드는 shadcn/ui, 낮은 수준의 접근성 primitive는 Radix를 우선 검토합니다. 단순한 화면은 React 자체 기능만으로 끝내는 선택도 정상입니다.
검증 기준: 2026-07-19, 각 프로젝트의 공식 최신 문서 기준입니다. 라이브러리는 빠르게 바뀌므로 실제 도입 전에는 package.json과 lockfile, 마이그레이션 문서를 다시 확인하세요. 이 글의 추천은 편집 판단이며 모든 팀에 통용되는 성능 보장은 아닙니다.
- 상태 소유권부터 나누기
- 조건·장점·제약·권장 상황 비교
- TanStack Query 캐시와 무효화
- shadcn/ui와 Radix 역할
- 폼과 스키마 검증
- 도입 순서와 결론
- 공식 근거와 다음 학습
1. 상태 소유권부터 나누면 조합이 단순해집니다

컴포넌트 안에서 끝나는 값은 먼저 React에 둡니다
입력창 임시 값, 한 컴포넌트의 열림 상태, 가까운 부모·자식만 공유하는 값은 useState나 Context로 충분할 수 있습니다. 전역 store를 먼저 만들면 데이터의 실제 소유자가 흐려지고 테스트 범위가 커집니다. 여러 화면에서 같은 클라이언트 상태를 읽고 갱신해야 할 때만 외부 store를 검토합니다.
서버가 원본인 값은 일반 전역 상태와 분리합니다
상품 목록, 사용자 프로필, 게시글처럼 서버가 원본인 값에는 로딩·실패·재시도·캐시·재검증이 따라옵니다. 같은 응답을 Zustand와 TanStack Query 양쪽에 복사하면 어느 쪽이 최신인지 결정하기 어려워집니다. 서버 응답은 Query 캐시에 두고, 선택된 탭이나 편집 중 임시 값처럼 클라이언트가 소유한 상태만 store에 두는 편이 안전합니다.
2. 조건·장점·제약·권장 상황 비교표

| 선택지 | 조건 | 장점 | 제약 | 권장 상황 |
|---|---|---|---|---|
| React state·Context | 범위가 작고 갱신 규칙이 단순함 | 추가 의존성 없음 | 빈번한 전역 갱신·복잡한 파생 상태에는 설계 부담 | 폼 일부, 모달, 테마처럼 좁은 상태 |
| Zustand | 여러 컴포넌트가 작은 클라이언트 상태를 공유 | hook 기반 API와 적은 보일러플레이트 | 팀 규칙을 직접 정해야 하며 서버 캐시 기능은 목적이 아님 | 장바구니 UI, 필터, 편집기 임시 상태 |
| Redux Toolkit | 액션 흐름·미들웨어·DevTools·팀 규약이 중요 | 공식 권장 Redux 도구와 예측 가능한 구조 | 작은 앱에는 구조 비용이 더 큼 | 큰 팀, 복잡한 도메인 이벤트, 감사 가능한 변경 흐름 |
| TanStack Query | 원본이 API에 있고 캐시·재요청이 필요 | 서버 상태 생명주기를 일관되게 처리 | query key·staleTime·무효화 규칙을 설계해야 함 | 목록·상세·사용자 정보·mutation 후 동기화 |
Zustand 공식 문서는 hook 기반 store와 selector 구독을 보여주고, Redux Toolkit은 현재 Redux 로직을 작성하는 표준 방식이라고 명시합니다. 실제 API와 팀 운영 방식은 Zustand 공식 문서, Redux Toolkit 시작 문서에서 확인하세요.
3. TanStack Query는 캐시 기본값과 무효화를 함께 설계합니다
stale과 삭제를 같은 개념으로 보지 않습니다
공식 기본값에서는 query 데이터가 기본적으로 stale로 취급되어 mount, 창 focus, 네트워크 재연결 같은 조건에서 백그라운드 refetch가 발생할 수 있습니다. staleTime은 “얼마 동안 새 데이터로 볼지”를 정하며, inactive query의 정리 시점은 별도 설정입니다. 요청이 많다는 이유만으로 캐시를 제거하기보다 데이터 변경 주기에 맞춰 staleTime을 먼저 조정합니다. 근거는 TanStack Query Important Defaults입니다.
mutation 성공 뒤에는 관련 query key를 명시적으로 무효화합니다
무효화는 일치하는 query를 stale로 표시하고, 현재 화면에서 관찰 중이라면 백그라운드 refetch를 유도합니다. 문자열을 여기저기 복사하지 말고 query key factory 또는 상수로 관리하면 빠뜨릴 가능성이 줄어듭니다. 공식 동작은 TanStack Query Query Invalidation에서 확인할 수 있습니다.
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
const todosKey = ['todos'] as const;
export function TodoList() {
const queryClient = useQueryClient();
const todos = useQuery({
queryKey: todosKey,
queryFn: fetchTodos,
staleTime: 60_000,
});
const addTodo = useMutation({
mutationFn: createTodo,
onSuccess: () => queryClient.invalidateQueries({ queryKey: todosKey }),
});
// 실제 UI에서는 loading/error/empty 상태를 각각 렌더링합니다.
return null;
}
4. shadcn/ui와 Radix는 같은 층의 선택지가 아닙니다

shadcn/ui는 설치 후 코드 소유권과 유지보수 책임을 함께 가져옵니다
shadcn/ui는 전통적인 단일 npm 컴포넌트 패키지라기보다 실제 컴포넌트 코드를 프로젝트로 배포하는 방식입니다. 커스터마이징이 쉽지만, 수정한 코드와 upstream 변경을 팀이 관리해야 합니다. shadcn/ui 공식 소개의 “Open Code” 설명을 기준으로 판단하세요.
2026년에는 “shadcn/ui는 항상 Radix 기반”이라고 단정하면 안 됩니다
공식 변경 기록은 Radix와 Base UI 중 primitive를 고를 수 있는 흐름을 안내합니다. 따라서 생성된 컴포넌트의 import와 CLI 설정을 확인해야 합니다. shadcn/ui 2026 Base UI 변경 기록을 기준으로 기존 설명을 교정했습니다. Radix 자체는 접근성·키보드·focus 관리에 집중한 낮은 수준의 primitive이며, 세부 역할은 Radix Primitives 공식 소개에서 확인할 수 있습니다. 어느 쪽을 쓰더라도 실제 라벨, 키보드 순서, 화면낭독기 동작은 제품 문맥에서 다시 테스트해야 합니다.
5. 폼은 필드 상태와 데이터 계약을 분리합니다

검색창처럼 필드가 적으면 React state로 충분합니다. 필드가 많고 touched·dirty·오류 표시를 일관되게 다뤄야 하면 React Hook Form을 검토합니다. Zod는 런타임 입력을 파싱해 데이터 계약을 확인하는 도구입니다. 타입 추론만 믿지 말고 API 경계에서 실제로 parse하며, 서버도 별도로 검증해야 합니다. 시작 API는 React Hook Form 공식 시작 문서, Zod 공식 문서에서 확인하세요.
6. 도입 순서와 명시적 결론

- 현재 문제를 “클라이언트 공유 상태·서버 상태·UI primitive·폼 계약” 중 하나로 분류합니다.
- React 자체 기능으로 해결 가능한지 먼저 확인합니다.
- 후보 하나로 작은 화면을 구현하고 번들·접근성·테스트·팀 학습 비용을 기록합니다.
- query key, store 경계, 컴포넌트 소유권, 스키마 위치를 짧은 결정 기록으로 남깁니다.
- 도입 후 사용하지 않는 중복 상태와 래퍼를 제거합니다.
결론: 서버 데이터가 있다면 TanStack Query 같은 서버 상태 계층을 먼저 정하고, 남은 클라이언트 공유 상태가 실제로 있을 때 Zustand 또는 Redux Toolkit을 선택하세요. UI는 빠른 출발보다 수정·업데이트 책임을 감당할 수 있는지를 기준으로 고릅니다. 다음 행동은 현재 화면의 상태 목록을 작성해 소유권 4종으로 분류하는 것입니다.
공식 근거와 다음 학습 경로
내부 학습 순서
실습 파일
파일 경로를 확인하고 같은 프로젝트 안에 저장하세요. 이미지 등 소스 목록에 없는 파일과 실행 안내는 실습 ZIP에 포함되어 있습니다.
전체 코드
exercise.js
// 학습용 판단 모델: 모든 제품에 적용되는 자동 추천기가 아닙니다.
export function chooseOwner({ remote = false, shared = false, complex = false } = {}) {
if (remote) return 'server-cache';
if (complex) return shared ? 'reducer-context' : 'reducer';
return shared ? 'lift-state' : 'local-state';
}
exercise.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { chooseOwner } from './exercise.js';
test('source ownership precedes tool choice', () => {
assert.equal(chooseOwner(), 'local-state');
assert.equal(chooseOwner({ shared: true }), 'lift-state');
assert.equal(chooseOwner({ complex: true, shared: true }), 'reducer-context');
assert.equal(chooseOwner({ remote: true, shared: true }), 'server-cache');
});
package.json
{
"name": "react-decision-407",
"private": true,
"type": "module",
"engines": {
"node": ">=22.12.0"
},
"scripts": {
"test": "node --test"
}
}
도구를 설치하기 전에 풀어볼 선택 문제
이 글에 나오는 도구를 전부 설치하지 않습니다. 먼저 원본이 서버인지, 몇 컴포넌트가 공유하는지, 변경 규칙이 복잡한지 적으세요. 검색창 하나는 useState로, 가까운 형제의 공유는 state 올리기로 시작할 수 있습니다. Context는 전달 경로, reducer는 변경 규칙을 다룹니다.
| 문제 | 우선 설계 | 추가 도구의 도입 근거 |
|---|---|---|
| 두 필드 검색 | 로컬 state와 파생값 | 복잡한 검증·dirty 관리가 생기면 폼 도구 |
| 상품 API | 서버가 원본인 데이터 | 캐시·재요청·무효화가 반복되면 Query |
| 복잡한 편집 작업 | action과 reducer | 화면 경계를 넘는 구독이 많아지면 store 검토 |
ZIP은 추가 의존성이 없는 Node 판단 연습입니다. npm test는 원본 위치를 우선하는 분기 규칙을 확인합니다. 이 함수는 학습 모델이며 실제 팀 요구를 자동으로 판정하지 않습니다. 요구가 없는데 패키지를 설치하는 습관 대신 선택 이유를 한 문장씩 남깁니다.
아래 ZIP은 이 글의 핵심 동작을 직접 확인하는 독립 실습입니다. 압축을 풀고 README의 순서대로 실행하세요. 실습 코드 ZIP 다운로드
라이브러리 연결 학습 과정
React의 컴포넌트·props·state·이벤트와 TypeScript 객체·배열 타입을 알고 시작합니다. 도구를 한 번에 조합하기보다 각 단계의 확인 과제를 수행한 다음 다음 단계로 이동하세요.
| 순서 | 학습 시작 글 | 완료 기준 |
|---|---|---|
| 1 | Tailwind 기본·레이아웃 | 간격과 배치 축을 설명하고 반응형 카드를 조정합니다. |
| 2 | UI 선택·경로 설정 | Radix와 shadcn/ui의 역할, 입력과 상호작용 검수 범위를 구분합니다. |
| 3 | Zustand 기초 | 지역·공유 상태를 나누고 store와 action을 연결합니다. |
| 4 | 선택 구독·저장 | selector·미들웨어·persist의 역할과 구독 해제를 확인합니다. |
| 5 | 서버 상태 조회 | Provider·queryFn·queryKey와 로딩·오류·빈 결과를 처리합니다. |
| 6 | 캐시와 변경 요청 | 무효화·응답 반영·낙관적 변경을 비교하고 실패를 복구합니다. |
| 7 | 동기화 예외 점검 | 동시 수정과 비활성 캐시의 제약을 확인합니다. |
공통 실습 프로젝트
전체 코드 ZIP 다운로드 후 README의 설치 명령을 실행하세요. 라우팅, 입력·알림, 복합 UI, 미들웨어, 로컬 Todo, 서버 Todo 메뉴로 나누어 두었습니다. package-lock.json으로 실습 버전을 고정했습니다.
UI 파일은 Radix·Sonner·Embla 기반의 축약 학습용 구현입니다. shadcn CLI 생성물 전체와 같지는 않습니다. 자동 테스트 7개·타입 검사·빌드·실제 로컬 HTTP CRUD를 확인했으며 실제 브라우저 접근성·모바일 화면은 별도 확인 대상입니다.
필수 과정과 선택 심화
우선 일반적인 목록 캐시로 조회와 변경 요청을 완성합니다. 목록에는 ID만, 상세에는 객체만 저장하는 수동 정규화는 선택 심화입니다. 데이터 중복이 줄어드는 대신 캐시 수명과 disabled 쿼리의 동기화를 직접 책임져야 합니다. 정규화를 적용했다는 사실만으로 서버와 항상 일치하는 것은 아닙니다.
이 글이 도움이 되었나요?
React 학습 순서
필수 19개 · 전체 23개
읽음 기록 관리
전체 과정 목차 (23개)
- 필수 길잡이 · React 학습 순서: 컴포넌트·props·state부터 상태관리까지
- 필수 학습 · React Vite 사용법: Vite 8 프로젝트 생성·실행·빌드
- 필수 학습 · React 컴포넌트 개념 정리: UI 재사용 구조 잡기
- 필수 학습 · React JSX 문법 사용법: 조건부 렌더링과 리스트 처리
- 필수 학습 · React props 사용법: 부모에서 자식으로 데이터 전달하는 구조
- 필수 학습 · React useState 사용법: state와 객체 배열 업데이트 기준
- 선택 참고 · React state 업데이트 안됨 문제 해결
- 필수 선수 · React 리스트 key 경고 해결 기준: index key를 피해야 하는 이유
- 필수 학습 · React 제어 컴포넌트 폼과 상태 끌어올리기 첫 실습 가이드
- 필수 학습 · React useRef 실습: 입력 포커스와 state의 역할 나누기
- 필수 선수 · React useEffect 두 번 실행되는 이유: StrictMode·API 중복 해결
- 선택 참고 · React Maximum update depth exceeded 오류 해결: 무한 렌더링 원인 찾기
- 선택 참고 · React Cannot update 오류 해결: 렌더링 중 setState 원인
- 필수 학습 · React useReducer와 Context 실습: 작업 목록 상태를 여러 컴포넌트에서 공유하기
- 필수 학습 · React 컴포넌트 props 타입 지정하기: 부모와 자식 사이의 값 구조 잡기
- 선택 참고 · React Hook Form 에러 메시지 표시 문제 해결: validation이 안 보일 때 체크리스트
- 필수 길잡이 · React 라이브러리 조합 가이드: Zustand·TanStack Query·shadcn/ui 선택 기준 현재 글
- 필수 학습 · Next.js 커스텀 훅 설계: 프론트엔드 상태 관리 구조 잡기
- 필수 학습 · React Compiler 기준: useMemo useCallback 언제 줄일까
- 필수 학습 · React·TypeScript 검색 필터 만들기: 상태와 결과 목록 연결
- 필수 학습 · React Router v7 실습: BrowserRouter부터 Layout·Outlet·상세 경로까지
- 필수 학습 · shadcn/ui 시작 실습: Vite 설정·components.json·입력 폼·Sonner
- 필수 학습 · shadcn/ui 복합 컴포넌트 실습: Dialog·Popover·Carousel과 접근성
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.