null_ong2-design-system
v0.2.2
Published
Production-ready React design system library
Maintainers
Readme
null_ong2-design-system
프로덕션 레디 React 디자인 시스템 라이브러리. 설치 즉시 사용 가능 (Zero-config).
설치
npm install null_ong2-design-systemPeer dependencies — React 18 이상 필요
npm install react react-dom빠른 시작
1. 스타일 시트 import (앱 진입점에서 1회)
// app/layout.tsx (Next.js App Router)
// 또는 _app.tsx (Next.js Pages Router)
// 또는 main.tsx (Vite/CRA)
import 'null_ong2-design-system/styles.css';v0.2.2부터 변경: 이전엔 자동 주입이었으나 SSR(Next.js App Router 등)에서 FOUC 발생 → 별도 CSS 파일을 export하도록 수정. 빌드 도구가 SSR HTML에 자동으로 인라인합니다.
2. 컴포넌트 사용
React Server Component 환경(Next.js App Router)에서도 import 가능합니다 — 패키지 내부에 "use client" 지시자가 포함되어 있습니다.
import { Button, Input, Alert, Stack } from 'null_ong2-design-system';
function App() {
return (
<Stack spacing="lg">
<Alert variant="info">디자인 시스템에 오신 것을 환영합니다.</Alert>
<Input
id="email"
label="이메일"
type="email"
placeholder="[email protected]"
required
/>
<Button variant="primary" onClick={() => alert('저장 완료')}>
저장하기
</Button>
</Stack>
);
}컴포넌트 목록
| 카테고리 | 컴포넌트 |
|---------|---------|
| Layout | Box, Flex, Stack, Grid, Container |
| Typography | Text, Heading, Label |
| Core UI | Button |
| Form | Input, Textarea, Checkbox, Radio, Select, Switch |
| Feedback | Spinner, Skeleton, Alert |
| Data Display | Avatar, Badge, Carousel |
| Navigation | Tabs |
| Overlay | Modal, Tooltip, DropdownMenu, Toast (ToastProvider + useToast) |
Layout 컴포넌트
Box
범용 레이아웃 컨테이너. as prop으로 렌더링 HTML 요소를 변경할 수 있다 (Polymorphic).
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| as | ElementType | 'div' | 렌더링할 HTML 요소 또는 컴포넌트 |
| padding | SpacingKey | — | 4방향 패딩 |
| paddingX | SpacingKey | — | 수평 패딩 |
| paddingY | SpacingKey | — | 수직 패딩 |
| margin | SpacingKey | — | 4방향 마진 |
| marginX | SpacingKey | — | 수평 마진 |
| marginY | SpacingKey | — | 수직 마진 |
| borderRadius | RadiusKey | — | 테두리 둥글기 |
| bg | string | — | 배경색 (CSS 변수 또는 색상값) |
| display | CSSProperties['display'] | — | display 속성 |
| width | CSSProperties['width'] | — | 너비 |
| height | CSSProperties['height'] | — | 높이 |
| overflow | CSSProperties['overflow'] | — | overflow 속성 |
SpacingKey:
'0''px''0.5''1''2''3''4''5''6''8''10''12''16''xs''sm''md''lg''xl''2xl'RadiusKey:
'none''sm''md''lg''xl''2xl''full'
import { Box } from 'null_ong2-design-system';
// 기본 사용
<Box padding="md" borderRadius="lg" bg="var(--ds-color-neutral-50)">
카드 콘텐츠
</Box>
// 다른 요소로 렌더링
<Box as="section" padding="xl" marginY="lg">
섹션 콘텐츠
</Box>
// 수평/수직 패딩 분리
<Box paddingX="lg" paddingY="sm" borderRadius="md">
배너
</Box>Flex
Flexbox 레이아웃 컴포넌트. as prop 지원.
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| as | ElementType | 'div' | 렌더링할 요소 |
| direction | CSSProperties['flexDirection'] | — | 'row' 'column' 등 |
| align | CSSProperties['alignItems'] | — | 'center' 'flex-start' 등 |
| justify | CSSProperties['justifyContent'] | — | 'center' 'space-between' 등 |
| wrap | CSSProperties['flexWrap'] | — | 'wrap' 'nowrap' 등 |
| gap | SpacingKey | — | 자식 요소 간 간격 |
| columnGap | SpacingKey | — | 열 간 간격 |
| rowGap | SpacingKey | — | 행 간 간격 |
| flex | CSSProperties['flex'] | — | flex 축약 속성 |
import { Flex } from 'null_ong2-design-system';
// 헤더: 양쪽 끝 배치
<Flex justify="space-between" align="center">
<span>로고</span>
<nav>메뉴</nav>
</Flex>
// 수평 아이템 간격
<Flex gap="md" wrap="wrap">
<Tag>React</Tag>
<Tag>TypeScript</Tag>
<Tag>Storybook</Tag>
</Flex>
// nav 요소로 렌더링
<Flex as="nav" gap="sm" align="center">
<a href="/">홈</a>
<a href="/about">소개</a>
</Flex>Stack
자식 요소를 일정 간격으로 쌓아 배치하는 컴포넌트. as prop 지원.
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| as | ElementType | 'div' | 렌더링할 요소 |
| spacing | SpacingKey | 'md' | 자식 요소 간 간격 |
| direction | 'column' | 'row' | 'column' | 쌓는 방향 |
| align | CSSProperties['alignItems'] | — | 교차축 정렬 |
| justify | CSSProperties['justifyContent'] | — | 주축 정렬 |
import { Stack } from 'null_ong2-design-system';
// 세로 카드 목록
<Stack spacing="md">
<Card>항목 1</Card>
<Card>항목 2</Card>
<Card>항목 3</Card>
</Stack>
// 넓은 간격의 섹션 구분
<Stack spacing="xl">
<Section>첫 번째 섹션</Section>
<Section>두 번째 섹션</Section>
</Stack>
// 가로 방향 스택
<Stack direction="row" spacing="sm" align="center">
<Avatar />
<span>사용자 이름</span>
</Stack>Grid
CSS Grid 레이아웃 컴포넌트. as prop 지원.
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| as | ElementType | 'div' | 렌더링할 요소 |
| columns | number \| string | — | 열 개수(숫자) 또는 grid-template-columns 값(문자열) |
| rows | number \| string | — | 행 개수 또는 grid-template-rows 값 |
| gap | SpacingKey | — | 셀 간격 (전체) |
| columnGap | SpacingKey | — | 열 간 간격 |
| rowGap | SpacingKey | — | 행 간 간격 |
| align | CSSProperties['alignItems'] | — | 셀 세로 정렬 |
| justify | CSSProperties['justifyItems'] | — | 셀 가로 정렬 |
columns에 숫자를 전달하면repeat(N, minmax(0, 1fr))로 자동 변환됩니다.
import { Grid } from 'null_ong2-design-system';
// 3열 그리드
<Grid columns={3} gap="md">
<Card>1</Card>
<Card>2</Card>
<Card>3</Card>
<Card>4</Card>
<Card>5</Card>
<Card>6</Card>
</Grid>
// 반응형 자동 채움
<Grid columns="repeat(auto-fill, minmax(250px, 1fr))" gap="lg">
{products.map((p) => <ProductCard key={p.id} product={p} />)}
</Grid>
// 커스텀 열 비율
<Grid columns="1fr 2fr 1fr" gap="md">
<Sidebar />
<MainContent />
<Aside />
</Grid>
// 열/행 간격 분리
<Grid columns={2} columnGap="lg" rowGap="sm">
<Cell>A</Cell>
<Cell>B</Cell>
<Cell>C</Cell>
<Cell>D</Cell>
</Grid>Container
콘텐츠의 최대 너비를 제한하고 가운데 정렬하는 컨테이너 컴포넌트. as prop 지원.
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| as | ElementType | 'div' | 렌더링할 요소 |
| maxWidth | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' | 'lg' | 최대 너비 |
| maxWidth | 값 |
|----------|-----|
| sm | 640px |
| md | 768px |
| lg | 1024px |
| xl | 1280px |
| 2xl | 1536px |
| full | 100% |
import { Container } from 'null_ong2-design-system';
// 기본 사용 (1024px)
<Container>
<main>페이지 콘텐츠</main>
</Container>
// 좁은 콘텐츠 (640px) — 블로그 글 등
<Container maxWidth="sm">
<article>블로그 본문...</article>
</Container>
// main 요소로 렌더링
<Container as="main" maxWidth="xl">
대시보드
</Container>Typography 컴포넌트
Text
본문 텍스트 컴포넌트. as prop 지원 (기본: <p>).
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| as | ElementType | 'p' | 렌더링할 요소 |
| size | FontSizeKey | 'base' | 폰트 크기 |
| weight | FontWeightKey | 'normal' | 폰트 두께 |
| color | 'default' | 'muted' | 'primary' | 'success' | 'warning' | 'danger' | 'default' | 텍스트 색상 |
| align | CSSProperties['textAlign'] | — | 텍스트 정렬 |
| truncate | boolean | false | 말줄임표 처리 |
FontSizeKey:
'xs''sm''base''lg''xl''2xl''3xl''4xl'FontWeightKey:
'normal''medium''semibold''bold'
import { Text } from 'null_ong2-design-system';
// 기본 단락
<Text>일반 본문 텍스트입니다.</Text>
// 크기 & 두께 조절
<Text size="lg" weight="semibold">큰 강조 텍스트</Text>
// 시맨틱 색상
<Text size="sm" color="muted">부가 설명 텍스트</Text>
<Text color="danger">오류 메시지입니다.</Text>
<Text color="success">성공적으로 처리되었습니다.</Text>
// 인라인 요소로 사용
<Text as="span" weight="bold" color="primary">강조 단어</Text>
// 말줄임표 (텍스트가 넘칠 때)
<div style={{ width: '200px' }}>
<Text truncate>
이 텍스트는 컨테이너 너비를 초과하면 말줄임표(...)로 처리됩니다.
</Text>
</div>
// 텍스트 정렬
<Text align="center">가운데 정렬 텍스트</Text>Heading
섹션 제목 컴포넌트. as prop으로 h1~h6 레벨을 지정하고, size로 시각적 크기를 독립적으로 설정할 수 있다.
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| as | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'h2' | 헤딩 레벨 |
| size | FontSizeKey | 레벨에 따라 자동 결정 | 시각적 폰트 크기 |
| color | string | --ds-color-neutral-900 | 텍스트 색상 |
| align | CSSProperties['textAlign'] | — | 텍스트 정렬 |
레벨별 기본 크기:
| 레벨 | 기본 size | 실제 크기 |
|------|-----------|----------|
| h1 | 4xl | 36px |
| h2 | 3xl | 30px |
| h3 | 2xl | 24px |
| h4 | xl | 20px |
| h5 | lg | 18px |
| h6 | base | 16px |
import { Heading } from 'null_ong2-design-system';
// 기본 사용 (h2 렌더링, 30px)
<Heading>섹션 제목</Heading>
// h1 페이지 제목
<Heading as="h1">메인 페이지 제목</Heading>
// SEO 구조와 시각적 크기 분리
// h2이지만 시각적으로 sm(14px) 크기
<Heading as="h2" size="sm">사이드바 제목</Heading>
// 색상 & 정렬
<Heading as="h3" color="var(--ds-color-primary-600)" align="center">
가운데 정렬된 섹션 제목
</Heading>Label
폼 입력 요소와 연결하는 레이블 컴포넌트.
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| htmlFor | string | — | 연결할 input의 id |
| required | boolean | — | 필수 여부 (빨간 * 표시) |
import { Label } from 'null_ong2-design-system';
// 기본 레이블
<Label htmlFor="username">사용자명</Label>
<input id="username" />
// 필수 표시
<Label htmlFor="email" required>이메일</Label>
<input id="email" type="email" />참고:
Input,Textarea,Select컴포넌트는labelprop을 제공하면 내부적으로 Label을 자동 렌더링합니다. 별도로 Label을 사용하지 않아도 됩니다.
Core UI 컴포넌트
Button
사용자 액션을 트리거하는 버튼 컴포넌트.
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| variant | 'primary' | 'secondary' | 'ghost' | 'danger' | 'primary' | 시각적 스타일 |
| size | 'sm' | 'md' | 'lg' | 'md' | 버튼 크기 |
| isLoading | boolean | false | 로딩 상태 (스피너 표시 + 클릭 비활성화) |
| disabled | boolean | false | 비활성화 |
| leftIcon | ReactNode | — | 텍스트 왼쪽 아이콘 |
| rightIcon | ReactNode | — | 텍스트 오른쪽 아이콘 |
ref를 전달할 수 있습니다 (forwardRef적용).
import { Button } from 'null_ong2-design-system';
// 기본 variant
<Button variant="primary">확인</Button>
<Button variant="secondary">취소</Button>
<Button variant="ghost">더 보기</Button>
<Button variant="danger">삭제</Button>
// 크기
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
// 로딩 상태 — 스피너 자동 표시, 클릭 불가
<Button isLoading>저장 중...</Button>
<Button variant="danger" isLoading>삭제 중...</Button>
// 비활성화
<Button disabled>제출 불가</Button>
// 아이콘 포함
<Button leftIcon={<PlusIcon />}>항목 추가</Button>
<Button rightIcon={<ArrowRightIcon />} variant="secondary">다음 단계</Button>
// ref 사용
const buttonRef = useRef<HTMLButtonElement>(null);
<Button ref={buttonRef} onClick={handleClick}>포커스 테스트</Button>Form 컴포넌트
Input
텍스트 입력 컴포넌트. 레이블, 힌트, 에러 메시지를 내장 지원한다.
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| id | string | — | input id (레이블 연결에 사용) |
| label | string | — | 레이블 텍스트 |
| hint | string | — | 힌트 메시지 (에러 없을 때 표시) |
| error | string | — | 에러 메시지 (있으면 에러 스타일 적용) |
| required | boolean | — | 필수 여부 (레이블에 * 표시) |
| type | string | 'text' | input type (email, password, number 등) |
| placeholder | string | — | 플레이스홀더 |
| disabled | boolean | false | 비활성화 |
ref를 전달할 수 있습니다. 모든<input>HTML 속성을 그대로 사용할 수 있습니다.
import { Input } from 'null_ong2-design-system';
// 기본 사용
<Input id="name" label="이름" placeholder="이름을 입력하세요" />
// 필수 입력 + 힌트
<Input
id="email"
label="이메일"
type="email"
placeholder="[email protected]"
hint="업무용 이메일을 입력하세요"
required
/>
// 에러 상태 — 에러 메시지 표시 + 빨간 테두리
<Input
id="password"
label="비밀번호"
type="password"
error="8자 이상 입력해주세요"
/>
// 비활성화
<Input
id="readonly"
label="읽기 전용"
defaultValue="수정할 수 없습니다"
disabled
/>
// ref 사용
const inputRef = useRef<HTMLInputElement>(null);
<Input ref={inputRef} id="search" placeholder="검색어 입력" />Textarea
여러 줄 텍스트 입력 컴포넌트. Input과 동일한 레이블/힌트/에러 패턴을 따른다.
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| id | string | — | textarea id |
| label | string | — | 레이블 텍스트 |
| hint | string | — | 힌트 메시지 |
| error | string | — | 에러 메시지 |
| required | boolean | — | 필수 여부 |
| rows | number | — | 초기 표시 행 수 |
| disabled | boolean | false | 비활성화 |
import { Textarea } from 'null_ong2-design-system';
// 기본 사용
<Textarea id="desc" label="설명" rows={4} placeholder="내용을 입력하세요" />
// 에러 상태
<Textarea
id="review"
label="후기"
error="최소 20자 이상 입력해주세요"
/>
// 필수 + 힌트
<Textarea
id="bio"
label="자기소개"
rows={5}
hint="최대 500자까지 입력 가능합니다"
required
/>Checkbox
체크박스 컴포넌트. Controlled 및 Uncontrolled 모두 지원.
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| id | string | — | checkbox id |
| label | string | — | 체크박스 레이블 |
| checked | boolean | — | 체크 상태 (controlled) |
| defaultChecked | boolean | — | 초기 체크 상태 (uncontrolled) |
| onChange | (checked: boolean, event) => void | — | 상태 변경 핸들러 |
| disabled | boolean | — | 비활성화 |
onChange는 첫 번째 인자로boolean(체크 여부)을 받습니다.
import { Checkbox } from 'null_ong2-design-system';
// Uncontrolled
<Checkbox id="agree" label="이용약관에 동의합니다" />
// Controlled
const [agreed, setAgreed] = useState(false);
<Checkbox
id="terms"
label="개인정보 처리에 동의합니다"
checked={agreed}
onChange={setAgreed}
/>
// 비활성화 상태
<Checkbox id="locked" label="변경 불가" disabled defaultChecked />Radio
라디오 버튼 컴포넌트. 동일한 name을 가진 그룹 중 하나를 선택할 수 있다.
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| id | string | — | radio id |
| name | string | — | 라디오 그룹 이름 (동일 그룹은 같은 name) |
| value | string | — | 선택 값 |
| label | string | — | 라디오 레이블 |
| checked | boolean | — | 선택 상태 (controlled) |
| onChange | (value: string, event) => void | — | 값 변경 핸들러 |
| disabled | boolean | — | 비활성화 |
onChange는 첫 번째 인자로string(선택된 value)을 받습니다.
import { Radio } from 'null_ong2-design-system';
// 라디오 그룹
const [method, setMethod] = useState('email');
<Radio
id="contact-email"
name="contact"
value="email"
label="이메일"
checked={method === 'email'}
onChange={setMethod}
/>
<Radio
id="contact-phone"
name="contact"
value="phone"
label="전화"
checked={method === 'phone'}
onChange={setMethod}
/>
<Radio
id="contact-sms"
name="contact"
value="sms"
label="SMS"
checked={method === 'sms'}
onChange={setMethod}
/>Select
드롭다운 선택 컴포넌트. 옵션 목록을 options prop으로 전달한다.
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| id | string | — | select id |
| label | string | — | 레이블 텍스트 |
| options | SelectOption[] | 필수 | 선택 옵션 목록 |
| placeholder | string | — | 플레이스홀더 옵션 텍스트 |
| hint | string | — | 힌트 메시지 |
| error | string | — | 에러 메시지 |
| required | boolean | — | 필수 여부 |
| disabled | boolean | false | 비활성화 |
// SelectOption 타입
interface SelectOption {
value: string;
label: string;
disabled?: boolean; // 개별 옵션 비활성화
}import { Select } from 'null_ong2-design-system';
const countries = [
{ value: 'kr', label: '대한민국' },
{ value: 'us', label: '미국' },
{ value: 'jp', label: '일본' },
{ value: 'cn', label: '중국', disabled: true }, // 선택 불가
];
// 기본 사용
<Select
id="country"
label="국가"
options={countries}
placeholder="선택하세요"
/>
// 에러 상태
<Select
id="country"
label="국가"
options={countries}
placeholder="선택하세요"
error="국가를 선택해주세요"
/>
// Controlled
const [country, setCountry] = useState('');
<Select
id="country"
label="거주 국가"
options={countries}
value={country}
onChange={(e) => setCountry(e.target.value)}
hint="현재 거주 중인 국가를 선택하세요"
required
/>Feedback 컴포넌트
Spinner
로딩 상태를 표시하는 회전 스피너 컴포넌트.
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| size | 'xs' | 'sm' | 'md' | 'lg' | 'md' | 스피너 크기 |
| label | string | '로딩 중' | 스크린 리더를 위한 레이블 |
스피너 색상은 부모 요소의
color를 상속합니다 (currentColor).
import { Spinner } from 'null_ong2-design-system';
// 기본 사용
<Spinner />
// 크기
<Spinner size="xs" />
<Spinner size="sm" />
<Spinner size="md" />
<Spinner size="lg" />
// 색상 변경 (부모 color 상속)
<span style={{ color: '#3b82f6' }}>
<Spinner size="md" />
</span>
// 커스텀 접근성 레이블
<Spinner label="데이터를 불러오는 중입니다" />Skeleton
콘텐츠 로딩 중 플레이스홀더로 사용하는 shimmer 컴포넌트.
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| width | number \| string | — | 너비 (숫자=px, 문자열=CSS 값) |
| height | number \| string | — | 높이 |
| variant | 'rectangular' | 'text' | 'circle' | 'rectangular' | 형태 |
import { Skeleton } from 'null_ong2-design-system';
// 이미지 플레이스홀더
<Skeleton width={300} height={200} />
// 텍스트 줄 (height 자동 = 1em)
<Skeleton variant="text" width="80%" />
<Skeleton variant="text" width="60%" />
// 아바타 원형
<Skeleton variant="circle" width={48} height={48} />
// 카드 로딩 상태 조합
<Flex gap="md" align="flex-start">
<Skeleton variant="circle" width={48} height={48} />
<Stack spacing="sm" style={{ flex: 1 }}>
<Skeleton variant="text" width="60%" />
<Skeleton variant="text" width="90%" />
<Skeleton variant="text" width="75%" />
</Stack>
</Flex>
// CSS 퍼센트 값
<Skeleton width="100%" height={120} />Alert
사용자에게 피드백 메시지를 전달하는 알림 컴포넌트. 각 variant마다 기본 아이콘이 포함되어 있다.
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| variant | 'info' | 'success' | 'warning' | 'danger' | 'info' | 알림 종류 |
| title | string | — | 알림 제목 (선택) |
| children | ReactNode | 필수 | 알림 내용 |
| icon | ReactNode | variant별 기본 아이콘 | 커스텀 아이콘 |
warning과danger는role="alert"(즉시 알림),info와success는role="status"(비간섭 알림)를 사용합니다.
import { Alert } from 'null_ong2-design-system';
// 정보 알림
<Alert variant="info">시스템 점검이 예정되어 있습니다.</Alert>
// 성공 — 제목 + 내용
<Alert variant="success" title="저장 완료">
변경 사항이 성공적으로 저장되었습니다.
</Alert>
// 경고
<Alert variant="warning" title="주의">
이 작업은 되돌릴 수 없습니다. 신중히 진행해주세요.
</Alert>
// 오류
<Alert variant="danger" title="오류 발생">
요청을 처리하는 도중 문제가 발생했습니다. 잠시 후 다시 시도해주세요.
</Alert>
// 커스텀 아이콘
<Alert variant="info" icon={<BellIcon />}>
새 알림이 있습니다.
</Alert>
// 제목 없이 내용만
<Alert variant="warning">
비밀번호가 곧 만료됩니다. 변경해주세요.
</Alert>Switch (Form)
ON/OFF 즉시 반영 토글. Checkbox와 달리 설정 화면에 적합.
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| id | string | — | switch id |
| label | string | — | 레이블 텍스트 |
| size | 'sm' | 'md' | 'lg' | 'md' | 크기 |
| checked | boolean | — | 상태 (controlled) |
| defaultChecked | boolean | — | 초기 상태 (uncontrolled) |
| onChange | (checked: boolean, event) => void | — | 변경 핸들러 |
| disabled | boolean | — | 비활성화 |
import { Switch } from 'null_ong2-design-system';
const [enabled, setEnabled] = useState(false);
<Switch id="notify" label="알림 받기" checked={enabled} onChange={setEnabled} />Avatar (Data Display)
이미지 + 이니셜 fallback 아바타.
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| src | string | — | 이미지 URL |
| name | string | — | 이름 (alt + 이니셜 fallback에 사용) |
| alt | string | — | 이미지 alt (지정 시 우선) |
| size | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 크기 |
| shape | 'circle' | 'square' | 'circle' | 형태 |
import { Avatar } from 'null_ong2-design-system';
<Avatar src="/me.jpg" name="홍길동" />
<Avatar name="John Doe" size="lg" /> // 이미지 없으면 "JD" 표시
<Avatar src="/broken.jpg" name="Jane" /> // 로드 실패 시 "JA"로 fallbackBadge (Data Display)
상태/카운트 표시 라벨.
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| variant | 'neutral' | 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'neutral' | 시각 스타일 |
| size | 'sm' | 'md' | 'md' | 크기 |
| dot | boolean | false | 점 형태 (텍스트 없이 알림 표시) |
import { Badge } from 'null_ong2-design-system';
<Badge variant="success">활성</Badge>
<Badge variant="danger">99+</Badge>
<Badge variant="danger" dot /> // 빨간 점만 표시Carousel (Data Display)
슬라이드를 한 번에 하나씩 보여주는 캐루셀.
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| items | ReactNode[] | 필수 | 슬라이드 콘텐츠 배열 |
| index | number | — | 현재 인덱스 (controlled) |
| defaultIndex | number | 0 | 초기 인덱스 |
| onChange | (index: number) => void | — | 변경 핸들러 |
| autoPlayInterval | number | 0 | 자동 재생 간격(ms). 0이면 비활성 |
| loop | boolean | true | 무한 순환 |
| showArrows | boolean | true | 좌우 화살표 표시 |
| showIndicators | boolean | true | 점 인디케이터 표시 |
import { Carousel } from 'null_ong2-design-system';
<Carousel
items={[
<img src="/1.jpg" alt="배너 1" />,
<img src="/2.jpg" alt="배너 2" />,
<img src="/3.jpg" alt="배너 3" />,
]}
autoPlayInterval={3000}
/>Tabs (Navigation)
컴파운드 컴포넌트 패턴 기반 탭. 키보드 화살표 내비게이션 지원.
| 컴포넌트 | 역할 |
|----------|------|
| Tabs.Root | 컨텍스트 제공자 (defaultValue / value / onChange / orientation) |
| Tabs.List | 탭 트리거 컨테이너 (role="tablist") |
| Tabs.Trigger | 개별 탭 버튼 (value / disabled) |
| Tabs.Panel | 매칭되는 트리거가 활성일 때 렌더 (value) |
import { Tabs } from 'null_ong2-design-system';
<Tabs.Root defaultValue="profile">
<Tabs.List aria-label="계정 섹션">
<Tabs.Trigger value="profile">프로필</Tabs.Trigger>
<Tabs.Trigger value="account">계정</Tabs.Trigger>
<Tabs.Trigger value="notifications">알림</Tabs.Trigger>
</Tabs.List>
<Tabs.Panel value="profile">프로필 화면</Tabs.Panel>
<Tabs.Panel value="account">계정 화면</Tabs.Panel>
<Tabs.Panel value="notifications">알림 설정</Tabs.Panel>
</Tabs.Root>
// 세로 탭
<Tabs.Root defaultValue="overview" orientation="vertical">...</Tabs.Root>Modal (Overlay)
오버레이 다이얼로그. 포커스 트랩 + ESC 닫기 + 오버레이 클릭 닫기 자동 적용.
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| isOpen | boolean | 필수 | 열림 상태 |
| onClose | () => void | 필수 | 닫기 핸들러 |
| title | string | — | 제목 |
| children | ReactNode | 필수 | 본문 |
| footer | ReactNode | — | 푸터 (버튼 그룹 등) |
| size | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 크기 |
| closeOnOverlayClick | boolean | true | 오버레이 클릭으로 닫기 |
| closeOnEscape | boolean | true | ESC로 닫기 |
| showCloseButton | boolean | true | 닫기 버튼 표시 |
import { Modal, Button } from 'null_ong2-design-system';
const [open, setOpen] = useState(false);
<Button onClick={() => setOpen(true)}>삭제</Button>
<Modal
isOpen={open}
onClose={() => setOpen(false)}
title="삭제 확인"
footer={
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
<Button variant="secondary" onClick={() => setOpen(false)}>취소</Button>
<Button variant="danger" onClick={handleDelete}>삭제</Button>
</div>
}
>
정말 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
</Modal>Tooltip (Overlay)
호버/포커스 시 부가 설명 표시. @floating-ui/react 기반 자동 위치 계산.
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| content | ReactNode | 필수 | 툴팁 내용 |
| children | ReactElement | 필수 | 트리거 요소 (단일 element) |
| placement | Placement | 'top' | 위치 (top/bottom/left/right + start/end) |
| delay | number | 200 | 표시 지연(ms) |
| disabled | boolean | false | 비활성화 |
import { Tooltip, Button } from 'null_ong2-design-system';
<Tooltip content="저장합니다 (Cmd+S)">
<Button>저장</Button>
</Tooltip>
<Tooltip content="설명" placement="right">
<Button variant="ghost">정보</Button>
</Tooltip>DropdownMenu (Overlay)
클릭 트리거 드롭다운 메뉴.
| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| trigger | ReactElement | 필수 | 트리거 요소 |
| items | DropdownMenuItem[] | 필수 | 메뉴 항목 목록 |
| placement | Placement | 'bottom-start' | 메뉴 위치 |
interface DropdownMenuItem {
key: string;
label: ReactNode;
onSelect?: () => void;
disabled?: boolean;
destructive?: boolean; // 빨간 텍스트
}import { DropdownMenu, Button } from 'null_ong2-design-system';
<DropdownMenu
trigger={<Button variant="secondary">옵션</Button>}
items={[
{ key: 'edit', label: '수정', onSelect: handleEdit },
{ key: 'duplicate', label: '복제', onSelect: handleDuplicate },
{ key: 'delete', label: '삭제', onSelect: handleDelete, destructive: true },
]}
/>Toast (Overlay)
전역 알림 시스템. 앱 루트에 <ToastProvider>를 한 번 감싸고, useToast() 훅으로 사용.
1. Provider 설정
import { ToastProvider } from 'null_ong2-design-system';
function App() {
return (
<ToastProvider position="bottom-right" maxToasts={5}>
<YourApp />
</ToastProvider>
);
}| Prop | 타입 | 기본값 | 설명 |
|------|-----|--------|------|
| position | 'top-left' | 'top-right' | 'top-center' | 'bottom-left' | 'bottom-right' | 'bottom-center' | 'bottom-right' | 표시 위치 |
| maxToasts | number | 5 | 동시 표시 최대 개수 |
2. 사용
import { useToast } from 'null_ong2-design-system';
function SaveButton() {
const toast = useToast();
return (
<Button
onClick={async () => {
try {
await save();
toast.success('저장되었습니다');
} catch {
toast.danger('저장에 실패했습니다', { duration: 5000 });
}
}}
>
저장
</Button>
);
}Toast API
| 메서드 | 설명 |
|--------|------|
| toast.show(message, options?) | 일반 토스트 |
| toast.success(message, options?) | 성공 |
| toast.warning(message, options?) | 경고 |
| toast.danger(message, options?) | 오류 |
| toast.info(message, options?) | 정보 |
| toast.dismiss(id) | 특정 토스트 닫기 (id는 show 반환값) |
options.duration — 자동 소멸 시간(ms). 기본 3000. 0이면 수동 닫기만 가능.
디자인 토큰
모든 시각적 값은 CSS 커스텀 프로퍼티(변수)로 정의되어 있습니다. import 시 자동으로 :root에 주입됩니다.
색상
/* 브랜드 */
--ds-color-primary-50 /* #eff6ff */
--ds-color-primary-500 /* #3b82f6 */
--ds-color-primary-600 /* #2563eb */
--ds-color-primary-700 /* #1d4ed8 */
/* 중립 (Neutral) */
--ds-color-neutral-0 /* #ffffff */
--ds-color-neutral-100 /* #f3f4f6 */
--ds-color-neutral-500 /* #6b7280 */
--ds-color-neutral-900 /* #111827 */
/* 시맨틱 */
--ds-color-success /* #22c55e */
--ds-color-warning /* #f59e0b */
--ds-color-danger /* #ef4444 */
--ds-color-info /* #3b82f6 */간격 (Spacing)
--ds-spacing-xs /* 4px */
--ds-spacing-sm /* 8px */
--ds-spacing-md /* 16px */
--ds-spacing-lg /* 24px */
--ds-spacing-xl /* 32px */
--ds-spacing-2xl /* 48px */테두리 둥글기 (Radius)
--ds-radius-sm /* 4px */
--ds-radius-md /* 6px */
--ds-radius-lg /* 8px */
--ds-radius-xl /* 12px */
--ds-radius-full /* 9999px */TypeScript에서 토큰 사용
import { SPACING, RADIUS, FONT_SIZE, FONT_WEIGHT, Z_INDEX } from 'null_ong2-design-system';
// SPACING.md → 'var(--ds-spacing-md)'
// RADIUS.lg → 'var(--ds-radius-lg)'
// FONT_SIZE.xl → 'var(--ds-font-size-xl)'테마 커스터마이징
소비 프로젝트의 글로벌 CSS에서 CSS 변수를 덮어쓰면 테마를 변경할 수 있습니다:
:root {
/* 브랜드 색상을 보라색으로 변경 */
--ds-color-primary-500: #8b5cf6;
--ds-color-primary-600: #7c3aed;
--ds-color-primary-700: #6d28d9;
/* 테두리를 더 둥글게 */
--ds-radius-md: 8px;
/* 커스텀 폰트 */
--ds-font-sans: 'Pretendard', -apple-system, sans-serif;
}Hooks
useControllable
Controlled/Uncontrolled 상태를 통합 관리하는 훅.
import { useControllable } from 'null_ong2-design-system';
function Toggle({ value, defaultValue = false, onChange }) {
const [isOn, setIsOn] = useControllable({
value,
defaultValue,
onChange,
});
return <button onClick={() => setIsOn(!isOn)}>{isOn ? 'ON' : 'OFF'}</button>;
}
// Uncontrolled (내부 상태 자동 관리)
<Toggle defaultValue={false} />
// Controlled (외부에서 상태 관리)
<Toggle value={isEnabled} onChange={setIsEnabled} />설계 철학
- Zero-config — 설치 후 import만 하면 동작. CSS import나 ThemeProvider 설정 불필요.
- 접근성 기본 내장 — ARIA 속성, 키보드 내비게이션, 시맨틱 HTML이 기본 적용.
- 타입 안전 — TypeScript
strict: true환경에서 작성. 모든 Props 타입 export. - Polymorphic — Layout 컴포넌트에
asprop 지원으로 렌더링 요소 자유 변경. - 토큰 기반 — 모든 시각적 값이 CSS 변수. 소비 프로젝트에서 변수 override로 테마 커스터마이징 가능.
- Tree-shakable — 사용하는 컴포넌트만 번들에 포함.
개발 환경
# 의존성 설치
npm install
# Storybook 실행 (http://localhost:6006)
npm run storybook
# 테스트
npm test
# 타입 체크
npm run type-check
# 빌드
npm run build기여 가이드
- 레포지토리 Fork
- 기능 브랜치 생성:
git checkout -b feat/new-component - 컴포넌트 파일 구조 준수:
ComponentName.ui.tsx— Presentational 컴포넌트ComponentName.ui.test.tsx— 테스트ComponentName.ui.stories.tsx— Storybook 스토리index.ts— barrel export
- 모든 검사 통과 확인:
npm run type-check && npm run test && npm run build - Pull Request 제출
라이선스
MIT © null_ong2
