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 (ํ๋ก์ ํธ ๋ฌธ์)