ESLint ์„ธํŒ…ํ•˜๊ธฐ (+ lint-staged)

2025. 8. 2. 14:49ใ†Front-end/Tools

๋ฐ˜์‘ํ˜•

๋ชฉ์ฐจ

1. ESLint๋ž€?

2. โš™๏ธ ESLint ์„ค์น˜ ๋ฐ ๊ธฐ๋ณธ ์„ค์ •

3. ๐Ÿ”— Prettier ์„ค์น˜ ๋ฐ ESLint์™€ ์—ฐ๋™

4. ๐Ÿ“‚ ์„ค์ • ํŒŒ์ผ ์ž‘์„ฑ (eslint.config.js)

5. ๐Ÿค– Husky + lint-staged๋กœ ์ปค๋ฐ‹ ์ „ ์ž๋™ ๊ฒ€์‚ฌ ์„ค์ •


1. ESLint๋ž€?

ESLint๋Š” JS/TS ์ฝ”๋“œ์—์„œ ๋ฌธ๋ฒ• ์˜ค๋ฅ˜, ์Šคํƒ€์ผ ์œ„๋ฐ˜, ๋ฒ„๊ทธ ๊ฐ€๋Šฅ์„ฑ ๋“ฑ์„ ์‚ฌ์ „์— ๊ฐ์ง€ํ•˜๋Š”

์ •์  ๋ถ„์„ ๋„๊ตฌ(linter)์ด๋‹ค.

 

ESLint๋ฅผ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•˜๋Š” ๊ณผ์ •์„ ์•Œ์•„๋ณด์ž.

  1. โš™๏ธ ESLint ์„ค์น˜ ๋ฐ ๊ธฐ๋ณธ ์„ค์ • (+ํ”Œ๋Ÿฌ๊ทธ์ธ ์ถ”๊ฐ€)
  2. ๐Ÿ”— Prettier ์„ค์น˜ ๋ฐ ESLint์™€ ์—ฐ๋™
  3. ๐Ÿ“‚ ์„ค์ • ํŒŒ์ผ ์ž‘์„ฑ (eslint.config.js)
  4. ๐Ÿค– Husky + lint-staged๋กœ ์ปค๋ฐ‹ ์ „ ์ž๋™ ๊ฒ€์‚ฌ ์„ค์ •

 


2. โš™๏ธ ESLint ์„ค์น˜ ๋ฐ ๊ธฐ๋ณธ ์„ค์ •

๋ช…๋ น์–ด๋กœ eslint๋ฅผ ์„ค์น˜ํ•˜์ž

npm install -D eslint
npx eslint --init

 

 

์ด ๋•Œ ํ•„์š”ํ•œ ํ”Œ๋Ÿฌ๊ทธ์ธ๋“ค์„ ๊ฐ™์ด ์ถ”๊ฐ€ํ•ด๋„ ๋œ๋‹ค.

npm install -D \
    eslint \
    @typescript-eslint/parser \
    @typescript-eslint/eslint-plugin \
    eslint-plugin-react \
    eslint-plugin-react-hooks \
    eslint-plugin-jsx-a11y \
    eslint-plugin-prettier \
    eslint-config-prettier \
    eslint-plugin-storybook
  • @typescript-eslint/parser : ESLint๊ฐ€ TypeScript ๋ฌธ๋ฒ•์„ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ํŒŒ์„œ
  • @typescript-eslint/eslint-plugin : TypeScript ์ „์šฉ ๋ฆฐํŠธ ๊ทœ์น™์„ ์ œ๊ณตํ•˜๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ
  • eslint-plugin-react : React ๋ฌธ๋ฒ• ๋ฐ ์ปดํฌ๋„ŒํŠธ ๊ด€๋ จ ๋ฆฐํŠธ ๊ทœ์น™ ์ œ๊ณต
  • eslint-plugin-react-hooks : React Hooks ์‚ฌ์šฉ ๊ทœ์น™์„ ๊ฒ€์‚ฌ (rules-of-hooks, exhaustive-deps ๋“ฑ)
  • eslint-plugin-jsx-a11y : JSX ์ฝ”๋“œ์—์„œ ์ ‘๊ทผ์„ฑ(a11y) ๊ด€๋ จ ๋ฌธ์ œ๋ฅผ ๊ฐ์ง€
  • eslint-plugin-prettier : Prettier ํฌ๋งทํŒ… ์˜ค๋ฅ˜๋ฅผ ESLint๋กœ ํ‘œ์‹œ
  • eslint-config-prettier : Prettier์™€ ์ถฉ๋Œํ•˜๋Š” ESLint ๊ทœ์น™์„ ๋น„ํ™œ์„ฑํ™”
  • eslint-plugin-storybook : Storybook ์ฝ”๋“œ์— ๋งž๋Š” ๋ฆฐํŠธ ๊ทœ์น™ ์ œ๊ณต (๊ตฌ์„ฑ ์š”์†Œ, ๋ฉ”ํƒ€ ์ •์˜ ๋“ฑ)

 

 

