TL;DR


export과 import 문

ES6에서 소개된 export과 import 문은 여러 개의 코드 단위, 즉 모듈들로 하여금 서로 의존하고 코드를 공유할 수 있게 합니다. 두 문을 사용하면 코드를 모듈로 분리하고 다른 모듈에서 필요한 기능이나 변수를 불러와 사용할 수 있습니다.

export

export 문을 사용해 변수, 함수, 객체, 클래스 등의 엔티티를 외부로 내보낼 수 있습니다.

index.html
1<html lang="en">
2  <head>
3    <meta charset="UTF-8" />
4    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5    <script type="module" src="./main.js"></script>
6    <title>moonkorea</title>
7  </head>
8  <body>
9    <h1>moonkorea</h1>
10  </body>
11</html>;
12// main.js
13// ...
14
15// calc.js
16export const oddNumbers = [1, 3, 5, 7];
17export const sum = (a, b) => a + b;
18export const sub = (a, b) => a - b;

export 키워드는 선언부 앞에 사용할 수도 있고 먼저 엔티티를 정의한 후 내보낼 엔티티들을 묶어 내보낼 수도 있습니다.

calc.js
1const oddNumbers = [1, 3, 5, 7];
2const sum = (a, b) => a + b;
3const sub = (a, b) => a - b;
4export { oddNumbers, sum, sub };

위와 같은 형태의 export 문법은 named export이라고 하는데요, named export 말고도 개체를 내보낼 수 있는 default export가 있습니다.

user.js
1export default class User { // 선언부에 export default
2  constructor(name) {
3    this.name = name;
4  }
5}
6// 또는
7class User {
8  constructor(name) {
9    this.name = name;
10  }
11}
12export default User // 선언 후 export default
13// 또는
14class User {
15  constructor(name) {
16    this.name = name;
17  }
18}
19export { User as default }; // 선언 후 export { as default}

export default 문법으로 모듈을 내보내면 해당 모듈에는 하나의 개체만 있다는 것을 나타낼 수 있습니다.

모듈 내에 named export과 default export을 같이 사용해도 문제는 없습니다. 다만 export default는 모듈당 하나만 정의할 수 있습니다.

default export은 모듈에서 개체 하나만 내보낸다는 것을 의미하기 때문에 식별자 없이 익명으로 내보낼 수 있습니다.

greet.js
1export default class {
2  constructor(message) {
3    this.message = message;
4  }
5  greet() {
6    console.log(this.message);
7  }
8}
9// someModule.js
10export default {
11  name: "moon",
12  age: 31
13};

named export은 식별자 없이 익명으로 내보낼 수 없습니다.


import

export 문으로 엔티티를 내보내면 import 문으로 다른 모듈에서 현재 모듈로 불러와 사용할 수 있습니다.

main.js
1console.log('hello world');
2import { oddNumbers, sum } from './calc.js';
3console.log(oddNumbers);
4console.log(sum(1, 10));

만약 import 해올 엔티티들이 많아지면 import * as <obj >처럼 모든 것들을 네임스페이스(객체 형태)로 가져올 수 있습니다.

main.js
1import * as calc from './calc.js';
2console.log(calc.oddNumbers);
3console.log(calc.sum(1, 10));

네임스페이스를 사용해서 더 적은 코드로 모든 것들을 불러올 수 있지만 가능하면 불러오는 모듈에서 사용될 엔티티들을 명시해 주는 것이 좋습니다. 이와 관련해서는 아래 내용에서 더 다루겠습니다.

named export으로 내보낸 엔티티는 불러올 때 규칙이 중괄호와 함께 정확한 엔티티명으로 불러와야 하는데 default export으로 내보낸 경우 문법이 조금 다릅니다.

