๋ณธ๋ฌธ์œผ๋กœ ๋ฐ”๋กœ ๊ฐ€๊ธฐ
๋กœ๊ณ 
๊ฐœ๋ฐœ

[JS] ESLint ํ•œ ์ž…

์ฝ๋Š” ์‹œ๊ฐ„ 15๋ถ„
ESLint๋กœ ์ฝ”๋“œ๋ฅผ ๊นจ๋—ํ•˜๊ฒŒ ํ•ด๋ด…์‹œ๋‹ค. ๊ธ€์˜ ์ธ๋„ค์ผ"

#๋“ค์–ด๊ฐ€๋ฉฐ

์•ˆ์จ๋ณธ ์‚ฌ๋žŒ์€ ์žˆ์ง€๋งŒ, ํ•œ ๋ฒˆ๋งŒ ์จ๋ณธ ์‚ฌ๋žŒ์€ ์—†๋‹ค๋Š” ESLint์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด์ž. ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ํฌ๊ฒŒ ํ•„์š”์„ฑ์„ ๋Š๋ผ์ง€ ๋ชปํ•˜์ง€๋งŒ, ํ˜‘์—… ํ™˜๊ฒฝ์—์„œ๋Š” ๊ฐœ๋ฐœ์ž๋“ค์˜ ์ฝ”๋”ฉ ์Šคํƒ€์ผ์ด ๋‹ค์–‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ฝ”๋“œ์˜ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜๋‹ค. ์ด๋Ÿฐ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ํšจ๊ณผ์ ์ธ ๋ฐฉ๋ฒ• ์ค‘ ํ•˜๋‚˜๊ฐ€ ESLint๋‹ค. Prettier์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ๋” ๊ฐ•๋ ฅํ•˜๋‹ค! ํ•˜์ง€๋งŒ ์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ๋Š” Prettier์— ๊ด€๋ จ๋œ ๋‚ด์šฉ์€ ์—†๊ณ  ESLint์— ๋Œ€ํ•ด์„œ ์ง‘์ค‘์ ์œผ๋กœ ์•Œ์•„๋ณด๋ ค๊ณ  ํ•œ๋‹ค. ์—ฌํƒœ ๋ฆฐํŠธ๋ฅผ ์ž˜ ์ ์šฉํ•ด์„œ ์‚ฌ์šฉํ•ด์™”์ง€๋งŒ ์–ด๋–ป๊ฒŒ ๋ฆฐํŠธ๊ฐ€ ๋™์ž‘ํ•˜๋Š”์ง€์— ๋Œ€ํ•ด ๊ถ๊ธˆํ–ˆ๋˜ ์‚ฌ๋žŒ๋“ค์—๊ฒŒ ๋„์›€์ด ๋  ๊ฒƒ์ด๋ผ ์ƒ๊ฐํ•œ๋‹ค.

#ESLint ๋ž€?

ESLint(์ •์  ๋ถ„์„ ๋„๊ตฌ)๋Š” ์ฝ”๋“œ๋ฅผ ์ •์ ์œผ๋กœ ๋ถ„์„ํ•ด์„œ ๋ฌธ๋ฒ• ์˜ค๋ฅ˜๋‚˜ ์˜คํƒ€ ๋“ฑ์˜ ์ž ์žฌ์  ์—๋Ÿฌ๋ฅผ ์ฐพ์•„๋‚ด๊ณ , ์ฝ”๋”ฉ ์ปจ๋ฒค์…˜ ๊ฒ€์ฆ์„ ์ž๋™ํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋Œ€ํ˜•ํšŒ์‚ฌ์ธ Airbnb๋‚˜ Naver ๋“ฑ ์—์„œ๋Š” ์ž๊ธฐ๋“ค์˜ ESLint ์„ค์ •์„ ๊ณต๊ฐœํ•ด๋†“๊ธฐ๋„ ํ•œ๋‹ค. ๊ทธ๋งŒํผ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ๋„๊ตฌ์ด๋‹ค. ๊ทธ๋Ÿผ ์–ด๋–ค ๋ฐฉ๋ฒ•์œผ๋กœ ์ฝ”๋“œ๋ฅผ ์ •์  ๋ถ„์„ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฑธ๊นŒ? ESLint๋Š” ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๊ตฌ๋ฌธ ๋ถ„์„๊ธฐ์ธ Espree๋ฅผ ์ด์šฉํ•ด์„œ ์ฝ”๋“œ๋ฅผ ๋ฌธ์ž์—ด๋กœ ์ฝ์€ ๋’ค ์ฝ”๋“œ๋ฅผ ๊ตฌ์กฐํ™” ํ•œ๋‹ค. ์—ฌ๊ธฐ์„œ ๊ตฌ์กฐํ™”ํ•œ ํŠธ๋ฆฌ๋ฅผ AST(Abstract Syntax Tree)๋ผ๊ณ  ํ•˜๊ณ , ์ด ํŠธ๋ฆฌ๋ฅผ ๊ฐ€์ง€๊ณ  ์—ฌ๋Ÿฌ๊ฐ€์ง€ eslint rule๊ณผ ๋Œ€์กฐํ•ด์„œ ๊ฒฝ๊ณ ๋ฅผ ํ†ตํ•ด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฐœ๋ฐœ์ž์—๊ฒŒ ์•Œ๋ ค์ค€๋‹ค.

