나만의 디자인 시스템을 만들어보자! - 프로젝트 세팅

흑우·2023년 12월 29일
0

서론

이번 글에서는 선택한 기술 스택을 바탕으로 프로젝트를 세팅해보겠습니다. 프로젝트 세팅이 제대로 됐는지 확인하기 위해 간단한 샘플 코드도 작성해볼 예정입니다.

Create Project

npm create vite@latest

저번 글에서 말씀드렸듯이 저는 빌드 툴로 Vite를 선택했습니다. 해당 명령어를 통해 Vite 프로젝트를 생성해주겠습니다.

저는 TypeScript와 React를 선택했습니다.

Vanilla Extract

Vanilla Extract 관련 의존성 추가

npm install @vanilla-extract/css @vanilla-extract/sprinkles @vanilla-extract/recipes @vanilla-extract/vite-plugin @vanilla-extract/dynamic 

@vanilla-extract/css

vanilla extract의 core 라이브러리입니다. 해당 라이브러리만 사용해도 디자인 시스템을 만드는 데는 문제가 없지만 개발자 경험을 향상시켜주는 다양한 라이브러리를 지원해주기 때문에 저는 해당 라이브러리까지 사용했습니다.

@vanilla-extract/sprinkles

스타일 선언을 축약해주는 기능을 지원하는 패키지입니다.

@vanilla-extract/recipes

다양한 변형 css를 생성하는 패키지입니다.

@vanilla-extract/dynamic

동적 테마를 지원해주는 패키지입니다.

@vanilla-extract/vite-plugin

vite가 vaniila extract를 해석할 수 있게 해주는 플러그인입니다.

Vanilla 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가 해석할 수 있게 해줍니다.

Vanilla Extract Sample Code

// 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 명령어를 통해 스타일이 정상적으로 적용되는지 테스트해봅니다.

Storybook

Storybook 의존성 추가

npx storybook@latest init

해당 명령어를 입력하면 storybook이 자동으로 의존성을 주입하고 샘플 코드까지 다 만들어줍니다.

하지만 자동으로 생성된 샘플 코드보다 직접 작성해봐야 이해를 하기 때문에 한 번 보고 다 삭제해줍니다.

Storybook Sample Code

// 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을 실행해줍니다.

Jest

Jest 과련 의존성 추가

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

package.json 수정

"scripts": {
	...
    "test": "jest"
}

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      
    }
}

Test Sample Code

// 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 명이 다르기 때문에 실패하는 것이 정상입니다.

Reference

profile
흑우 모르는 흑우 없제~

0개의 댓글