Oxlint JS 플러그인 미리보기
올해 초 우리는 커뮤니티의 의견을 요청했습니다. (커뮤니티 피드백 링크)
이를 바탕으로 오목한(또는 오픈엑스포트) 기능에 맞춰 커스텀 자바스크립트 플러그인 지원 설계를 도출했습니다. 오늘 우리는 수개월간의 연구, 프로토타이핑, 그리고 마침내 구현한 결과를 발표하게 되어 기쁩니다:
오목은 자바스크립트로 작성된 플러그인을 지원합니다!
주요 기능
- ESLint 호환 플러그인 API. 오목은 수정 없이 많은 기존 ESLint 플러그인을 실행할 수 있습니다.
- 조금 다른 대안형 API. 더 뛰어난 성능을 제공합니다.
이 미리보기 버전이 무엇인지, 무엇이 아닌지
이 미리보기 출시는 단지 시작일 뿐입니다. 중요한 점은 다음과 같습니다:
- 이번 초기 릴리스는 모든 ESLint 플러그인 API를 구현하지는 않았습니다.
- 성능은 좋지만, 앞으로 훨씬 더 개선될 예정입니다 — 여러 가지 최적화 작업이 준비되어 있습니다.
코드 검사 규칙에서 가장 자주 사용되는 일부 기능 구현됨은 이미 작동하며, 많은 기존 ESLint 규칙들이 동작할 것입니다. 하지만 토큰과 관련된 기능은 부재하므로, 스타일링(서식) 관련 규칙은 작동하지 않습니다.
우리는 사용자 분들께 이 기능을 체험해 보시고 피드백을 주시기를 초대하며, 다음 단계 개발 방향에 대한 우선순위를 공유해주셨으면 합니다.
이 블로그 포스트는 아래 내용을 다룹니다
- 어떻게 사용하는지.
- 앞으로 어떤 것이 기대되는지.
- 오목이 "케이크를 먹고도 유지한다"는 접근 방식을 가능하게 하는 기술적 세부 사항 — 즉, ESLint 호환성과 뛰어난 성능을 동시에 제공하는 방법.
빠른 시작
프로젝트에 오목을 설치하세요:
pnpm add -D oxlint커스텀 자바스크립트 플러그인을 작성하세요:
// plugin.js
// 가장 간단한 규칙 — 디버거 금지
const rule = {
create(context) {
return {
DebuggerStatement(node) {
context.report({
message: "디버거 사용 금지!",
node,
});
},
};
},
};
const plugin = {
meta: {
name: "best-plugin-ever",
},
rules: {
"no-debugger": rule,
},
};
export default plugin;플러그인을 활성화하는 설정 파일 생성:
// .oxlintrc.json
{
"jsPlugins": ["./plugin.js"],
"rules": {
"best-plugin-ever/no-debugger": "error"
}
}검사 대상 파일 추가:
// foo.js
debugger;오목 실행:
pnpm oxlint다음과 같은 결과를 기대할 수 있습니다:
x best-plugin-ever(no-debugger): 디버거 사용 금지!
,-[foo.js:1:1]
1 | debugger;
: ^^^^^^^^^
`----플러그인 작성에 대한 보다 자세한 내용은 문서를 참조하세요.
대안형 API
오목은 약간 다르지만 더 뛰어난 성능을 제공하는 대안형 API도 제공합니다.
이 대안형 API는 오목뿐만 아니라, ESLint와도 호환되는 플러그인을 만듭니다.
클래스 선언이 5개 이상 포함된 파일을 감지하는 예제 규칙:
ESLint 버전
const rule = {
create(context) {
let classCount = 0;
return {
ClassDeclaration(node) {
classCount++;
if (classCount === 6) {
context.report({ message: "클래스가 너무 많습니다", node });
}
},
};
},
};대안형 API 버전
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 });
}
},
};
},
});차이점
- 규칙 객체를
defineRule(...)로 래핑하세요.
- const rule = {
+ const rule = defineRule({create대신createOnce를 사용하세요.
- create(context) {
+ createOnce(context) {create본문 내에서 각 파일별 설정 작업을before후크로 이동하세요.
- let classCount = 0;
+ let classCount;
return {
+ before() {
+ classCount = 0; // 카운터 리셋
+ },
ClassDeclaration(node) {
classCount++;
if (classCount === 6) {
context.report({ message: "클래스가 너무 많습니다", node });
}
},
};
},
});이것이 유일한 주요 차이점입니다. create는 (ESLint의 메서드처럼) 각 파일마다 반복적으로 호출되지만, createOnce는 오직 한 번만 호출됩니다.
기타 모든 API는 ESLint와 완전히 동일하게 동작합니다.
이 대안형 API가 성능 향상 잠재력을 지닌 이유는 문서에서 설명하고 있습니다.
성능
앞서 언급했듯이, 이번 오목 자바스크립트 플러그인 초기 미리보기 릴리스에서는 성능이 주요 목표가 아니었습니다. 우리의 주요 목적은 실제 프로젝트에서 유용하게 사용할 수 있도록 충분한 API를 확충하고, 초기 사용자들의 피드백을 수집하는 것이었습니다.
현재 성능은 나쁘지 않지만, 어느 정도 우수하다고 말하기 어렵습니다.
하지만 — 우리가 강조하고 싶은 점은 — 다음 버전 프로토타입은 우리가 채택한 아키텍처 설계가 다양한 최적화를 적용하면 초과 성능을 달성할 수 있음을 보여줍니다 (자세한 내용은 내부 동작 참조).
앞 몇 달 동안 이러한 최적화를 적용할 예정이며, 현재 버전보다 수배 이상의 속도 향상을 경험하실 수 있을 것입니다.
그럼에도 불구하고, 이 최적화 없이도 오목의 성능은 여전히 경쟁력이 있습니다.
중간 크기의 타입스크립트 프로젝트 vuejs/core에 대해 오목과 ESLint 비교:
| 라이너 | 시간 |
|---|---|
| ESLint | 4,116 ms |
| ESLint 멀티스레딩 | 3,710 ms |
| 오목 | 48 ms |
| 오목 + 커스텀 자바스크립트 플러그인 | 236 ms |
상세 정보
INFO
- 벤치마킹 리포지토리: https://github.com/overlookmotel/vue-core-cam/tree/bench-js-plugins
- 테스트 장비: 맥북 에어 M3, 24GB RAM
- 벤치마킹 명령어:
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'참고: 작성 시점(1.23.0)의 온라인 패키지 버전에는 이 벤치마킹에 영향을 미치는 버그가 있으며, 자바스크립트 플러그인의 비용을 크게 과소평가합니다. 위 결과는 해당 버그가 수정된 최신 main 브랜치(이 커밋)를 사용하여 얻었습니다. 추가로 아래 내용도 확인해 주세요.
이 예제에서 오목에 간단한 자바스크립트 플러그인을 추가하면 상당한 비용이 발생하지만, 전체적으로 오목은 새 멀티스레딩 버전의 ESLint보다 15배 빠릅니다.
물론 더 복잡한 자바스크립트 플러그인이나 수많은 플러그인은 성능 비용이 더 클 수 있습니다.
기능
오목은 대부분의 기존 규칙 및 플러그인이 추상구문 트리(결합도 검사)에 의존하는 경우에 사용되는, 대부분의 ESLint API를 지원합니다. 이는 대부분의 “코드 수정” 형 규칙을 포함합니다.
하지만 아직 토큰 기반의 API를 지원하지 않아, 스타일링(서식) 관련 규칙은 작동하지 않습니다.
지원됨
- AST 탐색
- AST 조사 (
node.parent,context.sourceCode.getAncestors) - 수정 기능
- 선택자 (ESLint 문서)
SourceCodeAPI (예:context.sourceCode.getText(node))
아직 지원되지 않음
- 언어 서버(편집기) 지원
- 규칙 옵션
- 제안 사항
범위 분석(구현 완료 v1.25.0부터)- 토큰 및 주석과 관련된
SourceCodeAPI (예:context.sourceCode.getTokens(node)) - 제어 흐름 분석
앞으로의 계획
앞 몇 달 동안 우리는 다음과 같은 일을 수행할 예정입니다:
1. 플러그인 API 표면 완전화
목표는 100%의 ESLint 플러그인 API 표면을 지원하여, 오목이 결국 어떤 종류의 기존 ESLint 플러그인도 수정 없이 실행할 수 있게 되는 것입니다.
2. 성능 향상
성능은 이미 괜찮지만, 프로토타이핑 과정에서 더 많은 최적화로 인한 의미 있는 성능 향상 가능성을 입증했습니다. 이를 적용하여 오목의 자바스크립트 플러그인이 가능한 한 루스트 수준의 속도로 작동하도록 하겠습니다.
내부 동작
이 글의 나머지 부분은 오목의 자바스크립트 플러그인 사용에 필수적인 것은 아닙니다. 하지만 여러분이 우리의 구현 방식에 대해 기술적으로 관심이 있다면 계속 읽어보세요...
핵심 질문: ESLint 호환성은 유지해야 할까?
올해 초 우리가 커뮤니티에 제기했던 질문은, 오목이 ESLint 호환 플러그인 API를 목표로 삼아야 할지 여부였습니다.
물론, ESLint 호환 인터페이스는 친숙함과 이전의 이전 과정이 쉬워지는 점에서 이상적인 선택입니다.
하지만 오목은 뛰어난 성능으로 잘 알려져 있으며, 이를 너무 많이 희생하는 것은 바람직하지 않습니다.
최근 몇 달간의 프로토타이핑 작업의 주된 목적은, 성능과 ESLint 호환성 사이의 상호 교환 비용(trade-off)을 정량화하고, 두 가지를 모두 만족하는 “케이크를 먹고도 유지한다”는 해결책이 존재하는지 탐색하는 것이었습니다. 여기서 “접근 가능한 성능”이란 매우 빠른 속도를 의미합니다.
우리는 다양한 접근 방식의 조합을 통해 두 요구 사항을 동시에 충족할 수 있는 방법을 찾았다고 믿습니다.
대안형 API
왜 이 API가 더 높은 성능을 가능하게 하는지에 대한 설명은 문서에서 확인하세요.
원시 전송 (Raw transfer)
Oxc과 같은 도구는 자바스크립트/타입스크립트 파일의 코드를 “AST”(추상 구문 트리) 형태로 표현합니다.
이러한 AST는 그 자체로 매우 큽니다 — 원본 소스 코드보다 훨씬 큽니다.
일반적으로 자바스크립트와 루스트와 같은 네이티브 언어 간의 효율적인 상호 운용성을 방해하는 가장 큰 장벽은, 이러한 거대한 데이터 구조를 “두 세계” 간에 전달할 때 필요한 직렬화 및 역직렬화 과정입니다.
자바스크립트와 루스트 간에 AST를 이동시키는 가장 간단하고 일반적인 방법은:
AST를 JSON으로 직렬화 → 문자열로 전송 → JSON.parse로 재생성하는 것입니다. 그러나 이 방식은 매우 느립니다. 종종 이런 변환 비용이 처음부터 네이티브 코드를 사용하는 성능 이득을 압도합니다.
다른 직렬화 형식은 JSON보다 더 효율적이지만, 그래도 상당한 오버헤드가 존재합니다.
우리는 원시 전송(raw transfer)이라는 기법을 개발했습니다. 이는 루스트의 네이티브 메모리 구조를 직렬화 형식으로 사용함으로써, 직렬화 과정 자체를 완전히 생략하는 방식입니다 (자세한 설명은 여기 참조).
“원시 전송”은 현재 자바스크립트 플러그인 구현의 기초입니다.
게으른 역직렬화 (Lazy deserialization)
특히 워커 스레드를 통해 여러 CPU 코어에서 자바스크립트를 실행할 때, 좋은 성능을 저해하는 두 번째 주요 원인은 가비지 컬렉터(Garbage Collector)입니다. 생성한 모든 객체는 메모리를 회수하기 위해 파괴되어야 합니다. 자바스크립트에서는 가비지 컬렉터가 이 작업을 담당합니다.
자바스크립트 엔진(예: V8)은 매우 최적화되어 있지만, 여전히 가비지 컬렉션은 비용이 큰 작업이며, 실제 작업에 필요한 프로세서 리소스를 "절취"합니다.
우리는 게으른 역직렬화(lazy deserialization) 방식의 AST 방문자를 프로토타이핑했습니다. 이는 실제로 필요한 부분만 역직렬화하며, 필요 없는 부분은 처리하지 않습니다.
예를 들어, 당신의 린트 규칙이 클래스 선언과 관련된다면, 이 방문자는 대부분의 AST를 거의 무관하게 통과하며, 실제 필요한 ClassDeclaration 노드에 대해서만 자바스크립트 객체를 생성해서 규칙 코드로 전달합니다. 나머지 부분(변수 선언, if 문, 함수 등)에 대해서는 노드 객체를 전혀 생성할 필요가 없습니다.
이 방식은 두 가지 효과를 낳습니다:
- 원시 전송은 직렬화 비용을 0으로 줄이고, 게으름은 역직렬화 측면에서도 극적으로 감소시킵니다.
- 가비지 컬렉터에 대한 압박이 크게 줄어듭니다.
덴오도 비슷한 접근을 취했으며, 마르빈 하게마이스터의 블로그 포스트(속도 향상 글 시리즈 11편)에서 아주 명확하게 설명하고 있습니다. 덴오 린트는 탁월하게 효율적인 구현을 갖고 있습니다.
하지만 우리는 게으른 역직렬화와 원시 전송의 조합이 정말 뛰어난 성능을 제공한다는 것을 발견했습니다. 우리 실험 결과, 이 두 가지 오버헤드를 제거하면 자바스크립트 플러그인의 실행 속도가 훨씬 빨라짐을 확인했습니다.
이 최적화는 현재 버전의 자바스크립트 플러그인에 포함되어 있지 않습니다. 미래 버전에 구현될 예정입니다.
사용해 보세요!
자바스크립트 플러그인을 사용해 보시고 경험을 공유해 주세요. 긍정적이든 부정적이든 모든 피드백을 진심으로 환영합니다.
특히, 오목이 당신의 플러그인 작동을 위해 필요한 일부 기능이 부족하다고 느끼신다면 알려주세요. 우리는 향후 몇 달 동안 이 결함을 채워나갈 것이며, 수요가 가장 높은 항목을 우선순위로 삼을 것입니다.
좋은 린팅 되세요!
수정: 2025년 10월 18일
10월 9일에 처음 공개된 이 블로그 포스트에는, 오목 자바스크립트 플러그인의 성능이 현실보다 훨씬 좋게 나타나도록 보이는 벤치마크 결과가 포함되어 있었습니다. 이는 오목 내부 버그 때문이었으며, 특정 설정 오버라이드를 포함한 구성에서 수많은 파일에서 자바스크립트 플러그인이 건너뛰는 문제가 발생했습니다. 이로 인해 저희가 인용한 벤치마크에서 자바스크립트 플러그인의 성능이 과도하게 과대평가되었습니다.
이 잘못된 점에 대해 진심으로 사과드리며, 허링턴 다크홀름 님이 해당 오류를 지적해 주셔서 감사드립니다.


