본문 바로가기
공부/Project

eslint-plugin-layered-imports 제작기 - 5 | 배포와 회고

by Electrohyun 2026. 6. 21.

<eslint-plugin-layered-imports 제작기 시리즈> 이어서 보기


 

 

 안녕하세요! electrohyun입니다. eslint-plugin-layered-imports 제작기, 마지막 편입니다!

 

 1편에서는 제가 왜 import 정리 규칙을 만들고 싶었는지, 그리고 import를 어떤 기준으로 나누고 싶었는지 정리했습니다.

 

 2편에서는 ESLint Rule이 AST에서 ImportDeclaration을 읽고, import source를 builtin, external, internal, relative 그룹으로 분류하는 과정을 다뤘습니다.

 

 3편에서는 autofix를 구현하면서 빈 줄을 삽입하고, import 순서를 안전하게 재정렬하는 과정을 정리했습니다.

 

 4편에서는 internalAliases, groups, internalLayerOrder 옵션을 추가하고, FSD 구조를 위한 preset까지 만들어보았습니다.

 

 이제 마지막으로 남은 것은 이 플러그인을 실제 npm 패키지로 배포하는 과정입니다. 이번 글에서는 이 플러그인을 npm 패키지로 배포하면서 정리했던 내용과, 실제 프로젝트에 적용해본 뒤 느낀 점을 작성해보려고 합니다.

 


 

(0) 들어가기

 지금까지 만든 것은 “ESLint Rule”입니다. 그런데 npm에 배포하려는 것은 “ESLint Plugin”입니다. 사용자는 제 rule 파일을 직접 import해서 사용하는 것이 아닙니다. 사용자는 우선 다음과 같이 패키지를 설치합니다.

 

npm install -D eslint-plugin-layered-imports

 

ESLint config에서 플러그인을 등록한 뒤,

 

import layeredImports from "eslint-plugin-layered-imports";

export default [
  {
    plugins: {
      "layered-imports": layeredImports,
    },
    rules: {
      "layered-imports/import-spacing": "error",
    },
  },
];

 

 이런 식으로 rule을 사용하게 됩니다. 즉, rule 파일 하나만 존재한다고 해서 사용자가 바로 쓸 수 있는 것은 아니었습니다. 사용자가 import할 수 있는 플러그인 객체가 필요하고, 그 안에 rule이 등록되어 있어야 합니다. 또한 preset을 제공하려면 configs를 통해 외부로 노출해야 합니다. 그래서 이번 편의 시작점은 src/index.ts입니다.

 

(1) ESLint 플러그인의 입구, index.ts

 이번 플러그인의 진입점은 src/index.ts입니다. 대략적인 형태는 다음과 같습니다.

 

import importSpacing from "./rules/import-spacing";

const plugin = {
  meta: {
    name: "eslint-plugin-layered-imports",
    version: "0.0.0",
  },
  rules: {
    "import-spacing": importSpacing,
  },
  configs: {
    fsd: {},
  },
};

export default plugin;

 

 여기서 중요한 점은, 이 파일이 사용자가 패키지를 import했을 때 만나게 되는 입구라는 것입니다. 저는 이전까지 src/rules/import-spacing.ts 안에서 rule을 열심히 만들었습니다. 하지만 rule이 아무리 잘 작성되어 있어도, plugin 객체의 rules에 등록되어 있지 않으면 사용자는 ESLint config에서 사용할 수 없습니다.

 

rules: {
  "import-spacing": importSpacing,
}

 

이렇게 등록했기 때문에 사용자는 다음과 같은 이름으로 rule을 켤 수 있습니다.

 

"layered-imports/import-spacing": "error"

 

여기서 앞의 layered-imports는 ESLint config에서 플러그인을 등록할 때 정한 이름입니다.

 

plugins: {
  "layered-imports": layeredImports,
}

 

그리고 뒤의 import-spacing은 plugin 객체의 rules에 등록한 rule 이름입니다. 처음에는 이 이름들이 조금 헷갈렸습니다. 패키지 이름은 eslint-plugin-layered-imports인데, 실제 rule을 사용할 때는 "layered-imports/import-spacing"처럼 작성하니까요. 정리하면 다음과 같습니다.

 

npm package name
-> eslint-plugin-layered-imports

ESLint config에서 등록한 plugin name
-> layered-imports

plugin.rules에 등록한 rule name
-> import-spacing

