프론트엔드 도구 개발 입문: OXC Parser로 코드 구조 이해하기
AST(추상 구문 트리)를 처음부터 배우고, OXC Parser로 JavaScript 코드를 파싱하여 첫 번째 코드 분석 도구를 만들어 보세요.
프론트엔드 도구 개발 입문: OXC Parser로 코드 구조 이해하기
ESLint가 어떻게 코드 문제를 발견하는지 궁금하신 적이 있나요? Babel은 어떻게 ES6를 ES5로 변환할까요? Prettier는 어떻게 코드를 다시 정렬할까요?
모든 답은 같은 핵심 기술인 **AST(추상 구문 트리)**로 연결됩니다. 그리고 Parser(파서)는 코드를 AST로 변환하는 도구입니다.
오늘은 OXC Parser로 이 마법의 베일을 벗겨보겠습니다.
AST란 무엇인가요?
쉬운 비유로 이해하기
보물지도가 있다고 상상해 보세요. “큰 나무에서 출발해서 북쪽으로 100걸음, 동쪽으로 50걸음 걸어서 파세요”라고 적혀 있습니다.
컴퓨터는 이 문장을 이해할 수 있는 구조로 바꿔야 합니다:
명령
├── 시작점: 큰 나무
├── 단계
│ ├── 단계1: 북쪽으로 100걸음
│ └── 단계2: 동쪽으로 50걸음
└── 동작: 파기
AST는 코드의 “트리 형태 지도”입니다. 코드 문자열을 트리로 변환하고, 각 노드는 코드의 한 요소를 나타냅니다.
간단한 예시
이 코드를 보세요:
const greeting = "Hello, World!";
AST 구조는 대략 다음과 같습니다:
Program (프로그램)
└── VariableDeclaration (변수 선언)
├── kind: "const"
└── declarations
└── VariableDeclarator
├── id: Identifier (식별자)
│ └── name: "greeting"
└── init: Literal (리터럴)
└── value: "Hello, World!"
왜 프론트엔드 도구는 AST가 필요한가요?
코드를 “이해”해야 하는 모든 도구는 AST가 필요합니다:
| 도구 | 용도 | AST의 역할 |
|---|---|---|
| ESLint | 코드 검사 | AST를 순회하며 문제 패턴 발견 |
| Babel | 코드 변환 | AST 노드를 수정하고 새 코드 생성 |
| Prettier | 코드 포맷팅 | AST를 기반으로 다시 정렬 |
| TypeScript | 타입 검사 | AST에서 타입 추론 수행 |
| Webpack | 코드 번들링 | AST의 의존성 관계 분석 |
Parser의 역할
Parser(파서)의 작업 흐름:
코드 문자열 → 어휘 분석 → 구문 분석 → AST
브라우저도 사용합니다
브라우저가 JavaScript를 실행할 때:
- 파싱 단계: 코드 문자열을 AST로 변환
- 컴파일 단계: AST를 바이트코드로 컴파일
- 실행 단계: 바이트코드 실행
Babel, ESLint도 사용합니다
- @babel/parser: Babel의 파서
- @typescript-eslint/parser: ESLint의 TypeScript 파서
- @oxc/parser: OXC의 고성능 파서
OXC Parser의 특징:
- 극한의 속도: @babel/parser보다 20-50배 빠름
- 완전한 지원: JavaScript, TypeScript, JSX
- 좋은 호환성: ESTree와 호환되는 출력 형식
실전: Parser로 코드 파싱하기
직접 해봅시다!
OXC Parser 설치
# 프로젝트 내 설치
npm install @oxc-parser
# 또는 pnpm 사용
pnpm add @oxc-parser
가장 간단한 예시
parse-demo.js 생성:
// parse-demo.js
import { parseSync } from "@oxc-parser";
const code = `const greeting = "Hello, OXC!";`;
const ast = parseSync(code, {
lang: "js",
});
console.log(JSON.stringify(ast, null, 2));
실행:
node parse-demo.js
출력:
{
"type": "Program",
"start": 0,
"end": 32,
"body": [
{
"type": "VariableDeclaration",
"kind": "const",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "greeting"
},
"init": {
"type": "Literal",
"value": "Hello, OXC!"
}
}
]
}
]
}
비동기 파싱
큰 파일의 경우 비동기 방법을 권장합니다:
import { parseAsync } from "@oxc-parser";
const code = `// 대량의 코드...`;
const ast = await parseAsync(code, {
lang: "js",
});
AST 출력 단계별로 이해하기
조금 더 복잡한 예시를 분석해 봅시다:
function add(a, b) {
return a + b;
}
파싱된 AST 구조:
import { parseSync } from "@oxc-parser";
const code = `
function add(a, b) {
return a + b;
}
`;
const ast = parseSync(code, { lang: "js" });
최상위 구조: Program
AST의 루트 노드는 Program입니다:
{
type: "Program",
body: [...], // 모든 최상위 문장 포함
sourceType: "script"
}
함수 선언: FunctionDeclaration
{
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "add" // 함수명
},
params: [ // 매개변수 목록
{ type: "Identifier", name: "a" },
{ type: "Identifier", name: "b" }
],
body: { // 함수 본문
type: "BlockStatement",
body: [...]
}
}
반환 문: ReturnStatement
{
type: "ReturnStatement",
argument: { // 반환값 표현식
type: "BinaryExpression",
operator: "+",
left: { type: "Identifier", name: "a" },
right: { type: "Identifier", name: "b" }
}
}
일반적인 노드 타입
| 노드 타입 | 설명 | 예시 |
|---|---|---|
Identifier | 식별자 (변수명, 함수명) | foo, bar |
Literal | 리터럴 | 123, "hello", true |
VariableDeclaration | 변수 선언 | const x = 1; |
FunctionDeclaration | 함수 선언 | function foo() {} |
CallExpression | 함수 호출 | foo() |
BinaryExpression | 이항 연산 | a + b |
MemberExpression | 멤버 접근 | obj.prop |
ArrowFunctionExpression | 화살표 함수 | (x) => x * 2 |
실용적인 미니 프로젝트: 코드의 함수 수 세기
Parser를 사용하여 실용적인 도구를 만들어 봅시다. 코드에 정의된 함수가 몇 개인지 세어보겠습니다.
통계 스크립트 생성
count-functions.js 생성:
// count-functions.js
import { parseSync } from "@oxc-parser";
import fs from "fs";
function countFunctions(code) {
const ast = parseSync(code, { lang: "js" });
let functionCount = 0;
let arrowFunctionCount = 0;
// AST 재귀 순회
function traverse(node) {
if (!node || typeof node !== "object") return;
// 노드 타입 확인
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression") {
functionCount++;
}
if (node.type === "ArrowFunctionExpression") {
arrowFunctionCount++;
}
// 모든 자식 노드 재귀 순회
for (const key of Object.keys(node)) {
const child = node[key];
if (Array.isArray(child)) {
child.forEach(traverse);
} else if (child && typeof child === "object") {
traverse(child);
}
}
}
traverse(ast);
return {
regularFunctions: functionCount,
arrowFunctions: arrowFunctionCount,
total: functionCount + arrowFunctionCount,
};
}
// 테스트
const testCode = `
function add(a, b) {
return a + b;
}
const multiply = function(a, b) {
return a * b;
};
const subtract = (a, b) => a - b;
const numbers = [1, 2, 3].map(n => n * 2);
`;
const result = countFunctions(testCode);
console.log("함수 통계 결과:", result);
실행 결과:
함수 통계 결과: {
regularFunctions: 2,
arrowFunctions: 2,
total: 4
}
더 실용적인 버전: 파일 분석
// analyze-file.js
import { parseSync } from "@oxc-parser";
import fs from "fs";
function analyzeFile(filePath) {
const code = fs.readFileSync(filePath, "utf-8");
const ast = parseSync(code, {
lang: filePath.endsWith(".ts") ? "ts" : "js",
});
const stats = {
functions: 0,
classes: 0,
imports: 0,
exports: 0,
variables: 0,
};
function traverse(node) {
if (!node || typeof node !== "object") return;
switch (node.type) {
case "FunctionDeclaration":
case "FunctionExpression":
case "ArrowFunctionExpression":
stats.functions++;
break;
case "ClassDeclaration":
stats.classes++;
break;
case "ImportDeclaration":
stats.imports++;
break;
case "ExportNamedDeclaration":
case "ExportDefaultDeclaration":
stats.exports++;
break;
case "VariableDeclaration":
stats.variables += node.declarations.length;
break;
}
for (const key of Object.keys(node)) {
const child = node[key];
if (Array.isArray(child)) {
child.forEach(traverse);
} else if (child && typeof child === "object") {
traverse(child);
}
}
}
traverse(ast);
return stats;
}
// 사용 예시
const stats = analyzeFile("./src/index.js");
console.log("파일 분석 결과:", stats);
TypeScript 파싱
OXC Parser는 TypeScript를 잘 지원합니다:
import { parseSync } from "@oxc-parser";
const tsCode = `
interface User {
name: string;
age: number;
}
function greet(user: User): string {
return \`Hello, \${user.name}!\`;
}
`;
const ast = parseSync(tsCode, {
lang: "ts",
// 타입 정보 유지
astType: "ts",
});
console.log(ast);
JSX / TSX 파싱
import { parseSync } from "@oxc-parser";
const jsxCode = `
function Button({ onClick, children }) {
return (
<button className="btn" onClick={onClick}>
{children}
</button>
);
}
`;
const ast = parseSync(jsxCode, {
lang: "jsx",
});
성능 우위
OXC Parser와 @babel/parser의 성능을 비교해 봅시다.
테스트 코드
// benchmark.js
import { parseSync as oxcParse } from "@oxc-parser";
import { parse as babelParse } from "@babel/parser";
// 대량의 테스트 코드 생성
function generateCode(lines) {
let code = "";
for (let i = 0; i < lines; i++) {
code += `const variable${i} = ${i};\n`;
code += `function function${i}() { return variable${i} * 2; }\n`;
}
return code;
}
const largeCode = generateCode(1000); // 2000줄 코드
// OXC Parser 테스트
console.time("OXC Parser");
for (let i = 0; i < 10; i++) {
oxcParse(largeCode, { lang: "js" });
}
console.timeEnd("OXC Parser");
// Babel Parser 테스트
console.time("Babel Parser");
for (let i = 0; i < 10; i++) {
babelParse(largeCode, {
sourceType: "module",
});
}
console.timeEnd("Babel Parser");
테스트 결과
OXC Parser: 45ms
Babel Parser: 890ms
OXC Parser가 Babel Parser보다 약 20배 빠릅니다!
성능 비교표
| 코드 규모 | @babel/parser | @oxc/parser | 향상 배수 |
|---|---|---|---|
| 100줄 | 15ms | 1ms | 15배 |
| 1000줄 | 89ms | 4ms | 22배 |
| 10000줄 | 890ms | 45ms | 20배 |
실제 응용 시나리오
1. 커스텀 ESLint 규칙
// console.log 사용 감지
function noConsoleLog(code) {
const ast = parseSync(code, { lang: "js" });
const violations = [];
function traverse(node) {
if (!node || typeof node !== "object") return;
if (
node.type === "CallExpression" &&
node.callee?.object?.name === "console" &&
node.callee?.property?.name === "log"
) {
violations.push({
line: node.loc?.start?.line,
message: "console.log 사용을 피하세요",
});
}
for (const key of Object.keys(node)) {
const child = node[key];
if (Array.isArray(child)) {
child.forEach(traverse);
} else if (child && typeof child === "object") {
traverse(child);
}
}
}
traverse(ast);
return violations;
}
2. 코드 통계 도구
// 코드 복잡도 계산
function calculateComplexity(code) {
const ast = parseSync(code, { lang: "js" });
let complexity = 1; // 기본 복잡도
function traverse(node) {
if (!node || typeof node !== "object") return;
// 복잡도를 증가시키는 구조
if (["IfStatement", "ForStatement", "WhileStatement", "SwitchCase", "CatchClause"].includes(node.type)) {
complexity++;
}
if (node.type === "ConditionalExpression") {
complexity++;
}
if (node.type === "LogicalExpression") {
complexity++;
}
for (const key of Object.keys(node)) {
const child = node[key];
if (Array.isArray(child)) {
child.forEach(traverse);
} else if (child && typeof child === "object") {
traverse(child);
}
}
}
traverse(ast);
return complexity;
}
3. 코드 생성 도구
// 모든 내보내기 함수명 추출
function extractExports(code) {
const ast = parseSync(code, { lang: "js" });
const exports = [];
for (const node of ast.body) {
if (node.type === "ExportNamedDeclaration") {
if (node.declaration?.type === "FunctionDeclaration") {
exports.push(node.declaration.id.name);
}
}
if (node.type === "ExportDefaultDeclaration") {
if (node.declaration.type === "Identifier") {
exports.push(`default: ${node.declaration.name}`);
}
}
}
return exports;
}
요약
본문에서는 OXC Parser의 핵심 개념과 사용법을 소개했습니다:
| 내용 | 설명 |
|---|---|
| AST | 코드의 트리 형태 표현, 프론트엔드 도구의 기초 |
| Parser | 코드 문자열을 AST로 변환 |
| 노드 타입 | Identifier, Literal, FunctionDeclaration 등 |
| AST 순회 | 모든 노드를 재귀적으로 순회하며 분석 |
Parser의 핵심 가치: 코드를 프로그램이 “이해”하고 “조작”할 수 있게 만듦.
다음 단계
Parser의 더 많은 API 옵션을 알고 싶으신가요? **OXC 한국어 문서 - Parser 챕터**를 방문하세요.
다음 글에서는 OXC Transformer를 배워보겠습니다. TypeScript를 JavaScript로 컴파일하는 방법을 알아봅시다!
💡 관련 읽기: