Skip to content

Commit c4b73be

Browse files
committed
ADD : [Javascript] plop 포스트 추가
1 parent 07542d1 commit c4b73be

File tree

1 file changed

+391
-0
lines changed

1 file changed

+391
-0
lines changed
Lines changed: 391 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,391 @@
1+
---
2+
title: "[Javascript] : plop 라이브러리로 디자인 시스템 컴포넌트 파일을 템플릿화 해보자!"
3+
excerpt_separator: <!--more-->
4+
categories:
5+
- Javascript
6+
- plop
7+
tags:
8+
- Javascript
9+
- plop
10+
# 이미지 url (썸네일 필요한 경우 추가)
11+
image: /assets/img/thumbnail/javascript-logo.png
12+
# 기본 비노출 상태, 노출하고 싶은 경우 아래 옵션 제거
13+
# published: false
14+
---
15+
16+
> 이 글로 얻을 수 있는 정보
17+
> 1. plop를 이용한 파일 템플릿화
18+
> 2. plop 라이브러리 설치 및 사용법
19+
> 2. plop 템플릿
20+
> 3. handlebars 템플릿
21+
{: .prompt-tip }
22+
23+
## 0. 개요
24+
디자인 시스템 컴포넌트를 구성하면서 스토리북 관련 구글링 중에 파일을 템플릿화 하여 쉽게 생성할 수 있는 plop 라이브러리를 알게되어 디자인 시스템 파일 템플릿 방법을 알아보고 템플릿을 공유해보려 합니다.
25+
26+
## 1. plop 라이브러리란?
27+
**[plop](https://www.npmjs.com/package/plop)**라이브러리는 개요에서 말했듯이 <span class="highlighting-underline">파일을 템플릿화 하여 쉽게 생성</span>할 수 있게 도와주는 라이브러리 입니다.
28+
29+
plop은 프롬프트 라이브러리인 **[inquirer](https://www.npmjs.com/package/inquirer)**와 텍스트 형식을 생성하는 템플릿 언어를 사용해 템플릿을 만드는 **[handlebars](https://www.npmjs.com/package/handlebars)** 라이브러리로 만들어졌습니다. 즉, 프롬프트를 통해 정보를 입력받고 그 정보로 템플릿 언어로 템플릿을 만들어 파일을 생성해주는 것입니다.
30+
31+
## 2. plop 라이브러리 설치 및 사용법
32+
자세한 내용은 **[plop 라이브러리 공식 홈페이지](https://www.npmjs.com/package/plop)**에서 보는 게 더 낫기 때문에 설치 방법과 사용법을 간략하게 핵심만 알아보겠습니다.
33+
34+
### 2-1. plop 라이브러리 설치
35+
plop 라이브러리를 파일 만들 때만 사용하니, production 환경까지 반영되지 않아도 되기 때문에 dev로 설치해줍니다.
36+
37+
```typescript
38+
// yarn
39+
yarn add -dev plop
40+
41+
// npm
42+
npm install --save-dev plop
43+
```
44+
45+
### 2-2. plop script 등록 및 type 설정
46+
<span class="highlighting-underline">plop을 사용하기 위한 script와 ESM(ECMAScript Modules)를 사용하기 위해 type을 지정</span>해줍니다. (CJS(CommonJS)를 사용하려면 type을 지정해주지 않아도 괜찮습니다.)
47+
48+
> CJS(CommonJS), ESM(ECMAScript Modules)란?
49+
>
50+
> 파일 모듈화를 진행하기 위한 모듈 시스템을 일컫습니다. 따로 package.json에 설정하지 않으면 CJS가 기본입니다.<br/>
51+
> `CJS는 require / module.exports`를 사용하고, `ESM은 import/export` 문을 사용해서 파일 모듈화를 진행할 수 있습니다.<br/>
52+
> CJS/EMS와 package.json의 type, exports 관심이 있으시다면 관련 다음 글을 읽어봐도 좋습니다.<br/>
53+
> [토스 - CommonJS와 ESM에 모두 대응하는 라이브러리 개발하기: exports field](https://toss.tech/article/commonjs-esm-exports-field)
54+
{: .prompt-info }
55+
56+
```typescript
57+
// package.json
58+
{
59+
...
60+
"type": "module",
61+
"scripts": {
62+
...
63+
"plop": "plop"
64+
}
65+
}
66+
```
67+
68+
### 2-3. plopfile.js 작성
69+
폴더의 root 경로에 `plopfile.js`를 만들고 다음과 같이 <span class="highlighting-underline">setGenerator를 사용하여 프롬프트를 구성하고 템플릿 파일을 이용해 파일들을 템플릿화</span> 할 수 있습니다.
70+
71+
setGenerator의 prompots 관련 옵션은 **[inquirer](https://www.npmjs.com/package/inquirer)**에서 actions의 templateFile 관련 옵션은 **[handlebars](https://www.npmjs.com/package/handlebars)**에서 확인할 수 있습니다.
72+
73+
더 자세한 내용은 다음에서 이어집니다.
74+
75+
```typescript
76+
export default function (plop) {
77+
// setGenerator로 프롬프트 구성과 파일 템플릿화를 진행할 수 있습니다.
78+
plop.setGenerator('generator name', {
79+
description: 'generator description',
80+
prompts: [], // inquirer를 이용해 prompts를 구성할 수 있습니다.
81+
actions: [] // actions 관련 옵션은 plop에서 actions내부에 templateFile 관련해서는 handlebars에서 확인할 수 있습니다.
82+
});
83+
};
84+
```
85+
86+
### 2-4. plop 실행
87+
script를 지정한 plop 명령어를 통해 구성한 plop을 실행하여 설정한 프롬프트를 보여주고 파일을 생성할 수 있습니다.
88+
89+
```typescript
90+
// yarn
91+
yarn plop
92+
93+
// npm
94+
npm plop
95+
```
96+
97+
![plop](https://camo.githubusercontent.com/7bca9ee3b02142d21a35ded0d74eac47fc76aebdba2dbb9eb9a9798e34ad13ba/68747470733a2f2f692e696d6775722e636f6d2f70656e55466b722e676966)
98+
99+
## 3. plop 템플릿 공유 및 꿀팁
100+
팀에서는 Next.js14를 사용하고 있으며, 만들고 있는 디자인 시스템 컴포넌트의 기본적인 파일 구조는 다음과 같이 이루어져 있어 필요한 부분은 수정하여서 사용하면 좋을 것 같습니다.
101+
102+
```typescript
103+
Component
104+
index.tsx
105+
type.d.ts
106+
index.modules.scss
107+
index.stories.tsx
108+
```
109+
110+
### 3-1. plop파일
111+
plopfile.js에서 눈에 띄는 부분과 관련 링크를 적어놓겠습니다.
112+
113+
- `plop.setHelper`: handlebars에서 사용하기 위한 메서드 여기에서는 propmts 폴더 선택을 도와주는 라이브러리를 사용 - <https://handlebarsjs.com/guide/#custom-helpers>
114+
- `plop.setPrompt`: 사용자들이 plop을 이용해 만든 다양한 라이브러리를 등록해서 사용할 수 있음 - <https://github.com/plopjs/awesome-plop>
115+
116+
위에서 <span class="highlighting-underline">handlebars 라이브러리는 {{ }} 내부에 있는 값들을 치환하여 파일을 만들어줄 수 있습니다.</span> handlebars 내부에서 사용할 수 있는 함수 조건문 등을 사용할 수도 있는데, 공식 문서가 잘 되어 있어 공식 문서를 참고하시면 되겠습니다.
117+
118+
```javascript
119+
// plopfile.js
120+
121+
import inquirer from "inquirer";
122+
import inquirerDirectory from "inquirer-directory";
123+
124+
export default function (plop) {
125+
// hbs 템플릿(handlebars)에서 사용할 포함 여부 함수 정의
126+
plop.setHelper("includes", function (arr, values) {
127+
const valueList = values.split(",");
128+
129+
return valueList.some((value) => arr.includes(value));
130+
});
131+
132+
// 폴더 선택 라이브러리 적용
133+
plop.setPrompt("directory", inquirerDirectory);
134+
// prompt 생성
135+
plop.setGenerator("design-system-ui", {
136+
description: "Create design system ui",
137+
prompts: [
138+
// 폴더 선택(setPrompt에서 적용한 폴더 선택 라이브러리 사용)
139+
{
140+
type: "directory",
141+
name: "path",
142+
message: `1. 컴포넌트를 생성할 폴더를 선택해주세요 (⬆️ 버튼을 누르면 빠르게 선택(choose this directory) 할 수 있어요)`,
143+
basePath: "ui",
144+
},
145+
// 컴포넌트 이름 입력
146+
{
147+
type: "input",
148+
name: "name",
149+
message: "2. 컴포넌트를 이름을 입력해주세요",
150+
},
151+
// 스토리북 옵션 선택
152+
{
153+
type: "checkbox",
154+
message: "3. 스토리북 Meta에 사용할 옵션을 선택해주세요",
155+
name: "options",
156+
loop: false,
157+
choices: [
158+
new inquirer.Separator("====== 기본 옵션 ======"),
159+
{
160+
value: "args",
161+
name: "args ",
162+
disabled: "컴포넌트 props 값 설정",
163+
},
164+
new inquirer.Separator("====== 선택 옵션 ======"),
165+
{
166+
value: "storyHeight",
167+
name: "story.height (Story가 보여질 영역 높이 조절)",
168+
},
169+
{
170+
value: "sourceCode",
171+
name: "source.code (Story에 보여질 source code 관련 설정)",
172+
},
173+
{
174+
value: "argTypes",
175+
name: "argTypes (컴포넌트 props의 type관련 내용 설정)",
176+
},
177+
{
178+
value: "renderOrDecorators",
179+
name: "render or decorators (Story 렌더링 마크업,스타일링,동작 제어)",
180+
},
181+
],
182+
},
183+
],
184+
// templateFile에서 prompt로 입력한 정보를 받아 파일을 생성
185+
actions: [
186+
// index.tsx 생성
187+
{
188+
type: "add",
189+
path: "ui/{{path}}/{{pascalCase name}}/index.tsx",
190+
templateFile: "plop-templates/design-system-ui/Component.tsx.hbs",
191+
},
192+
// type.d.ts 생성
193+
{
194+
type: "add",
195+
path: "ui/{{path}}/{{pascalCase name}}/type.d.ts",
196+
templateFile: "plop-templates/design-system-ui/Type.d.ts.hbs",
197+
},
198+
// index.module.scss 생성
199+
{
200+
type: "add",
201+
path: "ui/{{path}}/{{pascalCase name}}/index.module.scss",
202+
},
203+
// index.stories.tsx 생성
204+
{
205+
type: "add",
206+
path: "ui/{{path}}/{{pascalCase name}}/index.stories.tsx",
207+
templateFile: "plop-templates/design-system-ui/Story.tsx.hbs",
208+
},
209+
],
210+
});
211+
}
212+
```
213+
214+
### 3-2. handlebars 템플릿 파일
215+
plop파일에서 정의한 `includes helper`를 사용하는 것을 볼 수 있고 **if문을 통하여 템플릿을 조건부로 적용**하였습니다.
216+
217+
> handlebars에서 helper 사용법
218+
>
219+
> 기본 적으로 **\{\{ 헬퍼이름 메서드인자 \}\}** 형식으로 사용하며, 조건문 안에 넣을 때는 괄호로 감싸서( **\{\{#if (includes options "storyHeight,sourceCode")\}\}** ) 사용하게 됩니다.<br/>
220+
> e.g. **\{\{ pascalCase name \}\}** : pascalCase는 handlebars에서 기본으로 제공하는 메서드 인데, prompt에서 받은 name 값을 pascalCase 메서드를 통해 pascalCase로 만들어줍니다.
221+
{: .prompt-info }
222+
223+
#### Story.tsx.hbs
224+
225+
```javascript
226+
import {{pascalCase name}} from "@design-system/ui/{{path}}/{{pascalCase name}}";
227+
import type { Meta, StoryObj } from "@storybook/react";
228+
229+
/**
230+
* 여기에 해당 컴포넌트에 대한 설명을 적어주세요. 미기재 시 {{pascalCase name}} 컴포넌트의 JSDoc이 노출됩니다.
231+
* (Storybook에서 parameters.docs.description.component 보다 JSDoc을 권장합니다.)
232+
* @see {https://storybook.js.org/docs/api/doc-blocks/doc-block-description#writing-descriptions}
233+
*/
234+
const meta: Meta<typeof {{pascalCase name}}> = {
235+
component: {{pascalCase name}},
236+
{{#if (includes options "storyHeight,sourceCode")}}
237+
parameters: {
238+
docs: {
239+
{{#if (includes options "storyHeight")}}
240+
story: {
241+
// (Optional) story가 보여질 영역 높이를 조절할 수 있습니다.
242+
height: "200px",
243+
},
244+
{{/if}}
245+
{{#if (includes options "sourceCode")}}
246+
source: {
247+
/**
248+
* (Optional)
249+
* storybook에 노출되는 source code를 직접 작성할 수 있습니다. (dedent를 사용해 깔끔하게 작성하는 게 좋습니다.)
250+
*
251+
* code와 아래의 transform 둘 다 작성하면 transform은 무시됩니다.
252+
*/
253+
code: dedent`
254+
const [state, setstate] = useState();
255+
256+
return (
257+
<Component>
258+
<SubComponent/>
259+
</Component>
260+
);`,
261+
/**
262+
* (Optional)
263+
* storybook에 노출되는 source code를 변환할 수 있습니다.
264+
*
265+
* 합성 컴포넌트인 SubComponent를 표현하려고 Meta.component에 Component.SubComponent로 작성해주어도
266+
* source code에는 SubComponent로 노출되기 때문에 합성 컴포넌트를 표현할 때 사용할 수 있습니다.
267+
*/
268+
transform: (code: string) =>
269+
code.replaceAll("SubComponent", "Component.SubComponent"),
270+
},
271+
{{/if}}
272+
},
273+
},
274+
{{/if}}
275+
{{#if (includes options "argTypes")}}
276+
argTypes: {
277+
props1: {
278+
// (Optional) 미기재 시 해당 prop의 JSDoc이 노출됩니다.
279+
description: "여기에 props1의 설명을 적어주세요.",
280+
table: {
281+
// description 아래 위치하며, type을 나타내줍니다
282+
type: {
283+
summary: "default 값을 표시해 줄 수 있습니다.",
284+
detail: "detail은 summary를 누르면 노출됩니다.",
285+
},
286+
/**
287+
* (Optional)
288+
* args에 props1이 있고 defaultValue가 정의되어 있지 않으면 args에 정의된 값이 default 값으로 노출됩니다.
289+
*/
290+
defaultValue: {
291+
summary: "default 값을 표시해 줄 수 있습니다.",
292+
detail: "detail은 summary를 누르면 노출됩니다.",
293+
},
294+
/**
295+
* (Optional)
296+
* props1에 category를 정의하면 props1이 기존 위치가 아닌 토글로 된 category에 노출됩니다.
297+
* subcategory는 정의한 category 아래에 표시됩니다.
298+
* 합성 컴포넌트나, 중첩된 props를 표시하고 싶을 때 사용할 수 있습니다.
299+
*/
300+
category: "category",
301+
subcategory: "subcategory",
302+
// (Optional) Args Table에서 props1을 제거합니다.
303+
disable: true,
304+
// (Optional) Args Table에서 props1이 읽기 전용임을 나타냅니다.
305+
readonly: true,
306+
},
307+
/**
308+
* (Optional)
309+
* Args Table에서 사용자 조작에 관한 설정을 넣을 수 있습니다.
310+
* 특정 type(select, radio 등..)의 경우에 options 값이 필요합니다.
311+
* @see {https://storybook.js.org/docs/api/arg-types#control}
312+
*/
313+
control: "select",
314+
options: ["option1", "option2"],
315+
},
316+
},
317+
{{/if}}
318+
// (Optional) Meta에서 args에 입력한 값은 Args Table에 default 값으로 노출됩니다.
319+
args: {
320+
props1: "여기에 props1 타입에 맞는 값을 입력해주세요",
321+
},
322+
{{#if (includes options "renderOrDecorators")}}
323+
/**
324+
* (Optional) render or decorators
325+
* Storybook에 렌더링 될 컴포넌트에 추가로 마크업/스타일링을 하거나 동작 제어가 필요할 때 사용합니다.
326+
*
327+
* storybook 내 이벤트 발생 시 args를 변경할 때 useArgs Addon과 함께 사용하면 좋습니다.
328+
* @see {https://storybook.js.org/docs/writing-stories/args#setting-args-from-within-a-story}
329+
*/
330+
// 하나의 컴포넌트를 렌더링할 때 주로 사용됩니다.
331+
decorators: [
332+
(Story, context) => {
333+
return <Story {...context} args={context.args} />;
334+
},
335+
],
336+
// 여러 개의 컴포넌트나 합성컴포넌트를 렌더링할 때 주로 사용됩니다.
337+
render: (args) => {
338+
return (
339+
<>
340+
<Component>
341+
<SubComponent />
342+
</Component>
343+
<Component>
344+
<SubComponent />
345+
</Component>
346+
</>
347+
);
348+
},
349+
{{/if}}
350+
};
351+
352+
export default meta;
353+
354+
type Story = StoryObj<typeof {{pascalCase name}}>;
355+
356+
/**
357+
* 여기에 해당 Story에 대한 설명을 적어주세요
358+
* Storybook에서 parameters.docs.description.story 보다 JSDoc을 권장합니다.
359+
* @see {https://storybook.js.org/docs/api/doc-blocks/doc-block-description#writing-descriptions}
360+
*/
361+
export const Default: Story = {
362+
args: { props1: "option1" },
363+
};
364+
```
365+
366+
#### Component.tsx.hbs
367+
368+
```javascript
369+
import classNames from "classnames/bind";
370+
import type { {{pascalCase name}}Props } from "@design-system/ui/{{path}}/{{pascalCase name}}/type";
371+
import style from "@design-system/ui/{{path}}/{{pascalCase name}}/index.module.scss";
372+
373+
const cx = classNames.bind(style);
374+
375+
function {{pascalCase name}}(props: {{pascalCase name}}Props) {
376+
return <div />;
377+
}
378+
379+
export default {{pascalCase name}};
380+
```
381+
382+
#### Type.d.ts.hbs
383+
384+
```javascript
385+
export interface {{pascalCase name}}Props {}
386+
```
387+
388+
## 마치며
389+
동료들과 개발할 때, 어느 정도 규칙을 만들며 반복되는 업무를 자동화하고 특정 부분들을 템플릿화 하는 것이 생산성을 높여줄 수 있다고 생각합니다. 이 plop 라이브러리뿐만 아니라 다양한 라이브러리를 잘 활용하면 생산성을 많이 높여줄 수 있을 것 같습니다.
390+
391+
더 좋은 plop 템플릿을 구성하시거나 다른 좋은 방법이 있으시다면 다양한 피드백으로 공유해주시면 감사하겠습니다 🙏

0 commit comments

Comments
 (0)