본문 바로가기메뉴 바로가기


들어가며

안녕하세요. 카카오 FE플랫폼팀의 Joah입니다. 최근에 팀에서 사용하는 JavaScript 스타일 가이드를 개선하는 업무에 참여했습니다. 업무를 하며 스타일 가이드에서 사용하고 있는 ESLint에 관심이 생기게 되었고, 어떻게 동작하는지 소스코드를 분석해보다가 ESLint에 직접 컨트리뷰션 하는 경험도 하게 되었습니다.

이 과정들에서 알게 된 ESLint를 조금 더 잘 활용할 수 있는 방법에 대해 정리해 보았는데요. 기본적인 사용법은 포함하고 있지 않기 때문에 궁금하신 분은 ESLint getting-started를 참고하시면 됩니다.

이 글에서는 ESLint의 설정 공유하는 방법, 규칙을 직접 만드는 방법에 대해 정리했습니다. 글 중간중간 스타일 가이드 업무를 하며 알게 된 점이나, 컨트리뷰션을 하며 얻게 된 지식도 정리해 보았습니다.


ESLint

ESLint는 JavaScript, JSX의 정적 분석 도구로 오픈 소스 프로젝트입니다. 코드를 분석해 문법적인 오류나 안티 패턴을 찾아주고 일관된 코드 스타일로 작성하도록 도와줍니다. JSLint, JSHint와 같이 다른 JavaScript 정적 분석 도구들도 있지만, ESLint가 커스터마이징이 쉽고 확장성이 뛰어나 많이 쓰이고 있는 추세입니다. ESLint는 스타일 가이드를 좀 더 편리하게 적용하기 위해 사용하기도 하는데, 외부에 공개되어 많은 개발자가 사용 중인 Airbnb Style Guide, Google Style Guide 가 그 대표적인 예입니다.


ESLint 설정 공유하기

ESLint는 Shareable Configs라는 기능을 제공하고 있습니다. 이는 누군가 만들어 놓은 ESLint 설정을 npm을 이용해 쉽게 설치, 확장해서 사용할 수 있는 기능입니다. FE플랫폼팀에서도 이 기능을 이용해 스타일 가이드에 맞는 설정을 공유해 사용하고 있습니다.

1. 설정 만들기

Shareable Configs를 만들기 위해 필요한 파일은 package.json과 index.js입니다.

eslint-config-[설정 이름]/
 ├── index.js
 └── package.json

package.json

package.json 작성 시 첫 번째로 주의할 점은 패키지 이름입니다. npm을 통해 공유 가능한 설정을 만들기 위해서는 eslint-config-[설정 이름] 형식을 사용해야 합니다. 두 번째로 주의할 점은 peerDependencies를 명시해 주어야 한다는 것입니다. ESLint와 플러그인의 버전마다 추가된 규칙이나 옵션이 있기 때문에 설정한 규칙을 지원하는 버전을 명시해 줍니다.

{
  "name": "eslint-config-[설정 이름]",
  "peerDependencies": {
    "eslint": ">=6",
    "eslint-plugin-import": "^2.18.2"
      // ...
  }
}

1. 확장할 설정

프로젝트의 ESLint 설정을 할 때처럼, 공유 설정에서도 extends 항목에 이미 npm에 공유된 설정을 사용할 수 있습니다. 또한 프로젝트 내부에 있는 설정을 확장할 수도 있는데, 스타일 가이드 작업 시 이를 이용해 ECMAScript 버전, 분석의 대상(ECMAscript 버전 JSDoc) 등에 따라 규칙을 디렉터리로 분류해 관리하고 있습니다.

 eslint-config-.../
   ├── index.js
   ├── package.json
   ├── es6/
   │    └── index.js
   ├── comment/
   |    └── index.js
   | ...

2. 플러그인 추가

