Bigtablet Design System에 기여해 주셔서 감사합니다!
- Node.js 18+
- pnpm 10.20.0+ (필수)
# 저장소 클론
git clone https://github.com/Bigtablet/bigtablet-design-system.git
cd bigtablet-design-system
# 의존성 설치
pnpm install
# Storybook 실행
pnpm storybook
# 테스트 실행
pnpm test
# 빌드
pnpm build| 스크립트 | 설명 |
|---|---|
pnpm storybook |
Storybook 개발 서버 (port 6006) |
pnpm build |
라이브러리 빌드 |
pnpm dev |
Watch 모드 개발 |
pnpm test |
테스트 실행 |
pnpm test:watch |
테스트 Watch 모드 |
pnpm test:coverage |
커버리지 리포트 |
pnpm test:storybook |
a11y 테스트 (Storybook + Playwright) |
pnpm lint |
ESLint 실행 |
pnpm typecheck |
TypeScript 타입 체크 |
작업 전에 관련 Issue가 있는지 확인하거나 새로 생성합니다.
# Issue 생성
gh issue create --title "feat: Add new component" --body "Description..."git checkout develop
git pull origin develop
git checkout -b feat/new-component컴포넌트 개발 시 다음 구조를 따릅니다. 폴더 이름은 kebab-case 로 짓고, 테스트·스토리 파일은 그 폴더명을 그대로 씁니다:
src/ui/{category}/{component-name}/
├── index.tsx # 컴포넌트 구현
├── style.scss # Global SCSS 스타일
├── {component-name}.test.tsx # 테스트
└── {component-name}.stories.tsx # Storybook (선택)
overflow-y: auto 를 쓰는 곳에는 @include token.scrollable; 을 함께 넣는다 — 스크롤바가
얇고 브랜드색이 된다. 네이티브 스크롤을 그대로 쓴다(커스텀 스크롤 컴포넌트는 두지 않는다).
.panel_list {
max-height: token.$overlay_list_max_height;
overflow-y: auto;
@include token.scrollable;
}목록에서 방향키로 활성 항목을 옮긴다면 useListboxPopup 의 listRef 를 스크롤 컨테이너에
붙인다 — 활성 항목이 화면 밖으로 나갈 때만 따라 스크롤한다. 포커스가 입력에 남는 APG 패턴에서는
브라우저가 알아서 스크롤해 주지 않는다.
컴포넌트 안에 문구를 박지 말고 로케일 카탈로그에 키를 만든다
(src/ui/system/locale-provider/messages.ts — ko 와 en 양쪽).
// ❌ 이렇게 두면 <LocaleProvider> 로 바꿀 수 없다
const Foo = ({ hint = "드래그해서 옮기세요" }: FooProps) => …
// ✅ prop 은 그대로 두고 기본값만 카탈로그에서 받는다
const Foo = ({ hint: hintProp }: FooProps) => {
const t = useLocaleText();
const hint = hintProp ?? t("foo.hint");키는 섹션 안에서 알파벳순으로 넣는다 (combobox → datePicker → dateRange → dropdown …).
아무 데나 끼우면 다음 사람이 위치를 예측할 수 없다.
pnpm check:defaults 가 다섯 가지를 막는다.
- 컴포넌트에 박아 넣은 한글 문구 — prop 기본값이든 JSX 안이든. prop 이름으로 고르지 않는다
(
*Label/*Text패턴만 보던 시절rowClickHint·hint·label여섯 개가 그대로 새어 나갔다) - 카탈로그 문구에 한글이 있는지
- 카탈로그 키 ↔
t("...")호출 양방향 — 키만 넣고 배선을 잊거나, 없는 키를 부르는 것 - 카탈로그 키가 섹션 안에서 알파벳순인지
docs/COMPONENTS.mdprop 표의 Default 열이 실제 기본값과 같은지
개발자 콘솔로만 나가는 메시지([Bigtablet DS] …)는 대상이 아니다 — 사용자가 아니라 개발자가 읽는다.
pnpm check:dark-text 가 막는다. --bt-color-status-*(bare)·--bt-color-brand-primary 처럼
양 테마에서 같은 값인 색을 테마 표면 위의 텍스트로 쓰면 다크에서 AA 미달이다 - 실측으로
danger Button 이 다크에서 3.06:1(페이지)·2.85:1(패널) 이었다. 텍스트에는 _on_surface
(Vanilla -text) 쪽을 쓴다. 고정 표면 위 텍스트(_on_primary·_on_dark·_on_default·
_on_container)는 이름으로 예외 처리된다. 자세한 표는 THEMING.
a11y 스토리 러너는 라이트만 돌아 axe 가 이 결함을 못 잡는다 - 그래서 정적 검사가 필요하다.
컴포넌트 폴더 안의 세 파일이 같은 이름을 공유해야 한다. pnpm check:filenames 가 테스트·스토리 파일명을 그 폴더명(또는 같은 폴더의 소스 파일명)과 대조한다 - 폴더 이름 자체는 검사하지 않으므로 kebab-case 로 짓는 것은 규약으로 지킨다:
src/ui/display/data-view/
├── index.tsx
├── data-view.test.tsx
└── data-view.stories.tsx
컴포넌트 이름(DataView)이 아니라 폴더 이름이다. 목록에서 짝이 바로 보이고, 폴더를 옮길 때
파일명을 따로 고칠 일이 없다.
두 가지만 예외다 - 옆에 같은 이름의 소스가 있는 테스트(crop.util.ts ↔ crop.util.test.ts)와
카테고리 폴더에 바로 놓인 공용 테스트(src/ui/overlay/overlay-escape.test.tsx). src/utils/** 는
폴더가 아니라 모듈 단위라 대상이 아니다.
이름을 바꿀 때는 git mv 를 쓴다. 대소문자만 다른 경우(Card.test.tsx → card.test.tsx)는
이 저장소가 core.ignorecase=true 이고 macOS 파일시스템이 대소문자를 구분하지 않아, 임시 이름을
거친 2단계 git mv 가 필요하다 - 한 번에 옮기면 git 이 같은 파일로 본다.
pnpm check:deprecated 가 막는다. 스토리는 소비자가 복사해 가는 자리라, 폐기된 prop 을
쓰면 그대로 퍼진다. 타입은 아직 살아 있어 tsc 가 잡지 못하고, 대체 prop 과 렌더 결과가 같아
a11y 러너도 잡지 못한다.
검사 방식은 문자열·주석을 지우고 argTypes 블록을 떼어낸 뒤 남은 곳에서 prop=(JSX 속성)과
prop:(args 값)을 찾는다. 그래서 argTypes 에 deprecated 임을 적어 두는 것은 위반이 아니다 -
props 표에 이유를 남기는 쪽이 오히려 옳다.
일부러 시연해야 하면(구 prop 이 아직 동작함을 보이는 마이그레이션 스토리 등) 그 줄이나 윗줄에 이유를 적는다:
// deprecated-ok: 구 prop 이 아직 동작함을 보이는 스토리
<Toggle onChange={setOn} ariaLabel="알림" />문서 작업 중 손으로 넷을 찾았는데(Dropdown.fullWidth·Toggle.onChange·Textarea.onChangeAction),
이 검사를 붙이자 못 찾은 넷이 더 나왔다 - Accordion·OtpInput 의 onChange.
폐기 표시는 산문이 아니라 선언에 붙인다. 주석에 "구 onChange 도 허용(@deprecated)" 처럼
적어 두면 IDE 가 취소선을 그리지 못하고 이 검사도 어느 멤버가 폐기됐는지 알 수 없다.
union 타입이면 멤버마다 인라인 JSDoc 을 둔다:
type PaginationCallbacks =
| {
onPageChange: (page: number) => void;
/** @deprecated `onPageChange` 를 사용하세요. */
onChange?: (page: number) => void;
}
| { … };검사는 @deprecated 를 prop 선언에 연결하지 못하면 실패한다 - 조용히 넘기면 그 컴포넌트가
검사에서 통째로 빠진다. 초판이 그렇게 만들어져서 DatePicker·Pagination 의 스토리 5곳
실사용을 통과시켰다. 타입 별칭·함수의 폐기(export type ButtonAsButton = …)는 prop 이 아니므로
대상이 아니다 - tsc 가 직접 알려준다.
모든 컴포넌트는 테스트가 필요합니다:
// {component-name}.test.tsx
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { ComponentName } from "./index";
describe("ComponentName", () => {
it("renders correctly", () => {
render(<ComponentName>Content</ComponentName>);
expect(screen.getByText("Content")).toBeInTheDocument();
});
});git add .
git commit -m "feat: add ComponentName component"git push origin feat/new-component
# PR 생성 (develop 브랜치로)
gh pr create --base develop --title "feat/new-component" --body "..."- 모든 컴포넌트는
"use client"디렉티브 사용 - Props 인터페이스는 HTML 요소 속성을 확장
forwardRef사용 권장
"use client";
import type * as React from "react";
import { cn } from "../../../utils";
import "./style.scss";
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary";
size?: "sm" | "md" | "lg";
}
export const Button = ({
variant = "primary",
size = "md",
className,
children,
...props
}: ButtonProps) => {
return (
<button
className={cn(
"button",
`button_variant_${variant}`,
`button_size_${size}`,
className
)}
{...props}
>
{children}
</button>
);
};- Global SCSS 사용 (
style.scss) - 클래스명은 snake_case
- 하드코딩된 값 대신 토큰 사용
@use "src/styles/token" as token;
.button {
display: inline-flex;
align-items: center;
border-radius: token.$radius_md;
// `transition: all` 금지 - 바뀌는 속성만 명시한다
transition: background-color token.$transition_fast, color token.$transition_fast;
&_variant_filled {
background-color: token.$color_brand_primary;
color: token.$color_brand_on_primary;
}
&_size_md {
height: 40px;
padding: 0 token.$spacing_16;
}
}
@media (prefers-reduced-motion: reduce) {
.button {
transition: none;
}
}cn() 유틸리티를 사용합니다:
import { cn } from "../../../utils";
const buttonClassName = cn(
"button",
`button_size_${size}`,
`button_variant_${variant}`,
isActive && "button_active",
className
);label: message
- 라벨을 앞에, 커밋 내용을 뒤에 작성
- 모두 소문자, 필요시 camelCase 사용
- 메시지는 영문으로 작성
| Label | Description |
|---|---|
feat |
새로운 기능 추가 |
fix |
기능/코드 수정 |
bug |
버그/에러 수정 |
merge |
브랜치 병합 |
deploy |
프로젝트 배포 / 관련 문서 작업 |
docs |
문서 추가/수정 |
delete |
코드/파일/문서 삭제 |
note |
주석 추가/제거 |
style |
코드 스타일/구조 수정 |
config |
설정 파일 / 의존성 / 라이브러리 관련 수정 |
etc |
기타 |
tada |
프로젝트 생성 |
git commit -m "feat: add Toggle component"
git commit -m "fix: resolve Modal focus trap issue"
git commit -m "docs: update README installation guide"
git commit -m "style: refactor Button className pattern"label/domain
| 브랜치명 | 설명 |
|---|---|
feat/sidebar |
사이드바 기능 추가 |
fix/auth |
인증 도메인 코드 수정 |
style/button |
버튼 스타일 변경 |
docs/readme |
README 문서 수정 |
main- 프로덕션 브랜치 (배포용)develop- 개발 브랜치 (PR 기본 대상)
- 항상
develop브랜치로 PR 생성 main브랜치로의 직접 PR은 금지
브랜치명과 동일하게 작성:
feat/new-component
fix/modal-focus
한글로 작성합니다:
섹션 제목은 아래 네 개를 그대로 씁니다. 첫 섹션은 리터럴 ## 작업 개요 입니다 — 제목은 PR title 이 담당하므로 본문에 따로 넣지 않습니다.
## 작업 개요
이슈 #000 - 무엇을 왜 했는지 한두 문단.
## 작업한 내용
- [x] 작업1
- [x] 작업2
## 검증
- 테스트/빌드/실측 결과
## 전달할 추가 이슈
- 이슈1섹션별 필수 여부와 자주 하는 실수는 CLAUDE.md 를 참고하세요.
gh pr create --base develop --title "feat/new-component" --body "$(cat <<'EOF'
## 새 컴포넌트 추가
## 작업한 내용
- [x] ComponentName 컴포넌트 구현
- [x] 단위 테스트 작성
- [x] Storybook 스토리 추가
## 전달할 추가 이슈
- 없음
EOF
)"- 병합 전 반드시 코드 리뷰어 approve 필요
- 병합 커밋 메시지:
merge: branch-name - main 배포:
merge: release
- Architecture - 프로젝트 구조
- Testing - 테스트 작성 가이드
- Components - 컴포넌트 API