블로그

프론트엔드 모듈 해석 입문: import 뒤의 마법 이해하기

모듈 경로 해석 메커니즘을 깊이 이해하고, OXC Resolver로 경로 별칭과 TypeScript 경로 매핑을 처리하며, 직접 모듈 해석 도구를 구축해 보세요.

LibDoc Team 2026년 3월 12일 OXC 시리즈 68 분 읽기
#oxc #resolver #모듈 해석 #path alias #rust

프론트엔드 모듈 해석 입문: import 뒤의 마법 이해하기

import { Button } from '@/components/Button'를 작성할 때, 브라우저나 번들 도구가 @/가 어디를 가리키는지 어떻게 아는지 궁금하신 적이 있나요?

이것이 바로 **모듈 해석(Module Resolution)**의 역할입니다. 오늘은 고성능 모듈 경로 해석 도구인 OXC Resolver를 배워보겠습니다.

모듈 해석이란 무엇인가요?

간단한 질문

코드에서 다음과 같이 작성할 때:

import { something } from './utils/helper';

JavaScript 런타임이나 번들 도구는 몇 가지 질문에 답해야 합니다:

  1. ./utils/helper가 파일인가요, 디렉토리인가요?
  2. 디렉토리라면, 진입 파일이 index.js인가요, index.ts인가요?
  3. 파일 확장자가 .js, .ts, .mjs 중 어느 것인가요?
  4. 이 경로에 해당하는 실제 파일은 어디에 있나요?

모듈 해석기의 역할은 이 질문들에 답하는 것입니다.

Node.js 해석 규칙

Node.js에는 표준 모듈 해석 규칙이 있습니다:

1. 상대 경로

import './utils';      // 현재 디렉토리의 utils
import '../lib/helper'; // 상위 디렉토리의 lib/helper

2. 절대 경로

import '/home/user/project/utils';  // 루트 디렉토리에서 시작

3. 모듈 경로 (node_modules)

import 'lodash';       // node_modules/lodash 찾기
import 'react/dom';    // node_modules/react/dom 찾기

경로 별칭

현대 프론트엔드 프로젝트에서는 경로 별칭을 사용하여 import를 단순화합니다:

// 별칭 없이
import { Button } from '../../../components/Button';
import { formatDate } from '../../../utils/date';

// 별칭 사용
import { Button } from '@/components/Button';
import { formatDate } from '@utils/date';

경로 별칭은 코드를 더 깔끔하게 만들지만, 해석기의 지원이 필요합니다.

왜 해석 속도가 중요한가요?

대형 프로젝트에서 모듈 해석은 빈번한 작업입니다:

  • 개발 서버 시작: 모든 진입 파일의 의존성 해석
  • 핫 업데이트: 변경된 파일의 의존성 재해석
  • 타입 검사: TypeScript가 모든 타입 참조를 해석해야 함

프로젝트에 수천 개의 모듈이 있다면, 해석기의 성능이 개발 경험에 큰 영향을 미칩니다.

Resolver의 역할

OXC Resolver는 다음 기능을 제공합니다:

  • 빠른 해석: Node.js 기본 해석보다 몇 배 빠름
  • 경로 별칭 지원: @/, ~ 등 별칭 처리
  • TypeScript 경로 매핑: tsconfig.jsonpaths 설정 지원
  • 다중 확장자 지원: .js, .ts, .jsx 등 자동 시도

빠른 시작

설치

npm install @oxc-resolver

기본 사용

import { Resolver } from "@oxc-resolver";

// 해석기 인스턴스 생성
const resolver = new Resolver({
  // 프로젝트 루트 디렉토리
  projectRoot: process.cwd(),
});

// 동기 해석
const result = resolver.sync("./src", "./utils/helper");

console.log(result);
// {
//   path: "/home/user/project/src/utils/helper.js",
//   error: null
// }

// 비동기 해석
const asyncResult = await resolver.async("./src", "./utils/helper");

해석 결과

// 성공적으로 해석
{
  path: "/absolute/path/to/resolved/file.js",
  error: null
}

// 해석 실패
{
  path: null,
  error: "Cannot find module './nonexistent'"
}

