๐ŸŒฑ <groot/>
์ฝ”๋“œ๋Š” ์–ด๋–ป๊ฒŒ ํŠธ๋ฆฌ๊ฐ€ ๋˜๋Š”๊ฐ€ โ€” AST์™€ ํŒŒ์‹ฑ, ๊ทธ๋ฆฌ๊ณ  JSXยทTS ํŒŒ์‹ฑํ•˜๊ธฐ | Groot's Tech Blog
๐Ÿ‹JavaScript

์ฝ”๋“œ๋Š” ์–ด๋–ป๊ฒŒ ํŠธ๋ฆฌ๊ฐ€ ๋˜๋Š”๊ฐ€ โ€” AST์™€ ํŒŒ์‹ฑ, ๊ทธ๋ฆฌ๊ณ  JSXยทTS ํŒŒ์‹ฑํ•˜๊ธฐ

2026.07.02ยท5๋ถ„ ์ฝ๊ธฐ

์•ž์„  ๊ธ€์—์„œ ์šฐํด๋ฆญ์œผ๋กœ ์ปดํฌ๋„ŒํŠธ ์†Œ์Šค๋กœ ์ ํ”„ํ•˜๋Š” ๋„๊ตฌ๋ฅผ ๋งŒ๋“  ์ด์•ผ๊ธฐ๋ฅผ ํ–ˆ๋‹ค. ๊ทธ ๋„๊ตฌ์˜ ์‹ฌ์žฅ์€ "์ฝ”๋“œ๋ฅผ ์ฝ์–ด์„œ ์›ํ•˜๋Š” ์œ„์น˜๋ฅผ ์ฐพ์•„๋‚ด๋Š”" ๋ถ€๋ถ„, ์ฆ‰ AST ํŒŒ์‹ฑ์ด๋‹ค. ๋งŒ๋“ค๋ฉด์„œ AST๋ฅผ ์ œ๋Œ€๋กœ ํŒŒ๊ฒŒ ๋๋Š”๋ฐ, ์•Œ๊ณ  ๋‚˜๋‹ˆ ESLintยทBabelยทPrettierยท์ฝ”๋“œ๋ชจ๋“œ๊ฐ€ ์ „๋ถ€ ๊ฐ™์€ ์›๋ฆฌ ์œ„์— ์„œ ์žˆ๋‹ค๋Š” ๊ฑธ ๊นจ๋‹ฌ์•˜๋‹ค. ๊ทธ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•œ๋‹ค.

AST๊ฐ€ ๋ญ”๊ฐ€

์šฐ๋ฆฌ๊ฐ€ ์“ฐ๋Š” ์†Œ์Šค ์ฝ”๋“œ๋Š” ์ปดํ“จํ„ฐ ์ž…์žฅ์—์„  ๊ทธ๋ƒฅ ๊ธ€์ž์˜ ๋‚˜์—ด(๋ฌธ์ž์—ด) ์ด๋‹ค. function์ด๋ผ๋Š” ๋‹ค์„ฏ ๊ธ€์ž๊ฐ€ ํ•จ์ˆ˜ ์„ ์–ธ์˜ ์‹œ์ž‘์ด๋ผ๋Š” ๊ฑธ ์ปดํ“จํ„ฐ๋Š” ์ฒ˜์Œ๋ถ€ํ„ฐ ์•„๋Š” ๊ฒŒ ์•„๋‹ˆ๋‹ค. ์ด ๋ฌธ์ž์—ด์„ ์˜๋ฏธ ๋‹จ์œ„๋กœ ๊ตฌ์กฐํ™”ํ•œ ํŠธ๋ฆฌ๊ฐ€ ๋ฐ”๋กœ AST(Abstract Syntax Tree, ์ถ”์ƒ ๊ตฌ๋ฌธ ํŠธ๋ฆฌ) ๋‹ค.

const x = 1 + 2;

์ด ํ•œ ์ค„์€ ๋Œ€์ถฉ ์ด๋Ÿฐ ํŠธ๋ฆฌ๊ฐ€ ๋œ๋‹ค.

VariableDeclaration (const)
โ””โ”€ VariableDeclarator
   โ”œโ”€ Identifier         name: x
   โ””โ”€ BinaryExpression   operator: +
      โ”œโ”€ Literal         value: 1
      โ””โ”€ Literal         value: 2