사용자가 rules에서 사용하는 이름
-> layered-imports/import-spacing

 

 즉, rule을 만들었다고 끝나는 것이 아니라, “이 rule을 사용자가 어떤 이름으로 사용할 수 있게 할 것인가”까지 정하는 것입니다. 4편에서 만든 FSD preset도 마찬가지입니다.

 

configs: {
  fsd: {},
}

 

 이렇게 configs.fsd를 통해 노출했기 때문에 사용자는 다음처럼 사용할 수 있습니다.

 

import layeredImports from "@electrohyun/eslint-plugin-layered-imports";

export default [
  layeredImports.configs.fsd,
];

 

 이 부분에서 느낀 점은, index.ts는 단순히 파일을 모아서 export하는 곳이 아니라는 점입니다. 사용자가 이 패키지를 어떤 방식으로 만날지 결정하는 공개 API에 가깝습니다. rule 내부 구현은 나중에 바뀔 수 있지만, 사용자가 바라보는 이름과 구조는 쉽게 바꾸기 어렵습니다. 그래서 이 파일을 작성할 때는 “내부에서 편한 구조”보다 “사용자가 가져다 쓰기 좋은 구조”를 생각해야 했습니다.

 

(2) package.json 작성하기

 다음으로 정리할 파일은 package.json입니다. 프로젝트를 만들 때마다 있던 파일이지만, 보통은 의존성 목록이나 script를 관리하는 파일 정도로 여겨왔는데요, 제가 개발을 접한지 얼마 안됐을 무렵 입문했던 영상에서 패키지 제목이나 설명을 작성하던 부분이 생각났고, 또 npm 배포도 해볼까 하는 생각이 들어 정리하게 되었습니다. npm 패키지를 배포하는 입장이 되면, package.json의 역할도 중요합니다. 사용자가 이 패키지를 설치한 뒤, 어떤 파일을 읽어야 하는지 알려주는 역할을 하기 때문입니다. 대략 다음과 같은 설정이 필요했습니다.

 

{
  "name": "eslint-plugin-layered-imports",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  },
  "files": ["dist"],
  "peerDependencies": {
    "eslint": ">=9"
  }
}

 

하나씩 깊게 파고들면 훨씬 많은 이야기를 할 수 있겠지만, 이번 글에서는 제가 왜 이 설정들이 필요하다고 느꼈는지를 중심으로 정리해보겠습니다.

 

"name": "eslint-plugin-layered-imports"

 

1. 먼저 name입니다. npm 패키지 이름을 의미합니다.

 

"type": "module"

 

2. 그다음은 type입니다. 이 패키지는 ESM 기준으로 작성했습니다. 그래서 패키지 전체를 ESM 기준으로 해석하도록 type: "module"을 설정했습니다. 하지만 여기서 끝이 아닙니다. 모든 사용자가 ESM 환경에서만 사용하는 것은 아니기 때문에, 어떤 환경에서는 CommonJS 형태로 패키지를 읽을 수 있어야 합니다. 그래서 main과 module을 함께 지정했습니다.

 

"main": "./dist/index.cjs",

"module": "./dist/index.js"

 

3. main은 CommonJS 사용자가 읽을 파일을 가리키고, module은 ESM 환경에서 읽을 파일을 가리킵니다.

 

"types": "./dist/index.d.ts"

 

4. 그리고 TypeScript 사용자를 위해 타입 선언 파일도 지정했습니다. 소스 코드는 src/index.ts에 있지만, 사용자가 npm에서 설치한 뒤 읽는 것은 TypeScript 원본이 아닙니다. 빌드된 dist 파일입니다. 즉, 제가 작성하는 파일은 src/index.ts지만, npm 사용자 입장에서 실제 진입점은 다음 파일들입니다. (dist/index.js, dist/index.cjs, dist/index.d.ts)

 

"exports": {
  ".": {
    "types": "./dist/index.d.ts",
    "import": "./dist/index.js",
    "require": "./dist/index.cjs"
  }
}

 

5. exports입니다. exports는 이 패키지의 공개 진입점을 명확히 지정합니다. 사용자가 패키지를 import할 때...

 

import layeredImports from "eslint-plugin-layered-imports";

 

 이러한 요청이 어떤 파일로 연결되어야 하는지를 알려주는 역할입니다. ESM, CommonJS, 타입 선언까지 같이 고려하려면 exports에서 진입점을 명확하게 지정하는 편이 좋다고 판단했습니다.

 

"files": ["dist"]

 

6. files입니다. 이 설정은 npm에 어떤 파일을 포함할지 정하는 역할입니다. 소스 코드, 테스트 코드, 설정 파일까지 전부 npm 패키지에 포함할 필요는 없습니다. 사용자가 실제로 필요한 것은 빌드된 결과물이기 때문에, 저는 dist만 포함하도록 설정했습니다.

 

"peerDependencies": {
  "eslint": ">=9"
}

 

7. 마지막으로 peerDependencies입니다. ESLint 플러그인은 독립적으로 실행되는 프로그램이 아니라, 사용자의 프로젝트에 설치된 ESLint와 함께 동작하는 확장입니다. 그래서 플러그인 안에 ESLint를 포함하기보다, 사용자의 프로젝트에 설치된 ESLint를 기준으로 동작하도록 peerDependencies를 작성해두는 것이 자연스럽다고 판단했습니다.

 

 

(3) tsup으로 배포용 파일 만들기

 다음으로 필요한 것은 빌드입니다. React 앱을 만들 때는 보통 Vite나 Next.js가 많은 부분을 처리해줍니다. 개발자는 src 안에 코드를 작성하고, 빌드 명령어를 실행하면 앱이 배포 가능한 형태로 만들어집니다. 하지만 npm 라이브러리는 조금 다릅니다. 제가 작성한 코드는 TypeScript 파일입니다. (src/index.ts, src/rules/import-spacing.ts)

 

 하지만 사용자가 npm에서 설치한 뒤 바로 TypeScript 원본을 읽는 것은 아닙니다. 사용자가 import할 수 있는 JavaScript 파일과 타입 선언 파일을 만들어줘야 합니다. 그래서 이번 플러그인에서는 tsup을 사용했습니다. 설정은 대략 다음과 같습니다.

 

import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["src/index.ts"],
  format: ["esm", "cjs"],
  dts: true,
  clean: true,
});

 

entry: ["src/index.ts"]

 

 1. 여기서 entry는 빌드 시작점입니다. 앞에서 말했듯이, src/index.ts는 플러그인의 입구입니다. 따라서 이 파일을 기준으로 빌드를 시작하면, 내부에서 import하고 있는 rule 파일들도 함께 빌드 대상에 포함됩니다.

 

format: ["esm", "cjs"]

 

 2. 다음은 format입니다. ESM과 CommonJS 형식을 모두 생성하도록 했습니다. 요즘 프론트엔드 프로젝트에서는 ESM을 많이 사용하지만, 패키지를 배포하는 입장에서는 다양한 사용 환경을 생각해야 했습니다. 그래서 import로 읽는 환경과 require로 읽는 환경을 모두 고려했습니다.

 

dts: true

 

 3. 다음은 dts입니다. TypeScript 타입 선언 파일을 생성합니다. 이 설정을 켜면 빌드 결과에 index.d.ts가 포함됩니다. 사용자가 TypeScript 환경에서 이 플러그인을 import했을 때 타입 정보를 확인할 수 있게 됩니다.

 

clean: true

 

 4. 마지막으로 clean입니다. 빌드 전에 dist 폴더를 비웁니다. 이 설정이 없으면 이전 빌드 결과물이 남아 있을 수 있습니다. 배포용 파일을 만들 때 이전 결과물이 섞이면 헷갈릴 수 있으므로, 빌드할 때마다 깨끗한 상태에서 다시 생성하도록 했습니다.

 

dist/index.js
dist/index.cjs
dist/index.d.ts

 

 5. 빌드 결과는 위와 같습니다. 각 파일의 역할을 정리하면 이렇습니다.

 

 - dist/index.js -> ESM 환경에서 사용하는 파일

 - dist/index.cjs -> CommonJS 환경에서 사용하는 파일

 - dist/index.d.ts -> TypeScript 타입 선언 파일

 

이 과정을 거치고 나니 package.json의 설정이 더 잘 이해되었습니다. main, module, types, exports 모두 이 빌드 결과물을 가리킵니다. 즉, tsup은 TypeScript 소스 코드를 배포 가능한 결과물로 만들고, package.json은 사용자가 그 결과물 중 어떤 파일을 읽어야 하는지 알려주는 구조라고 봐주시면 될 것 같습니다.

 

(4) 배포 전에 돌리는 검증

 배포 전에 최소한의 검증 과정도 필요했습니다. 이번 플러그인에서는 다음과 같은 script를 사용했습니다.

 

