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.name
이 console
이고 property.name
이 log
이면 console.log
이라는 것을 파악할 수 있다.
이를 통해서 만족한다면 ESLint에게 report를 하고 최종적으로 개발자에게 error
, warn
등으로 표현해서 알려준다.
결과
warn으로 출력하기
error로 출력하기
나만의 규칙(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을 참고해서 나만의 규칙을 정의하는 파일을 만들고 구성하였다.
크게 meta
와 create
를 만든다. meta
는 규칙에 대한 메타정보를 넣어주고, create에서 실제로 어떻게 찾고 report
할지 규칙을 만드는 것이다. 여기서는 deprecated-import
라는 이름으로 파일을 만들었다.
"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()
를 사용해서 자동으로 고치는 기능도 만들 수 있다.
"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할 예시 파일을 추가하고 실행해보자.
import { test } from "@/utils"
import dayjs from "./module/utils"
npx eslint ./example.js
만들어본 다른 Rules
- fix-me: 주석으로
// FIXME {내용}
을 작성하는 경우{내용}을(를) 확인해주세요.
라고 나온다. - to-do: 주석으로
// TODO {내용}
을 작성하는 경우{내용} 작업이 있습니다.
라고 나온다. - check-console-log:
console.log
가 있는 경우console.log가 있습니다. 사용하지 않을 경우 제거해주세요.
라고 메시지가 나오며--fix
를 하게 되면console.log
가 제거된다.
후기
ESLint는 만들어져 있는 plugins나 rules만 사용할 줄만 알았다. 이번 기회에 어떻게 작동하고 만드는지 제대로 학습할 수 있었다. 커스터마이징 할 수 있는 건 알았지만 생각보다 만들 수 있는 게 좋았다. 다음에 기회가 된다면 프로젝트에 코딩 컨벤션을 팀에서 정하고 그에 맞는 구성을 해보고 싶다.
> 추가
원래 이전 프로젝트에서는 외부 모듈과 내부 모듈을 특성에 맞게 합치고 Sorting 하는 기능을 만들려고 해보려고 했으나 이미 sort-import
라는 좋은 plugin이 존재했다. 다음에 기회가 되면 사용해 보고 올려보려고 한다.