Thief of Wealth
article thumbnail

React로 진행하는 프로젝트에서 테스팅을 한다면 많은 사람들이 react testing library를 사용할 것이다.

rtl에는 다양한 메서드들이 존재하는데, 우리가 원하는 DOM 요소를 찾기 위해서는 

getByRole, getByAlt, getByText... 등의 메서드 들을 사용해야 한다.

 

이때 문제가 발생한다.

label이나, role, alt가 들어간 요소들이 없다면?

 

해당 DOM요소가 어디 있는지, 어떻게 위치하는지에 대한 고민에 비용이 엄청나게 소모되고 테스트가 재미없어진다.

어떻게 할 수가 없어 해당 DOM요소에 querySelector를 사용하여 DOM구조에 심하게 의존적인 코드가 나오는 경우도 있다.

 

바로 그 때, 이런 생각이 들기 시작한다.

 

vanilla javascript로 할때는 id, class등의 속성을 selector에 주면 바로 해당 DOM요소를 가지고 올 수 있었는데....

 

그래서 rtl에서는 testid라는 것을 제공한다. data 속성을 이용한것으로, 해당 DOM에 selecting할 data-testid를 주어서 테스팅할때, 해당 DOM요소를 선택하기 위해 크게 고민할 필요성이 사라진다.

 

실제로 testid를 도입한뒤 테스트 코드 작성의 체감 난이도가 엄청 낮아짐을 느낄 수 있었다.

 

하지만 testid는 "테스트"떄에만 사용되는 속성이다.

이런 속성이 실제 배포나 개발단에서 보이게 된다면 코드가 예쁘지 않을 것이다.

 

그래서 같은 팀원인 곤이와 고민을 했다.

"번들링 과정에서 data-testid 속성을 모두 제거하자"가 결론이었다.

 

그럼 언제 제거할 것인가?
webpack-plugin으로 구현한다면, webpack을 번들링하고 난 이후에 적용될 것이고,

babel-plugin으로 구현한다면 webpack이 babel-loader를 호출하는 시점에서 적용이 된다는 것을 리서치했다.

 

이왕 제거한다면 웹팩이 loader를 호출하는 시점에서 제거를 하는것이 맞다는 판단을 내렸고,

npm으로 배포까지 해보기로 결심했다.

본격적으로 babel 플러그인을 제작하기 시작했다.

 

babel은 트랜스파일러인데, 플러그인을 만들기 위해서는 바벨의 대략적인 동작과정을 이해해야 했다.

가장 핵심적인 부분은 코드로 추상 구문 트리 (AST)를 만든다는 것이다.

 

그리고 플러그인에서 각 AST의 노드를 순회하여 우리가 원하는 동작을 수행할 수 있었다.

(이때 AST의 트리구조가, 일반적인 DOM 노드 트리의 구조와 비슷하다고 머리에서 혼동이 와서 많은 헤멤이 있었다.)

 

결과물은 아래 주소에서 확인할 수 있다!

https://www.npmjs.com/package/babel-plugin-remove-react-jsx-attribute

 

babel-plugin-remove-react-jsx-attribute

``` npm install --save-dev babel-plugin-remove-react-jsx-attribute

www.npmjs.com

 

기대도 하지 않았는데 2일만에 100회 이상의 설치수를 기록해서 뭔가 뿌듯하다.

 

 

141회나 설치해주셔서 감사합니다! 🙏

코드를 분석해보자!

babel 동작원리는 대략적으로 이해하는 것에 오래걸렸지, 실제로 코드에 반영하기까지의 과정은 크게 어렵지 않았다.

 

module.exports = function () {
  return {
    visitor: {
      Program(path, state) {
        const attributes = state.opts.attributes;

        if (!Array.isArray(attributes)) {
          throw new Error("babel-plugin-remove-react-jsx-attribute\n: The attributes must be Array!");
        }

        const Visitor = {
          JSXIdentifier(path) {
            attributes.forEach((attribute) => {
              const isRegExpMatched = attribute instanceof RegExp && attribute.test(path.node.name);
              const isExactMatch = attribute === path.node.name;
              if (isRegExpMatched || isExactMatch) path.parentPath.remove();
            });
          },
        };

        path.traverse(Visitor);
      },
    },
  };
};

 

해석하자면, 바벨 플러그인이 위 함수를 호출한다.

visitor -> Program은 바벨 플러그인의 정해진 flow이니 너무 낯설어할 필요 없다.

 

핵심은 Visitor이다.

path를 Vistor가 순회하는데, 우리가 사용한 JSXIdentifier가 path를 받는다. (우리는 react의 JSX가 타겟이기 때문!)

그리고 .babelrc등의 설정파일에서 플러그인과 함께 넘겨준 옵션을 비교하여 일치한다면

 

해당 path의 부모를 삭제시킨다.

엥?

 

해당 속성을 찾았는데, 부모 path를 삭제한다니, 이해가 되지 않을 수 있다.

다시 말하지만 저기서 traverse는 AST 노드를 순회하는 것이다.

data-testid="테스트아이디값" 이라는 속성이 있다면,

 

바벨은 다음과같은 트리로 만든다.

부모: =

왼쪽자식: data-testid

오른쪽자식: "테스트아이디값"

 

Visitor는 유저가 넣어준 속성값이 왼쪽자식과 일치함을 알게되었으므로,

data-testid="테스트아이디값" 구문을 전체적으로 제거하기위해서

부모노드인 =를 제거하는것이다!

 

코드는 간단하지만, 그 기초를 학습하기엔 간단하지 않았던 짧고 굵은 사이드 프로젝트였다.

혼자가 아니라, 학습에 열정이있는 곤이와 함께라 가능했던! 아주아주 만족스러운 경험이었다.

 

다음에 필요한 도구가 있다면 이렇게 함께 학습하고 직접 만들어가는 경험을 지속적으로 해볼 계획이다!

 

 

바벨 플러그인을 만들때 아주아주 많은 도움이 되었던 사이트들

https://nicedoc.io/thejameskyle/babel-handbook/translations/ko/plugin-handbook.md#toc-traversal

 

https://assets.ctfassets.net/nn534z2fqr9f/4pwL91N1BMrptrYv5EylhX/7dceef7cf5da44a125b1e47df0852dd4/100664_356577165_Nicol_Ribaudo_babelhow-to.pdf

 

https://lihautan.com/step-by-step-guide-for-writing-a-babel-transformation/

 

Step-by-step guide for writing a custom babel transformation

Writing your first babel plugin

lihautan.com

 

 

profile on loading

Loading...