자바스크립트 기초 #7 모듈 — import와 export

기초 시리즈 마지막 편입니다. 지금까지 다룬 도구들로 코드를 짜다 보면 한 파일이 빠르게 길어져요. 이번 글에서는 여러 파일로 나누는 도구 — ES Modules를 정리합니다.

모듈이 왜 필요한가? #

작은 스크립트는 한 파일이면 충분하지만, 실제 프로젝트는:

  • 함수가 100개씩 모여 있는 한 파일은 읽기 어렵다
  • 같은 변수 이름이 다른 곳에서 쓰여 충돌
  • 어떤 함수가 어디서 정의됐는지 추적 어려움

이걸 해결하려고 자바스크립트는 파일 단위로 코드를 분리하고, 필요한 것만 가져다 쓰는 시스템 — 모듈을 가지고 있습니다.

Named Export — 이름을 그대로 #

가장 단순한 형태부터.

src/math.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

export const PI = 3.14159;

export 키워드를 함수/변수 앞에 붙이면 그게 그대로 모듈 외부에 공개됩니다. 가져다 쓰는 쪽:

src/main.js
import { add, subtract, PI } from './math.js';

console.log(add(2, 3));        // 5
console.log(subtract(5, 2));   // 3
console.log(PI);                // 3.14159

import { ... }의 중괄호가 핵심입니다. 모듈이 공개한 이름들 중 필요한 것만 골라서 가져옵니다. 없는 이름을 가져오려고 하면 에러가 납니다.

한꺼번에 export #

선언하는 위치마다 export를 붙이는 대신, 끝에 한 번에 묶어서 적을 수도 있습니다.

끝에 모아서
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

export { add, subtract };

기능은 같습니다. 팀 컨벤션에 따라 둘 중 한 쪽으로 통일해 쓰면 됩니다.

이름 바꾸기 — as #

이름 바꾸기
// export 시
export { add as plus, subtract as minus };

// import 시
import { add as sum } from './math.js';

라이브러리 내부 이름과 외부 노출 이름을 다르게 할 때, 또는 다른 모듈에서 같은 이름이 와서 충돌할 때 씁니다.

Default Export — 모듈의 대표 #

모듈 하나에 하나만 둘 수 있는 export로, “이 모듈의 대표 값"에 해당합니다.

src/Logger.js
export default class Logger {
  log(message) {
    console.log(`[LOG] ${message}`);
  }
}
가져오기
import Logger from './Logger.js';
// 중괄호 없음, 이름은 자유

const logger = new Logger();
logger.log('hello');

import 이름의 이름은 호출하는 쪽이 자유롭게 정합니다. 같은 default를 어떤 파일에서는 Logger로, 다른 파일에서는 MyLogger로 받아도 OK.

default는 정말 필요한가? #

리액트 같은 라이브러리가 import React from 'react'처럼 default를 쓰니까 필수처럼 보이지만, default보다 named가 더 좋다는 게 요즘 컨벤션입니다. 이유는:

  1. 자동완성이 더 정확 — 모듈에 어떤 이름이 있는지 IDE가 더 잘 알려줌
  2. 리팩터링 안전 — 이름을 바꾸면 다른 파일에도 자동 반영
  3. 호출하는 쪽이 이름을 마음대로 정하면 코드 일관성 깨짐

대부분의 코드는 named export로 충분합니다. default는 컴포넌트(리액트) 또는 라이브러리의 단일 인스턴스(createLogger() 결과 등)에서만 쓰는 게 좋습니다.

Re-export — 한 곳에 모으기 #

여러 파일을 한 진입점에서 모아 내보낼 때.

src/utils/index.js
export { add, subtract } from './math.js';
export { capitalize, slugify } from './string.js';
export { default as Logger } from './Logger.js';
가져오는 쪽
import { add, capitalize, Logger } from './utils';
// './utils/index.js' 가 자동으로 잡힘

폴더 안의 여러 파일을 하나의 모듈처럼 노출하는 패턴입니다. 라이브러리들이 흔히 채택하는 구조 — npm에 올라간 패키지의 진입점 파일이 정확히 이 모양입니다.

Side-Effect Import — 실행만 #

값을 가져오지 않고 모듈을 실행만 시키고 싶을 때.

실행만
import './styles.css';        // CSS 적용
import './polyfills.js';      // 전역 환경 셋업

CSS, 폴리필, 또는 모듈이 자기 안에서 자동으로 뭔가 실행하는 (예: 글로벌 객체에 등록) 경우에 적합합니다. 실무에서는 CSS가 가장 흔합니다.

Dynamic Import — 런타임에 부르기 #

