eatthefrog

모던 자바스크립트 모듈 본문

프론트엔드 노트

모던 자바스크립트 모듈

eater_forg 2024. 12. 31. 19:19

ES6 모듈 VS 일반 스크립트 (Script)

 

주요 특징

  1. 변수 범위 (Scoped to module):
    • 모듈 내부의 변수는 기본적으로 모듈 범위(scope) 내에만 접근 가능합니다.
    • 전역(global) 스코프에 영향을 주지 않습니다.
  2. 기본 모드 (Default mode):
    • ES6 모듈은 기본적으로 **엄격 모드(Strict mode)**에서 실행됩니다.
    • 오류를 방지하고 안전한 코드를 작성하도록 돕습니다.
  3. this 키워드:
    • 최상위 레벨에서의 this는 undefined로 설정됩니다. (전역 객체를 참조하지 않음)
  4. import와 export 지원:
    • 모듈에서 데이터를 내보내(export)고 가져오기(import)가 가능합니다.

 

 

사용법

(1) 내보내기 (Export)

  • Named Export: 이름이 있는 변수, 함수, 객체 등을 내보냄.
// math.js
export function rand(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}
export const pi = 3.14;
  • Default Export: 기본 내보내기로 하나의 값을 지정.
// utils.js
export default function greet(name) {
  return `Hello, ${name}`;
}

(2) 가져오기 (Import)

  • Named Import: 내보낸 이름과 동일한 이름으로 가져옴.
import { rand, pi } from './math.js';
console.log(rand(1, 10)); // 랜덤 숫자
console.log(pi); // 3.14
  • Default Import: 기본으로 내보낸 값을 가져올 때.
import greet from './utils.js';
console.log(greet('NewJeans')); // "Hello, NewJeans"
  • 모두 가져오기:
import * as MathUtils from './math.js';
console.log(MathUtils.rand(1, 10));
console.log(MathUtils.pi);

 

 

왜쓰는가? 

=> ES6 모듈로 전역 오염 방지

전역 네임스페이스 오염은
모든 스크립트가 같은 전역 스코프를 공유
할 때 발생합니다. 하나의 파일에서 정의된 변수, 함수, 객체가 의도치 않게 다른 파일이나 스크립트에 영향을 줄 수 있습니다. 

 

ES6 모듈을 사용하면 각 파일(모듈)은
독립된 스코프
를 가집니다. 전역 변수와 함수가 기본적으로 외부에 노출되지 않으므로, 충돌을 방지할 수 있습니다.
// script1.js
export const appName = "WeatherApp";
export function logAppName() {
  console.log(`App: ${appName}`);
}
// script2.js
import { appName, logAppName } from './script1.js';
var appName = "TodoApp"; // 같은 이름의 전역 변수이지만, 이 변수는 script2.js 내부에서만 유효
logAppName(); // 출력: App: WeatherApp

결과:

  • appName과 logAppName은 모듈 스코프 안에 캡슐화되어 전역 스코프를 오염시키지 않습니다.
    • import로 가져온 appName은 Script1.js의 appName입니다.
    • var appName = "TodoApp";은 script2.js의 로컬 변수로, logAppName() 함수와는 전혀 관련이 없습니다.
  • 다른 파일에서 동일한 변수 이름(appName)을 사용해도 문제가 없습니다.

 

다른 구체적인 예: 라이브러리 충돌 방지

문제 상황: 여러 라이브러리가 같은 전역 이름 사용

<script src="library1.js"></script> <!-- library1.js에서 `utils`를 정의 -->
<script src="library2.js"></script> <!-- library2.js에서도 `utils`를 정의 -->

<script>
  // library1.js와 library2.js의 `utils`가 충돌
  utils.doSomething(); // 어떤 utils가 호출될지 불확실
</script>

ES6 모듈 사용 시 :

// library1.js
export const utils = {
  doSomething: () => console.log("Library1 Utils"),
};

// library2.js
export const utils = {
  doSomething: () => console.log("Library2 Utils"),
};

// app.js
import { utils as utils1 } from './library1.js';
import { utils as utils2 } from './library2.js';

utils1.doSomething(); // 출력: Library1 Utils
utils2.doSomething(); // 출력: Library2 Utils

 

