[Javascript] 모듈의 임포트(Import)와 익스포트(Export) 기초

모듈은 큰 자바스크립트, 또는 큰 프로젝트를 기능 단위, 또는 파일 단위로 분리해서 관리를 하는 방식을 말합니다.

기능 단위, 또는 파일 단위로 분리한 자바스크립트 기능은 특정한 기능을 하거나 용도에 따라 적당히 분리를 하게 되고, 이렇게 분리한 하나의 단위를 모듈이라고 합니다.

모듈은 메인 코드와 모듈, 또는 모듈 사이에 상호 호출을 하는 규칙을 정해서 그 규칙에 따라서 상호 동작을 합니다.

모듈은 모듈 자신을 호출하는 코드에서 어떻게 모듈의 기능과 데이터에 접근을 할 수 하는지 export 지시자로 표현을 합니다. 모듈을 가져와 사용하는 코드 쪽에서는 모듈을 가져와 기능과 데이터에 접근을 하기 위한 이름을 import 지시자로 정의합니다.

자바스크립트 코딩 을 할 때는 하나의 기능은 하나의 모듈로, 그리고 하나의 자바스크립트 파일로 분리해서 작성하는 것을 기본으로 합니다. 많은 자바스크립트 프로젝트에서, 또 배포되는 대부분의 자사스크립트 모듈들이 하나의 파일에 하나의 기능을 구현하고, 또 하나의 모듈로 접근할 수 있도록 export 합니다.

하나의 파일에는 하나의 모듈이 있을 수도, 여러 개의 모듈이 있을 수도 있습니다. 또 모듈에서 노출하는 대상이 함수만이거나 데이터만 일 수도 있습니다.

모듈은 모듈을 사용할 코드에서 필요한 것들만 노출할 수 있기 때문에, 보안성을 높일 수 있습니다.

자바스크립트로 구동되는 프로그램 입장에서는 현재 웹 페이지에서 필요하지 않은 모듈을 로딩하지 않음으로 해서 더 적은 메모리 공간을 차지하고, 모듈로 인한 성능적인 이슈들을 신경 쓰지 않아도 되는 장점이 있습니다.

모듈의 익스포트

모듈의 기능을 다른 모듈, 코드에서 사용하려면 모듈의 변수와 기능을 익스포트 해서 사용할 수 있도록 허용해야 합니다. 모듈의 보안성은 익스포트 하지 않은 변수나 함수를 사용할 수 없도록 차단합니다.

변수/함수의 노출은 두 가지 방법으로 하며 기본적인 방법은 다음과 같이 변수와 함수 앞에 export 지시자를 더 붙여서 외부에 노출하는 변수/함수임을 표시합니다.

export const name = 'layer';

export function color(ctx, color){
    ctx.style.color = color;
}

export function transform(ctx, x, y, z) {
    ctx.rotate3d(x,y,x);
}

또는 다음과 같이 노출할 변수/함수를 export 지시자와 중괄호로 열거해서 노출을 하기도 합니다. 모듈 안에 변수/함수가 많고 일부만 노출하는 경우 이 방법이 보다 좋은 관리 방법입니다. 하단에 익스포트를 하는 경우 변수/함수 앞에 표시한 export 지시자는 삭제합니다.

const name = 'layer';

function color(ctx, color){
    ctx.style.color = color;
}

function transform(ctx, x, y, z) {
    ctx.rotate3d(x,y,x);
}

export {name, color, transform}

모듈을 임포트해서 사용하기

모듈에서 익스포트로 노출한 변수/함수를 HTML 웹페이지에서 사용할 수 있으려면 먼저 모듈 스크립트 태그로 모듈의 경로를 지정해 모듈을 가져와야 합니다.

모듈 경로를 지정할 때는 "/"로 시작하는 절대 경로(웹서버 루트로부터의 경로), 또는 "."으로 시작하는 상대경로(현재 웹페이지의 경로 기준)로 표현할 수 있습니다.

<script type="module" src="./runner.js"></script>

가져온 모듈의 변수, 함수를 사용할 수 있도록 스코프 네임을 모듈 스크립트 안에 임포트(import) 지시자로 선언합니다.

<script type="module">
    import {runTest as runner} from './runner.js';
</script>

 모듈 임포트는 모듈 스크립트 안에 선언해야 합니다. 그렇지 않으면 다음과 같이 에러가 발생합니다.

runner.js 파일에는 runTest() 함수 한 개만이 정의되어 있고 이 함수 자체를 익스포트 하고 있습니다.

export function runTest(){
    console.log('run!')
}

익스포트 한 runTest 함수 이름을 임포트 지시자에서 runner로 변경해서 사용하는 것으로 선언했으므로 runner()를 실행하면 콘솔 메시지가 출력됩니다.

모듈의 보안과 CORS

1. Strict mode

모듈안의 모든 코드는 스트릭트 모드(strict mode)로 동작합니다. 따라서 스트릭트 모드가 아닌 일반 환경에서 잘 실행되었던 코드여도 모듈 안에서 실행할 때는 에러가 발생할 수 있습니다. 나중에라도 모듈로 분리할 코드들은 기본적으로 "strict mode"를 선언하고 작성하는 것이 좋습니다.