공유 설정에서 플러그인을 설정할 수도 있는데, ESLint에서 기본으로 제공하지 않는 다양한 규칙을 플러그인을 통해 사용할 수 있습니다. 업무를 하며 스타일 가이드 공유할 설정에 추가할 만한 플러그인들을 조사해 보았는데요. 그 과정에서 알게 된 유용한 플러그인들입니다.

  • eslint-plugin-jsdoc JSDoc을 활용하면 소스코드의 문서화를 쉽게 할 수 있는데요. 이 플러그인은 JSDoc을 정적 분석하는 역할을 합니다. 플러그인의 규칙을 통해 JSDoc을 항상 사용하도록 설정할 수도 있고, 작성이 되어있을 경우에만 JSDoc의 포맷, 스타일 검사를 할 수도 있습니다.
  • typescript-eslint TS(TypeScript)에 대한 정적 분석을 지원하는 플러그인입니다. 기존에 TS의 정적 분석 도구로는 TSLint 가 많이 쓰였지만 곧 deprecated될 예정입니다. TSLint에서 마이그레이션시 주의해야 할 점은 TSLint 가 제공하던 규칙을 모두 제공하지는 않으며 일부 규칙의 경우 동작이 다르다는 것입니다. 이에 대한 자세한 내용은 프로젝트의 로드맵을 참고하시면 됩니다.
  • eslint-plugin-markdown 마크다운에 있는 코드 블록을 대상으로 ESLint를 실행할 수 있는 플러그인입니다. ESLint의 custom processor를 이용해 마크다운의 코드 블록에 대해서만 정적 분석을 합니다. 이 플러그인을 이용하면 마크다운에도 린팅을 적용할 수 있어 스타일 가이드, 튜토리얼과 같이 코드가 많이 들어가는 문서에 유용하게 쓸 수 있습니다.

3. 사용할 규칙 설정

rules에는 직접 규칙을 설정할 수 있으며, 설정할 수 있는 기본 규칙과 옵션은 ESLint Rules에서 확인하실 수 있습니다. 설정한 규칙을 어긴 코드가 있을 때 warning 또는 error를 발생시키도록 설정할 수 있는데, error가 하나라도 발생할 경우 ESLint의 종료 코드(exit code)가 1이 되며, warning은 종료 코드에 영향을 미치지 않습니다. 작업한 스타일 가이드에서는 no-debugger와 같이 일부 개발 과정 중에 어길 수 있는 규칙들을 warning으로 설정해 사용하고 있습니다.

index.js

index.js에는 공유할 설정을 작성합니다. 확장할 설정, 플러그인, 사용할 규칙 등을 설정할 수 있습니다.

module.exports = {
  // 확장할 설정
  "extends": [ "eslint:recommended", ...],
  // 플러그인 추가
  "plugins": [ "import", ... ],
  // 사용할 규칙 설정
  "rules": {
      "semi": "error",
      "no-console": "error",
  },
}

2. 공유 설정 배포하기

위 작업이 끝나면 다음 명령어로 npm에 패키지를 배포합니다.

$ npm publish

3. 배포된 공유 설정 사용하기

npm에 올라간 설정을 사용할 때는 다음과 같은 명령어로 패키지를 설치합니다.

$ npm install -D eslint-config-[설정 이름]

설치한 패키지를 프로젝트의 ESLint 설정 파일에서 extends 하면 공유한 설정이 적용됩니다.

{
  "extends": ["설정 이름"],
}

ESLint 규칙 만들기

간혹 ESLint에서 지원하지 않지만 필요한 규칙이 있을 수 있습니다. 이 경우 ESLint plugin을 직접 만들어 새로운 규칙을 구현해 사용할 수 있습니다.

1. 프로젝트 생성

ESLint plugin 프로젝트 구조는 yeomangenerator-eslint를 이용해 쉽게 생성할 수 있습니다. 이를 위해 두 패키지를 설치합니다.

$ npm install -g yo
$ npm install -g generator-eslint

설치 후 프로젝트 디렉터리를 생성하고 이동합니다. yo eslint:plugin 명령어를 실행하고 프로젝트 설정에 필요한 정보를 입력하면 프로젝트에 필요한 파일이 생성됩니다.

