REPO DEEP DIVE · 2026-07-29 · ADOBE/REACT-SPECTRUM · 3층 설계로 만든 접근성 컴포넌트 왕국

adobe/react-spectrum 딥다이브
"상태 · 동작 · 디자인"을 완전히 분리한 컴포넌트 모노레포

react-spectrum은 Adobe가 만드는 단일 저장소, 4개 라이브러리다. "어떻게 상태가 바뀌는가(React Stately)" · "어떻게 접근 가능하게 동작하는가(React Aria)" · "어떻게 보이는가(React Spectrum / S2)"를 층층이 분리해, 같은 로직 위에 완전히 다른 디자인을 자유롭게 얹을 수 있게 만들었다. 그 자체가 하나의 UI 킷이 아니라, UI 킷을 만드는 재료 창고에 가깝다.

가장 큰 특징은 "컴포넌트 하나(예: Select)가 상태 훅 → ARIA 훅 → 스타일 컴포넌트, 3단계를 순서대로 통과하며 완성된다"는 점이다. 각 단계는 독립적으로 npm에 배포되고, 독립적으로 테스트되며, 독립적으로 재사용 가능하다. 그래서 Adobe 자체 제품(Photoshop 웹, Adobe Express 등)뿐 아니라 완전히 다른 디자인 시스템을 만들고 싶은 팀도 React Aria 훅만 가져다 쓸 수 있다.

