Ray Book
모듈 시스템

모듈이 필요한 이유

전역 스코프 오염과 이름 충돌 문제를 해결하기 위해 모듈 시스템이 어떻게 발전해왔는지 살펴봅니다

javascriptmoduleiifescope

스크립트 태그의 시대

초기 웹 개발에서는 JavaScript 파일을 <script> 태그로 HTML에 직접 삽입했습니다.

<script src="utils.js"></script>
<script src="validation.js"></script>
<script src="app.js"></script>

이 방식의 핵심 문제는 모든 변수가 전역 스코프를 공유한다는 것입니다.

전역 스코프 오염

파일이 분리되어 있어도, 실행 환경은 하나입니다.

// utils.js
var name = "유틸리티";
function format(value) {
  return value.toFixed(2);
}

// validation.js
var name = "검증기"; // utils.js의 name을 덮어씀!
function format(value) {
  return value.trim(); // utils.js의 format도 덮어씀!
}

// app.js
console.log(name);      // "검증기", 의도한 값인가?
console.log(format(42)); // 에러!, 숫자에 trim()은 없음

파일 수가 늘어날수록 이름 충돌의 위험은 기하급수적으로 증가합니다. 팀 규모가 커지면 누가 어떤 변수를 선언했는지 추적하기 어려워집니다.

의존성 순서 문제

스크립트 태그의 순서가 곧 로딩 순서입니다. 의존성을 개발자가 수동으로 관리해야 합니다.

<!-- jquery를 먼저 로드해야 플러그인이 동작 -->
<script src="jquery.js"></script>
<script src="jquery.plugin.js"></script>

<!-- 순서가 바뀌면? -->
<script src="jquery.plugin.js"></script> <!-- 에러! jQuery is not defined -->
<script src="jquery.js"></script>

프로젝트가 커질수록 수십 개의 스크립트 순서를 올바르게 유지하는 것은 점점 어려워집니다.

해결 시도 1: 네임스페이스 패턴

전역 변수 하나에 모든 것을 담아 충돌을 줄이는 방법입니다.

// 전역 객체 하나로 묶기
var MyApp = MyApp || {};

MyApp.utils = {
  format: function(value) {
    return value.toFixed(2);
  }
};

MyApp.validation = {
  format: function(value) {
    return value.trim();
  }
};

// 사용
MyApp.utils.format(42);      // "42.00"
MyApp.validation.format(" hi "); // "hi"

충돌은 줄었지만, 여전히 MyApp 자체는 전역이고, 내부 프로퍼티에 누구든 접근하고 수정할 수 있습니다. 캡슐화가 없습니다.

해결 시도 2: IIFE

Immediately Invoked Function Expression (즉시 실행 함수 표현식) 은 함수 스코프를 활용해 변수를 격리합니다.

var MyModule = (function() {
  // 이 안의 변수는 외부에서 접근 불가
  var privateCount = 0;

  function privateHelper() {
    return privateCount++;
  }

  // 공개할 것만 반환
  return {
    increment: function() {
      return privateHelper();
    },
    getCount: function() {
      return privateCount;
    }
  };
})();

MyModule.increment();
console.log(MyModule.getCount()); // 1
console.log(MyModule.privateCount); // undefined, 접근 불가

드디어 캡슐화가 가능해졌습니다. 하지만 모듈 간의 의존성 선언이 명시적이지 않고, 여전히 전역에 MyModule이 노출됩니다.

해결 시도 3: 노출 모듈 패턴

IIFE를 발전시킨 형태로, 공개 인터페이스를 명확하게 정의합니다.

var Calculator = (function() {
  var history = [];

  function add(a, b) {
    var result = a + b;
    history.push(result);
    return result;
  }

  function getHistory() {
    return [...history]; // 복사본 반환
  }

  // 공개 API 명시
  return {
    add: add,
    getHistory: getHistory
  };
})();

가독성은 좋아졌지만, 근본적인 한계는 동일합니다.

  1. 의존성 관리가 수동, 다른 모듈이 필요하면 전역에서 가져와야 합니다
  2. 비동기 로딩 불가, 모든 스크립트가 순서대로 로드되어야 합니다
  3. 정적 분석 불가, 어떤 모듈이 어떤 모듈에 의존하는지 도구가 파악할 수 없습니다

진짜 모듈 시스템이 필요하다

패턴으로 해결할 수 있는 한계에 도달했습니다. 필요한 것은:

요구사항패턴의 한계
스코프 격리IIFE로 가능, 하지만 번거로움
명시적 의존성불가능, 전역에서 암묵적으로 참조
비동기 로딩불가능, 스크립트 태그 순서에 의존
정적 분석불가능, 런타임에서만 확인 가능
캡슐화부분적, 여전히 전역 노출

이 문제를 언어나 런타임 수준에서 해결하기 위해 CommonJSESM (ES Modules) 이 등장했습니다.

다음 단계

다음 글에서는 Node.js의 CommonJS와 브라우저 표준인 ESM을 비교하며, 각각의 동작 방식과 차이점을 살펴보겠습니다.