{
  "scripts": {
    "build": "tsup",
    "test": "vitest",
    "test:run": "vitest run",
    "typecheck": "tsc --noEmit"
  }
}

 

 각 명령어의 역할은 다음과 같습니다.

 - pnpm test -> 개발 중 테스트를 watch mode로 실행할 때 사용했습니다.

 - pnpm test:run -> 한 번 테스트를 실행하고 종료할 때 사용했습니다. 배포 전에는 pnpm test의 watch mode보다 이 명령어가 더 적절했습니다.

 - pnpm typecheck -> TypeScript 타입 검사를 실행합니다.

 - pnpm build -> 배포용 dist 파일을 생성합니다. 이번 플러그인을 만들면서 테스트의 중요성을 계속 느꼈습니다. 특히 ESLint Rule은 사용자의 코드를 검사합니다.

 

 여기까지는 그래도 문제를 알려주는 검증들이지만, autofix는 사용자의 코드를 직접 수정합니다. 빈 줄을 삽입하는 정도는 상대적으로 부담이 덜하지만, import 순서를 재정렬하는 순간부터는 코드의 의미가 바뀌지 않는지 신경써야 합니다. 3편에서 side-effect import가 포함된 block은 자동 수정하지 않도록 처리한 것도 이 때문이었습니다. 물론 테스트가 있다고 해서 모든 문제가 사라지는 것은 아니지만, 정해둔 기준은 테스트로 고정해두고 배포하기 위함이었습니다.

 

(5) README 작성하기

 npm 패키지는 코드만 있다고 끝나지 않습니다. 사용자는 npm 페이지나 GitHub 저장소에 들어왔을 때 README를 먼저 봅니다. 이 패키지가 무엇을 하는지, 왜 필요한지, 어떻게 설치하는지, 어떻게 설정하는지를 README에서 판단합니다. 그래서 README에는 다음 내용을 기재했습니다.

 

1. 설치 방법

npm install -D eslint-plugin-layered-imports

 

2. 기본 사용법

import layeredImports from "eslint-plugin-layered-imports";

 

export default [

  {

    plugins: {

      "layered-imports": layeredImports,

    },

    rules: {

      "layered-imports/import-spacing": "error",

    },

  },

];

 

 이 플러그인은 ESLint flat config 기준으로 사용할 수 있도록 작성했습니다. 사용자가 패키지를 설치한 뒤 가장 먼저 막히는 부분은 보통 “그래서 config에 어떻게 적어야 하지?”라는 점이라고 생각했습니다. 그래서 기본 설정 예시는 최대한 바로 복사해서 사용할 수 있는 형태로 작성하고자 했습니다.

 

3. 옵션 사용법

"layered-imports/import-spacing": [

  "error",

  {

    internalAliases: ["@/", "~/", "@app/"],

    groups: ["builtin", "external", "internal", "relative"],

    internalLayerOrder: [

      "app",

      "pages",

      "widgets",

      "features",

      "entities",

      "shared"

    ]

  }

]

 

 4편에서 다뤘던 옵션들이 여기서 문서화됩니다. internalAliases는 프로젝트 내부 alias를 지정합니다. groups는 import group 순서를 지정합니다. internalLayerOrder는 internal import 안에서 FSD layer 순서를 지정합니다.

 

 코드로 구현할 때는 옵션 객체를 어떻게 읽고 검증할지에 집중했습니다. README를 작성할 때는 반대로 생각했습니다. 사용자가 이 옵션을 왜 써야 하는가, 어떤 상황에서 이 옵션이 필요한가, 기본값만으로 충분한 경우와 직접 설정해야 하는 경우는 무엇인가. 이런 질문을 기준으로 설명을 작성했습니다.

 

4. FSD preset 사용법입니다.

import layeredImports from "@electrohyun/eslint-plugin-layered-imports";

 