import 키워드를 함수처럼 쓰면 비동기로 모듈을 가져올 수 있습니다.

dynamic import
async function loadHeavy() {
  const module = await import('./heavy.js');
  module.run();
}

button.addEventListener('click', loadHeavy);

반환은 Promise. 모듈이 가지는 export 들이 모두 객체 속성으로 들어 있습니다.

이 패턴은 사용자가 그 기능을 쓸 때만 코드를 다운로드 하는 코드 스플리팅의 기반입니다. Next.js의 dynamic(), React의 lazy()가 모두 이걸 감싼 것입니다.

* import — 전체를 namespace로 #

전체 import
import * as math from './math.js';

math.add(2, 3);     // 5
math.PI;            // 3.14159

모듈의 모든 export를 math 객체의 속성으로 받아옵니다. 어떤 게 있을지 정확히 모를 때, 또는 namespace처럼 묶어 쓰고 싶을 때 적합합니다. 일반적인 경우에는 { add, PI }처럼 콕 집어 가져오는 게 더 깔끔합니다.

브라우저에서 모듈 쓰기 #

HTML에서 모듈 스크립트를 로드하려면 type="module" 어트리뷰트가 필수.

index.html
<script type="module" src="./main.js"></script>

type="module"이 있으면:

  • import/export가 동작
  • 자동으로 defer처럼 동작 (DOM 다 만든 뒤 실행)
  • 변수가 글로벌이 아닌 모듈 스코프로 격리

이 어트리뷰트 없이 <script src="...">로 두면 옛 모드라 import가 동작하지 않습니다. Vite 같은 도구는 알아서 처리해 줍니다.

.js 확장자 — 써야 하나요? #

이 부분은 자바스크립트 생태계에서 매우 헷갈리는 지점입니다.

  • 표준 ES Modules (브라우저, Node): 확장자 명시 필수import './math.js'
  • 번들러 통과 코드 (Vite, webpack, Next.js): 보통 확장자 생략 가능import './math'

이 시리즈에서는 모던 흐름을 따라 확장자를 명시 합니다. Node와 브라우저 표준에서 모두 동작하기 때문입니다. 다만 리액트/Next.js 같은 환경의 코드는 보통 확장자를 생략한 걸 자주 보게 됩니다.

CommonJS — Node의 옛 모듈 시스템 #

Node가 ESM이 표준화되기 전에 자체적으로 만들었던 모듈 시스템.

CommonJS — 옛 Node 스타일
// 내보내기
function add(a, b) {
  return a + b;
}
module.exports = { add };

// 가져오기
const { add } = require('./math');

require / module.exports 모양이 보이면 CommonJS 입니다. 옛 패키지나 옛 자료에서 자주 만나요. 새 코드는 거의 ESM(import/export) 으로 쓰지만, 호환성 문제로 두 시스템이 섞여 동작하는 경우가 아직 많습니다.

대부분의 모던 도구(Vite, Next.js, Bun)는 두 시스템을 자동으로 처리해 주니, 입문 단계에서는 너무 깊이 신경 쓰지 않아도 됩니다.

마무리 #

이번 글에서 정리한 내용:

  • ESM(ES Modules) 가 자바스크립트 표준 모듈 시스템
  • exportimport { ... } — named export가 기본
  • export default는 모듈 하나에 하나, 호출하는 쪽이 이름 자유
  • named export가 default보다 자동완성/리팩터에 유리
  • Re-export로 폴더의 여러 파일을 한 진입점에 모음
  • Dynamic import로 비동기 로드 (코드 스플리팅 기반)
  • <script type="module">이 브라우저에서 ESM 활성화
  • CommonJS(require/module.exports) 는 Node의 옛 시스템

기초 시리즈를 마치며 #

7편을 거치며 우리가 다룬 내용:

  1. 시작과 셋업 — 환경 만들기 (#1)
  2. 변수와 타입let/const, 8 타입, 원시 vs 참조 (#2)
  3. 제어 흐름 — if/for/switch, for...of (#3)
  4. 함수 — 선언/표현식/화살표, 매개변수, 호이스팅 (#4)
  5. 객체와 배열 — map/filter/reduce, spread, 디스트럭처링 (#5)
  6. 문자열과 템플릿 리터럴 — 메서드, 정규식 기초 (#6)
  7. 모듈import/export (이번 글)

여기까지 익히면 자바스크립트로 작은 도구나 스크립트를 자신 있게 짤 수 있습니다. 다음 중급 시리즈에서는 클래스, 비동기(Promise/async/await), 이터레이터, 옵셔널 체이닝, fetch — 모던 자바스크립트의 진짜 표현력을 끌어올리는 도구들을 다룹니다.

X