콘텐츠로 이동

@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가 하나라도 남아 있으면 실패합니다. 토큰만 쓴다는 규칙을 리뷰가 아니라 테스트가 지킵니다.

완료 상태: 앱 전체 provider 하나에서 테마와 접근성이 적용된 버튼을 렌더링합니다.

Terminal window
pnpm add @gj-kit/expo-ui

2. 앱이 소유할 경계를 정합니다

섹션 제목: “2. 앱이 소유할 경계를 정합니다”

테마는 한 번만 만들고 앱을 감싸는 컴포넌트에 UiProvider를 둡니다. 네이티브 Button·IconButton·Menu·Select는 누름 피드백을 네이티브 스타일로 처리하므로 기본 상호작용에 NativeWind가 필요하지 않습니다.

먼저 아래 코드를 복사한 뒤, 위에서 언급한 앱 소유 값만 교체하세요.

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 state
const 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, 제품 고유 문구를 이 패키지에 넣기 위한 용도로는 사용하지 마세요.

엔진: node >=20

Peer 지원 범위 필요한 위치
react >=18 패키지 통합
react-native >=0.79 패키지 통합
react-native-safe-area-context >=4 문서화된 optional subpath에서만
react-native-web >=0.21 문서화된 optional 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에는 선언 파일에서 생성한 모든 공개 함수, 클래스, 타입, 상수, 오류 계약이 있습니다. 각 항목은 import path, release signature, package version을 표시합니다.

관계 패키지 함께 쓰는 이유
단독 사용 — 다른 GJ Kit 패키지 없이 도입할 수 있습니다.
선택 조합 @gj-kit/expo-media 접근성 primitive로 미디어 선택·업로드 상태를 표현할 때 함께 사용합니다.
선택 조합 @gj-kit/expo-auth 두 패키지를 결합하지 않은 채 앱 소유 세션 상태를 표시할 때 함께 사용합니다.

npm · GitHub