$ mkdir eslint-plugin-my-plugin
$ cd eslint-plugin-my-plugin
$ yo eslint:plugin

# What is your name? joah
# What is the plugin ID? my-plugin
# Type a short description of this plugin: test
# Does this plugin contain custom ESLint rules? Yes
# Does this plugin contain one or more processors? No

// 생성되는 파일들
# create package.json
# create lib/index.js
# create README.md

2. 규칙 생성

이제 생성된 프로젝트에 새로운 규칙을 만들 수 있습니다. 이를 위해 yo eslint:rule 명령어를 실행하고 만들 규칙에 대한 정보를 입력하면 규칙을 작성할 수 있도록 보일러 플레이트가 생성됩니다.

$ yo eslint:rule

# What is your name? joah
# Where will this rule be published? ESLint Plugin
# What is the rule ID? no-deprecated-funcs
# Type a short description of this rule: test
# Type a short example of the code that will fail: test

// 생성되는 파일들
# create docs/rules/no-deprecated-funcs.md
# create lib/rules/no-deprecated-funcs.js
# create tests/lib/rules/no-deprecated-funcs.js

생성된 파일 중 lib/rules 하위에 규칙 이름으로 생성된 js 파일이 있습니다. 이곳에 새로운 규칙을 구현하면 됩니다. 하지만 규칙을 구현하기 위해서는 ESLint가 어떻게 동작하는지 이해해야 합니다. 동작 방식을 간략하게 살펴보겠습니다.

ESLint 동작 방식

어떤 과정으로 ESLint 가 동작하는지 간략하게 표현한 그림입니다.

(1) Parser

ESLint를 실행하면 Parser가 자바스크립트 코드를 분석하여 AST(Abstract Syntax Tree)를 만듭니다. 사용할 Parser는 babel-eslint, Esprima 등 선택해 설정할 수 있으며 따로 설정하지 않을 경우에는 기본으로 Espree를 사용합니다.

(2) AST

AST(Abstract Syntax Tree)는 컴파일러에서 널리 사용되는 자료구조로 소스코드의 구조를 트리 형태로 표현한 것입니다. AST를 구성하고 있는 노드는 자신이 어떤 노드인지를 알려주는 노드 타입, 소스 코드에서 노드의 위치, 하위 자식 노드들에 대한 레퍼런스 등을 가지고 있습니다. 이해를 돕기 위한 예제 코드와 그에 대해 Espree Parser가 생성한 AST를 그림으로 표현했습니다.

const foo = 5 + bar();

그림에서 보이는 VariableDeclaration, Identifier, BinaryExpression…등 이 노드의 타입을 나타내며, Id, Init, Left, Right와 같이, 노드 사이의 선을 보면 부모 노드 아래에서 자식 노드가 어떤 역할을 하고 있는지 알 수 있습니다.

실제 Espree가 만드는 AST 노드에는 더 많은 데이터가 담겨 있는데요. 각 노드 타입에 대한 스펙이 궁금하신 분은 ESTree spec을 참고하시면 됩니다. JavaScript 코드에 따른 AST의 구조가 궁금하신 분은 ESLint parser 데모 페이지에서, 작성한 코드에 따른 AST를 JSON 포맷으로 확인하실 수 있습니다.

(3) Linter + Rule

Linter는 설정에 있는 규칙들을 생성한 뒤, Parser 가 생성한 AST를 순회하며 AST 노드 타입과 같은 이름의 이벤트를 발생시킵니다. 발생한 이벤트는 규칙(Rule)의 리스너에게 전달되어, 해당 노드가 규칙을 지키고 있는지 검사합니다. 규칙에 맞지 않는 경우 이에 대해 보고(Report) 하며 가능한 경우 코드를 알아서 규칙에 맞게 수정해주는 Fixer를 생성할 수 있습니다.