user.js
1export default class User {
2  constructor(name) {
3    this.name = name;
4  }
5}
main.js
1import User from './user';
2// 또는
3import Participant from './user'; // 다른 이름으로 불러오기
4// 또는
5import { default as Participant } from './user'; // 명시적으로 다른 이름으로 불러오기
6// 또는
7import { default as User } from './user'; // 명시적으로 불러오기
8new User('moonkorea');

default export로 내보낸 경우 중괄호 없이 임의의 이름으로 불러오거나 { default as SomeName }으로 불러올 수 있습니다.

위 예제처럼 default export로 내보낸 엔티티를 import 할 때는 새로운 이름으로 엔티티를 모듈에서 불러올 수 있어 자유도가 높은데 name export이라고 해서 안되는 건 아닙니다.

named export으로 내보내진 엔티티의 경우 불러올 때 as 키워드로 이름을 새로 지정할 수 있습니다.

main.js
1import { sum as plus } from './calc';
2import { add } from './someLibrary1';
3import { add as sum } from './someLibrary2';
4console.log(plus(1, 2));
5console.log(add(1, 2));
6console.log(sum(1, 2));

두 개의 다른 라이브러리나 코드베이스에서 동일한 이름의 함수를 하나의 모듈에서 사용할 경우 이름을 유연하게 지정해서 사용할 수 있습니다.

문법과 명명 규칙이 다양해서 일관된 컨벤션으로 사용하는 것이 바람직합니다.

  • import와 export 문은 모듈의 맨 위나 맨 아래 정의할 수 있고 동작에는 차이가 없습니다.
main.js
1// import { getName } from './user.js'; // 스크립트 위에 정의하던
2getName();
3import { getName } from './user.js'; // 스크립트 아래에 정의하던 동일하게 동작
  • 정적 import와 export 문은 중괄호 안에서 동작하지 않습니다.
main.js
1if (condition) {
2  import { getName } from './user.js';
3}

모듈 다시 내보내기

위에서 다룬 예제들은 하나의 모듈에서 몇 개의 함수들만 불러와 사용했는데 10개, 20개의 모듈에서 함수를 불러와 사용할 경우에는 어떻게 불러올까요?

main.js
1import { func1 } from './module1.js';
2import { someFunc1, someFunc2 } from './module2.js';
3import { func3 } from './module3.js';
4import { func4 } from './module4.js';
5// import ..
6// import ..
7import { func20 } from './module20.js';

main.js에서 여러 함수를 20개의 모듈에서 불러오게 되면 코드가 길어지고 개발자에 따라 보기에 지저분할 수 있습니다.


export .. from ..

export from 문을 사용하면 하나의 모듈에서 여러 함수, 변수 등의 엔티티들을 불러와 해당 모듈에서 모두 불러오고 내보낼 수 있습니다.

combinedModule.js
1export { func1 } from './module1.js';
2export { someFunc1, someFunc2 } from './module2.js';
3// export .. from ..
4export { func20 } from './module20.js';

그리고 함수들을 불러와 사용하는 모듈에서는 아래와 같이 사용할 수 있겠죠.

main.js
1import { func1, someFunc2, ... , func20 } from './combinedModule.js';
2// 또는
3import * as someNamespace from './combinedModule.js';

참고로 위 combinedModule.js는 아래와 동일하게 동작합니다.

combinedModule.js
1import { func1 } from './module1.js';
2export { func1 };
3import { someFunc1, someFunc2 } from './module2.js';
4export { someFunc1, someFunc2 };
5// ..
6import { func20 } from './module20.js';
7export { func20 };

named export의 경우 위와 같이 작성할 수 있는데요, default export의 경우 조금 다릅니다.

user.js
1export default class User {
2  constructor(name) {
3    this.name = name;
4  }
5}
combinedModule.js
1export User from './user'; // 에러
2export { default as User } from './user'; // OK
3// 또는
4export { default } from './user'; // OK

그리고 불러오는 모듈에서 사용할 때는 아래와 같이 사용할 수 있겠죠.

