주요 콘텐츠로 건너뛰기

Your Ultimate Guide to Mastering ESLint Config

· 17분 읽기
Evan Carter
Evan Carter
Senior frontend

TLDR:

Mastering ESLint Config

ESLint isn’t just a linter—it’s a scalable policy engine for code quality and architecture. This guide shows how to set up modern flat config, tailor rules for React, Vue, and TypeScript, pick plugins that prevent real bugs, and automate checks with Husky and lint-staged. You’ll also learn how to enforce Feature-Sliced Design boundaries to reduce coupling, improve onboarding, and keep large frontend projects maintainable.

ESLint config is the difference between a codebase that stays predictable as it grows and one that slowly drifts into inconsistent “lint roulette.” With modern flat config (eslint.config.js), a thoughtful rule strategy, and automation through Git hooks and CI, you can turn linting into a reliable quality gate—especially when paired with a scalable architecture like Feature-Sliced Design (FSD) from feature-sliced.design.


Quick Navigation


Step-by-step: Install ESLint and generate a baseline config

If your goal is "get linting running today," your fastest path is to install ESLint and generate an initial config using the official initializer. ESLint's docs now center around flat config (eslint.config.js), which is the default since ESLint v9.

1) Add ESLint to an existing project

  1. Ensure you have a package.json (most projects already do).
  2. Run the config wizard:
npx eslint --init

Under the hood, this runs:

npm init @eslint/config

and generates an eslint.config.js file (flat config).

  1. Add scripts:
{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix"
}
}
  1. Run it:
npm run lint

2) Start from scratch (new repo)

If you’re bootstrapping a new app (Vite, Next.js, Vue CLI, Nuxt, etc.), install ESLint early so the team’s conventions are consistent from the first commit.

  • Option A: Run the wizard after scaffolding:
npm init @eslint/config@latest
  • Option B: Install manually, then write the config:
npm install --save-dev eslint

Then create eslint.config.js yourself (we’ll do this in the next sections).

3) Sanity-check your first run

A practical baseline for early adoption:

  • Treat correctness and safety as errors
  • Treat stylistic opinions as warnings (or leave to Prettier)
  • Use --fix only for safe fixes and format-related rules

This avoids the “turn on 300 rules, break 2,000 files” trap—and keeps the rollout positive.


Why a good ESLint config is a scaling strategy, not a style preference

A key principle in software engineering is that constraints reduce coordination cost. ESLint is not just a linter; it’s a programmable policy engine for code quality, consistency, and architectural boundaries.

Here’s why teams keep investing in it:

  • Faster code reviews: reviewers focus on logic and design, not missing dependencies or inconsistent patterns.
  • Safer refactors: rules catch dead code, unsafe patterns, and API misuse early.
  • Lower onboarding time: newcomers learn conventions by seeing consistent enforcement.
  • Better modularity: when you enforce import boundaries and public APIs, you reduce coupling and improve cohesion.

ESLint's popularity reflects its role in modern toolchains: it continues to grow massively in npm usage, with weekly downloads climbing dramatically over 2025.

The meta-lesson: if your project hurts from spaghetti code, slow onboarding, or “monolith creep,” you don’t fix it with more rules alone. You fix it by combining:

  • Architecture that guides structure (e.g., Feature-Sliced Design)
  • Linting that enforces boundaries and conventions
  • Automation that makes compliance effortless

How ESLint configuration works in 2026: flat config, composition, overrides

ESLint v9 made flat config (eslint.config.js) the default, and the legacy .eslintrc format is deprecated and no longer automatically searched for.

Flat config mental model

Think of flat config as an ordered list of configuration objects that match files by glob:

  • Each object can target a set of files (files)
  • You can attach rules, plugins, parser/language options, and ignores
  • Order matters: later entries can refine or override earlier ones

This model makes advanced setups clearer, especially for large codebases.

The modern helpers: defineConfig(), extends, and globalIgnores()

The ESLint team introduced defineConfig() to improve type safety and composition, and brought back extends inside flat config to simplify combining configs. They also added a clearer way to express global ignores.