export default [

  layeredImports.configs.fsd,

];

 

 FSD preset은 사용자가 매번 긴 옵션을 직접 작성하지 않아도 되도록 제공한 설정 묶음입니다. 처음에는 preset도 “있으면 편하겠다” 정도로 생각했지만, 문서화까지 하고 나니 이 기능의 의미가 조금 더 분명해졌습니다. 직접 옵션을 설정할 수도 있고, FSD 구조를 사용한다면 preset을 가져다 쓸 수도 있습니다.

 

 5. 로고 만들기

 

 

 이제 이걸로 리드미 작성은 끝입니다. 하지만 항상 제가 개발을 하며 사용하던 라이브러리엔 로고가 꼭 하나씩 있던 게 떠올랐습니다. 그리하여 eslint-plugin-layered-imports라는 이름에 걸맞는 로고를 고민했고, 바로 떠올랐습니다. 계층이 있는 import이므로, 무언가가 쌓여있고, 그런 느낌은 케이크가 떠올랐습니다. 그 중에서도 형형색색의 FSD 구조가 떠올랐고, 저는 거기에서부터 레인보우 케이크가 생각나 이런 로고를 작성하게 되었네요.

 

 로고 디자인 이유는 다음과 같습니다.

 

 (1) 실제 무지개 순서가 아닌 이유는, 완벽한 순서 혹은 로직을 가진 플러그인은 아니기 때문입니다.

 

 (2) 그렇지만 완벽하지 않다는 점은 오히려 커스텀할 수 있도록, 사용자에게 원하는 층(계층)을 원하는 색(순서)으로 사용할 수 있음을 전하고자 하는 뜻을 담았습니다.

 

 (3) 그리고 케이크는 맛있습니다. 행복하죠(?). 사용자가 이 플러그인을 사용하면서 개발이 조금 더 행복해졌으면 하는 의미를 담았습니다.

(6) npm publish 하기

 이렇게 빌드와 문서 정리, 로고 제작까지 마친 뒤에는 npm에 배포했습니다. 흐름은 대략 다음과 같습니다.

 

pnpm build

pnpm test:run

pnpm typecheck

npm login

npm publish --access public

 

 pnpm build, 먼저 빌드합니다. 이 명령어로 dist 폴더를 생성합니다.

 

 pnpm test:run, 그다음 테스트를 실행합니다. 

 

 pnpm typecheck, 그리고 타입체크를 실행합니다.

 

 npm login, 여기까지 통과했다면, 이제 npm에 로그인합니다.

 

 npm publish, 마지막으로 publish를 실행합니다.

 

 처음 npm publish를 실행할 때는 조금 긴장됐습니다. 로컬에서 테스트를 돌리고, 빌드도 통과하고, README도 작성했지만, publish를 하는 순간 이 패키지는 다른 사람이 설치할 수 있는 공개 패키지가 됩니다. 물론 사용자가 많지 않을 수도 있고, 당장 누군가가 바로 설치하지 않을 수도 있습니다. 그럼에도 npm에 올라간다는 것은 “내 로컬에서만 돌아가는 코드”가 아니라 “외부에서 사용할 여지가 생기는 도구”가 된다는 뜻입니다. 그래서 마지막으로 다음과 같은 내용들을 확인했습니다.

 

1. dist 파일이 제대로 생성되었는가?

2. package.json의 main, module, types, exports가 dist를 잘 가리키는가?

3. npm에 불필요한 파일이 포함되지 않도록 files 설정이 되어 있는가?

4. README의 설치 방법과 사용 예시가 실제로 맞는가?

5. 테스트와 타입체크가 통과하는가?

 

이 과정을 거치고 나서, 배포를 진행했습니다.

 

(7) 실제 프로젝트에 적용해보며 느낀 점

https://www.ggogit.com/

 

가장 친절한 Git 여행 | 꼬깃

게임처럼 Git 개념을 배우는 꼬깃의 시작 화면입니다.

www.ggogit.com

 

 이 플러그인은 처음부터 제 프로젝트에 적용하기 위해 만들었습니다. 제가 진행 중인 Git 학습 게임 프로젝트인 꼬깃에서도 import 정리는 계속 신경쓰이는 부분이었습니다. 프로젝트가 작을 때는 import가 조금 섞여 있어도 크게 문제처럼 느껴지지 않습니다. 파일 수도 많지 않고, 제가 작성한 코드가 대부분이기 때문입니다. 하지만 기능이 늘어나고, 폴더 구조가 생기고, features, entities, shared 같은 레이어를 나누기 시작하면 import 구문도 점점 정보를 가진다고 생각합니다.

 

import React from "react";

import { userApi } from "@/entities/user";
import { Button } from "@/shared/ui";