실전: 모듈 경로 해석하기

실제 예시로 Resolver 사용법을 배워봅시다.

예시 프로젝트 구조

my-project/
├── src/
│   ├── components/
│   │   ├── Button.tsx
│   │   └── index.ts
│   ├── utils/
│   │   ├── date.ts
│   │   └── string.ts
│   └── index.ts
├── tsconfig.json
└── package.json

기본 해석

import { Resolver } from "@oxc-resolver";

const resolver = new Resolver({
  projectRoot: "/path/to/my-project",
});

// 상대 경로 해석
resolver.sync("/path/to/my-project/src", "./components/Button");
// → /path/to/my-project/src/components/Button.tsx

// 디렉토리 해석 (자동으로 index 찾기)
resolver.sync("/path/to/my-project/src", "./components");
// → /path/to/my-project/src/components/index.ts

경로 별칭 설정

const resolver = new Resolver({
  projectRoot: "/path/to/my-project",

  // 별칭 설정
  alias: {
    "@": "./src",
    "@components": "./src/components",
    "@utils": "./src/utils",
  },
});

// 별칭으로 해석
resolver.sync("/path/to/my-project", "@/components/Button");
// → /path/to/my-project/src/components/Button.tsx

resolver.sync("/path/to/my-project", "@utils/date");
// → /path/to/my-project/src/utils/date.ts

TypeScript 경로 매핑 지원

tsconfig.json 설정을 가정합니다:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"]
    }
  }
}

Resolver가 자동으로 읽습니다:

const resolver = new Resolver({
  projectRoot: "/path/to/my-project",
  tsconfig: "./tsconfig.json",  // tsconfig 경로 지정
});

// tsconfig의 paths 설정 자동 적용
resolver.sync("/path/to/my-project", "@components/Button");
// → /path/to/my-project/src/components/Button.tsx

설정 옵션 상세

기본 설정

const resolver = new Resolver({
  // 프로젝트 루트 디렉토리
  projectRoot: process.cwd(),

  // 모듈 디렉토리 (기본 node_modules)
  modules: ["node_modules"],

  // 자동으로 시도할 확장자
  extensions: [".js", ".jsx", ".ts", ".tsx", ".json"],

  // 디렉토리 기본 파일
  mainFiles: ["index"],

  // 별칭 설정
  alias: {
    "@": "./src",
  },
});

별칭 설정 상세

const resolver = new Resolver({
  alias: {
    // 단순 매핑
    "@": "./src",

    // 정확한 매칭
    "lodash": "./vendor/lodash.js",

    // 접두사 매칭
    "react-native$": "./custom-react-native.js",

    // 배열 형태 (여러 후보)
    "@components": ["./src/components", "./src/shared/components"],
  },
});

조건부 내보내기 지원

Node.js의 exports 필드:

// package.json
{
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs",
      "default": "./dist/index.js"
    },
    "./utils": "./utils/index.js"
  }
}

Resolver 설정:

const resolver = new Resolver({
  // 해석 조건 지정
  conditionNames: ["import", "require", "default"],
});

모듈 해석 전략

const resolver = new Resolver({
  // Node.js 스타일 (기본)
  resolutionStrategy: "node",

  // Classic 스타일 (TypeScript 호환)
  resolutionStrategy: "classic",
});

실전 미니 프로젝트: import 분석 도구 구축

Resolver를 사용하여 실용적인 도구를 만들어 봅시다. 프로젝트의 import 의존성을 분석합니다.

분석 스크립트 생성

// analyze-imports.js
import { Resolver } from "@oxc-resolver";
import { parseSync } from "@oxc-parser";
import fs from "fs";
import path from "path";

// 설정
const config = {
  projectRoot: process.cwd(),
  entry: "./src/index.ts",
  alias: {
    "@": "./src",
  },
};

// 해석기 생성
const resolver = new Resolver({
  projectRoot: config.projectRoot,
  alias: config.alias,
  extensions: [".ts", ".tsx", ".js", ".jsx"],
});

// 이미 해석한 파일 저장 (순환 방지)
const visited = new Set();
const dependencies = new Map();