main.js
1import User from './combinedModule.js';
2// 또는
3import SomeOtherName from './combinedModule.js'; // 다른 이름으로 불러오기
4// 또는
5import { default as User } from './combinedModule.js'; // 명시적으로 불러오기

동적으로 모듈 가져오기

export과 import 문은 모듈 간 코드를 공유하고 코드 구조의 중심을 잡아주는 뼈대를 구축한다고 볼 수 있는데요, 브라우저는 import 문을 만나면 해당 모듈을 가져오기 전에 모든 의존성을 먼저 불러오고 번들러를 사용하는 경우 빌드 타임에 코드가 번들링 됩니다. 두 문은 정적으로 빌드 타임에 동작하기 때문에 런타임이나 조건부로 모듈을 불러올 수 없습니다.

모듈을 동적으로 불러오기 위해서는 import() 표현식을 사용합니다.


import()

import() 문이 실행되면 모듈에서 내보내는 엔티티들을 객체로 담은 프로미스를 반환합니다.

main.js
1let path = prompt('모듈 경로를 입력하세요');
2import(path)
3  .then(obj => console.log('모듈 객체 : ', obj))
4  .catch(err => console.log(err));
5// 또는
6let module = await import(modulePath);

동적으로 모듈을 불러올 때 다음과 같이 사용할 수 있겠죠.

index.html
1<!doctype html>
2<script>
3  async function load() {
4    const blog = await import('./blog.js');
5    blog.hi(); // 안녕하세요.
6    blog.bye(); // 안녕히 가세요.
7  }
8</script>
9// ..
10<button onclick='load()'>클릭</button>

동적으로 불러올 때 모듈을 조건문에서도 사용할 수 있습니다.

main.js
1const { getUrl } = await import('/blog.js');
2if (condition) {
3  getUrl();
4}

import()를 사용해서 다수의 모듈을 동적으로 불러올 수도 있습니다.

main.js
1const promises = Promise.all([
2  import('/module1.js'),
3  import('module2.js'),
4]);
5promises.then(res => console.log('promises:', res));

트리 쉐이킹

앞서 모듈을 불러올 때 네임스페이스를 사용해서 내보내진 모든 엔티티들을 한 번에 불러오는 것보다 구체적으로 불러올 엔티티들을 명시하는 것이 바람직하다고 했는데요, 네임스페이스를 활용하면 코드가 간결해지고 짧아지지만 몇 가지 트레이드오프가 있습니다 : 과도한 중첩, 네이밍, 트리 쉐이킹 등.

번들러를 사용해서 빌드 할 경우 번들러는 최종 번들에서 사용되지 않는 코드를 빌드 과정에서 제거하는 트리 쉐이킹 과정을 거치게 됩니다.

트리 쉐이킹은 사용되지 않는 코드를 제거하고 최종 번들의 크기를 최소화합니다. 사용되지 않는 변수, 함수 등의 코드는 번들링 과정에서 제거되고 브라우저가 다운로드 받아야 하는 리소스의 크기를 줄입니다.

Webpack, Rollup 등 번들러는 모듈 간 의존성을 파악하고 하나 또는 여러 파일(번들)로 병합하고 최적화합니다. 이 과정에서 사용되지 않는 코드를 제거하고 최신 자바스크립트 문법을 다른 버전의 브라우저에서도 호환되게 변환하는 트랜스파일 과정 등의 번들링 작업을 수행합니다.


웹팩

1.1 웹팩


heavyModule.js
1export function foo() {}
2export function bar() {}
3// ..
4export function funcT() {}
module1.js
1import * as SomeNamespace from './heavyModule.js';
2const result1 = SomeNamespace.foo();
3const result2 = SomeNamespace.bar();

module1에서는 네이스페이스로 foo 함수와 bar 함수만 사용하고 있습니다. 실제로 모듈 간 의존성이 없는 코드도 최종 번들에 포함될 수 있어 트리 쉐이킹의 이점을 살리지 못할 수 있습니다.

출처