Practical implications:

  • You can write config like other modern tools (Vite/Nuxt-style defineConfig)
  • You can extend shareable configs without guessing whether they are arrays or objects
  • You can express ignores explicitly (and avoid accidental linting of build artifacts)

.eslintignore is not the path forward

If you're migrating from legacy config, note that flat config does not load .eslintignore. You should move ignore patterns into eslint.config.js using ignores or globalIgnores().

Comparison table: legacy vs flat config

NeedLegacy (.eslintrc.*)Flat config (eslint.config.js)
Plugin loadingString-based ("plugins": ["react"])Import objects/modules directly
Applying rules per file typeoverridesMultiple objects with files globs
Ignore behavior.eslintignore supportedUse ignores / globalIgnores()
Status in v9+Deprecated, not auto-discoveredDefault format

ESLint provides a migration guide and a migrator tool for converting older configs.

A clean "base" flat config skeleton

Below is a pragmatic structure you can reuse and extend:

// eslint.config.js
export default [
// 1) global ignores: build output, generated files, coverage
{ ignores: ["**/dist/**", "**/build/**", "**/coverage/**"] },

// 2) base JS rules for all source files
{
files: ["**/*.{js,jsx,ts,tsx,vue}"],
rules: {
"no-undef": "error",
"no-unused-vars": "warn"
}
},

// 3) per-environment overrides (tests, scripts, config)
{
files: ["**/*.config.{js,ts}", "**/scripts/**/*.{js,ts}"],
rules: { "no-console": "off" }
}
];

In the next sections, we’ll refine this for React/TypeScript and Vue/TypeScript, then show how to enforce FSD boundaries.


Framework recipe: React + TypeScript (and Next.js)

React And TypeScript

Modern React projects typically need three layers of linting:

  1. Core JavaScript rules (ESLint recommended)
  2. TypeScript-aware linting (typescript-eslint)
  3. React semantics and hooks correctness (React plugins)

Install the essentials

A practical set for React + TypeScript:

  • eslint
  • typescript, plus typescript-eslint tooling and recommended configs
  • eslint-plugin-react (JSX/React rules)
  • eslint-plugin-react-hooks (Hooks correctness)
  • eslint-plugin-jsx-a11y (accessibility in JSX)
  • Optional but common: eslint-plugin-import (import hygiene), eslint-config-prettier (avoid formatter conflicts)

Example install (npm):

npm install --save-dev eslint typescript typescript-eslint eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y eslint-plugin-import eslint-config-prettier

Example React + TS flat config (team-friendly defaults)

This is intentionally opinionated toward correctness, maintainability, and clear overrides.

// eslint.config.js (React + TypeScript)
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import jsxA11y from "eslint-plugin-jsx-a11y";

