이번 글에서는 선택한 기술 스택을 바탕으로 프로젝트를 세팅해보겠습니다. 프로젝트 세팅이 제대로 됐는지 확인하기 위해 간단한 샘플 코드도 작성해볼 예정입니다.
npm create vite@latest
저번 글에서 말씀드렸듯이 저는 빌드 툴로 Vite를 선택했습니다. 해당 명령어를 통해 Vite 프로젝트를 생성해주겠습니다.
저는 TypeScript와 React를 선택했습니다.
npm install @vanilla-extract/css @vanilla-extract/sprinkles @vanilla-extract/recipes @vanilla-extract/vite-plugin @vanilla-extract/dynamic
vanilla extract의 core 라이브러리입니다. 해당 라이브러리만 사용해도 디자인 시스템을 만드는 데는 문제가 없지만 개발자 경험을 향상시켜주는 다양한 라이브러리를 지원해주기 때문에 저는 해당 라이브러리까지 사용했습니다.
스타일 선언을 축약해주는 기능을 지원하는 패키지입니다.
다양한 변형 css를 생성하는 패키지입니다.
동적 테마를 지원해주는 패키지입니다.
vite가 vaniila extract를 해석할 수 있게 해주는 플러그인입니다.
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
export default defineConfig({
plugins: [
vanillaExtractPlugin(),
react(),
],
});
vanillaExtractPlugin을 통해서 Vanilla Extract를 Vite가 해석할 수 있게 해줍니다.
// Button.css.ts
import { recipe } from "@vanilla-extract/recipes";
export const button = recipe({
base: {
border: "none",
backgroundColor: "black",
color: "white",
fontWeight: "bold",
},
variants: {
size: {
sm: {
padding: "5px",
},
md: {
padding: "10px",
},
lg: {
padding: "15px",
},
},
},
});
// Button.tsx
import React from "react";
import { button } from "./Button.css";
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
size?: "sm" | "md" | "lg";
}
const Button: React.FC<ButtonProps> = ({ size, ...props }) => {
return (
<button
{...props}
className={button({
size,
})}
>
{props.children}
</button>
);
};
export default Button;
// App.tsx
import Button from "./components/Button.tsx";
function App() {
return (
<div>
<Button size="lg">버튼</Button>
</div>
);
}
export default App;
간단한 Sample Code를 생성하고 npm run dev
명령어를 통해 스타일이 정상적으로 적용되는지 테스트해봅니다.
npx storybook@latest init
해당 명령어를 입력하면 storybook이 자동으로 의존성을 주입하고 샘플 코드까지 다 만들어줍니다.
하지만 자동으로 생성된 샘플 코드보다 직접 작성해봐야 이해를 하기 때문에 한 번 보고 다 삭제해줍니다.
// Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import Button from "./Button";
const meta = {
title: "UI/Button",
component: Button,
tags: ["autodocs"],
parameters: { layout: "centered" },
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Small: Story = {
args: { size: "sm", children: "Click Me" },
};
export const Medium: Story = {
args: { size: "md", children: "Click Me" },
};
export const LargeOnClick: Story = {
args: {
size: "lg",
children: "Click Me",
onClick: () => alert("Clicked!"),
},
};
export const Disabled: Story = {
args: { size: "lg", children: "Click Me", disabled: true },
};
아까 만든 Button 컴포넌트와 맞는 Storybook Sample Code를 작성하고 npm run storybook
명령어를 통해 Storybook을 실행해줍니다.
npm install -D jest @types/jest ts-node ts-jest @testing-library/react identity-obj-proxy jest-environment-jsdom @testing-library/jest-dom jest-svg-transformer @vanilla-extract/jest-transform @vanilla-extract/babel-plugin
"scripts": {
...
"test": "jest"
}
// jest.config.ts
export default {
testEnvironment: "jsdom",
transform: {
"^.+\\.tsx?$": [
"ts-jest",
{
babelConfig: {
plugins: ["@vanilla-extract/babel-plugin"],
},
},
],
"\\.css\\.ts$": "@vanilla-extract/jest-transform"
},
moduleNameMapper: {
"^.+\\.svg$": "jest-svg-transformer",
"\\.(css|less|sass|scss)$": "identity-obj-proxy",
},
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
};
// jest.setup.ts
import "@testing-library/jest-dom";
// .eslintrc.cjs
module.exports = {
env: {
...,
module: "node",
}
}
// tsconfig.json
{
"compilerOptions":{
...,
"esModuleInterop": true
}
}
// Button.test.tsx
import '@testing-library/jest-dom';
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import Button from '../components/Button';
describe('Button', () => {
afterEach(() => cleanup());
it('renders', () => {
render(<Button data-testid="btn" />);
const button = screen.getByTestId('btn');
expect(button).toBeInTheDocument();
});
it('fires an event on onClick', () => {
const fn = jest.fn();
render(<Button onClick={fn} data-testid="btn" />);
const button = screen.getByTestId('btn');
fireEvent.click(button);
expect(fn).toHaveBeenCalled();
});
it('renders the correct classes', () => {
const fn = jest.fn();
render(<Button size="lg" onClick={fn} data-testid="p-btn" />);
const pBtn = screen.getByTestId('p-btn');
expect(pBtn).toHaveClass('hyeon-button');
});
});
npm run test
명령어를 통해 test 코드가 정상적으로 작동하는지 테스트 해보시면 됩니다.
마지막 테스트는 class 명이 다르기 때문에 실패하는 것이 정상입니다.