10장. 모듈
모듈화 프로그래밍의 목표
-> 큰 프로그램을 코드 모듈로 분리해서 모듈 개발자가 예측하지 못한 상황에서도 코드 전체가 정확히 실행되도록 하는 것.
모듈화
-> 프로그램의 세부 사항을 캡슐화하고 전역 네임 스페이스를 깔끔하게 유지해서 모듈이 다른 모듈의 변수, 함수, 클래스를 수정하는 사고를 막는 것.
10.1 클래스, 객체, 클로저를 사용하는 모듈
클래스 메서드가 다른 클래스 메서드와 독립적인 이유는 각 클래스 메서드가 독립적인 프로토타입 객체의 프로퍼티로 정의됐기 때문이다.
객체가 모듈의 성격을 가지므로 클래스 역시 모듈의 성격을 띤다.
자바스크립트 객체에 프로퍼티를 추가해도 전역 네임스페이스나 다른 객체의 프로퍼티에 영향을 끼치지 않는다.
함수에서 선언한 로컬 변수와 중첩된 함수는 그 안에서만 사용할 수 있는 비공개 값이다.
즉, 즉시 호출하는 함수 표현식을 사용해 세부 사항과 유틸리티 함수를 내부에 숨기고 모듈의 공개 API만 반환해서 일종의 모듈성을 얻을 수 있다.
모듈성을 확보하는 것은, 모듈에 아이템이 하나 이상 있을 때 더 흥미롭다.
예를 들어 다음 코드는 mean()과 stddev() 함수를 내보내고 세부 사항은 숨기는 통계 모듈이다.
const stats = (function() {
// 모듈에서만 사용하는 비공개 유틸리티 함수
const sum = (x, y) => x + y;
const square = x => x * x;
// 내보낼 공개 함수
function mean(data) {
return data.reduce(sum) / data.length;
}
// 내보낼 공개 함수
function stddev(data) {
let m = mean(data);
return Math.sqrt(
data.map(x => x - m).map(square).reduce(sum) / (data.length - 1)
);
}
// 공개 함수를 객체 프로퍼티로 내보낸다.
return { mean, stddev };
}());
// 모듈은 다음과 같이 사용한다.
stats.mean([1, 3, 5, 7, 9]) // 5
stats.stddev([1, 3, 5, 7, 9]) // Math.sqrt(10)
클로저를 사용하는 자동 모듈화
파일의 시작과 끝에 텍스트를 조금 삽입해서 자바스크립트 코드를 모듈로 바꿀 수 있다.
내보낼 값과 그렇지 않은 값을 구분할 수 있도록 코드에서 규칙을 조금만 지키면 된다.
파일 세트를 받아서 즉시 호출하는 함수 표현식으로
각 파일 콘텐츠를 래퍼로 감싸고 이들을 큰 파일 하나에 병합하며, 각 함수의 반환값을 추적하는 도구를 만들어보자.
const modules = {};
function require(moduleName) { return modules[moduleName]; }
modules["stats.js"] = (function() {
const exports = {};
//stats.js 파일 내용이 여기에 들어간다.
const sum = (x, y) => x + y;
const square = x => x * x;
exports.mean = function(data) { ... };
exports.stddev = function(data) { ... };
return exports;
}());
앞의 예제처럼 모듈을 파일 하나로 합치면 다음과 같이 사용할 수 있다.
// 필요한 모듈 또는 그 콘텐츠 참조를 가져온다.
const stats = require("stats.js");
// 모듈을 사용하는 코드를 작성한다.
let average = stats.mean([...s]); // 평균은 20
10.2 노드 모듈
노드에서 각 파일은 비공개 네임스페이스를 가진 독립적 모듈이다. 파일에서 정의한 상수, 변수, 함수, 클래스는 모두 파일에서 내보내지 않는 한 비공개이다. 모듈에서 명시적으로 내보내야만 다른 모듈에서 그 값을 가져올 수 있다.
노드 모듈은 require() 함수를 통해 다른 모듈을 가져오고, Exports 객체의 프로퍼티를 수정하거나 module.exports 객체 자체를 바꾸는 방법으로 공개 API를 내보낸다.
노드 내보내기
노드의 전역 객체 exports는 항상 정의되어 있다. 여러 가지 값을 내보내는 노드 모듈을 만들 때 다음과 같이 이 객체의 프로퍼티로 할당하면 된다.
const sum = (x, y) => x + y;
const square = x => x * x;
exports.mean = data => data.reduce(sum) / data.length;
exports.stddev = function(d) {
let m = exports.mean(d);
return Math.sqrt(d.map(x => x - m).map(square).reduce(sum) / (d.length - 1));
};
module.exports = { mean, stddev }
노드 가져오기
노드 모듈은 require() 함수를 호출해서 다른 모듈을 가져온다.
이 함수의 인자는 가져올 모듈 이름이며 반환값은 모듈이 내보내는 값(함수, 클래스, 객체 등)이다.
노드에 내장된 시스템 모듈이나 패키지 매니저로 설치한 모듈을 가져올 때는 /를 쓰지 않고 다음과 같이 모듈 이름만 쓴다.
/를 쓰면 파일 시스템 경로로 바뀐다.
// 노드에 내장된 모듈
const fs = require("fs"); // 내장된 파일 시스템 모듈
const http = require("http"); // 내장된 HTTP 모듈
// 따로 설치한 서드파티 모듈 익스프레스
const express = require("express");
// 직접 만든 모듈. 모듈 이름에 현재 모듈에 대한 상대적 경로를 써야함.
const stats = require('./stats.js');
모듈에서 여러 프로퍼티가 있는 객체를 내보낸다면 객체 전체를 가져올 수도 있고 분해 할당을 통해 원하는 프로퍼티만 가져올 수도 있다.
// 함수를 포함해 stats 객체 전체를 가져온다.
const stats = require('./stats.js');
// 필요없는 함수도 포함되었지만 이들은 모두 "stats" 네임스페이스로 정리되어 있다.
let average = stats.mean(data);
// 분해 할당을 통해 원하는 함수만 로컬 네임스페이스에 가져올 수도 있다.
const { stddev } = reqires('./stats.js');
// stddev() 함수의 네임스페이스가 사라졌다.
let sd = stddev(data);
10.3 ES6 모듈
ES6의 모듈성은 노드의 모듈성과 같은 개념.
각 파일이 하나의 모듈이며 파일에서 정의한 상수, 변수, 함수, 클래스는 명시적으로 내보내지 않는 한 해당 모듈에서만 사용된다.
모듈에서 값을 내보내면 다른 모듈에서 명시적으로 가져와 사용할 수 있다.
먼저, ES6 모듈에는 일반적인 자바스크립트의 '스크립트'와 중요한 차이가 있다는 점을 알아야 한다.
가장 큰 차이는 모듈성 자체이다. 일반적인 스크립트에서는 최상위에서 선언한 변수, 함수, 클래스가 모두 전역 컨텍스트에 들어간다. 모듈에서는 각 파일에 비공개 컨텍스트가 있으며 import와 export 문을 사용할 수 있다.
모듈의 코드에서는 with 문이나 arguments 객체, 선언되지 않은 변수를 사용할 수 없다. ES6 모듈은 스트릭트 모드보다 좀 더 엄격하다. 모듈에서는 최상위 코드에서도 this가 undefined이다.
ES6 내보내기
export const PI = Math.PI;
export function degreesToRadians(d) { return d * PI / 180; }
export class Circle {
constructor(r) { this.r = r; }
area() { return PI * this.r * this.r; }
}
다음과 같이 하나로 묶을 수도 있다.
export { Circle, degreesToRadians, PI };
함수나 클래스 하나만 내보내는 모듈을 만드는 경우가 많은데, 이럴 때는 보통 export 대신 export default를 사용한다.
export default class BitSet {
// 클래스 바디
}
export를 사용하는 일반 내보내기는 이름이 있는 선언에서만 사용할 수 있다.
export default를 사용하는 디폴트 내보내기는 익명 함수 표현식과 익명 클래스 표현식을 포함해 어떤 표현식이든 내보낼 수 있다.
export default를 사용하면 객체 리터럴도 내보낼 수 있다. export default 뒤에 중괄호가 있다면 실제로 객체 리터럴을 내보내는 것이다.
디폴트 내보내기는 여러 개를 쓸 수 없다.
export 키워드는 자바스크립트 코드의 최상위 레벨에만 존재할 수 있다.
모듈은 항상 같은 값을 내보내며 내보낸 심벌은 모듈을 실제로 실행하기 전에 평가할 수 있다.
ES6 가져오기
import BitSet from './bitset.js'
가져온 값이 할당된 식별자는 const 키워드를 사용한 것처럼 상수로 선언된다.
가져오기 역시 모듈의 최상위 레벨에만 존재할 수 있다.
함수 선언과 마찬가지로 가져오기는 모듈 맨 위로 끌어올려진다.
값을 가져올 모듈 이름은 따옴표 안에 불변하는 문자열 리터럴로 표기한다. 값이 문자열인 변수나 표현식을 사용할 수 없고, 백틱 역시 사용할 수 없다. (템플릿 리터럴은 변수를 받으므로 일정하지 않기 때문)
현재 모듈과 같은 디렉터리에서 모듈을 가져올 때는 모듈 이름 앞에 ./를 붙여야 한다.
여러 값을 내보내는 모듈에서 가져올 때는 조금 다른 문법을 사용한다.
import { mean, stddev } from "./stats.js";
디폴트 내보내기를 사용할 때는 이름이 없어도 된다고 설명했다. 대신 값을 가져오는 모듈에서 이름을 지정한다.
하지만 default를 사용하지 않는 내보내기에서는 내보내는 값에 이름이 있고, 가져오는 모듈에서는 그 이름으로 값을 참조한다.
위의 중괄호는 분해 할당과 비슷하게 동작한다.
여러 가지를 내보내는 모듈에서 모든 것을 가져올 때는 다음과 같이 기재할 수 있다.
import * as stats from "./stats.js";
이 import 문은 객체를 생성하고 stats라는 상수에 그 객체를 할당한다. default를 사용하지 않는 내보내기 값은 stats 객체의 프로퍼티가 된다. 이 프로퍼티는 상수와 마찬가지로 삭제하거나 변경할 수 없다.
위 예제처럼 와일드카드를 써서 가져오면 stats.mean() 과 stats.stddev() 처럼 stats 객체를 통해 호출한다.
export와 export default 를 둘 다 사용하는 모듈에서 가져올 때는 다음과 같이 기재한다.
import Histogram, { mean, stddev } from "./histogram-stats.js"
아무 것도 가져오지 않을 때도, import를 사용할 수 있다.
import "./analytics.js"
예를 들어 웹 앱 분석 모듈에서 이벤트 핸들러를 등록하고 이벤트 핸들러에서 필요한 데이터를 필요한 시간에 서버에 전송한다고 하자. 이 모듈은 독립적이며 아무 것도 내보낼 필요가 없지만, 프로그램의 일부로 실행되려면 import 문으로 불러와야 한다.
이름을 바꾸는 가져오기와 내보내기
두 모듈에서 서로 다른 값을 같은 이름으로 내보냈을 때
import { render as renderImage } from "./imageutils.js";
import { render as renderUI } from "./ui.js";
디폴트 내보내기로 내보냈을 경우에는 이름 설정을 자유롭게 할 수 있다.'
웹의 자바스크립트 모듈
웹 브라우저에서 import 지시자를 네이티브로 사용할 때는 반드시 <script type="module"> 태그를 써서 모듈 코드임을 알려야 한다.
ES6 모듈은 각 모듈이 가져오기의 정적 세트를 갖는다는 특징이 있다.
웹 브라우저는 하나의 모듈에서 시작해 그 모듈이 가져오는 모듈을 모두 불러오고, 그렇게 불러온 모듈이 가져오는 모듈을 불러오는 식으로 프로그램 전체를 불러온다. import 문의 모듈 지정자는 상대적 URL로 취급된다.
<script type="module"> 태그는 모듈 프로그램의 출발점이다.
<script type="module">import "./main.js";</script>
<script type="module"> 태그 안에 있는 것은 인라인 ES6 모듈이며 export 문도 이와 같이 사용할 수 있다. 하지만 별 의미는 없다.
HTML <script> 태그는 인라인 모듈에 이름을 정의할 방법이 없으므로 이런 식으로 값을 내보내더라도 다른 모듈에서 가져올 방법이 없다.
import.meta.url
ES6 모듈에서 import.meta는 현재 실행 중인 모듈에 관한 메타데이터를 담은 객체를 참조한다.
이 객체의 url 프로퍼티는 모듈을 불러온 URL이다. 노드에서는 file://URL 이다.
import.meta.url 의 대표적인 사용처는 모듈과 같은 디렉터리, 또는 그 디렉터리에 상대적인 경로를 통해 이미지, 데이터 파일, 기타 자원을 참조하는 것이다. URL() 생성자를 사용하면 상대 URL을 import.meta.url 같은 절대 URL을 기준으로 쉽게 해석할 수 있다.
예를 들어 지역에 맞게 변환해야 할 문자열이 포함된 모듈이 있고 그 지역화 파일은 모듈과 같은 l10n/ 디렉터리에 저장됐다고 하자.
다음과 같은 함수를 만들어 문자열을 가져올 URL을 얻을 수 있다.
function localStringsURL(locale) {
return new URL(`l10n/${locale}.json`, import.meta,url);
}