export default [
{ ignores: ["**/dist/**", "**/.next/**", "**/coverage/**"] },

// Base JS recommended
js.configs.recommended,

// TypeScript recommended
...tseslint.configs.recommended,

// React + hooks + a11y
{
files: ["**/*.{jsx,tsx}"],
plugins: {
react,
"react-hooks": reactHooks,
"jsx-a11y": jsxA11y
},
languageOptions: {
parserOptions: { ecmaFeatures: { jsx: true } }
},
rules: {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
},

// Tests override
{
files: ["**/*.{test,spec}.{ts,tsx,js,jsx}"],
rules: {
"no-console": "off"
}
}
];

Why this works:

  • Correctness first: Hooks rules catch real bugs early.
  • Scoped rules: JSX-only rules don’t run on non-React files
  • Clear overrides: test files can relax logging and certain patterns

Typed linting (optional, high ROI for large teams)

Typed linting enables rules that require type information (e.g., catching unsafe promise handling, misused await, unsafe member access). The tradeoff is performance and configuration complexity.

typescript-eslint provides a structured approach, including parserOptions.projectService and a recommended "type-checked" preset.

When to enable it:

  • You have a large codebase with frequent refactors
  • You want lint rules that catch type-level mistakes before runtime
  • You can invest in performance tuning (caching, targeted globs)

If you’re on Next.js

Next.js ships an ESLint configuration package that includes recommended React and Hooks rules, reducing setup time.

A common approach:

  • Use eslint-config-next as a base
  • Add TypeScript linting and architecture rules on top
  • Keep formatting with Prettier (and disable conflicts)

This is a strong default for product teams who want speed without losing rigor.


Framework recipe: Vue 3 + TypeScript (and Nuxt)

Vue Framework

Vue introduces an extra dimension: linting Single File Components (.vue) where template and script live together. For this, you'll typically use eslint-plugin-vue, which provides flat-config-friendly bundles.

Install the essentials

Typical Vue 3 + TS set:

  • eslint
  • typescript, typescript-eslint
  • eslint-plugin-vue
  • Optional: @vue/eslint-config-typescript (commonly used in create-vue setups)
  • Optional: eslint-config-prettier, eslint-plugin-import

Example install:

npm install --save-dev eslint typescript typescript-eslint eslint-plugin-vue eslint-config-prettier

Example Vue + TS flat config (with Vue's bundle configs)

eslint-plugin-vue exposes prebuilt configurations for eslint.config.js, often as arrays you combine.

// eslint.config.js (Vue 3 + TypeScript)
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import vue from "eslint-plugin-vue";

export default [
{ ignores: ["**/dist/**", "**/.nuxt/**", "**/coverage/**"] },

js.configs.recommended,
...tseslint.configs.recommended,

// Vue flat recommended bundle
...vue.configs["flat/recommended"],

// Fine-tune Vue SFC rules
{
files: ["**/*.vue"],
rules: {
"vue/multi-word-component-names": "off"
}
}
];

What this buys you:

  • Consistent template conventions
  • Better correctness around Vue-specific patterns
  • A predictable baseline for teams migrating from “anything goes” to shared standards

Nuxt note

Nuxt projects often come with opinions and tooling defaults. The robust strategy is to:

  • Adopt Nuxt defaults first for velocity
  • Add TypeScript linting and architecture enforcement incrementally
  • Keep overrides isolated to your app’s domain (not framework internals)

ESLint plugins that pay rent: curated add-ons for real teams

Plugins are where ESLint moves from “general JavaScript hygiene” to “your stack’s correctness rules.” But more plugins also mean:

  • More configuration complexity
  • More maintenance (updates, deprecations)
  • More supply chain surface area

So the goal is not “install everything,” but “install what consistently prevents defects.”

A pragmatic plugin selection table (3 columns)

GoalUseful plugins/configsWhy it matters
React correctnesseslint-plugin-react, eslint-plugin-react-hooksEnforces component patterns and hooks rules
Vue SFC conventionseslint-plugin-vueLints templates + script coherently
TypeScript-aware rulestypescript-eslint configsCatches TS-specific issues early
Accessibilityeslint-plugin-jsx-a11yPrevents common a11y regressions
Import hygieneeslint-plugin-importReduces circular deps and messy boundaries
Formatting conflictseslint-config-prettierAvoids rule wars with Prettier

A security-minded note about linting dependencies

Linting configs and plugins run in your developer/CI environment. That makes them part of your software supply chain. In 2025, malicious versions were published for widely used ESLint/Prettier-related packages, with advisories and a CVE documenting impacted versions and behavior.

Keep the workflow safe without slowing it down:

  • Pin versions for critical tooling in large orgs
  • Use lockfiles and review dependency diffs
  • Enable automated auditing (GitHub Dependabot / Snyk / etc.)
  • Prefer reputable sources and monitor advisories for high-impact packages

This is not fear—this is normal operational maturity for teams shipping production software.


Mastering ESLint rules: severity, options, and team-friendly conventions

Once ESLint is running, most teams hit the same question:

“Which rules should we enable, and how strict should we be?”

The best answer is to treat your ruleset like an API: stable defaults, clear exceptions, and thoughtful evolution.

Rule severity: off, warn, error

The most scalable rule strategy is:

  • Error: correctness and safety (bugs, invalid patterns, architectural violations)
  • Warn: code health and refactor guidance (complexity, risky patterns)
  • Off: purely stylistic rules that a formatter handles

Example:

rules: {
"no-debugger": "error",
"no-console": "warn",
"eqeqeq": ["error", "always"],
"complexity": ["warn", 12]
}

Rule options are where you express architecture

Rules become powerful when you encode team conventions:

  • Allowed import paths
  • Public API usage only
  • Layer boundaries
  • Naming conventions for files and exports

A classic example is restricting deep imports (avoiding hidden coupling):

rules: {
"no-restricted-imports": ["error", {
"patterns": [
{
"group": ["@/entities/**/**", "!@/entities/**/index.ts"],
"message": "Import from the public API (index.ts) only."
}
]
}]
}

This looks small, but it creates huge long-term benefits: you reduce accidental dependence on internal modules and make refactors safer.

Overrides: treat “special files” as special

Many teams fail by applying one strict config to everything, then disabling rules everywhere. Instead, add precise overrides:

  • *.test.* and *.spec.*
  • Storybook stories (*.stories.*)
  • Build/config scripts
  • Generated files (ignored globally)

Flat config makes this straightforward with multiple config objects and files globs.

Typed linting: choose it deliberately

Typed linting gives you stronger guarantees, and typescript-eslint provides guidance and presets for it.

A sustainable approach:

  • Start without type-aware rules (fast feedback)
  • Enable typed linting only for selected directories (core domain code)
  • Expand as performance and confidence allow

That aligns nicely with real-world constraints in monorepos and large teams.


Automate linting: editor UX, pre-commit hooks, and CI pipelines

A well-written ESLint config is only half the system. The other half is when and where it runs.

Your goal is a smooth developer experience:

  • Fast feedback in-editor
  • Automatic checking before commits
  • A reliable gate in CI (no “works on my machine”)

1) In-editor linting

Most teams rely on:

  • ESLint editor integrations (VS Code / JetBrains)
  • Auto-fix on save for safe rules
  • Format-on-save via Prettier (optional, but common)

This reduces back-and-forth and makes compliance feel effortless.

2) Pre-commit hooks with Husky + lint-staged

