동적 import() — 지연 로딩과 코드 분할

ES2020 동적 import()로 필요할 때만 모듈을 로드하는 지연 로딩과 라우트 기반 코드 분할 구현 방법, 정적 import와의 차이를 정리합니다.

· 5 min read · PALDYN Team

지난 글에서 importexport의 모든 문법 형태를 살펴봤습니다. 이번에는 정적 import와 다른 동적 import() 를 다룹니다. 초기 번들 크기를 줄이고, 사용자가 특정 기능을 필요로 할 때만 코드를 불러오는 지연 로딩(lazy loading)코드 분할(code splitting) 의 핵심 도구입니다.

동적 import()란?

import(specifier)는 런타임에 모듈을 비동기로 로드하는 함수처럼 생긴 구문입니다. Promise<ModuleNamespace>를 반환합니다.

// 정적 import: 항상 파일 최상위에서, 항상 로드됨
import { heavyLib } from './heavy.mjs';

// 동적 import: 필요한 시점에, 조건부로 로드
const { heavyLib } = await import('./heavy.mjs');

정적 import는 번들에 항상 포함되지만, 동적 import()는 별도 청크(chunk)로 분리되어 필요할 때 네트워크 요청으로 가져옵니다.

정적 import vs 동적 import()

기본 문법

// await를 쓰는 방법 (async 함수 안 또는 TLA 환경)
const module = await import('./module.mjs');
console.log(module.default); // default export
console.log(module.add);     // named export

// 구조 분해
const { add, PI } = await import('./math.mjs');

// .then() 체이닝
import('./analytics.mjs')
  .then(({ track }) => track('page_view'))
  .catch(console.error);

반환값 구조

import()가 resolve되면 모듈 네임스페이스 객체를 받습니다. 이 객체는 default, 그리고 모든 named export를 프로퍼티로 가집니다.

const ns = await import('./math.mjs');
// ns.default — default export
// ns.add      — named export 'add'
// ns.PI       — named export 'PI'

주요 활용 패턴

동적 import() 활용 패턴

라우트 기반 코드 분할

SPA에서 각 페이지를 별도 청크로 분리해 초기 로딩 속도를 개선합니다.

const routes = {
  '/home':  () => import('./pages/Home.mjs'),
  '/about': () => import('./pages/About.mjs'),
  '/chart': () => import('./pages/Chart.mjs'),
};

async function navigate(path) {
  const { default: Page } = await routes[path]();
  Page.render();
}

조건부 폴리필

구형 브라우저에서만 폴리필을 불러옵니다.

if (!('structuredClone' in globalThis)) {
  await import('./polyfills/structured-clone.mjs');
}

사용자 액션 트리거

버튼 클릭, 스크롤 진입 시 무거운 라이브러리를 로드합니다.

document.getElementById('chart-btn').addEventListener('click', async () => {
  const { Chart } = await import('https://cdn.example.com/chart.mjs');
  new Chart(ctx, config);
});

import.meta.url과 함께 사용

모듈 내부에서 현재 파일의 URL 기준으로 상대 경로를 동적으로 구성할 수 있습니다.

const locale = navigator.language.slice(0, 2); // 'ko', 'en', ...
const { messages } = await import(
  new URL(`./i18n/${locale}.mjs`, import.meta.url)
);

번들러(Webpack, Vite)는 이런 동적 경로를 패턴으로 분석해 관련 파일을 자동으로 분리합니다.

번들러에서의 동작

Webpack이나 Vite는 import() 호출을 감지해 자동으로 별도 청크를 생성합니다.

// Vite/Rollup의 주석으로 청크 이름 지정
const module = await import(
  /* webpackChunkName: "analytics" */
  './analytics.mjs'
);

에러 처리

네트워크 오류나 모듈 오류는 Promise rejection으로 전파됩니다.

try {
  const { parse } = await import('./parser.mjs');
  parse(data);
} catch (err) {
  console.error('모듈 로드 실패:', err);
}

정리

항목정적 import동적 import()
위치최상위만어디서든
경로리터럴만표현식 가능
로드 시점파일 파싱 시호출 시
반환값없음(바인딩)Promise
번들항상 포함별도 청크
용도핵심 의존성지연/조건부

초기 페이지 로드 성능이 중요하다면 핵심 코드만 정적으로, 나머지는 동적 import()로 지연하는 전략이 효과적입니다.


지난 글: import/export 문법 총정리

다음 글: Top-Level Await — 모듈 최상위의 비동기 처리


읽어주셔서 감사합니다. 😊