WeniVooks

검색

JavaScript 에센셜

모듈 시스템

1. 모듈 시스템

모듈은 코드를 특정 기능이나 목적에 따라 분리하여 관리할 수 있는 방법입니다. 모듈 시스템을 사용하면 코드의 재사용성과 유지보수성을 높일 수 있습니다.

프로젝트 규모가 커질 수록 모듈의 중요성이 커집니다. 모듈 시스템을 사용하면 코드의 가독성을 높이고, 충돌을 방지하며, 의존성을 관리할 수 있습니다. 자바스크립트에서는 ES6부터 모듈 시스템이 도입되었습니다.

2. 모듈의 역사

2-1. script

자바스크립트는 처음에 모듈 시스템이 없었습니다. 모든 코드는 전역 스코프에서 실행되었고, 전역 변수와 함수가 충돌할 위험이 있었습니다. 이를 해결하기 위해 IIFE(즉시 실행 함수 표현식) 패턴이 사용되었습니다.

또한 코드를 순서대로 불러오기 때문에 의존성 관리가 어렵습니다. 스크립트 로딩 순서를 수동으로 관리해야 합니다. utils.js에 작성된 함수를 app.js에서 사용하려면, app.js보다 먼저 utils.js를 불러와야 합니다.

<script src="utils.js"></script>
<script src="app.js"></script>
<script src="utils.js"></script>
<script src="app.js"></script>
2-2. ES6 모듈 (ESM)

ES6부터는 모듈 시스템이 도입되어, importexport 키워드를 사용하여 모듈을 정의하고 사용할 수 있습니다. 이를 통해 코드의 가독성과 재사용성을 높일 수 있습니다.

// utils.js
export function dateFormat(date) {
  return date.toISOString().slice(0, 10);
}
 
export const PI = 3.141592;
 
// app.js
import { dateFormat, PI } from './utils.js';
const today = new Date();
console.log(dateFormat(today)); // 2024-04-30
 
const r = 10;
console.log('넓이: ', r ** 2 * PI); // 3.141592
// utils.js
export function dateFormat(date) {
  return date.toISOString().slice(0, 10);
}
 
export const PI = 3.141592;
 
// app.js
import { dateFormat, PI } from './utils.js';
const today = new Date();
console.log(dateFormat(today)); // 2024-04-30
 
const r = 10;
console.log('넓이: ', r ** 2 * PI); // 3.141592

3. 모듈 사용해보기

모듈을 사용하기 위해서는 type="module" 속성을 추가해야 합니다. 이를 통해 브라우저는 해당 스크립트를 모듈로 인식합니다.

<script type="module" src="app.js"></script>
 
<!-- 또는 인라인으로 -->
<script type="module">
  import { add } from './math.js';
  console.log(add(2, 3));
</script>
<script type="module" src="app.js"></script>
 
<!-- 또는 인라인으로 -->
<script type="module">
  import { add } from './math.js';
  console.log(add(2, 3));
</script>

모듈은 기본적으로 strict mode로 실행됩니다. 따라서 use strict를 명시하지 않아도 엄격 모드가 적용됩니다. 또한, 모듈은 기본적으로 defer 속성이 적용되는 것처럼 로드되므로, DOMContentLoaded 이벤트 발생 직전에 실행됩니다.

DOMContentLoaded
DOMContentLoaded 이벤트는 HTML 문서가 완전히 로드되고 파싱되었을 때 발생합니다. 이 이벤트는 모든 DOM 요소가 생성된 후에 발생하므로, 스크립트가 DOM 요소에 접근할 수 있습니다.

스크립트가 DOMContentLoaded 이벤트보다 먼저 실행되면, DOM 요소에 접근할 수 없습니다. 따라서 스크립트는 DOMContentLoaded 이벤트가 발생한 후에 실행해야 합니다.

document.addEventListener('DOMContentLoaded', () => {
  // DOM 요소에 접근할 수 있음
  const element = document.querySelector('#myElement');
  console.log(element);
});
document.addEventListener('DOMContentLoaded', () => {
  // DOM 요소에 접근할 수 있음
  const element = document.querySelector('#myElement');
  console.log(element);
});

모듈은 각각 독립적인 스코프를 가지므로 전역 변수를 공유하지 않습니다. 따라서 모듈 내에서 정의된 변수나 함수는 다른 모듈이나 전역 스코프에서 접근할 수 없습니다. 이를 통해 코드의 충돌을 방지할 수 있습니다.

// myModule.js
const userName = 'licat';
export function greet() {
  console.log(`Hello, ${userName}`);
}
// module2.js
import { greet } from './myModule.js';
greet(); // Hello, licat
console.log(userName); // ReferenceError: userName is not defined
// myModule.js
const userName = 'licat';
export function greet() {
  console.log(`Hello, ${userName}`);
}
// module2.js
import { greet } from './myModule.js';
greet(); // Hello, licat
console.log(userName); // ReferenceError: userName is not defined
3.1 내보내기 / 가져오기

모듈에서 변수를 내보내려면 export 키워드를 사용합니다. 내보낸 변수는 다른 모듈에서 import 키워드를 사용하여 가져올 수 있습니다. as 키워드를 사용하면 모듈의 이름을 변경할 수 있습니다.

// math.js
export const PI = 3.14;
export function add(a, b) {
  return a + b;
}
export function subtract(a, b) {
  return a - b;
}
 
// app.js
import { PI, add as sum, subtract as sub } from './math.js';
 
console.log(PI); // 3.14
console.log(sum(2, 3)); // 5
console.log(sub(5, 2)); // 3
// math.js
export const PI = 3.14;
export function add(a, b) {
  return a + b;
}
export function subtract(a, b) {
  return a - b;
}
 