#Espree

Espree๋Š” ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์ฝ”๋“œ๋ฅผ ์–ด๋–ป๊ฒŒ ํŒŒ์‹ฑํ•˜๋Š”๊ฑธ๊นŒ? AST Explorer์—์„œ ์ฝ”๋“œ๊ฐ€ ์–ด๋–ค ํ˜•ํƒœ๋กœ ๊ตฌ์กฐํ™” ๋˜๋Š”์ง€ ์ง์ ‘ ๋ˆˆ์œผ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. (์šฐ์ธก ๊ฒฐ๊ณผ ํ™”๋ฉด์—์„œ Treeํ˜•ํƒœ, JSONํ˜•ํƒœ๋กœ ํ† ๊ธ€ ํ•  ์ˆ˜ ์žˆ๋‹ค.)

js
const a = 1;
js
const a = 1;

์ด ์ฝ”๋“œ๋ฅผ ํŒŒ์‹ฑํ•œ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด

json
{
  "type": "Program",
  "start": 0,
  "end": 12,
  "body": [ // ํ”„๋กœ๊ทธ๋žจ์˜ ๋ณธ๋ฌธ์„ ๊ตฌ์„ฑํ•˜๋Š” ์š”์†Œ๋“ค, ์ง€๊ธˆ์€ const a = 1; ํ•˜๋‚˜๋งŒ ์žˆ๋‹ค.
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 12,
      "declarations": [
        {
          "type": "VariableDeclarator", // ๋ณ€์ˆ˜ ์„ ์–ธ
          "start": 6,
          "end": 11,
          "id": {
            "type": "Identifier", // ๋ณ€์ˆ˜ ์ด๋ฆ„
            "start": 6,
            "end": 7,
            "name": "a" // ๋ณ€์ˆ˜ a ๋Š”
          },
          "init": { // ๋ณ€์ˆ˜ ์ดˆ๊ธฐํ™”
            "type": "Literal", // ๋ฆฌํ„ฐ๋Ÿด ๊ฐ’
            "start": 10,
            "end": 11,
            "value": 1, // 1์œผ๋กœ ์ดˆ๊ธฐํ™” ๋œ๋‹ค.
            "raw": "1"
          }
        }
      ],
      "kind": "const" // ๋ณ€์ˆ˜ ์„ ์–ธ์˜ ์ข…๋ฅ˜ const์ธ์ง€ let์ธ์ง€ var์ธ์ง€
    }
  ],
  "sourceType": "module"
}
json
{
  "type": "Program",
  "start": 0,
  "end": 12,
  "body": [ // ํ”„๋กœ๊ทธ๋žจ์˜ ๋ณธ๋ฌธ์„ ๊ตฌ์„ฑํ•˜๋Š” ์š”์†Œ๋“ค, ์ง€๊ธˆ์€ const a = 1; ํ•˜๋‚˜๋งŒ ์žˆ๋‹ค.
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 12,
      "declarations": [
        {
          "type": "VariableDeclarator", // ๋ณ€์ˆ˜ ์„ ์–ธ
          "start": 6,
          "end": 11,
          "id": {
            "type": "Identifier", // ๋ณ€์ˆ˜ ์ด๋ฆ„
            "start": 6,
            "end": 7,
            "name": "a" // ๋ณ€์ˆ˜ a ๋Š”
          },
          "init": { // ๋ณ€์ˆ˜ ์ดˆ๊ธฐํ™”
            "type": "Literal", // ๋ฆฌํ„ฐ๋Ÿด ๊ฐ’
            "start": 10,
            "end": 11,
            "value": 1, // 1์œผ๋กœ ์ดˆ๊ธฐํ™” ๋œ๋‹ค.
            "raw": "1"
          }
        }
      ],
      "kind": "const" // ๋ณ€์ˆ˜ ์„ ์–ธ์˜ ์ข…๋ฅ˜ const์ธ์ง€ let์ธ์ง€ var์ธ์ง€
    }
  ],
  "sourceType": "module"
}

