@howmuchhome-web/ui
얼마집 디자인 시스템 패키지. web/dashboard(Next.js)와 native(Expo) 앱이 공유하는 공통 UI 컴포넌트를 제공합니다.
monorepo 내부 패키지로 별도 설치 없이 사용합니다. 컴포넌트를 새로 만들 때는 packages/ui/CLAUDE.md의 완성 체크리스트를 함께 참고하세요. native 소비 세부(Metro 아이콘 치환·NativeWind 등)는 크로스플랫폼 UI와 Metro와 의존성에 있습니다.
진입점
package.json의 exports가 정의하는 진입점입니다.
| 진입점 | 대상 | 실제 경로 |
|---|---|---|
@howmuchhome-web/ui/v2 | web/dashboard — 현행 컴포넌트 | v2/index.ts |
@howmuchhome-web/ui/native | native(Expo) — native 구현이 있는 컴포넌트만 | native/index.ts |
@howmuchhome-web/ui/v1 | 레거시 (수정 금지) | v1/index.ts |
@howmuchhome-web/ui/utils | 플랫폼 무관 유틸 컴포넌트/훅 | utils/index.ts |
web은
/v2, native는/native. 같은/v2경로로 자동 분기하지 않는 이유(번들러 조건·export *제약)와 native 배럴에 등록된 컴포넌트 목록은 크로스플랫폼 UI를 참고하세요.
// web / dashboard
import {Button, Dialog, useOverlay} from '@howmuchhome-web/ui/v2';
// native (Expo)
import {Button, Form} from '@howmuchhome-web/ui/native';plain HTML을 쓰지 않는 이유
버튼·입력 등을 <button>/<input>으로 직접 만들지 말고 반드시 디자인 시스템 컴포넌트를 씁니다. Button 하나가 색/사이즈 토큰, 로딩 스피너 전환, Interaction.Overlay(눌림 피드백), 비동기 autoLoading, disabled 오버레이, 폼 제출 연동을 모두 캡슐화합니다. 이 동작들을 각 화면에서 다시 구현하면 디자인·접근성·상호작용 일관성이 깨집니다.
// ✅
<Button style="filled" color="primary" onClick={handleClick}>확인</Button>
// ❌
<button className="rounded border px-4 py-2 ...">확인</button>디렉토리 구조
패키지 루트(packages/ui/) 바로 아래에 진입점 디렉토리가 있습니다(src/ 없음).
packages/ui/
├── v2/ # web 현행 컴포넌트 (+ .native.tsx / .shared.ts)
│ ├── Button/ Dialog/ Overlay/ Form/ ...
│ ├── common/ # 색 매핑(colors.ts)·공유 타입·InputView 등
│ ├── provider/ # HowmuchhomeUIProvider (UIProvider)
│ └── utils/ # OnPortal / Popover / InfiniteScroll 등 내부 유틸
├── native/index.ts # native 전용 배럴 (v2의 .native 구현만 re-export)
├── v1/ # 레거시
└── utils/ # 플랫폼 무관 컴포넌트/훅 (InputWrapperForIOS, usePreventScroll 등)각 컴포넌트 폴더의 파일 규약:
| 파일 | 역할 |
|---|---|
Foo.tsx | web 구현 (DOM/Tailwind, 'use client' 필수) |
Foo.native.tsx | native 구현 (RN + react-native-css) |
Foo.shared.ts | web/native 공유 순수 로직·상수 (DOM/RN import 금지) |
index.ts | named export |
Foo.stories.tsx / Foo.stories.native.tsx | Storybook |
대표 컴포넌트 (v2)
Button
<Button style="filled" color="primary" onClick={handleClick}>확인</Button>
<Button style="transparent" color="error" size="small">삭제</Button>| Prop | 타입 | 비고 |
|---|---|---|
style | 'filled' | 'transparent' | 필수 |
color | 'primary' | 'secondary' | 'tertiary' | 'error' | 'error-secondary' | 'white' | 필수 |
size | 'extra-small' | 'small' | 'medium' | 'large' | 기본 medium |
startIcon / endIcon | ReactElement<IconProps> | 크기·currentColor를 자동 주입 |
fullWidth | boolean | 기본 false |
loading | boolean | 스피너 표시 |
autoLoading | boolean | 'persist-on-success' | 기본 true. onClick이 Promise면 자동 로딩. 'persist-on-success'는 성공 시 로딩 유지(에러는 re-throw 필요) |
underline | boolean | 텍스트 밑줄 |
관련 export: SimpleTextButton, IconButton, ToggleButton(v2/Button/index.ts).
Dialog
DialogRoot에 하위 컴포넌트를 Object.assign으로 붙인 합성(compound) 컴포넌트입니다.
const dialog = useOverlay();
<Dialog controller={dialog.controller}>
<Dialog.Header>제목</Dialog.Header>
<Dialog.Body>내용</Dialog.Body>
<Dialog.Footer>
<Dialog.Buttons>
<Button style="filled" color="primary" onClick={dialog.close}>확인</Button>
</Dialog.Buttons>
</Dialog.Footer>
</Dialog>| 서브 컴포넌트 | 파일 |
|---|---|
Dialog (root) | Dialog/DialogRoot.tsx |
Dialog.Header | Dialog/DialogHeader.tsx |
Dialog.Body | Dialog/DialogBody.tsx |
Dialog.Footer | Dialog/DialogFooter.tsx |
Dialog.Buttons | Dialog/DialogFooterButtons.tsx |
useOverlay
오버레이(Dialog/Drawer 등) 열기·닫기 컨트롤러 훅입니다.
const {controller, open, close} = useOverlay({
initialState: false,
disableHistoryStack: false, // 기본 false: close()가 history.back()으로 popstate 트리거
onOpen: () => {/* 열릴 때 form 초기화 등 */},
});close()는 Promise<void>를 반환합니다(web은 즉시 resolve, native와 API 호환). 기본값에서 web close()는 history.back()을 호출해 안드로이드 뒤로가기/popstate와 오버레이 닫힘을 연동합니다. 이 동작을 끄려면 disableHistoryStack: true.
이 외 다수 컴포넌트가 있습니다(v2/index.ts 전체 목록): Alert/Confirm, TextField/PasswordField/SearchBar/Textarea, Checkbox/RadioGroup/ToggleSwitch, Chip/Tag/Badge, Tabs/SegmentedControl/SegmentTab, Accordion/Collapsible, Dropdown/DropdownMenu/DropdownSelect, Toast/Snackbar, Scaffold/AppBar/PageLayout, Table/DataView, Carousel, DatePicker, ScrollPicker, SecurityKeypad 등.
Provider
앱 최상위 layout.tsx에 HowmuchhomeUIProvider(export 이름은 provider의 UIProvider)를 한 번 배치합니다. Portal 루트(#_root_portal), popstate 스택(오버레이 뒤로가기), window 레이아웃 컨텍스트, iOS 포커스 보정, 컨텍스트 메뉴 차단을 담당합니다.
import {HowmuchhomeUIProvider} from '@howmuchhome-web/ui/v2';
<HowmuchhomeUIProvider preventContextMenu>
{children}
</HowmuchhomeUIProvider>preventContextMenu는 기본 true(웹뷰에서 네이티브 느낌). 데스크톱 대시보드처럼 브라우저 우클릭 메뉴를 허용하려면 false.
색 / 테마 시스템
색 시스템은 @howmuchhome-web/style 패키지가 단일 소스입니다.
packages/style/colors.css— CSS 변수 정의. 원시 팔레트(--color-blue-500등, OKLCH 기준 RGB triple)와 그 위의 semantic v2 토큰(--color-fill-solid-brand-primary,--color-label-normal-primary,--color-border-normal-secondary,--color-background-normal-primary…)을 계층으로 둡니다.packages/style/colors/colorV4.ts— 동일 팔레트의 JS/TS 상수. CSS 변수를 쓸 수 없는 곳(RN 아이콘 색 등)에서 hex 값이 필요할 때 사용합니다.@howmuchhome-web/style에서colorV4와twMerge를 export합니다.
컴포넌트에서는 hex를 직접 쓰지 않고 semantic Tailwind 클래스를 씁니다. Tailwind 테마가 카테고리 접두(fill/label/border/background)를 벗겨 클래스명에 매핑하므로, 예를 들어 클래스 bg-solid-brand-primary → 변수 --color-fill-solid-brand-primary, text-normal-primary → --color-label-normal-primary로 해석됩니다.
Button 색 결정 로직은 v2/common/colors.ts에 단일 소스로 있습니다. getButtonBackgroundClassName·getButtonTextColorClassname이 (style, color)를 받아 semantic 클래스 문자열을 반환하고, web Button.tsx와 native Button.native.tsx가 같은 함수를 사용합니다. native는 이 클래스에 이미 담긴 group-disabled:/disabled: 변형을 런타임에 bare 클래스로 변환(Button.shared.ts)해서 disabled 색을 적용합니다 — 색 분기 자체는 common/colors.ts 한 곳에만 둡니다.
아이콘 / 에셋 연계
아이콘은 별도 패키지 **@howmuchhome-web/icon**입니다.
@howmuchhome-web/icon— 아이콘 컴포넌트 배럴(libs/index.ts)@howmuchhome-web/icon/types—IconProps등 타입
Button의 startIcon/endIcon은 ReactElement<IconProps>를 받아, size가 없으면 버튼 size에 맞춘 크기를, color가 없으면 currentColor를(native는 텍스트 색 hex를) 자동 주입합니다.
native에서는 Metro가 web 아이콘 구현을 native 구현으로 치환합니다. 세부는 Metro와 의존성을 참고하세요.
새 컴포넌트 추가
전체 체크리스트는 packages/ui/CLAUDE.md를 따릅니다. 요지:
- web은
v2에만 추가합니다.v1/은 레거시라 절대 수정하지 않습니다. v2/[ComponentName]/ComponentName.tsx('use client'필수) +index.tsnamed export +v2/index.ts에export * from './[ComponentName]'추가..stories.tsx작성 (Controls 동작하도록argTypes).- native에서도 쓰는 컴포넌트라면 플랫폼별로 분리합니다:
Foo.tsx(web) /Foo.native.tsx(RN +react-native-css) /Foo.shared.ts(공유 순수 로직). native 구현은native/index.ts배럴에 등록해야@howmuchhome-web/ui/native로 노출됩니다. - native className은
react-native의View/Text가 아니라react-native-css/components의 컴포넌트에 적용해야 먹습니다. 색은 web과 동일한 semantic 클래스를 쓰고, 분기가 필요하면common/colors.ts에 둡니다.
분리 규약·native 전용 컴포넌트 동작(Scaffold, BottomSheet, Reveal, OTPInput 등)의 세부는 크로스플랫폼 UI에 정리되어 있습니다.