<script type="module">
	a = 0 // Uncaught ReferenceError: a is not defined 에러 발생
</script>

 

2. 비동기 로딩

모듈을 로딩할 때는 자동으로 Promise 비동기 처리가 됩니다. 스크립트 태그로 모듈을 임포트 할 때도 defer 속성을 지정할 필요가 없습니다. 이 특징은 동적으로 모듈을 로딩할 때도 동일하게 적용됩니다.

<script type="module" src="runner.js"></script>

3. 로컬 실행

모듈을 임포트 한 자바스크립트 코드를 가진 HTML 파일을 웹 브라우저에서 직접 열면 CORS 오류가 발생합니다. 이것은 모듈의 보안성 규칙 때문에 발생하는 에러이며, 사용자가 임의로 끄거나 할 수 없습니다.

모듈이 포함된 코드를 테스트할 때는 반드시 서버 환경에서 테스트를 해야 합니다. 로컬 컴퓨터에서 테스트를 할 때도 로컬 웹서버를 통해서 localhost로 접근해야 합니다.

file://로 웹 브라우저에서 여는 모듈이 포함된 HTML 파일은 모듈 로딩 시점에서 CORS 에러가 발생합니다.

4. 스코프

모듈은 독립적인 자신만의 스코프를 가집니다. 모듈 안에서 정의한 변수, 함수는 모듈 안에서만 접근할 수 있습니다. 외부에서 접근해야 하는 변수, 함수는 익스포트로 노출시킨 후 임포트로 가져와야 합니다. 인라인 코드로 작성한 스크립트 또한 타입을 module로 선언해서 분리하면 스코프가 독립되기 때문에 모듈 밖의 코드에서 모듈 안의 변수나 함수에 접근할 수 없게 됩니다.

<script type="module">
    let character = "라이언";
</script>

<script type="module">
    character = '어피치' // Uncaught ReferenceError: character is not defined
</script>
<script>
    console.log(character) // Uncaught ReferenceError: character is not defined
</script>

디폴트(Default) 익스포트와 네임드(Named) 익스포트

우리가 사용하는 대부분의 모듈은 네임드 모듈입니다. 네임드 모듈이라는 것은 모듈 끝에 이 모듈을 어떤 이름으로 노출하겠다는 익스포트가 선언되어 있기 때문입니다. 

당연하지만 이름이 있어야 모듈을 임포트한 코드에서 이름으로 해당 모듈을 참조하고 접근할 수 있게 됩니다.

모듈을 작성할 때는 정말 특별히 예외적인 경우를 제외하고는 모두 네임드 익스포트를 하게 됩니다.

정말 특별히 예외적인 경우가 디폴트 익스포트, 또는 언네임드(익명) 익스포트입니다.

모듈을 익스포트 했지만 이름이 없는 경우 디폴트 익스포트라고 합니다. 자바스크립트에서 이름이 없는 경우는 한 가지입니다. 익명 함수로 정의했을 때이고, 이 익명 함수를 그대로 익스포트 하는 경우를 디폴트 익스포트, 또는 익명 익스포트라고 합니다.

간단하고 짧은 기능을 구현한 경우, 또는 메타 함수여서 임포트 하는 쪽에서 함수를 가져다 변경하거나 다른 모듈의 일부로 포함시키는 경우에 함수 이름을 별도로 지정하지 않습니다.

디폴트 익스포트로 노출하는 함수는 다음과 같이 정의합니다.

export default function(obj) {
    //start shape def
    obj.color = '#f0f0f0'
    obj.shape = 'rectangle'
    obj.rotate = '0deg'
}

디폴트 익스포트로 노출된 함수는 임포트 할 때 다음과 같이 익명 익스포트 한 함수의 이름을 별도로 부여합니다.

default는 생략 가능하기 때문에 줄여서 다음과 같이 표현하기도 합니다. 축약 표현으로 임포트를 할 때는 중괄호는 반드시 생략해야 합니다.

import {default as initShape} from './modules/init.js';

모듈의 이름 충돌 회피하기

모듈은 모듈에 작성한 객체, 함수 등을 노출할 때 선언한 이름으로 노출을 합니다.

예를 들어 다음과 같이 노출한 모듈을 임포트 해서 사용하면 모듈에 정의한 이름 그대로를 사용합니다. 이쪽이 훨씬 직관적이고 관리하기가 쉽습니다.

모듈의 이름이 중복되거나 유사해서 혼동이 있을 경우, 또는 구현한 모듈의 함수를 보다 직관적인 이름으로 사용하고 싶을 때 정의한 모듈의 이름을 변경해서 노출할 수 있습니다.

다음 노출 모듈들은 그대로 임포트를 하면 color와 shape 모듈의 이름이 같아서 "SyntaxError: redeclaration of import name" 오류(모듈 이름 중복 오류)가 발생합니다. 모듈 이름을 적절하게 재 정의하면 모듈의 용도를 분명하게 식별할 수 있게 할 수 있고, 오류도 방지할 수 있습니다.