๊ฐ„๋‹จํ•˜๊ฒŒ ๋ณ€์ˆ˜ a์— 1์„ ํ• ๋‹นํ•˜๋Š” ์ฝ”๋“œ์ž„์—๋„ ๊ต‰์žฅํžˆ ์ž์„ธํ•˜๊ณ  ๋‹ค์–‘ํ•œ ์ •๋ณด๊ฐ€ ๋‹ด๊ฒจ์žˆ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ์ฝ”๋“œ์˜ ์‹œ์ž‘์ ๊ณผ ๋, ๋ณ€์ˆ˜์ธ์ง€ ํ•จ์ˆ˜์ธ์ง€(ํ™”์‚ดํ‘œ ํ•จ์ˆ˜์ธ์ง€) ๋“ฑ๋“ฑ ์•„์ฃผ ์ƒ์„ธํ•œ ์ •๋ณด๊ฐ€ ๋‹ด๊ฒจ์žˆ๋‹ค. ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์ธ ๊ฒฝ์šฐ์—๋„ espree ๊ธฐ๋ฐ˜ ํŒŒ์„œ๊ฐ€ ์กด์žฌํ•ด์„œ, ๋ถ„์„ํ•  ์ˆ˜ ์žˆ๋‹ค. AST Explorer์—์„œ ๊ฒฐ๊ณผ๋ฌผ์„ ๋ณด๋ฉด ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ๊ตฌ์กฐํ™” ํ–ˆ์„ ๋•Œ ๋‚˜์˜จ ๊ฒฐ๊ณผ์— ๋”ํ•ด์„œ type์— ๊ด€ํ•œ ์ •๋ณด๋„ ์ถ”๊ฐ€์ ์œผ๋กœ ๊ฒฐ๊ณผ์— ๋ณด์—ฌ์ง„๋‹ค.

typescript ์ฝ”๋“œ ๊ฒฐ๊ณผ
typescript ์ฝ”๋“œ ๊ฒฐ๊ณผ

ESLint๋Š” ์ด espree๊ฐ€ ์ฝ”๋“œ๋ฅผ ๋ถ„์„ํ•œ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ€์ง€๊ณ  ๊ฒ€์‚ฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ณ , ์ง€์ •ํ•ด๋†“์€ ๊ทœ์น™์— ๋”ฐ๋ผ ์–ด๋””๊ฐ€ ์ž˜๋ชป๋๋Š”์ง€ ๊ทธ๋ฆฌ๊ณ  ์–ด๋–ป๊ฒŒ ์ˆ˜์ •ํ•ด์•ผํ•˜๋Š” ์ง€ ๊ฐœ์„  ๋ฐฉ๋ฒ•์„ ์ œ์‹œํ•ด์ค€๋‹ค. ์ด ๊ทœ์น™์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด์ž.

#ESLint Plugin๊ณผ Config

ESLint ์„ค์ •์„ ํ• ๋•Œ eslint-plugin- ์ด๋‚˜ eslint-config- ์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋‹ค์–‘ํ•œ npm ํŒจํ‚ค์ง€๊ฐ€ ์กด์žฌํ•œ๋‹ค. plugin๊ณผ config์˜ ์—ญํ• ๊ณผ ์ฐจ์ด์ ์„ ์‚ดํŽด๋ณด์ž.

Plugin

ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ์ƒˆ๋กœ์šด ๊ทœ์น™๋“ค์„ ESLint์— ์ถ”๊ฐ€ํ•œ๋‹ค. ์ด๋Ÿฐ ํ”Œ๋Ÿฌ๊ทธ์ธ๋“ค์€ ํŠน์ • ํ”„๋กœ๊ทธ๋ž˜๋ฐ ํŒจํ„ด, ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ, ํ”„๋ ˆ์ž„์›Œํฌ์— ๋Œ€ํ•œ ์ถ”๊ฐ€์ ์ธ ๊ฒ€์‚ฌ ๊ทœ์น™์„ ์ œ๊ณตํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด eslint-plugin-react๋Š” useEffect ์˜ ์˜์กด์„ฑ ๋ฐฐ์—ด์„ ์ฒดํฌํ•ด์ฃผ๊ฑฐ๋‚˜, ์ปดํฌ๋„ŒํŠธ์— key ์†์„ฑ์„ ์ฃผ์ง€ ์•Š์€ ๊ฒฝ์šฐ ๋“ฑ ๋‹ค์–‘ํ•œ ๋ถ€๋ถ„์„ ์ฒดํฌํ•ด์ค€๋‹ค.

