본문 바로가기
SPA/Next

Next.js 16, next lint 대신 ESLint CLI와 Biome 사이의 현실적인 선택

by J4J 2026. 8. 17.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 Next.js 16에서 제거된 next lint 명령어가 실제로 어떤 문제를 일으키는지, 그리고 ESLint CLI와 Biome로 대응하는 방법을 살펴보는 시간을 가져보려고 합니다.

 

 

 

next lint가 사라진 이유와 next build 변화

 

Next.js 15까지 사용하던 next lint 명령어를 16으로 올린 뒤에도 그대로 실행하면, 명령을 찾을 수 없다는 에러와 함께 곧바로 실패하는 경우를 마주치게 됩니다.

 

Next.js 16.0.0부터 next lint 명령어는 점진적으로 줄어드는 것이 아니라 next 패키지에서 완전히 삭제되었습니다.

 

next 패키지 안에는 애초에 lint 서브커맨드를 처리하는 코드 자체가 존재하지 않기 때문에, next lint를 실행하면 알 수 없는 명령이라는 에러와 함께 즉시 종료됩니다.

 

실제로 실행해 보면 다음과 유사한 형태의 에러 메시지를 확인할 수 있습니다.

 

// terminal
$ next lint
error: unknown command 'lint'

 

 

 

next lint 명령어뿐 아니라 next.config.ts의 eslint 옵션도 함께 제거되었습니다.

 

// ❌ Next.js 16에서는 더 이상 유효하지 않은 옵션
const nextConfig: NextConfig = {
  eslint: {
    ignoreDuringBuilds: true,
  },
}

 

이렇게 작성하면 next.config.ts가 실행되는 시점에 더 이상 지원되지 않는다는 경고가 먼저 출력되고, 뒤이은 TypeScript 검사 단계에서 NextConfig 타입에 eslint 필드가 존재하지 않는다는 타입 에러로 next build 자체가 실패합니다.

 

 

 

Next.js 15 이하에서는 next build가 기본적으로 ESLint를 함께 실행해 린트 에러가 있으면 빌드 자체를 실패시켰지만, Next.js 16부터는 next build가 린트를 전혀 실행하지 않습니다.

 

별도의 lint 스크립트 없이 next build 성공 여부만으로 코드 품질 게이트를 삼고 있던 CI 파이프라인이라면, 업그레이드 이후에는 린트 에러가 남아있는 코드도 그대로 빌드를 통과해 배포될 수 있습니다.

 

개인적으로 이번 변화에서 가장 위험하다고 느낀 지점은, 명령어 자체가 사라져서 바로 눈에 띄는 에러보다 오히려 이렇게 조용히 새는 경우였습니다.

 

그러면 기존 next lint 설정을 ESLint CLI 기반으로 어떻게 옮길 수 있는지부터 살펴보겠습니다.

 

 

반응형

 

 

next-lint-to-eslint-cli 코드모드로 마이그레이션

 

Next.js 팀은 기존 next lint 설정을 ESLint CLI 기반으로 옮겨주는 전용 코드모드를 제공합니다.

 

// terminal
$ npx @next/codemod@canary next-lint-to-eslint-cli .

 

이 코드모드는 다음 작업을 자동으로 수행합니다.

 

  • package.json의 lint 스크립트를 next lint에서 eslint .로 변경
  • 프로젝트 루트에 eslint.config.mjs 파일을 새로 생성
  • 기존 ESLint 설정이 있다면 최대한 보존하며 필요한 의존성을 devDependencies에 추가

 

// package.json (코드모드 실행 전)
"scripts": {
  "lint": "next lint"
}

// package.json (코드모드 실행 후)
"scripts": {
  "lint": "eslint ."
}

 

 

 

다만 코드모드가 생성하는 eslint.config.mjs는 새 프로젝트가 기본으로 받는 구조와 다릅니다.

 

코드모드는 @eslint/eslintrc의 FlatCompat이라는 기존 .eslintrc 형식의 extends 배열을 새 Flat Config 형식으로 그대로 감싸서 재사용할 수 있게 해주는 호환 계층을 이용해 eslint.config.mjs를 생성합니다.

 

// eslint.config.mjs (코드모드가 생성하는 방식)
import { dirname } from 'path'
import { fileURLToPath } from 'url'
import { FlatCompat } from '@eslint/eslintrc'

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

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

const eslintConfig = [
  ...compat.extends('next/core-web-vitals', 'next/typescript'),
  {
    ignores: ['node_modules/**', '.next/**', 'out/**', 'build/**', 'next-env.d.ts'],
  },
]

export default eslintConfig

 

반면 create-next-app으로 새로 만든 프로젝트는 FlatCompat 없이 eslint-config-next를 직접 import하는 방식을 기본으로 사용하며, 이 구조는 뒤에서 자세히 살펴보겠습니다.

 

 

 

다만 기존 .eslintrc가 plugin:import/recommended, plugin:@typescript-eslint/recommended처럼 여러 extends 체인으로 복잡하게 얽혀 있는 프로젝트라면, 코드모드가 내부적으로 사용하는 변환 도구가 예상과 다른 형식의 파일을 생성해 마이그레이션이 중간에 실패하는 경우가 GitHub 이슈로 보고되어 있습니다.

 