This is the sweet spot: lint only staged files, keep commits clean, and avoid “whole repo lint” on every commit.

lint-staged's official guidance is explicit: install lint-staged and run tools like ESLint for staged files; Husky is a popular choice for wiring Git hooks.

Install:

npm install --save-dev husky lint-staged

Initialize Husky and set up hooks (common workflow varies by version, but Husky's docs explain the "prepare" script and CI handling).

Example package.json:

{
"scripts": {
"prepare": "husky",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,vue}": [
"eslint --fix"
]
}
}

Example .husky/pre-commit:

#!/usr/bin/env sh
npx lint-staged

Why this works:

  • Only checks what’s being committed
  • Applies auto-fixes safely
  • Prevents broken commits without slowing daily work

3) CI pipeline linting

CI should run ESLint in a stricter mode:

  • No auto-fix
  • Fail fast on errors
  • Optionally limit warnings (or treat warnings as errors in release branches)

Example:

eslint . --max-warnings 0

Combine that with caching (--cache) for performance, especially in monorepos.


Enforce architecture with ESLint + Feature-Sliced Design

FSD Architecture

This is where ESLint becomes an architectural tool, not just a lint tool.

Feature-Sliced Design (FSD) organizes frontend code by layers and slices, encouraging:

  • High cohesion within slices
  • Controlled dependencies between layers
  • Explicit public APIs
  • Isolation of business logic from UI composition

FSD in one directory schema

A typical FSD structure:

src/
app/
processes/
pages/
widgets/
features/
entities/
shared/
  • Layers represent responsibility levels (from shared utilities up to app composition)
  • Slices inside layers represent cohesive modules (e.g., features/auth, entities/user)

Diagram (mental model): Imagine a grid:

  • Y-axis: layers (shared → entities → features → widgets → pages → app)
  • X-axis: slices (user, auth, cart, checkout...) Rules allow dependencies that move “up” in responsibility in controlled ways, while preventing sideways coupling that creates spaghetti.

