UI 라이브러리
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))
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, 또는 당신의 디자인)를 따로 파는 조립 부품점이다. 뼈대와 신경은 그대로 둔 채 피부만 통째로 갈아 끼울 수 있다.
Radix Primitives·Headless UI·Base UI·Ark UI 같은 "스타일 없이 동작만 주는" 컴포넌트 라이브러리가 최근 프론트엔드의 표준 문법이 됐다. React Aria는 이 흐름을 2020년부터 밀어붙인 선구자 중 하나다. 다른 라이브러리들이 여기 합류하면서 오히려 "react-spectrum의 접근성 깊이"가 재조명받고 있다.
AI가 UI 코드를 순식간에 찍어내는 시대에, 키보드 내비게이션·스크린리더 라벨·포커스 관리 같은 디테일은 오히려 더 쉽게 누락된다. React Aria처럼 "동작을 훅으로 캡슐화"해두면 AI가 만든 커스텀 UI에도 검증된 접근성 로직을 그대로 꽂을 수 있어, 최근 AI 코딩 워크플로에서 재조명되고 있다. 실제로 이 저장소에는 packages/dev/mcp/react-aria·packages/dev/mcp/s2라는 공식 MCP(Model Context Protocol) 서버가 포함돼, AI 에이전트가 React Aria/S2 문서를 직접 조회하며 코드를 짤 수 있게 되어 있다.
Adobe는 자체 디자인 시스템을 Spectrum 2로 새로 그리는 중이며, @react-spectrum/s2 패키지가 그 결과물이다. 여기서 쓰는 컴파일 타임 스타일 매크로(style macro)는 "런타임 CSS-in-JS 없이, 빌드 시점에 원자적 CSS로 컴파일"하는 실험적 접근이라 프론트엔드 성능 커뮤니티에서 자주 언급된다.
MUI·Chakra·Mantine 같은 완제품 킷은 빠르게 시작하기 좋지만 로직과 디자인이 붙어있다. react-spectrum(정확히는 그 안의 React Aria)은 재료만 파는 쪽이다. Radix·Base UI·Ark UI도 같은 철학이지만, react-spectrum은 Adobe 실제 제품(수백 개 화면)에서 10년 가까이 검증된 접근성 세부사항과 완전한 국제화(30개 이상 언어, 9종 달력)까지 포함한다는 깊이가 다르다.
키보드로만 메뉴를 열고 닫고 화살표로 이동하고, 스크린리더가 "몇 개 중 몇 번째 항목"이라고 읽어주고, 포커스가 열린 팝업 안에 갇혔다가(focus trap) 닫히면 원래 자리로 돌아오는 것 — 이걸 매번 새로 짜면 버그투성이가 된다. React Aria는 WAI-ARIA 명세를 코드로 미리 구현해, useSelect() 한 번 호출로 이 모든 걸 해결한다.
보통 UI 킷은 컴포넌트를 바꾸면 상태 관리·키보드 로직까지 통째로 새로 짜야 한다. react-spectrum은 상태(Stately)·동작(Aria)을 디자인(Spectrum)과 분리해 둬서, Spectrum 1 → Spectrum 2 전환처럼 큰 리디자인도 아래 두 층은 그대로 두고 위 껍데기만 바꿔 끼울 수 있다.
날짜·숫자 포맷, RTL(오른쪽에서 왼쪽 언어) 레이아웃, 지역별 달력 체계는 나중에 붙이려면 컴포넌트 구조 자체를 바꿔야 하는 경우가 많다. react-spectrum은 @internationalized/date·number를 맨 처음부터 상태 로직 안에 내장해, 그레고리력뿐 아니라 불교력·에티오피아력·헤브루력·인도력·이슬람력·일본력·페르시아력·타이완력까지 9종을 기본 지원한다.
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-stately/select·@react-aria/select 같은 스코프 패키지들은 껍데기다. 진짜 훅 구현은 packages/react-stately/src/와 packages/react-aria/src/라는 "번들 패키지"에 있고, 스코프 패키지는 그 파일을 package.json의 exports 서브패스 매핑을 통해 재수출만 한다. (이 구조는 5장·9장에서 실제 코드로 추적한다.)
가장 좋은 증명은 실제로 하나의 컴포넌트를 끝까지 따라가는 것이다. 여기서는 드롭다운 선택 컴포넌트(Spectrum에서는 Picker라는 이름)를 추적한다.
useSelectState (React Stately)
packages/react-stately/src/select/useSelectState.ts는 DOM도, 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나 서버 컴포넌트 상태 관리에 그대로 쓸 수 있을 만큼 플랫폼 독립적이다.
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 클래스도, 화면에 그려지는 모양도 전혀 없다.
같은 두 훅(useSelectState + useSelect) 위에 세 가지 다른 옷이 입혀진다.
| 완제품 | 실제 소스 위치 | 입히는 것 |
|---|---|---|
| Spectrum 1 Picker | @adobe/react-spectrum/src/picker/Picker.tsx | Spectrum 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의 연극은 배우(로직)가 똑같은데 의상만 다른 것이다.
Select·ListBox·Menu·Table·GridList·Tabs 등 "항목이 여러 개인" 모든 컴포넌트는 같은 컬렉션 모델을 공유한다. 사용자는 <Item>/<Section>(정적) 또는 배열 + 렌더 함수(동적, items={data} children={item => <Item>...</Item>})로 항목을 선언하고, 내부적으로 CollectionBuilder가 이를 Node<T> 트리로 변환한다. @react-types/shared/collections.d.ts에 ItemProps·AsyncLoadable(isLoading·onLoadMore) 같은 공통 타입이 정의돼, 모든 컴포넌트가 같은 방식으로 무한 스크롤·비동기 로딩을 지원한다.
@react-stately/virtualizer(레이아웃 계산: 어떤 항목이 어디 위치하는지 순수 계산)와 @react-aria/virtualizer(그 계산 결과를 실제 DOM에 붙이고 스크롤·포커스를 관리)로 나뉜다. Table·큰 ListBox·Calendar처럼 항목 수가 많은 컴포넌트가 이를 사용해, 화면에 보이는 행만 실제 DOM에 그리고 나머지는 가상 좌표로만 계산한다. 여기서도 "레이아웃 계산(상태)"과 "DOM 반영(동작)"이 각각 Stately/Aria로 나뉘는 동일한 철학이 반복된다.
@internationalized/date는 리액트에 의존하지 않는 순수 날짜 계산 라이브러리다. CalendarDate 클래스 하나로 그레고리력뿐 아니라 불교력·에티오피아력·헤브루력·인도력·이슬람력(여러 변형 포함)·일본력·페르시아력·타이완력까지 9종의 달력 시스템을 계산한다(packages/@internationalized/date/src/calendars/). @internationalized/number의 NumberParser는 latn·arab·hanidec(한자)·deva(데바나가리)·beng(벵골)·fullwide 6개 숫자 체계를 파싱한다. RTL(오른쪽에서 왼쪽 언어)은 I18nProvider가 isRTL(locale) 결과로 direction: 'rtl'|'ltr'을 context에 심어, 모든 하위 컴포넌트가 자동으로 좌우 반전된다.
packages/react-aria/src/interactions/usePress.ts는 이 저장소에서 가장 정교한 로우레벨 훅 중 하나다. 브라우저의 PointerEvent·TouchEvent·클릭 이벤트가 기기·브라우저마다 미묘하게 다르게 발생하는 문제를, 하나의 통일된 onPress 이벤트로 추상화한다. 스크린리더가 발생시키는 "가상 클릭"(isVirtualClick/isVirtualPointerEvent)을 실제 마우스 클릭과 구분하고, 터치로 드래그하다 원래 요소를 벗어나면 포인터 캡처를 해제해 다른 요소로 상호작용이 넘어가게 하며, iOS의 "포인터업 시 크기 0" 버그도 우회 처리한다. continuePropagation()이라는 탈출구로 상위 컴포넌트가 이벤트를 가로챌 수도 있다.
React Aria·React Stately 소스만 보면 CSS·색상·간격에 대한 코드가 단 한 줄도 없다. 실제 "Adobe스러운 화면"이 궁금하다면 @react-spectrum/* 또는 @react-spectrum/s2/* 쪽 소스(styles, vars.css, style 매크로)를 따로 봐야 한다. 이 분리 자체가 설계 의도이므로 "왜 여기엔 스타일이 없지?"라고 헤매지 않아야 한다.
| 요소 | 내용 |
|---|---|
| 패키지 매니저 | Yarn 4.13.0(Berry, workspaces) — package.json의 workspaces가 packages/react-stately·packages/react-aria·packages/@*/* 등을 지정 |
| 버전 배포 | Lerna 3.13.2의 lerna 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(Makefile이 clean·build·test 전체를 오케스트레이션, CIRCLE_BRANCH 감지) |
| 타입체크 | TypeScript(+ 실험적 tsgo) — check-types/check-types:tsc 스크립트로 이원화 |
모노레포 빌드 도구로 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 |
.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.
| 용도 | 설치 |
|---|---|
| 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 |
Spectrum 2의 style 매크로(with {type:'macro'})는 Parcel의 매크로 기능에 강하게 의존한다. Webpack·Vite 등 다른 번들러 프로젝트에 그대로 들고 가면 매크로가 해석되지 않을 수 있어, 공식 문서의 번들러별 통합 가이드를 확인해야 한다. React Aria/Stately 훅 자체는 번들러 독립적이다.
가장 빠른 길. <Provider theme={defaultTheme}>로 앱을 감싸고 <Picker><Item>...</Item></Picker>처럼 바로 쓴다. 접근성·다크모드·RTL이 기본 포함이라 별도 설정이 거의 없다. 대신 화면이 "Adobe스러운 모양"으로 고정된다.
Tailwind 등으로 직접 스타일링하되 로직은 가져다 쓰고 싶을 때. <Select><Button>...</Button><Popover><ListBox>...</ListBox></Popover></Select>처럼 하위 컴포넌트를 직접 배치하고, className에 (renderProps) => ... 함수를 넘겨 isOpen·isPressed 같은 상태에 따라 클래스를 바꾼다. 내부적으로는 결국 1절에서 추적한 useSelectState+useSelect가 그대로 돈다.
자체 디자인 시스템을 밑바닥부터 만드는 팀은 useSelectState·useSelect 훅만 가져다 자신만의 JSX·CSS를 얹는다. 이 방식이 정확히 @react-spectrum/picker와 @react-spectrum/s2가 이미 사내에서 하고 있는 일이다 — 즉 "Adobe가 자체 디자인 시스템에 하는 것과 동일한 절차"를 외부 팀도 그대로 밟을 수 있다.
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.json의 exports 서브패스("./*": {"source": "./exports/*.ts", ...})가 이 트릭의 핵심이다.
① Spectrum 1에서는 완제품 컴포넌트 이름이 Picker다(Select가 아니다). ② react-aria-components에서는 <Select>라는 이름을 쓴다. ③ 상태 훅 이름은 useSelectState다. "Select를 검색했는데 안 나온다"면 Picker 쪽도 함께 찾아봐야 한다.
6장에서 확인했듯 이 저장소는 Nx도, Changesets도 쓰지 않는다. 버전 관리는 package.json 버전 필드 수동/스크립트 관리 + Lerna의 publish from-package다. 최신 유행 도구 이름으로 넘겨짚지 말고 실제 설정 파일(lerna.json, Makefile, .circleci/config.yml)을 확인하는 습관이 중요하다.
이 문서는 GitHub 별점 수를 인용하지 않는다. react-spectrum처럼 오래되고(수년 간 릴리스) 사내 실사용(수백 개 Adobe 제품 화면)이 검증 기준인 저장소는, 별점보다 실제 소스 구조·CI 매트릭스·릴리스 빈도가 신뢰도를 보여주는 더 정확한 지표다.
| 라이브러리 | 철학·차별점 |
|---|---|
| React Aria (이 저장소) | 가장 오래되고 깊은 접근성·국제화 커버리지(9종 달력, 30개+ 언어). 상태(Stately)·동작(Aria)·디자인(Spectrum) 3층 분리가 가장 명시적. 다만 API 표면이 넓고 학습 곡선이 있다. |
| Radix Primitives | React 생태계에서 가장 널리 쓰이는 unstyled 컴포넌트. shadcn/ui의 기반. 컴포넌트별 단일 패키지 설치가 쉽고 생태계(Tailwind 조합 문서)가 크지만, 국제화·달력 시스템 등 깊이는 React Aria에 못 미친다. |
| Headless UI | Tailwind Labs 제작. 컴포넌트 수가 적고 가볍다. React Aria만큼 폭넓은 컴포넌트·상호작용 커버리지는 없지만 진입 장벽이 가장 낮다. |
| Base UI | MUI 팀과 Radix 팀 일부가 합류해 만든 신흥 unstyled 라이브러리. React Aria·Radix의 장점을 절충하려는 최신 시도. |
| Ark UI | Zag.js(프레임워크 독립 상태 머신) 기반이라 React 외에 Vue·Solid·Svelte에도 같은 로직을 쓸 수 있다는 게 가장 큰 차별점. react-spectrum의 "Stately/Aria 분리"와 유사한 철학을 다중 프레임워크로 확장한 형태. |
Radix·Headless UI·Base UI가 "잘 정리된 부품 세트"라면, React Aria는 부품 하나하나를 국제 표준(ARIA 명세) 문서까지 대조해가며 만든 정밀 부품 공장에 가깝다. Ark UI는 그 부품을 "다른 자동차 브랜드(프레임워크)에도 끼울 수 있게" 규격화한 쪽이다.
react-spectrum(정확히는 그 안의 React Aria + React Stately)은 "우리 회사만의 디자인 시스템을 밑바닥부터 만들어야 하는 팀"에게 가장 큰 가치가 있다. 상태와 동작을 이미 검증된 훅으로 가져오고, 디자인만 온전히 자기 것으로 만들 수 있기 때문이다. @adobe/react-spectrum을 완제품으로 그대로 쓰고 싶은 팀에게도 물론 유효하지만, 이 경우엔 Adobe 톤앤매너를 그대로 받아들여야 한다.
접근성(a11y)을 진지하게 다뤄야 하는 프로덕트 — 공공기관·금융·의료·대기업 내부 툴 — 팀에게도 강력히 추천된다. 키보드·스크린리더·RTL·다국어 달력까지 이미 검증된 로직을 쓸 수 있다는 점이 가장 큰 무기다. 반대로 "빠르게 프로토타입만 찍고 싶다"면 Radix+shadcn/ui나 Headless UI 쪽이 진입 장벽이 낮다.
이 저장소를 공부하는 것 자체로도 배울 게 많다. "로직과 렌더링을 분리하는 설계", "하나의 npm 패키지를 재수출 계층으로 잘게 쪼개 배포하는 기법", "브라우저 입력 이벤트를 하나로 통합하는 정교한 이벤트 시스템(usePress)"은 이 저장소가 아니어도 어디서나 써먹을 수 있는 프론트엔드 아키텍처 지식이다.
모든 경로와 명령은 react-spectrum 레포 루트 기준이다. 개발 서버와 테스트를 실행하는 과제는 해당 작업본의 Node·Yarn 설정에 맞는 환경과 의존성이 이미 준비되어 있다고 가정한다. 소스를 바꾸지 않고 기존 예제와 테스트만으로 진행할 수 있다.
목표: 설치하는 패키지 이름과 실제 구현 파일의 위치를 구분하고, 상태·동작·렌더링의 연결을 직접 찾는다.
cat packages/@react-aria/select/src/index.ts
cat packages/@react-stately/select/src/index.ts
cat packages/react-aria/exports/useSelect.ts
cat packages/react-stately/exports/useSelectState.ts
packages/react-aria/package.json의 exports에서 ./*과 source 매핑을 찾아, react-aria/useSelect가 개발 소스로 연결되는 과정을 설명한다.packages/react-stately/src/select/useSelectState.ts, packages/react-aria/src/select/useSelect.ts, packages/react-aria-components/src/Select.tsx를 순서대로 읽는다. 상태 객체·DOM 참조·반환된 속성이 다음 단계로 전달되는 위치를 표시한다.완료 기준: 스코프 패키지 → 공개 진입 파일 → 실제 훅 구현 → RAC 컴포넌트의 경로 지도를 만들고, 선택값·키보드 처리·JSX 렌더링의 담당 파일을 각각 설명할 수 있다.
목표: JSX 조립 구조가 키보드 동작과 접근성 속성으로 어떻게 이어지는지 관찰한다.
packages/react-aria-components/stories/Select.stories.tsx의 SelectRenderProps를 읽는다. Label·Button·SelectValue·Popover·ListBox의 역할과 isOpen을 사용하는 부분을 찾는다.React Aria Components/Select 아래 해당 예제를 연다.yarn start
aria-expanded, 목록의 role, 닫힌 뒤 포커스 위치를 확인한다. 열림·닫힘에 따라 화살표 표시가 달라지는 이유를 예제의 렌더 함수와 연결한다.완료 기준: 열기·이동·선택·취소 네 동작에 대해 입력 키, 선택값, 열림 상태, 포커스 위치를 표로 남긴다. Enter 선택 뒤 표시값이 바뀌고, Escape로 취소하면 직전 선택값이 유지되는지 확인한다.
목표: 브라우저 이벤트와 라이브러리가 제공하는 논리적인 누름 이벤트의 차이를 이해한다.
packages/react-aria/test/interactions/usePress.test.js의 keyboard events 묶음과 비활성 요소 테스트를 읽는다. 각 사례에서 보내는 이벤트와 기대하는 onPress 발생 여부를 먼저 예측한다.-t는 테스트 이름을 고르므로, 실행된 테스트가 있는지도 결과에서 확인한다.yarn test packages/react-aria/test/interactions/usePress.test.js --runInBand -t 'keyboard events|disabled elements'
packages/react-aria/src/interactions/usePress.ts에서 isDisabled와 반복 키 입력을 처리하는 분기를 찾는다. 테스트의 repeat: true와 일반 키 입력이 다른 경로를 타는 이유를 적는다.onPressStart·onPressUp·onPressEnd·onPress의 기대 순서를 정리한다. 다른 입력 방식에도 이 순서가 그대로 적용되는지는 별도 사례로 대조한다.완료 기준: 정상 누름, 반복 키 이벤트, 비활성 요소의 세 행으로 예측·실행 결과·관련 소스 분기를 연결한 표를 만든다. 선택한 테스트가 통과했는지와 건너뛴 테스트가 무엇인지 구분해 기록한다.
목표: 날짜를 단순한 밀리초 덧셈으로 다루지 않고, 달력 규칙에 맞춰 계산하는 설계를 익힌다.
packages/@internationalized/date/src/CalendarDate.ts에서 add의 반환형과 toDate의 인자를 확인한다. 날짜만 가진 값과 시간대가 필요한 변환을 구분한다.date, nextMonth, leapDay가 각각 어떤 날짜인지 먼저 예측한다. 코드는 CalendarDate를 사용하는 계산 예시다.import {CalendarDate} from '@internationalized/date';
const date = new CalendarDate(2020, 8, 31);
const nextMonth = date.add({months: 1});
const leapDay = new CalendarDate(2020, 2, 28).add({days: 1});
packages/@internationalized/date/tests/manipulation.test.js에서 월말 보정과 윤년 사례를 찾아 예측을 대조한 뒤 실행한다.yarn test packages/@internationalized/date/tests/manipulation.test.js --runInBand -t 'CalendarDate manipulation'
packages/@internationalized/date/src/manipulation.ts의 add가 원본을 복사하는 위치와 날짜 범위를 보정하는 부분을 찾는다. 윤년이 아닌 해의 2월 사례도 테스트에서 함께 비교한다.완료 기준: 원본은 2020-08-31로 유지되고 다음 달은 2020-09-30, 윤년 계산은 2020-02-29가 되는 이유를 설명한다. 한 달 더하기를 일정한 일수 더하기로 바꾸면 안 되는 이유와, 원본을 바꾸지 않는 설계의 이점도 적는다.
한 주마다 읽은 내용을 작은 결과물 하나로 남긴다. 아래 경로는 모두 레포 루트 기준이며, 앞 주의 Select 경로 지도를 다음 주의 상태표와 동작표에 계속 덧붙이면 된다.
| 주차 | 배울 개념 | 이 레포에서 읽을 곳 | 결과물 |
|---|---|---|---|
| 1주차 | 모노레포, 워크스페이스, 공개 진입점과 실제 구현의 구분 | package.json, .yarnrc.yml, lerna.json, packages/@react-aria/select/src/index.ts, packages/react-aria/exports/useSelect.ts |
Select의 가져오기 경로 지도와 개발·테스트 명령 목록. 버전 번호는 패키지별로 어디에 적히는지도 표시한다. |
| 2주차 | 제어·비제어 상태, 열림 상태, 선택 상태, 컬렉션 | packages/react-stately/src/select/useSelectState.ts, packages/react-stately/src/overlays/useOverlayTriggerState.ts, packages/react-stately/src/utils/useControlledState.ts, packages/react-stately/src/list/useListState.ts |
열기·닫기·선택의 상태 전이표. 부모가 상태를 관리할 때와 내부에서 관리할 때를 나눠 그린다. |
| 3주차 | ARIA 연결, 키보드 이동, 누름 이벤트 통합, 포커스 관리 | packages/react-aria/src/select/useSelect.ts, packages/react-aria/src/interactions/usePress.ts, packages/react-aria/src/focus/FocusScope.tsx, packages/react-aria/test/interactions/usePress.test.js |
입력 방식별 동작표와 라벨·버튼·목록의 연결도. 각 동작이 상태를 바꾸는지 포커스를 옮기는지 구분한다. |
| 4주차 | Context, 렌더 함수, 동작과 스타일의 분리, 스타일 매크로 | packages/react-aria-components/src/Select.tsx, packages/@adobe/react-spectrum/src/picker/Picker.tsx, packages/@react-spectrum/s2/src/Picker.tsx, packages/@react-spectrum/s2/style/style-macro.ts |
RAC·Spectrum 1·S2의 조립 방식과 스타일 처리 비교표. 추가로 examples/rac-spectrum-tailwind/src/components/SelectBoxGroup.tsx에서 RAC의 RadioGroup에 자체 디자인을 입히는 예를 찾아 정리한다. |
| 5주차 | 날짜 값의 불변성, 달력 규칙, 시간대 변환의 경계 | packages/@internationalized/date/src/CalendarDate.ts, packages/@internationalized/date/src/manipulation.ts, packages/@internationalized/date/src/calendars/, packages/@internationalized/date/tests/manipulation.test.js |
월말·윤년·연도 변경의 입력과 기대 결과 표. 날짜 계산과 표시 형식, 시간대 변환을 별개 단계로 설명한다. |
| 6주차 | 단위 테스트, 서버 렌더링과 하이드레이션, 실제 브라우저 검증의 역할 | jest.config.js, jest.ssr.config.js, vitest.browser.config.ts, packages/react-aria-components/test/Select.test.js, packages/react-aria-components/test/Select.ssr.test.js |
같은 Select를 어떤 환경에서 무엇으로 검증할지 적은 계획서. 실행한 사례·실행하지 않은 사례·수동 키보드 확인 결과를 구분해 담는다. |
마지막에는 “선택 목록을 열고 항목을 고른 뒤 닫는다”라는 동작을 골라 상태 변화 → 접근성 속성·이벤트 → JSX 조립 → 검증 사례로 이어서 설명해 보자. 파일 이름을 외우는 것보다 각 층이 주고받는 값과 책임을 짚는 것이 목표다.
package.json의 workspaces가 packages/ 아래 개발 패키지를 묶는다.react-aria/useSelect처럼 패키지 이름 뒤에 붙는 공개 가져오기 경로다. 이 레포의 스코프 패키지는 재수출을 사용하며, exports 매핑과 진입 파일을 따라가야 실제 구현을 찾을 수 있다.useSelectState는 목록·열림·검증 상태를 조합해 Select가 사용할 상태 객체를 만든다.useSelect는 상태와 DOM 참조를 받아 버튼·라벨·목록 등에 전달할 속성을 반환한다.packages/react-aria-components/src/Select.tsx는 상태와 동작을 하위 컴포넌트에 연결하면서, 사용자가 구조와 스타일을 정할 수 있게 한다.isOpen과 defaultOpen이 각각 그 역할을 하며, onOpenChange로 변경을 알린다.aria-labelledby 같은 속성으로 라벨과 선택값을 연결하며, 실제 키보드 동작은 별도의 이벤트 처리와 함께 구현한다.useSelect가 반환하는 triggerProps·labelProps·menuProps는 각각 전달할 대상이 다르므로, 이름에 맞는 곳에 연결해야 한다.SelectStateContext 등으로 상태와 속성을 나누어, SelectValue 같은 하위 요소가 선택 상태를 읽게 한다.SelectRenderProps 예제는 isOpen을 받아 열림·닫힘에 따라 화살표를 바꾼다.packages/react-stately/src/collections/CollectionBuilder.ts는 항목 선언을 컬렉션 노드로 만들며, 목록과 선택 로직이 이 구조를 사용한다.packages/react-aria/src/focus/FocusScope.tsx는 contain·restoreFocus·autoFocus로 이러한 정책을 설정한다.usePress는 onPress와 누름 시작·종료 이벤트를 제공하며, 비활성 요소나 반복 입력 같은 조건도 함께 처리한다.CalendarDate는 특정 달력의 날짜를 시간 정보 없이 표현하며, add 같은 연산은 원본을 바꾸지 않고 새 날짜를 반환한다. 이 레포의 @internationalized/date에서는 날짜 계산과 시간대가 필요한 toDate 변환을 구분한다.packages/@react-spectrum/s2/style/style-macro.ts는 S2의 스타일 생성에 쓰이며, 실행 중 상태·입력 처리와는 다른 역할을 맡는다.