바벨 플러그인을 만들어보며 JS AST(Abstract syntax trees)를 배워봅시다.
해당 글의 번역입니다 : https://dev.to/viveknayyar/revealing-the-magic-of-ast-by-writing-babel-plugins-1h01
💡 동기
이 블로그를 작성하는 동기는
추상 구문 트리가 무엇이며 우리가 매일 사용하는 대부분의 도구에서 어떻게 중요한 역할을 하는지
모든 사람이 쉽게 이해할 수 있도록 하는 것입니다.
🤔 AST가 뭘까요?
모든 새로운 개념과 마찬가지로 구체적인 정의부터 시작하겠습니다.
위키피디아에 따르면 :
추상 구문 트리는 프로그래밍 언어로 작성된 소스 코드의 트리 표현입니다.
트리의 각 노드는 소스 코드에서 발생하는 구조를 나타냅니다.
const a = 5 + 3;
이것은 매우 간단한 변수 할당 및 두 숫자의 덧셈입니다.
이 간단한 작업은 토큰화(Tokenization) 및 구문 분석(Parsing) 과정을 거칩니다.
🕹️ 토큰화 (어휘 분석)
토큰화(Tokenization) 또는 어휘 분석(Lexical analysis)은
함수가 코드를 문자열로 읽고 토큰 목록으로 분할하는 단계입니다.
interface Token {
type: string,
value: string
}
🧵 구문 분석(Parsing / Syntatic Analysis)
토큰화 작업의 결과는 토큰 배열을 제공하며,
우리는 AST 파서(babylon 또는 acorn 또는 espree)를 통해 이를 전달합니다.
이 파서는 AST 노드 사이에 종속성을 설정하여 AST 노드 트리로 변환합니다.
우리가 작성한 매우 간단한 코드는 추상 구문 트리(Abstract Syntax Tree)라고 하는 노드 트리로 변환됩니다.
그리고 그 전체 트리는 다음과 같은 방식으로 json으로 표현됩니다.
{
"type": "VariableDeclaration",
"declarations": [{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "BinaryExpression",
"left": {
"type": "Literal",
"value": 5
},
"operator": "+",
"right": {
"type": "Literal",
"value": 3
}
}
}],
"kind": "const"
}
이 json 객체에서 type이라는 이름의 매개변수를 확인합니다.
이를 AST 노드 타입이라고 합니다.
여러 타입의 AST 노드가 존재하며
babel의 경우 다음을 참조할 수 있습니다.
espree 파서(eslint가 사용하는 것)의 경우 여기에서 참조할 수 있습니다.
Eslint AST Node Types
이제 더 이상 시간을 낭비하지 않고 첫 번째 babel 플러그인을 작성합니다.
이 플러그인은 코드의 모든 debugger 문을 제거합니다.
📕 Babel Plugin - Remove Debugger
function test() {
const a = 5;
debugger;
const b = 6;
}
바벨 플러그인 작성하기
1단계: 타겟팅하려는 AST 노드 타입을 식별합니다.
2단계: 편집기를 실행하고 새 파일을 만듭니다.
이름을 removeDebugger.js로 지정하겠습니다.
이것은 플러그인 파일이 될 것입니다.
module.exports = function(babel) {
return {
name: "remove-debugger-plugin", // this is optional
visitor: {
}
};
};
키 visitor가 포함된 다른 중첩 개체가 포함된 개체를 반환합니다.
visitor 패턴 때문에 방문자라는 이름이 붙여졌습니다.
3단계: 대상으로 지정하려는 노드 타입은 DebuggerStatement 입니다.
module.exports = function(babel) {
return {
name: "remove-debugger-plugin", // this is optional
visitor: {
DebuggerStatement: function(path) {
}
}
};
};
우리가 목표로 삼고자 하는 모든 노드는 visitor object 내부에 DebuggerStatement 키가 있어야 합니다.
4단계: 이제 이 babel 플러그인 구현을 위해 남은 유일한 단계는 디버거 문 노드를 제거하는 것입니다.
다음과 같이 수행합니다.
module.exports = function(babel) {
return {
name: "remove-debugger-plugin", // this is optional
visitor: {
DebuggerStatement: function(path) {
path.remove();
}
}
};
};
이제 우리의 첫 babel 플러그인이 완성되었습니다!
이 babel 플러그인은 AST에서 노드를 제거하여 AST를 조작하는 방법을 설명합니다.
다음 플러그인에서는 기존 노드를 편집하고 다른 노드로 변환하는 방법을 설명합니다.
📕 Babel Plugin - Alert To Console
해당 코드를
function test() {
const a = 5;
alert(a);
}
다음과 같이 변경합니다.
function test() {
const a = 5;
console.warn(a);
}
1단계: 타겟팅하려는 AST 노드 타입을 식별합니다.
AST 탐색기로 이동하여 from 코드를 복사하여 붙여넣고 alert을 클릭합니다.
오른쪽의 노드 타입이 강조 표시됩니다.
대상으로 지정할 노드 타입이 CallExpression이네요.
모든 함수 호출은 CallExpression이고
객체 함수 호출(메서드 호출)은 MemberExpression입니다.
따라서 alert는 CallExpression이고
console.warn은 MemberExpression입니다.
MemberExpression에는 항상 Object(console)와 property(warn)가 있습니다.
2단계: 다시 한 번 편집기를 실행하고 새 파일을 만듭니다.
이름을 convertAlertToConsole.js로 지정하겠습니다.
이전처럼 스켈레톤 코드로 플러그인을 시작합니다.
module.exports = function(babel) {
const t = babel.types;
return {
name: "convert-alert-to-console", // this is optional
visitor: {
}
};
};
3단계: 이제 대상으로 지정해야 하는 노드가 CallExpression이라는 것을 알았으므로 코드를 작성해 보겠습니다.
module.exports = function(babel) {
const t = babel.types;
return {
name: "convert-alert-to-console", // this is optional
visitor: {
CallExpression: function(path)
}
}
};
};
4단계: if 조건을 지정합니다.
다른 모든 함수 호출을 대상으로 지정하지 않고 alert라는 이름의 호출 표현식만 대상으로 지정하기 위해서입니다.
module.exports = function(babel) {
const t = babel.types;
return {
name: "convert-alert-to-console", // this is optional
visitor: {
CallExpression: function(path) {
if (path.node.callee.name === "alert") {
}
}
}
};
};
이제 남은 부분은 무엇으로 교체할지 알아내는 것뿐입니다.
5 단계: AST 탐색기로 돌아가 이번에는 to 코드를 복사하여 붙여넣습니다.
module.exports = function(babel) {
const t = babel.types;
return {
name: "convert-alert-to-console", // this is optional
visitor: {
CallExpression: function(path) {
if (path.node.callee.name === "alert") {
const args = path.node.arguments;
path.replaceWith(
t.callExpression(
t.memberExpression(t.identifier("console"), t.identifier("warn")),
args
)
);
}
}
}
};
};
이게 끝입니다. 우리는 우리의 두번째 플러그인을 작성했습니다. 쉽지 않나요?
📕 보너스 플러그인 - 리액트 앱에서 data-test-id 제거
1단계: 타겟팅하려는 AST 노드 타입을 식별합니다.
AST 탐색기로 이동하여 코드에서 복사하여 붙여넣고 data-test-id를 클릭합니다.
오른쪽의 노드 타입이 강조 표시됩니다.
대상으로 할 노드 타입이 JSXAttribute라는 것을 알 수 있습니다.
2단계: 편집기를 실행하고 새 파일을 만듭니다.
이름을 removeDataAttribute.js로 지정하겠습니다.
이전처럼 스켈레톤 코드로 시작합니다.
module.exports = function(babel) {
return {
name: "remove-date-test-id", // this is optional
visitor: {
}
};
};
3단계: 타겟 노드가 JSXArrtibute임을 알았으므로, 코드를 작성합니다.
module.exports = function(babel) {
return {
name: "remove-date-test-id", // this is optional
visitor: {
JSXAttribute: function(path) {
}
}
};
};
4단계: 이 jsx 속성 노드를 제거합니다.
module.exports = function(babel) {
return {
name: "remove-date-test-id", // this is optional
visitor: {
JSXAttribute: function(path) {
if(path.node.name.name === "data-test-id") {
path.remove();
}
}
}
};
};
이게 끝입니다. 우리는 다른 플러그인을 얻었습니다. 🥳 🥳 🥳
Github Repo: https://github.com/vivek12345/webcamp-zagreb-demo
🍬 결론
🔗 References
- Leveling up parsing game by Vaidehi Joshi
- AST explorer by Felix Kling
- Babel handbook
- Step by step guide to write a babel transformation
- Magical land of AST's
'FrontEnd' 카테고리의 다른 글
[React] React.cloneElement 사용 사례 (0) | 2022.09.15 |
---|---|
리액트 디자인 패턴 : 컴파운드 컴포넌트 패턴 [Compound Component Pattern] 2 (0) | 2022.09.15 |
[typescript] d.ts 파일을 js 프로젝트에서 사용할 수 있을까? (3) | 2022.09.14 |
Redux Toolkit : Usage Guide(사용 가이드) (0) | 2022.09.13 |
Redux Toolkit : Immer와 함께 사용하기 (0) | 2022.09.12 |