모듈

345·2023년 7월 12일

모던 JavaScript

목록 보기
23/23

CommonJS 와 ES modules

모듈의 기능과 사용을 살펴보기 전에, CommonJS 가 뭐고 ES modules 가 뭔지 알아봅시다.
둘은 모두 어떤 스크립트에서 다른 스크립트의 내용을 가져다 쓰기 위한 모듈 사용 방법인데요,
CommonJS(CJS) 는 좀 옛날 방식, ES Modules(ESM) 은 최근 쓰이는 방식입니다.


  • CommonJS

CommonJSmodule.exports 로 모듈을 내보내고,
require() 로 모듈을 가져옵니다.

// 📁 util.js

module.exports.add = function(a, b) {
        return a + b;
} 

module.exports.subtract = function(a, b) {
        return a - b;
}


// 📁 main.js
const {add, subtract} = require('./util')

console.log(add(5, 5)) // 10
console.log(subtract(10, 5)) // 5

  • ES modules

ES modulesexport 로 모듈을 내보내고
import 로 모듈을 가져옵니다.


node.js 환경의 CJS, ESM

node.js 환경은 기본적으로 CJS 를 쓰고 있습니다.
하지만 package.json 파일에 type: module 로 지정해주면 ESM 을 사용합니다.

{
  "type": "module"
}

이러면 ESM 을 사용해야하고, CJS 를 사용하면 오류가 납니다.

type: module 인 상태에서 index.js 파일의 내용을 CJS 방식으로 사용하기 위해서는,
index.js 파일의 이름을 index.cjs 로 변경해야 합니다.

그럼 이 자바스크립트 파일에서 CJS 방식을 사용한다고 알려주어 잘 동작합니다.


🧩 모듈

📌 이하의 글은 ESM 모듈 시스템을 전제로 합니다.

스크립트 하나의 길이가 길어지자 여러 개의 파일로 분리하여 사용하기 시작했습니다.
이렇게 분리된 것을 모듈이라고 하는데, 모듈은 하나의 스크립트입니다.

모듈 안의 변수나 함수 앞에 export 지시자를 붙여 외부에서 접근할 수 있도록 하며,
import 지시자를 사용하여 외부 모듈의 기능(export 붙은 것)을 가져오도록 합니다.

// 📁 sayHi.js
export function sayHi(user) {
  alert(`Hello, ${user}!`);
}


// 📁 main.js
import {sayHi} from './sayHi.js';

alert(sayHi); // 함수
sayHi('John'); // Hello, John!

❗모듈은 로컬 파일에서는 동작하지 않고, HTTP / HTTPS 프로토콜을 통해서만 동작합니다.


type 🟰 module 인 스크립트❓

스크립트에type="module" 로 속성을 지정하면 모듈로서 사용합니다.

<!doctype html>
<script type="module" src="user.js"></script>
<script type="module" src="hello.js"></script>

이게 type="module" 을 적지 않은 스크립트와는 무슨 차이가 있는걸까요?
모듈의 기능에 대해 알아봅시다.

❗모듈의 기능

일반 스크립트와 모듈의 차이는 다음과 같습니다.

  • 항상 엄격 모드로 실행됨
  • 모듈 레벨 스코프의 존재
    • 외부 스크립트에서 모듈에 그냥 접근 ❌
    • 모듈은 변수, 함수를 공유하지 않음
  • 모듈을 외부에 공개하려면 export, export 된 모듈을 가져오려면 import
  • 단 한 번만 평가됨
    • 동일한 모듈이 여러 곳에서 따로 import 되어도 모듈은 최초 호출 시 단 한 번만 실행
      • 실행 후 결과는 모듈을 import 하는 모든 곳에 내보내짐
  • 모듈은 최상위 레벨 thisundefined (일반 스크립트의 경우는 전역 객체)

  • 모듈 레벨 스코프
<!doctype html>
<script src="user.js"></script>
<script src="hello.js"></script>

위처럼 일반 스크립트로 두 js 파일을 포함해줬다면,
user.js 에서 선언한 변수는 hello.js 에서도 사용이 가능합니다.