이런 경우에는 코드모드에 의존하지 않고 ESLint 공식 마이그레이션 가이드를 참고해 eslint.config.mjs를 직접 작성하는 것이 좋습니다.

 

그러면 코드모드가 만들어주는 설정, 그리고 새 프로젝트의 기본 설정이 실제로 어떤 구조로 되어 있는지 eslint.config.mjs를 직접 뜯어보겠습니다.

 

 

 

 

Flat Config로 전환된 @next/eslint-plugin-next 직접 설정하기

 

Next.js 16에서는 @next/eslint-plugin-next 자체도 Flat Config를 기본 포맷으로 전환했습니다.

 

Flat Config는 .eslintrc의 extends 체인 대신 배열 하나에 여러 설정 객체를 순서대로 나열해 규칙을 병합하는 ESLint 9의 기본 설정 포맷이며, ESLint 10.0.0부터는 legacy .eslintrc 지원이 이미 완전히 빠졌으며 Next.js도 여기에 맞춰 기본값을 전환했습니다.

 

create-next-app으로 새로 만든 프로젝트의 eslint.config.mjs는 다음과 같은 구조를 갖습니다.

 

// eslint.config.mjs
import nextVitals from 'eslint-config-next/core-web-vitals'
import nextTs from 'eslint-config-next/typescript'
import { defineConfig, globalIgnores } from 'eslint/config'

const eslintConfig = defineConfig([
  ...nextVitals,
  ...nextTs,
  globalIgnores(['.next/**', 'out/**', 'build/**', 'next-env.d.ts']),
])

export default eslintConfig

 

defineConfig, globalIgnores는 여러 설정 배열을 하나로 합치고 특정 경로를 린트 대상에서 제외해 주는 ESLint 9 내장 헬퍼입니다.

 

TypeScript 프로젝트라면 eslint-config-next/typescript도 함께 import해야 typescript-eslint의 권장 규칙까지 적용됩니다.

 

 

 

// ❌ 존재하지 않는 서브패스
import nextReact from 'eslint-config-next/react'

 

이렇게 작성하면 eslint-config-next 패키지가 공개하는 경로 목록에 등록되지 않은 서브패스라 모듈을 찾을 수 없다는 에러가 발생합니다.

 

 

 

// ✅ eslint-config-next가 공개하는 서브패스만 사용
import nextVitals from 'eslint-config-next/core-web-vitals'
import nextTs from 'eslint-config-next/typescript'

 

그러므로 eslint-config-next가 공개하는 서브패스인 core-web-vitals, typescript, parser, 그리고 기본 경로 중에서만 import 경로를 선택하는 것이 좋습니다.

 

여기까지는 next lint를 ESLint CLI로 그대로 옮기는 방법이었습니다.

 

그러면 아예 다른 도구인 Biome으로 전환하고 싶은 경우는 어떻게 접근해야 할지 살펴보겠습니다.

 

 

 

 

next lint 대신 Biome을 선택하는 경우

 

Biome은 포매팅과 린트를 하나의 Rust 기반 바이너리로 처리하는 올인원 도구로, Next.js 15.5부터 create-next-app 설치 과정에서 ESLint 대신 선택할 수 있는 옵션으로 공식 지원되기 시작했습니다.

 

Biome에는 package.json에 등록된 프레임워크 의존성을 감지해 그 프레임워크 전용 규칙을 자동으로 함께 켜주는 domain이라는 기능이 있으며, next 의존성이 있으면 별도 설정 없이도 Next.js 전용 도메인이 자동으로 활성화됩니다.

 

이 도메인에는 총 12개 규칙이 포함되어 있고, next/image 대신 img 태그를 직접 쓰는 실수를 잡아주는 noImgElement를 포함해 다수가 기본(recommended)으로 활성화됩니다.

 

다만 @next/eslint-plugin-next의 core-web-vitals 규칙셋에 포함된 Next.js 전용 규칙 21개 전부를 Biome이 커버하는 것은 아니며, 페이지 이동에 a 태그를 그대로 쓰는 것을 잡아주는 no-html-link-for-pages처럼 대응하는 Biome 규칙이 없는 항목도 남아 있습니다.

 

 

 

// terminal
$ npm install --save-dev --save-exact @biomejs/biome

 

// biome.json
{
  "linter": {
    "enabled": true,
    "domains": {
      "next": "recommended"
    },
    "rules": {
      "preset": "recommended"
    }
  },
  "formatter": {
    "enabled": true
  }
}

 

domains의 next 값은 package.json에 next 의존성이 있으면 자동으로 recommended가 적용되므로 생략해도 동일하게 동작하지만, 명시적으로 남겨두면 의도를 코드로 드러낼 수 있습니다.

 

 

 