// 단일 파일 분석
function analyzeFile(filePath) {
  if (visited.has(filePath)) return;
  visited.add(filePath);

  console.log(`📄 Analyzing: ${path.relative(config.projectRoot, filePath)}`);

  // 파일 읽기 및 파싱
  const code = fs.readFileSync(filePath, "utf-8");
  const ast = parseSync(code, {
    lang: filePath.endsWith(".tsx") ? "tsx" : "ts",
  });

  const imports = [];

  // AST 순회하며 import 문 찾기
  function traverse(node) {
    if (!node || typeof node !== "object") return;

    if (node.type === "ImportDeclaration") {
      imports.push(node.source.value);
    }

    if (node.type === "ExportNamedDeclaration" && node.source) {
      imports.push(node.source.value);
    }

    if (node.type === "ExportAllDeclaration" && node.source) {
      imports.push(node.source.value);
    }

    // 동적 import 처리
    if (
      node.type === "CallExpression" &&
      node.callee.type === "Import"
    ) {
      if (node.arguments[0]?.type === "Literal") {
        imports.push(node.arguments[0].value);
      }
    }

    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);

  // 각 import 해석
  const resolvedImports = [];
  for (const importPath of imports) {
    const dir = path.dirname(filePath);
    const result = resolver.sync(dir, importPath);

    resolvedImports.push({
      raw: importPath,
      resolved: result.path,
      error: result.error,
    });

    // 로컬 파일이면 계속 분석
    if (
      result.path &&
      !result.path.includes("node_modules") &&
      !visited.has(result.path)
    ) {
      analyzeFile(result.path);
    }
  }

  dependencies.set(filePath, resolvedImports);
}

// 분석 시작
console.log("🚀 Starting import analysis...\n");

const entryPath = path.resolve(config.projectRoot, config.entry);
analyzeFile(entryPath);

// 결과 출력
console.log("\n📊 Analysis Results:\n");

console.log("Files analyzed:", visited.size);
console.log("\nDependency Graph:");

for (const [file, imports] of dependencies) {
  const relativeFile = path.relative(config.projectRoot, file);
  console.log(`\n  ${relativeFile}`);
  for (const imp of imports) {
    if (imp.error) {
      console.log(`    ❌ ${imp.raw} (${imp.error})`);
    } else if (imp.resolved?.includes("node_modules")) {
      console.log(`    📦 ${imp.raw} → node_modules`);
    } else {
      const resolvedRelative = path.relative(config.projectRoot, imp.resolved);
      console.log(`    ✅ ${imp.raw} → ${resolvedRelative}`);
    }
  }
}

// 통계
let totalImports = 0;
let externalImports = 0;
let localImports = 0;
let errors = 0;

for (const [, imports] of dependencies) {
  for (const imp of imports) {
    totalImports++;
    if (imp.error) errors++;
    else if (imp.resolved?.includes("node_modules")) externalImports++;
    else localImports++;
  }
}

console.log("\n📈 Statistics:");
console.log(`  Total imports: ${totalImports}`);
console.log(`  Local imports: ${localImports}`);
console.log(`  External imports (node_modules): ${externalImports}`);
console.log(`  Errors: ${errors}`);

분석 실행

node analyze-imports.js

출력 예시:

🚀 Starting import analysis...

📄 Analyzing: src/index.ts
📄 Analyzing: src/components/Button.tsx
📄 Analyzing: src/utils/date.ts
📄 Analyzing: src/utils/string.ts

📊 Analysis Results:

Files analyzed: 4

Dependency Graph:

  src/index.ts
    ✅ ./components/Button → src/components/Button.tsx
    ✅ @/utils/date → src/utils/date.ts
    📦 react → node_modules

  src/components/Button.tsx
    📦 react → node_modules
    ✅ @/utils/string → src/utils/string.ts

  src/utils/date.ts
    📦 dayjs → node_modules

  src/utils/string.ts
    📦 lodash → node_modules

📈 Statistics:
  Total imports: 7
  Local imports: 3
  External imports (node_modules): 4
  Errors: 0

응용 시나리오

1. 커스텀 빌드 도구