1 + 2๊ฐ€ "๋ง์…ˆ์ด๊ณ , ์™ผ์ชฝ์€ 1, ์˜ค๋ฅธ์ชฝ์€ 2"๋ผ๋Š” ๊ด€๊ณ„๊ฐ€ ํŠธ๋ฆฌ๋กœ ํ‘œํ˜„๋œ๋‹ค. ์ด๋ ‡๊ฒŒ ๊ตฌ์กฐ๊ฐ€ ์žกํžˆ๋ฉด ์ฝ”๋“œ๋ฅผ ํ”„๋กœ๊ทธ๋žจ์œผ๋กœ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๋‹ค. ๋ฆฐํ„ฐ๋Š” ์ด ํŠธ๋ฆฌ๋ฅผ ๋ณด๊ณ  ๊ทœ์น™์„ ๊ฒ€์‚ฌํ•˜๊ณ , ๋ฒˆ๋“ค๋Ÿฌ๋Š” import๋ฅผ ์ฐพ๊ณ , ํฌ๋งคํ„ฐ๋Š” ํŠธ๋ฆฌ๋ฅผ ๋‹ค์‹œ ์˜ˆ์˜๊ฒŒ ์ถœ๋ ฅํ•œ๋‹ค.

ํŒŒ์‹ฑ์€ ๋‘ ๋‹จ๊ณ„๋‹ค: ํ† ํฐํ™” โ†’ ํŠธ๋ฆฌ ๋งŒ๋“ค๊ธฐ

๋ฌธ์ž์—ด์ด ํŠธ๋ฆฌ๊ฐ€ ๋˜๊ธฐ๊นŒ์ง€๋Š” ๋ณดํ†ต ๋‘ ๋‹จ๊ณ„๋ฅผ ๊ฑฐ์นœ๋‹ค.

1. ๋ ‰์‹ฑ(Lexing / ํ† ํฐํ™”) โ€” ๋ฌธ์ž์—ด์„ ์˜๋ฏธ ์žˆ๋Š” ์ตœ์†Œ ๋‹จ์œ„์ธ ํ† ํฐ์œผ๋กœ ์ชผ๊ฐ ๋‹ค.

const x = 1 + 2;
โ†’ [const] [x] [=] [1] [+] [2] [;]

2. ํŒŒ์‹ฑ(Parsing) โ€” ํ† ํฐ๋“ค์„ ๋ฌธ๋ฒ• ๊ทœ์น™์— ๋”ฐ๋ผ ํŠธ๋ฆฌ๋กœ ์กฐ๋ฆฝํ•œ๋‹ค. [1] [+] [2]๋ฅผ ๋ณด๋ฉด "์ด๊ฑด BinaryExpression์ด๊ตฌ๋‚˜" ํ•˜๊ณ  ๋…ธ๋“œ๋ฅผ ๋งŒ๋“ ๋‹ค.

JavaScript ์ง„์˜์—์„œ ์ด ์ผ์„ ํ•˜๋Š” ๋Œ€ํ‘œ์ ์ธ ํŒŒ์„œ๊ฐ€ acorn ์ด๋‹ค. ์ž‘๊ณ  ๋น ๋ฅด๊ณ , ๊ฒฐ๊ณผ AST๊ฐ€ ESTree๋ผ๋Š” ํ‘œ์ค€ ์ŠคํŽ™์„ ๋”ฐ๋ฅธ๋‹ค. ESLint์˜ ํŒŒ์„œ(espree)๋„ acorn ๊ธฐ๋ฐ˜์ด๋ผ ESTree ํ˜ธํ™˜์ด๋‹ค. ๋‹ค๋งŒ Babel์€ ์ž์ฒด AST ํฌ๋งท์„ ์“ฐ๋Š”๋ฐ, ESTree์™€ ๋น„์Šทํ•˜์ง€๋งŒ ํ˜ธํ™˜๋˜์ง„ ์•Š๋Š”๋‹ค (์˜ˆ: ๋ฌธ์ž์—ด์ด Literal์ด ์•„๋‹ˆ๋ผ StringLiteral ๋…ธ๋“œ๋‹ค).

์‹ค์ œ๋กœ ํŒŒ์‹ฑํ•ด๋ณด๊ธฐ