import styles from "./style.module.css";

 

 이런 식으로 정리되어 있으면 파일을 열었을 때 의존성 흐름이 조금 더 잘 보입니다. 외부 라이브러리를 쓰는지, 프로젝트 내부에서는 어떤 레이어를 참조하는지, 현재 파일 주변의 상대 경로 import가 있는지 확인하기 편해집니다.

 

 플러그인을 하나 만든다고 해서 코드 품질이 아주 뛰어나게 올라가는 것은 아니지만, 반복되는 하나의 작업을 자동화함으로써 워크플로우에서 제거한다는 점에 의의를 두었습니다. 새 파일을 만들 때마다 import 순서를 신경써야 하고, 다른 파일에서 코드를 복사해오면 import가 섞이고, 기능 구현에 집중하다 보면 빈 줄 하나 정도는 금방 흐트러진다고 생각합니다. 그래서 저는 이런 규칙은 사람이 계속 기억해서 지키기보다, 도구가 잡아주는 편이 더 낫다고 생각했습니다. 특히 autofix가 있다면 더 좋습니다. 사용자는 매번 직접 줄을 옮기거나 빈 줄을 넣지 않아도 되므로, lint error가 발생하면 eslint --fix를 통해 정리할 수 있습니다.

 

(8) 아쉬운 점과 개선 방향

 그리하여, 배포까지 마쳤지만, 만들고 나서 이렇게 후기를 작성하다 보니, 아직 아쉬운 점도 조금 보였습니다. 오히려 배포를 하고 나서 개선할 부분이 보이는 느낌이었습니다.

 

1. 더 많은 import 케이스 지원

 먼저 import 케이스를 더 다양하게 다뤄야 합니다. 대표적으로 type-only import가 있습니다. 

 

import type { User } from "@/entities/user";

 

 TypeScript에서는 타입만 가져오는 import가 따로 존재합니다. 현재 rule이 이를 어느 정도 처리할 수 있더라도, type-only import를 일반 import와 같은 기준으로 둘지, 별도 정책을 둘지 고민해볼 수 있습니다.

 

import "./global.css";

 

 side-effect import도 더 고민할 수 있습니다. 3편에서 작성했듯, side-effect import는 순서를 바꾸면 실행 순서가 달라질 수 있습니다. 그래서 자동 수정 대상에서 제외하는 기준을 두었습니다. 하지만 앞으로는 side-effect import를 아예 어떤 위치에 두도록 권장할지, report만 할지, 설정으로 제어할지 같은 정책을 더 정리할 수 있을 것 같습니다.

 

 주석 처리도 더 보완할 수 있습니다. import 위에 붙은 주석을 함께 이동하도록 처리했지만, 실제 프로젝트에서는 주석 형태가 다양할 수 있습니다. 이 부분도 추후 더 많은 케이스로 테스트해보고 싶습니다.

 

2. 옵션 설계 개선

옵션 설계도 더 개선할 수 있습니다. 현재는 groups, internalAliases, internalLayerOrder를 제공합니다.

 

{
  internalAliases: ["@/"],
  groups: ["builtin", "external", "internal", "relative"],
  internalLayerOrder: [
    "app",
    "pages",
    "widgets",
    "features",
    "entities",
    "shared"
  ]
}

 

 사용자가 잘못된 옵션을 넣었을 때 더 친절한 피드백을 줄 수 있지는 않을까 하는 생각이 들었습니다.

 

 1. 예를 들어 groups에는 정해진 네 가지 group만 들어갈 수 있어야 합니다. "builtin", "external", "internal", "relative".

 2. 또한 같은 group이 중복되면 순서 판단이 애매해지므로, 중복을 막아야 합니다. 4편에서 schema의 필요성을 다루긴 했지만, 실제 사용자가 마주할 설정 실수는 더 많을 수 있습니다. 옵션이 늘어날수록 schema의 역할도 더 중요해진다고 생각했습니다.

 3. internalLayerOrder도 마찬가지입니다. FSD 기준으로는 app, pages, widgets, features, entities, shared 순서를 제공했지만, 프로젝트마다 구조가 다를 수 있습니다. 나중에는 internal layer를 단순 prefix 기준으로 볼지, alias 이후 첫 번째 segment를 기준으로 볼지, 더 세밀한 제어가 필요할지도 고민해볼 수 있습니다.

 

3. preset 추가 고민