하지만 모듈로 만들면 스크립트는 변수를 서로 공유하지 않습니다.
그 모듈 스크립트만의 스코프가 따로 정의되기 때문입니다.

모듈의 변수를 사용하려면 export, import 를 해줘야 합니다.

<script type="module">
  // user는 해당 모듈 안에서만 접근 가능합니다.
  let user = "John";
</script>

<script type="module">
  alert(user); // Error: user is not defined
</script>

// type="module" 을 지우면 변수가 공유되어 John 이 alert 됨

  • 모듈은 한 번만 평가됨

모듈이 여러 번 import 되어도 정작 코드는 최초 호출 시 한 번만 실행됩니다.

예를 들면, 객체를 export 하는 모듈이 있다고 합시다.
이 모듈에 정의된 객체를 받아 조작하는 스크립트가 여러개라면,
각 스크립트는 모두 같은 객체를 참조하게 됩니다.

// 📁 admin.js 파일
export let admin = {
  name: "John"
};

// 📁 1.js 파일
import {admin} from './admin.js';
admin.name = "Pete";

// 📁 2.js 파일
import {admin} from './admin.js';
alert(admin.name); // Pete

// 1.js와 2.js 모두 같은 객체를 가져오므로
// 1.js에서 객체에 가한 조작을 2.js에서도 확인할 수 있습니다.

실행된 모듈이 import 된 모든 곳에서 공유됩니다.
한 스크립트에서 변경한 상태가 동일한 모듈을 import 한 다른 스크립트에서도 적용됩니다.
하나의 대상을 참조하기 때문입니다.

이 특성을 사용하여 초기화 등에 쓰곤 합니다.


브라우저의 모듈

브라우저 환경에서 type="module" 이 붙으면 특정한 기능을 가집니다.

  • 지연 실행
    • 모듈은 HTML 문서 준비 완료까지 대기 상태, HTML 문서 완료되면 실행
      • 따라서, 모듈은 항상 완전한 HTML 에 접근 가능
      • defer 속성을 붙인 것처럼 실행됨
    • 일반 스크립트는 HTML 완료 전이라도 바로 실행, 따라서 모듈 스크립트보다 먼저 실행

  • 인라인 스크립트의 비동기 처리
    • 일반 스크립트에서 async 속성은 외부 스크립트를 불러올 때만 유효하지만,
      모듈은 인라인 스크립트에도 적용됨
    • 인라인 모듈 스크립트에 async 를 붙이면 다른 스크립트나 HTML 처리를 기다리지 않고 바로 실행
<!-- 필요한 모듈(analytics.js)의 로드가 끝나면 -->
<!-- 문서나 다른 <script>가 로드되길 기다리지 않고 바로 실행 -->
<script async type="module">
  import {counter} from './analytics.js';

  counter.count();
</script>

❓ 외부 스크립트, 인라인 스크립트

<script src="hello.js" />
위처럼 src 를 지정하여 외부에 있는 파일을 포함한 게 외부 스크립트,
그러지 않고 그냥 html 에서 <script> 태그 내부에 작성한 게 인라인 스크립트입니다.

  • 외부 스크립트
    • src 값이 동일하면 한 번만 실행
    • 외부 사이트 등 다른 오리진에서 모듈을 불러오려면 CORS 헤더가 필요
<!-- src 값이 동일함. my.js는 한 번만 로드 및 실행 -->
<script type="module" src="my.js"></script>
<script type="module" src="my.js"></script>

  • 경로 없는 모듈은 에러가 남
import {sayHi} from 'sayHi'; // Error!
// './sayHi.js'와 같이 경로 정보를 지정해 주어야 함

모듈 내보내고 가져오기

exportimport 를 사용하는 다양한 방법에 대해 알아봅시다.

export 와 import

변수나 함수, 클래스 앞에 export 를 붙여 내보낼 수 있습니다.
이렇게 export 한 것은 다른 파일에서 import 로 가져오는 게 가능합니다.

export { 변수명, 함수명...} 으로 내보내는 것도 가능합니다.

// 배열 내보내기
export let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

// 상수 내보내기
export const MODULES_BECAME_STANDARD_YEAR = 2015;

// 클래스 내보내기
export class User {
  constructor(name) {
    this.name = name;
  }
}

