์ด ๊ธ๋ก ์ป์ ์ ์๋ ์ ๋ณด
1. plop๋ฅผ ์ด์ฉํ ํ์ผ ํ ํ๋ฆฟํ
2. plop ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น ๋ฐ ์ฌ์ฉ๋ฒ
2. plop ํ ํ๋ฆฟ
3. handlebars ํ ํ๋ฆฟ
0. ๊ฐ์
๋์์ธ ์์คํ ์ปดํฌ๋ํธ๋ฅผ ๊ตฌ์ฑํ๋ฉด์ ์คํ ๋ฆฌ๋ถ ๊ด๋ จ ๊ตฌ๊ธ๋ง ์ค์ ํ์ผ์ ํ ํ๋ฆฟํ ํ์ฌ ์ฝ๊ฒ ์์ฑํ ์ ์๋ plop ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์๊ฒ๋์ด ๋์์ธ ์์คํ ํ์ผ ํ ํ๋ฆฟ ๋ฐฉ๋ฒ์ ์์๋ณด๊ณ ํ ํ๋ฆฟ์ ๊ณต์ ํด๋ณด๋ ค ํฉ๋๋ค.
1. plop ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋?
plop๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๊ฐ์์์ ๋งํ๋ฏ์ด ํ์ผ์ ํ ํ๋ฆฟํ ํ์ฌ ์ฝ๊ฒ ์์ฑํ ์ ์๊ฒ ๋์์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ๋๋ค.
plop์ ํ๋กฌํํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ inquirer์ ํ ์คํธ ํ์์ ์์ฑํ๋ ํ ํ๋ฆฟ ์ธ์ด๋ฅผ ์ฌ์ฉํด ํ ํ๋ฆฟ์ ๋ง๋๋ handlebars ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก ๋ง๋ค์ด์ก์ต๋๋ค. ์ฆ, ํ๋กฌํํธ๋ฅผ ํตํด ์ ๋ณด๋ฅผ ์ ๋ ฅ๋ฐ๊ณ ๊ทธ ์ ๋ณด๋ก ํ ํ๋ฆฟ ์ธ์ด๋ก ํ ํ๋ฆฟ์ ๋ง๋ค์ด ํ์ผ์ ์์ฑํด์ฃผ๋ ๊ฒ์ ๋๋ค.
2. plop ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น ๋ฐ ์ฌ์ฉ๋ฒ
์์ธํ ๋ด์ฉ์ plop ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๊ณต์ ํํ์ด์ง์์ ๋ณด๋ ๊ฒ ๋ ๋ซ๊ธฐ ๋๋ฌธ์ ์ค์น ๋ฐฉ๋ฒ๊ณผ ์ฌ์ฉ๋ฒ์ ๊ฐ๋ตํ๊ฒ ํต์ฌ๋ง ์์๋ณด๊ฒ ์ต๋๋ค.
2-1. plop ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น
plop ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ผ ๋ง๋ค ๋๋ง ์ฌ์ฉํ๋, production ํ๊ฒฝ๊น์ง ๋ฐ์๋์ง ์์๋ ๋๊ธฐ ๋๋ฌธ์ dev๋ก ์ค์นํด์ค๋๋ค.
// yarn
yarn add -dev plop
// npm
npm install --save-dev plop
2-2. plop script ๋ฑ๋ก ๋ฐ type ์ค์
plop์ ์ฌ์ฉํ๊ธฐ ์ํ stript์ ESM(ECMAScript Modules)๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด type์ ์ง์ ํด์ค๋๋ค. (CJS(CommonJS)๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด type์ ์ง์ ํด์ฃผ์ง ์์๋ ๊ด์ฐฎ์ต๋๋ค.)
CJS(CommonJS), ESM(ECMAScript Modules)๋?
ํ์ผ ๋ชจ๋ํ๋ฅผ ์งํํ๊ธฐ ์ํ ๋ชจ๋ ์์คํ ์ ์ผ์ปซ์ต๋๋ค. ๋ฐ๋ก package.json์ ์ค์ ํ์ง ์์ผ๋ฉด CJS๊ฐ ๊ธฐ๋ณธ์ ๋๋ค.
CJS๋ require / module.exports๋ฅผ ์ฌ์ฉํ๊ณ , ESM์ import/export ๋ฌธ์ ์ฌ์ฉํด์ ํ์ผ ๋ชจ๋ํ๋ฅผ ์งํํ ์ ์์ต๋๋ค.
CJS/EMS์ package.json์ type, exports ๊ด์ฌ์ด ์์ผ์๋ค๋ฉด ๊ด๋ จ ๋ค์ ๊ธ์ ์ฝ์ด๋ด๋ ์ข์ต๋๋ค.
ํ ์ค - CommonJS์ ESM์ ๋ชจ๋ ๋์ํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๊ฐ๋ฐํ๊ธฐ: exports field
// package.json
{
...
"type": "module",
"scripts": {
...
"plop": "plop"
}
}
2-3. plopfile.js ์์ฑ
ํด๋์ root ๊ฒฝ๋ก์ plopfile.js๋ฅผ ๋ง๋ค๊ณ ๋ค์๊ณผ ๊ฐ์ด setGenerator๋ฅผ ์ฌ์ฉํ์ฌ ํ๋กฌํํธ๋ฅผ ๊ตฌ์ฑํ๊ณ ํ ํ๋ฆฟ ํ์ผ์ ์ด์ฉํด ํ์ผ๋ค์ ํ ํ๋ฆฟํ ํ ์ ์์ต๋๋ค.
setGenerator์ prompots ๊ด๋ จ ์ต์ ์ inquirer์์ actions์ templateFile ๊ด๋ จ ์ต์ ์ handlebars์์ ํ์ธํ ์ ์์ต๋๋ค.
๋ ์์ธํ ๋ด์ฉ์ ๋ค์์์ ์ด์ด์ง๋๋ค.
export default function (plop) {
// setGenerator๋ก ํ๋กฌํํธ ๊ตฌ์ฑ๊ณผ ํ์ผ ํ
ํ๋ฆฟํ๋ฅผ ์งํํ ์ ์์ต๋๋ค.
plop.setGenerator('generator name', {
description: 'generator description',
prompts: [], // inquirer๋ฅผ ์ด์ฉํด prompts๋ฅผ ๊ตฌ์ฑํ ์ ์์ต๋๋ค.
actions: [] // actions ๊ด๋ จ ์ต์
์ plop์์ actions๋ด๋ถ์ templateFile ๊ด๋ จํด์๋ handlebars์์ ํ์ธํ ์ ์์ต๋๋ค.
});
};
2-4. plop ์คํ
script๋ฅผ ์ง์ ํ plop ๋ช ๋ น์ด๋ฅผ ํตํด ๊ตฌ์ฑํ plop์ ์คํํ์ฌ ์ค์ ํ ํ๋กฌํํธ๋ฅผ ๋ณด์ฌ์ฃผ๊ณ ํ์ผ์ ์์ฑํ ์ ์์ต๋๋ค.
// yarn
yarn plop
// npm
npm plop

