모듈의 개념

임준영·2021년 4월 11일
0

모듈의 개념

애플리케이션의 규모가 커질수록 언젠간 파일을 여러 개로 분리하는 시점이 오는데, 이때 분리된 파일 각각을 모듈(module)이라고 부릅니다. 모듈은 대개 클래스 하나 혹은 특정한 목적을 가진 복수의 함수로 구성된 라이브러리 하나로 구성됩니다.

초기에는 스크립트는 크기도 작고 기능도 단순했기 때문에 자바스크립트는 긴 세월 동안 모듈 관련 표준 문법 없이 성장할 수 있었습니다. 즉, 새로운 문법을 만들 필요가 없었던 것이죠. 그런데 스크립트의 크기가 점차 커지고 기능도 복잡해지자 자바스크립트 커뮤니티는 특별한 라이브러리를 만들어 필요한 모듈을 언제든지 불러올 수 있게 해준다거나 코드를 모듈 단위로 구성해 주는 방법을 만드는 등 다양한 시도를 합니다.

1. 모듈이란?

모듈은 단지 파일 하나에 불과합니다. 스크립트 하나는 모듈 하나입니다.

모듈에 특수한 지시자 exportimport를 적용하면 다른 모듈을 불러와 불러온 모듈에 있는 함수를 호출하는 것과 같은 기능 공유가 가능합니다.

  • export 지시자를 변수나 함수 앞에 붙이면 외부 모듈에서 해당 변수나 함수에 접근할 수 있습니다.(모듈 내보내기)

  • import 지시자를 사용하면 외부 모듈의 기능을 가져올 수 있습니다. (모듈 가져오기)

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

이제 import 지시자를 사용해 main.js에서 함수 sayHi를 사용할 수 있게 해보겠습니다.

import { sayHi } from './sayHi.js'

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

위 예시에서 import 지시자는 상대 경로를 이용해 모듈을 가져오고, sayHi.js에서 내보낸 함수 sayHi 함수를 호출 합니다.

이제 브라우저에서 모듈이 어떻게 동작하는지 예시를 이용해 알아보겠습니다.

모듈은 특수한 키워드나 기능과 함께 사용되므로 <script type="module"> 같은 속성을 설정해 해당 스크립트가 모듈이란 걸 브라우저가 알 수 있게 해줘야 합니다.

<!doctype html>
<script type="module">
 import { sayHi } from './say.js';

 document.body.innerHtml = sayHi('Junyoung');
</script>

이제 브라우저가 자동으로 모듈을 가져오고 평가한 다음 이를 실행한 것을 확인할 수 있습니다.

2. 모듈의 핵심 기능

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

  • 엄격 모드로 실행됨

모듈은 항상 엄격 모드로 실행됩니다. 선언되지 않는 변수에 값을 할당하는 등의 코드는 에러를 발생시킵니다.

<script type="module">
 a = 5; // 에러
</script>

모듈 레벨 스코프

모듈은 자신만의 스코프가 있습니다. 따라서 모듈 내부에서 정의한 변수나 함수는 다른 스크립트에서 접근할 수 없습니다.

user.js와 hello.js를 가져오고 user.js에서 선언한 변수 user를 hello.js에서 사용하면 에러가 나는 것을 확인할 수 있습니다.

외부에 공개하려는 모듈은 export해야 하고, 내보내진 모듈을 가져와 사용하려면 import를 해줘야 합니다.

  • 단 한 번만 평가됨

동일한 모듈이 여러 곳에서 사용되더라도 모듈은 최초 호출 시 단 한 번만 실행됩니다. 실행 후 결과는 이 모듈을 가져가려는 모든 모듈에 내보내 집니다.

이런 작동방식은 중요한 결과를 초래합니다. 예시를 통해 이에 대해 알아봅시다.
alert 함수가 있는 모듈(alert.js)을 여러 모듈에서 가져오기로 해봅시다. alert 창은 단 한번만 나타납니다.

// alert.js
alert("모듈이 평가되었습니다!");
// 동일한 모듈을 여러 모듈에서 가져오기
// 1.js
import `./alert.js`; // alert 창에 '모듈이 평가되었습니다!'가 출력됩니다.

// 2.js
import `./alert.js`; // 아무 일도 발생하지 않습니다.

실무에선 최상위 레벨 모듈을 대개 초기화나 내부 데이터 구조를 만들 때 사용합니다. 이것들을 내보내 재사용하는 것이죠

3. 모듈 내보내고 가져오기

export와 import와 지시자는 다양한 방식으로 활용됩니다.

선언부 앞에 export 붙이기

// 배열 내보내기
export let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', '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}; // 두 함수를 내보냅니다.

import

무언갈 가져오고 싶다면 아래와 같이 이에 대한 목록을 만들어 import {...} 안에 명시해주면 됩니다.

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

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

가져올 것이 많으면 import * as 처럼 객체 형태로 원하는 것들을 가져올 수 있습니다.

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

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

