블로그

프론트엔드 도구 개발 입문: OXC Parser로 코드 구조 이해하기

AST(추상 구문 트리)를 처음부터 배우고, OXC Parser로 JavaScript 코드를 파싱하여 첫 번째 코드 분석 도구를 만들어 보세요.

LibDoc Team 2026년 3월 6일 OXC 시리즈 59 분 읽기
#oxc #parser #ast #코드 파싱 #rust

프론트엔드 도구 개발 입문: 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를 실행할 때:

  1. 파싱 단계: 코드 문자열을 AST로 변환
  2. 컴파일 단계: AST를 바이트코드로 컴파일
  3. 실행 단계: 바이트코드 실행

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줄15ms1ms15배
1000줄89ms4ms22배
10000줄890ms45ms20배

실제 응용 시나리오

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로 컴파일하는 방법을 알아봅시다!


💡 관련 읽기: