본문으로 건너뛰기
yceffort
PostsSeriesTagsAbout🧪 Research
EN

Tweaks

theme
accent palette
film grain
minimal mode
BACK TO INDEX
◆ ESSAY
--min
--year
KOoriginal

mailMail icongithubtwitter
yceffort
•
© 2026
•
https://yceffort.kr
BACK TO INDEX
◆ ESSAY

나만의 eslint 룰 만들어보기

avatar
yceffort
2022-06-26 · 8분
8min
2022year
KOoriginal
eslintjavascript

Table of Contents

  • Introduction
  • no-restricted-imports를 쓰는 방법
  • eslint 룰 만들기?
    • eslint 동작 방식 이해
    • 간단한 예제
    • import React 만들어보기
  • 참고

Introduction

react@17 이 업데이트 되면서 더이상 jsx, tsx 파일에 import React를 할 필요가 없어졌다. 참고 이를 사용함으로써 여러가지 이점이 있지만, 무엇보다 번들 사이즈가 줄어든 다는 장점이 가장 크다. (아주 작은 정도지만)

그러나 기존 react@16 기반의 코드에서 저 import React from 'react' 코드를 모두 제거하기란 쉽지 않다. import React from 'react'를 모두 찾고 검색해서 지우는 방법도 있겠지만, 저 사이에 무엇이라도 껴 있다면, (import React, { MouseEvent } from 'react' 와 같이) 이 방법도 소용이 없다. 그래서 어떻게 해결할까 고민하던 중, eslint가 있으니 이를 활용하면 쉽게 해결할 수 있지 않을까 하는 아이디어가 떠올랐다.

no-restricted-imports를 쓰는 방법

아마도 대부분의 프로젝트에서는 eslint를 사용 중일 것이다. 그래서 eslint에 있는 기본 룰인 no-restricted-imports를 사용해서 해결해보자.

module.exports = {
  rules: {
    'react/react-in-jsx-scope': ['off'],
    'no-restricted-imports': [
      'error',
      {
        paths: [
          {
            name: 'react',
            importNames: ['default'],
            message: "import React from 'react' makes bundle size larger.",
          },
        ],
      },
    ],
  },
}

react 라는 import가 있고, 이 importNames이 기본값 (React)일 경우 에러메시지를 띄우는 방법이다. 이방법을 활용하면 같은 원리로 트리쉐이킹이 안되는 lodash import 하는 것을 막을 수 있다.

하지만 아쉽게도 이 방법은 자동으로 fix 까지 해주지 않는다. 물론 자동으로 import를 해서 fix 할 수도 있겠지만, 그것보다는 개발자가 직접 수정하는 것이 더 안전할 것이다.

eslint 룰 만들기?

이 방법으로 문제를 해결하긴 했지만, 갑자기 궁금했졌다. 내가 직접 관련된 문제를 해결할 수 있는 rules을 만들어 볼 순 없을까? 🧐

eslint 동작 방식 이해

eslint 의 동작방식을 이해하기 위해서 알아야 하는 단 한가지는 바로 AST다. 이 글을 요약해서 설명하자면, AST는 우리가 작성한 코드를 기반으로 트리 구조의 데이터 스트럭쳐를 만들어 낸다. 즉, eslint 는 코드를 AST를 활용해서 트리구조를 만든 다음, 여기에서 지적하고 싶은 코드를 만들어서 룰로 저장하는 것이다.

간단한 예제

먼저, 한 글자 짜리 변수를 막는 룰을 만든다고 가정해보자. https://astexplorer.net/ 에서 변수 선언문 트리를 만들면, 아래와 같은 결과를 얻을 수 있다.

const hello = 'world'

그럼 아래와 같은 트리를 확인할 수 있다.

{
  "type": "Program",
  "start": 0,
  "end": 21,
  "loc": {
    "start": {
      "line": 1,
      "column": 0
    },
    "end": {
      "line": 1,
      "column": 21
    }
  },
  "range": [0, 21],
  "errors": [],
  "comments": [],
  "sourceType": "module",
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 21,
      "loc": {
        "start": {
          "line": 1,
          "column": 0
        },
        "end": {
          "line": 1,
          "column": 21
        }
      },
      "range": [0, 21],
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 21,
          "loc": {
            "start": {
              "line": 1,
              "column": 6
            },
            "end": {
              "line": 1,
              "column": 21
            }
          },
          "range": [6, 21],
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 11,
            "loc": {
              "start": {
                "line": 1,
                "column": 6
              },
              "end": {
                "line": 1,
                "column": 11
              },
              "identifierName": "hello"
            },
            "range": [6, 11],
            "name": "hello",
            "_babelType": "Identifier"
          },
          "init": {
            "type": "Literal",
            "start": 14,
            "end": 21,
            "loc": {
              "start": {
                "line": 1,
                "column": 14
              },
              "end": {
                "line": 1,
                "column": 21
              }
            },
            "range": [14, 21],
            "value": "world",
            "raw": "\"world\"",
            "_babelType": "Literal"
          },
          "_babelType": "VariableDeclarator"
        }
      ],
      "kind": "const",
      "_babelType": "VariableDeclaration"
    }
  ]
}

그리고 룰을 작성하기에 앞서, 먼저 룰이 포함되어 있는 프로젝트를 하나 만들어야 한다. (npm init) 그리고 중요한 것은, eslint-plugin-으로 시작해야 한다.

그리고 index.js를 만들고 다음과 같이 파일을 만들어보자.

module.exports = {
  rules: {
    // 룰 이름을 선언한다.
    'variable-length': (context) => ({
      // 변수 선언하는 부분은 VariableDeclarator 이다.
      VariableDeclarator: (node) => {
        // 변수명은 여기에 있다. (위 json 참고)
        if (node.id.name.length < 2) {
          context.report(
            node,
            `Variable names should be longer than 1 character`,
          )
        }
      },
    }),
  },
}

그리고 해당 룰을 적용해보자

/workspaces/eslint-plugin-import-yceffort/test/index.js
  3:7   warning  Variable names should be longer than 1 character VariableDeclarator  yceffort-rules/var-length

와...!!!

이번에는 옵션을 주어서, 특정 한글자 짜리 변수는 허용하도록 해보자. 예를 들어서 _와 같이.

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}`,
          )
        }
      },
    }),
  },
}
const rootRule = require('../.eslintrc.js')

module.exports = {
  ...rootRule,
  plugins: ['yceffort-rules'],
  rules: {
    'yceffort-rules/var-length': ['warn', {allowed: ['_']}],
  },
}

import React 만들어보기

원래 글의 목적이었던, import React from "react"나 import React from "lodash"와 같은 default import 를 막는 rule을 만들어보자.

먼저 AST Explorer로 트리구조를 살펴보자.

import React, {MouseEvent} from 'react'
import lodash from 'lodash'
{
  "type": "Program",
  "start": 0,
  "end": 69,
  "loc": {
    "start": {
      "line": 1,
      "column": 0
    },
    "end": {
      "line": 2,
      "column": 27
    }
  },
  "comments": [],
  "range": [0, 69],
  "sourceType": "module",
  "body": [
    {
      "type": "ImportDeclaration",
      "start": 0,
      "end": 41,
      "loc": {
        "start": {
          "line": 1,
          "column": 0
        },
        "end": {
          "line": 1,
          "column": 41
        }
      },
      "specifiers": [
        {
          "type": "ImportDefaultSpecifier",
          "start": 7,
          "end": 12,
          "loc": {
            "start": {
              "line": 1,
              "column": 7
            },
            "end": {
              "line": 1,
              "column": 12
            }
          },
          "local": {
            "type": "Identifier",
            "start": 7,
            "end": 12,
            "loc": {
              "start": {
                "line": 1,
                "column": 7
              },
              "end": {
                "line": 1,
                "column": 12
              },
              "identifierName": "React"
            },
            "name": "React",
            "range": [7, 12],
            "_babelType": "Identifier"
          },
          "range": [7, 12],
          "_babelType": "ImportDefaultSpecifier"
        },
        {
          "type": "ImportSpecifier",
          "start": 16,
          "end": 26,
          "loc": {
            "start": {
              "line": 1,
              "column": 16
            },
            "end": {
              "line": 1,
              "column": 26
            }
          },
          "imported": {
            "type": "Identifier",
            "start": 16,
            "end": 26,
            "loc": {
              "start": {
                "line": 1,
                "column": 16
              },
              "end": {
                "line": 1,
                "column": 26
              },
              "identifierName": "MouseEvent"
            },
            "name": "MouseEvent",
            "range": [16, 26],
            "_babelType": "Identifier"
          },
          "importKind": null,
          "local": {
            "type": "Identifier",
            "start": 16,
            "end": 26,
            "loc": {
              "start": {
                "line": 1,
                "column": 16
              },
              "end": {
                "line": 1,
                "column": 26
              },
              "identifierName": "MouseEvent"
            },
            "name": "MouseEvent",
            "range": [16, 26],
            "_babelType": "Identifier"
          },
          "range": [16, 26],
          "_babelType": "ImportSpecifier"
        }
      ],
      "importKind": "value",
      "source": {
        "type": "Literal",
        "start": 34,
        "end": 41,
        "loc": {
          "start": {
            "line": 1,
            "column": 34
          },
          "end": {
            "line": 1,
            "column": 41
          }
        },
        "extra": {
          "rawValue": "react",
          "raw": "'react'"
        },
        "value": "react",
        "range": [34, 41],
        "_babelType": "StringLiteral",
        "raw": "'react'"
      },
      "range": [0, 41],
      "_babelType": "ImportDeclaration"
    },
    {
      "type": "ImportDeclaration",
      "start": 42,
      "end": 69,
      "loc": {
        "start": {
          "line": 2,
          "column": 0
        },
        "end": {
          "line": 2,
          "column": 27
        }
      },
      "specifiers": [
        {
          "type": "ImportDefaultSpecifier",
          "start": 49,
          "end": 55,
          "loc": {
            "start": {
              "line": 2,
              "column": 7
            },
            "end": {
              "line": 2,
              "column": 13
            }
          },
          "local": {
            "type": "Identifier",
            "start": 49,
            "end": 55,
            "loc": {
              "start": {
                "line": 2,
                "column": 7
              },
              "end": {
                "line": 2,
                "column": 13
              },
              "identifierName": "lodash"
            },
            "name": "lodash",
            "range": [49, 55],
            "_babelType": "Identifier"
          },
          "range": [49, 55],
          "_babelType": "ImportDefaultSpecifier"
        }
      ],
      "importKind": "value",
      "source": {
        "type": "Literal",
        "start": 61,
        "end": 69,
        "loc": {
          "start": {
            "line": 2,
            "column": 19
          },
          "end": {
            "line": 2,
            "column": 27
          }
        },
        "extra": {
          "rawValue": "lodash",
          "raw": "'lodash'"
        },
        "value": "lodash",
        "range": [61, 69],
        "_babelType": "StringLiteral",
        "raw": "'lodash'"
      },
      "range": [42, 69],
      "_babelType": "ImportDeclaration"
    }
  ]
}

위 AST 트리를 기반으로 룰을 만들어보자.

module.exports = {
  rules: {
    'default-import': (context) => ({
      ImportDeclaration: (node) => {
        const found = node.specifiers.find(
          (i) => i.type === 'ImportDefaultSpecifier',
        )

        if (found) {
          const { options } = context
          const option = options.find((opt) => 'path' in opt)
          const paths = option.path || []

          if (paths.includes(node.source.value)) {
            context.report(node, `import ${node.source.value}는 하면 안되잉`)
          }
        }
      },
    }),
}
@yceffort ➜ /workspaces/eslint-plugin-import-yceffort/test (main ✗) $ npm run lint

> test@1.0.0 lint
> eslint '**/*.{js,ts}'

/workspaces/eslint-plugin-import-yceffort/test/index.js
  1:1  warning  import react는 하면 안되잉                                                 yceffort-rules/default-import
  2:1  warning  import lodash는 하면 안되잉                                                yceffort-rules/default-import

참고

eslint-plugin-yceffort-rules

물론 그냥 연습만 해보느라 여러가지로 썩 좋지 못한 코드니, 실제로 사용할 때는 적절하게 리팩토링을 해서 써보자.

관련 글

  • #javascript#eslint

    eslint, prettier, editorconfig 로 코드 컨벤션을 맞춘 후기

    예민이가 된 기분

    2020-11-11·11분
  • #eslint#javascript

    eslint-config-yceffort, 나만의 eslint-config 만들기

    나만의 일관된 javascript code를 위하여 만들어보았습니다.

    2020-09-15·3분
  • #javascript#animation#web-animations-api

    number-flow를 구형 브라우저로 이식하기: 다섯 가지 결정과 두 가지 번복

    number-flow가 애니메이션을 켜는 최소 버전은 Chrome 125, Safari 17.2다. 이 하한을 Chrome 66과 WebKit 16.4까지 내리는 포크를 만들면서 내린 결정들과, 뒤집게 된 판단 두 가지, 그리고 자동 강등을 포기한 Safari 버그 조사의 기록.

    2026-08-11·44분
  • #typescript#oxc#eslint

    typescript@7을 설치하면 벌어지는 일들: 블로그 모노레포 마이그레이션 기록

    pnpm lint가 12분 32초 걸리던 모노레포에 typescript 7.0.2를 넣어봤다. 타입체크는 조용히 지나갔는데 next build가 깨졌고, lint는 크래시했다. eslint와 prettier를 oxlint와 oxfmt로 갈아탄 하루의 연쇄 반응과 전후 실측 기록. 미리 말해두면, 빌드는 빨라지지 않았다.

    2026-08-10·17분

새 글을 놓치고 싶지 않으시다면 RSS로 구독해 주세요.

RSS 구독 →

yceffort — 프론트엔드 엔지니어입니다. 발표·기술 자문·기고 문의는 이곳에서 받고 있습니다.

← Back to the blogIssue on GitHub →