Config

Config๋กœ ์‹œ์ž‘ํ•˜๋Š” ํŒจํ‚ค์ง€๋Š” ์•ž์„œ ๋งํ–ˆ๋˜ Plugin๋“ค๊ณผ rules๋ฅผ ๋ฌถ์–ด์„œ ์ œ๊ณตํ•˜๋Š” ์„ค์ •ํŒŒ์ผ๋กœ ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค. ๋Œ€ํ‘œ์ ์œผ๋กœ eslint-config-airbnb, eslint-config-prettier, eslint-config-naver ๋“ฑ ๋‹ค์–‘ํ•œ ํŒจํ‚ค์ง€๊ฐ€ ์กด์žฌํ•œ๋‹ค.

config ์ปค๋‹ค๋ž€ ์„ค์ •ํŒŒ์ผ
config ์ปค๋‹ค๋ž€ ์„ค์ •ํŒŒ์ผ
eslint-config-next์˜ ๋‚ด๋ถ€ ์ฝ”๋“œ
eslint-config-next์˜ ๋‚ด๋ถ€ ์ฝ”๋“œ

.eslintrc ์„ค์ • ํŒŒ์ผ์—์„œ

.eslintrc ์„ค์ • ํŒŒ์ผ์—์„œ extends๋Š” ๋‹ค๋ฅธ ์„ค์ •ํŒŒ์ผ์„ ์ƒ์†(extends)๋ฐ›์•„์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋œ๋‹ค. plugins๋Š” ์ถ”๊ฐ€์ ์ธ ๊ทœ์น™์ด๋‚˜ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์ถ”๊ฐ€ํ•  ๋•Œ ์‚ฌ์šฉ๋œ๋‹ค. rules์—๋Š” ๊ทธ ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์–ด๊ฒผ์„ ๋•Œ ๋™์ž‘ํ•˜๊ณ ํ”ˆ๊ฑธ ์ž…๋ ฅํ•ด์ฃผ๋ฉด ๋œ๋‹ค.(ํ™œ์„ฑํ™”, ๋น„ํ™œ์„ฑํ™”, ์—๋Ÿฌ, ๊ฒฝ๊ณ  ๋“ฑ๋“ฑ)

#๋‚˜๋งŒ์˜ Custom ESLint Rule ๋งŒ๋“ค๊ธฐ

ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋‹ค๋ณด๋ฉด, ํŒ€ ๋‚ด์—์„œ ์ฝ”๋”ฉ ์ปจ๋ฒค์…˜์„ ์œ ์ง€ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜๋‹ค. ๊ทธ๊ฑธ ์œ„ํ•ด์„œ ESLint์˜ ๋„์›€์„ ๋ฐ›๋Š”๋ฐ, ๊ฐ€๋” ํŒ€์ด ์ •ํ•œ ํŠน์ •ํ•œ ๊ทœ์น™์„ ์ฝ”๋“œ์— ๋ฐ˜์˜ํ•˜๊ณ  ์‹ถ์–ด๋„ ๊ธฐ์กด์— ์ œ๊ณต๋˜๋Š” ESLint ์„ค์ •์ด๋‚˜ ํ”Œ๋Ÿฌ๊ทธ์ธ์œผ๋กœ๋Š” ์ถฉ๋ถ„ํžˆ ๋Œ€์‘ํ•  ์ˆ˜ ์—†๋Š” ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ๋‹ค. ์ด ๋•Œ, ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ๊ทœ์น™์„ ๋งŒ๋“ค์–ด์„œ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๊ทœ์น™์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•๊ณผ ์•„์˜ˆ ์ƒˆ๋กœ์šด ๊ทœ์น™์„ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๋Š”๋ฐ ์—ฌ๊ธฐ์„œ๋Š” ํ›„์ž์˜ ๋ฐฉ๋ฒ•์„ ์˜ˆ์‹œ๋กœ ๋“ค์–ด์„œ ์„ค๋ช…ํ•œ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ๋ณ€์ˆ˜์˜ ๊ธธ์ด๋Š” ์ตœ์†Œ 2๊ธ€์ž ์ด์ƒ๋งŒ ํ—ˆ์šฉํ•˜๊ณ  ์‹ถ์€ ESLint ๊ทœ์น™์„ ์ƒ์„ฑํ•œ๋‹ค๊ณ  ํ•ด๋ณด์ž. ๊ทœ์น™์„ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋จผ์ € ๋ณ€์ˆ˜๋ฅผ ์„ ์–ธํ–ˆ์„ ๋•Œ, AST๊ฐ€ ์–ด๋–ป๊ฒŒ ์ƒ์„ฑ๋๋Š” ์ง€ ํ™•์ธํ•ด์•ผํ•œ๋‹ค.

