모듈의 일반적 의미
모듈이란 애플리케이션을 구성하는 개별적 요소로서 재사용 가능한 코드 조각을 말한다. 일반적으로 모듈은 기능을 기준으로 파일 단위로 분리되며, 모듈이 성립하기 위해서는 자신만의 파일 스코프(모듈 스코프)를 가질 수 있어야 한다.
모듈의 특징은 다음과 같다.
- 모듈의 자산(변수, 함수, 객체 등)은 기본적으로 비공개 상태다. 즉, 자신만의 파일 스코프를 갖는 모듈의 모든 자산은 캡슐화되어 다른 모듈에서 접근할 수 없다.
- 모듈은 애플리케이션이나 다른 모듈에 의해 재사용되어야 의미가 있다. 따라서 모듈은 공개가 필요한 자산에 한정하여 명시적으로 선택적 공개가 가능하다. 이를 export라 한다.
- 공개(export)된 모듈의 자산은 다른 모듈에서 재사용할 수 있다. 공개된 모듈의 자산을 사용하는 모듈을 모듈 사용자(module consumer)라 한다. 모듈 사용자는 모듈이 공개한 자산 중 일부 또는 전체를 선택해 자신의 스코프 내로 불러들여 재사용할 수 있다. 이를 import라 한다.
이처럼 모듈은 애플리케이션과 분리되어 개별적으로 존재하다가 필요에 따라 다른 모듈에 의해 재사용된다. 모듈은 기능별로 분리되어 개별적인 파일로 작성되기 때문에 코드의 단위를 명확히 분리하여 애플리케이션을 구성할 수 있고, 재사용성이 좋아 개발 효율성과 유지보수성을 높일 수 있다.
자바스크립트와 모듈
자바스크립트는 웹페이지의 단순한 보조 기능을 처리하기 위한 제한적인 용도를 목적으로 태어났다. 이러한 태생적 한계로 인해 다른 프로그래밍 언어와 비교할 때 부족한 부분이 있는데, 대표적으로 자바스크립트는 모듈 시스템을 지원하지 않았다. 즉, 자바스크립트는 모듈이 성립하기 위해 필요한 파일 스코프와 import, export를 지원하지 않았다.
C 언어는 #include, 자바는 import 등 대부분의 프로그래밍 언어는 모듈 기능을 제공했지만, 클라이언트 사이드 자바스크립트는 script 태그를 사용하여 외부의 자바스크립트 파일을 로드할 수는 있어도 파일마다 독립적인 파일 스코프를 갖지 못했다. 다시 말해, 자바스크립트 파일을 여러 개의 파일로 분리하여 script 태그로 로드해도 분리된 자바스크립트 파일들은 결국 하나의 자바스크립트 파일 내에 있는 것처럼 동작했다.
이로 인한 문제점은 다음과 같았다.
- 모든 자바스크립트 파일이 하나의 전역을 공유한다.
- 분리된 자바스크립트 파일들의 전역 변수가 중복되는 등의 문제가 발생할 수 있다.
이러한 문제로 인하여 자바스크립트에서 모듈을 구현하는 것은 불가능했다.
자바스크립트를 클라이언트 사이드에 국한하지 않고 범용적으로 사용하려는 움직임이 생기면서 모듈 시스템은 반드시 해결해야 하는 핵심 과제가 되었고, 이런 상황에서 CommonJS와 AMD(Asynchronous Module Definition)가 제안되었다.
이로써 자바스크립트의 모듈 시스템은 크게 CommonJS와 AMD 진영으로 나뉘게 되었고, 브라우저 환경에서 모듈을 사용하기 위해서는 CommonJS 또는 AMD를 구현한 모듈 로더 라이브러리를 사용해야 하는 상황이 되었다.
자바스크립트의 런타임 환경인 Node.js는 모듈 시스템의 사실상 표준인 CommonJS를 채택했다. Node.js는 독자적인 진화를 거쳐 현재는 CommonJS 사양과 100% 동일하지는 않지만 기본적으로 CommonJS 사양을 따르고 있다. 즉, Node.js는 ECMAScript 표준 사양은 아니지만 모듈 시스템을 지원하며, 이로 인해 Node.js 환경에서는 파일별로 독립적인 파일 스코프를 갖게 되었다.
ES6 모듈(ESM)
이러한 상황에서 ES6는 클라이언트 사이드 자바스크립트에서도 동작하는 모듈 기능을 추가했다. IE를 제외한 대부분의 브라우저(Chrome 61, FF 60, SF 10.1, Edge 16 이상)에서 ES6 모듈을 사용할 수 있다.
ES6 모듈(앞으로 ESM이라 부른다)의 사용법은 간단하다. script 태그에 type="module" 어트리뷰트를 추가하면 로드된 자바스크립트 파일은 모듈로서 동작한다. 일반적인 자바스크립트 파일이 아닌 ESM임을 명확히 하기 위해 ESM의 파일 확장자는 mjs를 사용할 것을 권장한다.
<script type="module" src="app.mjs"></script>
ESM에는 클래스와 마찬가지로 기본적으로 strict mode가 적용된다.
모듈 스코프
ESM은 독자적인 모듈 스코프를 갖는다. ESM이 아닌 일반적인 자바스크립트 파일은 script 태그로 분리해서 로드해도 독자적인 모듈 스코프를 갖지 않는데, 이는 자바스크립트의 기본적인 스코프 동작방식 때문이다.
다음 예제를 통해 일반 자바스크립트 파일의 스코프 동작을 살펴보자.
// foo.js
// x 변수는 전역 변수다
var x = 'foo';
console.log(window.x); // foo
// bar.js
// x 변수는 전역 변수다. foo.js에서 선언한 전역 변수 x와 중복된 선언이다
var x = 'bar';
console.log(window.x); // bar
// HTML
<!DOCTYPE html>
<html>
<body>
<script src="foo.js"></script>
<script src="bar.js"></script>
</body>
</html>
위 예제의 HTML에서 script 태그로 분리해서 로드된 2개의 자바스크립트 파일은 하나의 자바스크립트 파일 내에 있는 것처럼 동작한다. 즉, 하나의 전역을 공유한다. 따라서 foo.js에서 선언한 x 변수와 bar.js에서 선언한 x 변수는 중복 선언되며 의도치 않게 x 변수의 값이 덮어써진다. 이러한 문제는 실제 애플리케이션 개발에서 심각한 문제를 야기할 수 있다.
// foo.mjs
// x 변수는 더 이상 전역 변수가 아니며 window 객체의 프로퍼티도 아니다
var x = 'foo';
console.log(x); // foo
console.log(window.x); // undefined
// bar.mjs
// x 변수는 전역 변수가 아니며 window 객체의 프로퍼티도 아니다
// foo.mjs에서 선언한 x 변수와는 스코프가 다른 독립적인 변수다
var x = 'bar';
console.log(x); // bar
console.log(window.x); // undefined
// HTML
<!DOCTYPE html>
<html>
<body>
<script type="module" src="foo.mjs"></script>
<script type="module" src="bar.mjs"></script>
</body>
</html>
반면 ESM은 파일 자체의 독자적인 모듈 스코프를 제공한다. 이는 각 모듈이 서로 완전히 독립된 파일 스코프를 가짐을 의미한다.
// foo.mjs
const x = 'foo';
console.log(x); // foo
// bar.mjs
// 다른 모듈에서 선언한 변수는 참조할 수 없다
console.log(x); // ReferenceError: x is not defined
모듈 내에서 선언한 식별자는 모듈 스코프가 다르기 때문에 모듈 외부에서 참조할 수 없다. 이는 각 모듈이 완전히 독립된 렉시컬 스코프를 가짐을 의미한다.
모듈 스코프는 전역 변수의 중복 선언이나 의도치 않은 값의 변경을 방지하고, 모듈별로 완전히 독립된 스코프를 제공함으로써 모듈간의 결합도를 낮추고 독립성을 보장한다.
export 키워드
모듈은 독자적인 모듈 스코프를 갖기 때문에 모듈 내부에서 선언한 모든 식별자는 기본적으로 해당 모듈 내부에서만 참조할 수 있다. 하지만 다른 모듈에서 재사용할 수 있게 하려면 export 키워드를 통해 선택적으로 공개해야 한다. export 키워드는 선언문 앞에 사용할 수 있으며, 변수, 함수, 클래스 등 모든 식별자를 export할 수 있다.
// lib.mjs
// 변수의 공개
export const pi = Math.PI;
// 함수의 공개
export function square(x) {
return x * x;
}
// 클래스의 공개
export class Person {
constructor(name) {
this.name = name;
}
sayHello() {
return `Hello, I'm ${this.name}!`;
}
}
선언문 앞에 매번 export 키워드를 붙이는 것이 번거롭다면 export할 대상을 하나의 객체로 구성하여 한 번에 export할 수도 있다. 이 방식은 모듈에서 공개할 대상이 많을 때 유용하다.
// lib.mjs
const pi = Math.PI;
function square(x) {
return x * x;
}
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
return `Hello, I'm ${this.name}!`;
}
}
// 변수, 함수, 클래스를 하나의 객체로 구성하여 공개
export { pi, square, Person };
export한 식별자는 외부에서 사용될 것을 전제로 하므로, 캡슐화가 필요한 대상은 export하지 않고 모듈 내부에서만 사용하도록 구현하는 것이 좋다. 이를 통해 모듈의 내부 구현을 캡슐화하고 필요한 API만을 외부에 노출할 수 있다.
import 키워드
다른 모듈에서 export한 식별자를 자신의 모듈 스코프 내부로 로드하려면 import 키워드를 사용한다. import 문은 모듈이 어디에 위치하는지 지정하는 모듈 지정자(module specifier)와 함께 사용한다.
// app.mjs
import { pi, square, Person } from './lib.mjs';
console.log(pi); // 3.141592653589793
console.log(square(10)); // 100
const person = new Person('Lee');
console.log(person.sayHello()); // Hello, I'm Lee!
ESM의 경우에는 파일 확장자를 반드시 지정해야 한다.
모듈이 export한 식별자 이름을 일일이 지정하지 않고 하나의 이름으로 한 번에 import할 수도 있다.
// app.mjs
import * as lib from './lib.mjs';
console.log(lib.pi); // 3.141592653589793
console.log(lib.square(10)); // 100
const person = new lib.Person('Lee');
console.log(person.sayHello()); // Hello, I'm Lee!
이때 import되는 식별자들은 위처럼 as 뒤에 지정한 이름의 객체에 프로퍼티로 할당된다.
// app.mjs
import {
pi as PI,
square as getSquare,
Person as Human
} from './lib.mjs';
console.log(PI); // 3.141592653589793
console.log(getSquare(10)); // 100
const person = new Human('Lee');
console.log(person.sayHello()); // Hello, I'm Lee!
또한, 모듈이 export한 식별자의 이름을 변경하여 import할 수도 있다. 이는 이름 충돌을 방지하거나 더 명확한 이름을 사용하고 싶을 때 유용하다.
// lib.mjs
export default function(x) {
return x * x;
}
// 또는 표현식으로도 가능하다
export default x => x * x;
모듈에서 하나의 값만 export할 때는 default 키워드를 사용할 수 있다. default export는 모듈당 하나만 있을 수 있으며, 기본적으로 이름 없이 하나의 값을 export한다.
// lib.mjs
// SyntaxError: Unexpected token 'const'
export default const foo = () => {};
// 대신 이렇게 해야 한다
const foo = () => {};
export default foo;
단, default 키워드를 사용할 때는 var, let, const 같은 변수 선언 키워드는 사용할 수 없다는 제약이 있다.
// app.mjs
import square from './lib.mjs';
// 임의의 다른 이름으로도 import 가능하다
import calculateSquare from './lib.mjs';
console.log(square(3)); // 9
console.log(calculateSquare(3)); // 9
default 키워드와 함께 export한 모듈은 {} 없이 임의의 이름으로 import할 수 있다. 이는 모듈의 주요 기능을 가져올 때 더 간단한 문법을 제공한다.
// lib.mjs
export const version = 'v1.0';
export default class Calculator {
add(a, b) { return a + b; }
subtract(a, b) { return a - b; }
}
// app.mjs
import Calculator, { version } from './lib.mjs';
console.log(version); // v1.0
const calc = new Calculator();
console.log(calc.add(1, 2)); // 3
한 모듈에서 default export와 named export를 함께 사용할 수도 있다.
이처럼 모듈 시스템을 통해 코드의 재사용성을 높이고, 코드의 관리와 유지보수를 용이하게 만들 수 있다. 또한 모듈별로 독립된 스코프를 가지므로 전역 변수의 남용을 막고 코드의 충돌을 방지할 수 있다.
요약
모듈의 기본 개념과 특징
모듈의 정의와 특성
- 모듈은 애플리케이션을 구성하는 개별적 요소로서 재사용 가능한 코드 조각이다.
- 일반적으로 모듈은 기능을 기준으로 파일 단위로 분리되며, 모듈이 성립하려면 자신만의 파일 스코프(모듈 스코프)를 가져야 한다.
- 모듈의 자산(변수, 함수, 객체 등)은 기본적으로 비공개 상태로, 캡슐화되어 다른 모듈에서 접근할 수 없다.
모듈의 사용과 이점
- 모듈은 필요한 자산만을 선택적으로 공개(export)하고, 다른 모듈에서는 이를 불러와(import) 재사용할 수 있다.
- 코드의 단위를 명확히 분리하여 애플리케이션을 구성할 수 있다.
- 재사용성이 높아 개발 효율성과 유지보수성이 향상된다.
자바스크립트와 모듈의 역사
초기 자바스크립트의 한계
- 자바스크립트는 원래 모듈 시스템을 지원하지 않았다.
- script 태그로 파일을 분리해도 모든 파일이 하나의 전역 스코프를 공유했다.
- 이로 인해 전역 변수 충돌 등의 문제가 발생했다.
모듈 시스템의 발전
1. CommonJS와 AMD의 등장
- 자바스크립트의 범용적 사용을 위해 모듈 시스템이 필요해짐
- CommonJS와 AMD가 대안으로 제시됨
- Node.js는 CommonJS를 채택하여 모듈 시스템 구현
2. Node.js의 역할
- CommonJS 사양을 기반으로 한 모듈 시스템 제공
- 파일별로 독립적인 모듈 스코프 지원
- ECMAScript 표준은 아니지만 실질적인 모듈 시스템 표준으로 자리잡음
ES6 모듈(ESM)
ESM의 도입
- ES6에서 클라이언트 사이드 자바스크립트를 위한 모듈 기능 추가
- IE를 제외한 대부분의 모던 브라우저에서 지원
- script 태그에 type="module" 어트리뷰트를 추가하여 사용
- 파일 확장자로 .mjs 사용 권장
모듈 스코프의 특징
1. 독립적인 스코프
- 각 모듈은 독자적인 모듈 스코프를 가짐
- 모듈 내 변수는 전역 변수가 아님
- window 객체의 프로퍼티가 아님
2. 스코프 격리
// foo.mjs
const x = 'foo';
console.log(x); // foo
// bar.mjs
console.log(x); // ReferenceError: x is not defined
export 키워드의 활용
1. 개별 export
export const pi = Math.PI;
export function square(x) { return x * x; }
export class Person {
constructor(name) { this.name = name; }
}
2. 일괄 export
const pi = Math.PI;
function square(x) { return x * x; }
class Person {
constructor(name) { this.name = name; }
}
export { pi, square, Person };
import 키워드의 활용
1. 기본 import
import { pi, square, Person } from './lib.mjs';
2. 전체 import
import * as lib from './lib.mjs';
3. 이름 변경 import
import { pi as PI, square as sq } from './lib.mjs';
모듈의 고급 기능
default export/import
// 내보내기
export default function(x) { return x * x; }
// 가져오기
import square from './lib.mjs';
모듈 결합 사용
// lib.mjs
export const version = 'v1.0';
export default class Calculator {
add(a, b) { return a + b; }
}
// app.mjs
import Calculator, { version } from './lib.mjs';
모듈 사용 시 주의사항
파일 확장자
- ESM 사용 시 파일 확장자(.mjs) 생략 불가
- 상대 경로 또는 절대 경로로 모듈 지정 필요
strict mode
- ESM은 자동으로 strict mode 적용
- 별도의 'use strict' 선언 불필요
모듈 스코프 격리
- 모듈 간 변수 충돌 방지
- 전역 스코프 오염 방지
- 캡슐화를 통한 정보 은닉
예상문제 [🔥]
https://github.com/junh0328/prepare_frontend_interview?tab=readme-ov-file
모듈이란 무엇인가요?
모듈이란 애플리케이션을 구성하는 개별적 요소로서 재사용 가능한 코드 조각을 말합니다. 모듈은 세 가지 특징을 가지고 있습니다.
첫째, 모듈은 파일 단위로 분리되어 자신만의 스코프(모듈 스코프)를 가집니다.
둘째, 모듈 내부에 선언된 모든 식별자는 기본적으로 비공개 상태로, 외부에서 접근할 수 없습니다. 이를 캡슐화라고 합니다.
셋째, 모듈은 필요한 자산(함수, 객체, 변수 등)을 export, import하여 다른 모듈과 재사용할 수 있습니다. 모듈은 코드를 기능별로 분리하고, 재사용성을 높여 개발 효율성과 유지보수성을 향상시켜줍니다.
모듈 시스템을 사용하면 어떤 이점이 있나요?
모듈 시스템을 사용하면 코드의 가독성과 유지보수성이 향상됩니다. 기능별로 코드를 분리할 수 있어 개발 효율이 높아지고, 모듈 간 의존성을 명확히 할 수 있습니다. 또한 네임스페이스 오염을 방지하고, 코드 재사용성도 높일 수 있습니다.
자바스크립트에서 모듈 시스템은 어떻게 발전해왔나요?
자바스크립트는 초기에 모듈 시스템을 지원하지 않았습니다. script 태그로 파일을 분리해도 하나의 전역 스코프를 공유했고, 이는 변수 충돌 등의 문제를 야기했죠. 이후 자바스크립트가 브라우저 밖에서도 사용되면서 모듈의 필요성이 대두되었고, CommonJS와 AMD(Asynchronous Module Definition)라는 두 진영에서 각자의 모듈 명세를 내놓았습니다.
Node.js는 CommonJS 사양을 채택하여 자체적인 모듈 시스템을 구현했습니다. 비록 공식 표준은 아니었지만 사실상의 모듈 표준으로 자리잡았죠. 그리고 ES6에서 드디어 클라이언트 사이드 자바스크립트를 위한 모듈 기능이 추가되었습니다. 이를 ESM(ECMAScript Module)이라고 합니다.
ESM(ES6 모듈)의 주요 특징은 무엇인가요?
ESM의 가장 큰 특징은 모듈 스코프입니다. 모듈 내에서 선언된 변수는 더 이상 전역 변수가 아니며, window 객체의 프로퍼티도 아닙니다. 각 모듈은 독립적인 스코프를 가지므로, 모듈 간에 변수명 충돌이 발생하지 않습니다.
또한 ESM은 'use strict'가 자동 적용되므로 엄격 모드로 동작합니다. ESM을 사용하려면 script 태그에 type="module" 속성을 추가하고, 모듈 파일의 확장자로는 mjs를 권장합니다.
ESM에서 내보내기(export)와 가져오기(import)는 어떻게 하나요?
ESM에서는 export와 import 키워드를 사용하여 모듈 간 자산을 공유합니다. export를 통해 변수, 함수, 클래스 등을 내보낼 수 있습니다. 선언문 앞에 export를 붙이거나, 하나의 객체로 묶어서 한번에 export할 수 있습니다.
예를 들면 다음과 같습니다.
export const pi = 3.14;
export function add(x, y) { return x + y; }
import를 통해서는 다른 모듈에서 내보낸 식별자를 가져올 수 있습니다. 가져올 대상을 중괄호 안에 나열하고, from 뒤에 모듈의 위치를 지정합니다.
import { pi, add } from './myModule.mjs';
또는 별칭(as)을 사용하거나, 전체를 하나의 객체로 가져올 수도 있습니다.
import * as myModule from './myModule.mjs';
ESM에서 모듈 경로는 어떻게 지정하나요?
경로는 상대 경로 또는 절대 경로로 지정해야 합니다. 다만, 브라우저에서 ESM을 사용할 때는 파일 확장자로 .mjs를 권장합니다. 하지만 Node.js에서는 package.json의 "type" 필드를 "module"로 설정하면 .js 파일도 ESM으로 인식합니다.
import { func } from './myModule.mjs'; // 상대 경로
import { func } from '/path/to/myModule.mjs'; // 절대 경로
예를 들어 위의 코드와 같습니다.
동기 모듈과 비동기 모듈의 차이는 무엇인가요?
동기 모듈은 모듈이 로드되는 동안 코드 실행이 blocking됩니다. 즉, 모듈이 완전히 로드되고 실행될 때까지 다음 코드로 진행하지 않습니다. CommonJS는 전통적으로 동기 모듈로 사용되었지만, 최신 버전의 Node.js에서는 CommonJS 모듈도 비동기적으로 로드할 수 있습니다.
반면 비동기 모듈은 non-blocking 방식으로 작동합니다. 모듈 로딩을 시작하고 바로 다음 코드로 진행하며, 모듈이 로드되면 콜백 함수가 실행됩니다. AMD가 비동기 모듈의 예입니다.
브라우저에서 ESM을 사용할 때 주의할 점은 무엇인가요?
브라우저에서 ESM을 사용할 때는 CORS(Cross-Origin Resource Sharing) 정책을 고려해야 합니다. 모듈 파일은 항상 CORS 헤더와 함께 제공되어야 하죠. 또한 브라우저의 모듈 캐싱 동작을 이해하고 있어야 합니다. 브라우저는 모듈 파일을 캐싱하므로, 모듈 파일을 업데이트해도 브라우저에 즉시 반영되지 않을 수 있습니다.
default export/import란 무엇인가요?
모듈에서 하나의 값만 내보낼 때는 default 키워드를 사용할 수 있습니다.
// 내보내기
export default function() { ... }
// 가져오기
import myFunc from './myModule.mjs';
default export는 모듈당 하나만 가능하며, 중괄호 없이 임의의 이름으로 import할 수 있습니다.
default export와 named export를 함께 사용할 수도 있습니다.
import myClass, { myVar, myFunc } from './myModule.mjs';
이때는 default export를 중괄호 밖에, named export를 중괄호 안에 넣어 import합니다. 이때 default export는 import 시에 어떤 이름으로든 사용할 수 있다는 점이 named export와의 주요 차이점입니다.
'🧱 프론트엔드 주제 > JavaScript' 카테고리의 다른 글
[모던 자바스크립트 Deep Dive] 49장 - Babel과 Webpack을 이용한 ES6+/ES.NEXT 개발 환경 구축 (1) | 2024.12.08 |
---|---|
[모던 자바스크립트 Deep Dive] 47장 - 에러 처리 (0) | 2024.11.30 |
[모던 자바스크립트 Deep Dive] 46장 - 제너레이터와 async/await (1) | 2024.11.30 |
[모던 자바스크립트 Deep Dive] 45장 - Promise (0) | 2024.11.26 |
[모던 자바스크립트 Deep Dive] 44장 - REST API (2) | 2024.11.24 |