본문 바로가기

FrontEnd

[Babel] 바벨 플러그인을 작성하며 AST 배우기

반응형

바벨 플러그인을 만들어보며 JS AST(Abstract syntax trees)를 배워봅시다.

해당 글의 번역입니다 : https://dev.to/viveknayyar/revealing-the-magic-of-ast-by-writing-babel-plugins-1h01

 

Revealing the magic of AST by writing babel plugins

When you hear Abstract syntax trees, what is the first thought that occurs in your mind? Something t...

dev.to

추상 구문 트리를 들었을 때 가장 먼저 떠오르는 생각은 무엇입니까?
컴파일러? 복잡한 트리 조작? 비트 조작? 🤔
경력이 얼마 안될 때 AST는 낮은 수준의 컴파일러와 트랜스파일러에 관한 복잡한 용어라고 생각했습니다.

💡 동기

이 블로그를 작성하는 동기는
추상 구문 트리가 무엇이며 우리가 매일 사용하는 대부분의 도구에서 어떻게 중요한 역할을 하는지
모든 사람이 쉽게 이해할 수 있도록 하는 것입니다.

 

babel, webpack, parcel, eslint, codemods, css 파서, CSS in js...
이 모든 도구는 AST의 마법을 사용하여 코드를 조작하고 다른 것으로 변환합니다.
이 게시물에서 우리는 이 마법을 해석하고 그 과정에서 아주 간단한 babel 플러그인을 작성하는 방법을 배웁니다. 🎉

🤔 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 노드 트리로 변환합니다.

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의 경우 다음을 참조할 수 있습니다.

Babel AST Node Types

 

espree 파서(eslint가 사용하는 것)의 경우 여기에서 참조할 수 있습니다.
Eslint AST Node Types

Babel, webpack, Parcel 및 이러한 모든 도구는
공통적인 접근 방식을 사용합니다.
그들은 먼저 우리의 코드를 AST 트리로 변환한 다음
여기에 일부 변환(transformations)(추가, 편집, 업데이트, 삭제)을 적용하고
이러한 변환을 이용해 새 트리를 만든 다음 다시 사람이 읽을 수 있는 코드로 변환합니다.
특정 코드 줄의 AST 트리 표현이 어떻게 보이는지 이해하려면 항상 AST Explorer를 확인하는 것이 좋습니다.

이제 더 이상 시간을 낭비하지 않고 첫 번째 babel 플러그인을 작성합니다.
이 플러그인은 코드의 모든 debugger 문을 제거합니다.


📕 Babel Plugin - Remove Debugger

리포지토리의 여러 위치에 다음 코드를 포함하는 것을 고려하십시오.
function test() {
   const a = 5;
   debugger;
   const b = 6;
}
이 디버거 문이 프로덕션 앱에서 끝나는 것을 원하지 않는다는 것은 분명합니다.
(참고: 실제 앱에는 이러한 실수를 방지하는 데 도움이 되는 일부 번들러 또는 일부 배포 파이프라인 단계가 있지만
이 예제에서는 이러한 배포 파이프라인이 없다고 가정하겠습니다.)
해당 작업을 수행하는 babel 플러그인을 작성합니다.

바벨 플러그인 작성하기

1단계: 타겟팅하려는 AST 노드 타입을 식별합니다.

AST Explorer로 이동하여 라인 2를 클릭하면
노드 타입이 노란색으로 강조 표시되고
대상으로 지정해야 하는 AST 노드가 DebuggerStatement임을 알 수 있습니다.


2단계: 편집기를 실행하고 새 파일을 만듭니다.

이름을 removeDebugger.js로 지정하겠습니다.
이것은 플러그인 파일이 될 것입니다.

 

우리가 지금부터 작성하는 모든 babel 플러그인은 공통 패턴을 따릅니다.
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

모든 alert 문을 console.warn 문으로 변환합니다.

해당 코드를

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 코드를 복사하여 붙여넣습니다.

console.warn을 클릭하면 모든 함수 호출이 call expression이지만
이것은 객체 속성 함수 호출이기 때문에
(object property function call)
다른 호출 표현식으로 대체해야 한다는 것을 말해줍니다.
(MemberExpression)
이것이 호출 수신자로 내부에 멤버 표현식(MemberExpression)이 있는 호출 표현식(CallExpression)이 필요한 이유입니다.
 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


🍬 결론

이 게시물을 통해 AST가 복잡하지 않으며
우리 모두가
linter 플러그인을 만들거나
babel 플러그인을 작성하거나
css in js 라이브러리를 작성하거나
codemod를 사용하여 대규모 리팩터링을 수행하여
개발자 도구 생태계를 개선할 수 있다는 것을 이해하는 데 도움이 되길 바랍니다.

🔗 References

 

 

반응형