js
const a = "1234"
js
const a = "1234"

๋ณ€์ˆ˜๋ช…์˜ ๊ธธ์ด๊ฐ€ 1์ธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ๋‹ค.

json
{
  "type": "Program",
  "start": 0,
  "end": 17,
  "range": [
    0,
    17
  ],
  "body": [
    {
      "type": "VariableDeclaration", // 1) ๋ณ€์ˆ˜์„ ์–ธ ์ „์ฒด๋ฅผ ๋‚˜ํƒ€๋ƒ„
      "start": 0,
      "end": 17,
      "range": [
        0,
        17
      ],
      "declarations": [ // 2) ๋ณ€์ˆ˜ ์„ ์–ธ ๋ชฉ๋ก
        {
          "type": "VariableDeclarator", // 3) const๋ฅผ ์ œ์™ธํ•œ a = "1234";
          "start": 6,
          "end": 16,
          "range": [
            6,
            16
          ],
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 7,
            "range": [
              6,
              7
            ],
            "name": "a" // 4) ์šฐ๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ ๊ฒƒ
          },
          "init": {
            "type": "Literal",
            "start": 10,
            "end": 16,
            "range": [
              10,
              16
            ],
            "value": "1234",
            "raw": "\"1234\""
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}
json
{
  "type": "Program",
  "start": 0,
  "end": 17,
  "range": [
    0,
    17
  ],
  "body": [
    {
      "type": "VariableDeclaration", // 1) ๋ณ€์ˆ˜์„ ์–ธ ์ „์ฒด๋ฅผ ๋‚˜ํƒ€๋ƒ„
      "start": 0,
      "end": 17,
      "range": [
        0,
        17
      ],
      "declarations": [ // 2) ๋ณ€์ˆ˜ ์„ ์–ธ ๋ชฉ๋ก
        {
          "type": "VariableDeclarator", // 3) const๋ฅผ ์ œ์™ธํ•œ a = "1234";
          "start": 6,
          "end": 16,
          "range": [
            6,
            16
          ],
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 7,
            "range": [
              6,
              7
            ],
            "name": "a" // 4) ์šฐ๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ ๊ฒƒ
          },
          "init": {
            "type": "Literal",
            "start": 10,
            "end": 16,
            "range": [
              10,
              16
            ],
            "value": "1234",
            "raw": "\"1234\""
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}
  1. VariableDeclaration: ๋ณ€์ˆ˜ ์„ ์–ธ ์ „์ฒด๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค. const a = "1234"const a = "1234"
  2. declarations: ๋ณ€์ˆ˜ ์„ ์–ธ ๋ชฉ๋ก์„ ๋‚˜ํƒ€๋‚ธ๋‹ค. ์—ฌ๊ธฐ์„œ๋Š” const a = "1234"const a = "1234" ๋ผ์„œ ๋ฐฐ์—ด์— ํ•˜๋‚˜๋ฐ–์— ์—†์ง€๋งŒ, const a = "1234", b = "56";const a = "1234", b = "56"; ์ด๋Ÿฐ ์‹์œผ๋กœ ์„ ์–ธํ•˜๋ฉด declarations ๋ฐฐ์—ด์— ์š”์†Œ๊ฐ€ ์ถ”๊ฐ€๋œ๋‹ค.
  3. VariableDeclarator: const๋ฅผ ์ œ์™ธํ•œ a = "1234"a = "1234" ๋ถ€๋ถ„์„ ์˜๋ฏธํ•œ๋‹ค.
  4. name: ๋ณ€์ˆ˜๋ช…์„ ์˜๋ฏธํ•˜๋Š”๋ฐ, ์šฐ๋ฆฌ๋Š” ์ด name์˜ ๊ธธ์ด๋ฅผ ์ฒดํฌํ•ด์„œ 2๋ณด๋‹ค ์ž‘๋‹ค๋ฉด ๊ฒฝ๊ณ ๋ฅผ ํ•ด์ฃผ๋Š” ๋ฆฐํŠธ ๋ฃฐ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋‹ค.

#ESLint Rule(Plugin) ํ”„๋กœ์ ํŠธ ์ƒ์„ฑํ•˜๊ธฐ

ESLint ๊ทœ์น™์„ ์ž‘์„ฑํ•˜๊ธฐ ์œ„ํ•ด, ์•ฑ ํ”„๋กœ์ ํŠธ์™€ ๋ณ„๊ฐœ๋กœ ํ”Œ๋Ÿฌ๊ทธ์ธ ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•ด์•ผํ•œ๋‹ค. ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ๋ฌด์กฐ๊ฑด eslint-plugin- ์œผ๋กœ ์‹œ์ž‘ํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ปจ๋ฒค์…˜์— ๋งž์ถฐ์„œ ์ƒ์„ฑํ•ด์ฃผ์ž.

bash
mkdir eslint-plugin-my
cd eslint-plugin-my
npm init --y
touch index.js
bash
mkdir eslint-plugin-my
cd eslint-plugin-my
npm init --y
touch index.js

index.jx ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ  ๊ทœ์น™์„ ์ž‘์„ฑํ•œ๋‹ค.

js
module.exports.rules = {
  // ... meta ํ•„๋“œ
 
  // ๋ฃฐ ์ด๋ฆ„์„ ์„ ์–ธํ•œ๋‹ค.
  "variable-length": {
    create: function (context) {
      return {
        // VariableDeclarator ๋…ธ๋“œ๊ฐ€ ๋ฐœ๊ฒฌ๋  ๋•Œ ์‹คํ–‰๋˜๋Š” ํ•จ์ˆ˜
        VariableDeclarator: (node) => {
          // node.id.name์€ ๋ณ€์ˆ˜์˜ ์ด๋ฆ„์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค. (์œ„์ชฝ json ์ฐธ๊ณ )
          // ์—ฌ๊ธฐ์„œ๋Š” ๋ณ€์ˆ˜ ์ด๋ฆ„์˜ ๊ธธ์ด๋ฅผ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค.
          if (node.id.name.length < 2) {
            context.report({
              node,
              message: "Variable names should be longer than 1 character"
            });
          }
        }
      };
    }
  }
};
js
module.exports.rules = {
  // ... meta ํ•„๋“œ
 
  // ๋ฃฐ ์ด๋ฆ„์„ ์„ ์–ธํ•œ๋‹ค.
  "variable-length": {
    create: function (context) {
      return {
        // VariableDeclarator ๋…ธ๋“œ๊ฐ€ ๋ฐœ๊ฒฌ๋  ๋•Œ ์‹คํ–‰๋˜๋Š” ํ•จ์ˆ˜
        VariableDeclarator: (node) => {
          // node.id.name์€ ๋ณ€์ˆ˜์˜ ์ด๋ฆ„์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค. (์œ„์ชฝ json ์ฐธ๊ณ )
          // ์—ฌ๊ธฐ์„œ๋Š” ๋ณ€์ˆ˜ ์ด๋ฆ„์˜ ๊ธธ์ด๋ฅผ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค.
          if (node.id.name.length < 2) {
            context.report({
              node,
              message: "Variable names should be longer than 1 character"
            });
          }
        }
      };
    }
  }
};

์ƒ๋žตํ•œ meta ํ•„๋“œ๋Š” ๋ฃฐ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ํ•„๋“œ๋‹ค. ์‹ค์ œ๋กœ ๊ทœ์น™์ด ๋™์ž‘ํ•˜๊ธฐ ์œ„ํ•œ ์ฝ”๋“œ์™€๋Š” ๊ด€๋ จ์ด ์—†๊ธฐ ๋•Œ๋ฌธ์— ์ƒ๋žตํ–ˆ๋‹ค.

create ํ•จ์ˆ˜๋Š” ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์ด ๊ฐ์ฒด์—์„œ๋Š” ์ฝ”๋“œ ์Šค๋ฉœ์„ ๊ฐ์ง€ํ•  ์„ ํƒ์ž๋‚˜ ์ด๋ฒคํŠธ๋ช…์„ ์„ ์–ธํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด ์ฝ”๋“œ์˜ ํ•ต์‹ฌ์€ VariableDeclarator ๋…ธ๋“œ์— ๋Œ€ํ•œ ์ฝœ๋ฐฑํ•จ์ˆ˜์ธ๋ฐ ์ด ํ•จ์ˆ˜๋Š” ๊ฐ ๋ณ€์ˆ˜ ์„ ์–ธ์ž ๋…ธ๋“œ(VariableDeclarator)๊ฐ€ ๋ฐœ๊ฒฌ๋  ๋•Œ ๋งˆ๋‹ค ํ˜ธ์ถœ๋œ๋‹ค. ํ•จ์ˆ˜ ๋‚ด๋ถ€์—์„œ๋Š” node.id.name์„ ํ†ตํ•ด ๋ณ€์ˆ˜ ์ด๋ฆ„์— ์ ‘๊ทผํ•˜๊ณ , ์ด ์ด๋ฆ„์˜ ๊ธธ์ด๊ฐ€ 2๋ณด๋‹ค ์ž‘์€์ง€ ๊ฒ€์‚ฌํ•œ๋‹ค. ๋งŒ์•ฝ ์ž‘๋‹ค๋ฉด context.report ๋ฉ”์„œ๋“œ๋กœ ESLint์—๊ฒŒ ๊ฒฝ๊ณ ๋ฅผ ๋ณด๊ณ ํ•œ๋‹ค.

๋งŒ์•ฝ meta ํ•„๋“œ์—์„œ ๋ฉ”์„ธ์ง€๋ฅผ ์ž‘์„ฑํ•ด๋†จ๋‹ค๋ฉด meta.messages ๊ฐ€์ ธ์˜ฌ messageId๋ฅผ report์— ์ „๋‹ฌํ•ด์ฃผ๋ฉด ๊ทธ ๋ฉ”์„ธ์ง€๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ๋„ ์žˆ๊ณ , fix๋ฅผ ํ†ตํ•ด์„œ ์ž๋™์œผ๋กœ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋Š” ์ฝ”๋“œ๋ฅผ ๋„ฃ์–ด ์ค„ ์ˆ˜๋„ ์žˆ๋‹ค. (์˜ˆ, useEffect์˜ ์˜์กด์„ฑ ๋ฐฐ์—ด์„ ์ž๋™์œผ๋กœ ๋„ฃ์–ด์ฃผ๋Š” ๊ทธ๋Ÿฐ ์ฝ”๋“œ)

#์•ฑ์— ์ปค์Šคํ…€ ๊ทœ์น™ ์ ์šฉํ•˜๊ธฐ

ํ”Œ๋Ÿฌ๊ทธ์ธ ํ”„๋กœ์ ํŠธ๋ฅผ ์ž‘์„ฑํ–ˆ์œผ๋‹ˆ ์•ฑ ํ”„๋กœ์ ํŠธ๋กœ ๋Œ์•„๊ฐ€์„œ ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  ๋ฆฐํŠธ ๊ทœ์น™์„ ์ ์šฉํ•ด๋ณด์ž.

ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์šฐ๋ฆฌ ํ”„๋กœ์ ํŠธ์—์„œ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ ํ”„๋กœ์ ํŠธ๋ฅผ npm์— ๋“ฑ๋กํ•˜๊ณ , ์•ฑ์—์„œ ์„ค์น˜ํ•ด์•ผ ํ•˜๋Š”๋ฐ ํ”Œ๋Ÿฌ๊ทธ์ธ์„ publish ํ•˜์ง€ ์•Š๊ณ  ๋กœ์ปฌ์—์„œ ์—ฐ๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ๋‹ค.

ํ”Œ๋Ÿฌ๊ทธ์ธ ํ”„๋กœ์ ํŠธ์—์„œ ์•„๋ž˜์˜ ๋ช…๋ น์–ด๋กœ ๋งํฌ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

bash
cd eslint-plugin-my
npm link
bash
cd eslint-plugin-my
npm link

๊ทธ ๋‹ค์Œ ์•ฑ ํ”„๋กœ์ ํŠธ์—์„œ ์—ฐ๊ฒฐํ•ด์ฃผ์ž

bash
cd my-app
npm link eslint-plugin-my
bash
cd my-app
npm link eslint-plugin-my
์—ฐ๊ฒฐ๋œ ๋ชจ์Šต
์—ฐ๊ฒฐ๋œ ๋ชจ์Šต

์—ฐ๊ฒฐ์ด ์ž˜ ๋๋‹ค๋ฉด, ์•ฑ ํ”„๋กœ์ ํŠธ์˜ ESLint ์„ค์ • ํŒŒ์ผ์—์„œ

js
module.exports = {
  // ...
  plugins: ['react-refresh', 'my'], // <-
  rules: {
    'react-refresh/only-export-components': [
      'warn',
      {allowConstantExport: true},
    ],
    'react/prop-types': 'warn',
    'my/variable-length': 'warn' // <-
  }
};
js
module.exports = {
  // ...
  plugins: ['react-refresh', 'my'], // <-
  rules: {
    'react-refresh/only-export-components': [
      'warn',
      {allowConstantExport: true},
    ],
    'react/prop-types': 'warn',
    'my/variable-length': 'warn' // <-
  }
};

ํ”Œ๋Ÿฌ๊ทธ์ธ ๋ฐฐ์—ด์— ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  eslint-plugin-my ์—์„œ์˜ my๋ฅผ ์ž…๋ ฅํ•˜๊ณ , rules ๋ฅผ ์„ค์ •ํ•  ๋•, ์šฐ๋ฆฌ my(rules) ๋‚ด๋ถ€์˜ variable-length๋ฅผ ์–ด๋–ป๊ฒŒ ์ œ์–ดํ•  ๊ฒƒ์ธ์ง€ ์ž‘์„ฑํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

์ด์ œ ์•ฑ์—์„œ const a = 1;const a = 1; ์ฒ˜๋Ÿผ ๋ณ€์ˆ˜๋ช…์ด ํ•œ ๊ธ€์ž๋ผ๋ฉด ๊ฒฝ๊ณ ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ฌ ๊ฒƒ์ด๋‹ค.

๋ณ€์ˆ˜๋ช… ๊ธธ์ด๊ฐ€ 1์ธ ๊ฒฝ์šฐ
๋ณ€์ˆ˜๋ช… ๊ธธ์ด๊ฐ€ 1์ธ ๊ฒฝ์šฐ
map((_, index) => ) ์ฒ˜๋Ÿผ `_`๋Š” ์ œ์™ธํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ
js
module.exports = {
    'var-length': (context) => ({
      VariableDeclarator: (node) => {
        const { options } = context
        const allowedList = options.find((opt) => 'allowed' in opt)
        const allowed = allowedList.allowed || []
 
        if (node.id.name.length < 2 && !allowed.includes(node.id.name)) { // ์กฐ๊ฑด ์ถ”๊ฐ€
          context.report(
            node,
            `Variable names should be longer than 1 character ${node.type}`,
          )
        }
      },
    }),
  },
}
js
module.exports = {
    'var-length': (context) => ({
      VariableDeclarator: (node) => {
        const { options } = context
        const allowedList = options.find((opt) => 'allowed' in opt)
        const allowed = allowedList.allowed || []
 
        if (node.id.name.length < 2 && !allowed.includes(node.id.name)) { // ์กฐ๊ฑด ์ถ”๊ฐ€
          context.report(
            node,
            `Variable names should be longer than 1 character ${node.type}`,
          )
        }
      },
    }),
  },
}
js
module.exports = {
// ...
rules: {
  // ...
  {allowConstantExport: true},
    ],
    'react/prop-types': 'warn',
    'my/variable-length': ['warn', { allowed: ['_'] }] // <-
  }
};
js
module.exports = {
// ...
rules: {
  // ...
  {allowConstantExport: true},
    ],
    'react/prop-types': 'warn',
    'my/variable-length': ['warn', { allowed: ['_'] }] // <-
  }
};
_ ๋Š” ๊ฒฝ๊ณ ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค.
_ ๋Š” ๊ฒฝ๊ณ ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค.

#๋งˆ์น˜๋ฉฐ

๊ทธ๋ƒฅ ์—์–ด๋น„์—”๋น„ ๋•Œ๋ ค๋ฐ•๊ณ  rules ๊ฒ€์ƒ‰ํ•ด์„œ warn ์œผ๋กœ ๋ฐ”๊ฟ”๊ฐ€๋ฉด์„œ ESLint๋ฅผ ์‚ฌ์šฉํ–ˆ์—ˆ์ง€๋งŒ, ์ด๋ฒˆ ํฌ์ŠคํŒ…์„ ์ž‘์„ฑํ•˜๋ฉด์„œ ๋‹จ์ˆœํžˆ ๊ทœ์น™์„ ์ ์šฉํ•˜๋Š” ๊ฒƒ์„ ๋„˜์–ด์„œ, ๊ทธ ๊ทœ์น™๋“ค์ด ์™œ ์ค‘์š”ํ•˜๊ณ  ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€๋ฅผ ์ดํ•ดํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

#์ฐธ๊ณ ์ž๋ฃŒ

๋‚˜๋งŒ์˜ eslint ๋ฃฐ ๋งŒ๋“ค์–ด๋ณด๊ธฐ - yceffort

๋ชจ๋˜ ๋ฆฌ์•กํŠธ Deep Dive

์ปค์Šคํ…€ ESLint ๊ทœ์น™ ๋งŒ๋“ค๊ธฐ - essem