Lyraλ OpenAI Apps SDK λμμΈ κ°μ΄λλΌμΈμ μ€μνλ©°, μ κ·Όμ±κ³Ό μ¬μ©μ±μ μ΅μ°μ μΌλ‘ νλ νλμ μΈ React κΈ°λ° λμμΈ μμ€ν μ λλ€. Base UI Componentsλ₯Ό κΈ°λ°μΌλ‘ ꡬμΆλμμΌλ©°, 체κ³μ μΈ λμμΈ ν ν°κ³Ό μ¬μ¬μ© κ°λ₯ν μ»΄ν¬λνΈλ₯Ό μ 곡ν©λλ€.
OpenAI Apps SDK μ€μ: Lyraλ OpenAIμ λμμΈ κ°μ΄λλΌμΈμ λ°λΌ μΌκ΄λκ³ μ κ·Ό κ°λ₯ν μ¬μ©μ κ²½νμ μ 곡ν©λλ€. λ¨μ λ°°κ²½, gradient λ―Έμ¬μ©, λͺ νν κ³μΈ΅ ꡬ쑰, μ΅μνμ μ‘μ μ ν λ± OpenAIμ λμμΈ μ² νμ λ°μνμμ΅λλ€.
- π€ OpenAI Apps SDK μ€μ: OpenAI λμμΈ κ°μ΄λλΌμΈμ λ°λ₯Έ μΌκ΄λ UX
- π¨ 체κ³μ μΈ λμμΈ ν ν°: Style Dictionary κΈ°λ° ν ν° μμ€ν μΌλ‘ μΌκ΄λ λμμΈ μΈμ΄ μ 곡
- βΏοΈ μ κ·Όμ± μ°μ : Base UI Components κΈ°λ°μ WCAG 2.1 AA μ€μ μ»΄ν¬λνΈ
- π± λ°μν λμμΈ: Polaris λ°©μμ λ―Έλμ΄ μΏΌλ¦¬ μμ€ν μΌλ‘ λͺ¨λ λλ°μ΄μ€ μ§μ
- π CSS Modules: μ€νμΌ μΆ©λ μλ μμ ν μ€μ½ν μ€νμΌλ§
- π§ͺ μμ ν ν μ€νΈ: Vitest κΈ°λ° μ λ ν μ€νΈ λ° Storybook μΈν°λμ ν μ€νΈ
- π νλΆν λ¬Έμν: StorybookμΌλ‘ μμ±λ μΈν°λν°λΈ μ»΄ν¬λνΈ λ¬Έμ
- π§ TypeScript: μλ²½ν νμ μ μ μ 곡
- π λͺ¨λ Έλ ν¬ κ΅¬μ‘°: Turborepo κΈ°λ° κ³ μ±λ₯ λΉλ μμ€ν
lyra/
βββ apps/
β βββ web/ # μΉ μ ν리μΌμ΄μ
(Vite)
βββ packages/
β βββ design-tokens/ # λμμΈ ν ν° μμ€ν
β βββ ui/ # UI μ»΄ν¬λνΈ λΌμ΄λΈλ¬λ¦¬ (Storybook ν¬ν¨)
β βββ eslint-config/ # ESLint 곡μ μ€μ
β βββ typescript-config/ # TypeScript 곡μ μ€μ
βββ docs/ # νλ‘μ νΈ λ¬Έμ
- Node.js 18.x μ΄μ
- pnpm 10.x μ΄μ
# μ μ₯μ ν΄λ‘
git clone https://github.com/YuJM/lyra.git
cd lyra
# μμ‘΄μ± μ€μΉ
pnpm install# λͺ¨λ ν¨ν€μ§λ₯Ό watch λͺ¨λλ‘ μ€ν
pnpm dev
# Storybook λ¬Έμ μλ² μ€ν (localhost:6006)
pnpm dev --filter=@lyra/ui# λͺ¨λ ν¨ν€μ§ λΉλ
pnpm build
# νΉμ ν¨ν€μ§λ§ λΉλ
pnpm build --filter=@lyra/uiλμμΈ μμ€ν μ ν΅μ¬ ν ν°μ κ΄λ¦¬νλ ν¨ν€μ§μ λλ€.
μ 곡νλ ν ν°:
- μμ (Color primitives)
- νμ΄ν¬κ·ΈλνΌ (Font family, size, weight, line height)
- κ°κ²© (Spacing scale)
- λΈλ μ΄ν¬ν¬μΈνΈ (Responsive breakpoints)
- κ·Έλ¦Όμ (Shadow tokens)
- ν λ리 (Border radius, width)
- μ λλ©μ΄μ (Duration, easing)
- Z-index (Layering system)
κΈ°μ μ€ν:
- Style Dictionary (ν ν° λ³ν)
- DTCG ν¬λ§· μ§μ
- CSS, JavaScript, JSON νμ μΆλ ₯
- Polaris λ°©μ λ―Έλμ΄ μΏΌλ¦¬ μλ μμ±
μ¬μ©λ²:
import '@lyra/design-tokens/css';
// CSS λ³μλ‘ μ¬μ©
.element {
color: var(--color-blue-600);
padding: var(--spacing-4);
font-size: var(--font-size-base);
}Base UI Components κΈ°λ°μ μ κ·Όμ± μ°μ React μ»΄ν¬λνΈ λΌμ΄λΈλ¬λ¦¬μ λλ€.
μ 곡 μ»΄ν¬λνΈ:
- Button: λ€μν variantλ₯Ό μ§μνλ λ²νΌ
- Checkbox: λ¨μΌ/κ·Έλ£Ή 체ν¬λ°μ€
- Radio: λΌλμ€ λ²νΌ λ° κ·Έλ£Ή
- Switch: ν κΈ μ€μμΉ
- Field: νΌ νλ κ΅¬μ± μμ (Label, Control, Description, Error)
- Select: λλ‘λ€μ΄ μ ν μ»΄ν¬λνΈ
- Dialog: λͺ¨λ¬ λ€μ΄μΌλ‘κ·Έ
- Tooltip: ν΄ν
κΈ°μ μ€ν:
- React 19
- Base UI Components
- CSS Modules + PostCSS
- Rollup (λΉλ μμ€ν )
- Vitest (ν μ€ν )
- Storybook (λ¬Έμν)
μ¬μ©λ²:
import { Button, Field, Select } from '@lyra/ui';
import '@lyra/ui/styles';
function App() {
return (
<>
<Button variant="primary">μ μΆ</Button>
<Field.Root>
<Field.Label>μ΄λ©μΌ</Field.Label>
<Field.Control type="email" />
<Field.Description>λ‘κ·ΈμΈμ μ¬μ©ν μ΄λ©μΌμ
λλ€</Field.Description>
</Field.Root>
<Select.Root>
<Select.Trigger>
<Select.Value placeholder="μ ννμΈμ" />
</Select.Trigger>
<Select.Portal>
<Select.Popup>
<Select.Item value="1">μ΅μ
1</Select.Item>
<Select.Item value="2">μ΅μ
2</Select.Item>
</Select.Popup>
</Select.Portal>
</Select.Root>
</>
);
}pnpm dev # λͺ¨λ ν¨ν€μ§λ₯Ό watch λͺ¨λλ‘ μ€ν
pnpm dev --filter=@lyra/ui # νΉμ ν¨ν€μ§λ§ μ€νpnpm build # λͺ¨λ ν¨ν€μ§ λΉλ
pnpm build --filter=@lyra/ui # UI ν¨ν€μ§ λ° Storybook λΉλpnpm test # λͺ¨λ ν
μ€νΈ μ€ν
pnpm test --filter=@lyra/ui # UI ν¨ν€μ§ ν
μ€νΈλ§ μ€ν
pnpm test:watch # Watch λͺ¨λλ‘ ν
μ€νΈpnpm lint # λͺ¨λ ν¨ν€μ§ λ¦°ν
pnpm lint:fix # λ¦°νΈ μλ¬ μλ μμ pnpm clean # node_modules λ° λΉλ κ²°κ³Όλ¬Ό μμ - μ»΄ν¬λνΈ νμΌ μμ±
// packages/ui/src/components/my-component/my-component.tsx
import * as BaseUI from '@base-ui-components/react/MyComponent';
import styles from './my-component.module.css';
export function MyComponent({ children, ...props }) {
return (
<BaseUI.Root {...props} className={styles.root}>
{children}
</BaseUI.Root>
);
}- μ€νμΌ μμ±
/* packages/ui/src/components/my-component/my-component.module.css */
.root {
padding: var(--spacing-4);
background: var(--color-bg-surface-default);
}- ν μ€νΈ μμ±
// packages/ui/src/components/my-component/my-component.test.tsx
import { render, screen } from '@testing-library/react';
import { MyComponent } from './my-component';
describe('MyComponent', () => {
it('renders children', () => {
render(<MyComponent>Test</MyComponent>);
expect(screen.getByText('Test')).toBeInTheDocument();
});
});- Storybook μ€ν 리 μΆκ°
// packages/ui/src/stories/components/my-component/my-component.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { MyComponent } from '../../../components/my-component/my-component';
const meta = {
title: "MyComponent",
component: MyComponent,
tags: ["autodocs"],
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: "Hello World",
},
};- export μΆκ°
// packages/ui/src/index.tsx
export { MyComponent } from './components/my-component/my-component';.button {
/* μμ ν ν° */
color: var(--color-text-primary);
background: var(--color-bg-primary-default);
border-color: var(--color-border-default);
/* κ°κ²© ν ν° */
padding: var(--spacing-2) var(--spacing-4);
margin: var(--spacing-4);
/* νμ΄ν¬κ·ΈλνΌ ν ν° */
font-family: var(--font-family-sans);
font-size: var(--font-size-base);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-normal);
/* ν
λ리 ν ν° */
border-radius: var(--border-radius-md);
/* μ λλ©μ΄μ
ν ν° */
transition-duration: var(--duration-fast);
transition-timing-function: var(--easing-ease-in-out);
}.container {
width: 100%;
}
/* 640px μ΄ν (λͺ¨λ°μΌ) */
@media (--sm-down) {
.container {
padding: var(--spacing-2);
}
}
/* 640px μ΄μ (νλΈλ¦Ώ+) */
@media (--sm-up) {
.container {
padding: var(--spacing-4);
}
}
/* 640px ~ 768px (νλΈλ¦Ώλ§) */
@media (--sm-only) {
.container {
padding: var(--spacing-3);
}
}# λͺ¨λ ν
μ€νΈ μ€ν
pnpm test
# Watch λͺ¨λ
pnpm test:watch
# 컀λ²λ¦¬μ§ 리ν¬νΈ
pnpm test:coverageimport { expect, userEvent, within } from '@storybook/test';
export const InteractionTest: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole('button');
await userEvent.click(button);
await expect(button).toHaveAttribute('aria-pressed', 'true');
},
};# κ°λ° λͺ¨λ
pnpm dev --filter=@lyra/ui
# λΉλ
pnpm build --filter=@lyra/ui
# Storybookλ§ μ€ν
cd packages/ui && pnpm storybookStorybookμ http://localhost:6006 μμ μ€νλ©λλ€.
- React 19: UI λΌμ΄λΈλ¬λ¦¬
- TypeScript: νμ μμ μ±
- Base UI Components: μ κ·Όμ± μ°μ ν€λλ¦¬μ€ μ»΄ν¬λνΈ
- Turborepo: λͺ¨λ Έλ ν¬ λΉλ μμ€ν
- pnpm: ν¨ν€μ§ λ§€λμ
- Rollup: UI ν¨ν€μ§ λ²λ€λ¬
- Vite: κ°λ° μλ² λ° λΉλ λꡬ
- CSS Modules: μ€μ½ν μ€νμΌλ§
- PostCSS: CSS λ³ν
- postcss-nesting
- postcss-custom-media
- postcss-mixins
- postcss-global-data
- Style Dictionary: λμμΈ ν ν° λ³ν
- Vitest: μ λ ν μ€νΈ νλ μμν¬
- Testing Library: React μ»΄ν¬λνΈ ν μ€ν
- Storybook: μ»΄ν¬λνΈ λ¬Έμν λ° μΈν°λμ ν μ€νΈ
- Chromatic: μκ°μ νκ· ν μ€νΈ
- ESLint: μ½λ λ¦°ν
- TypeScript: μ μ νμ κ²μ¬
- Changesets: λ²μ κ΄λ¦¬ λ° μ²΄μΈμ§λ‘κ·Έ
μ΄ νλ‘μ νΈλ Changesetsλ₯Ό μ¬μ©νμ¬ λ²μ μ κ΄λ¦¬ν©λλ€.
pnpm changeset- λ³κ²½λ ν¨ν€μ§ μ ν
- λ²μ λ²ν νμ μ ν (major/minor/patch)
- λ³κ²½ μ¬ν μμ½ μμ±
pnpm changeset versionpnpm releaseμ΄μμ ν 리νμ€νΈλ μΈμ λ νμν©λλ€!
- Fork the repository
- Create your feature branch (
git checkout -b feat/amazing-feature) - Commit your changes (
git commit -m 'feat: add amazing feature') - Push to the branch (
git push origin feat/amazing-feature) - Open a Pull Request
MIT
- Repository
- Storybook (λ°°ν¬ μμ )
- Documentation (νλ‘μ νΈ λ¬Έμ)