junyeokk
Blog
DevOps·2025. 11. 16

Prettier import 자동 정렬

프로젝트가 커지면 파일 하나에 import가 20~30줄씩 쌓인다. 처음에는 나름 순서를 맞춰서 쓰지만, 여러 명이 작업하다 보면 금방 뒤죽박죽이 된다. React import 아래에 로컬 유틸이 있고, 그 아래 다시 서드파티 라이브러리가 오고, 상대경로 import가 여기저기 흩어진다.

typescript
import { useState } from "react";
import { format } from "date-fns";
import { Button } from "@/components/ui/button";
import axios from "axios";
import { cn } from "@/lib/utils";
import { useRouter } from "next/navigation";
import { UserProfile } from "@/domain/auth/types";
import "./styles.css";

이걸 매번 수동으로 정리하는 건 비효율적이고, 팀 내에서 "import 순서 규칙"을 정해봤자 코드 리뷰 때마다 지적하는 것도 한계가 있다. Prettier가 코드 포매팅을 자동화하듯이, import 순서도 자동화할 수 있으면 좋겠다는 발상에서 나온 게 @trivago/prettier-plugin-sort-imports다.


왜 Prettier 플러그인인가

import 정렬 도구는 여러 가지가 있다. ESLint의 eslint-plugin-import에도 order 규칙이 있고, simple-import-sort 같은 ESLint 플러그인도 있다.

도구방식자동 수정설정 복잡도
eslint-plugin-import/orderESLint 규칙--fix로 가능높음 (그룹, 알파벳 등 세부 설정 多)
simple-import-sortESLint 플러그인--fix로 가능낮음 (거의 설정 없이 동작)
@trivago/prettier-plugin-sort-importsPrettier 플러그인저장 시 자동중간 (정규식 패턴으로 그룹 정의)
@ianvs/prettier-plugin-sort-importsPrettier 플러그인 (fork)저장 시 자동중간 (trivago fork, 추가 기능)

ESLint 기반 도구는 린팅 파이프라인에서 동작한다. --fix로 자동 수정할 수 있지만, 에디터에서 저장할 때 Prettier만 돌리는 환경이라면 별도로 ESLint fix를 연결해야 한다. Prettier 플러그인 방식은 이미 Prettier를 쓰고 있다면 .prettierrc에 설정 몇 줄만 추가하면 되고, 저장할 때 코드 포매팅과 동시에 import도 정렬된다. 별도의 ESLint 설정이나 추가 파이프라인 없이 기존 포매팅 흐름에 자연스럽게 끼워넣을 수 있다는 게 장점이다.


설치

bash
npm install -D @trivago/prettier-plugin-sort-imports
# 또는
pnpm add -D @trivago/prettier-plugin-sort-imports

Prettier 3.x 이상을 사용한다면 플러그인 버전도 4.x 이상이어야 한다. Prettier 3에서 플러그인 API가 바뀌었기 때문에, 구버전 플러그인은 SyntaxError: Unexpected token이나 Cannot find module 같은 에러가 발생할 수 있다.


기본 설정

.prettierrc에 플러그인을 등록하고 정렬 규칙을 정의한다.

json
{
  "plugins": ["@trivago/prettier-plugin-sort-imports"],
  "importOrder": [
    "^react$",
    "^next",
    "<THIRD_PARTY_MODULES>",
    "^@/(.*)$",
    "^[./]"
  ],
  "importOrderSeparation": true,
  "importOrderSortSpecifiers": true
}

이 설정을 적용하면 아까 뒤죽박죽이었던 import가 이렇게 정렬된다:

typescript
import { useState } from "react";

import { useRouter } from "next/navigation";

import axios from "axios";
import { format } from "date-fns";

import { UserProfile } from "@/domain/auth/types";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";

import "./styles.css";

react → next → 서드파티 → 내부 alias → 상대경로 순서로 깔끔하게 정리된다.


importOrder 패턴 상세

