plop을 사용하여 .stories 템플릿 자동 생성하기

jh·2024년 7월 26일

디자인 시스템

목록 보기
6/14

보통 storybook으로 컴포넌트 테스트를 하게 되면

  1. **.stories 파일 생성
  2. 필요한 것 import
  3. 컴포넌트에서 사용하는 prop에 대해서 args에 선언(자동 완성이 안되기 때문에 하나씩 보면서 옮겨야된다)
  4. 예시로 보여줄 컴포넌트 선언

3번의 경우 react-docgen-typescript 이라는 라이브러리로 해결이 어느정도 가능하지만
1,2번의 경우 모든 stories 파일에서 공통적으로 반복해서 해줘야 한다

import type { Meta, StoryObj } from "@storybook/react"

import { Test } from "../components/Test/Test.tsx"

const meta = {
  title: "Test",
  component: Test,
  tags: ["autodocs"],
} satisfies Meta<typeof Test>

export default meta

type Story = StoryObj<typeof meta>

export const Primary: Story = {
  args: {},
}

예전에는 이런 예시 파일같은걸 하나 만들어놓고 복붙한 다음에 컴포넌트 이름 부분만 전체 블럭으로 잡고 수정해버리면 되긴 하지만 개인적으로 그게 더 귀찮다...

그래서 이런 템플릿을 자동으로 생성해주는 기능이 없을까 하다가 plop 이라는 라이브러리를 알게 되었다

plop이란

Plop is a little tool that saves you time and helps your team build new files with consistency

plop은 nodeJs 기반의 도구로, 코드(파일) 생성 작업을 자동화 할 수 있는 라이브러리이다
plop은 사용자가 지정한 템플릿을 기반으로 코드를 작성해주는데, 이를 통해 일관된 코드 스타일을 유지할 수 있게 해준다

템플릿 코드를 보면 이해하기 쉬운데

//stories.tsx.hbs
import type { Meta, StoryObj } from "@storybook/react"

import  { {{pascalCase name}} } from "../components/{{pascalCase name}}/{{pascalCase name}}.tsx"

const meta = {
  title: "{{pascalCase name}}",
  component: {{pascalCase name}},
  tags: ["autodocs"],
  } satisfies Meta<typeof {{pascalCase name}}>

export default meta

type Story = StoryObj<typeof meta>

export const Primary: Story = {
  args: {},
}

템플릿 파일은 .hbs 확장자를 통해 만들고, {{pascalCase name}} 부분에는 입력받은 컴포넌트 이름이 들어가게 될 것이다

import { promises as fs } from "fs"
import path from "path"
import { fileURLToPath } from "url"

async function getAllFiles(dirPath) {
  let files = []
  const items = await fs.readdir(dirPath, { withFileTypes: true })

  for (const item of items) {
    const fullPath = path.join(dirPath, item.name)

    if (item.isDirectory()) {
      const nestedFiles = await getAllFiles(fullPath)
      files = files.concat(nestedFiles)
    } else {
      files.push(fullPath)
    }
  }

  return files
}

export default async function writeStories(plop) {
  const __filename = fileURLToPath(import.meta.url)
  const __dirname = path.dirname(__filename)
  const dir = path.join(__dirname, "../ui/src/components")
  const files = await getAllFiles(dir)
  const fileNames = files.map((file) => path.relative(dir, file))

  plop.setGenerator("Story", {
    description: "Create a story file",
    prompts: [
      {
        type: "input",
        name: "name",
        message: "stories/** 파일을 생성할 컴포넌트 이름을 입력해주세요",
        validate: (input) => {
          const componentPath = `${input}.tsx`
          const isValid = fileNames.some((fileName) =>
            fileName.endsWith(componentPath),
          )

          if (!isValid) {
            console.log("\n만들어진 컴포넌트 이름을 입력해주세요.")
            return false
          }

          return true
        },
      },
    ],
    actions: [
      {
        type: "add",
        path: "../ui/src/stories/{{pascalCase name}}.stories.tsx",
        templateFile: "Stories.tsx.hbs",
      },
    ],
  })
}

plop을 통해 해당 코드를 실행하게 되면
1. 터미널을 통해 컴포넌트 이름을 입력받는다

  • 디렉토리 안에 해당 컴포넌트가 존재하지 않으면 종료시킨다
  • 현재 components 경로에 디렉토리가 중첩되어있어서, 재귀함수 getAllFiles 를 통해 components/ 경로에 있는 모든 파일들을 다 찾은 후, 그 중에서 입력받은 컴포넌트가 존재하는지를 찾고 있다
  1. 컴포넌트가 존재한다면, actions 부분의 코드가 실행된다
  • path 경로에, 만들어 놓은 template 파일을 이용하여 파일을 생성한다
  • pascalCase name 은 입력받은 컴포넌트 이름이 들어간다

완성본

//Test.stories.tsx
import type { Meta, StoryObj } from "@storybook/react"

import { Test } from "../components/Test/Test.tsx"

const meta = {
  title: "Test",
  component: Test,
  tags: ["autodocs"],
} satisfies Meta<typeof Test>

export default meta

type Story = StoryObj<typeof meta>

export const Primary: Story = {
  args: {},
}

이제 귀찮게 복붙하지 않아도 명령어 하나로 stories 템플릿을 생성할 수 있다

0개의 댓글