TypeScript #6 모듈(Modules)
Prog. Langs & Tools/TypeScript

TypeScript #6 모듈(Modules)

오늘은 타입스크립트 모듈(modules)에 대해 공부했던 내용을 정리해 보려고 한다.

모듈

모듈은 독립 가능한 기능의 단위이다. 여러 모듈을 결합하면 하나의 프로그램을 만들 수 있는데, 모듈을 사용하면 다음과 같은 장점이 있다.

  1. 유지 보수의 용이성 : 중복 코드의 최소화
  2. 전역 스코프 오염을 방지 : 이름 공간이 파일 단위로 제한되어 전역 이름 공간을 침범하지 않음
  3. 재사용성 향상 : 모듈을 다른 프로젝트에 공유하여 재사용 가능

모듈러 프로그래밍(modular programming)은 프로그램의 설계 기술로 모듈의 분리와 손쉬운 교체를 중심으로 설계한다.  모듈러 프로그래밍은 다음 세 단계를 통해 진행된다.

  1. 모듈을 식별함 : 공통 기능이 무엇인지, 다만 설계와 분석만 가지고는 어려움
  2. 모듈을 분리해 선언함 : 함수 내부에서 발견되는 공통 기능은 모듈로 분리. 단위 테스트 쉬워지고 에러 발생 확률 줄어듦
  3. 모듈을 외부로 공개함

모듈은 크게 두 가지로 나누어 볼 수가 있다. 바로 내부 모듈(internal module)과 외부 모듈(external module)이다. 내부 모듈은 네임스페이스를 의미하고 외부 모듈은 export로 선언 되어 외부에 공개된 모듈이다.

내부 모듈

내부 모듈인 네임스페이스는 전역 이름 공간과 분리된 네임스페이스 단위의 이름공간이다. 따라서 같은 네임스페이스의 이름 공간이라면 파일 B가 파일 A에 선언된 모듈을 참조(reference)할 수 있는데 참조할 때는 별도의 참조문을 선언할 필요가 없다. 같은 네임스페이스 안에서는 이름을 중복해서 클래스, 함수, 변수 등을 선언할 수 없다. 하지만 다른 네임스페이스 간에는 이름이 같아도 충돌이 없다.

외부 모듈

export로 선언한 모듈을 외부 모듈이라고 한다. 일반적으로 말하는 모듈은 외부 모듈을 가리킨다. 외부 모듈의 이름 공간과 관련된 특징을 살펴보면 외부 모듈의 이름 공간은 파일 내부로 제한되는 특징이 있다. 예를 들어 export function hello() {}를 hello1.ts과 hello2.ts에서 동시에 선언하는 건 가능하다. 하지만 두 파일 안에서 function hello() {}를 선언하는 건 외부 모듈로 선언하는 것이 아니게 되고, 전역 스코프의 이름 공간을 공유해 이름 충돌이 발생하게 된다.


네임스페이스

네임스페이스는 하나의 독립된 이름 공간을 만들고 여러 파일에 걸쳐 하나의 이름 공간을 공유할 수 있다. 네임스페이스와 같은 역할을 하는 키워드로 module이 있는데, 역할과 기능상 차이가 없다. 컴파일 후에도 동일한 코드를 반환한다.

namespace Hello { ... }

// deprecated
module Hello { ... }

// Hello 내부 모듈
namespace Hello {
  function print() {
    console.log('Hello!');
  }
}

// ES6 컴파일 후 코드
var Hello;
(function (Hello) {
  function print() {
    console.log('Hello!');
  }
})(Hello || (Hello = {}));

내부 모듈은 JS ES6로 컴파일 될 때 즉시 실행 함수로 변환이 된다. 즉시 실행 함수 내부는 전역 스코프와 분리된 이름 공간이다. 즉시 실행 함수로 보낼 인수로 'Hello || (Hello = {})'을 설정했는데 모듈이 있다면 전달하고 없다면 Hello에 {}으로 초기화해 전달한다. 이 방법은 모듈 패턴에서 느슨한 확장(loose augmentation)으로 불린다. 이에 반대는 Hello만을 인수로 전달하는 경우로 단단한 확장(tight augmentation)이라고 부른다.

하나의 파일에 여러개의 네임스페이스를 선언할 수도 있으며, 네임스페이스마다 이름 공간이 구분되어 있으므로 서로의 이름 공간에 접근하려면 모듈을 export로 선언하여야 한다. 이 때 네임스페이스 간에 모듈을 서로 주고받을 수 있으며, 전역 이름 공간에서 네임스페이스 내부에 선언된 외부 모듈을 호출할 수도 있다. 

프로젝트 규모가 커지면 파일 단위로 모듈을 분할해야 한다. 이 때 네임스페이스를 이용하면 여러 파일에 걸쳐 하나의 네임스페이스의 이름 공간을 공유할 수 있다. 타입스크립트는 네임스페이스를 이용해 논리적 그룹화(logical grouping)를 제공하는데, 네임스페이스의 이름이 같다면 컴파일 시 논리적 영역으로 묶어 컴파일 할 수 있게 만든다. 아래의 코드에서 tsc 명령어를 이용해 프로젝트 단위로 컴파일을 하면 별다른 참조가 없어도 이상 없이 컴파일이 된다.

// car1.ts

namespace Car {
  export let auto:boolean = false;
  
  export interface ICar {
    name: string;
    vendor: string;
  }
}

// car2.ts

/// <reference path='car1.ts' />
namespace Car {
  let wheels: number;
  console.log(auto);
  
  class Taxi implements ICar {
    name: string;
    vendor: string;
  }
}  
console.log(Car.auto);

 그런데 프로젝트 단위 컴파일이 아닌, 특정 파일(car2.ts)만을 컴파일하면 문제가 발생한다. 따라서 car2.ts 파일 상단처럼 명시적으로 참조 경로(reference path)를 설정해 주어야 한다. 그러면 다음과 같이 $tsc car2.ts 만 컴파일 해 주어도 이상 없이 진행이 된다. 참조경로를 추가하면 컴파일러가 car2.ts 파일이 참조하는 외부 파일이 무엇인지를 인식할 수 있게 된다.


모듈 시스템

모듈 로더(module loader)는 모듈 파일에 선언된 모듈을 실행할 수 있다. 타입스크립트에서는 모듈을 정의하거나 호출할 때 ES6 모듈을 이용한다. 이 때 ES6가 표준이지만 더 많은 브라우저에서 지원하게 하고자 ES5 표준으로 컴파일하고 ES6 모듈은 모듈 로더를 통해 호출할 수 있다. 가장 많이 쓰고 대표적인 모듈 로더는 웹팩(Webpack)이다. 웹팩은 종속 모듈을 정적 출력으로 만들 수 있는 모듈 번들러로 지원하는 모듈 형식은 CommonJS, AMD이다. Webpack에 대해서는 추후에 별도로 포스팅을 하려고 한다.