importOrder는 정규식 패턴의 배열이다. 배열에서 위에 있을수록 먼저 배치된다. 각 패턴에 매칭되는 import끼리 하나의 그룹을 이루고, 패턴에 매칭되지 않는 import는 <THIRD_PARTY_MODULES> 위치에 들어간다.

패턴 매칭 규칙

json
"importOrder": [
  "^react$",          // 정확히 "react"만 매칭
  "^next",            // "next"로 시작하는 모든 것 (next/navigation, next/image 등)
  "<THIRD_PARTY_MODULES>", // node_modules에서 오는 나머지 전부
  "^@/domain/(.*)$",  // @/domain/ 하위 경로
  "^@/components/(.*)$", // @/components/ 하위 경로
  "^@/(.*)$",         // 나머지 @ alias 경로
  "^[./]"             // 상대경로 (./ 또는 ../)
]

패턴은 import 경로(from 뒤의 문자열)에 대해 매칭된다. import { Button } from "@/components/ui/button"이라면 @/components/ui/button이 매칭 대상이다.

주의할 점은 패턴의 순서가 우선순위라는 것이다. ^@/domain/(.*)$^@/(.*)$가 둘 다 있으면, @/domain/auth/types는 더 먼저 나오는 ^@/domain/(.*)$에 매칭된다. 만약 순서가 뒤바뀌면 ^@/(.*)$가 먼저 잡아버려서 domain 그룹이 의미 없어진다.

<THIRD_PARTY_MODULES> 특수 토큰

이 토큰은 node_modules에서 오는 패키지를 의미한다. importOrder 배열에 명시적으로 넣어야 한다. 생략하면 서드파티 모듈이 최상단에 배치된다.

json
// 서드파티를 react/next 뒤에 배치하고 싶다면
"importOrder": ["^react$", "^next", "<THIRD_PARTY_MODULES>", "^@/(.*)$", "^[./]"]

// 서드파티를 맨 위에 두고 싶다면
"importOrder": ["<THIRD_PARTY_MODULES>", "^@/(.*)$", "^[./]"]

side-effect import

import "./styles.css" 같은 side-effect import(from 없이 경로만 있는 import)는 기본적으로 정렬 대상에서 제외되고 원래 위치에 남는다. 이를 정렬에 포함하려면 importOrderSideEffects 옵션을 사용한다(v4.x 이상).


주요 옵션

importOrderSeparation

json
"importOrderSeparation": true

true로 설정하면 각 그룹 사이에 빈 줄이 삽입된다. false면 빈 줄 없이 모든 import가 연속으로 나열된다.

빈 줄이 있으면 그룹 구분이 시각적으로 명확해진다. 하지만 import가 많지 않은 파일에서는 오히려 공간을 차지하기만 할 수 있어서 팀 취향에 따라 선택하면 된다.

typescript
// importOrderSeparation: true
import { useState } from "react";

import { useRouter } from "next/navigation";

import axios from "axios";

import { Button } from "@/components/ui/button";

// importOrderSeparation: false
import { useState } from "react";
import { useRouter } from "next/navigation";
import axios from "axios";
import { Button } from "@/components/ui/button";

importOrderSortSpecifiers

json
"importOrderSortSpecifiers": true

true로 설정하면 하나의 import 문 안에서 named import들이 알파벳 순으로 정렬된다.

typescript
// 정렬 전
import { useState, useEffect, useCallback, useMemo } from "react";

// importOrderSortSpecifiers: true 적용 후
import { useCallback, useEffect, useMemo, useState } from "react";

이 옵션이 없으면 import 문의 순서는 바뀌지만 중괄호 안의 순서는 그대로다. 코드 리뷰에서 "useEffect를 useState 앞에 쓰지 마세요" 같은 불필요한 지적을 없애려면 켜두는 게 좋다.

importOrderCaseInsensitive

json
"importOrderCaseInsensitive": true

정렬 시 대소문자를 무시한다. 기본적으로 대문자가 소문자보다 앞에 오는데(Button < cn), 이 옵션을 켜면 순수 알파벳 순으로 정렬된다.