결과:

  • utils 객체를 독립적으로 가져와 이름 충돌 없이 사용 가능합니다.
  • as 키워드를 사용해 이름을 바꿔 충돌을 피할 수 있습니다.

 

 

 

😉Bonus

자바스크립트의 as 문법

as는 주로 모듈 가져오기(import/export) 또는 TypeScript에서 사용됩니다. 순수 JavaScript에서는 as 키워드 자체는 존재하지 않지만, ES6 모듈을 사용할 때 import와 함께 별칭(alias)을 지정하는 데 사용됩니다.

1. ES6 모듈에서의 as 사용

모듈에서 특정 이름으로 내보낸(export) 값을 다른 이름으로 가져오고 싶을 때 사용합니다.

// module.js
export const myFunction = () => console.log("Hello, world!");
export const anotherFunction = () => console.log("Another function");

// main.js
import { myFunction as newFunctionName } from './module.js';

newFunctionName(); // Hello, world!

위 예시에서 myFunction을 newFunctionName이라는 이름으로 가져왔습니다. 이는 이름 충돌을 방지하거나 더 의미 있는 이름을 사용할 때 유용합니다.

 


모듈 가져오기가 단순 복사가 아닌 이유

모듈 시스템이 단순히 코드를 복사해 오는 방식이 아니라, 참조(reference)를 통해 값을 관리하기 때문입니다.

1. 모듈은 기본적으로 싱글톤(singleton)

모듈은 한 번 로드되면 해당 모듈의 상태가 공유됩니다. 즉, 동일한 모듈을 여러 곳에서 가져와도 모듈의 상태는 한 곳에서 관리됩니다.

// counter.js
let count = 0;

export const increment = () => count++;
export const getCount = () => count;

// main.js
import { increment, getCount } from './counter.js';

increment();
console.log(getCount()); // 1

increment();
console.log(getCount()); // 2

위 예제에서 increment 함수가 count 값을 변경하면, 다른 파일에서 가져온 getCount를 호출할 때도 변경된 값이 유지됩니다. 이처럼 모듈은 단순 복사본이 아니라 원본 상태를 참조합니다.


2. 모듈 캐싱(module caching)

모듈(공유되는 데이터) 은 한 번 로드되면 캐시에 저장되어, 이후 같은 모듈을 가져올 때는 반복해서 새로 가져오지 않고, 이미 로드된 캐시된 버전을 재사용합니다. (변경사항들이 축적됨)

// module.js
console.log("Module loaded");
export const sayHello = () => console.log("Hello");

// main.js
import { sayHello } from './module.js';
import { sayHello as sayHiAgain } from './module.js';

// 출력
// Module loaded
sayHello();  // Hello
sayHiAgain(); // Hello

모듈 로드 시 console.log("Module loaded")가 한 번만 출력되는 이유는, 모듈이 한 번 로드된 후에는 캐시된 버전을 사용하기 때문입니다.


3. 변수 상태 공유와 동기화

모듈 내의 변수나 함수는 원본 상태를 공유하며, 한 번 데이터를 바꾸면 모두가 그 변화를 관찰할 수 있습니다.

// shared.js
export const sharedArray = [];

// moduleA.js
import { sharedArray } from './shared.js';
sharedArray.push('A');

// moduleB.js
import { sharedArray } from './shared.js';
sharedArray.push('B');

// main.js
import './moduleA.js';
import './moduleB.js';
import { sharedArray } from './shared.js';

console.log(sharedArray); // ['A', 'B']

모든 파일에서 동일한 sharedArray를 참조하기 때문에, 어떤 모듈에서 값을 추가하거나 수정하면 전체에 영향을 미칩니다.


요약

  1. as는 주로 모듈 가져오기에서 별칭(alias)을 지정하거나 TypeScript에서 타입 단언에 사용됩니다.
  2. 모듈 가져오기는 단순 복사가 아닌 참조를 통해 관리되며, 모듈은 싱글톤으로 동작하고 상태를 공유합니다.
  3. 모듈 캐싱과 상태 공유 덕분에 효율적으로 동기화된 데이터를 관리할 수 있습니다.