๊ฐ„๋‹จํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ํŒŒ์„œ์— ๋„ฃ์–ด๋ณด์ž.

function Hello({ name }) {
  return <h1 className="t">Hi {name}</h1>;
}

acorn(+ JSXยทTS ํ”Œ๋Ÿฌ๊ทธ์ธ)์œผ๋กœ ํŒŒ์‹ฑํ•˜๋ฉด ์ด๋Ÿฐ ํŠธ๋ฆฌ๊ฐ€ ๋‚˜์˜จ๋‹ค. ๊ด„ํ˜ธ ์•ˆ์€ (์ค„:์นธ) ์œ„์น˜๋‹ค.

Program
โ””โ”€ FunctionDeclaration          (1:0)
   โ”œโ”€ Identifier   name=Hello
   โ”œโ”€ ObjectPattern             ({ name } ๊ตฌ์กฐ๋ถ„ํ•ด)
   โ””โ”€ BlockStatement
      โ””โ”€ ReturnStatement        (2:2)
         โ””โ”€ JSXElement          (2:9)
            โ”œโ”€ JSXOpeningElement
            โ”‚  โ”œโ”€ JSXIdentifier   name=h1
            โ”‚  โ””โ”€ JSXAttribute    className="t"
            โ”œโ”€ JSXText            "Hi "
            โ”œโ”€ JSXExpressionContainer
            โ”‚  โ””โ”€ Identifier      name=name
            โ””โ”€ JSXClosingElement  name=h1

์—ฌ๊ธฐ์„œ ์ฃผ๋ชฉํ•  ๊ฒƒ:

  • ๋ชจ๋“  ๋…ธ๋“œ์— type ์ด ์žˆ๋‹ค. (FunctionDeclaration, JSXElementโ€ฆ)
  • locations: true ์˜ต์…˜์„ ์ฃผ๋ฉด ๊ฐ ๋…ธ๋“œ๊ฐ€ loc(์ค„/์นธ) ๊ณผ start/end (๋ฌธ์ž ์˜คํ”„์…‹) ์œ„์น˜๋ฅผ ๋“ค๊ณ  ์žˆ๋‹ค. โ†’ "์ด ์š”์†Œ๊ฐ€ ์†Œ์Šค์˜ ์–ด๋””์ธ์ง€"๋ฅผ ์•„๋Š” ๊ทผ๊ฑฐ๊ฐ€ ๋ฐ”๋กœ ์ด๊ฒƒ์ด๋‹ค.
  • <h1>์€ JSXElement, ๊ทธ ์—ฌ๋Š” ํƒœ๊ทธ๋Š” JSXOpeningElement, ์•ˆ์˜ {name}์€ JSXExpressionContainer๋กœ ํ‘œํ˜„๋œ๋‹ค.

๋‚ด๊ฐ€ ๋งŒ๋“  ๋„๊ตฌ๋Š” ์ด ํŠธ๋ฆฌ๋ฅผ ํ›‘๋‹ค๊ฐ€ JSXElement๋ฅผ ๋งŒ๋‚˜๋ฉด "๋„ˆ๋Š” 2:9์—์„œ ์™”๊ตฌ๋‚˜"๋ฅผ ๊ธฐ์–ตํ•ด๋’€๋‹ค๊ฐ€ ๊ทธ ์œ„์น˜ ์ •๋ณด๋ฅผ ์ฝ”๋“œ์— ์‹ฌ๋Š”๋‹ค.

๋ฌธ์ œ: ์ˆœ์ˆ˜ JS ํŒŒ์„œ๋Š” JSX๋„ TS๋„ ๋ชจ๋ฅธ๋‹ค

์—ฌ๊ธฐ์„œ ์ฒซ ๋ฒˆ์งธ ๋ฒฝ์„ ๋งŒ๋‚œ๋‹ค. <h1> ๊ฐ™์€ JSX๋‚˜ : string ๊ฐ™์€ TypeScript ๋ฌธ๋ฒ•์€ ํ‘œ์ค€ JavaScript ๋ฌธ๋ฒ•์ด ์•„๋‹ˆ๋‹ค. acorn๋งŒ์œผ๋กœ๋Š” ํŒŒ์‹ฑํ•˜๋‹ค ์—๋Ÿฌ๊ฐ€ ๋‚œ๋‹ค.

