프론트엔드 모듈 해석 입문: import 뒤의 마법 이해하기
모듈 경로 해석 메커니즘을 깊이 이해하고, OXC Resolver로 경로 별칭과 TypeScript 경로 매핑을 처리하며, 직접 모듈 해석 도구를 구축해 보세요.
프론트엔드 모듈 해석 입문: import 뒤의 마법 이해하기
import { Button } from '@/components/Button'를 작성할 때, 브라우저나 번들 도구가 @/가 어디를 가리키는지 어떻게 아는지 궁금하신 적이 있나요?
이것이 바로 **모듈 해석(Module Resolution)**의 역할입니다. 오늘은 고성능 모듈 경로 해석 도구인 OXC Resolver를 배워보겠습니다.
모듈 해석이란 무엇인가요?
간단한 질문
코드에서 다음과 같이 작성할 때:
import { something } from './utils/helper';
JavaScript 런타임이나 번들 도구는 몇 가지 질문에 답해야 합니다:
./utils/helper가 파일인가요, 디렉토리인가요?- 디렉토리라면, 진입 파일이
index.js인가요,index.ts인가요? - 파일 확장자가
.js,.ts,.mjs중 어느 것인가요? - 이 경로에 해당하는 실제 파일은 어디에 있나요?
모듈 해석기의 역할은 이 질문들에 답하는 것입니다.
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.json의paths설정 지원 - 다중 확장자 지원:
.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-resolve | 450ms |
| OXC Resolver | 85ms |
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 챕터**를 방문하세요.
다음 글에서는 모든 모듈을 조합하여 완전한 초고속 프론트엔드 워크플로우를 구축해 보겠습니다!
💡 관련 읽기: