개발 업무에서 코드 스타일 일관성과 코드 품질을 유지하는 건 매우 중요합니다. 협업 프로젝트나 대규모 코드베이스에 일관되지 않은 코드 스타일과 실수가 쌓이면, 코드 가독성과 유지보수성이 저하됩니다. 이는 개발팀 협업에 방해가 되고, 팀 생산성까지 떨어뜨릴 수 있습니다.
Prettier와 ESLint를 사용하면 이러한 문제를 해결하는 데 도움이 됩니다. Prettier는 JavaScript, TypeScript, HTML, CSS 등 다양한 언어의 코드를 일관된 스타일로 자동 포맷팅하는 도구입니다. ESLint는 JavaScript, TypeScript 코드의 잠재 오류를 사전 감지하고, 정해진 코드 스타일을 강제해 일관성을 유지하도록 돕는 도구입니다.
이 글에서는 코드 스타일 일관성과 코드 품질을 유지하기 위해 Next.js 프로젝트에서 Prettier와 ESLint를 사용하는 방법을 살펴보겠습니다.
1. 도구 설치
ESLint
다음 명령을 입력해 Next.js 프로젝트, ESLint, Tailwind CSS를 설치합니다.
npx create-next-app@latest
✔ What is your project named? … nextjs-prettier-eslint
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes
Prettier
-
설치
다음 명령을 입력해 Prettier를 설치합니다.
npm install -D prettier
또는
yarn add -D prettier
-
설정
프로젝트 루트에
.prettierrc
파일을 생성하고, 아래 예시와 같이 설정합니다.{
"printWidth": 120,
"trailingComma": "es5",
"tabWidth": 4,
"semi": true,
"singleQuote": true,
"endOfLine": "auto",
"useTabs": false,
"arrowParens": "always",
"tailwindConfig": "./tailwind.config.ts"
}"printWidth"
: 한 줄의 최대 길이를 설정합니다."trailingComma"
"none"
: 마지막 요소 뒤에 쉼표를 추가하지 않습니다."es5"
: ES5에서 허용될 때만 마지막 요소 뒤에 쉼표를 추가합니다. 배열, 객체, 함수 인자 등에서 마지막 요소 뒤에 쉼표가 추가됩니다."all"
: 가능한 모든 곳에서 마지막 요소 뒤에 쉼표를 추가합니다. TypeScript나 Flow와 같은 언어의 함수 인자나 제네릭 타입 등에도 쉼표가 추가됩니다.
"tabWidth"
: 탭의 크기를 설정합니다."singleQuote"
: 문자열에 홑따옴표(')를 사용할지 겹따옴표(")를 사용할지 설정합니다. true면 홑따옴표를 사용합니다."endOfLine"
"auto"
: Prettier가 현재 파일에서 사용 중인 줄 바꿈 문자를 자동으로 감지하고 이를 유지합니다.
"useTabs"
: 탭을 사용할지 공백을 사용할지 설정합니다. false면 공백, true면 탭을 사용합니다."arrowParens"
: 화살표 함수에서 인자가 하나일 때 괄호를 사용할지 설정합니다."avoid"
(생략) 또는"always"
(항상 사용) 중 선택할 수 있습니다.
2. prettier-plugin-tailwindcss로 Prettier 활용
prettier-plugin-tailwindcss는 Prettier와 Tailwind CSS를 함께 사용하는 플러그인입니다. 이는 Prettier가 코드를 포맷팅할 때 Tailwind CSS 유틸리티 클래스를 지정된 기준에 따라 알파벳 순서로 자동 정렬합니다. 이로써 코드 스타일의 일관성을 유지하고 가독성을 높입니다.
플러그인 설치
다음 명령을 입력해 prettier-plugin-tailwindcss를 설치합니다.
npm install -D prettier-plugin-tailwindcss
또는
yarn add -D prettier-plugin-tailwindcss
- 클래스는 Tailwind 아키텍처에 따라 아래 순서로 정렬됩니다.
- 기본(Base) 레이어 클래스(예: container)
- 컴포넌트(Component) 레이어 클래스
- 유틸리티(Utility) 레이어 클래스(예: text-center, bg-red-500)
- 유틸리티 레이어에서 클래스는 기능에 따라 아래 순서로 추가 정렬됩니다.
- 레이아웃과 구조에 영향을 미치는 클래스
- 시각적 스타일과 관련된 클래스
설정
.prettierrc
파일에 다음과 같이 설정합니다.
{
.
.
.
//이전 설정들...
"plugins": ["prettier-plugin-tailwindcss"]
}
설정이 적용되지 않을 때,
-
settings.json 설정을 확인합니다.
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode" -
VS Code를 재시작합니다.
3. eslint-plugin-tailwindcss로 ESLint 활용
eslint-plugin-tailwindcss는 Tailwind CSS와 ESLint를 함께 사용하는 플러그인입니다. 이는 JSX 또는 일반 HTML에서 사용된 Tailwind CSS 클래스명을 검사하고, 올바른 Tailwind CSS 클래스명을 사용하는지 확인합니다. 이로써 코드 스타일의 일관성을 유지하도록 지원합니다.
플러그인 설치
다음 명령을 입력해 eslint-plugin-tailwindcss를 설치합니다.
npm install -D eslint-plugin-tailwindcss
또는
yarn add -D eslint-plugin-tailwindcss
설정
아래와 같이 설정합니다.
{
"plugins": ["tailwindcss"],
"extends": ["next/core-web-vitals", "plugin:tailwindcss/recommended"],
"rules": {
"tailwindcss/no-custom-classname": "off",
"tailwindcss/classnames-order": "off"
}
}
"tailwindcss/no-custom-classname"
: JSX 또는 HTML에서 Tailwind CSS로 정의되지 않은 사용자 정의 클래스명을 사용할 때 경고 또는 오류를 발생시키는 규칙입니다. 커스텀 클래스가 있으면 오류가 생기므로, 이 예시에서는“off”
로 설정합니다."tailwindcss/classnames-order"
: Tailwind CSS 클래스명을 알파벳 순서로 정렬하도록 요구하는 규칙입니다.“prettier-plugin-tailwindcss”
plugin으로 Tailwind CSS 클래스명을 정렬하기에 이 예시에서는“off”
로 설정합니다.
4. @tanstack/eslint-plugin-query로 ESLint 활용
@tanstack/eslint-plugin-query는 프론트엔드 애플리케이션의 데이터 관리, 상태 관리를 최적화하는 ESLint 플러그인입니다. 이는 TanStack Query를 사용할 때 발생하는 오류와 성능 문제를 방지하고, 최적의 코드 품질을 유지하도록 규칙과 권장 사항을 제공합니다. 이로써 코드 스타일의 일관성을 유지하고, 가독성을 향상하도록 돕습니다.
플러그인 설치
다음 명령을 입력해 @tanstack/eslint-plugin-query를 설치합니다.
npm i -D @tanstack/eslint-plugin-query
또는
yarn add -D @tanstack/eslint-plugin-query
설정
아래와 같이 설정합니다.
{
"plugins": ["@tanstack/query"],
"extends": ["plugin:@tanstack/query/recommended"],
"rules": {
// "@tanstack/query/exhaustive-deps": "error",
// "@tanstack/query/stable-query-client": "error",
// "@tanstack/query/no-rest-destructuring": "warn",
// "@tanstack/query/no-unstable-deps": "warn"
}
}
-
"@tanstack/query/exhaustive-deps"
: Query function 내부에서 사용하는 모든 변수를 Query key에 포함합니다.✅ 올바른 예
useQuery({
queryKey: ['todo', todoId],
queryFn: () => api.getTodo(todoId),
})
const todoQueries = {
detail: (id) => ({ queryKey: ['todo', id], queryFn: () => api.getTodo(id) }),
} -
"@tanstack/query/stable-query-client"
: 애플리케이션의 전체 라이프사이클 동안 하나의 QueryClient 인스턴스만 생성합니다.✅ 올바른 예
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
<Home />
</QueryClientProvider>
)
}function App() {
const [queryClient] = useState(() => new QueryClient())
return (
<QueryClientProvider client={queryClient}>
<Home />
</QueryClientProvider>
)
} -
"@tanstack/query/no-rest-destructuring"
: Query 결과에서 실제 필요한 필드에만 구독을 걸도록 설정합니다.✅ 올바른 예
const todosQuery = useQuery({
queryKey: ['todos'],
queryFn: () => api.getTodos(),
})
// normal object destructuring is fine
const { data: todos } = todosQuery -
"@tanstack/query/no-unstable-deps"
: 아래 Query hook의 반환값을 구조 분해해 얻은 개별값을 React hook의 의존성 배열에 전달합니다.- useQuery
- useSuspenseQuery
- useQueries
- useSuspenseQueries
- useInfiniteQuery
- useSuspenseInfiniteQuery
- useMutation
✅ 올바른 예
/* eslint "@tanstack/query/no-unstable-deps": "warn" */
import { useCallback } from 'React'
import { useMutation } from '@tanstack/react-query'
function Component() {
const { mutate } = useMutation({ mutationFn: (value: string) => value })
const callback = useCallback(() => {
mutate('hello')
}, [mutate])
return null
}
5. eslint-plugin-import로 ESLint 활용
eslint-plugin-import는 JavaScript, TypeScript 프로젝트의 import/export 문법을 위한 ESLint 플러그인입니다. 이는 코드에서 모듈을 가져오고 내보내는 방식의 규칙을 제공합니다. 이로써 코드 스타일의 일관성과 코드 품질을 유지하도록 지원합니다.
플러그인 설치
다음 명령을 입력해 eslint-plugin-import를 설치합니다.
npm install -D eslint-plugin-import
또는
yarn add -D eslint-plugin-import
설정
아래와 같이 설정합니다.
{
"plugins": ["import"],
"rules": {
"import/order": [
"warn",
{
"groups": [
"builtin", // Node.js 기본 모듈
"external", // 외부 라이브러리 모듈
["internal", "parent", "sibling"], // 프로젝트 내부 모듈
"index", // 인덱스 파일
"object", // import 객체 (e.g., import * as _ from 'lodash')
"type" // 타입 import (TypeScript에서)
],
"pathGroups": [
{
"pattern": "react",
"group": "external",
"position": "before"
},
{
"pattern": "{react-query,**/query}",
"group": "external",
"position": "after"
},
{
"pattern": "{app/components/**,app/**/components/**}",
"group": "internal",
"position": "after"
},
{
"pattern": "hooks/**",
"group": "internal",
"position": "after"
},
{
"pattern": "utils/**",
"group": "internal",
"position": "after"
},
{
"pattern": "{styles/**,images/**}",
"group": "internal",
"position": "after"
}
],
"pathGroupsExcludedImportTypes": ["builtin"],
"alphabetize": {
"order": "asc",
"caseInsensitive": true
},
"newlines-between": "always"
}
]
}
}
-
“groups”
: import 문을 그룹으로 나눠 정렬하는 기준을 정의합니다.“builtin”
: Node.js의 기본 모듈(fs, path 등)“external”
: 외부 라이브러리 모듈(react, lodash 등)“internal”
: 프로젝트 내부에서 정의한 모듈“parent"
: 부모 디렉터리에서 가져온 모듈“sibling”
: 형제 디렉터리에서 가져온 모듈“index”
: 디렉터리의 index.js나 index.ts 파일“object”
: 객체로 가져오는 import 문(예: import * as _ from 'lodash').“type”
: TypeScript에서 사용하는 타입을 가져오는 import 문(예:import type { MyType } from './types'
).
-
“pathGroups”
: 특정 패턴의 경로에 import 순서를 세밀하게 제어합니다.“pattern”
: 특정 패턴에 맞는 import 문을 지정합니다.“group”
: 이 import 문이 속할 그룹을 지정합니다(external, internal 등).“position”
: 해당 그룹에서 위치를 지정합니다(before, after).
-
“pathGroupsExcludedImportTypes”
: 여기 나열된 타입의 import 문은 pathGroups에서 지정된 그룹으로 구분되지 않습니다. 이때 Node.js의 기본 모듈(builtin)은 pathGroups 영향을 받지 않습니다. -
“alphabetize”
: import 문을 알파벳순으로 정렬하는 옵션“order”
:"asc"
로 설정되면, 오름차순으로 정렬합니다.“caseInsensitive”
:true
로 설정되면, 대소문자를 구분하지 않고 정렬합니다.
-
“newlines-between”
: 서로 다른 import 그룹 사이에 항상 빈 줄을 추가합니다.다음은 정렬 예시입니다.
import React, { useState, useEffect } from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import AppHeader from 'app/components/AppHeader';
import UserProfile from 'app/user/components/UserProfile';
import RouteConfig from 'app/router/RouteConfig';
import useCustomHook from 'hooks/useCustomHook';
import { formatDate } from 'utils/date';
import './styles/global.css';
import './images/logo.png';