acorn์€ ์ด ๋ฌธ์ œ๋ฅผ ํ”Œ๋Ÿฌ๊ทธ์ธ์œผ๋กœ ํŒŒ์„œ๋ฅผ ํ™•์žฅํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ํ‘ผ๋‹ค.

import * as acorn from "acorn";
import acornJsx from "acorn-jsx";
import acornTs from "acorn-typescript";

// TypeScript ํ”Œ๋Ÿฌ๊ทธ์ธ์ด JSX ํ”Œ๋Ÿฌ๊ทธ์ธ๋ณด๋‹ค ๋จผ์ € ์™€์•ผ ํ•œ๋‹ค
const ParserTSX = acorn.Parser.extend(acornTs(), acornJsx());

Parser.extend(...)๋กœ ๊ธฐ์กด ํŒŒ์„œ์— JSXยทTS ๋ฌธ๋ฒ•์„ ์ดํ•ดํ•˜๋Š” ๋Šฅ๋ ฅ์„ ์–น์€ ์ƒˆ ํŒŒ์„œ๋ฅผ ๋งŒ๋“ ๋‹ค. ์ˆœ์„œ๋„ ์ค‘์š”ํ•˜๋‹ค. TS ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ๋จผ์ € ์–น์–ด์•ผ ํƒ€์ž… ๋ฌธ๋ฒ•์„ ์ œ๋Œ€๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค.

ํ•จ์ •: .tsx์˜ <T,> ์ œ๋„ค๋ฆญ

JSX์™€ TS ์ œ๋„ค๋ฆญ์€ ๋ฌธ๋ฒ•์ด ์ถฉ๋Œํ•œ๋‹ค. .tsx์—์„œ

const identity = <T,>(x: T) => x;

์˜ <T,>๋ฅผ JSX ํŒŒ์„œ๋Š” JSX ํƒœ๊ทธ์˜ ์‹œ์ž‘์œผ๋กœ ์˜คํ•ดํ•œ๋‹ค. ๊ทธ๋ž˜์„œ ๋‚˜๋Š” .ts ํŒŒ์ผ์€ ์•„์˜ˆ JSX ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ๋นผ๊ณ  ํŒŒ์‹ฑํ•˜๋„๋ก ํŒŒ์„œ๋ฅผ ๋‘ ๋ฒŒ๋กœ ๋‚˜๋ˆด๋‹ค. (.ts์—” JSX๊ฐ€ ์˜ฌ ์ˆ˜ ์—†์œผ๋‹ˆ ์•ˆ์ „ํ•˜๊ณ , .tsx๋งŒ JSX๋ฅผ ํ—ˆ์šฉํ•œ๋‹ค.)

const ParserTS = acorn.Parser.extend(acornTs());          // .ts ์ „์šฉ
const JSX_FILE_RE = /\.(jsx?|tsx)$/;
const getParser = (file: string) =>
  JSX_FILE_RE.test(file) ? ParserTSX : ParserTS;

ํŠธ๋ฆฌ ์ˆœํšŒ โ€” ์›ํ•˜๋Š” ๋…ธ๋“œ ์ฐพ๊ธฐ

ํŠธ๋ฆฌ๋ฅผ ๋งŒ๋“ค์—ˆ์œผ๋ฉด, ์ด์ œ ์›ํ•˜๋Š” ๋…ธ๋“œ(JSX ์š”์†Œ, ์ปดํฌ๋„ŒํŠธ ์„ ์–ธ)๋ฅผ ์ฐพ์•„ ๋Œ์•„๋‹ค๋…€์•ผ ํ•œ๋‹ค. ์ด๊ฑธ ํŠธ๋ฆฌ ์ˆœํšŒ(traversal) ๋ผ๊ณ  ํ•˜๊ณ , estree-walker ๊ฐ™์€ ๋„๊ตฌ๋ฅผ ์“ฐ๋ฉด ํŽธํ•˜๋‹ค.

import { walk } from "estree-walker";

const parentStack: any[] = [];