(저장소 adobe/react-spectrum · 라이선스 Apache-2.0 · 모노레포 루트 버전 2.21.0-beta.0 · React 19.2 지원(16~19 전부 테스트) · @react-stately(33개) · @react-aria(55개) · @react-spectrum(66개) 스코프 패키지 + react-aria-components(선언형 레이어, 컴포넌트 69종) + @internationalized/* · 소스 TS/TSX 약 2,549개(테스트·스토리 제외), 전체 파일 1만여 개 · 빌드 yarn 4 + Lerna + Parcel + Make · CI는 CircleCI(잡 30여 개) · 최신 커밋 1259843b(2026-07-29))
목차
  1. 한 줄 정체
  2. 왜 지금 뜨는가
  3. 무엇을 해결하나
  4. 전체 구조
  5. 핵심 설계·동작 원리
  6. 기술 스택·의존성
  7. 설치·요구사항
  8. 실제 사용 흐름
  9. 함정·오해 방지
  10. 대안 비교
  11. 정리 — 누가 쓰면 좋은가

1한 줄 정체

한 문장으로: "상태 로직·접근성 동작·디자인 스타일을 3층으로 쌓아 만든, 컴포넌트계의 반도체 팹"

react-spectrum은 컴포넌트를 "완제품"이 아니라 "재료·중간재·완제품"의 3단 생산 라인으로 나눠 파는 저장소다. 예를 들어 드롭다운 하나를 만들 때, ① @react-stately/select가 "지금 뭐가 선택됐는지, 메뉴가 열렸는지"를 관리하고, ② @react-aria/select가 거기에 "키보드 화살표·포커스·스크린리더 안내"를 씌워 순수 DOM 속성(props)만 돌려주고, ③ @react-spectrum/picker 또는 react-aria-components<Select>가 그 위에 실제 화면을 그린다. 세 층 모두 따로 npm에 배포되고 따로 테스트된다.

이 구조 덕에 Adobe는 "같은 로직 위에 Spectrum 1(구 디자인)과 Spectrum 2(신 디자인)를 동시에" 얹을 수 있고, 외부 팀은 "Adobe 디자인은 필요 없고 접근성 로직만" 가져다 자기 디자인 시스템을 만들 수 있다. React Aria 자체가 "unstyled(스타일 없는) 컴포넌트" 생태계의 대표 주자로 꼽히는 이유다.

한 줄 비유

"뼈대(상태) · 신경(동작) · 피부(디자인)"를 따로 만들어 조립하는 인체 공장

보통 UI 라이브러리는 뼈대·신경·피부가 한 몸에 붙어 나온다. 마음에 안 드는 피부(디자인)를 바꾸려면 몸 전체를 다시 만들어야 한다.

react-spectrum은 뼈대(React Stately), 신경계(React Aria), 피부(React Spectrum/S2, 또는 당신의 디자인)를 따로 파는 조립 부품점이다. 뼈대와 신경은 그대로 둔 채 피부만 통째로 갈아 끼울 수 있다.

2왜 지금 뜨는가

"unstyled 컴포넌트" 트렌드의 원조 · AI 시대 접근성 회귀 · Spectrum 2 전환기

① "unstyled 컴포넌트" 붐의 뿌리

Radix Primitives·Headless UI·Base UI·Ark UI 같은 "스타일 없이 동작만 주는" 컴포넌트 라이브러리가 최근 프론트엔드의 표준 문법이 됐다. React Aria는 이 흐름을 2020년부터 밀어붙인 선구자 중 하나다. 다른 라이브러리들이 여기 합류하면서 오히려 "react-spectrum의 접근성 깊이"가 재조명받고 있다.

② AI 코드 생성 시대의 "접근성 부채" 문제

AI가 UI 코드를 순식간에 찍어내는 시대에, 키보드 내비게이션·스크린리더 라벨·포커스 관리 같은 디테일은 오히려 더 쉽게 누락된다. React Aria처럼 "동작을 훅으로 캡슐화"해두면 AI가 만든 커스텀 UI에도 검증된 접근성 로직을 그대로 꽂을 수 있어, 최근 AI 코딩 워크플로에서 재조명되고 있다. 실제로 이 저장소에는 packages/dev/mcp/react-aria·packages/dev/mcp/s2라는 공식 MCP(Model Context Protocol) 서버가 포함돼, AI 에이전트가 React Aria/S2 문서를 직접 조회하며 코드를 짤 수 있게 되어 있다.

③ Spectrum 2(S2) 전환과 스타일 매크로 실험

Adobe는 자체 디자인 시스템을 Spectrum 2로 새로 그리는 중이며, @react-spectrum/s2 패키지가 그 결과물이다. 여기서 쓰는 컴파일 타임 스타일 매크로(style macro)는 "런타임 CSS-in-JS 없이, 빌드 시점에 원자적 CSS로 컴파일"하는 실험적 접근이라 프론트엔드 성능 커뮤니티에서 자주 언급된다.

경쟁 지형
"완제품 UI 킷"과 "재료 창고"의 차이

MUI·Chakra·Mantine 같은 완제품 킷은 빠르게 시작하기 좋지만 로직과 디자인이 붙어있다. react-spectrum(정확히는 그 안의 React Aria)은 재료만 파는 쪽이다. Radix·Base UI·Ark UI도 같은 철학이지만, react-spectrum은 Adobe 실제 제품(수백 개 화면)에서 10년 가까이 검증된 접근성 세부사항완전한 국제화(30개 이상 언어, 9종 달력)까지 포함한다는 깊이가 다르다.

3무엇을 해결하나

"접근성은 어렵다" · "디자인 시스템 교체는 전면 재작성" · "국제화는 나중에" 세 가지 고질병
문제 ①

웹 접근성(a11y)은 혼자 구현하기 매우 어렵다

키보드로만 메뉴를 열고 닫고 화살표로 이동하고, 스크린리더가 "몇 개 중 몇 번째 항목"이라고 읽어주고, 포커스가 열린 팝업 안에 갇혔다가(focus trap) 닫히면 원래 자리로 돌아오는 것 — 이걸 매번 새로 짜면 버그투성이가 된다. React Aria는 WAI-ARIA 명세를 코드로 미리 구현해, useSelect() 한 번 호출로 이 모든 걸 해결한다.

문제 ②

디자인 시스템을 바꾸면 로직까지 다 새로 짜야 한다

보통 UI 킷은 컴포넌트를 바꾸면 상태 관리·키보드 로직까지 통째로 새로 짜야 한다. react-spectrum은 상태(Stately)·동작(Aria)을 디자인(Spectrum)과 분리해 둬서, Spectrum 1 → Spectrum 2 전환처럼 큰 리디자인도 아래 두 층은 그대로 두고 위 껍데기만 바꿔 끼울 수 있다.

문제 ③

국제화(i18n)는 항상 "나중에 붙이는 애물단지"가 된다

날짜·숫자 포맷, RTL(오른쪽에서 왼쪽 언어) 레이아웃, 지역별 달력 체계는 나중에 붙이려면 컴포넌트 구조 자체를 바꿔야 하는 경우가 많다. react-spectrum은 @internationalized/date·number맨 처음부터 상태 로직 안에 내장해, 그레고리력뿐 아니라 불교력·에티오피아력·헤브루력·인도력·이슬람력·일본력·페르시아력·타이완력까지 9종을 기본 지원한다.

4전체 구조

모노레포 하나, 4개 라이브러리, 220개 이상의 독립 패키지

packages/ 아래에 라이브러리별로 스코프가 나뉜다. 실제 클론해서 세어보면 @react-stately 33개, @react-aria 55개, @react-spectrum 66개 스코프 패키지가 있고, 여기에 react-aria-components(선언형 레이어, 컴포넌트 소스 파일 69개), @internationalized/*(date·number·message·string·string-compiler), @react-types/*(순수 타입), @spectrum-icons/*가 더해진다.

┌────────────────────────────────────────────────────────────────┐ │ REACT-SPECTRUM 모노레포 전체 지도 │ └────────────────────────────────────────────────────────────────┘ packages/ ├── react-stately/ # "번들 패키지" — 실제 훅 소스가 여기 있음 │ └── src/select/useSelectState.ts ... ├── react-aria/ # "번들 패키지" — 실제 훅 소스가 여기 있음 │ └── src/select/useSelect.ts ... ├── react-aria-components/ # 선언형 JSX 레이어(RAC) — <Select><Button>... │ ├── @react-stately/* (33개) # select, list, table, calendar, color, dnd ... │ └── select/src/index.ts → "export {useSelectState} from 'react-stately/useSelectState'" ├── @react-aria/* (55개) # select, listbox, menu, focus, i18n, virtualizer ... │ └── select/src/index.ts → "export {useSelect} from 'react-aria/useSelect'" ├── @react-spectrum/* (66개) # picker, table, combobox, s2(Spectrum 2), theme-* ... │ ├── @internationalized/ date · number · message · string · string-compiler ├── @react-types/* # 순수 타입 정의(.d.ts) — 모든 층이 공유 ├── @spectrum-icons/* # SVG 아이콘 → React 컴포넌트 ├── @adobe/react-spectrum # 완성형 "올인원" 패키지(v3.47.2, "S1") └── dev/mcp/{react-aria,s2} # AI 에이전트용 공식 MCP 문서 서버

핵심 포인트: @react-stately/select·@react-aria/select 같은 스코프 패키지들은 껍데기다. 진짜 훅 구현은 packages/react-stately/src/packages/react-aria/src/라는 "번들 패키지"에 있고, 스코프 패키지는 그 파일을 package.jsonexports 서브패스 매핑을 통해 재수출만 한다. (이 구조는 5장·9장에서 실제 코드로 추적한다.)

5핵심 설계·동작 원리

Select/Picker 하나를 3층 끝까지 추적 · Collections API · 가상화 · 국제화 · usePress

① 3층 아키텍처를 실제 소스로 추적하기 — "Select"

가장 좋은 증명은 실제로 하나의 컴포넌트를 끝까지 따라가는 것이다. 여기서는 드롭다운 선택 컴포넌트(Spectrum에서는 Picker라는 이름)를 추적한다.

1단계 — 상태만 아는 useSelectState (React Stately)

packages/react-stately/src/select/useSelectState.tsDOM도, ARIA도, 렌더링도 전혀 모른다. 하는 일은 딱 세 가지: useListState로 항목 컬렉션과 선택 상태를 관리하고, useOverlayTriggerState로 "메뉴가 열렸는지"를 관리하고, useFormValidationState로 폼 유효성 상태를 관리한 뒤, 이 셋을 하나의 SelectState 객체로 합쳐 돌려준다.

export function useSelectState<T, M>(props): SelectState<T, M> {
  let triggerState = useOverlayTriggerState(props);       // 열림/닫힘
  let listState = useListState({ ...props, selectionMode }); // 선택/컬렉션
  let validationState = useFormValidationState({ ...props });  // 폼 검증
  return { ...validationState, ...listState, ...triggerState, value, setValue, open, toggle };
}

이 함수만 떼어서 React Native나 서버 컴포넌트 상태 관리에 그대로 쓸 수 있을 만큼 플랫폼 독립적이다.

2단계 — 동작·접근성을 입히는 useSelect (React Aria)

packages/react-aria/src/select/useSelect.ts는 위 state와 DOM ref를 받아, 순수 DOM 속성 객체(props bag)만 돌려준다. 내부적으로 useMenuTrigger(버튼-메뉴 연결), useTypeSelect(문자 입력으로 항목 점프), useField(라벨·에러 연결), ListKeyboardDelegate(방향키 이동 규칙)를 조합하고, ArrowLeft/ArrowRight로 값을 순환시키는 단축키까지 직접 등록한다. 반환값은 triggerProps·labelProps·menuProps·valueProps·descriptionProps·errorMessageProps·hiddenSelectProps — 전부 <button {...triggerProps}>처럼 그대로 엘리먼트에 뿌릴 수 있는 순수 객체다. 여기엔 CSS 클래스도, 화면에 그려지는 모양도 전혀 없다.

3단계 — 디자인을 입히는 완제품 컴포넌트 (React Spectrum / RAC / S2)

같은 두 훅(useSelectState + useSelect) 위에 세 가지 다른 옷이 입혀진다.

완제품실제 소스 위치입히는 것
Spectrum 1 Picker@adobe/react-spectrum/src/picker/Picker.tsxSpectrum CSS(vars.css) 클래스, 데스크탑은 Popover·모바일은 Tray로 자동 전환, 가상화된 리스트박스
RAC <Select>react-aria-components/src/Select.tsx스타일 없음. 대신 className/style에 render prop 함수를 받고, Context로 하위 컴포넌트에 상태 전파
Spectrum 2 Picker@react-spectrum/s2/src/Picker.tsx컴파일 타임 style 매크로(style({...}))로 원자적 CSS 생성 — 런타임 CSS-in-JS 없음

세 완제품 모두 import {useSelect} from 'react-aria/useSelect'import {useSelectState} from 'react-stately/useSelectState'토씨 하나 다르지 않게 그대로 가져다 쓴다. 다른 것은 오직 "그 결과를 어떤 태그·어떤 클래스로 그리느냐"뿐이다.

비유

연극의 대본(Stately)·연출 지시(Aria)·무대 의상(Spectrum)을 떠올리면 된다. 대본은 "누가 언제 등장해 무슨 대사를 하는지"만 정한다. 연출 지시는 "이 대사를 할 때 몸을 어느 방향으로 돌리고, 조명이 어디를 비추는지"를 정한다. 의상은 완전히 갈아입혀도 대본과 연출은 그대로다 — Spectrum 1의 연극과 Spectrum 2의 연극은 배우(로직)가 똑같은데 의상만 다른 것이다.

용어
React Aria (동작 훅)
키보드·마우스·터치·스크린리더 상호작용을 처리해, DOM 엘리먼트에 그대로 뿌릴 수 있는 props 객체(onKeyDown, role, aria-* 등)만 돌려주는 훅 모음. 화면에 아무것도 그리지 않는다.
용어
React Stately (상태 훅)
선택값·열림 상태·컬렉션 등을 관리하는 순수 로직 훅 모음. DOM도 ARIA도 모른다. React Native 등 웹이 아닌 환경에서도 그대로 재사용 가능하다.

② Collections API — "리스트를 리스트답게 다루는" 공통 언어

Select·ListBox·Menu·Table·GridList·Tabs 등 "항목이 여러 개인" 모든 컴포넌트는 같은 컬렉션 모델을 공유한다. 사용자는 <Item>/<Section>(정적) 또는 배열 + 렌더 함수(동적, items={data} children={item => <Item>...</Item>})로 항목을 선언하고, 내부적으로 CollectionBuilder가 이를 Node<T> 트리로 변환한다. @react-types/shared/collections.d.tsItemProps·AsyncLoadable(isLoading·onLoadMore) 같은 공통 타입이 정의돼, 모든 컴포넌트가 같은 방식으로 무한 스크롤·비동기 로딩을 지원한다.

③ 가상화(Virtualizer) — 수만 개 항목도 가볍게

@react-stately/virtualizer(레이아웃 계산: 어떤 항목이 어디 위치하는지 순수 계산)와 @react-aria/virtualizer(그 계산 결과를 실제 DOM에 붙이고 스크롤·포커스를 관리)로 나뉜다. Table·큰 ListBox·Calendar처럼 항목 수가 많은 컴포넌트가 이를 사용해, 화면에 보이는 행만 실제 DOM에 그리고 나머지는 가상 좌표로만 계산한다. 여기서도 "레이아웃 계산(상태)"과 "DOM 반영(동작)"이 각각 Stately/Aria로 나뉘는 동일한 철학이 반복된다.

④ 국제화 — 날짜·숫자·문자열·RTL을 프레임워크 독립적으로

@internationalized/date는 리액트에 의존하지 않는 순수 날짜 계산 라이브러리다. CalendarDate 클래스 하나로 그레고리력뿐 아니라 불교력·에티오피아력·헤브루력·인도력·이슬람력(여러 변형 포함)·일본력·페르시아력·타이완력까지 9종의 달력 시스템을 계산한다(packages/@internationalized/date/src/calendars/). @internationalized/numberNumberParserlatn·arab·hanidec(한자)·deva(데바나가리)·beng(벵골)·fullwide 6개 숫자 체계를 파싱한다. RTL(오른쪽에서 왼쪽 언어)은 I18nProviderisRTL(locale) 결과로 direction: 'rtl'|'ltr'을 context에 심어, 모든 하위 컴포넌트가 자동으로 좌우 반전된다.

용어
RTL (Right-to-Left)
아랍어·히브리어처럼 글이 오른쪽에서 왼쪽으로 흐르는 언어. 이 경우 화살표 키의 좌/우 의미, 아이콘 위치, 레이아웃 순서가 전부 반전돼야 한다. react-spectrum은 이를 컴포넌트마다 따로 처리하지 않고 공통 context로 자동 반영한다.

⑤ usePress — 마우스·터치·펜·키보드·스크린리더를 하나의 "누름" 이벤트로

packages/react-aria/src/interactions/usePress.ts는 이 저장소에서 가장 정교한 로우레벨 훅 중 하나다. 브라우저의 PointerEvent·TouchEvent·클릭 이벤트가 기기·브라우저마다 미묘하게 다르게 발생하는 문제를, 하나의 통일된 onPress 이벤트로 추상화한다. 스크린리더가 발생시키는 "가상 클릭"(isVirtualClick/isVirtualPointerEvent)을 실제 마우스 클릭과 구분하고, 터치로 드래그하다 원래 요소를 벗어나면 포인터 캡처를 해제해 다른 요소로 상호작용이 넘어가게 하며, iOS의 "포인터업 시 크기 0" 버그도 우회 처리한다. continuePropagation()이라는 탈출구로 상위 컴포넌트가 이벤트를 가로챌 수도 있다.

중요한 경계
"디자인은 하나도 없다" — React Aria/Stately만 읽으면 놓치는 것

React Aria·React Stately 소스만 보면 CSS·색상·간격에 대한 코드가 단 한 줄도 없다. 실제 "Adobe스러운 화면"이 궁금하다면 @react-spectrum/* 또는 @react-spectrum/s2/* 쪽 소스(styles, vars.css, style 매크로)를 따로 봐야 한다. 이 분리 자체가 설계 의도이므로 "왜 여기엔 스타일이 없지?"라고 헤매지 않아야 한다.

6기술 스택·의존성

yarn 4 + Lerna 독립 버전 배포 · Parcel 빌드 · CircleCI 30여 개 잡

① 패키지 매니저·버전 관리 — "버전의 진실원"

요소내용
패키지 매니저Yarn 4.13.0(Berry, workspaces) — package.jsonworkspacespackages/react-stately·packages/react-aria·packages/@*/* 등을 지정
버전 배포Lerna 3.13.2lerna publish from-package — Changesets가 아니라, 각 패키지의 package.json 버전 필드 자체가 진실원이며 Lerna는 이미 올라간 버전을 그대로 배포만 한다
버전 독립성react-aria 3.50.0 · react-stately 3.48.0 · react-aria-components 1.19.0 · @react-aria/select 3.18.1 · @react-stately/select 3.10.1 · @adobe/react-spectrum 3.47.2 · @react-spectrum/s2 1.5.1 — 220여 개 패키지가 모두 각자 다른 버전으로 독립 배포된다
빌드Parcel 2.16.3(문서 MDX 컴파일, 스토리북 정적 빌드, S2 style 매크로용 Rust 트랜스포머) + Make(Makefileclean·build·test 전체를 오케스트레이션, CIRCLE_BRANCH 감지)
타입체크TypeScript(+ 실험적 tsgo) — check-types/check-types:tsc 스크립트로 이원화
함정
"nx 모노레포"가 아니다 — 실제로는 yarn workspaces + Lerna + Make

모노레포 빌드 도구로 Nx가 쓰일 거라 짐작하기 쉽지만, 저장소에 nx.json은 없다. 실제 태스크 오케스트레이션은 Makefile + 각 패키지의 package.json 스크립트 + CircleCI가 담당한다. "유명한 모노레포 도구 이름"만 보고 넘겨짚지 말고 package.json·Makefile을 직접 열어 확인해야 한다.

② 프론트엔드 의존성

영역핵심 의존성
React 지원 범위기본은 React 19.2. install-16/install-17/install-18/install-canary 스크립트로 16.8~canary까지 실제 CI 매트릭스에서 검증(test-canary 잡)
Spectrum 1 스타일@adobe/spectrum-css-temp(정적 CSS 클래스) — classNames() 유틸로 조건부 클래스 결합
Spectrum 2 스타일자체 style 매크로(@react-spectrum/s2/style/style-macro.ts) — Parcel의 with {type:'macro'} 매크로 임포트로 빌드 타임에 원자적 CSS를 생성, 런타임 CSS-in-JS 비용 없음. 구버전 @react-spectrum/style-macro-s1도 병존
선언형 레이어react-aria-components — Context + render props로 완전히 스타일 없는 조립형 컴포넌트(<Select><Button><ListBox> 등 69개 소스 파일)
테스트Jest 30(단위) + Vitest + Playwright(test:browser, 브라우저 실동작) + axe-playwright(접근성 자동 검사) + react-test-renderer + Storybook test-runner

③ CI 파이프라인 — CircleCI

.circleci/config.yml(3만 자 이상) 안에 30여 개 잡이 정의돼 있다: rsp/rsp-large/rsp-xlarge(리소스 등급별 실행기), test/test-ssr/test-canary/test-ssr-canary(SSR·React canary 호환성까지 검증), test-browser(Playwright 브라우저 테스트), lint, ts-diff/skills-diff/weekly-api-diff(공개 API 표면 변경 자동 감지), storybook, verdaccio(로컬 npm 레지스트리에 실제로 배포해보는 통합 테스트), v-nextjs/v-rac-rsp-tailwind/v-starters(실사용 스타터 앱 검증), weekly-chromatic(비주얼 회귀), publish-nightly, prod-docs/beta-docs.

7설치·요구사항

세 가지 진입점 — 완제품(Spectrum) · 동작만(Aria) · 선언형(RAC)
용도설치
Adobe 디자인 그대로 쓰기npm install @adobe/react-spectrum — Provider로 감싸고 바로 사용, 테마·다크모드 자동 대응
새 Spectrum 2 디자인npm install @react-spectrum/s2 + Parcel(매크로 지원 필요)
동작 훅만(직접 디자인)npm install react-aria react-stately(또는 @react-aria/select처럼 필요한 것만 낱개 설치)
선언형 조립형npm install react-aria-components — Tailwind와 함께 쓰는 예제가 문서에 풍부
레포 자체 개발yarn install(Node·Yarn 4 필요) → yarn start(Storybook, 9003 포트) / yarn test(Jest, STRICT_MODE=1 VIRT_ON=1) / yarn build:docs
주의
S2의 style 매크로는 번들러를 가린다

Spectrum 2의 style 매크로(with {type:'macro'})는 Parcel의 매크로 기능에 강하게 의존한다. Webpack·Vite 등 다른 번들러 프로젝트에 그대로 들고 가면 매크로가 해석되지 않을 수 있어, 공식 문서의 번들러별 통합 가이드를 확인해야 한다. React Aria/Stately 훅 자체는 번들러 독립적이다.

8실제 사용 흐름

완제품 → 선언형 조립 → 순수 훅, 세 가지 실전 경로
흐름 ①

Adobe 디자인을 그대로 — @adobe/react-spectrum

가장 빠른 길. <Provider theme={defaultTheme}>로 앱을 감싸고 <Picker><Item>...</Item></Picker>처럼 바로 쓴다. 접근성·다크모드·RTL이 기본 포함이라 별도 설정이 거의 없다. 대신 화면이 "Adobe스러운 모양"으로 고정된다.

흐름 ②

내 디자인 + 검증된 동작 — react-aria-components(RAC)

Tailwind 등으로 직접 스타일링하되 로직은 가져다 쓰고 싶을 때. <Select><Button>...</Button><Popover><ListBox>...</ListBox></Popover></Select>처럼 하위 컴포넌트를 직접 배치하고, className(renderProps) => ... 함수를 넘겨 isOpen·isPressed 같은 상태에 따라 클래스를 바꾼다. 내부적으로는 결국 1절에서 추적한 useSelectState+useSelect가 그대로 돈다.

흐름 ③

완전히 새로운 디자인 시스템 — react-aria + react-stately 훅만

자체 디자인 시스템을 밑바닥부터 만드는 팀은 useSelectState·useSelect 훅만 가져다 자신만의 JSX·CSS를 얹는다. 이 방식이 정확히 @react-spectrum/picker@react-spectrum/s2가 이미 사내에서 하고 있는 일이다 — 즉 "Adobe가 자체 디자인 시스템에 하는 것과 동일한 절차"를 외부 팀도 그대로 밟을 수 있다.

9함정·오해 방지

패키지 이름 = 소스 위치가 아니다 · 모듈 3개 이름이 헷갈린다 · 별점만 보지 말 것
함정 ①
"@react-aria/select 폴더를 열면 진짜 코드가 있겠지" — 아니다

packages/@react-aria/select/src/index.ts를 열어보면 20줄도 안 되는 재수출 파일뿐이다: export {useSelect, HiddenSelect} from 'react-aria/useSelect';. 진짜 구현은 packages/react-aria/src/select/useSelect.ts(번들 패키지 react-aria 안)에 있다. react-stately/@react-stately/select 관계도 똑같다. 220여 개 스코프 패키지 중 다수가 이런 "얇은 재수출 껍데기"이므로, 코드를 찾다가 스코프 패키지 폴더에서 멈추면 시간을 낭비한다.

왜 이렇게 만들었나: 소스는 한 곳(번들 패키지)에 모아 유지보수하되, npm에는 잘게 쪼갠 패키지로 배포해 "필요한 컴포넌트만 설치"가 가능하게 하기 위해서다. package.jsonexports 서브패스("./*": {"source": "./exports/*.ts", ...})가 이 트릭의 핵심이다.

함정 ②
"Select"라는 이름이 세 곳에서 다른 걸 가리킨다

① Spectrum 1에서는 완제품 컴포넌트 이름이 Picker다(Select가 아니다). ② react-aria-components에서는 <Select>라는 이름을 쓴다. ③ 상태 훅 이름은 useSelectState다. "Select를 검색했는데 안 나온다"면 Picker 쪽도 함께 찾아봐야 한다.

함정 ③
"nx", "Changesets" 같은 유행 도구를 쓸 거라 짐작하지 말 것

6장에서 확인했듯 이 저장소는 Nx도, Changesets도 쓰지 않는다. 버전 관리는 package.json 버전 필드 수동/스크립트 관리 + Lerna의 publish from-package다. 최신 유행 도구 이름으로 넘겨짚지 말고 실제 설정 파일(lerna.json, Makefile, .circleci/config.yml)을 확인하는 습관이 중요하다.

참고
별점·트렌딩 수치보다 실물 구조를 봐야 하는 이유

이 문서는 GitHub 별점 수를 인용하지 않는다. react-spectrum처럼 오래되고(수년 간 릴리스) 사내 실사용(수백 개 Adobe 제품 화면)이 검증 기준인 저장소는, 별점보다 실제 소스 구조·CI 매트릭스·릴리스 빈도가 신뢰도를 보여주는 더 정확한 지표다.

10대안 비교

"동작만 주는" unstyled 라이브러리 4파전
라이브러리철학·차별점
React Aria
(이 저장소)
가장 오래되고 깊은 접근성·국제화 커버리지(9종 달력, 30개+ 언어). 상태(Stately)·동작(Aria)·디자인(Spectrum) 3층 분리가 가장 명시적. 다만 API 표면이 넓고 학습 곡선이 있다.
Radix PrimitivesReact 생태계에서 가장 널리 쓰이는 unstyled 컴포넌트. shadcn/ui의 기반. 컴포넌트별 단일 패키지 설치가 쉽고 생태계(Tailwind 조합 문서)가 크지만, 국제화·달력 시스템 등 깊이는 React Aria에 못 미친다.
Headless UITailwind Labs 제작. 컴포넌트 수가 적고 가볍다. React Aria만큼 폭넓은 컴포넌트·상호작용 커버리지는 없지만 진입 장벽이 가장 낮다.
Base UIMUI 팀과 Radix 팀 일부가 합류해 만든 신흥 unstyled 라이브러리. React Aria·Radix의 장점을 절충하려는 최신 시도.
Ark UIZag.js(프레임워크 독립 상태 머신) 기반이라 React 외에 Vue·Solid·Svelte에도 같은 로직을 쓸 수 있다는 게 가장 큰 차별점. react-spectrum의 "Stately/Aria 분리"와 유사한 철학을 다중 프레임워크로 확장한 형태.
비유

Radix·Headless UI·Base UI가 "잘 정리된 부품 세트"라면, React Aria는 부품 하나하나를 국제 표준(ARIA 명세) 문서까지 대조해가며 만든 정밀 부품 공장에 가깝다. Ark UI는 그 부품을 "다른 자동차 브랜드(프레임워크)에도 끼울 수 있게" 규격화한 쪽이다.

11정리 — 누가 쓰면 좋은가

"디자인 시스템을 직접 만드는 팀"과 "접근성을 제대로 하고 싶은 팀"에게

react-spectrum(정확히는 그 안의 React Aria + React Stately)은 "우리 회사만의 디자인 시스템을 밑바닥부터 만들어야 하는 팀"에게 가장 큰 가치가 있다. 상태와 동작을 이미 검증된 훅으로 가져오고, 디자인만 온전히 자기 것으로 만들 수 있기 때문이다. @adobe/react-spectrum을 완제품으로 그대로 쓰고 싶은 팀에게도 물론 유효하지만, 이 경우엔 Adobe 톤앤매너를 그대로 받아들여야 한다.

접근성(a11y)을 진지하게 다뤄야 하는 프로덕트 — 공공기관·금융·의료·대기업 내부 툴 — 팀에게도 강력히 추천된다. 키보드·스크린리더·RTL·다국어 달력까지 이미 검증된 로직을 쓸 수 있다는 점이 가장 큰 무기다. 반대로 "빠르게 프로토타입만 찍고 싶다"면 Radix+shadcn/ui나 Headless UI 쪽이 진입 장벽이 낮다.

이 저장소를 공부하는 것 자체로도 배울 게 많다. "로직과 렌더링을 분리하는 설계", "하나의 npm 패키지를 재수출 계층으로 잘게 쪼개 배포하는 기법", "브라우저 입력 이벤트를 하나로 통합하는 정교한 이벤트 시스템(usePress)"은 이 저장소가 아니어도 어디서나 써먹을 수 있는 프론트엔드 아키텍처 지식이다.