@gj-kit/expo-ui
이름 없는 IconButton·Tabs·Slider가 스크린 리더 버그가 아니라 컴파일 에러가 되는 React Native·웹 UI primitive 모음입니다.
- npm
0.8.5 - 런타임 의존성 0
- @ts-expect-error 가드 332개
- 유닛 테스트 850개 이상
- 컴포넌트 58개, 색상 role 31개
계속 학습하기
섹션 제목: “계속 학습하기”왜 필요한가
섹션 제목: “왜 필요한가”React Native 디자인 시스템의 사고는 조용히 일어납니다. accessibility label 없는 IconButton이 그대로 배포되고, Tabs의 value 오타 하나로 panel이 빈 화면이 되고, EmptyState의 action이 onPress 없이 눌러도 아무 일 없는 버튼으로 렌더되고, 손으로 조립한 theme 객체가 style에 undefined를 흘립니다. 어느 것도 빌드를 깨뜨리지 않으니 스크린 리더에서, 프로덕션에서, 남의 기기에서야 드러납니다.
무엇으로 막는가
섹션 제목: “무엇으로 막는가”- 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가 하나라도 남아 있으면 실패합니다. 토큰만 쓴다는 규칙을 리뷰가 아니라 테스트가 지킵니다.
Golden path
섹션 제목: “Golden path”완료 상태: 앱 전체 provider 하나에서 테마와 접근성이 적용된 버튼을 렌더링합니다.
1. 설치
섹션 제목: “1. 설치”pnpm add @gj-kit/expo-ui2. 앱이 소유할 경계를 정합니다
섹션 제목: “2. 앱이 소유할 경계를 정합니다”테마는 한 번만 만들고 앱을 감싸는 컴포넌트에 UiProvider를 둡니다. 네이티브 Button·IconButton·Menu·Select는 누름 피드백을 네이티브 스타일로 처리하므로 기본 상호작용에 NativeWind가 필요하지 않습니다.
3. 최소 연결부터 시작합니다
섹션 제목: “3. 최소 연결부터 시작합니다”먼저 아래 코드를 복사한 뒤, 위에서 언급한 앱 소유 값만 교체하세요.
import { Button, UiProvider, enStrings } from '@gj-kit/expo-ui';import { createThemes } from '@gj-kit/expo-ui/theme';
const themes = createThemes();
export function App() { return ( <UiProvider theme={themes} strings={enStrings}> <Button label="Get started" onPress={() => {}} /> </UiProvider> );}실제로는 이렇게 걸립니다
섹션 제목: “실제로는 이렇게 걸립니다”Tabs는 tablist의 접근성 이름과 item value마다 대응하는 panel을 필수로 받기 때문에, 빠뜨린 panel은 빈 화면이 아니라 빌드 실패로 드러납니다.
import { Tabs, Text, UiProvider, createThemes } from '@gj-kit/expo-ui';
declare const onChange: (value: 'overview' | 'history') => void; // the app owns tab stateconst items = [{ label: 'Overview', value: 'overview' }, { label: 'History', value: 'history' }] as const;
export const ProfileTabs = () => ( <UiProvider theme={createThemes({ light: { colors: { primary: '#1769C2' } } })}> <Tabs accessibilityLabel="Profile sections" items={items} value="overview" onChange={onChange} // Delete the history entry below and tsc stops the build: // TS2741: Property 'history' is missing in type '{ overview: JSX.Element; }' panels={{ overview: <Text>Overview</Text>, history: <Text>History</Text> }} /> </UiProvider>);사용할 때
섹션 제목: “사용할 때”네이티브와 웹에서 하나의 디자인 언어, 제어 컴포넌트 상태, 오버레이, 접근성 동작을 함께 유지해야 할 때 사용합니다.
사용하지 않을 때
섹션 제목: “사용하지 않을 때”제품 route, 데이터 store, analytics, 제품 고유 문구를 이 패키지에 넣기 위한 용도로는 사용하지 마세요.
환경과 peer
섹션 제목: “환경과 peer”엔진: node >=20
| Peer | 지원 범위 | 필요한 위치 |
|---|---|---|
react |
>=18 |
패키지 통합 |
react-native |
>=0.79 |
패키지 통합 |
react-native-safe-area-context |
>=4 |
문서화된 optional subpath에서만 |
react-native-web |
>=0.21 |
문서화된 optional subpath에서만 |
공개 subpath
섹션 제목: “공개 subpath”| Entry | Exported symbols |
|---|---|
Root entry |
361 |
./insets |
11 |
./insets/pure |
6 |
./tailwind |
4 |
./theme |
26 |
안전 경계
섹션 제목: “안전 경계”optional safe-area와 React Native Web peer는 문서화된 subpath에서만 가져오고, 제품 문구는 프리미티브에 넣지 말고 strings로 주입하세요.
API reference
섹션 제목: “API reference”전체 API reference에는 선언 파일에서 생성한 모든 공개 함수, 클래스, 타입, 상수, 오류 계약이 있습니다. 각 항목은 import path, release signature, package version을 표시합니다.
패키지 관계
섹션 제목: “패키지 관계”| 관계 | 패키지 | 함께 쓰는 이유 |
|---|---|---|
| 단독 사용 | — | 다른 GJ Kit 패키지 없이 도입할 수 있습니다. |
| 선택 조합 | @gj-kit/expo-media |
접근성 primitive로 미디어 선택·업로드 상태를 표현할 때 함께 사용합니다. |
| 선택 조합 | @gj-kit/expo-auth |
두 패키지를 결합하지 않은 채 앱 소유 세션 상태를 표시할 때 함께 사용합니다. |