walk(ast, {
  enter(node) {
    const parent = parentStack[parentStack.length - 1] ?? null;

    if (node.type === "JSXElement") {
      // ์ด JSX ์š”์†Œ์˜ ์œ„์น˜(node.loc.start)๋ฅผ ๊ธฐ๋ก
    }
    if (node.type === "FunctionDeclaration" && /^[A-Z]/.test(node.id?.name)) {
      // ๋Œ€๋ฌธ์ž๋กœ ์‹œ์ž‘ โ†’ ์ปดํฌ๋„ŒํŠธ๋กœ ๊ฐ„์ฃผ
    }

    parentStack.push(node);
  },
  leave() {
    parentStack.pop();
  },
});

enter๋Š” ๋…ธ๋“œ์— ๋“ค์–ด๊ฐˆ ๋•Œ, leave๋Š” ๋‚˜์˜ฌ ๋•Œ ํ˜ธ์ถœ๋œ๋‹ค. ์ด๋•Œ ๋ถ€๋ชจ ์Šคํƒ์„ ์ง์ ‘ ๊ด€๋ฆฌํ•˜๋ฉด "์ง€๊ธˆ ์ด ๋…ธ๋“œ๊ฐ€ ์–ด๋–ค ๋ฌธ๋งฅ ์•ˆ์— ์žˆ๋Š”์ง€"๋ฅผ ์•Œ ์ˆ˜ ์žˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด const Button = () => ...๊ฐ€ ์ตœ์ƒ์œ„์ธ์ง€, ๋‹ค๋ฅธ ๋ธ”๋ก ์•ˆ์— ์žˆ๋Š”์ง€, export๋กœ ๊ฐ์‹ธ์กŒ๋Š”์ง€๋ฅผ ๋ถ€๋ชจ/์กฐ๋ถ€๋ชจ ๋…ธ๋“œ๋กœ ํŒ๋‹จํ•œ๋‹ค. ์ปดํฌ๋„ŒํŠธ์ธ์ง€ ์•„๋‹Œ์ง€๋ฅผ ์ •ํ™•ํžˆ ๊ฐ€๋ฆฌ๋ ค๋ฉด ์ด ๋ฌธ๋งฅ ํŒ๋‹จ์ด ๊ผญ ํ•„์š”ํ–ˆ๋‹ค.

์œ„์น˜๋Š” ์•Œ์•˜๊ณ  โ€” ์ฝ”๋“œ๋Š” ์–ด๋–ป๊ฒŒ ๊ณ ์น˜๋‚˜

์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ๊ฒฐ์ •์„ ํ–ˆ๋‹ค. AST๋ฅผ ๋‹ค์‹œ ์ฝ”๋“œ๋กœ ๋ฝ‘์ง€ ์•Š๋Š”๋‹ค.

ํŒŒ์‹ฑํ•œ AST๋ฅผ ์ˆ˜์ •ํ•œ ๋’ค ์ฝ”๋“œ ์ œ๋„ˆ๋ ˆ์ดํ„ฐ๋กœ ๋‹ค์‹œ ์ถœ๋ ฅํ•˜๋ฉด, ์›๋ณธ์˜ ๊ณต๋ฐฑยท๋”ฐ์˜ดํ‘œ ์Šคํƒ€์ผยท์ฃผ์„ ๊ฐ™์€ ๊ฒŒ ๋ฏธ๋ฌ˜ํ•˜๊ฒŒ ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ๋‹ค. ๋‚˜๋Š” ์œ„์น˜๋งŒ ์•Œ๋ฉด ๋์œผ๋ฏ€๋กœ, ํŒŒ์‹ฑ์€ ์ขŒํ‘œ๋ฅผ ์–ป๋Š” ์šฉ๋„๋กœ๋งŒ ์“ฐ๊ณ  ์‹ค์ œ ์ˆ˜์ •์€ ์›๋ณธ ๋ฌธ์ž์—ด์— ์ง์ ‘ ํ–ˆ๋‹ค.

์ด๋•Œ ์“ด ๊ฒŒ MagicString ์ด๋‹ค. ๋ฌธ์ž์—ด์˜ ํŠน์ • ์˜คํ”„์…‹์— ์‚ฝ์ž…ํ•  ํ…์ŠคํŠธ๋ฅผ ๊ธฐ๋ก๋งŒ ํ•ด๋‘๋Š” ๋ฐฉ์‹์ด๋ผ ์‚ฝ์ž…์€ ์‚ฌ์‹ค์ƒ ์ƒ์ˆ˜ ๋น„์šฉ์ด๊ณ , ์ตœ์ข… ๋ฌธ์ž์—ด ์ƒ์„ฑ์€ ๋งˆ์ง€๋ง‰์— ํ•œ ๋ฒˆ๋งŒ ํ•œ๋‹ค. ์†Œ์Šค๋งต๋„ ๋งŒ๋“ค์–ด์ค€๋‹ค.