// 📁 say.js
function sayHi(user) {
  alert(`Hello, ${user}!`);
}

function sayBye(user) {
  alert(`Bye, ${user}!`);
}

export {sayHi, sayBye}; // 두 함수를 내보냄

import 할 땐 이름으로 가져옵니다.

// 📁 main.js
import {sayHi, sayBye} from './say.js';

sayHi('John'); // Hello, John!
sayBye('John'); // Bye, John!

import * 로 모두 가져오기

// 📁 main.js
import * as say from './say.js';

say.sayHi('John');
say.sayBye('John');

이렇게 * 를 붙여 가져오면 모듈이 export 하는 대상을 모두 가져올 수 있습니다.
위에서처럼 say 라는 객체에 프로퍼티로 담아 가져오고 있습니다.

보통은 이러지 않고 하나하나 가져오는걸 권장합니다.


export default 로 내보내기

내보내고자 하는 개체 앞에 export default 를 붙이면,
그 모듈은 import 할 때 무조건 그 개체를 가져옵니다.

따라서, export default 는 한 파일 당 한 번만 사용합니다.

// 📁 user.js
export default class User { // export 옆에 'default'를 추가해보았습니다.
  constructor(name) {
    this.name = name;
  }
}

// 📁 main.js
import User from './user.js'; // {User}가 아닌 User로 클래스를 가져왔습니다.

new User('John');

가져오는 대상이 딱 하나로 정해져있으니 import 할 때 모듈의 개체와 이름이 달라도 괜찮습니다.

하나의 모듈에서 export 와 export default 를 섞어쓰지 않는 걸 권장합니다.


가져온 모듈을 다시 내보내기

export ... from ... 문법을 쓰면 가져온 개체를 다시 내보냅니다.
보통 모듈의 기능만 내보내고 모듈 자체의 내용을 숨길 때 사용합니다.

// helpers.js 와 user.js 를 숨기고 싶을 때

// 📁 auth/index.js
// login과 logout을 가지고 온 후 바로 내보냅니다.
// import 후에 바로 export 하는 것과 동일
export {login, logout} from './helpers.js';

// User 가져온 후 바로 내보냅니다.
export {default as User} from './user.js';
...

default export 를 다시 내보내려면 export {default} from ... 을 사용해야 합니다.

export * from './user.js'; // named export를 다시 내보내기
export {default} from './user.js'; // default export를 다시 내보내기

동적으로 모듈 가져오기

import 는 블록 내부에서 사용할 수 없어 조건에 따라 모듈을 가져올 수 없습니다.
또한, 모듈 경로는 문자열만 허용되기 때문에 동적으로 경로를 지정할 수 없죠.

따라서 모듈을 동적으로 불러오고 싶을 땐 다음과 같은 방법을 사용합니다.

import( ) 표현식

import(module) 표현식은 모듈을 읽고
모듈이 export 하는 것을 모두 포함하는 객체를 담은 이행 상태의 프라미스를 반환합니다.

이 방식을 사용하면 코드 내 어디서든 동적으로 모듈을 가져올 수 있습니다.

import( ) 는 함수 호출이 아닌 특수 문법이므로, 함수처럼 사용할 수는 없습니다.


  • Promise 얻어와 모듈 사용하기
let modulePath = prompt("어떤 모듈을 불러오고 싶으세요?");

import(modulePath)
  .then(obj => <모듈 객체>)
  .catch(err => <로딩 에러, e.g. 해당하는 모듈이 없는 경우>)

다음과 같이 async/await 와 함께 사용하는 것도 가능합니다.

let {hi, bye} = await import('./say.js');

hi();
bye();

  • default export 가져오기

default export 한 모듈은 객체의 default 프로퍼티를 사용하여 가져옵니다.

// 📁 say.js
export default function() {
  alert("export default한 모듈을 불러왔습니다!");
}

let obj = await import('./say.js');
let say = obj.default;
// 위 두 줄을 let {default: say} = await import('./say.js'); 같이 한 줄로 줄일 수 있습니다.

say();
profile
기록용 블로그 + 오류가 있을 수 있습니다🔥

0개의 댓글