logo
PostsInvestingHomebarArticlesAbout

야 너두 ESLint 규칙 만들 수 있어

2023.06.20 / 10min

Intro

프로젝트를 구성하고 개발하다 보면 문법 오류, 오타를 개발하는 시점에 잡고 싶을 때가 있다. 또는 회사만의 코딩 컨벤션을 정하고 준수하면 가독성이 좋아지고 안티 패턴의 코드를 예방할 수 있다. 그럴 때 우리가 사용하는게 정적 분석이다.

이번 기회에, 그 당시에는 하지 못했던 작업인 나만의 규칙(Rule)을 만들어 보려고 한다.


TOC

정적 분석으로 Static Testing하기

자바스크립트는 다른 언어에 비해 유연한 문법을 가지고 있다. 이 특징이 뜻하지 않은 문제를 일으킨다. 가령, 문법적 오류가 아니라서 찾기 어려운 버그를 만들거나, 개발자의 의도를 파악하기 어려운 코드를 만들거나, 컴파일 단계가 없어서 코드를 실행하기 전까지는 알 수 없는 오류를 만든다.

이를 방지하기 위해서 테스트의 종류 중 하나인 Static Testing으로 해결할 수 있다. Javascript에서는 Lint Tool(ex. ESLint, JSLint등등)를 사용한 정적 분석으로 문법 오류나 오타 등의 잠재적 에러를 찾아낸다.

정적 분석을 이용하면 코딩 컨벤션을 자동으로 검증하고 잠재적 에러를 찾아내 자바스크립트의 단점을 보완할 수 있다.


ESLint란 무엇일까

ESLint는 니콜라스 자카스가 만든 도구로 가장 널리 사용된다. 특히 Airbnb, PayPal, facebook와 같은 대형회사에서도 활발하게 사용될 정도로 신뢰할 수 있는 도구이다. 자바스크립트 구문 분석기인 Espree를 사용해 AST(Abstract Syntax Tree) 를 만들어 코드를 직접 평가하는 것으로 알려져 있고 방대한 규칙(Rule)뿐만 아니라 다양한 환경과 포맷터(Formatter)를 지원한다. 규칙(Rule)이나 포맷터(Formatter)를 직접 만들 수 있는 기능도 제공하기 때문에, 프로젝트의 특성에 따라 다양한 방식으로 커스터마이징 할 수 있다.


AST 이용하기

ESLint는 Abstract Syntax Tree(AST) 를 이용해서 규칙(Rule)을 정의하고 적용한다.

AST는 소스 코드를 읽어낸 뒤 각 코드에서 구문 정보를 정리하여 나타낸 트리 형태의 자료구조이다.

AST의 상세한 구조는 파서마다 약간의 차이가 있지만, AST Explorer라는 도구를 사용하면 소스 코드를 넣었을 때 어떤 AST가 나오는 지를 쉽게 확인할 수 있다.

직접 소스 코드를 파싱해보자.
function getBarString() {
  return "bar" + undefined; // FIXME 수정하기
}
console.log(getBarString())

예시로 위의 코드를 AST Explorer에 들어가서 넣게 되면 아래와 같이 나온다.

{
  "type": "Program",
  "start": 0,
  "end": 43,
  "body": [
    {
      "type": "FunctionDeclaration",
      "start": 0,
      "end": 42,
      "id": {
        "type": "Identifier",
        "start": 9,
        "end": 21,
        "name": "getBarString"
      },
      "expression": false,
      "generator": false,
      "async": false,
      "params": [],
      "body": {
        "type": "BlockStatement",
        "start": 24,
        "end": 42,
        "body": [
          {
            "type": "ReturnStatement",
            "start": 28,
            "end": 40,
            "argument": {
              "type": "Literal",
              "start": 35,
              "end": 40,
              "value": "bar",
              "raw": "\"bar\""
            }
          }
        ]
      }
    }
  ],
  "sourceType": "module"
}

ESLint는 그럼 어떻게 작동하나?

ESLint는 Espree라고 하는 파서를 통해 소스 코드를 파싱하고, 결과를 각 플러그인에서 순회하며 규칙을 실행하는 것이다. 우리가 원하는 규칙을 직접 플러그인을 통해 정의하고, 실행할 수 있다.

우리가 Espree AST를 사용한다면, ESLint 규칙도 쉽게 만들 수 있다.

간단하게 console.log를 찾아서 지우라고 알려주는 Rule을 만들어 보자.

{
  "type": "ExpressionStatement",
  "start": 0,
  "end": 17,
  "expression": {
    "type": "CallExpression",
    "start": 0,
    "end": 17,
    "callee": {
      "type": "MemberExpression",
      "start": 0,
      "end": 11,
      "object": {
        "type": "Identifier",
        "start": 0,
        "end": 7,
        "name": "console"
      },
      "property": {
        "type": "Identifier",
        "start": 8,
        "end": 11,
        "name": "log"
      },
      "computed": false,
      "optional": false
    },
    "arguments": [
      {
        "type": "Literal",
        "start": 12,
        "end": 16,
        "value": "!!",
        "raw": "'!!'"
      }
    ],
    "optional": false
  }
}

위의 Tree에서 MemberExpression안에 있는 object.nameconsole이고 property.namelog이면 console.log이라는 것을 파악할 수 있다.