그러므로 어떤 상황에서 Biome을 선택하는 것이 좋은지 먼저 정리해 볼 필요가 있습니다.

 

  • 포매팅과 일반적인 코드 스타일 린트가 목적이고, next 도메인이 커버하지 못하는 나머지 규칙 몇 개는 포기해도 괜찮은 프로젝트
  • ESLint의 느린 실행 속도가 체감될 만큼 큰 모노레포에서 포매팅만이라도 빠르게 처리하고 싶은 경우

 

 

 

반대로 no-html-link-for-pages처럼 Biome의 next 도메인이 커버하지 못하는 규칙까지 그대로 유지하고 싶다면, Biome으로 완전히 대체하는 대신 포매팅은 Biome, 나머지 Next.js 전용 규칙만 최소한의 ESLint로 남기는 하이브리드 구성이 현실적인 절충안이 됩니다.

 

Biome이든 ESLint CLI든, 선택한 도구를 실제로 CI 파이프라인에 연결하는 방법을 마지막으로 살펴보겠습니다.

 

 

 

 

CI 파이프라인에서 next lint 호출부 교체하기

 

package.json의 lint 스크립트가 바뀌었다면, CI 워크플로우 안에서 next lint를 직접 호출하던 부분도 함께 손보는 것이 좋습니다.

 

// ❌ .github/workflows/ci.yml (변경 전)
- name: Lint
  run: npx next lint

 

이렇게 남겨두면 앞서 살펴본 것처럼 next lint 명령어 자체가 사라졌으므로 이 스텝은 알 수 없는 명령 에러로 곧바로 실패합니다.

 

 

 

// ✅ .github/workflows/ci.yml (변경 후)
- name: Lint
  run: npm run lint

 

그러므로 CI 워크플로우의 lint 스텝은 next lint를 직접 호출하는 대신 package.json에 정의된 lint 스크립트를 통해 실행하는 것이 좋습니다.

 

 

 

추가적으로 CI 파이프라인을 구성할 때 짚어야 할 지점이 하나 더 있습니다.

 

next build가 더 이상 린트를 실행하지 않는다는 점을 고려하면, build 스텝과는 별도로 lint 스텝을 명시적으로 CI 워크플로우에 두는 것이 안전합니다.

 

 

 

지금까지 살펴본 내용을 정리하면 다음과 같습니다.

 

  • next lint 명령어와 next.config의 eslint 옵션은 Next.js 16.0.0부터 완전히 제거됨 — next build도 더 이상 린트를 실행하지 않음
  • next-lint-to-eslint-cli 코드모드가 package.json 스크립트 변경과 eslint.config.mjs 생성을 자동화하지만, FlatCompat 기반이라 새 프로젝트의 네이티브 방식과 구조가 다름
  • eslint-config-next는 기본 경로, core-web-vitals, typescript, parser 4개 서브패스만 공개 — 그 외 경로를 import하면 에러
  • Biome은 next 의존성을 감지해 12개 규칙의 전용 도메인을 자동 활성화하지만, @next/eslint-plugin-next의 21개 규칙 전부를 커버하진 않으므로 완전 대체보다 하이브리드 구성이 현실적
  • CI 워크플로우에서 next lint를 직접 호출하던 스텝은 package.json의 lint 스크립트 실행으로 교체하고, build와 별도로 lint 스텝을 명시적으로 두는 것이 좋음

 

 

 

타입 에러로 바로 드러나는 실수는 next lint가 사라졌다는 사실 자체만으로도 금방 알아챌 수 있지만, next build 성공만 보고 린트를 건너뛰던 파이프라인은 조용히 새기 쉽다는 점에서 이번 변화가 특히 신경 쓰였습니다.

 

그러므로 Next.js 16으로 업그레이드하는 프로젝트라면 코드모드 실행 이후에도 CI 워크플로우 전체를 한 번 더 점검해, next lint를 직접 호출하는 지점이 남아있지 않은지 확인하는 것이 좋습니다.

 

 

 

 

정리

 

ESLint CLI와 Biome 중 어느 쪽을 선택할지는 프로젝트가 @next/eslint-plugin-next의 규칙을 얼마나 빠짐없이 유지해야 하는지에 달려 있으며, 두 도구의 차이를 표로 정리하면 다음과 같습니다.

 

구분 ESLint CLI Biome
설정 방식 eslint.config.mjs에서 eslint-config-next 서브패스를 조합하는 Flat Config biome.json 하나로 린트와 포매팅을 함께 설정
Next.js 규칙 커버리지 @next/eslint-plugin-next의 core-web-vitals 규칙 21개 전부 지원 next 도메인이 자동 활성화하는 12개 규칙만 지원
실행 방식 Node.js 기반 CLI, 포매팅은 별도 도구 필요 Rust 기반 단일 바이너리, 린트와 포매팅을 함께 처리
적합한 상황 no-html-link-for-pages처럼 Biome이 아직 커버하지 못하는 규칙까지 빠짐없이 유지해야 하는 프로젝트 포매팅 속도가 중요하거나, 나머지 규칙만 최소한의 ESLint로 보완하는 하이브리드 구성이 가능한 프로젝트

 

 

 

 

 

 

 

이상으로 Next.js 16에서 제거된 next lint 명령어를 대신해 ESLint CLI와 Biome 중 어떤 도구를 선택하면 좋을지에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글