// 간단한 번들러 핵심 로직
async function bundle(entryFile) {
  const resolver = new Resolver({ projectRoot: process.cwd() });
  const modules = new Map();

  async function loadModule(filePath, importer) {
    if (modules.has(filePath)) return;

    const result = resolver.sync(path.dirname(importer), filePath);
    if (result.error) throw new Error(result.error);

    const code = fs.readFileSync(result.path, "utf-8");
    modules.set(result.path, code);

    // import를 파싱하고 재귀적으로 로드...
  }

  await loadModule(entryFile, process.cwd());
  return modules;
}

2. Monorepo 프로젝트

// monorepo의 패키지 참조 처리
const resolver = new Resolver({
  projectRoot: "/path/to/monorepo",
  alias: {
    "@myorg/core": "./packages/core/src",
    "@myorg/utils": "./packages/utils/src",
    "@myorg/ui": "./packages/ui/src",
  },
});

3. 순환 의존성 감지

function detectCircularDeps(entryPath) {
  const resolver = new Resolver({ projectRoot: process.cwd() });
  const visiting = new Set();
  const circular = [];

  function check(filePath, path) {
    if (visiting.has(filePath)) {
      circular.push([...path, filePath]);
      return;
    }
    visiting.add(filePath);

    // import를 해석하고 재귀적으로 검사...

    visiting.delete(filePath);
  }

  check(entryPath, []);
  return circular;
}

성능 우위

비교 테스트

// benchmark.js
import { Resolver as OxcResolver } from "@oxc-resolver";
import { createSyncFn } from "enhanced-resolve";

const code = fs.readFileSync("./large-project-imports.json", "utf-8");
const imports = JSON.parse(code); // 1000개의 import 경로

// OXC Resolver
const oxcResolver = new OxcResolver({ projectRoot: process.cwd() });
console.time("OXC Resolver");
for (const imp of imports) {
  oxcResolver.sync(process.cwd(), imp);
}
console.timeEnd("OXC Resolver");

// enhanced-resolve
const enhancedResolver = createSyncFn({
  /* config */
});
console.time("enhanced-resolve");
for (const imp of imports) {
  enhancedResolver(process.cwd(), imp);
}
console.timeEnd("enhanced-resolve");

테스트 결과

해석기1000회 해석 시간
enhanced-resolve450ms
OXC Resolver85ms

OXC Resolver가 약 5배 빠릅니다!

자주 묻는 질문

node_modules의 패키지를 해석하지 못해요?

설정이 올바른지 확인하세요:

const resolver = new Resolver({
  projectRoot: process.cwd(),
  modules: ["node_modules"],  // modules 설정 확인
});

TypeScript 경로 매핑이 작동하지 않아요?

tsconfig.json의 설정을 확인하세요:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

그리고 Resolver에서 지정하세요:

const resolver = new Resolver({
  projectRoot: process.cwd(),
  tsconfig: "./tsconfig.json",
});

다른 확장자 우선순위는요?

const resolver = new Resolver({
  extensions: [".ts", ".tsx", ".js", ".jsx"],  // 순서대로 시도
});

패키지의 브라우저/Node 버전은 어떻게 처리하나요?

const resolver = new Resolver({
  conditionNames: ["browser", "import", "require", "default"],
  // 브라우저 우선, 그 다음 ES Module, 마지막으로 CommonJS
});

요약

본문에서는 OXC Resolver의 핵심 사용법을 소개했습니다:

내용설명
모듈 해석import 경로를 실제 파일 경로로 변환
경로 별칭@/ 등 별칭으로 import 단순화 지원
TypeScript 통합tsconfig.json의 paths 설정 읽기
성능 우위전통적 해석기보다 5배 이상 빠름

Resolver의 핵심 가치: 모듈 해석을 빠르고 정확하게.

다음 단계

Resolver의 더 많은 API를 알고 싶으신가요? **OXC 한국어 문서 - Resolver 챕터**를 방문하세요.

다음 글에서는 모든 모듈을 조합하여 완전한 초고속 프론트엔드 워크플로우를 구축해 보겠습니다!


💡 관련 읽기: