Module

김수정·2020년 5월 13일
0

모듈이란?

개발하는 애플리케이션의 크기가 커질 때 파일을 분리하는데, 이 때 분리된 파일 각각을 '모듈'이라고 부릅니다.
보통 클래스 하나 혹은 복수의 함수로 구성된 라이브러리 하나로 나뉩니다.

자바스크립트의 모듈 표준이 없었을 때, 써드파티 라이브러리들로 모듈시스템을 구현했습니다.
ex) AMD, CommonJS, UMD

그러나 이제는 모듈표준문법이 존재합니다.

모듈간의 관계

다른 모듈을 불러오고 내 모듈을 내보냄으로써 작게 쪼갠 모듈들을 조합하여 하나의 애플리케이션을 만들 수 있습니다.

export 변수나 함수 앞에 붙여 외부 모듈에서 해당 변수나 함수에 접근할 수 있습니다.

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

import 외부 모듈을 가져올 수 있습니다.

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

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

모듈을 브라우저에서 사용하려면 html에서 script를 불러올 때 <script type="module">이라고 표시를 해줘야합니다.

모듈의 특징

일반 스크립트와 다른 모듈의 특징을 알아봅시다.

(1) 엄격모드(strict mode)로 실행됩니다.
(2) 모듈은 자신만의 scope가 있어서, export하지 않은 자신의 변수나 함수들을 외부와 공유하지 않습니다.
(3) 모듈은 최초 호출 시 단 한 번만 실행되며, 이후 동일한 모듈이 여러 곳에서 사용되어도 처음 실행된 값이 공유됩니다.
(4) import.meta 호스트환경에 따라 제공되는 내용이 다르지만 현재 모듈에 대한 정보를 제공해줍니다.
(5) 모듈 최상위 레벨의 this는 undefined입니다. 전역객체가 아닙니다.

브라우저 모듈의 특정기능

브라우저 호스트 환경에서의 모듈의 특징이 몇가지 더 있습니다.

(1) 지연실행
스크립트의 defer 속성처럼 실행됩니다.
스크립트를 로드할 때는 html처리가 멈추지 않고 병렬적으로 불러옵니다.
html문서가 온전히 만들어진 이후에 실행되므로 html 문서 내 요소에 접근이 가능합니다.
스크립트의 상대적 순서가 유지됩니다. 문서상 위쪽의 스크립트부터 실행됩니다.

(2) 인라인 스크립트의 비동기 처리
async 속성을 붙여 다른 스크립트나 html이 처리되길 기다리지 않고 바로 실행될 수 있습니다.
광고, 문서레벨 이벤트리스터, 어디에도 종속되지 않는 기능 구현 시 유용합니다.

<!-- 필요한 모듈(analytics.js)의 로드가 끝나면 -->
<!-- 문서나 다른 <script>가 로드되길 기다리지 않고 바로 실행됩니다.-->
<script async type="module">
  import {counter} from './analytics.js';

  counter.count();
</script>

(3) 외부 스크립트

  • src 속성값이 동일한 외부 스크립트는 한 번만 실행합니다.
  • 다른 origin에서 모듈 스크립트를 불러오려면 모듈이 저장되어 있는 원격 서버가 Access-Control-Allow-Origin: *헤더를 제공해야만 모듈을 불러올 수 있습니다. CORS에러가 납니다.

(4) 경로가 없는 모듈을 허용하지 않습니다.
브라우저는 항상 import 뒤에 경로가 있어야 합니다. Node.js나 번들링 툴은 경로가 없어도 찾는 방법이 존재합니다.

(5) 호환성을 위한 nomodule
구식 브라우저는 모듈을 모르기 때문에 모듈 실행이 안되는 구식 브라우저 호환성을 위해 nomodule속성이 있습니다.

<script type="module">
  alert("모던 브라우저를 사용하고 계시군요.");
</script>

<script nomodule>
  alert("type=module을 해석할 수 있는 브라우저는 nomodule 타입의 스크립트는 넘어갑니다. 따라서 이 alert 문은 실행되지 않습니다.")
  alert("오래된 브라우저를 사용하고 있다면, type=module이 붙은 스크립트는 무시합니다. 대신 이 alert 문이 실행됩니다.");
</script>

빌드 툴

대개 브라우저에 모듈을 직접 사용하기보다는 웹팩같은 번들러를 사용합니다.
브라우저 호환성 문제도 있고, 브라우저용 모듈보다 더 많은 기능을 활용할 수 있으며, 최적화도 함께 해주기 때문입니다.

export/import 심화

import/export문은 내보낼 요소 위에 적든, 아래에 적든 위치는 상관없습니다.

