목차
자바스크립트로 웹사이트를 개발하다 보면 규모가 점점 커져서 파일을 분리하고 싶은 욕구가 생길 때가 종종 있습니다.
그래서 파일을 기능별로 분리시킵니다. 이럴 때 각각의 파일 하나를 모듈이라고 부릅니다.
공부를 하다보면 모듈을 내보내고 가져오는 예제를 자주 만나고는 합니다. 그런데 예제마다 가져오는 방식이 다릅니다.
그 차이는 무엇이고 모듈이란 무엇인지 알아보겠습니다.
아래 코드부터 보겠습니다.
// import로 가져오는 경우도 있습니다.
import {example} from 'example.js';
// require로 가져오는 경우도 있습니다.
const example = require('example.js');
이 둘의 차이가 무엇이길래 가져오는 방식이 다를까요? 이것을 알려면 우선 역사를 조금 알아야할 필요가 있을 것 같습니다.
모듈이 생긴 과정(헷갈리는 모듈 사용법 설명)
자바스크립트는 처음에 개발할 때는 웹페이지의 부가적인 기능을 담당하는 역할로 만들어졌기에 그 한계로 인해서 다른 언어에 비해서 지원하지 않는 부분이 많았습니다. 그런데 규모가 점점 커지면서 파일을 여러개로 분리하고 싶은 욕구가 만들어지기 시작했고 그 결과로 CommonJS, AMD와 같은 모듈 라이브러리가 생기기 시작했습니다.
서버 작업을 할 수 있는 Node.js에서는 모듈 시스템을 CommonJS를 채택하였고 이 방식을 따르고 있습니다.
CommonJS의 모듈 내보내기 불러오기
CommonJS에서 사용하는 모듈 방식은 아래와 같습니다.
// CommonJS 에서 모듈 불러오기 방식
// a.js
const example = require('example');
console.log(example); // { name: 'hello', age: 20 }
// example.js
const example = {
name: 'hello',
age: 20
}
// CommonJS에서 모듈 내보내기 방식
module.exports = example;
이런 상황에서 ES6에서 모듈시스템이 등록되었고 Node.js와 주요 브라우저에서 지원하게 되었습니다.
모던 자바스크립트에서 쓰이는 모듈은 아래와 같습니다.
ES6에서 모듈 내보내기 불러오기
// a.js
// ES 모듈 불러오기
import {example} from 'example.js';
console.log(example); // {name: 'hello', age: 20}
// example.js
// ES 모듈 내보내기
export const example = {
name: 'hello',
age: 20
}
하지만 이 코드를 작성하고 브라우저에서 열어본다면 실행이 안될 수 있습니다. 브라우저에서 확인하기 위해서는 어떻게 해야할까요? 아래 코드를 봐주시면 감사합니다.
// index.html
<html>
<head></head>
<body>
<!-- 브라우저에서 모듈을 사용하기 위해서는 type="module"같은 속성을
할당해서 브라우저에게 해당 스크립트는 모듈이야~ 라고 말해줘야합니다. -->
<script type="module" src="example.js"></script>
<script type="module" src="a.js"></script>
</body>
</html>
위에 주석에 달아놓은 것 처럼 브라우저에게 해당 스크립트가 모듈임을 알려야합니다.
브라우저를 통해서 열었는데 에러가 난다면?
-> 로컬에서 파일을 열어서 그런 것일 수 있습니다. 모듈은 file:// 프로토콜을 통해서 열게 되면 import, export가 작동하지 않습니다.
저는 vsCode에디터를 사용하는데 플러그인 중에 Live Server 라는 것이 있습니다. 그것을 설치해서 html파일을 열면 잘 작동할 것입니다. (모듈은 http or https 프로토콜을 통해서만 작동합니다.)
ES방식 모듈 내보내기, 가져오기
위에서 내보내고 가져오는 방법에 대한 간단한 코드를 보셨을 것입니다. 하지만 불러오고 내보내는 방식에 여러가지가 있어서 위에서 설명한 방법을 제외하고 코드를 보여드리겠습니다.
// 하나의 객체에 담아서 한번에 내보내기 (a.js)
const example = {
name: 'hello'
}
const age = 20;
function printString(str){
console.log(str);
}
export {example, age, printString};
// 가져오기 (b.js)
import {example, age, printString} from './a.js';
console.log(example); // {name: 'hello'}
console.log(age); // 20
printString('모듈 가져오기') // 모듈 가져오기
// 이름을 변경하면서 가져오기 (b.js)
import {example as Example, age as AGE, printString as 문자열찍기함수} from './a.js';
console.log(Example); // {name: 'hello'}
console.log(AGE);// 20
문자열찍기함수('모듈 가져오기') // 모듈 가져오기
이렇게 구조 분해할당으로 하나씩 꺼내올 수 있고 이름을 변경하면서도 가져올 수 있습니다.
내보낸 객체를 한번에 받는 것도 아래와 같이 가능합니다.
// 하나의 객체에 담아서 한번에 내보내기 (a.js)
const example = {
name: 'hello'
}
const age = 20;
function printString(str){
console.log(str);
}
export {example, age, printString};
// 한번에 가져오기 (b.js)
import * as myObj from './a.js';
console.log(myObj.example); // {name: 'hello'}
console.log(myObj.age); // 20
myObj.printString('모듈 가져오기') // 모듈 가져오기
또한 모듈에서 하나만 내보낼 때는 default를 사용할 수 있습니다.
주의할 점은 const, let, var는 default 키워드 사용시 에러가 납니다.
// 에러가 나는 상황
export default const a = 10;
export default let myFunc = () => {
console.log('hello');
}
export default var b = 10;
// 에러 안나게 변경하려면?
const a = 10;
export default a;
let myFunc = () => {
console.log('hello');
}
export default myFunc;
export default는 파일하나 즉 모듈에서 하나만을 내보낼 때 사용이 가능합니다. 그러면 받아올 때는 어떻게 받아올까요?
위에서 export로 내보냈을 때는 내보낸 변수, 함수의 이름을 그대로 가져와야 했습니다.
하지만 export default로 내보낼 때는 이름을 아무거나로 해도 가져올 수 있습니다.
// a.js (export default로 내보내기)
const myFunc = () => {
console.log('hello');
}
export default myFunc;
// b.js (가져오기)
import 아무거나 from './a.js';
아무거나(); // hello
제 의견으로는 코드의 해석이 쉽게 되도록 이름을 설정하는게 좋아보입니다.
사용법을 간략히 설명하였으니 이제 개념에 대해서 설명하겠습니다.
ES모듈 개념
모듈은 하나의 파일을 말합니다. 스크립트 하나는 모듈입니다.
그러면 파일을 분리할 때 모듈과 일반 스크립트의 차이점에는 어떤 것이 있을지 보겠습니다.
일반 스크립트 | 모듈 |
엄격모드가 아니다. (use strict를 선언시 가능) | 기본적으로 엄격모드이다. |
스코프가 전역이라 외부에서 해당 스크립트에 대한 접근이 가능하다. | 자신만의 스코프가 존재하여 외부에서 접근이 불가능 합니다. |
호출할 때 마다 평가가 계속 된다. 같은 외부파일을 불러오면 불러온 수 만큼 실행이 됩니다. | 모듈이 여러곳에서 사용되어도 최초 호출시 단 한번만 평가가 됩니다. 모듈이 실행된 결과는 이 모듈을 불러오는 곳에 내보내집니다. |
동기적으로 실행됩니다. 해당 스크립트를 마주치면 로드할 때 까지 HTML파싱이 멈춥니다. | 지연실행이 됩니다. (defer 속성이 붙은것 처럼 실행됩니다.) HTML 문서가 완전히 만들어지고나서 실행이 된다는 것입니다. |
script태그에 async속성은 외부 스크립트를 불러올 때만 가능합니다. | script태그에 async속성을 적용할 때 인라인에 적용하는 것이 가능합니다. |
위의 표에 대한 추가적으로 설명이 필요할 것 같아 아래에 자세한 설명을 붙이겠습니다.
모듈은 기본적으로 엄격모드이다.
자바스크립트는 문법이 매우 유연한 언어이기 때문에 엄격모드를 실행시키게 되면 생각치 못한 에러를 발견하고는 합니다.
모듈이 엄격모드라는 근거를 코드를 통해서 보겠습니다.
// example.js(모듈)
x = 10;
console.log(x); // Uncaught ReferenceError: x is not defined
// a.js(일반 스크립트)
x = 10;
console.log(x); // 10
// a.js (일반 스크립트에 엄격모드 적용)
"use strict"
x = 10;
console.log(x); // Uncaught ReferenceError: x is not defined
문법을 지키지 않으면 엄격모드에서는 에러를 뿜는것을 볼 수 있습니다. 마찬가지로 모듈로서 작동하는 스크립트파일에서도 엄격모드를 따로 적용하지 않아도 같은 에러를 뿜는 것을 볼 수 있습니다.
모듈은 자신만의 스코프가 존재하여 외부에서 접근이 불가능 합니다.
일반 스크립트의 경우에는 스코프가 전역이라서 아래와 같은 코드가 정상적으로 동작합니다.
// a.js
var x = 10;
<!-- index.html -->
<html>
<head></head>
<body>
<script src="a.js"></script>
<script>
console.log(window.x); // 10
</script>
</body>
</html>
하지만 모듈의 경우는 자신만의 스코프를 가지고 있기에 같은 코드를 작성하면 원하는대로 동작이 되지 않습니다.
<!-- index.html -->
<html>
<head></head>
<body>
<script type="module" src="a.js"></script>
<script>
console.log(window.x); // undefined
</script>
</body>
</html>
모듈은 여러곳에서 실행해도 한번만 평가된다.
간단한 코드를 통해서 확인해보겠습니다.
var x = 10;
console.log(x);
<!-- index.html -->
<html>
<head></head>
<body>
<script type="module" src="a.js"></script>
<script type="module" src="a.js"></script>
<script type="module" src="a.js"></script>
<script type="module" src="a.js"></script>
<script type="module" src="a.js"></script>
</body>
</html>
결과를 예상해본다면 console에 5개의 10이 찍혀야 할 것 같습니다. 하지만 모듈은 한번만 평가되기에 결과는 아래와 같습니다.
그러면 일반 스크립트로 같은 코드를 작성한다면 어떻게 될까요?
5번이 평가된 모습을 볼 수 있습니다. const 로 선언했을 때는 이미 x가 존재하므로 에러가 나오게 되어서 var로 예시를 들었습니다.
지연실행이 됩니다.
지연실행이 된다는것을 알아내기 위해서는 자바스크립트에서 DOM을 이용하면 될 것 같습니다. 우선 일반 스크립트로 예시를 들어보겠습니다.
<script>
const elem = document.querySelector('h1');
console.log(elem.innerHTML);
//Uncaught TypeError: Cannot read properties of null (reading 'innerHTML')
</script>
<h1>모듈</h1>
위의 스크립트 태그를 만나게 되면 HTML 파싱을 멈추고 스크립트 태그가 다 로드가 되고나서 html 파싱을 하기 때문에 스크립트 태그가 실행될 때는 h1이라는 태그가 없습니다.
하지만 모듈은 지연실행이 되기 때문에 스크립트 태그를 만나도 백그라운드에서 로드를 진행하고 HTML 파싱을 멈추지 않고,
HTML파싱이 끝나면 그때 비로소 실행하게 됩니다.
<script type="module">
const elem = document.querySelector('h1');
console.log(elem.innerHTML); // 모듈
</script>
<h1>모듈</h1>
script태그에 async속성을 적용할 때 인라인에 적용하는 것이 가능합니다.
script태그에 async속성을 부여하게 되면 스크립트가 나머지 페이지와는 비동기적으로 실행됨을 나타내며, 브라우저가 페이지를 파싱하는 동안에도 스크립트가 사용가능해지면 곧바로 실행됩니다.
일반 스크립트에서는 async속성은 외부 스크립트에만 사용이 가능합니다. 하지만 모듈은 인라인이 가능합니다.
모듈을 사용할 때 TIP
제가 여태까지 예시를 든 것은 확장자가 .js로 끝나는 파일을 모듈로 하였습니다.
하지만 구글에서는 .mjs를 권장하고 있습니다.
그 이유는 무엇일까요?
1. .mjs 파일만 보고도 모듈임을 알 수 있습니다.
2. node.js의 실험적인 모듈 기능은 .js에서는 지원하지 않습니다. .mjs에서만 지원합니다.
그리고 실무에 가까운 프로젝트를 하게 된다면 스크립트에 type="module"을 사용해서 하기보다는 모듈 번들러를 사용할 것입니다.
저는 여러분들에게 '웹팩'이라는 것을 공부해보길 추천합니다. 공식문서가 번역도 잘되어 있으니 읽어보시면 도움이 될 것입니다.
긴 글 읽어주셔서 감사합니다. 다음에는 테스트 코드에 대한 설명을 공부해서 돌아오겠습니다.
'자바스크립트' 카테고리의 다른 글
react-query v3 에서 v4 (tanstack/react-query)로 올릴 때 꼭 주의해야할 사항! (+ 해결방안) (0) | 2023.01.02 |
---|---|
자바스크립트 문자열 앞 뒤에 원하는 길이만큼 문자 추가하기(padEnd, padStart) (0) | 2022.04.24 |
옵셔널 체이닝(optional chaining)의 개념 및 장점을 알아보자 (0) | 2022.04.21 |
자바스크립트 nullish 연산자 (OR 연산자와 차이점) (0) | 2022.04.16 |