์ด ๊ธ๋ก ์ป์ ์ ์๋ ์ ๋ณด
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 ํ ํ๋ฆฟ์ ๊ตฌ์ฑํ์๊ฑฐ๋ ๋ค๋ฅธ ์ข์ ๋ฐฉ๋ฒ์ด ์์ผ์๋ค๋ฉด ๋ค์ํ ํผ๋๋ฐฑ์ผ๋ก ๊ณต์ ํด์ฃผ์๋ฉด ๊ฐ์ฌํ๊ฒ ์ต๋๋ค ๐
๋๊ธ