이 저장소가 대체 무엇인가.
보통 오픈소스 디자인시스템은 컴포넌트를 npm install하고 그 안을 들여다보지 않는 게 정상이다. 문제가 생기면 라이브러리를 <div>로 감싸거나(wrapping), CSS 우선순위 전쟁을 벌이거나, 포크(fork)해서 직접 고친다. Astryx는 정반대 전제에서 출발한다 — "컴포넌트는 어떤 단계에서든 조립 가능해야 하고, 정말 필요하면 그 컴포넌트의 전체 소스코드를 프로젝트 안으로 완전히 가져와 내 것으로 만들 수 있어야 한다."
이 사상이 실제로 코드가 된 것이 astryx swizzle Button 명령이다 — Radix UI 문서에 있는 "swizzle" 개념(Docusaurus에서 처음 대중화된 용어)을 컴포넌트 라이브러리 단위로 가져와, 껍데기가 아니라 진짜 소스 파일을 통째로 복사해준다.
README의 자기소개는 "An open source design system that's fully customizable and built for how we build now — by people and the agents working alongside them" — "완전히 커스터마이즈 가능하고, 사람과 그 옆에서 함께 일하는 에이전트가 지금 방식대로 만드는 디자인시스템"이다. 실제로 저장소를 열어보면 이 문장이 마케팅 카피가 아니라는 걸 바로 알 수 있다 — 루트에 CLAUDE.md와 .claude/ 디렉토리(커스텀 슬래시 명령·스킬 포함)가 그대로 커밋되어 있고, 컴포넌트 문서는 .doc.mjs라는 별도 형식으로 관리되어 CLI가 토큰 절약형("dense") 문서를 즉석에서 생성할 수 있게 되어 있다.
Meta 내부에서는 XDS라는 이름으로 불렸다(저장소 곳곳의 @astryxdesign/* 패키지명, xdsClassName() 같은 내부 함수명, CLAUDE.md 첫 줄의 "# XDS"에 그 흔적이 남아 있다). 오픈소스로 나오며 이름은 Astryx로 바뀌었지만, 코드베이스 내부 관례와 GitHub 위키 링크 상당수는 여전히 XDS/facebookexperimental/xds를 가리킨다 — 8년간 쌓인 실제 프로덕션 시스템을 그대로 옮겼다는 증거다.
트렌딩 이유 · MUI/Chakra/shadcn 등 기존 디자인시스템 대비 장점.
React 생태계에는 이미 MUI, Chakra UI, Ant Design, Mantine, 그리고 "복사해서 쓰는" shadcn/ui까지 다양한 선택지가 있다. Astryx가 새삼 주목받는 이유는 단순히 "컴포넌트가 많아서"가 아니라, "소유권(ownership)"이라는 오래된 딜레마를 정면으로 다루는 방식 때문이다. 라이브러리 도입 초기엔 편하지만, 나중에 "이 컴포넌트 딱 하나만 다르게 만들고 싶다"는 요구가 생기면 대부분의 시스템은 셋 중 하나를 강요한다 — ① 감싸서 땜질하기(wrapper hell), ② 라이브러리 CSS와 힘겨루기하기(specificity war), ③ 포크해서 영영 업스트림과 갈라서기.
| 비교 축 | MUI / Chakra / Ant Design | shadcn/ui | Astryx의 접근 |
|---|---|---|---|
| 기본 소유 방식 | node_modules 안의 블랙박스, 커스텀은 theme API로 제한 | 처음부터 소스를 프로젝트에 복사(코드젠 방식) | 기본은 npm 패키지, 필요할 때만 swizzle로 개별 컴포넌트를 소스로 전환 |
| 스타일링 엔진 | Emotion/styled-components(런타임 CSS-in-JS) | Tailwind 유틸리티 클래스 | StyleX(빌드타임 아토믹 CSS) — 컴파일되면 사용자에겐 안 보임 |
| 스타일 오버라이드 | sx prop, theme override API를 배워야 함 | Tailwind 클래스를 직접 편집(이미 소스가 내 것이므로) | className 하나로 Tailwind·CSS Modules·플레인 CSS 등 뭐든 사용 가능 |
| 테마 커스터마이즈 | JS 객체로 테마 토큰 전체를 정의해야 함 | Tailwind config + CSS 변수 혼재 | 테마 = CSS 커스텀 프로퍼티 오버라이드 집합 하나. 포크·래핑 없이 브랜드화 |
| AI 에이전트 대응 | 일반 문서 사이트, AI 전용 설계 없음 | MCP 서버 등 커뮤니티 도구가 사후에 추가됨 | CLI/문서/컴포넌트 계약이 처음부터 "사람+에이전트 동시 사용" 전제로 설계 |
런타임 CSS-in-JS 라이브러리는 클래스 우선순위·특이도(specificity) 문제로 !important를 남발하게 만들기 쉽고, 테마 API가 예상하지 못한 디자인 요구(예: 특정 컴포넌트만 살짝 다른 그림자·라운딩)를 만나면 결국 <div>로 감싸는 wrapper 컴포넌트가 쌓인다. shadcn 계열은 "복사해서 네 것"이라는 철학은 좋지만, 애초에 라이브러리 형태가 아니라서 150개 이상의 일관된 시스템으로 관리하기엔 구조가 약하다.
대부분의 커스터마이즈는 1단계(테마의 CSS 변수 몇 개 바꾸기)로 끝난다. 그걸로도 부족하면 2단계(원하는 프레임워크의 className으로 특정 지점만 오버라이드). 그마저도 부족한, 정말 컴포넌트 내부 구조 자체를 바꿔야 하는 드문 경우에만 3단계(swizzle로 소스를 통째로 가져와 그 순간부터 직접 관리)를 쓴다. 대부분의 사용자는 3단계까지 갈 필요가 없다는 게 핵심 설계 목표다.
렌트카(기존 라이브러리)는 편하지만 시트 포지션 정도만 조절되고, 엔진을 손볼 순 없다. 직접 조립한 차(shadcn류)는 처음부터 내 것이지만 부품 하나하나를 다 챙겨야 한다. Astryx는 "평소엔 정비소가 다 관리해주는 리스카를 몰다가, 정말 마음에 안 드는 부품 하나가 생기면 그 부품만 소유권을 넘겨받아 내 차고에서 직접 뜯어고칠 수 있는" 하이브리드 모델에 가깝다 — swizzle 한 번으로 "빌려 쓰는 것"에서 "소유한 것"으로 그 컴포넌트만 전환된다.
또 하나의 차별점은 "사람과 에이전트가 같은 방식으로 빌드한다"는 철학이 슬로건에 그치지 않는다는 점이다. packages/cli는 --dense 플래그로 토큰 효율적인 문서를 뽑아주고, 컴포넌트 문서(.doc.mjs)는 애초에 기계가 파싱하기 쉬운 구조화된 형식으로 작성되며, 저장소 자체에 Claude Code용 커스텀 명령(.claude/commands/)까지 들어있다. "AI가 코드를 잘 짜게 도와주는 문서"가 아니라 "AI 에이전트를 위한 1급 인터페이스"로 설계했다는 증거다.
React + StyleX 컴포넌트 라이브러리, pnpm 모노레포 툴링, 자체 CLI.
| 기술 / 버전 | 역할 |
|---|---|
| React 19.2+ (peer dependency) | 모든 컴포넌트의 기반. use client 지시어가 컴포넌트 최상단에 있어 Next.js RSC(서버 컴포넌트) 환경도 고려 |
StyleX 0.18~0.19 (@stylexjs/stylex, @stylexjs/babel-plugin) | Meta가 만든 빌드타임 아토믹 CSS-in-JS. stylex.create()로 스타일 정의 → 빌드 시 클래스명으로 컴파일, 런타임 오버헤드 없음 |
| TypeScript 6.0 | 전 컴포넌트에 타입 정의(.d.ts) 자동 생성. Props는 BaseProps 공통 타입을 확장 |
CSS Custom Properties + light-dark() | 모든 색상 토큰이 light-dark(라이트값, 다크값) 함수로 정의되어, 별도 다크모드 CSS 없이 한 줄로 라이트/다크 동시 지원 |
CSS @layer (레이어) | reset → astryx-base → astryx-theme 순서로 캐스케이드 레이어를 나눠, 사용자 CSS/Tailwind와 우선순위 충돌 없이 공존 |
| OKLCH 색공간 | 테마 색상 팔레트(예: neutral 테마)가 OKLCH에서 색상환 위치를 계산해 파생 — 색이 밝기 단계마다 "같은 색"으로 인식되도록 설계 |
| 기술 / 버전 | 역할 |
|---|---|
| pnpm 10.34 + Corepack | packageManager 필드로 정확한 pnpm 버전을 못박아, corepack enable 한 번이면 전원이 같은 버전 사용 |
| 워크스페이스 | apps/*, packages/*, packages/themes/*, internal/* — 4개 그룹으로 나뉜 pnpm 워크스페이스 |
Changesets (@changesets/cli) | 패키지별 변경 이력·semver 범프를 커밋 단위로 기록. Astryx는 배포 패키지 9개를 fixed 그룹으로 묶어 항상 같은 버전으로 동시 배포 |
| Husky | Git pre-commit 훅에서 lint-staged + pnpm check:repo(동기화·패키지 경계·changeset 존재 검사) 실행 |
| Vitest 2.1 + jsdom | 루트 vitest.config.ts 하나가 전 패키지 테스트를 관장. StyleX babel 플러그인을 dev 모드로 물려 테스트 환경에서도 스타일 적용 |
Babel (@babel/cli, @stylexjs/babel-plugin) | packages/core의 ESM 빌드 파이프라인. StyleX 컴파일이 이 단계에서 일어남 |
| esbuild + tsup | 테마 패키지·CLI 번들링에 사용하는 경량 번들러 |
| ESLint 10 (Flat Config) + eslint-plugin-react-compiler | React Compiler(19의 자동 메모이제이션) 규칙까지 포함한 최신 flat config |
자체 ESLint 플러그인 (internal/eslint-plugin-astryx) | 디자인시스템 전용 규칙 14종 — 아래 4장에서 상세 |
| Playwright + axe-core | 접근성(a11y) 자동 검사를 CI에 통합(@axe-core/playwright) |
| Storybook | apps/storybook에서 150개 이상 컴포넌트를 시각적으로 문서화·프리뷰 |
| 기술 | 역할 |
|---|---|
| Commander 12 | astryx component / template / swizzle / theme / upgrade / docs / init 등 서브커맨드 파싱 |
| @clack/prompts | 대화형 CLI 프롬프트(테마 선택, 덮어쓰기 확인 등)의 세련된 터미널 UI |
| jiti | 사용자 설정 파일을 별도 빌드 없이 즉석 로드 |
| jscodeshift 17 | 버전 업그레이드용 코드모드(codemod) 엔진 — AST 기반으로 사용자 코드를 안전하게 변형 |
| Zod 4 | CLI 옵션·API 응답의 런타임 스키마 검증 |
| gpt-tokenizer | peer dependency — --dense 문서 출력 시 실제 토큰 수를 계산해 예산을 관리 |
stylex.create({...}) 호출을 정적 분석해 아토믹 CSS 클래스(속성 하나당 클래스 하나, 예: .x1a2b3c{color:red})로 미리 컴파일한다. 런타임 오버헤드가 없고, 같은 스타일 규칙을 여러 컴포넌트가 공유하면 CSS 용량이 거의 늘지 않는다는 게 핵심 장점이다. Astryx의 모든 컴포넌트가 이 방식으로 스타일링되지만, 최종 사용자에게는 컴파일된 CSS 파일만 보여 "무엇으로 만들었는지"가 전혀 드러나지 않는다.Foundations → Components → Patterns 3계층 + StyleX 컴파일 흐름 + 테마 오버라이드 + swizzle eject.
README가 명시하는 아키텍처는 3단 계층이다. Foundations(타이포그래피·색상·레이아웃·접근성의 기초 토큰), Components(타입 완비된 재사용 UI 블록 150개 이상), Patterns(테이블 페이지·상세 페이지 레이아웃·폼 위저드·내비게이션·데이터 입력 같은 검증된 화면 조합). 아래 단계가 위 단계의 재료가 되는 전형적인 디자인시스템 피라미드지만, Astryx는 여기에 CLI가 각 계층을 넘나드는 진입점이라는 4번째 축을 더한다.
packages/core/src/Button/Button.tsx를 열어보면 컴포넌트 파일 안에 바로 stylex.create({...})가 있다 — 별도의 .css나 .module.css 파일이 아니라 TS 파일 안에서 타입 체크되는 스타일 객체다. 빌드 시 Babel의 @stylexjs/babel-plugin이 이 객체를 정적으로 분석해 아토믹 클래스로 치환한다.
// packages/core/src/Button/Button.tsx (실제 구조 축약)
import * as stylex from '@stylexjs/stylex';
import {colorVars, spacingVars, radiusVars} from '../theme/tokens.stylex';
const styles = stylex.create({
base: {
display: 'inline-flex',
gap: spacingVars['--spacing-2'],
borderRadius: `var(--_button-radius, ${radiusVars['--radius-element']})`,
transitionDuration: {
default: durationVars['--duration-fast'],
// 모션 최소화 설정을 존중 — 접근성 기본값
'@media (prefers-reduced-motion: reduce)': '0s',
},
},
pressable: {
transform: { default: 'scale(1)', ':active': 'scale(0.98)' }, // 의사클래스는 중첩 객체로
},
});
주목할 점은 같은 라이브러리가 두 가지 다른 클래스 접두사(prefix)를 쓴다는 것이다. packages/build/src/babel.js는 파일 경로가 packages/core/·packages/themes/·node_modules/@astryxdesign/ 밑이면 astryx 접두사(.astryx78zum5)를, 그 외 사용자 프로덕트 코드는 x 접두사(.x78zum5)를 붙이도록 같은 StyleX 플러그인을 두 벌 인스턴스화해 파일 단위로 갈아 끼운다. 라이브러리 클래스와 사용자 클래스가 이름 공간에서 절대 충돌하지 않도록 하는 실전적인 트릭이다.
@astryxdesign/build의 astryxBabelPlugin()은 파일 경로로 "이건 라이브러리 코드인지, 이걸 쓰는 프로덕트 코드인지"를 판별해 서로 다른 StyleX 옵션(주로 classNamePrefix)을 적용한다. Babel visitor를 동적으로 감싸 state.opts를 그때그때 바꿔치기하는 방식으로 구현되어 있다 — 하나의 babel.config만으로 라이브러리·앱 두 세계의 클래스 이름을 분리하는 실용적인 해법.
Astryx의 모든 디자인 토큰(--color-accent, --spacing-2, --radius-element 등 수십 개)은 packages/core/src/theme/tokens.stylex.ts의 stylex.defineVars()로 정의된 CSS 커스텀 프로퍼티다. 색상 값은 전부 light-dark(라이트, 다크) 함수 하나로 표현되어, 별도의 다크모드 스타일시트 없이 브라우저가 알아서 골라 쓴다.
// packages/core/src/theme/tokens.stylex.ts (실제 발췌)
export const colorDefaults = {
'--color-accent': 'light-dark(#0064E0, #2694FE)',
'--color-background-surface': 'light-dark(#FFFFFF, #1F1F22)',
'--color-text-primary': 'light-dark(#0A1317, #DFE2E5)',
// ... 색상/간격/반경/그림자/타이포그래피 토큰 계속
};
새 테마를 만드는 건 이 토큰 맵을 부분적으로만덮어쓰는 defineTheme() 호출 하나다. packages/themes/neutral/src/neutralTheme.ts는 OKLCH 색공간에서 색상환 위치를 계산해 뽑아낸 팔레트로 전체 색상 토큰만 오버라이드하고, 나머지(간격·반경 등)는 기본값을 그대로 물려받는다 — "테마 = 토큰 오버라이드 집합"이라는 설계가 코드로 정확히 드러난다.
CSS 배포 순서도 신중하게 설계되어 있다 — @layer reset, astryx-base, astryx-theme 3단 캐스케이드 레이어로 reset.css → astryx.css(컴포넌트 스타일) → theme.css(토큰 오버라이드) 순서를 강제한다. Tailwind와 함께 쓸 때는 tailwind-theme.css가 @theme inline으로 Astryx 토큰을 bg-surface·text-primary 같은 Tailwind 유틸리티 클래스에 자동으로 연결해준다.
packages/cli/src/commands/swizzle.mjs(417줄)를 직접 읽어보면 단순히 "파일 복사"가 아니라는 걸 알 수 있다. ① 경로 안전성 검사(assertWithin으로 --output이 .. 경로 탈출을 시도하는지 사전 차단), ② 기존 파일과 충돌 시 대화형 확인(또는 --overwrite), ③ 상대 경로 import를 패키지 경로로 재작성하는 3단계를 거친다.
swizzle 직후엔 선택적으로 gap report(간극 보고)를 남길 수 있다 — "왜 swizzle이 필요했는지"를 GitHub 이슈로 자동 접수(--gap "reason" --commit)해, 메인테이너가 "이 니즈를 컴포넌트 자체에 흡수해야 하는가"를 판단할 신호로 삼는다. 개별 사용자의 이탈(swizzle)이 프로젝트 전체의 개선 신호로 순환하는 피드백 루프다.
swizzle은 "실패를 인정하는 탈출구"가 아니라 정식으로 지원되는 3단계 커스터마이즈 경로로 설계되어 있다. CLI가 gap report까지 챙긴다는 건, 메인테이너 팀이 "사람들이 swizzle하는 이유"를 데이터로 수집해 다음 컴포넌트 설계에 반영하겠다는 의도다.
Astryx 코드베이스는 "규칙을 문서로만 적어두면 지켜지지 않는다"는 전제 위에서, 커스텀 ESLint 규칙과 SYNC 주석 계약으로 관례를 자동 강제한다. internal/eslint-plugin-astryx에는 no-classname-clobber(className이 내부 스타일을 완전히 덮어써 깨뜨리는 패턴 차단), require-ref-prop(포워딩 ref 누락 방지), no-hardcoded-anchor(라우터 없는 <a> 직접 사용 방지), boolean-prop-naming(불리언 prop 네이밍 통일) 등 14개 규칙이 있다.
각 소스 파일 헤더의 SYNC: 주석은 "이 파일을 고치면 어떤 다른 파일들도 같이 갱신해야 하는지"를 명시하고, scripts/check-sync.js가 CI에서 그 참조가 실제로 유효한지(존재하는 파일을 가리키는지) 검사한다. README 맨 위의 <!-- SYNC CONTRACT --> 주석도 같은 철학이다 — "아키텍처가 바뀌면 문서도 반드시 같이 바뀌어야 한다"는 규칙을 사람이 아니라 CI가 지키게 만든 것.
SYNC:로 연관 파일 목록을 명시하고, check:sync 스크립트가 그 링크의 존재 여부를 CI에서 기계적으로 검사한다. "문서가 낡았는지"를 사람 리뷰가 아니라 정적 검사로 잡아내는 구조다.apps/ · packages/{core,cli,build,themes} · internal/ · .changeset/ · .claude/.
| 파일/폴더 | 역할 |
|---|---|
packages/core/src/{Component}/{Component}.tsx | 컴포넌트 구현 본체. StyleX 스타일 정의가 파일 안에 함께 있다. |
packages/core/src/{Component}/{Component}.doc.mjs | props·기능·예시를 담은 구조화 문서. CLI의 component --dense가 이걸 읽어 압축 출력한다. |
packages/cli/src/commands/swizzle.mjs | 이 레포의 대표 차별화 기능 — 컴포넌트 소스를 안전하게 eject하는 로직. |
packages/build/src/babel.js | 라이브러리 코드와 사용자 코드에 서로 다른 StyleX 클래스 접두사를 부여하는 트릭. |
internal/eslint-plugin-astryx/ | 디자인시스템 전용 규칙 14개 — 관례를 CI에서 강제. |
internal/vibe-tests/ | "AI가 이 문서만 보고 올바른 컴포넌트 코드를 짤 수 있는가"를 정량 측정하는 자체 벤치마크. /vibe-test 커스텀 명령으로 실행. |
.changeset/config.json | 9개 배포 패키지를 fixed 그룹으로 묶어 항상 같은 버전으로 동시 배포하도록 설정. |
CLAUDE.md / .claude/ | Claude Code가 이 레포에서 작업할 때 참고하는 컨텍스트·커스텀 명령·스킬. 레포 자체가 "에이전트 친화" 철학의 실물 증거. |
Meta 내부에서 8년간 쓰이던 이름은 XDS였고, 그 흔적이 @astryxdesign/* 패키지 내부의 xdsClassName() 같은 함수명, CLAUDE.md의 "# XDS" 제목, GitHub 위키가 여전히 가리키는 facebookexperimental/xds 링크에 남아 있다. 실제 npm 패키지명·GitHub 저장소명은 facebook/astryx/@astryxdesign이 맞다 — 코드를 읽다 XDS라는 이름이 나와도 별개 프로젝트가 아니라 같은 레포의 옛 이름이니 헷갈리지 말 것.
이 저장소 하나로 배울 수 있는 것 + 실습 아이디어.
Tailwind(유틸리티 클래스를 HTML에 직접 나열)나 Emotion(런타임에 스타일을 계산)과 다른 "제3의 길"을 몸으로 익힐 수 있다. stylex.create()로 정의한 스타일 객체가 타입 체크되고, 조건부 스타일은 stylex.props(condition && styles.x)처럼 다루며, 의사클래스(:hover, :active)는 CSS 파일이 아니라 객체 안에 중첩해서 쓴다는 점이 특히 낯설면서도 배울 가치가 있다.
packages/core/src/Button/Button.tsx의 styles 객체를 그대로 복사해 미니 프로젝트에 StyleX를 설치하고 빌드해보자. 개발자 도구로 실제 생성된 클래스명(.astryx78zum5 형태)을 확인하고, 똑같은 display:'flex'를 쓰는 컴포넌트 두 개를 만들어 CSS 용량이 늘지 않는지(클래스 재사용) 관찰해본다.
tokens.stylex.ts의 colorDefaults가 색상 하나하나를 light-dark(...) 함수로 정의하는 방식, defineTheme()이 "일부 토큰만 오버라이드"를 허용하는 API 설계, neutral 테마가 OKLCH 색공간에서 색상환 각도를 계산해 팔레트를 뽑아내는 방식까지 — "토큰 기반 테마"가 실제 프로덕션에서 어떻게 짜이는지 정석 사례로 볼 수 있다.
defineTheme({name, tokens}) 형태의 미니 함수를 직접 구현해보자. 토큰 맵 하나를 받아 <style> 태그에 CSS 커스텀 프로퍼티로 주입하는 20줄짜리 함수만 만들어봐도 "테마 = 오버라이드 집합"이라는 설계의 핵심을 체감할 수 있다.
Astryx는 @astryxdesign/core·cli·build·테마 7종 등 9개 패키지를 .changeset/config.json의 fixed 그룹으로 묶어 항상 같은 버전 번호로 동시에 배포한다. 기여자는 PR마다 .changeset/{임의이름}.md 파일 하나(변경 요약 + 영향받는 패키지)를 추가하고, 릴리스 시점에 changeset version이 이걸 모아 CHANGELOG와 버전을 자동 생성한다.
2개짜리 미니 pnpm 모노레포를 만들어 @changesets/cli를 설치하고, pnpm changeset으로 변경 파일을 만든 뒤 changeset version이 package.json의 버전과 CHANGELOG.md를 어떻게 바꾸는지 직접 관찰해보자. fixed 옵션을 켜고 끄며 버전 번호가 어떻게 달라지는지 비교.
packages/cli/src/codemods/transforms/에는 v0.0.2부터 v0.1.2까지 버전별 코드 자동 변환 스크립트가 그대로 쌓여 있다. astryx upgrade --apply는 사용자의 @astryxdesign/core 버전을 감지해 그 사이 버전들의 코드모드를 순서대로 적용한다. jscodeshift는 정규식 치환이 아니라 AST(추상 구문 트리)를 파싱해 변형하므로, 문자열 우연 일치로 엉뚱한 코드를 건드릴 위험이 훨씬 적다.
jscodeshift를 설치하고, "함수 이름 oldName을 전부 newName으로 바꾸되 문자열 리터럴 안의 텍스트는 건드리지 않는" 간단한 transform을 짜보자. packages/cli/src/codemods/transforms/의 실제 transform 하나를 참고하면 AST 노드를 어떻게 찾고 바꾸는지 감을 잡을 수 있다.
Astryx는 @axe-core/playwright로 접근성 위반을 CI에서 자동 검출하고, StyleX 스타일 규칙에도 prefers-reduced-motion·@media (hover: hover) 가드가 관례로 박혀 있다(터치 기기에서 :hover 스타일이 "끈적하게" 남는 문제 방지). ESLint 규칙 중 require-ref-prop도 포워딩 ref가 빠지면 스크린 리더 연동이 깨질 수 있는 패턴을 잡아내는 접근성 관련 규칙이다.
본인이 만든 버튼 컴포넌트에 @media (prefers-reduced-motion: reduce)로 트랜지션을 끄는 코드와, @media (hover: hover)로 호버 스타일을 감싸는 코드를 추가해보고, 브라우저 개발자 도구의 "모션 줄이기" 시뮬레이션으로 실제 적용되는지 확인해보자.
internal/vibe-tests는 "AGENTS.md/CLAUDE.md 같은 문서가 LLM의 코드 생성 정확도에 실제로 얼마나 도움이 되는가"를 정량적으로 측정하는 자체 벤치마크다. 여러 프롬프트로 AI에게 컴포넌트 코드를 짜게 시키고, 결과가 "이스케이프 해치"(디자인시스템을 안 쓰고 날것의 HTML로 도망치는 패턴)를 썼는지 자동 평가한다. 심지어 "10턴 대화가 길어지면 품질이 얼마나 저하되는가"(degradation curve)까지 측정한다.
internal/vibe-tests/test-sets/default.json의 프롬프트 몇 개를 읽어보고, "이 디자인시스템 문서만 주고 AI에게 짜라고 하면 진짜 이 컴포넌트를 쓸까, 아니면 그냥 <button>을 쓸까"를 직접 실험해보자. 이 아이디어는 자신이 만드는 라이브러리의 README/문서 품질을 "AI가 정확히 따라 쓸 수 있는가"라는 기준으로 검증하는 새로운 습관으로 이어질 수 있다.
사용하는 쪽은 가볍다 — 기여·개발은 Node 22 + pnpm 10 + Corepack.
| 항목 | 요구사항 |
|---|---|
| 런타임 (사용/기여 공통) | Node.js 22 이상 |
| 패키지 매니저 (레포 개발 시) | pnpm 10 — corepack enable 한 번이면 package.json의 packageManager 필드가 지정한 정확한 버전(10.34.1)이 자동 설치됨. npm install -g pnpm@10이나 Homebrew/독립 설치 스크립트로도 대체 가능 |
| peer dependency (사용 시) | React ≥19.0.0, react-dom ≥19.0.0 — 그 이하 버전은 지원 대상 아님 |
| 설치 (일반 프로젝트에 도입) | npm install @astryxdesign/core @astryxdesign/theme-neutral (pnpm/yarn도 동일 문법으로 대체 가능) + npm install -D @astryxdesign/cli |
| 빌드 플러그인 필요 여부 | 불필요 — 사전 빌드된 CSS/JS를 그대로 import. StyleX 소스 빌드를 직접 하려는 경우에만 @astryxdesign/build의 babel/PostCSS/Vite 플러그인 필요 |
| OS | 크로스플랫폼(macOS·Linux·Windows) — Node/pnpm이 도는 환경이면 제약 없음 |
| 하드웨어 | 일반 개발 노트북으로 충분. 로컬 추론이나 GPU 요구사항 없음(컴포넌트 라이브러리 + 정적 CSS) |
| 테스트/CI 도구(레포 기여 시) | Vitest, Playwright(+ 브라우저 바이너리), ESLint 10 |
"쓰는" 사람은 npm install 두 줄이면 끝나는 가벼운 라이브러리다. 진짜 요구사항이 붙는 건 레포 자체를 기여·수정할 때다 — pnpm 정확한 버전, Node 22, 그리고 Playwright 같은 무거운 브라우저 테스트 도구까지 갖춰야 pnpm check:repo(동기화·경계·changeset 검사)를 통과할 수 있다.
난이도별 5단계 — 설치부터 codemod까지.
create-next-app으로 새 프로젝트를 만들고 @astryxdesign/core @astryxdesign/theme-neutral을 설치한 뒤, globals.css에 reset.css → astryx.css → theme-neutral/theme.css 3줄을 순서대로 import하고 <Theme theme={neutralTheme}>로 앱을 감싸보자. XDSButton(또는 Button) 하나를 렌더링해 화면에 뜨는지 확인. 목표: "빌드 플러그인 없이" 컴포넌트 라이브러리를 쓴다는 게 실제로 어떤 느낌인지 체험.
실습 1 결과물에 CSS 파일 하나를 추가해 --color-accent, --radius-element 등 토큰 몇 개를 원하는 값으로 오버라이드해보자(astryx theme --dense로 전체 토큰 목록을 CLI에서 바로 확인 가능). 컴포넌트 코드는 한 줄도 안 건드렸는데 버튼·체크박스 색·모서리가 전부 바뀌는 걸 확인. 목표: "테마 = 토큰 오버라이드"라는 설계를 손으로 체감.
npx astryx swizzle Button --output ./components/astryx를 실행해 Button 컴포넌트 소스 전체를 프로젝트로 가져온다. 결과 파일에서 import 경로가 @astryxdesign/core/theme 형태로 자동 재작성된 걸 확인하고, styles 객체에 새 variant(예: ghost 스타일)를 직접 추가해보자. 목표: "패키지에서 빌려 쓰던 것"이 "내 프로젝트 안의 진짜 소스"로 전환되는 순간을 체험.
npx astryx component --list로 전체 컴포넌트 목록을 본 뒤, 관심 있는 컴포넌트 하나에 npx astryx component Dialog --dense와 --detail brief 두 방식을 각각 실행해 출력 분량·정보량 차이를 비교해보자. npx astryx template --list로 템플릿도 훑어본다. 목표: "사람이 읽는 문서"와 "에이전트가 토큰 예산 안에서 읽는 문서"가 어떻게 다르게 설계되는지 체감.
레포를 클론해 internal/eslint-plugin-astryx/no-hardcoded-anchor.js(또는 다른 규칙 하나)를 읽고, 비슷한 패턴의 미니 ESLint 규칙(예: "console.log 직접 호출 금지")을 처음부터 작성해 테스트까지 통과시켜보자. 여유가 되면 packages/cli/src/codemods/transforms/의 최신 버전 transform 하나를 참고해, jscodeshift로 "특정 import 경로를 새 경로로 바꾸는" codemod를 직접 짜본다. 목표: "규칙을 문서가 아니라 코드로 강제한다"는 이 레포의 핵심 철학을 실제로 구현해보기.
이 저장소를 출발점으로, 6주 코스.
| 주차 | 주제 | 학습 자료 |
|---|---|---|
| 1주차 | React 19 기초 + 컴포넌트 라이브러리 설계 원리 | React 공식 문서(Server Components, use client) · Astryx BaseProps.ts 정독 · 실습 1 |
| 2주차 | StyleX 심화 — 아토믹 CSS-in-JS | StyleX 공식 문서(stylexjs.com) · Button.tsx·tokens.stylex.ts 정독 · Emotion/Tailwind와 비교표 직접 작성 |
| 3주차 | 디자인 토큰 · 테마 아키텍처 | defineTheme.ts·expandColorScale.ts 분석 · OKLCH 색공간 기초 학습 · 실습 2 |
| 4주차 | 모노레포 릴리스 엔지니어링 | Changesets 공식 문서 · .changeset/config.json의 fixed 옵션 실험 · pnpm 워크스페이스 문법 |
| 5주차 | CLI/코드모드 설계 | jscodeshift 공식 문서 · Commander.js 문서 · swizzle.mjs 전체 정독 · 실습 3, 5 |
| 6주차 | 접근성 + "에이전트 친화적" API 설계 | WAI-ARIA 저작 practices · internal/vibe-tests/README.md · 자신의 프로젝트 문서를 AI가 얼마나 정확히 따라 쓰는지 직접 측정 |
본문에 나온 용어 빠른 참조.
| 용어 | 의미 |
|---|---|
| StyleX | Meta가 만든 빌드타임 아토믹 CSS-in-JS 라이브러리. stylex.create()를 정적 분석해 클래스로 컴파일, 런타임 오버헤드 없음. |
| 디자인 토큰 (Design Token) | 색상·간격·반경·타이포그래피 값을 이름 붙인 변수로 관리하는 단위. Astryx는 CSS 커스텀 프로퍼티로 구현. |
| swizzle | 컴포넌트의 전체 소스 코드를 라이브러리에서 사용자 프로젝트로 복사해, 그 순간부터 사용자가 직접 소유·수정하게 만드는 동작. |
| CSS Custom Properties (CSS 변수) | --color-accent처럼 --로 시작하는 CSS 변수. var()로 참조하며, 런타임에 값만 바꿔도 스타일이 갱신된다. |
| light-dark() | 라이트/다크 모드 값을 한 CSS 함수 안에 같이 정의해, 별도 미디어쿼리 없이 브라우저가 알아서 고르게 하는 CSS 함수. |
CSS @layer (캐스케이드 레이어) | 스타일 우선순위를 소스 순서가 아니라 명시적 레이어 순서로 관리하는 CSS 기능. Astryx는 reset→base→theme 순서를 강제. |
| Changesets | 모노레포에서 패키지별 변경 이력·semver 범프를 PR 단위 마크다운 파일로 기록하고 자동 집계하는 릴리스 도구. |
| Codemod (코드모드) | AST를 분석해 소스 코드를 자동으로 안전하게 변형하는 스크립트. jscodeshift가 대표 도구. |
| 아토믹 CSS (Atomic CSS) | CSS 속성 하나당 클래스 하나를 생성하는 방식. 여러 컴포넌트가 같은 속성을 쓰면 클래스가 재사용되어 CSS 총량이 거의 늘지 않는다. |
| OKLCH | 인간의 색 지각에 더 가깝게 설계된 색공간. 밝기를 조절해도 "같은 색"으로 인식되는 팔레트를 만들기 쉽다. |
| Gap Report (간극 보고) | swizzle이 필요했던 이유를 GitHub 이슈로 자동 접수해, 메인테이너가 컴포넌트 개선 우선순위를 판단하는 신호로 삼는 피드백 장치. |
| Vibe Test | AI 에이전트가 프로젝트 문서만 보고 얼마나 정확하게 올바른 컴포넌트 코드를 생성하는지 정량 측정하는 자체 벤치마크. |