layeredImports.configs.fsd

 

 현재는 FSD preset만 제공합니다. 이 preset은 제가 사용하는 구조를 기준으로 만들었습니다. 하지만 다른 프로젝트에서는 다른 기준을 사용할 수 있습니다. 예를 들어 FSD를 사용하지 않는 일반적인 alias 기반 프로젝트라면, FSD preset이 오히려 맞지 않을 수 있습니다.

 

 그래서 나중에는 더 기본적인 preset을 추가하거나, preset 이름을 더 명확히 가져가는 것도 고민할 수 있을 것 같습니다. 다만 preset이 많아지면 선택지는 늘어나지만, 사용자는 어떤 것을 써야 할지 고민하게 됩니다. 그래서 preset을 추가한다면 “정말 자주 쓰이는 구조인가?”를 기준으로 판단해야 할 것 같습니다.

 

4. README와 문서 개선

 README도 아직 더 보완할 수 있습니다. 현재는 설치 방법, 기본 사용법, 옵션 사용법, FSD preset 사용법을 중심으로 작성했습니다. 하지만 실제 사용자를 생각하면 before/after 예시가 더 필요할 수 있습니다.

 

 예를 들어 다음과 같은 입력 코드가 있을 때,

 

import { Button } from "@/shared/ui";
import React from "react";
import styles from "./style.module.css";

 

 fix 이후 다음처럼 바뀐다는 것을 보여주면 더 직관적입니다.

 

import React from "react";

import { Button } from "@/shared/ui";

import styles from "./style.module.css";

 

 또한 안전하지 않은 autofix 조건도 문서에 명시할 수 있습니다. 예를 들어 side-effect import가 포함된 경우에는 report만 하고 자동 수정하지 않는다는 점을 README에서 설명하면, 사용자가 “왜 fix가 안 되지?”라고 느낄 때 도움이 될 수 있습니다. troubleshooting 섹션도 추가할 수 있을 것 같습니다.

 

예를 들어 다음과 같은 상황입니다.

- @/ alias를 internal로 인식하지 못하는 경우

- FSD preset을 적용했는데 원하는 순서와 다른 경우

- CommonJS 환경에서 import가 되지 않는 경우

- ESLint 버전이 맞지 않는 경우

 

 코드도 중요하지만, 결국 사용자가 막혔을 때 문서가 도와줘야 합니다. 이 부분은 보완해보려 합니다.

 

5. 테스트 강화

 테스트도 더 강화할 수 있습니다. 현재는 RuleTester와 vitest를 통해 주요 케이스를 검증했습니다. 하지만 실제 프로젝트에서는 더 다양한 import 형태가 존재합니다.

 

import React, { useMemo } from "react";

import type { User } from "@/entities/user";

import "./global.css";

import styles from "./style.module.css";

 

 이런 케이스들이 섞였을 때도 rule이 의도대로 동작하는지 더 확인해야 합니다. 특히 autofix output 테스트는 계속 늘려야 한다고 느꼈습니다. autofix는 사용자의 코드를 직접 바꾸기 때문에, 입력과 기대 output을 테스트로 고정해두는 것이 중요했습니다. 나중에는 실제 프로젝트 fixture를 기반으로 테스트해보는 것도 좋을 것 같습니다. 작은 예제 코드뿐 아니라, 실제 프로젝트에서 자주 나오는 import block을 테스트 데이터로 두면 더 현실적인 검증이 가능할 것 같습니다.

 

6. rule 이름에 대한 고민

 마지막으로 rule 이름에 대한 고민도 있습니다. 현재 rule 이름은 import-spacing입니다. 처음에는 그룹 사이의 빈 줄을 다루는 규칙으로 시작했기 때문에 이 이름이 자연스러웠습니다. 하지만 만들다 보니 rule의 범위가 조금 넓어졌습니다. 지금은 spacing뿐 아니라 다음 내용도 함께 다룹니다...

 

1. import group 분류

2. group 사이 빈 줄 검사

3. group order 검사

4. internal layer order 검사

5. autofix

 

 그러다 보니 import-spacing이라는 이름이 현재 rule의 역할을 충분히 설명하는지 고민이 생겼습니다. 나중에는 rule을 분리할 수도 있을 것 같습니다. 예를 들어 빈 줄만 다루는 rule, group order를 다루는 rule, internal layer order를 다루는 rule로 나눌 수도 있습니다. 혹은 rule 이름 자체를 더 넓은 의미로 바꾸는 방법도 있습니다. 물론 이미 배포한 rule 이름을 바꾸는 것은 사용자에게 breaking change가 될 수 있습니다. 그래서 이 부분은 당장 바꾸기보다, rule의 방향이 더 명확해졌을 때 신중하게 결정해야 할 것 같습니다.

 

