์ฝ๋๋ ์ด๋ป๊ฒ ํธ๋ฆฌ๊ฐ ๋๋๊ฐ โ AST์ ํ์ฑ, ๊ทธ๋ฆฌ๊ณ JSXยทTS ํ์ฑํ๊ธฐ
์์ ๊ธ์์ ์ฐํด๋ฆญ์ผ๋ก ์ปดํฌ๋ํธ ์์ค๋ก ์ ํํ๋ ๋๊ตฌ๋ฅผ ๋ง๋ ์ด์ผ๊ธฐ๋ฅผ ํ๋ค. ๊ทธ ๋๊ตฌ์ ์ฌ์ฅ์ "์ฝ๋๋ฅผ ์ฝ์ด์ ์ํ๋ ์์น๋ฅผ ์ฐพ์๋ด๋" ๋ถ๋ถ, ์ฆ 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๋ฅผ ๋ถ์ํด ๊ทธ๋ํ๋ฅผ ๋ง๋ฆ
"์ฝ๋๋ฅผ ๋ค๋ฃจ๋ ์ฝ๋"๋ฅผ ์ฐ๊ณ ์ถ์ ๋, ์ ๊ท์์ผ๋ก ์จ๋ฆํ๋ ๋์ ํ์์๊ฒ ๊ตฌ์กฐ๋ฅผ ๋ฌผ์ด๋ณด๋ฉด ๋๋ค๋ ๊ฒ. ์ด๊ฒ ์ด๋ฒ์ ์ป์ ๊ฐ์ฅ ํฐ ๋ฌด๊ธฐ์๋ค.
- ์ด ์๋ฆฌ๋ฅผ ์ค์ ๋ก ์ด ํ๋ก์ ํธ: react-code-locator
- ํ์/๋๊ตฌ: acorn ยท estree-walker ยท MagicString