3. plop ํ ํ๋ฆฟ ๊ณต์ ๋ฐ ๊ฟํ
ํ์์๋ Next.js14๋ฅผ ์ฌ์ฉํ๊ณ ์์ผ๋ฉฐ, ๋ง๋ค๊ณ ์๋ ๋์์ธ ์์คํ ์ปดํฌ๋ํธ์ ๊ธฐ๋ณธ์ ์ธ ํ์ผ ๊ตฌ์กฐ๋ ๋ค์๊ณผ ๊ฐ์ด ์ด๋ฃจ์ด์ ธ ์์ด ํ์ํ ๋ถ๋ถ์ ์์ ํ์ฌ์ ์ฌ์ฉํ๋ฉด ์ข์ ๊ฒ ๊ฐ์ต๋๋ค.
Component
ใดindex.tsx
ใดtype.d.ts
ใดindex.modules.scss
ใดindex.stories.tsx
3-1. plopํ์ผ
plopfile.js์์ ๋์ ๋๋ ๋ถ๋ถ๊ณผ ๊ด๋ จ ๋งํฌ๋ฅผ ์ ์ด๋๊ฒ ์ต๋๋ค.
- plop.setHelper: handlebars์์ ์ฌ์ฉํ๊ธฐ ์ํ ๋ฉ์๋ ์ฌ๊ธฐ์์๋ propmts ํด๋ ์ ํ์ ๋์์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉ - https://handlebarsjs.com/guide/#custom-helpers
- plop.setPrompt: ์ฌ์ฉ์๋ค์ด plop์ ์ด์ฉํด ๋ง๋ ๋ค์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๋ฑ๋กํด์ ์ฌ์ฉํ ์ ์์ - https://github.com/plopjs/awesome-plop
์์์ handlebars ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ {{ }} ๋ด๋ถ์ ์๋ ๊ฐ๋ค์ ์นํํ์ฌ ํ์ผ์ ๋ง๋ค์ด์ค ์ ์์ต๋๋ค. handlebars ๋ด๋ถ์์ ์ฌ์ฉํ ์ ์๋ ํจ์ ์กฐ๊ฑด๋ฌธ ๋ฑ์ ์ฌ์ฉํ ์๋ ์๋๋ฐ, ๊ณต์ ๋ฌธ์๊ฐ ์ ๋์ด ์์ด ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ์๋ฉด ๋๊ฒ ์ต๋๋ค.
// plopfile.js
import inquirer from "inquirer";
import inquirerDirectory from "inquirer-directory";
export default function (plop) {
// hbs ํ
ํ๋ฆฟ(handlebars)์์ ์ฌ์ฉํ ํฌํจ ์ฌ๋ถ ํจ์ ์ ์
plop.setHelper("includes", function (arr, values) {
const valueList = values.split(",");
return valueList.some((value) => arr.includes(value));
});
// ํด๋ ์ ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ์ฉ
plop.setPrompt("directory", inquirerDirectory);
// prompt ์์ฑ
plop.setGenerator("design-system-ui", {
description: "Create design system ui",
prompts: [
// ํด๋ ์ ํ(setPrompt์์ ์ ์ฉํ ํด๋ ์ ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ)
{
type: "directory",
name: "path",
message: `1. ์ปดํฌ๋ํธ๋ฅผ ์์ฑํ ํด๋๋ฅผ ์ ํํด์ฃผ์ธ์ (โฌ๏ธ ๋ฒํผ์ ๋๋ฅด๋ฉด ๋น ๋ฅด๊ฒ ์ ํ(choose this directory) ํ ์ ์์ด์)`,
basePath: "ui",
},
// ์ปดํฌ๋ํธ ์ด๋ฆ ์
๋ ฅ
{
type: "input",
name: "name",
message: "2. ์ปดํฌ๋ํธ๋ฅผ ์ด๋ฆ์ ์
๋ ฅํด์ฃผ์ธ์",
},
// ์คํ ๋ฆฌ๋ถ ์ต์
์ ํ
{
type: "checkbox",
message: "3. ์คํ ๋ฆฌ๋ถ Meta์ ์ฌ์ฉํ ์ต์
์ ์ ํํด์ฃผ์ธ์",
name: "options",
loop: false,
choices: [
new inquirer.Separator("====== ๊ธฐ๋ณธ ์ต์
======"),
{
value: "args",
name: "args ",
disabled: "์ปดํฌ๋ํธ props ๊ฐ ์ค์ ",
},
new inquirer.Separator("====== ์ ํ ์ต์
======"),
{
value: "storyHeight",
name: "story.height (Story๊ฐ ๋ณด์ฌ์ง ์์ญ ๋์ด ์กฐ์ )",
},
{
value: "sourceCode",
name: "source.code (Story์ ๋ณด์ฌ์ง source code ๊ด๋ จ ์ค์ )",
},
{
value: "argTypes",
name: "argTypes (์ปดํฌ๋ํธ props์ type๊ด๋ จ ๋ด์ฉ ์ค์ )",
},
{
value: "renderOrDecorators",
name: "render or decorators (Story ๋ ๋๋ง ๋งํฌ์
,์คํ์ผ๋ง,๋์ ์ ์ด)",
},
],
},
],
// templateFile์์ prompt๋ก ์
๋ ฅํ ์ ๋ณด๋ฅผ ๋ฐ์ ํ์ผ์ ์์ฑ
actions: [
// index.tsx ์์ฑ
{
type: "add",
path: "ui/{{path}}/{{pascalCase name}}/index.tsx",
templateFile: "plop-templates/design-system-ui/Component.tsx.hbs",
},
// type.d.ts ์์ฑ
{
type: "add",
path: "ui/{{path}}/{{pascalCase name}}/type.d.ts",
templateFile: "plop-templates/design-system-ui/Type.d.ts.hbs",
},
// index.module.scss ์์ฑ
{
type: "add",
path: "ui/{{path}}/{{pascalCase name}}/index.module.scss",
},
// index.stories.tsx ์์ฑ
{
type: "add",
path: "ui/{{path}}/{{pascalCase name}}/index.stories.tsx",
templateFile: "plop-templates/design-system-ui/Story.tsx.hbs",
},
],
});
}
3-2. handlebars ํ ํ๋ฆฟ ํ์ผ
plopํ์ผ์์ ์ ์ํ includes helper๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ๋ณผ ์ ์๊ณ if๋ฌธ์ ํตํ์ฌ ํ ํ๋ฆฟ์ ์กฐ๊ฑด๋ถ๋ก ์ ์ฉํ์์ต๋๋ค.
handlebars์์ helper ์ฌ์ฉ๋ฒ
๊ธฐ๋ณธ ์ ์ผ๋ก {{ ํฌํผ์ด๋ฆ ๋ฉ์๋์ธ์ }} ํ์์ผ๋ก ์ฌ์ฉํ๋ฉฐ, ์กฐ๊ฑด๋ฌธ ์์ ๋ฃ์ ๋๋ ๊ดํธ๋ก ๊ฐ์ธ์( {{#if (includes options "storyHeight,sourceCode")}} ) ์ฌ์ฉํ๊ฒ ๋ฉ๋๋ค.
ex) {{ pascalCase name }} : pascalCase๋ handlebars์์ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณตํ๋ ๋ฉ์๋ ์ธ๋ฐ, prompt์์ ๋ฐ์ name ๊ฐ์ pascalCase ๋ฉ์๋๋ฅผ ํตํด pascalCase๋ก ๋ง๋ค์ด์ค๋๋ค.
Story.tsx.hbs
import {{pascalCase name}} from "@design-system/ui/{{path}}/{{pascalCase name}}";
import type { Meta, StoryObj } from "@storybook/react";
/**
* ์ฌ๊ธฐ์ ํด๋น ์ปดํฌ๋ํธ์ ๋ํ ์ค๋ช
์ ์ ์ด์ฃผ์ธ์. ๋ฏธ๊ธฐ์ฌ ์ {{pascalCase name}} ์ปดํฌ๋ํธ์ JSDoc์ด ๋
ธ์ถ๋ฉ๋๋ค.
* (Storybook์์ parameters.docs.description.component ๋ณด๋ค JSDoc์ ๊ถ์ฅํฉ๋๋ค.)
* @see {https://storybook.js.org/docs/api/doc-blocks/doc-block-description#writing-descriptions}
*/
const meta: Meta<typeof {{pascalCase name}}> = {
component: {{pascalCase name}},
{{#if (includes options "storyHeight,sourceCode")}}
parameters: {
docs: {
{{#if (includes options "storyHeight")}}
story: {
// (Optional) story๊ฐ ๋ณด์ฌ์ง ์์ญ ๋์ด๋ฅผ ์กฐ์ ํ ์ ์์ต๋๋ค.
height: "200px",
},
{{/if}}
{{#if (includes options "sourceCode")}}
source: {
/**
* (Optional)
* storybook์ ๋
ธ์ถ๋๋ source code๋ฅผ ์ง์ ์์ฑํ ์ ์์ต๋๋ค. (dedent๋ฅผ ์ฌ์ฉํด ๊น๋ํ๊ฒ ์์ฑํ๋ ๊ฒ ์ข์ต๋๋ค.)
*
* code์ ์๋์ transform ๋ ๋ค ์์ฑํ๋ฉด transform์ ๋ฌด์๋ฉ๋๋ค.
*/
code: dedent`
const [state, setstate] = useState();
return (
<Component>
<SubComponent/>
</Component>
);`,
/**
* (Optional)
* storybook์ ๋
ธ์ถ๋๋ source code๋ฅผ ๋ณํํ ์ ์์ต๋๋ค.
*
* ํฉ์ฑ ์ปดํฌ๋ํธ์ธ SubComponent๋ฅผ ํํํ๋ ค๊ณ Meta.component์ Component.SubComponent๋ก ์์ฑํด์ฃผ์ด๋
* source code์๋ SubComponent๋ก ๋
ธ์ถ๋๊ธฐ ๋๋ฌธ์ ํฉ์ฑ ์ปดํฌ๋ํธ๋ฅผ ํํํ ๋ ์ฌ์ฉํ ์ ์์ต๋๋ค.
*/
transform: (code: string) =>
code.replaceAll("SubComponent", "Component.SubComponent"),
},
{{/if}}
},
},
{{/if}}
{{#if (includes options "argTypes")}}
argTypes: {
props1: {
// (Optional) ๋ฏธ๊ธฐ์ฌ ์ ํด๋น prop์ JSDoc์ด ๋
ธ์ถ๋ฉ๋๋ค.
description: "์ฌ๊ธฐ์ props1์ ์ค๋ช
์ ์ ์ด์ฃผ์ธ์.",
table: {
// description ์๋ ์์นํ๋ฉฐ, type์ ๋ํ๋ด์ค๋๋ค
type: {
summary: "default ๊ฐ์ ํ์ํด ์ค ์ ์์ต๋๋ค.",
detail: "detail์ summary๋ฅผ ๋๋ฅด๋ฉด ๋
ธ์ถ๋ฉ๋๋ค.",
},
/**
* (Optional)
* args์ props1์ด ์๊ณ defaultValue๊ฐ ์ ์๋์ด ์์ง ์์ผ๋ฉด args์ ์ ์๋ ๊ฐ์ด default ๊ฐ์ผ๋ก ๋
ธ์ถ๋ฉ๋๋ค.
*/
defaultValue: {
summary: "default ๊ฐ์ ํ์ํด ์ค ์ ์์ต๋๋ค.",
detail: "detail์ summary๋ฅผ ๋๋ฅด๋ฉด ๋
ธ์ถ๋ฉ๋๋ค.",
},
/**
* (Optional)
* props1์ category๋ฅผ ์ ์ํ๋ฉด props1์ด ๊ธฐ์กด ์์น๊ฐ ์๋ ํ ๊ธ๋ก ๋ category์ ๋
ธ์ถ๋ฉ๋๋ค.
* subcategory๋ ์ ์ํ category ์๋์ ํ์๋ฉ๋๋ค.
* ํฉ์ฑ ์ปดํฌ๋ํธ๋, ์ค์ฒฉ๋ props๋ฅผ ํ์ํ๊ณ ์ถ์ ๋ ์ฌ์ฉํ ์ ์์ต๋๋ค.
*/
category: "category",
subcategory: "subcategory",
// (Optional) Args Table์์ props1์ ์ ๊ฑฐํฉ๋๋ค.
disable: true,
// (Optional) Args Table์์ props1์ด ์ฝ๊ธฐ ์ ์ฉ์์ ๋ํ๋
๋๋ค.
readonly: true,
},
/**
* (Optional)
* Args Table์์ ์ฌ์ฉ์ ์กฐ์์ ๊ดํ ์ค์ ์ ๋ฃ์ ์ ์์ต๋๋ค.
* ํน์ type(select, radio ๋ฑ..)์ ๊ฒฝ์ฐ์ options ๊ฐ์ด ํ์ํฉ๋๋ค.
* @see {https://storybook.js.org/docs/api/arg-types#control}
*/
control: "select",
options: ["option1", "option2"],
},
},
{{/if}}
// (Optional) Meta์์ args์ ์
๋ ฅํ ๊ฐ์ Args Table์ default ๊ฐ์ผ๋ก ๋
ธ์ถ๋ฉ๋๋ค.
args: {
props1: "์ฌ๊ธฐ์ props1 ํ์
์ ๋ง๋ ๊ฐ์ ์
๋ ฅํด์ฃผ์ธ์",
},
{{#if (includes options "renderOrDecorators")}}
/**
* (Optional) render or decorators
* Storybook์ ๋ ๋๋ง ๋ ์ปดํฌ๋ํธ์ ์ถ๊ฐ๋ก ๋งํฌ์
/์คํ์ผ๋ง์ ํ๊ฑฐ๋ ๋์ ์ ์ด๊ฐ ํ์ํ ๋ ์ฌ์ฉํฉ๋๋ค.
*
* storybook ๋ด ์ด๋ฒคํธ ๋ฐ์ ์ args๋ฅผ ๋ณ๊ฒฝํ ๋ useArgs Addon๊ณผ ํจ๊ป ์ฌ์ฉํ๋ฉด ์ข์ต๋๋ค.
* @see {https://storybook.js.org/docs/writing-stories/args#setting-args-from-within-a-story}
*/
// ํ๋์ ์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋งํ ๋ ์ฃผ๋ก ์ฌ์ฉ๋ฉ๋๋ค.
decorators: [
(Story, context) => {
return <Story {...context} args={context.args} />;
},
],
// ์ฌ๋ฌ ๊ฐ์ ์ปดํฌ๋ํธ๋ ํฉ์ฑ์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋งํ ๋ ์ฃผ๋ก ์ฌ์ฉ๋ฉ๋๋ค.
render: (args) => {
return (
<>
<Component>
<SubComponent />
</Component>
<Component>
<SubComponent />
</Component>
</>
);
},
{{/if}}
};
export default meta;
type Story = StoryObj<typeof {{pascalCase name}}>;
/**
* ์ฌ๊ธฐ์ ํด๋น Story์ ๋ํ ์ค๋ช
์ ์ ์ด์ฃผ์ธ์
* Storybook์์ parameters.docs.description.story ๋ณด๋ค JSDoc์ ๊ถ์ฅํฉ๋๋ค.
* @see {https://storybook.js.org/docs/api/doc-blocks/doc-block-description#writing-descriptions}
*/
export const Default: Story = {
args: { props1: "option1" },
};
Component.tsx.hbs
import classNames from "classnames/bind";
import type { {{pascalCase name}}Props } from "@design-system/ui/{{path}}/{{pascalCase name}}/type";
import style from "@design-system/ui/{{path}}/{{pascalCase name}}/index.module.scss";
const cx = classNames.bind(style);
function {{pascalCase name}}(props: {{pascalCase name}}Props) {
return <div />;
}
export default {{pascalCase name}};
Type.d.ts.hbs
export interface {{pascalCase name}}Props {}
๋ง์น๋ฉฐ
๋๋ฃ๋ค๊ณผ ๊ฐ๋ฐํ ๋, ์ด๋ ์ ๋ ๊ท์น์ ๋ง๋ค๋ฉฐ ๋ฐ๋ณต๋๋ ์ ๋ฌด๋ฅผ ์๋ํํ๊ณ ํน์ ๋ถ๋ถ๋ค์ ํ ํ๋ฆฟํ ํ๋ ๊ฒ์ด ์์ฐ์ฑ์ ๋์ฌ์ค ์ ์๋ค๊ณ ์๊ฐํฉ๋๋ค. ์ด plop ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฟ๋ง ์๋๋ผ ๋ค์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ ํ์ฉํ๋ฉด ์์ฐ์ฑ์ ๋ง์ด ๋์ฌ์ค ์ ์์ ๊ฒ ๊ฐ์ต๋๋ค.
๋ ์ข์ plop ํ ํ๋ฆฟ์ ๊ตฌ์ฑํ์๊ฑฐ๋ ๋ค๋ฅธ ์ข์ ๋ฐฉ๋ฒ์ด ์์ผ์๋ค๋ฉด ๋ค์ํ ํผ๋๋ฐฑ์ผ๋ก ๊ณต์ ํด์ฃผ์๋ฉด ๊ฐ์ฌํ๊ฒ ์ต๋๋ค ๐
๋๊ธ