import MagicString from "magic-string";

const s = new MagicString(code);
// node.end: ๊ทธ ๋…ธ๋“œ๊ฐ€ ๋๋‚˜๋Š” ๋ฌธ์ž ์˜คํ”„์…‹
s.appendLeft(node.end, `\n${name}.__componentSourceLoc = "App.tsx:8:1";`);
return { code: s.toString(), map: s.generateMap({ hires: true }) };

์—ฌ๊ธฐ์„œ loc(์ค„/์นธ)๊ณผ start/end(๋ฌธ์ž ์˜คํ”„์…‹)์˜ ์ฐจ์ด๊ฐ€ ์ค‘์š”ํ•˜๋‹ค. ์‚ฌ๋žŒ์—๊ฒŒ ๋ณด์—ฌ์ค„ ์œ„์น˜๋Š” loc(์ค„:์นธ), ๋ฌธ์ž์—ด์„ ์ž๋ฅด๊ณ  ๋ถ™์ผ ๋•Œ ์“ฐ๋Š” ์ขŒํ‘œ๋Š” start/end(์˜คํ”„์…‹)๋‹ค. ๋‘˜ ๋‹ค ํŒŒ์„œ๊ฐ€ ์ค€๋‹ค.

์•Œ๊ณ  ๋‚˜๋‹ˆ ์„ธ์ƒ์ด ๋‹ฌ๋ผ ๋ณด์ธ๋‹ค

AST๋ฅผ ์ดํ•ดํ•˜๊ณ  ๋‚˜๋‹ˆ, ๊ทธ๋™์•ˆ ๋งˆ๋ฒ•์ฒ˜๋Ÿผ ๋ณด์ด๋˜ ๋„๊ตฌ๋“ค์ด ์ „๋ถ€ ๊ฐ™์€ ๋ผˆ๋Œ€ ์œ„์— ์žˆ๋‹ค๋Š” ๊ฒŒ ๋ณด์˜€๋‹ค.

  • ESLint ๊ทœ์น™ = AST๋ฅผ ์ˆœํšŒํ•˜๋ฉฐ ํŠน์ • ํŒจํ„ด์„ ๊ฒ€์‚ฌ
  • Babel ํ”Œ๋Ÿฌ๊ทธ์ธ / ์ฝ”๋“œ๋ชจ๋“œ = AST๋ฅผ ์ˆœํšŒยท์ˆ˜์ •ํ•˜๊ณ  ๋‹ค์‹œ ์ฝ”๋“œ๋กœ ์ถœ๋ ฅ
  • Prettier = AST๋ฅผ ํ‘œ์ค€ ์Šคํƒ€์ผ๋กœ ์žฌ์ถœ๋ ฅ
  • ๋ฒˆ๋“ค๋Ÿฌ = AST์—์„œ import/export๋ฅผ ๋ถ„์„ํ•ด ๊ทธ๋ž˜ํ”„๋ฅผ ๋งŒ๋“ฆ

"์ฝ”๋“œ๋ฅผ ๋‹ค๋ฃจ๋Š” ์ฝ”๋“œ"๋ฅผ ์“ฐ๊ณ  ์‹ถ์„ ๋•Œ, ์ •๊ทœ์‹์œผ๋กœ ์”จ๋ฆ„ํ•˜๋Š” ๋Œ€์‹  ํŒŒ์„œ์—๊ฒŒ ๊ตฌ์กฐ๋ฅผ ๋ฌผ์–ด๋ณด๋ฉด ๋œ๋‹ค๋Š” ๊ฒƒ. ์ด๊ฒŒ ์ด๋ฒˆ์— ์–ป์€ ๊ฐ€์žฅ ํฐ ๋ฌด๊ธฐ์˜€๋‹ค.

โ† ๋ชฉ๋ก์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ
ยฉ 2026 ยท Groot's Tech Blog. Built with Gatsby ๐ŸŒฑ