@gj-kit/expo-ui 핵심 개념
이름 없는 IconButton·Tabs·Slider가 스크린 리더 버그가 아니라 컴파일 에러가 되는 React Native·웹 UI primitive 모음입니다.
왜 이 경계가 필요한가
섹션 제목: “왜 이 경계가 필요한가”React Native 디자인 시스템의 사고는 조용히 일어납니다. accessibility label 없는 IconButton이 그대로 배포되고, Tabs의 value 오타 하나로 panel이 빈 화면이 되고, EmptyState의 action이 onPress 없이 눌러도 아무 일 없는 버튼으로 렌더되고, 손으로 조립한 theme 객체가 style에 undefined를 흘립니다. 어느 것도 빌드를 깨뜨리지 않으니 스크린 리더에서, 프로덕션에서, 남의 기기에서야 드러납니다.
접근성은 컴포넌트 계약의 일부입니다
섹션 제목: “접근성은 컴포넌트 계약의 일부입니다”접근성 이름, press handler, range label은 컴포넌트 타입이 요구하며 레이아웃 뒤에 확인할 리뷰 체크리스트가 아닙니다.
제어 상태는 완전한 형태를 가집니다
섹션 제목: “제어 상태는 완전한 형태를 가집니다”Tabs는 literal item value를 panel까지 전달하므로 누락한 panel은 빈 화면이 아니라 타입 오류가 됩니다.
패키지가 강제하는 것
섹션 제목: “패키지가 강제하는 것”- Android 팝업 좌표를 실제로 측정합니다 — Android anchored Menu·Select는 root 기준 트리거 page 좌표를 측정해 full-window Modal과 맞춥니다. 상태 표시줄 높이를 고정값으로 더하지 않으며 iOS·웹의 기존 배치 경로는 유지합니다.
- 접근성 이름을 타입이 요구합니다 — accessibilityLabel 없는 IconButton, 이름을 유추할 수 없는 rich children Button, thumb이 둘인데 label은 하나인 range Slider가 모두 타입 검사에서 거부됩니다.
- 죽은 버튼은 컴파일되지 않습니다 — Button·IconButton의 interaction prop이 union이라 disabled도 loading도 아니면 onPress가 필수이고, EmptyState의 action은 label과 onPress를 함께 요구합니다. 눌러도 아무 일 없는 버튼은 애초에 컴파일되지 않습니다.
- Tabs는 panel을 잃지 않습니다 — panels 타입이
Readonly<Record<NoInfer<ItemValue>, NonNullable<ReactNode>>>이고 value에도 NoInfer가 걸려 있어, items에 없는 value 오타는 물론 panel 하나 누락이나 null panel까지 타입 검사에서 걸립니다. - theme은 createTheme만 만듭니다 — UiProvider의 theme prop은 createTheme·createThemes가 찍어낸 branded Theme·ThemePair만 받습니다. 손으로 조립한 token 객체는 컴파일 에러라, 키가 빠진 반쪽 theme이 런타임에 undefined 스타일로 새지 않습니다.
- 소스에 디자인 리터럴이 없습니다 — guard 테스트가 src/components 아래 .tsx 파일을 재귀로 훑어, 따옴표 안 hex 색·숫자 fontSize·따옴표 안 숫자 fontWeight가 하나라도 남아 있으면 실패합니다. 토큰만 쓴다는 규칙을 리뷰가 아니라 테스트가 지킵니다.
앱에 남는 책임
섹션 제목: “앱에 남는 책임”optional safe-area와 React Native Web peer는 문서화된 subpath에서만 가져오고, 제품 문구는 프리미티브에 넣지 말고 strings로 주입하세요.
다음은 검증된 레시피 두 개입니다.