// app.js
import { PI, add as sum, subtract as sub } from './math.js';
 
console.log(PI); // 3.14
console.log(sum(2, 3)); // 5
console.log(sub(5, 2)); // 3
3.2 기본 내보내기

모듈에서 기본으로 내보낼 변수를 지정하려면 export default 키워드를 사용합니다. 기본 내보내기는 모듈당 하나만 사용할 수 있습니다.

가져올 때는 중괄호 없이 가져올 수 있습니다. 또한 원하는 이름으로 바로 가져올 수 있습니다.

// export
export default function myFunction() {
  console.log('Hello, world!');
}
 
// import
import myDefaultFunction from './myModule.js';
myDefaultFunction(); // Hello, world!
// export
export default function myFunction() {
  console.log('Hello, world!');
}
 
// import
import myDefaultFunction from './myModule.js';
myDefaultFunction(); // Hello, world!

다른 내보내기 모듈과 함께 사용할 수 있습니다.

// export
export const PI = 3.14;
export default function myFunction() {
  console.log('Hello, world!');
}
 
// import
import myFunction, { PI } from './myModule.js';
// export
export const PI = 3.14;
export default function myFunction() {
  console.log('Hello, world!');
}
 
// import
import myFunction, { PI } from './myModule.js';
3.3 동적 임포트

import 구분을 사용하여 가져오는 모듈은 정적 임포팅이 됩니다. 정적 임포트는 모듈을 가져오는 시점이 코드를 실행하기 전에 결정됩니다. 즉, 모듈을 가져오는 코드는 항상 모듈의 최상단에 위치해야 합니다. 이를 통해 모듈의 의존성을 명확하게 관리할 수 있습니다.

ES2020 동적 임포트 기능이 추가되었습니다. 이를 통해 모듈을 런타임 시점에서 동적으로 가져올 수 있습니다. import 함수를 사용하여 모듈을 가져오며 Promise를 반환합니다.

button.addEventListener('click', async () => {
  const module = await import('./myModule.js');
  module.myFunction();
  console.log(module.PI);
});
button.addEventListener('click', async () => {
  const module = await import('./myModule.js');
  module.myFunction();
  console.log(module.PI);
});

조건에 따라 모듈을 호출하거나 무거운 모듈을 나중에 로드할 수 있습니다. 이를 통해 초기 로딩 속도를 개선할 수 있습니다.

if (user.level === 'premium') {
  const { createMemo } = await import('./premium.js');
  createMemo();
}
if (user.level === 'premium') {
  const { createMemo } = await import('./premium.js');
  createMemo();
}

4. 모듈의 속성 관리

모듈에서 객체를 내보낼 때, 객체의 속성들의 특성을 관리할 수 있습니다. 객체의 속성은 Object.defineProperty() 메서드를 사용하여 정의할 수 있습니다. 이 메서드는 객체의 속성을 정의하고, 속성의 특성을 설정할 수 있습니다. 이러한 특성을 Property Descriptor라고 합니다.

const myObject = {};
Object.defineProperty(myObject, 'name', {
  value: 'licat',
  writable: false, // 속성 값 변경 불가
  enumerable: true, // 열거 가능
  configurable: false, // 삭제 불가
});
 
console.log(Object.getOwnPropertyDescriptor(myObject, 'name'));
const myObject = {};
Object.defineProperty(myObject, 'name', {
  value: 'licat',
  writable: false, // 속성 값 변경 불가
  enumerable: true, // 열거 가능
  configurable: false, // 삭제 불가
});
 
console.log(Object.getOwnPropertyDescriptor(myObject, 'name'));
  1. value : 속성의 값
  2. writable : true면 값 변경 가능, false면 읽기 전용
  3. enumerable : true면 열거 가능(for...in 루프에 포함됨), false면 열거 불가
  4. configurable : true면 속성 삭제 및 descriptor 변경 가능, false면 불가능

이러한 속성을 통해 외부 모듈이 특정 속성을 변경하거나 삭제하지 못하도록 보호할 수 있습니다. 예를 들어, writable 속성을 false로 설정하면 해당 속성의 값을 변경할 수 없습니다. configurable 속성을 false로 설정하면 해당 속성을 삭제할 수 없습니다.

계산된 속성이나 이름에 접근해야할 경우에는 getter, setter 메서를 사용할 수 있습니다.

  1. get : 속성의 getter 함수
  2. set : 속성의 setter 함수

Object.freeze()
객체를 내보내기 할 때 Object.freeze() 메서드를 사용하여 객체를 동결할 수 있습니다. 동결된 객체는 속성을 추가, 삭제, 수정할 수 없습니다. 즉, 객체의 구조를 변경할 수 없습니다.

const myObject = {
  name: 'licat',
  age: 30,
};
Object.freeze(myObject);
myObject.name = 'newName'; // 변경 불가
console.log(myObject.name); // 'licat'
delete myObject.age; // 삭제 불가
console.log(myObject.age); // 30
const myObject = {
  name: 'licat',
  age: 30,
};
Object.freeze(myObject);
myObject.name = 'newName'; // 변경 불가
console.log(myObject.name); // 'licat'
delete myObject.age; // 삭제 불가
console.log(myObject.age); // 30

객체를 읽기 전용으로 내보내기 할 때는 Object.freeze() 메서드를, 각각의 속성의 특성을 관리할 때는 Object.defineProperty() 메서드를 사용하여 속성을 정의할 수 있습니다. 이를 통해 외부 모듈이 객체의 속성을 변경하거나 삭제하지 못하도록 보호할 수 있습니다.

12.3 엄격 모드13장 최적화 (Optimization)