CloudOps Console v2 - ESLint 설정 구축
TL;DR
핵심 변경사항: ESLint 9 flat config 도입, airbnb 제거, FSD 아키텍처 경계 규칙 적용
- ✅ ESLint 9.x + flat config 기반 설계
- ✅
eslint-plugin-import-x로 마이그레이션 - ✅
eslint-plugin-boundaries로 FSD 레이어 의존성 강제 - ✅ 파일명 컨벤션 자동화
목차
배경
v1에서 ESLint 9.x와 eslint-config-airbnb 호환성 문제로 ESLint 8.x로 다운그레이드했던 경험이 있었다. 이번 v2에서는 처음부터 ESLint 9 flat config 기반으로 설계해서 이런 문제를 원천 차단하고 싶었다.
주요 변경 사항
1. ESLint 9 Flat Config 도입
기존 .eslintrc.js 방식 대신 eslint.config.js flat config 방식을 채택했다.
// eslint.config.js
export default defineConfig([
globalIgnores(['dist', 'node_modules']),
{
files: ['**/*.{ts,tsx}'],
extends: [js.configs.recommended, tseslint.configs.strictTypeChecked],
},
])
💡 flat config를 선택한 이유
- 배열 순서가 곧 우선순위라 직관적
import로 의존성이 명시적으로 보임overrides대신 배열에 객체 추가하면 돼서 단순함- ESM 네이티브 지원
2. airbnb 플러그인 제거
v1에서는 eslint-config-airbnb-typescript를 썼는데, 이게 2025년 5월에 archive되면서 더 이상 유지보수가 안 된다. v2에서는 필요한 규칙들을 직접 설정하는 방식으로 갔다.
⚠️ 주의: eslint-config-airbnb-typescript는 더 이상 유지보수되지 않음
// 직접 설정한 TypeScript 규칙
rules: {
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
'@typescript-eslint/naming-convention': [
'error',
{ selector: 'interface', format: ['PascalCase'] },
],
}
3. eslint-plugin-import → eslint-plugin-import-x
eslint-plugin-import가 ESLint 9 flat config를 제대로 지원하지 않아서 fork 버전인 eslint-plugin-import-x로 교체했다.
| 비교 항목 | eslint-plugin-import |
eslint-plugin-import-x |
|---|---|---|
| ESLint 9 지원 | ❌ 미지원 | ✅ 지원 |
| Flat Config | ❌ 미지원 | ✅ 지원 |
| 성능 | 보통 | ⚡ 개선됨 |
4. FSD 아키텍처 경계 규칙
eslint-plugin-boundaries로 FSD 레이어 간 의존성 규칙을 강제했다.
📦 boundaries 규칙 전체 코드 보기
'boundaries/element-types': ['error', {
default: 'disallow',
rules: [
{ from: 'app', allow: ['pages', 'widgets', 'features', 'entities', 'shared'] },
{ from: 'pages', allow: ['widgets', 'features', 'entities', 'shared'] },
{ from: 'widgets', allow: ['features', 'entities', 'shared'] },
{ from: 'features', allow: ['entities', 'shared'] },
{ from: 'entities', allow: ['shared'] },
{ from: 'shared', allow: ['shared'] },
],
}]
이 규칙의 핵심은 상위 레이어 → 하위 레이어로만 import 가능하게 제한하는 것이다.
app → pages → widgets → features → entities → shared
↓ ↓ ↓ ↓ ↓
✅ ✅ ✅ ✅ ✅
← ← ← ← ←
❌ ❌ ❌ ❌ ❌
5. Public API 패턴 강제
'import-x/no-internal-modules': ['error', {
allow: ['**/index.{ts,tsx,js}', '**/node_modules/**', '**/*.{svg,css}'],
}]
🎯 목적: 각 모듈의
index.ts를 통해서만 import하도록 강제해서 내부 구현을 캡슐화
// ✅ Good - Public API 사용
import { Button } from '@/shared/ui'
// ❌ Bad - 내부 모듈 직접 접근
import { Button } from '@/shared/ui/Button/Button'
6. 파일명 컨벤션
eslint-plugin-check-file로 파일명 규칙을 강제했다.
| 확장자 | 컨벤션 | 예시 |
|---|---|---|
.tsx |
PascalCase | UserProfile.tsx |
.ts |
camelCase | useAuth.ts |
| 폴더 | kebab-case | user-profile/ |
📦 설정 코드 보기
'check-file/filename-naming-convention': ['error', {
'**/*.tsx': 'PASCAL_CASE',
'**/*.ts': 'CAMEL_CASE',
}],
'check-file/folder-naming-convention': ['error', {
'src/**': 'KEBAB_CASE',
}]
7. Import 정렬
FSD 구조를 반영한 import 정렬 규칙을 적용했다.
📦 import 정렬 규칙 전체 코드 보기
'import-x/order': ['error', {
groups: ['builtin', 'external', 'internal', ['parent', 'sibling', 'index'], 'type'],
pathGroups: [
{ pattern: 'react', group: 'builtin', position: 'before' },
{ pattern: '@/app/**', group: 'internal', position: 'before' },
{ pattern: '@/pages/**', group: 'internal', position: 'before' },
{ pattern: '@/widgets/**', group: 'internal', position: 'before' },
{ pattern: '@/features/**', group: 'internal', position: 'before' },
{ pattern: '@/entities/**', group: 'internal', position: 'before' },
{ pattern: '@/shared/**', group: 'internal', position: 'before' },
],
'newlines-between': 'always',
}]
정렬 결과 예시:
// 1. builtin
import React from 'react'
// 2. external
import { QueryClient } from '@tanstack/react-query'
// 3. internal (FSD 순서)
import { AppProvider } from '@/app/providers'
import { HomePage } from '@/pages/home'
import { useAuth } from '@/features/auth'
import { Button } from '@/shared/ui'
// 4. types
import type { User } from '@/entities/user'
ESM 환경 이슈
package.json에 "type": "module"이 설정되어 있어서 ESM으로 동작한다. 이 경우 __dirname을 사용할 수 없다.
⚠️ 문제: v1에서는 CJS라
__dirname을 쓸 수 있었지만, v2는 ESM이라 다른 방식 필요
수정 전 ❌
'@': path.resolve(__dirname, './src')
수정 후 ✅
import { fileURLToPath, URL } from 'node:url'
'@': fileURLToPath(new URL('./src', import.meta.url))
| 모드 | "type": "module" |
__dirname 사용 |
|---|---|---|
| CJS | 없음 | ✅ 가능 |
| ESM | 있음 | ❌ 불가 |
💡 참고: ESM이 현재 표준이고, 트리 쉐이킹 같은 최적화에도 유리하니까
__dirname쓰려고 CJS로 돌아가는 건 비추천
기타 설정
Husky + lint-staged
커밋 전 자동으로 lint와 포맷팅을 실행한다.
# .husky/pre-commit
#!/bin/sh
npx lint-staged
// package.json
{
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"]
}
}
commitlint
Conventional Commits 스펙을 강제한다.
// commitlint.config.cjs
module.exports = {
extends: ['@commitlint/config-conventional'],
}
커밋 메시지 예시:
feat: 사용자 인증 기능 추가
fix: 로그인 버튼 클릭 이벤트 수정
docs: README 업데이트
v1 대비 개선점
| 항목 | v1 | v2 |
|---|---|---|
| ESLint 버전 | 8.x (호환성 문제로 다운그레이드) | 9.x flat config |
| 설정 방식 | .eslintrc.js + airbnb |
eslint.config.js 직접 설정 |
| import 플러그인 | eslint-plugin-import | eslint-plugin-import-x |
| 아키텍처 강제 | ❌ 없음 | ✅ FSD boundaries 규칙 |
| Public API | ❌ 없음 | ✅ no-internal-modules |
| 파일명 컨벤션 | ❌ 없음 | ✅ check-file |
향후 추가 예정
테스트 환경 구축할 때 아래 플러그인들 추가할 예정:
eslint-plugin-testing-library- Testing Library 베스트 프랙티스eslint-plugin-vitest- Vitest 규칙eslint-plugin-storybook- Storybook 베스트 프랙티스