importOrderParserPlugins

json
"importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"]

내부적으로 Babel 파서를 사용해서 AST를 파싱하는데, TypeScript나 JSX를 사용한다면 해당 파서 플러그인을 명시해야 한다. 보통은 파일 확장자를 보고 자동 감지하지만, 데코레이터(@) 문법을 쓰는 경우 decorators-legacy를 추가해야 파싱 에러가 안 난다.


prettier-plugin-tailwindcss와 함께 사용

Tailwind CSS를 쓰고 있다면 prettier-plugin-tailwindcss로 클래스를 자동 정렬하는 경우가 많다. 이 두 플러그인을 동시에 쓸 때는 plugins 배열에서 tailwindcss 플러그인이 반드시 마지막에 와야 한다.

json
{
  "plugins": [
    "@trivago/prettier-plugin-sort-imports",
    "prettier-plugin-tailwindcss"
  ]
}

이 순서가 중요한 이유는 Prettier 플러그인 체인의 동작 방식 때문이다. Prettier는 plugins 배열의 마지막 플러그인부터 역순으로 처리하는데, prettier-plugin-tailwindcss는 다른 플러그인들이 코드를 먼저 변환한 뒤 마지막에 Tailwind 클래스를 정렬하도록 설계되어 있다. 순서가 바뀌면 import 정렬이 Tailwind 플러그인의 출력을 다시 건드려서 충돌이 발생할 수 있다.

Tailwind 공식 문서에서도 이 플러그인과의 호환성을 명시하고 있고, prettier-plugin-tailwindcss를 항상 마지막에 두라고 안내하고 있다.


@ianvs/prettier-plugin-sort-imports (fork)

@trivago/prettier-plugin-sort-imports의 포크 버전으로, 몇 가지 추가 기능이 있다.

  • <BUILTIN_MODULES>: Node.js 내장 모듈(fs, path 등)을 별도 그룹으로 분리할 수 있다.
  • Type import 분리: import type { ... }을 일반 import와 별도 그룹으로 묶을 수 있다.
  • 빈 문자열로 그룹 분리: importOrder 배열에 ""를 넣으면 해당 위치에 빈 줄이 삽입된다. importOrderSeparation보다 세밀한 제어가 가능하다.
json
{
  "plugins": ["@ianvs/prettier-plugin-sort-imports"],
  "importOrder": [
    "^react$",
    "^next",
    "",
    "<THIRD_PARTY_MODULES>",
    "",
    "<TYPES>",
    "",
    "^@/(.*)$",
    "^[./]"
  ]
}

<TYPES> 토큰으로 import type 구문만 별도로 모을 수 있다. TypeScript 프로젝트에서 일반 import와 타입 import를 시각적으로 분리하고 싶다면 유용하다.


내부 동작 원리

이 플러그인이 import를 정렬하는 과정은 다음과 같다:

  1. AST 파싱: Prettier가 코드를 받으면 플러그인이 Babel 파서로 소스 코드를 AST(Abstract Syntax Tree)로 변환한다.
  2. ImportDeclaration 노드 추출: AST에서 ImportDeclaration 타입의 노드들만 추출한다. 이 노드에는 import 경로(source.value)와 imported specifier 정보가 들어 있다.
  3. 패턴 매칭: 각 import의 경로를 importOrder 배열의 정규식과 순서대로 매칭한다. 처음 매칭되는 패턴의 인덱스가 해당 import의 그룹 번호가 된다.
  4. 그룹 내 정렬: 같은 그룹에 속하는 import들을 경로 문자열 기준으로 알파벳 순 정렬한다.
  5. specifier 정렬: importOrderSortSpecifiers가 true면 각 import 문의 { } 안에 있는 specifier들도 알파벳 순으로 정렬한다.
  6. 코드 재생성: 정렬된 순서대로 import 문을 재생성하고, importOrderSeparation에 따라 그룹 사이에 빈 줄을 삽입한다.

