ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JavaScript | Babel Plugin 제작하기
    JavaScript/JavaScript 2020. 4. 22. 12:35

    Babel Plugin 제작하기

    바벨은 프리셋과 플러그인을 누구나 제작할 수 있는 API를 제공
    바벨 플러그인을 제작해 보면서 바벨의 내부 동작 구조를 이해하는 것이 목표


    AST 구조 확인

    바벨은 문자열로 입력되는 코드를 AST(abstract syntax tree)라를 구조체로 만들어서 처리하며
    플러그인에서는 AST를 기반으로 코드를 변경함

    AST의 구조는 https://astexplorer.net/ 사이트에서 확인 가능

    astexplorer 이용하기

    astextplorer 사이트에서 const v1 = a + b; 코드의 AST 확인
    바벨은 babylon이라는 파서를 이용해서 AST를 만들기 때문에
    파서 목록에서 babylon을 선택

    {
      "type": "File",
      "start": 0,
      "end": 17,
      "loc": {
        "start": {
          "line": 1,
          "column": 0
        },
        "end": {
          "line": 1,
          "column": 17
        }
      },
      "errors": [],
      "program": {
        "type": "Program",
        "start": 0,
        "end": 17,
        "loc": {
          "start": {
            "line": 1,
            "column": 0
          },
          "end": {
            "line": 1,
            "column": 17
          }
        },
        "sourceType": "module",
        "interpreter": null,
        "body": [
          {
            "type": "VariableDeclaration",
            "start": 0,
            "end": 17,
            "loc": {
              "start": {
                "line": 1,
                "column": 0
              },
              "end": {
                "line": 1,
                "column": 17
              }
            },
            "declarations": [
              {
                "type": "VariableDeclarator",
                "start": 6,
                "end": 16,
                "loc": {
                  "start": {
                    "line": 1,
                    "column": 6
                  },
                  "end": {
                    "line": 1,
                    "column": 16
                  }
                },
                "id": {
                  "type": "Identifier",
                  "start": 6,
                  "end": 8,
                  "loc": {
                    "start": {
                      "line": 1,
                      "column": 6
                    },
                    "end": {
                      "line": 1,
                      "column": 8
                    },
                    "identifierName": "v1"
                  },
                  "name": "v1"
                },
                "init": {
                  "type": "BinaryExpression",
                  "start": 11,
                  "end": 16,
                  "loc": {
                    "start": {
                      "line": 1,
                      "column": 11
                    },
                    "end": {
                      "line": 1,
                      "column": 16
                    }
                  },
                  "left": {
                    "type": "Identifier",
                    "start": 11,
                    "end": 12,
                    "loc": {
                      "start": {
                        "line": 1,
                        "column": 11
                      },
                      "end": {
                        "line": 1,
                        "column": 12
                      },
                      "identifierName": "a"
                    },
                    "name": "a"
                  },
                  "operator": "+",
                  "right": {
                    "type": "Identifier",
                    "start": 15,
                    "end": 16,
                    "loc": {
                      "start": {
                        "line": 1,
                        "column": 15
                      },
                      "end": {
                        "line": 1,
                        "column": 16
                      },
                      "identifierName": "b"
                    },
                    "name": "b"
                  }
                }
              }
            ],
            "kind": "const"
          }
        ],
        "directives": []
      },
      "comments": []
    }
    • AST의 각 노드는 type 속성을 가지며 루트 노드의 type은 program
    • 변수 선언은 VariableDeclaration 타입
    • declarations: 하나의 문장에서 여러 개의 변수를 선언할 수 있기 떄문에 배열로 관리
    • 선언된 변수를 나타내는 타입은 VariableDeclarator
    • v1, a, b 와 같이 프로그래머가 만들어낸 각종 이름은 Identifier 타입
      name 속성에서 v1, a, b 등의 이름 확인 가능
    • 사칙연산은 BinaryExpression 타입, left, right 속성으로 연산에 사용되는 변수가 값이 들어감
    • 타입의 종류는 매우 많기 때문에 외우기보다는 astexplorer 사이트에서 확인하거나 문서를 찾는 방법을 권장

    Babel Plugin 기본 구조

    바벨 플러그인은 하나의 자바스크립트 파일로 제작 가능

    module.exports = function({ types: t }){
        const node = t.BinaryExpression('+', t.Identifier('a'), t.Identifier('b'));
        console.log('isBinaryExpression:', t.isBinaryExpression(node));
        return {};
    }
    • types 매개변수를 가진 함수를 내보냄
    • types 매개변수를 이용해서 AST 노드 생성 가능
      여기서는 두 변수의 덧셈을 AST 노드로 작성
    • types 매개변수는 AST 노드의 타입을 검사하는 용도로도 사용
    • 빈 객체를 반환하면 아무런 일도 하지 않음

    Babel Plugin 함수가 반환하는 값의 형태

    module.exports = function({ types: t }){
        return {
            visitor: {
                Identifier(path){
                    console.log('Identifier name:', path.node.name);
                },
                BinaryExpression(path){
                    console.log('BinaryExpression operator:', path.node.operator);
                }
            }
        }
    }
    • visitor 객체 내부에서 노드의 타입으로 된 함수 정의 가능
      해당하는 타입의 노드가 생성되면 같은 이름의 함수가 호출됨
    • Identifier(): Identifier 타입의 노드가 생성되면 호출되는 함수
      const v1 = a + b; 코드가 입력되면 이 함수는 세 번 호출됨
    • BinaryExpression(): BinaryExpression 타입의 노드가 생성되면 호출
      const v1 = a + b; 코드 입력 시 한 번 호출

    Babel Plugin 제작: 모든 콘솔 로그 제거

    실습 프로젝트 생성

    $ mkdir test-babel-custom-plugin
    $ cd test-babel-custom-plugin
    $ npm init -y
    $ npm install @babel/core @babel/cli

    콘솔 로그 포함 소스 코드 작성

    루트 폴더에 src 폴더를 만들고 그 안에 code.js 파일 작성

    console.log("aaa");
    const v1 = 123;
    console.log("bbb");
    
    function onClick(e) {
      const v = e.target.value;
    }
    
    function add(a, b) {
      return a + b;
    }

    콘솔 로그 AST 확인

    astexplorer 사이트에서 확인

    {
      "type": "File",
      "start": 0,
      "end": 19,
      "loc": {
        "start": {
          "line": 1,
          "column": 0
        },
        "end": {
          "line": 1,
          "column": 19
        }
      },
      "errors": [],
      "program": {
        "type": "Program",
        "start": 0,
        "end": 19,
        "loc": {
          "start": {
            "line": 1,
            "column": 0
          },
          "end": {
            "line": 1,
            "column": 19
          }
        },
        "sourceType": "module",
        "interpreter": null,
        "body": [
          {
            "type": "ExpressionStatement",
            "start": 0,
            "end": 19,
            "loc": {
              "start": {
                "line": 1,
                "column": 0
              },
              "end": {
                "line": 1,
                "column": 19
              }
            },
            "expression": {
              "type": "CallExpression",
              "start": 0,
              "end": 19,
              "loc": {
                "start": {
                  "line": 1,
                  "column": 0
                },
                "end": {
                  "line": 1,
                  "column": 19
                }
              },
              "callee": {
                "type": "MemberExpression",
                "start": 0,
                "end": 11,
                "loc": {
                  "start": {
                    "line": 1,
                    "column": 0
                  },
                  "end": {
                    "line": 1,
                    "column": 11
                  }
                },
                "object": {
                  "type": "Identifier",
                  "start": 0,
                  "end": 7,
                  "loc": {
                    "start": {
                      "line": 1,
                      "column": 0
                    },
                    "end": {
                      "line": 1,
                      "column": 7
                    },
                    "identifierName": "console"
                  },
                  "name": "console"
                },
                "property": {
                  "type": "Identifier",
                  "start": 8,
                  "end": 11,
                  "loc": {
                    "start": {
                      "line": 1,
                      "column": 8
                    },
                    "end": {
                      "line": 1,
                      "column": 11
                    },
                    "identifierName": "log"
                  },
                  "name": "log"
                },
                "computed": false
              },
              "arguments": [
                {
                  "type": "StringLiteral",
                  "start": 12,
                  "end": 18,
                  "loc": {
                    "start": {
                      "line": 1,
                      "column": 12
                    },
                    "end": {
                      "line": 1,
                      "column": 18
                    }
                  },
                  "extra": {
                    "rawValue": "asdf",
                    "raw": "'asdf'"
                  },
                  "value": "asdf"
                }
              ]
            }
          }
        ],
        "directives": []
      },
      "comments": []
    }
    • body 노드를 보면 콘솔 로그 코드가 ExpressionStatement 노드로 시작하는 것을 확인 가능
    • CallExpression 타입: 함수 또는 메소드를 호출하는 코드의 타입
    • 메서드 호출은 CallExpression 노드 내부의 MemberExpression 노드로 만들어짐, MemberExpression 노드 내부에 객체와 메서드의 이름 정보 존재
    • arguments 노드에는 인자의 정보가 담김

    콘솔 로그 제거 플러그인 작성

    루트 폴더에 plugins 폴더를 만들고 remove-log.js 파일 작성

    module.exports = function ({ types: t }) {
      return {
        visitor: {
          ExpressionStatement(path) {
            if (t.isCallExpression(path.node.expression)) {
              if (t.isMemberExpression(path.node.expression.callee)) {
                const memberExp = path.node.expression.callee;
                if (
                  memberExp.object.name === "console" &&
                  memberExp.property.name === "log"
                ) {
                  path.remove();
                }
              }
            }
          },
        },
      };
    };
    
    • ExpressionStatement(): Expression노드가 생성되면 호출되도록 메서드 등록
    • 첫 번째 if문 조건식: Expression 노드의 expression 속성이 CallExpression 노드인지 검사
    • 두 번째 if문 조건식: callee 속성이 MemberExpression 노드인지 검사
    • 세 번째 if문 조건식: console 객체의 log 메서드가 호출된 것인지 검사
    • path.remove(): 모든 조건을 만족하면 AST에서 ExpressionStatement 노드 제거

    바벨 설정에 커스텀 플러그인 추가하기

    루트에 babel.config.js 파일을 만들고 아래 코드 작성

    const plugins = ["./plugins/remove-log.js"];
    module.exports = { plugins };

    실행 및 결과

    모든 console.log()가 제거된 것을 콘솔창에서 확인할 수 있음

    $ npx babel src/code.js
    const v1 = 123;
    
    function onClick(e) {
      const v = e.target.value;
    }
    
    function add(a, b) {
      return a + b;
    }

    Babel Plugin 제작: 함수 내부에 콘솔 로그 추가

    이름이 on으로 시작하는 모든 함수에 콘솔 로그를 추가하는 플러그인 제작

    함수 AST 구조 확인

    function f1(p1) { let v1; } 코드로 만들어진 AST

    {
      //...
      "program": {
        "type": "Program",
        "start": 0,
        "end": 25,
        "loc": {
          "start": {
            "line": 1,
            "column": 0
          },
          "end": {
            "line": 1,
            "column": 25
          }
        },
        "sourceType": "module",
        "interpreter": null,
        "body": [
          {
            "type": "FunctionDeclaration",
            "start": 0,
            "end": 25,
            "loc": {
              "start": {
                "line": 1,
                "column": 0
              },
              "end": {
                "line": 1,
                "column": 25
              }
            },
            "id": {
              "type": "Identifier",
              "start": 9,
              "end": 11,
              "loc": {
                "start": {
                  "line": 1,
                  "column": 9
                },
                "end": {
                  "line": 1,
                  "column": 11
                },
                "identifierName": "f1"
              },
              "name": "f1"
            },
            "generator": false,
            "async": false,
            "params": [
              {
                "type": "Identifier",
                "start": 12,
                "end": 14,
                "loc": {
                  "start": {
                    "line": 1,
                    "column": 12
                  },
                  "end": {
                    "line": 1,
                    "column": 14
                  },
                  "identifierName": "p1"
                },
                "name": "p1"
              }
            ],
            "body": {
              "type": "BlockStatement",
              "start": 16,
              "end": 25,
              "loc": {
                "start": {
                  "line": 1,
                  "column": 16
                },
                "end": {
                  "line": 1,
                  "column": 25
                }
              },
              "body": [...],
              // ...
    • 함수를 정의하는 코드는 FunctionDeclaration 노드로 만들어짐
    • 함수 이름은 id 속성에 들어 있음, 이 값이 on으로 시작하는지 검사하면 됨
    • BlockStatement 노드의 body 속성에는 함수의 모든 내부 코드에 대한 노드가 배열로 담겨 있음, 따라서 이 배열 가장 앞쪽에 콘솔 로그를 추가

    콘솔 로그 추가 플러그인 작성

    plugins 폴더 아래 insert-log.js 파일을 만들고 아래 코드 작성

    module.exports = function ({ types: t }) {
      return {
        visitor: {
          FunctionDeclaration(path) {
            if (path.node.id.name.substr(0, 2) === "on") {
              path
                .get("body")
                .unshiftContainer(
                  "body",
                  t.expressionStatement(
                    t.callExpression(
                      t.memberExpression(
                        t.identifier("console"),
                        t.identifier("log")
                      ),
                      [t.stringLiteral(`call ${path.node.id.name}`)]
                    )
                  )
                );
            }
          },
        },
      };
    };
    • FunctionDeclaration 노드가 생성되면 호출되는 함수 정의
    • if문 조건식: 함수 이름이 on으로 시작하는지 검사
    • body 배열의 앞쪽에 노드를 추가하기 위해 unshiftContainer 메서드 호출
    • t.expressionStatement(): 콘솔 로그 노드 생성, 이 노드는 console.log("call " + 함수 이름); 형태의 코드를 담고 있음

    실행 및 결과

    babel.config.js 파일을 수정해서 remove-log.js 플러그인을 insert-log.js 플러그인으로 교체

    const plugins = ["./plugins/insert-log.js"];
    module.exports = { plugins };
    $ npx babel src/code.js
    console.log("aaa");
    const v1 = 123;
    console.log("bbb");
    
    function onClick(e) {
      console.log("call onClick");
      const v = e.target.value;
    }
    
    function add(a, b) {
      return a + b;
    }
    • onClick 함수 내부에 "call onClick"이라는 내용의 콘솔 로그를 출력하는 코드가 생성됨
    • v1 변수 위 아래의 콘솔 로그가 다시 출력됨

    참고 도서

    • [ 실전 리액트 프로그래밍 ] / 저자_이재승 / 출판사_프로그래밍 인사이트

    바벨 커스텀 플러그인을 제작해보면서 바벨이 어떻게 동작하는지에 대해 알아보았습니다.

    당장 적용하기는 쉽지 않겠지만 우선은 바벨이 AST라는 구조체를 이용하고 각각의 노드와 타입을 알면
    이를 조작할 수 있다는 것 정도만 알아두어도 될 것 같습니다.

    'JavaScript > JavaScript' 카테고리의 다른 글

    Redux | Redux 개요 및 주요 개념  (0) 2020.05.14
    JavaScript | Webpack 기초 정리  (0) 2020.04.23
    JavaScript | Babel 기초 정리  (0) 2020.04.22
    JavaScript | Event Handling 이벤트 처리  (0) 2020.04.09
    JavaScript | With문  (0) 2020.02.18

    댓글

Designed by Tistory.