(9) 돌아보며...

 간단한 생각에서 시작했습니다.

 

 import 순서가 파일마다 다르고, 외부 라이브러리와 내부 alias와 상대 경로 import가 섞여 있는 것이 계속 반복되었습니다. 컨벤션 문서에 적어두고 개발을 계속했습니다.

 

 그러나 프로젝트가 바빠지면서 이런 규칙이 쉽게 흐트러졌습니다. 그래서 ESLint Rule로 만들어보자는 생각을 하게 되었습니다.

 

 처음에는 “import 문을 찾아서 순서만 보면 되지 않을까?” 정도로 생각했습니다. 하지만 실제로는 ESLint가 코드를 AST로 변환하고, rule은 그 AST를 탐색하면서 문제를 찾아야 했습니다.

 

 그래서 2편에서는 Program, ImportDeclaration, node.source.value, context.report() 같은 개념을 다뤘고, 그다음에는 문제를 찾는 것에 더해, 자동으로 고치는 단계로 넘어갔습니다.

 

 처음에는 빈 줄 하나 넣는 fixer로 시작했습니다. 하지만 import 순서를 재정렬하려고 하니, side-effect import, non-import statement, leading comment, 같은 group 내부 순서 유지 같은 기준이 필요했습니다. 명확한 기준과, 판단이 필요했습니다.

 

 4편에서는 옵션과 preset을 추가했습니다. 처음에는 제 프로젝트에서만 동작하면 충분하다고 생각했습니다. 하지만 패키지로 만들고 싶다면 다른 프로젝트의 alias와 구조도 고려해야 했습니다. 그래서 internalAliases, groups, internalLayerOrder를 옵션으로 받도록 했고, FSD 구조를 위한 preset도 제공했습니다.

 

 그리고 이번 5편에서는 이 rule을 npm 패키지로 배포하는 과정을 정리했습니다. rule을 만들고 끝나는 것이 아니라, plugin 객체로 노출하고, package.json 진입점을 정하고, tsup으로 빌드하고, README를 작성하고, npm에 publish하는 과정까지 이어졌습니다.

 

 그리하여, 돌아보면 import 순서를 정리하는 플러그인을 제작하면서 겪고 배운 것이 참 많습니다. ESLint가 코드를 어떻게 읽는지, rule이 문제를 어떻게 찾는지, fixer가 코드를 어떻게 수정하는지, schema가 옵션을 어떻게 검증하는지, preset이 설정을 어떻게 묶어주는지, npm 패키지가 사용자에게 어떻게 전달되는지까지 이어지는 경험을 배웠습니다.

 

 프로젝트를 하면서 무엇보다 좋았던 점은, 제가 불편하다고 느낀 문제를 가장 처음으로, 개발자 도구로 작성해봤다는 점입니다. 프론트엔드 개발을 하다 보면 많은 도구를 사용합니다. ESLint, Prettier, Vite, Next.js, TypeScript 등등... 이전에는 이런 도구들을 주로 “처음에만 까는 거”로만 바라봤었습니다. 다만 그 도구의 일부를 이번에 직접 만들어보면서, 도구가 어떤 식으로 코드를 읽고 판단하는지 조금은 더 가까이에서 볼 수 있었습니다.

 

 물론 아직 부족한 부분도 많습니다. 더 많은 import 케이스를 지원해야 하고, 옵션 설계도 다듬어야 하고, 문서와 테스트도 계속 보완해야 합니다. 갈 길은 여전히 멉니다...

 

 그래도 이번 작업을 통해 “작은 불편함을 그냥 넘기지 않고, 내가 직접 도구로 만들어볼 수 있다”는 경험을 얻을 수 있었습니다. 무엇보다 다시 이야기 하는 것이지만 짧은 시간이었지만 이 프로젝트는 정말 재밌었습니다. 끝까지 만들고 배포하면서, 많은 것을 배울 수 있었습니다.

 

 그래서... 저는 이제부터 앞으로도 프로젝트를 진행하면서 반복되는 불편함이 보이면, 앞으로 이따금씩 작은 도구로 만들어보려고 합니다. 그만큼 이번 경험이 값지다고 생각했습니다😊

 

 그럼 이상으로, eslint-plugin-layered-imports의 제작기는 여기서 마무리하겠습니다. 긴 글, 시리즈를 읽어주셔서 감사합니다! 지금까지 electrohyun이었습니다. ⚡😎