export

내보내기 방법들을 알아봅시다.

선언부 앞에 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;
  }
}

선언부와 떨어진 곳에 export

해당 모듈 안에서 내보낼 요소들을 한데 모아 export할 수도 있습니다.

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

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

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

export as

as로 이름을 재정의하여 내보낼 수 있습니다.

// 📁 say.js
...
export {sayHi as hi, sayBye as bye};

export default

모듈은 복수의 함수가 있는 라이브러리 형태일 수도 있지만 개체 하나만 선언된 형태일 수도 있습니다.
대체로 후자의 방법으로 만들기 때문에 이때는 내보낼 요소도 하나만 존재합니다.
하나만 내보낼 때는 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');

동일한 다른 표현으로는,

function sayHi(user) {
  alert(`Hello, ${user}!`);
}

// 함수 선언부 앞에 'export default'를 붙여준 것과 동일합니다.
export {sayHi as default};

named export와 default export를 동시에 할 경우,

// 📁 user.js
export default class User {
  constructor(name) {
    this.name = name;
  }
}

export function sayHi(user) {
  alert(`Hello, ${user}!`);
}

// 📁 main.js(1)
import {default as User, sayHi} from './user.js';

new User('John');

// 📁 main.js(2)
import * as user from './user.js';

let User = user.default; // default export
new User('John');

모듈 다시 내보내기

export...from...문법으로 가져온 개체를 즉시 다시 내보내기할 수 있습니다.
외부에 공개할 요소들만 따로 모아놓는다던지, import로 전부 따로 불러오기보다는 한 곳에서 편하게 불러오게 만들 때 좋습니다.

export {sayHi} from './say.js'; // sayHi를 다시 내보내기 함

// default export는 방식이 까다롭습니다.
export User from './user.js'; // X
export {default as User} from './user.js'; // O
export * from './user.js' // default export는 내보내지지 않고, named export만 됨.
export {default} from './user.js' // 위의 방법을 쓰고 싶다면 이 명령까지 set로 써줘야 함.

import

불러오는 방법을 알아봅시다.

import "경로"

가져오기만 하고 사용할 게 없는 경우에 사용합니다. 보통 css 파일들입니다.

import "./mod.css";

import *

하나씩 가져오고 싶다면 중괄호 안에 해당요소를 적어주면 됩니다.

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

가져올 것이 많다면 한번에 가져올 수 있습니다.

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

둘 다 가능하지만 성능면에서는 가져올 대상을 구체적으로 명시해서 가져오는 것이 좋습니다.
왜냐하면, 빌드 툴을 사용할 때 사용하지 않는 것들은 자동 삭제해주는데 전체를 불러오면 삭제가 되지 않습니다.

import as

as를 사용하면 이름을 바꿔서 모듈을 가져올 수 있습니다.
해당 모듈에서 import한 모듈 대상과 이름이 같을 때 유용하게 사용됩니다.

import { sayHi as hi, sayBye as bye } from './say.js';

import default

default export 한 것을 import할 때 {}를 붙이지 않아도 됩니다.
named export와 섞여 있다면 default로 불러올 수 있고, *로 전체를 불러왔다면 .default로 그 것을 사용할 수 있습니다.

import User from './user.js'; // {User}가 아닌 User로 클래스를 가져왔습니다.
import {default as User, sayHi} from './user.js';
import * as user from './user.js';

let User = user.default; // default export

첫 번째 예시처럼 user.js에서 가져온 default export는 이름을 User가 아닌 다른 무엇으로 해도 괜찮습니다.
그러나 그것이 무엇인지 빠르게 캐치하기 위해 보통 파일이름과 동일한 이름으로 작성합니다.

dynamic import

export/import문은 '정적인'방식입니다. Import뒤에는 원시 문자열만 들어갈 수 있습니다.
또한, 런타임이나 조건부로 불러오는 것도 안됩니다. 조건에 따라 불러오는 게 안된다는 얘기죠.

import(module)표현식은 모듈을 읽고 이 모듈이 내보내는 것들을 모두 포함하는 객체를 담은 프라미스를 반환합니다.
코드 어디서나 동적으로 호출 가능합니다. 함수처럼 생겼지만 함수는 아닙니다. aync-await도 가능합니다.

let modulePath = prompt("어떤 모듈을 불러오고 싶으세요?");

import(modulePath)
  .then(obj => <모듈 객체>)
  .catch(err => <로딩 에러, e.g. 해당하는 모듈이 없는 경우>)
let {hi, bye} = await import('./say.js');

hi();
bye();
profile
정리하는 개발자

0개의 댓글