이를 통해서 만족한다면 ESLint에게 report를 하고 최종적으로 개발자에게 error, warn 등으로 표현해서 알려준다.

결과

warn으로 출력하기

warn-console-log

error로 출력하기

error-console-log


나만의 규칙(Custom Rule) 만들기

이제 나만의 규칙(Custom Rule)을 만들어 보자.

하다보니 재미있어서 여러가지 주제로 만들어 보았는데, 그중 Deprecated된 Module을 찾아서 알려주는 규칙(Rule) 을 만드는 과정을 작성해 보았다.

다른 규칙(Rule)은 아래에 링크로 올려두었습니다.

프로젝트를 리팩토링하다 보면 더 이상 사용하지 않는 모듈이 생기기 마련이다. 그러나 개발자 수가 많다면 모르고 사용하게 되는데 이를 방지하기 위해서 사용할 만한 Rule을 만들어 보려고 한다.

mkdir eslint-custom-rule # create directory
cd eslint-custom-rule # move directory
npm init -y # set package.json

ESLint 규칙을 만들 공간과 package.json을 만들어 주었다.
그다음 ESLint - Custom Rule을 참고해서 나만의 규칙을 정의하는 파일을 만들고 구성하였다.

크게 metacreate를 만든다. meta는 규칙에 대한 메타정보를 넣어주고, create에서 실제로 어떻게 찾고 report할지 규칙을 만드는 것이다. 여기서는 deprecated-import라는 이름으로 파일을 만들었다.

deprecated-import.js
"use strict"

module.exports = {
  meta: {
    type: "problem",
    fixable: "code",
    schema: [
      {
        type: "array",
        items: {
          type: "string",
        },
        additionalProperties: false,
      }
    ],
    docs: {
      description: "deprecated된 Module 사용하지 않기",
    }
  },
  create(context) {
    function isIncludePath(srcValue, regexList) {
      for (let i = 0; i < regexList.length; i++) {
        const regex = regexList[i]

        if (!!RegExp(regex, "u").exec(srcValue)) {
          return true
        }
      }

      return false
    }

    return {
      ImportDeclaration(node) {
        const srcValue = node.source.value
        const regexGroup = context.options[0] || [];

        if (isIncludePath(srcValue, regexGroup)) {
          context.report({
            node,
            message: `${srcValue}은(는) Deprecated된 모듈입니다. 다른 모듈로 변경해주세요.`,
            fix(fixer) {
              return fixer.remove(node);
            }
          })
        }
      }
    };
  }
};

위의 코드에서 신경썼던 부분은 schema 부분과 isIncludePath() 부분으로 ESLint 규칙에는 parameters를 넘길 수 있다. 즉, parameters 형태를 정의하는 곳이 schema다.

isIncludePath()에서는 Deprecated된 모듈을 찾을 때는 단순히 해당 문자가 포함되었는지가 아닌 정규식으로 찾을 수 있도록 구성하였다.

포함되어 있다면 context.report를 통해서 리포팅하고 fix()를 사용해서 자동으로 고치는 기능도 만들 수 있다.

eslint.config.js
"use strict";

const deprecatedImport = require("./deprecated-import");

module.exports = [
  {
    files: ["**/*.js"],
    languageOptions: {
      sourceType: "module",
      ecmaVersion: "latest",
    },
    plugins: {
      "@snyung": {
        rules: {
          "deprecated-import": deprecatedImport,
        }
      },
    },
    rules: {
      "quotes": ["error", "double"],
      "@snyung/deprecated-import": ["error", ["@/utils", "./module/utils"]]
    },
  }
]

만든 규칙은 eslint.config.js에 추가하여 바로 사용할 수 있다. plugins에 원하는 이름으로 나만의(Custom) 규칙들을 추가하고 rules에서는 추가한 규칙의 severity(심각도)parameters를 작성해서 사용할 수 있다.

이제 linting할 예시 파일을 추가하고 실행해보자.

example.js
import { test } from "@/utils"
import dayjs from "./module/utils"
npx eslint ./example.js

deprecated-module-rules

만들어본 다른 Rules

  • fix-me: 주석으로 // FIXME {내용}을 작성하는 경우 {내용}을(를) 확인해주세요. 라고 나온다.
  • to-do: 주석으로 // TODO {내용}을 작성하는 경우 {내용} 작업이 있습니다. 라고 나온다.
  • check-console-log: console.log가 있는 경우 console.log가 있습니다. 사용하지 않을 경우 제거해주세요.라고 메시지가 나오며 --fix를 하게 되면 console.log가 제거된다.

후기

ESLint는 만들어져 있는 plugins나 rules만 사용할 줄만 알았다. 이번 기회에 어떻게 작동하고 만드는지 제대로 학습할 수 있었다. 커스터마이징 할 수 있는 건 알았지만 생각보다 만들 수 있는 게 좋았다. 다음에 기회가 된다면 프로젝트에 코딩 컨벤션을 팀에서 정하고 그에 맞는 구성을 해보고 싶다.

> 추가

원래 이전 프로젝트에서는 외부 모듈과 내부 모듈을 특성에 맞게 합치고 Sorting 하는 기능을 만들려고 해보려고 했으나 이미 sort-import라는 좋은 plugin이 존재했다. 다음에 기회가 되면 사용해 보고 올려보려고 한다.


Reference


avatar
snyungSoftware Engineer(from. 2018)
social-mailsocial-githubsocial-facebooksocial-book