이해를 돕기 위한 예제로, “getFriendNames”, “getFriendNames” 함수들이 deprecated 되었다고 가정하고, 새롭게 지원하는 “getNames” 함수를 사용하도록 자동 수정(autofix)을 지원하는 규칙을 만들어 보겠습니다. 먼저 함수를 호출식은 어떤 형태의 AST를 만드는지 확인합니다.

foo();

ESLint parser 데모 페이지를 통해 위 코드가 다음과 같은 AST를 생성하는 것을 확인할 수 있습니다.

{
  "type": "Program",
  "start": 0,
  "end": 6,
  "body": [
    {
      "type": "ExpressionStatement",
      "start": 0,
      "end": 6,
      "expression": {
        "type": "CallExpression",
        "start": 0,
        "end": 5,
        "callee": {
          "type": "Identifier",
          "start": 0,
          "end": 3,
          "name": "foo"
        },
        "arguments": []
      }
    }
  ],
  "sourceType": "module"
}

위 AST를 구조를 통해 규칙에서 구현해야할 로직을 정리해보면 다음과 같습니다.

  1. AST노드 타입(type)이 호출식(CallExpression)일 때,
  2. 호출식에 의해 호출당하는(callee) 노드의 이름(name)이 deprecated 된 함수 이름이라면,
  3. 린트 에러를 발생시키고, 새로운 함수이름으로 수정한다.

아래 코드는 이 로직을 그대로 구현한 규칙입니다.

const DEPRECATED_FUNCS = [
    'getFriendNames',
    'getFamilyNames'
];
const RECOMMENDED_FUNC = 'getNames';

module.exports = {
    meta: {
        // ...
        fixable: true,
    },

    create (context) {
        return {
            // 1. 노드 타입이 CallExpression 일 때
            CallExpression (node) {
                const {calle} = node;

                // 2. callee 의 이름이 deprecated 된 함수 이름이면,
                if (DEPRECATED_FUNCS.includes(calle.name)) {

                    // 3. 규칙을 어겼다고 보고함, 새로운 함수이름으로 수정
                    context.report({
                        node,
                        data: {deprecatedFunc: name},
                        message: `'{{deprecatedFunc}}()' 은 deprecated 되었습니다.`,
                        fix: fixer => fixer.replaceText(callee, RECOMMENDED_FUNC),
                    });
                }
            }
        };
    }
};

