Skip to content

Oxlint JS 플러그인 프리뷰

이 글은 Oxlint JS 플러그인 프리뷰 릴리스를 알립니다. JS 플러그인은 이후 알파에 도달했습니다. 최신 기능·개선은 Oxlint JS 플러그인 알파 발표를 참고하세요.


올해 초 커뮤니티 의견을 구한 Oxlint 커스텀 JS 플러그인 설계가 오늘, 수개월의 연구·프로토타입·구현 끝에 공개됩니다.

Oxlint는 JS로 작성한 플러그인을 지원합니다!

주요 특징

  • ESLint 호환 플러그인 API. 수정 없이 많은 기존 ESLint 플러그인을 실행할 수 있습니다.
  • 조금 다른 대체 API로 더 나은 성능을 열어 줍니다.

이번 릴리스가 되는 것과 아닌 것

프리뷰는 시작에 불과합니다. 참고할 점:

  • 첫 릴리스는 ESLint 플러그인 API 전체를 구현하지 않았습니다.
  • 성능은 좋지만 훨씬 나아질 예정입니다. 최적화 파이프라인이 많습니다.

코드 검사 규칙에 흔히 쓰이는 API 대부분이 구현되어 많은 ESLint 규칙이 이미 동작합니다. 토큰 관련 API는 없어 스타일(포맷) 규칙은 아직 안 됩니다.

직접 써 보시고 피드백을 주시면 다음 개발 우선순위에 반영하겠습니다.

이 글에서 다루는 내용

  1. 사용 방법
  2. 앞으로의 계획
  3. ESLint 호환과 뛰어난 성능을 동시에 노리는 기술적 배경

Quick Start

프로젝트에 Oxlint 설치:

sh
pnpm add -D oxlint

커스텀 JS 플러그인 작성:

js
// plugin.js

// 가장 단순한 규칙 — debugger 금지
const rule = {
  create(context) {
    return {
      DebuggerStatement(node) {
        context.report({
          message: "debugger를 사용할 수 없습니다!",
          node,
        });
      },
    };
  },
};

const plugin = {
  meta: {
    name: "best-plugin-ever",
  },
  rules: {
    "no-debugger": rule,
  },
};

export default plugin;

플러그인을 켜는 설정 파일:

json
// .oxlintrc.json
{
  "jsPlugins": ["./plugin.js"],
  "rules": {
    "best-plugin-ever/no-debugger": "error"
  }
}

린트할 파일 추가:

js
// foo.js
debugger;

Oxlint 실행:

sh
pnpm oxlint

기대 출력:

 x best-plugin-ever(no-debugger): debugger를 사용할 수 없습니다!
  ,-[foo.js:1:1]