위 예시처럼 한꺼번에 모든 걸 가져오는 방식을 사용하면 코드가 짧아집니다. 그런데도 어떤 걸 가져올 땐 그 대상을 구체적으로 명시하는게 좋습니다.

이렇게 하는 데는 몇 가지 이유가 있습니다.

  • 웹팩(webpack)과 같은 모던 빌드 툴은 로딩 속도를 높이기 위해 모듈들을 한데 모으는 번들링과 최적화를 수행합니다. 이 과정에서 사용하지 않는 리소스가 삭제 되기도 합니다.

아래와 같이 프로젝트에 서드파티 라이브러리인 say.js를 도입하였다 가정합시다. 이 라이브러리엔 수 많은 함수가 있습니다.

// say.js
export function sayHi() { ... }
export function sayBye() { ... }
export function becomeSilent() {...}

현재로선 say.js의 수 많은 함수 중 단 하나만 필요하기 때문에, 이 함수만 가져와 보겠습니다.

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

빌드 툴은 실제 사용되는 함수가 무엇인지 파악해, 그렇지 않는 함수는 최종 번들링 결과물에 포함하지 않습니다. 이 과정에서 불필요한 코드가 제거되기 때문에 빌드 결과물의 크기가 작아집니다. 이런 최적화 과정은 가지치기(tree-shaking)이라고 불립니다.

어떤 걸 가지고 올지 명시하면 이름을 간결하게 써줄 수 있습니다. say.sayHi() 보다 sayHi()가 더 간결합니다.

또한 어디서 쓰이는지 명확하기 때문에 코드 구조를 파악하기가 쉬워 리팩토링이나 유지보수에 도움이 됩니다.

import 'as'

as를 사용하면 이름을 바꿔서 모듈을 가져올 수 있습니다.
sayHi를 hi로, sayBye를 bye로 이름을 바꿔서 가져오는 예제입니다.

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

hi('Jun'); // Hello, Jun!
bye('Jun'); // Bye, Jun!

Export 'as'

export에도 as를 사용할 수 있습니다.
sayHi와 sayBye를 각각 hi와 bye로 이름을 바꿔 내보내 봅시다.

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

이제 다른 모듈에서 이 함수들을 가져올 때 이름은 hi와 bye가 됩니다.

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

say.hi('Jun'); // Hello, Jun!
say.bye('Jun'); // Bye, Jun!

export default

모듈은 크게 두 종류로 나뉩니다.

  1. 복수의 함수가 있는 라이브러리 형태의 모듈 (위 예시의 say.js)
  2. 개체 하나만 선언되어 있는 모듈 (아래의 user.js, class User 하나만 내보내기 함)

대개는 두 번째 방식으로 모듈을 만드는걸 선호하기 때문에 함수, 클래스, 변수 등의 개체는 전용 모듈 안에 구현됩니다.

그런데 이렇게 모듈을 만들다 보면 자연스레 파일 개수가 많아질 수 밖에 없습니다. 그렇더라도 모듈 이름을 잘 지어주고, 폴더에 파일을 잘 나눠 프로젝트를 구성하면 코드 탐색이 어렵지 않으므로 이는 전혀 문제가 되지 않습니다.

모듈은 export default라는 특별한 문법을 지원합니다. export default를 사용하면 해당 모듈엔 개체가 하나만 있다는 사실을 명확히 나타낼 수 있습니다.

내보내고자 하는 개체 앞에 export default를 붙여보겠습니다.

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

파일 하나엔 대개 export default가 하나만 있습니다.
이렇게 default를 붙여서 모듈을 내보내면 중괄호 {} 없이 모듈을 가져올 수 있습니다.
참고로 모듈에는 오직 하나의 export default를 사용해서 내보낼 수 있고, 그외에 내보내기 하면 가져오려는 모듈에서는 반드시 {...}로 가져와야 합니다.

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

new User('Jun');

4. 동적으로 모듈 가져오기

위에서 배운 export나 import 문은 정적인 방식입니다. 문법이 단순하고 제약사항이 있습니다. 첫 번째 제약은 import문에 동적 매개변수를 사용할 수 없다는 것이었습니다. 모듈 경로엔 원시 문자열만 들어갈 수 있기 때문에 함수 호출 결괏값을 경로로 쓰는 것이 불가능했습니다.

import ... from getModuleName(); // 모듈 경로는 문자열만 허용되기 때문에 에러가 발생합니다.

두 번째 제약은 런타임이나 조건부로 모듈을 불러올 수 없다는 점이었습니다.

if (...) {
    import ...; // 모듈을 조건부로 불러올 수 없으므로 에러 발생
}

{
    import ...; // import 문은 블록 안에 올 수 없으므로 에러 발생
}

이런 제약사항이 만들어진 이유는 import / expor는 코드 구조의 중심을 잡아주는 역할을 하기 때문입니다. 코드 구조를 분석해 모듈을 한데 모아 번들링하고, 사용하지 않는 모듈은 제거해야 하는데, 코드 구조가 간단하고 고정되어있을 때만 이런 작업이 가능합니다.

참조 사이트: https://ko.javascript.info/modules-intro

0개의 댓글