export { color as polygonColor, shape as polygonShape, transform as polygonTransform }
export { color as pencilColor, shape as pencilShape, thickness as pencilThickness }

외부에서 가져오는 모듈인 경우, 노출된 모듈의 이름을 변경할 수 없거나 변경할 수 있다고 해도 버전 관리 등 여러 가지 이유로 변경하지 않는 것이 원칙입니다.

이럴 때는 모듈을 임포트 하면서 모듈의 이름을 재정의해서 새로운 이름으로 모듈을 접근할 수 있습니다.

공통으로 사용하는 모듈인 경우 특히 노출하는 모듈의 이름을 변경하는 것보다는 가져온 모듈의 이름을 변경해서 사용하는 쪽을 권장합니다.

앞서 이름을 바꿔서 익스포트 했던 모듈들은 익스포트 하는 이름을 그대로 두고 다음처럼 임포트 할 때 이름을 변경해서 사용할 수도 있습니다.

import { color as polygonColor, shape as polygonShape, transform as polygonTransform } from './anim.js';
import { color as pencilColor, shape as pencilShape, thickness as pencilThickness } from './pencil.js';

모듈을 모아서 임포트/익스포트 하기

로딩하는 모듈 개수가 많아지면 관리하는 작업도 번거로워지는 단점이 있습니다.

수작업으로 불필요한 모듈들을 제거해서 세심하게 최적화를 할 수 있기도 하지만, 특정 모듈이 누락되거나 하는 경우가 발생하기도 합니다.

기본적으로 자바스크립트의 모듈은 한 개의 자바스크립트 파일에 한 개의 모듈을 정의하는 방식으로 구현을 하기 때문에 경우에 따라서는 한 화면에 로딩하는 모듈의 개수가 수십 개가 되기도 합니다.

기능적으로 유사하고, 작고 개수가 많은 모듈들은 하나의 파일로 통합해서 관리를 하면 유지보수가 조금 더 쉬워집니다.

파일이 하나가 된다고 모듈 자체가 한 개가 되는 것은 아니고, 모듈을 임포트 할 때 필요한 모듈만 선언하면 되기 때문에 모듈 임포트 행 수가 많이 줄어들게 됩니다.

export { color } from './anim/color.js';
export { shape } from './anim/shape.js';
export { transform } from './anim/transform.js';

이렇게 여러 개의 작은 모듈들을 로딩하는 것을 대신해 다음처럼 하나의 파일에 여러 모듈들을 담아서 한 번에 로딩할 수 있습니다.

import { color, shape, transform } from './anim.js';

이렇게 임포트 하면 3개의 모듈 이름을 각각 사용할 수 있습니다.

다만 이렇게 하면 모듈 이름 자체는 계속 늘어나기 때문에 코드가 커지면 모듈의 이름을 찾는 것도 일이 됩니다.

그래서 모듈들을 모아서 하나의 이름에 속한 하위 모듈로 모을 수 있습니다.

앞서 3개의 모듈을 한 번에 가져오는 임포트 문을 다음과 같이 Anim 모듈 하위로 모을 수 있습니다.

import * as Anim from './anim.js';

Anim 모듈 이름으로 묶은 모듈들은 다음과 같이 사용할 수 있습니다.

Anim.color.rainbox(document.querySelector('#rectangle'), 1s);
Anim.transform.rotate(360deg);
Anim.remove(document.querySelector('#rectangle'));

동적 모듈 임포트

콘텐츠 뷰 화면에서 콘텐츠를 수정하는 수정 버튼을 누르면 에디터 모듈을 동적으로 로딩해서 에디터를 초기화하는 코드를 실행하는 이벤트리스터를 하나 만들어 보겠습니다.

버튼 클릭 이벤트가 발생하면 import('url') 메서드로 모듈을 로딩합니다. 메서드 체인으로 then이 오는 것은 모듈이 비동기로 로딩된다는 뜻입니다. 모듈을 프로미스(Promise)로 로딩한 후 객체(Editor)를 반환합니다.

반환받은 객체를 초기화(init) 한 후 UI에 에디터를 표시하면  모듈 로딩 작업이 완료됩니다.

let btnEditor = document.querySelector('#btneditcontent');
btnEditor.addEventListener('click', () => {
    import('./modules/editor.js').then((Editor) => {
        Editor.init('#content')
    })
});

동적인 모듈 로딩은 불필요한 모듈을 현재 콘텍스트 안에 유지하지 않아도 되기 때문에 전체 코드가 무거워지지 않는 장점이 있습니다.

대부분의 경우에 동적 모듈 로딩은 충분히 괜찮은 선택이지만, 실시간성이 있거나, 빠른 응답성을 요구하는 이벤트 처리에 동적으로 모듈을 로딩하는 것은 바람직하지 않습니다.

비동기로 로딩한 모듈을 현재 콘텍스트와 연동하고 초기화를 하는 과정에서 많은 리소스 사용과 순간적인 지연이 발생할 수 있기 때문입니다.