Comparing architectural approaches (3 columns)

ApproachWhat it optimizes forCommon pain point at scale
MVC / MVPSimple separation of concernsBlurs boundaries in frontend apps; UI and domain logic still entangle
Atomic DesignVisual consistency and UI reuseGreat for components; weaker for domain boundaries and feature isolation
Domain-Driven DesignDomain modeling and bounded contextsPowerful, but can be heavy without a frontend-friendly structure
Feature-Sliced DesignModular slices + clear dependency flowRequires discipline and tooling enforcement early

Why FSD + ESLint fits: FSD defines the structure; ESLint ensures it stays intact across refactors and team growth.

Enforcing public APIs and slice boundaries with ESLint

A robust methodology for large-scale projects is to enforce these rules:

  • Slices import other slices only via public API files (index.ts)
  • Layers can only depend on allowed lower layers (no “back references”)
  • Deep imports are discouraged to prevent hidden coupling

Example boundary policy using no-restricted-imports:

rules: {
"no-restricted-imports": ["error", {
"patterns": [
// 1) No deep imports into entities internals
{
"group": ["@/entities/**/**", "!@/entities/**/index.ts"],
"message": "Use entities/* public API only."
},
// 2) Pages should not be imported by features/entities/shared
{
"group": ["@/pages/**"],
"message": "Do not import pages into lower layers."
}
]
}]
}

This turns architectural conventions into executable constraints.

Why this improves onboarding and refactoring

As demonstrated by projects using FSD, a consistent module boundary model:

  • Makes it easier to locate code (“where does this feature live?”)
  • Makes refactors less risky (dependencies are constrained)
  • Improves team autonomy (developers can work in slices with fewer collisions)

ESLint becomes your “always-on reviewer,” while FSD keeps the structure understandable.

To explore the methodology in depth, the official docs are the best starting point.

(We’ll repeat the official CTA in the conclusion.)


Scaling and troubleshooting: monorepos, performance, migrations

Monorepos: one config, many packages

In Nx/Turborepo/pnpm workspaces, you want:

  • A root ESLint config that defines baseline rules and ignores
  • Package-level overrides only where necessary
  • For TypeScript typed linting, consider project-aware configuration that scales

typescript-eslint describes parserOptions.projectService as a way to simplify typed linting configuration and improve scalability.

Practical tips:

  • Restrict files globs to src/** (avoid linting build output)
  • Use ignores/globalIgnores() for generated folders
  • Enable caching:
eslint . --cache

Flat config migration: don’t brute-force it

When migrating:

  1. Convert existing .eslintrc* using the official migrator (@eslint/migrate-config) as a starting point
  2. Validate plugin compatibility with flat config
  3. Move .eslintignore patterns into the config (flat config won't load it)
  4. Stabilize with a small rule set first, then expand

Remember: the goal is not “perfect rules,” it’s predictable behavior.

Common “flat config” surprises (and fixes)

  • Unexpected files linted: dotfiles and .eslintignore behavior differ in flat config—add explicit ignores.
  • Legacy configs not picked up: .eslintrc isn't auto-discovered in v9—use flat config or explicitly opt into legacy.
  • Plugins expose configs differently: use defineConfig() and extends when available to reduce "spread operator confusion."

Conclusion

Mastering ESLint is less about memorizing rules and more about building a system: a solid baseline config, scoped overrides, the right plugins, and automation that makes compliance effortless. Flat config (eslint.config.js) unlocks clearer composition, and modern helpers like defineConfig() and better ignore handling reduce friction in real-world setups. Most importantly, ESLint becomes a force multiplier when it enforces architectural constraints—especially alongside Feature-Sliced Design, which helps keep large frontend codebases modular, cohesive, and easier to refactor.

Ready to build scalable and maintainable frontend projects? Dive into the official Feature-Sliced Design Documentation to get started.

Have questions or want to share your experience? Visit our homepage to learn more!

Disclaimer: The architectural patterns discussed in this article are based on the Feature-Sliced Design methodology. For detailed implementation guides and the latest updates, please refer to the official documentation.