์›๋ž˜๋ผ๋ฉด Prettier๋„ ๊ฐ™์ด ์„ค์น˜/์„ค์ • ํ•ด์•ผํ•œ๋‹ค. ๊ทผ๋ฐ ๋”ฐ๋กœ ์ •๋ฆฌํ•ด์„œ ๋‚ด์šฉ์ด ๋‚˜๋ˆ ์ง

>> Prettier ๊ด€๋ จ ๊ธ€

 


3. ๐Ÿ”— Prettier์™€ ESLint ์—ฐ๋™

์ฝ”๋“œ ์Šคํƒ€์ผ ์ž๋™ ํฌ๋งทํ„ฐ์ธ prettier์™€ ESLint๋Š” ์ถฉ๋Œํ•  ๊ฐ€๋Šฅ์„ฑ๋„ ์žˆ๋‹ค.

๋‘˜์ด ์ถฉ๋Œํ•˜์ง€ ์•Š๋„๋ก ํ•˜๊ธฐ ์œ„ํ•ด eslint-config-prettier๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•œ๋‹ค.

 

์ถฉ๋Œ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ์•„๋ž˜์— ์ž‘์„ฑํ•  .eslintrcํŒŒ์ผ์— ์•„๋ž˜ ์„ค์ •์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

"extends": ["plugin: "pretier/recomended"]

์ด ์„ค์ •์œผ๋กœ ESLint๋Š” Prettier์™€ ์ถฉ๋Œํ•˜๋Š” ์ฝ”๋“œ ์Šคํƒ€์ผ ๊ทœ์น™์„ ๋ˆ๋‹ค.

 


4. ๐Ÿ“‚ ์„ค์ • ํŒŒ์ผ ์ž‘์„ฑ (.eslintrc, .eslintignore)

eslint ๊ทœ์น™์„ ์ž‘์„ฑํ•˜๋Š” .eslint.config.js๋ฅผ ์ž‘์„ฑํ•˜์ž.

 

์ฒ˜์Œ์—๋Š” .eslintrc๋กœ ์ž‘์„ฑ์„ ํ–ˆ์—ˆ์œผ๋‚˜, ์œ ์ง€๋ณด์ˆ˜์˜ ์šฉ์ด์™€ ์„ฑ๋Šฅ ํ–ฅ์ƒ ๋ชฉ์ ์œผ๋กœ ESLint v9 ๋ถ€ํ„ฐ๋Š” Flat Config ๋ฐฉ์‹์ด ๋„์ž…๋˜์–ด
eslint.config.js๊ฐ€ ๊ธฐ๋ณธ ์„ค์ • ๋ฐฉ์‹์œผ๋กœ ๋ฐ”๋€Œ์—ˆ๋‹ค๊ณ  ํ•œ๋‹ค. >>

 

eslint์˜ ๊ฒ€์‚ฌ์—์„œ ๋ฌด์‹œํ•  ํŒŒ์ผ๋“ค์„ ์ž‘์„ฑํ•˜๋Š” .eslintignore์„ ์ž‘์„ฑํ•˜์ž.

 

.eslint.config.js (ESLint Flat Config ํ˜•์‹)

import { FlatCompat } from "@eslint/eslintrc";
import tsPlugin from "@typescript-eslint/eslint-plugin";
import reactPlugin from "eslint-plugin-react";
import reactHooksPlugin from "eslint-plugin-react-hooks";
import prettierPlugin from "eslint-plugin-prettier";
import jsxA11yPlugin from "eslint-plugin-jsx-a11y";
import storybookPlugin from "eslint-plugin-storybook";

import { dirname } from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const compat = new FlatCompat({
  baseDirectory: __dirname,
});

const config = [
  ...compat.extends("next/core-web-vitals", "next/typescript"),
  {
    plugins: {
      "@typescript-eslint": tsPlugin,
      react: reactPlugin,
      "react-hooks": reactHooksPlugin,
      prettier: prettierPlugin,
      "jsx-a11y": jsxA11yPlugin,
      storybook: storybookPlugin,
    },
    languageOptions: {
      parserOptions: {
        ecmaVersion: 2020,
        sourceType: "module",
      },
    },
    rules: {
      "react/react-in-jsx-scope": "off",
      "@typescript-eslint/no-unused-vars": [
        "error",
        {
          vars: "all",
          varsIgnorePattern: "^_",
          args: "after-used",
          argsIgnorePattern: "^_",
        },
      ],
      "require-jsdoc": "off",
      "react/display-name": "off",
      "react-hooks/exhaustive-deps": "warn",
      "prettier/prettier": "error",
      "jsx-a11y/anchor-is-valid": [
        "error",
        {
          components: ["Link"],
          specialLink: ["hrefLeft", "hrefRight"],
          aspects: ["invalidHref", "preferButton"],
        },
      ],
    },
  },
];

export default config;

 

๊ฐ ํ•ญ๋ชฉ๋“ค ๊ฐ„๋‹จํ•œ ์„ค๋ช…

import { FlatCompat } ... FlatCompat์€ ๊ธฐ์กด .eslintrc ๊ธฐ๋ฐ˜ ์„ค์ •์„ ESLint Flat Config ๋ฐฉ์‹์œผ๋กœ ๋ณ€ํ™˜ํ•ด์ฃผ๋Š” ๋„์šฐ๋ฏธ๋‹ค.
Next.js ๋“ฑ์˜ ์„ค์ •๋“ค์ด ์•„์ง FlatConfig๋ฅผ ์ง์ ‘ ์ง€์›ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ํ•„์š”ํ•˜๋‹ค. >>
import ...(Plugin) FlatConfig ๋ฐฉ์‹์—์„œ๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์ง์ ‘ importํ•ด์„œ ๋“ฑ๋กํ•ด์•ผํ•œ๋‹ค.
import dirname
import fileURLToPath
FlatCompat์— ํ˜„์žฌ ํŒŒ์ผ ์œ„์น˜๋ฅผ ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•ด ํ˜„์žฌ ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ๊ตฌํ•˜๋Š” ์ฝ”๋“œ์ด๋‹ค.
ESM ํ™˜๊ฒฝ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ด๋Ÿฐ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•œ๋‹ค.
const compat .eslintrc์—์„œ ์‚ฌ์šฉํ•˜๋˜ extends: "next/core-web-vitals"๊ฐ™์€ ์„ค์ •์„
FlatConfig์—์„œ๋„ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋„๋ก ๋ณ€ํ™˜ํ•ด์ค€๋‹ค.
const config = [
...compat.extends(...),
Next.js ํ”„๋กœ์ ํŠธ์—์„œ ์ถ”์ฒœํ•˜๋Š” ๊ธฐ๋ณธ ESLint ์„ค์ •์„ ๋ถˆ๋Ÿฌ์™€ config ๋ฐฐ์—ด์— ํฌํ•จ์‹œํ‚จ๋‹ค.
plugins: { ... } importํ•œ ํ”Œ๋Ÿฌ๊ทธ์ธ๋“ค์„ ESLint์— ๋ช…์‹œ์ ์œผ๋กœ ๋“ฑ๋กํ•œ๋‹ค.
languageOptions: { ... } ์ตœ์‹  JS ๋ฌธ๋ฒ•, ๋ชจ๋“ˆ ์‹œ์Šคํ…œ์„ ์‚ฌ์šฉํ•œ๋‹ค๊ณ  ํŒŒ์„œ ์„ค์ •์„ ํ•œ๋‹ค.
rules : { ... } ⋅ react-injsx-scope : React17๋ถ€ํ„ฐ JSX๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ react๋ฅผ importํ•˜์ง€ ์•Š์•„๋„ ๋ผ์„œ off
⋅ no-unused-vars : ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ๋ณ€์ˆ˜/๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์—๋Ÿฌ์ฒ˜๋ฆฌ(์ด๋ฆ„์ด _๋กœ ์‹œ์ž‘ํ•˜๋ฉด ๋ฌด์‹œ)
⋅ require-jsdoc : JSDoc ์ฃผ์„ ํ•„์ˆ˜ ๊ทœ์น™ off
⋅ display-name : ์ต๋ช… ํ•จ์ˆ˜ ์ปดํฌ๋„ŒํŠธ์— displayName ์—†๋‹ค๊ณ  ๊ฒฝ๊ณ  x
⋅exhaustive-deps : useEffect ๋“ฑ์—์„œ ์˜์กด์„ฑ ๋ฐฐ์—ด ๋ˆ„๋ฝ ์‹œ ๊ฒฝ๊ณ 
⋅prettier/prettier : Prettier ํฌ๋งทํŒ… ์˜ค๋ฅ˜ ์‹œ, ESLint ์—๋Ÿฌ๋กœ ์ฒ˜๋ฆฌ
anchor-is-valid : Link ์ปดํฌ๋„ŒํŠธ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ (๋นˆ href, ์ž˜๋ชป๋œ ๋งํฌ ๋“ฑ ๊ฒ€์‚ฌ)
export default config; ์œ„์—์„œ ์ž‘์„ฑํ•œ ESLint ์„ค์ •์„ export
⋅ Flat Config์—์„œ๋Š” ์„ค์ • ํŒŒ์ผ์ด ๋ฐ˜๋“œ์‹œ config ๊ฐ์ฒด ๋ฐฐ์—ด์„ export default ํ•ด์•ผ ํ•œ๋‹ค.(๋ฐฐ์—ดํ˜• ์„ค์ • ๋ฐฉ์‹)
⋅ ๊ฐ์ฒด ๋‹จ์œ„๋กœ ์„ค์ •์ด ๋ณ‘ํ•ฉ๋˜๋ฉฐ, ๋‚˜์ค‘ ์ˆœ์„œ์— ๋“ฑ์žฅํ•œ ์„ค์ •์ด ์šฐ์„  ์ ์šฉ๋œ๋‹ค.
⋅ ๋ฐฐ์—ด ํ‘œํ˜„ ์—†์ด export default config [...]์™€ ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ ์ž‘์„ฑํ•˜๋ฉด Flat Config ๋ฌธ๋ฒ• ์š”๊ตฌ์‚ฌํ•ญ ๋ฏธ๋‹ฌ๋กœ
    ESLint๊ฐ€ ์ธ์‹ํ•˜์ง€ ๋ชปํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ๋ฐฉ์‹์„ ๋งž์ถ”์ž.

 

 

.eslintignore

ESLint ์ตœ์‹  ๋ฒ„์ „(9.x ์ด์ƒ) ๋ถ€ํ„ฐ๋Š” .eslintignore ๋Œ€์‹  eslint.config.js๋‚ด์— ignores ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์„ ๊ถŒ์žฅํ•ด์„œ

.eslintignore๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋‹ค.


5. ๐Ÿค– Husky + lint-staged๋กœ ์ปค๋ฐ‹ ์ „ ์ž๋™ ๊ฒ€์‚ฌ ์„ค์ •

์œ„ ์ž‘์„ฑํ•œ ์„ค์ •๋“ค์„ ์ž๋™์œผ๋กœ ํšจ์œจ์ ์œผ๋กœ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•ด Husky์™€ lint-staged๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

  • husky : ์ž๋™ํ™”
  • lint-staged : ํšจ์œจ (staging ํŒŒ์ผ๋งŒ ๊ฒ€์‚ฌํ•œ๋‹ค.)

 

Husky

๋ถ„๋ช… ๊ธ€์„ ์ผ๋˜ ๊ธฐ์–ต์ด ์žˆ๋Š”๋ฐ.. ์ด๊ฑด ์•„๋ฌดํŠผ ๋”ฐ๋กœ ์ •๋ฆฌํ•  ๊ฑฐ๋‹ค.

 

lint-staged

1. husky pre-commit ํ›… ์„ค์ •

husky์˜ pre-commit ํ›…์—์„œ npx lint-staged๋ฅผ ์‹คํ–‰ํ•˜๋„๋ก ๋ช…๋ น์–ด๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

npx husky add .husky/pre-commit "npx lint-staged"

 

 

์ด๋Ÿฌ๋ฉด pre-commit ํŒŒ์ผ์— ๋‚ด์šฉ์ด ์ž‘์„ฑ๋˜๋Š”๋ฐ, ์—ฌ๊ธฐ์„œ lint ๊ฒ€์‚ฌ ์—๋Ÿฌ ๋ฐœ์ƒ์‹œ ํ™•์‹คํ•˜๊ฒŒ ์ปค๋ฐ‹ ์ค‘์ง€ํ•˜๊ณ  ์‹ถ๋‹ค!!

ํ•˜๋ฉด exit 1์„ ๋ช…์‹œํ•ด์„œ ํ™•์‹คํ•˜๊ฒŒ ์ค‘์ง€์‹œํ‚ค๋„๋ก ์ˆ˜์ •ํ•˜์ž.

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged || exit 1

 

 

2. package.json ์„ค์ •

package.json์— lint-staged์— ๋Œ€ํ•œ ๋ช…๋ น์–ด ๋‹จ์ถ•์–ด๋ฅผ ์ž‘์„ฑํ•˜์ž.

"lint-staged": {
    "*.{ts,tsx,js}": [
      "eslint --fix --max-warnings=0",
      "prettier --write",
      "eslint --fix"
    ],
    "*.{json,md,css,html}": [
      "prettier --write"
    ]
  },
  • .ts, .tsx, .js ํ™•์žฅ์ž๋ฅผ ๊ฐ€์ง„ ํŒŒ์ผ์„ ๋Œ€์ƒ์œผ๋กœ ํ•œ๋‹ค.
    1. ESLint ์ž๋™ ์ˆ˜์ • + ๊ฒฝ๊ณ  ํ•œ๊ฐœ๋ผ๋„ ์žˆ์œผ๋ฉด ์ปค๋ฐ‹ ์‹คํŒจ(max-warnings=0)
        → ํŒ€ ๊ทœ๋ชจ๊ฐ€ ํฌ๊ฑฐ๋‚˜ ๋ ˆ๊ฑฐ์‹œ ์ฝ”๋“œ๊ฐ€ ๋งŽ์„ ๊ฒฝ์šฐ, ๊ฒฝ๊ณ ๋ฅผ ํ—ˆ์šฉํ•˜๊ณ  ์ ์ง„์  ๊ฐœ์„ ์„ ํ•˜๋Š” ๊ฒƒ์ด ํ˜„์‹ค์ ์ด๋‹ค.(max-warnings=10)
    2. Prettier๋กœ ์ฝ”๋“œ ์Šคํƒ€์ผ ํฌ๋งท ์ ์šฉ
    3. ESLint ์ž๋™ ์ˆ˜์ •์„ ํ•œ ๋ฒˆ ๋” ์ˆ˜ํ–‰ํ•˜์—ฌ ํฌ๋งทํŒ… ์ดํ›„ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์ด์Šˆ๋ฅผ ์žก์Œ
  • .json, .md, .css, .html ํ™•์žฅ์ž๋ฅผ ๊ฐ€์ง„ ํŒŒ์ผ์„ ๋Œ€์ƒ์œผ๋กœ ํ•œ๋‹ค.
    → ๋ฆฐํŠธ ๊ฒ€์‚ฌ๋ฅผ ํ•˜์ง€ ์•Š๊ณ  Prettier ์ฝ”๋“œ ์Šคํƒ€์ผ ํฌ๋งท๋งŒ ์ ์šฉ

์œ„์™€ ๊ฐ™์ด eslint --fix๋ฅผ ๋‘ ๋ฒˆ ์ ์šฉํ•˜๋ฉด ๋” ํ’ˆ์งˆ์ด ๋†’์€ ๊ฒ€์‚ฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ์œผ๋‚˜, ์†๋„ ๋ฐ ํšจ์œจ์ด ์กฐ๊ธˆ ๋–จ์–ด์ง„๋‹ค.

๊ฐ„๊ฒฐํ•˜๊ฒŒ ์•„๋ž˜์™€ ๊ฐ™์ด ๋ฆฐํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•ด๋„ ์ž˜ ์„ค๊ณ„๋œ ์„ค์ •์—์„œ๋Š” ๋Œ€๋ถ€๋ถ„์˜ ํฌ๋งทํŒ…/์ถฉ๋Œ ๋ฌธ์ œ๋Š” ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค.

"*.{ts,tsx,js}": [
  "prettier --write",
  "eslint --fix --max-warnings=0"
]

 

 

 

 

 

์ฐธ๊ณ 

๋”๋ณด๊ธฐ
๋ฐ˜์‘ํ˜•