이 과정이 Prettier의 포매팅 파이프라인 안에서 일어나기 때문에 별도의 CLI나 추가 도구 없이 prettier --write만으로 import 정렬까지 완료된다.


실전 설정 예시

Next.js + TypeScript 프로젝트에서 자주 쓰는 설정 패턴이다.

json
{
  "plugins": [
    "@trivago/prettier-plugin-sort-imports",
    "prettier-plugin-tailwindcss"
  ],
  "importOrder": [
    "^react$",
    "^next",
    "<THIRD_PARTY_MODULES>",
    "^@/domain/(.*)$",
    "^@/components/(.*)$",
    "^@/(.*)$",
    "^[./]"
  ],
  "importOrderSeparation": true,
  "importOrderSortSpecifiers": true,
  "singleQuote": true,
  "semi": true,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 120
}

이 설정의 의도를 풀어보면:

  1. react가 항상 최상단: React의 훅이나 타입을 가장 먼저 선언한다.
  2. next 관련이 그 다음: Next.js의 라우터, 이미지, 링크 등 프레임워크 레벨 import.
  3. 서드파티 라이브러리: axios, date-fns, zod 등 외부 패키지.
  4. domain 레이어: 비즈니스 로직, 타입 정의, API 호출 함수 등.
  5. components: UI 컴포넌트들.
  6. 나머지 alias: 유틸리티, 상수, 설정 등.
  7. 상대경로: 같은 폴더나 부모 폴더의 파일.

이렇게 하면 "이 파일이 외부에 어떤 의존성을 갖고 있는지"를 import 블록만 보고 한눈에 파악할 수 있다.


기존 프로젝트에 도입할 때

이미 코드가 많은 프로젝트에 도입하면 첫 실행 시 대량의 파일이 변경된다. 한 번에 prettier --write 해서 전체를 포매팅하고, 그 변경을 단독 커밋으로 남기는 게 좋다.

bash
npx prettier --write "**/*.{ts,tsx,js,jsx}" --ignore-path .gitignore

이렇게 하면 git blame이 깨지는 문제가 있는데, .git-blame-ignore-revs 파일에 해당 커밋 해시를 추가하면 된다.

정리

  • Prettier 파이프라인에 끼워넣어서 저장 시 import가 자동 정렬되므로, ESLint fix를 별도로 연결할 필요가 없다.
  • importOrder 배열의 정규식 순서가 곧 우선순위이고, <THIRD_PARTY_MODULES> 위치를 명시적으로 지정해야 의도한 그룹 배치가 된다.
  • @ianvs fork는 <TYPES> 토큰과 빈 문자열 구분자를 지원하므로, TypeScript 프로젝트에서 타입 import를 분리하고 싶다면 검토할 만하다.
# .git-blame-ignore-revs # Prettier import 정렬 일괄 적용 abc123def456...
bash
# .git-blame-ignore-revs
# Prettier import 정렬 일괄 적용
abc123def456...

GitHub은 이 파일을 자동 인식해서 웹 UI의 blame에서 해당 커밋을 건너뛴다.

관련 문서


트러블슈팅

파싱 에러

SyntaxError: This experimental syntax requires enabling one of the following parser plugin(s): "decorators"

데코레이터를 사용하는 프로젝트에서 자주 발생한다. importOrderParserPluginsdecorators-legacy를 추가하면 해결된다.

플러그인 로딩 실패

Cannot find module '@trivago/prettier-plugin-sort-imports'

pnpm을 사용하는 경우 hoisting 설정 때문에 Prettier가 플러그인을 찾지 못할 수 있다. .npmrcpublic-hoist-pattern[]=*prettier*를 추가하거나, .prettierrc에서 플러그인 경로를 명시적으로 지정한다.

정렬이 적용 안 될 때

Prettier 캐시가 원인일 수 있다. prettier --write --no-cache로 캐시를 무시하고 실행해보면 된다.