1 | debugger;
  : ^^^^^^^^^
  `----

플러그인 작성 자세한 내용은 문서를 참고하세요.

Alternative API

Oxlint는 성능을 높이기 위해 약간 다른 API도 제공합니다.

이 대체 API로 만든 플러그인은 ESLint와 Oxlint 모두와 호환됩니다.

파일에 클래스 선언이 5개를 넘으면 경고하는 규칙 예시:

ESLint 스타일

js
const rule = {
  create(context) {
    let classCount = 0;

    return {
      ClassDeclaration(node) {
        classCount++;
        if (classCount === 6) {
          context.report({ message: "클래스가 너무 많습니다", node });
        }
      },
    };
  },
};

대체 API 버전

js
import { defineRule } from "oxlint";

const rule = defineRule({
  createOnce(context) {
    // 카운터 변수 정의
    let classCount;

    return {
      before() {
        // 파일 AST를 순회하기 전에 카운터 초기화
        classCount = 0;
      },
      // 이전과 동일
      ClassDeclaration(node) {
        classCount++;
        if (classCount === 6) {
          context.report({ message: "클래스가 너무 많습니다", node });
        }
      },
    };
  },
});

차이점

  1. 규칙 객체를 defineRule(...)로 감쌉니다.
diff
- const rule = {
+ const rule = defineRule({
  1. create 대신 createOnce를 씁니다.
diff
-   create(context) {
+   createOnce(context) {
  1. 파일마다 돌아가는 준비 코드는 create 본문이 아니라 before 훅으로 옮깁니다.
diff
-     let classCount = 0;
+     let classCount;

      return {
+       before() {
+         classCount = 0; // 카운터 초기화
+       },
        ClassDeclaration(node) {
          classCount++;
          if (classCount === 6) {
            context.report({ message: "클래스가 너무 많습니다", node });
          }
        },
      };
    },
  });

실질적 차이는 이것뿐입니다. ESLint의 create는 파일마다 반복 호출되고, createOnce는 한 번만 호출됩니다.

그 외 API는 ESLint와 동일하게 동작합니다.

대체 API가 왜 성능 잠재력이 있는지는 문서를 참고하세요.

Performance

위에서 말했듯 프리뷰 초기에는 성능에 집중하지 않았습니다. 목표는 실제 프로젝트에 쓸 만큼 API를 채우고 초기 채택자 피드백을 모으는 것이었습니다.

현재 성능은 나쁘지 않지만 압도적이지는 않습니다.

다만 중요한 점은, 다음 버전 프로토타입이 보여 주듯 확정한 아키텍처는 여러 최적화가 더해지면 탁월한 성능을 낼 수 있다는 것입니다(Under the hood).

향후 몇 달에 그 최적화를 적용하면 지금보다 여러 배 빨라질 것입니다.

그 전이라도 Oxlint 성능은 경쟁력이 있습니다.

중간 규모 TypeScript 프로젝트 vuejs/core에서 ESLint 대 Oxlint:

LinterTime
ESLint4,116 ms
ESLint multi-threaded3,710 ms
Oxlint48 ms
Oxlint with custom JS plugin236 ms
상세

INFO

sh
hyperfine -i --warmup 3 \
  './node_modules/.bin/oxlint --silent' \
  './node_modules/.bin/oxlint -c .oxlintrc-with-custom-plugin.json --silent' \
  'USE_CUSTOM_PLUGIN=true ./node_modules/.bin/eslint .' \
  'USE_CUSTOM_PLUGIN=true ./node_modules/.bin/eslint . --concurrency=auto'

참고: 집필 시점 NPM의 Oxlint(1.23.0)에는 이 벤치에 영향을 주는 버그가 있어 JS 플러그인 비용을 크게 과소평가합니다. 위 수치는 버그 수정 후 최신 main 이 커밋 기준입니다. 아래 수정 사항도 참고하세요.

이 예에서는 단순 JS 플러그인을 Oxlint에 더해도 비용이 있지만, ESLint 새 다중 스레드 러너를 써도 Oxlint가 여전히 약 15배 빠릅니다.

복잡한 플러그인이 많으면 비용은 더 커집니다.

Features

Oxlint는 대부분 AST만 보는 플러그인·규칙에서 쓰는 ESLint API를 지원합니다. 대부분의 "코드 수정"형 규칙이 포함됩니다.

토큰 API는 아직 없어 스타일(포맷) 규칙은 동작하지 않습니다.

지원

  • AST 순회
  • AST 탐색(node.parent, context.sourceCode.getAncestors)
  • 수정(fix)
  • 셀렉터(ESLint 문서)
  • SourceCode API(예: context.sourceCode.getText(node))

아직 미지원

  • 언어 서버(IDE) 지원
  • 규칙 옵션
  • 제안(suggestions)
  • 스코프 분석 (v1.25.0부터 구현)
  • 토큰·코멘트 관련 SourceCode API(예: context.sourceCode.getTokens(node))
  • 제어 흐름 분석

What's next

향후 몇 달 동안:

1. 플러그인 API 면적 채우기

목표는 ESLint 플러그인 API 100% 지원으로, 결국 수정 없이 어떤 ESLint 플러그인이든 돌릴 수 있게 하는 것입니다.

2. 성능 개선

이미 나쁘지 않지만, 프로토타입에서 더 큰 이득이 있다는 것을 확인했습니다. 적용하여 JS 플러그인이 Rust에 최대한 가깝게 돌아가게 하겠습니다.

Under the hood

JS 플러그인 사용에는 필요 없는 내용입니다. 구현이 궁금하다면 계속 읽어 보세요.

큰 질문: ESLint 호환을 할 것인가 말 것인가?

올해 초 커뮤니티에 던진 질문은 Oxlint가 ESLint 호환 플러그인 API를 목표로 할지였습니다.

호환 인터페이스는 친숙함과 ESLint 이전 이점이 큽니다.

하지만 Oxlint는 성능으로 알려져 있고, 그것을 너무 잃으면 곤란합니다.

프로토타입의 주 목적은 성능과 ESLint 호환 사이 트레이드오프를 정량화하고, "케이크를 먹으면서 가지기"식 해법이 있는지 찾는 것이었습니다. (여기서 "허용 가능"은 꽤 빠름을 의미합니다.)

여러 접근을 조합해 두 요구를 모두 만족할 방법을 찾았다고 믿습니다.

대체 API

문서에서 이 API가 더 높은 성능 잠재력을 여는 이유를 설명합니다.

Raw transfer

Oxc 같은 도구는 JS/TS 파일을 "AST"(추상 구문 트리)로 표현합니다. AST는 원본 소스보다 훨씬 큽니다.

Rust 같은 네이티브와 JS 사이 빠른 상호운용의 가장 큰 장벽은 이 큰 구조를 "두 세계" 사이로 옮길 때의 직렬화·역직렬화입니다.

AST를 JS로 넘기는 가장 단순한 방법은 JSON을 직렬화해 문자열로 보내고 JSON.parse로 복원하는 것이지만 매우 느립니다. 변환 비용이 네이티브 사용 이득을 상쇄하기도 합니다. 다른 포맷이 JSON보다 낫지만 여전히 부담이 큽니다.

직렬화를 없애 Rust 메모리 레이아웃 자체를 포맷으로 쓰는 "raw transfer" 방식을 개발했습니다(동작 상세는 여기).

"Raw transfer"가 현재 JS 플러그인 구현의 기반입니다.

Lazy deserialization

성능의 두 번째 적, 특히 워커 스레드로 JS를 여러 코어에서 돌릴 때는 가비지 컬렉터입니다. 만든 객체마다 메모리를 회수해야 합니다. V8처럼 엔진은 최적화되어 있어도 GC는 비싸고 실제 작업 CPU를 빼앗습니다.

AST를 게으르게 역직렬화해 실제로 필요한 부분만 만드는 비지터를 프로토타입했습니다.

예를 들어 규칙이 클래스 선언과 관련되면, 비지터는 대부분의 AST를 가볍게 지나가고 ClassDeclaration 노트만 JS 객체로 만들어 규칙에 넘깁니다. 나머지(AST의 변수 선언, if, 함수 등)에는 노드 객체를 만들 필요가 없습니다.

효과는 두 가지입니다.

  1. Raw transfer로 직렬화 비용을 없애고, 게으름으로 역직렬화 비용도 크게 줄입니다.
  2. GC 부담이 크게 줄어듭니다.

Deno도 비슷한 접근을 취했고, Marvin Hagemeister의 글에 잘 설명되어 있으며 Deno lint 구현도 매우 효율적입니다.

다만 lazy deserialization과 "raw transfer"를 함께 쓸 때 정말 좋은 성능이 난다는 것을 확인했습니다. 두 오버헤드를 없애면 JS 플러그인이 훨씬 빠르게 돕니다.

이 최적화는 아직 현재 JS 플러그인 버전에 포함되지 않았습니다. 이후 버전에서 구현할 예정입니다.

Try it out!

JS 플러그인을 써 보시고 경험을 알려 주세요. 좋은 피드백도 나쁜 피드백도 감사합니다.

특히 플러그인에 필요한 API가 Oxlint에 없다면 알려 주세요. 몇 달 안에 API 공백을 메우며 수요가 큰 것부터 우선하겠습니다.

즐거운 린팅 되세요!


수정: 2025년 10월 18일

10월 9일에 공개한 이 글 초판의 벤치마크는 Oxlint JS 플러그인 성능이 실제보다 훨씬 좋아 보이게 나왔습니다. overrides가 있는 설정에서 많은 파일에서 JS 플러그인이 건너뛰어지는 Oxlint 버그 때문이었고, 인용한 벤치에서 JS 플러그인 비용을 크게 과대평가했습니다.

실수에 사과드리며, 오류를 알려 주신 Herrington Darkholme께 감사드립니다.