예제 코드를 보면 create() 가 반환 하는 객체의 메서드 이름이 CallExpression입니다. 이 부분이 Linter가 AST를 순회하다가, CallExpression 타입의 노드를 만나면 발생시키는 이벤트가 전달되어 실행되는 부분입니다. 매개변수(node)로는 CallExpression 타입에 해당하는 노드가 들어옵니다. 이후 해당 노드의 callee의 name 을 확인합니다. 확인한 이름이 deprecated 된 이름일 경우 context.report() 를 실행해 어디에 어떤 문제가 있는지를 보고합니다.

  • 더 복잡한 규칙 실제 규칙을 만들 때는 더 복잡한 로직과 부가적인 데이터가 필요합니다. 실제로 import 한 특정 모듈에서 사용 중인 함수인지, 글로벌 스코프에 정의된 변수를 사용 중인지 등 AST 에 관련된 복잡한 로직이 필요합니다. 그런 경우, ESLint 내부에서 활용중인 eslint-utils을 활용하면 스코프 확인, 레퍼런스 추적등을 비교적 쉽게 구현할 수 있습니다.
  • AST에 없는 정보 AST를 확인해보면, 세미콜론 ; 괄호 (, 와 같이 일부 소스코드에 대한 토큰 정보가 생략되어 있습니다. 이런 토큰 정보는 코드 스타일과 관련한 규칙을 만들 때 필요한데요. 규칙 생성 시 create의 매개변수로 주어지는 context의 getSourceCode() 메서드를 통해서, 소스 코드가 추상화되어있는 객체를 얻을 수 있습니다. 이를 이용해 노드 주변 토큰, 노드 사이의 토큰들에 대한 정보를 얻을 수 있습니다.
// SourceCode 사용 예시
module.exports = {
  // ...

  create (context) {
    const sourceCode = context.getSourceCode();
    //...
    
    sourceCode.getTokenAfter(nodeA); // 노드 뒤에 있는 토큰 반환
    sourCode.isSpaceBetween(nodeA, nodeB) // 노드 사이 공백 존재 여부
  }
}

(4) Report + Fixer

Report는 어떤 규칙을 어겼는지, 어떤 부분이 문제인지를 전달해 주는 역할을 합니다. 예제에서 context.report() 부분이 그 역할을 수행합니다.

context.report({
    node,
    data: {deprecatedFunc: name},
    message: `'{{deprecatedFunc}}()' 은 deprecated 되었습니다.`,
    // ...
});

context.report() 를 통해 전달하는 주요 데이터는 메시지(message)와 노드(node)입니다. 메시지는 에러로 출력될 텍스트 정보가 포함되어 있습니다. 노드에는 실제 소스코드에서 위치에 대한 정보가 포함되어 있기 때문에 규칙을 어긴 코드의 위치도 확인할 수 있습니다.

Fixer는 에러 발생 시 코드를 규칙에 맞게 수정하는 역할을 합니다.

const RECOMMENDED_FUNC = 'getNames';
// ...
context.report({
    // ...
    fix: fixer => fixer.replaceText(callee, RECOMMENDED_FUNC),
});

위 예제에서는 callee 가 가지고 있는 텍스트를 “getNames”로 바꾸는 동작을 합니다. ESLint 실행 시 –fix 옵션을 사용하면 동작하게 되며, fixer가 제공하는 메서드를 통해 특정 토큰, 노드가 가지고 있는 값을 수정하거나 노드를 삭제, 삽입할 수 있습니다.

3. 플러그인 배포

설정 만들기 작업이 끝나면 다음 명령어로 npm에 패키지를 배포합니다.

$ npm publish

4. 배포된 플러그인 사용하기

npm에 배포된 플러그인을 사용할 때는 다음과 같은 명령어로 패키지를 설치합니다.

$ npm install -D eslint-plugin-[플러그인 이름]

설치한 패키지를 프로젝트의 ESLint 설정 파일의 plugins에 설정하고, 만든 규칙을 rules에 등록하면 사용할 수 있습니다.

{
  "plugins": ["플러그인 이름"],
  "rules": {
    "플러그인 이름/규칙 이름": "error"  
  }
}

이후 규칙을 실행하거나, ESLint를 지원하도록 설정한 IDE를 사용하면 다음과 같은 결과를 확인할 수 있습니다.

  • ESLint 실행
  • IDE – ESLint 플러그인

마치며

이상으로 ESLint를 조금 더 잘 활용하는 방법으로 설정을 공유하는 방법과, 플러그인을 통해 직접 규칙을 만드는 방법을 정리해 보았습니다.

스타일 가이드를 위해 ESLint의 규칙들을 조사하면서, JavaScript의 Best Practice와 그 이유에 대해 알게 되는 좋은 경험을 할 수 있었습니다. 또한 사용할 때는 마법처럼 느껴졌던 ESLint가 어떻게 동작하는지 이해하게 되는 계기가 되었는데요. 스타일 가이드를 위해 ESLint를 활용하려는 개발자나, ESLint의 동작을 이해하고 직접 규칙을 만들어 보려는 개발자에게 이 글이 도움이 되었으면 좋겠습니다. 감사합니다.

함께해요 🙂

Reference

joah.yeon
joah.yeon 카카오에서 FrontEnd 업무를 하고 있습니다. FrontEnd 개발자를 위한 도구에 관심이 많습니다.
Top Tag
adaptive-hash-index
adt
agile
agilecoach
ai
Algorithm/ML
Algorithm/Ranking
almighty-data-transmitter
android
angular
anycast
applicative
Architecture
arena
async
aurora
Backend
bgp
ble
blind-recruitment
block
blockchain
bluetooth
brian
cahtbot
cd
ceph
certificate
certification
cgroup
ci
cite
clojure
close-wait
cloud
cloudera-manager
clustered-block
cmux
cnn
code-festival
code-review
coding
competition
component
conference
consul
container
contest
couchbase
COVID-19
cpp
Data
DB
deep-learning
developer
developers
devops
digitalization
digitaltransformation
dns
docker
employeecard
eslint
Featured
friendstime
front-end
functional-programming
funfunday
fzf
garbage-collection
gawibawibo
GC
github
go
graphdb
graphql
growth
ha
hadoop
hbase
hbase-manager
hbase-region-inspector
hbase-snashot
hbase-table-stat
hbase-tools
hri
id
ifkakao
infrastructure
innodb
internship
ios
item
Java
javascript
json
kafka
kakao
kakao-commerce
kakao-games
kakaoarena
kakaocon
kakaok
kakaokey
kakaokrew
kakaomap
kakaotalk
KCDC
khaiii
kubernetes
l3dsr
l4
links
load-balancing
machine-learning
marathon
meetup
melon
mesos
Messaging
microservice
mobil
monad
mtre
mysql
mysql-realtime-traffic-emulator
nand-flash
network
new
new-krew
nfc
nomad
ocp
open
opensource
openstack
OpenWork
page
parallel
PBA
programming-contest
pycon
python
quagga
reactive-programming
reactor
recommendation
recruitment
redis
redis-keys
redis-scan
rest
rubics
ruby
rxjs
s2graph
scala
scalaz
server
service
sharding
shopping
socket
spark
spark-streaming
SpringBoot
ssd
Statistics/Analysis
Stomp
storage
storm
style-guide
support
System
talk
talkchannel
tcp
tech
test
Thread-Debugging
time-wait
tmux
update
vim
vim-github-dashboard
vim-plugin
vue
web-cache
webapp
WebSocket
weekly
All Tag
adaptive-hash-index
adt
agile
agilecoach
ai
Algorithm/ML
Algorithm/Ranking
almighty-data-transmitter
android
angular
anycast
applicative
Architecture
arena
async
aurora
Backend
bgp
ble
blind-recruitment
block
blockchain
bluetooth
brian
cahtbot
cd
ceph
certificate
certification
cgroup
ci
cite
clojure
close-wait
cloud
cloudera-manager
clustered-block
cmux
cnn
code-festival
code-review
coding
competition
component
conference
consul
container
contest
couchbase
COVID-19
cpp
Data
DB
deep-learning
developer
developers
devops
digitalization
digitaltransformation
dns
docker
employeecard
eslint
Featured
friendstime
front-end
functional-programming
funfunday
fzf
garbage-collection
gawibawibo
GC
github
go
graphdb
graphql
growth
ha
hadoop
hbase
hbase-manager
hbase-region-inspector
hbase-snashot
hbase-table-stat
hbase-tools
hri
id
ifkakao
infrastructure
innodb
internship
ios
item
Java
javascript
json
kafka
kakao
kakao-commerce
kakao-games
kakaoarena
kakaocon
kakaok
kakaokey
kakaokrew
kakaomap
kakaotalk
KCDC
khaiii
kubernetes
l3dsr
l4
links
load-balancing
machine-learning
marathon
meetup
melon
mesos
Messaging
microservice
mobil
monad
mtre
mysql
mysql-realtime-traffic-emulator
nand-flash
network
new
new-krew
nfc
nomad
ocp
open
opensource
openstack
OpenWork
page
parallel
PBA
programming-contest
pycon
python
quagga
reactive-programming
reactor
recommendation
recruitment
redis
redis-keys
redis-scan
rest
rubics
ruby
rxjs
s2graph
scala
scalaz
server
service
sharding
shopping
socket
spark
spark-streaming
SpringBoot
ssd
Statistics/Analysis
Stomp
storage
storm
style-guide
support
System
talk
talkchannel
tcp
tech
test
Thread-Debugging
time-wait
tmux
update
vim
vim-github-dashboard
vim-plugin
vue
web-cache
webapp
WebSocket
weekly

위로