해당 포스트는 자바스크립트 완벽 가이드라는 교재로 스터디를 하면서 10장을 정리한 포스트입니다.
주관적으로 해석한 내용이 들어가 있어서 잘못된 내용이 포함될 수 있습니다.
또한 교재의 모든 내용을 정리하지 않고 주관적인 판단에 의해 필요한 내용만 작성했습니다.
📑 JavaScript 모듈화의 역사
모듈화란 큰 프로그램을 코드 모듈로 분리해서 작성하는 것을 말합니다.
각각의 파일마다 네임 스페이스를 유지하고 독립적으로 존재하도록 합니다.
먼저 Node.js
에서 CommonJS
라는 require()
기반의 클로저 모듈화 시스템을 도입했습니다.
하지만 JavaScript
의 표준으로 받아들여지지 못했고, ES6
에서 import
, export
를 이용한 모듈 시스템이 도입되었습니다.
( JavaScript
의 대부분의 모듈은 번들링 도구에 의존하고 있다고 합니다. )
⏳ 클로저 기반의 모듈 시스템
0️⃣ 클로저를 이용한 모듈화 예시
클로저를 이용해서 모듈화를 하는 예시입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const myModule = (() => {
// 모듈 내부에 숨겨진 값
const innerValue = 10;
const innerFunc = (...args) => args.reduce((acc, curr) => acc + curr);
// 모듈 외부로 보내질 값
const sum1To10 = () =>
innerFunc(
...Array(innerValue)
.fill(null)
.map((v, i) => i)
);
return { sum1To10 };
})();
const { sum1To10 } = myModule;
console.log(sum1To10());
// 접근 불가능
sum1To10.innerValue;
sum1To10.innerFunc;
1️⃣ 클로저를 이용한 자동 모듈화 예시
아래 코드는 webpack
같은 번들링 도구가 하는 일을 간단하게 요약한 코드입니다.
( 실제 Node.js
의 require()
함수도 비슷하게 동작한다고 하네요. )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const myModules = {};
const myRequire = (moduleNmae) => myModules[moduleNmae];
myModules["a.js"] = (() => {
// a.js 파일에 있는 내용이라고 가정
const myExports = {};
myExports.x = 10;
return myExports;
})();
myModules["b.js"] = (() => {
// b.js 파일에 있는 내용이라고 가정
const myExports = {};
myExports.func = () => 10;
return myExports;
})();
const a = myRequire("a.js");
const { func } = myRequire("b.js");
console.log(a.x); // 10
console.log(func()); // 10
⌚ Node.js의 모듈 시스템
Node
에서의 각 파일은 독립적인 namespace
를 가진 비공개 모듈입니다.
즉, 내보내지 않은 값들은 모두 비공개로 외부에서 접근할 수 없습니다.
0️⃣ 네임스페이스 ( namespace )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 아래는 "TypeScript" 문법
namespace app1 {
export const v = 10;
}
namespace app2 {
// "x"는 내보내지 않았기 때문에 접근할 방법이 없고 내부에서만 사용이 가능
const x = 1090;
export const v = 10 + x;
}
console.log(app1.v); // 10
console.log(app2.v); // 1100
1️⃣ Node의 내보내기와 가져오기
Node.js
즉, CommonJs
는 module.exports
를 사용해서 파일을 내보내고 가져옵니다.
기본적으로 상대 경로를 이용해서 다른 파일(모듈)에 접근((1)
)합니다.
하지만 기본 내장 모듈과 설치된 모듈은 절대 경로로 접근((2)
)할 수 있습니다.
그리고 확장자를 굳이 작성할 필요 없지만 명시적으로 작성하는 경우가 더 많다고 합니다.
a.js
1
2
3
const x = 10;
module.exports = x;
b.js
1
2
3
4
5
const func = () => 10;
module.exports = {
func,
};
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// (1)
const x = require("./a.js");
const { func } = require("./b");
console.log(x); // 10
console.log(func()); // 10
// (2) 내장 모듈
const http = require("http");
http
.createServer((request, response) => {
console.log("요청옴");
response.end("응답");
})
.listen(3000);
// (2) "npm"으로 설치한 모듈
const express = require("express");
const app = express();
app.get("/", (req, res) => res.json("응답"));
app.listen("3001", () => console.log("listen: 3001"));
⏰ ES6의 모듈 시스템
CommonJS
방식처럼 각 파일이 모듈이고, 내보내지 않는 한 외부에서 사용할 수 없으며, 내보내면 다른 파일에서 가져와 사용할 수 있습니다.
하지만 내보내는 방식과 가져오는 방식에서의 차이가 있기 때문에 그 방식에 대해서 알아보겠습니다.
import
로 가져온 경우에는 항상 최상위로 끌어올려져서 실행됩니다.
또한 클래스, 함수, 루프, 조건문 내부에서 실행할 수 없습니다.
그리고 따로 설정을 해주지 않았다면 상대 경로를 사용해야합니다.
0️⃣ Node에서 ES6의 모듈 시스템 사용하기
package.json
1
2
3
4
{
/* ... 나머지 생략 ... */
"type": "module"
}
a.js
1
2
3
const x = 10;
export { x };
b.js
1
2
3
const func = () => 10;
export { func };
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 여기서는 확장자를 생략하면 오류가 납니다... 🥲 ( 브라우저에서 import가 작동하는 방식과 맞추기 위해서 의도적으로 설계된 부분이라고 합니다... 🖐️ )
import { x } from "./a.js";
import { func } from "./b.js";
console.log(x); // 10
console.log(func()); // 10
// 내장 모듈
import { createServer } from "http";
createServer((request, response) => {
console.log("요청옴");
response.end("응답");
}).listen(3000);
// "npm"으로 설치한 모듈
import express from "express";
const app = express();
app.get("/", (req, res) => res.json("응답"));
app.listen("3001", () => console.log("listen: 3001"));
1️⃣ ES6의 모듈 시스템 알아보기
CRA
로 생성한 React.js
에서 사용하는 방법에 대해 알아보겠습니다.
1. export
export default
로 내보는 것은 하나만 가능- 선언과 동시에
export
로 내보낼 수 있음 - 선언 이후에 내보낼 때는
(3)
처럼 사용하고, 내보낼 때as
를 이용해서 이름을 바꿀 수 있음
MyComponent.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 가져오기
import React, { useState } from "react";
const MyComponent = () => {
React.useEffect(() => {
console.log("MyComponent 렌더링");
}, []);
const [value, setValue] = useState(0);
return (
<>
<h1>Hello, React.js {value}</h1>
</>
)
}
// (2)
export const MyComponent2 = () => {
return (
<>
<span>...</span>
</>
)
}
// (1) 내보내기
export default MyComponent;
// (1) export default를 두 개 사용하는 것은 안됨
// export default MyComponent2;
const v = 10;
// (3) export로 여러 개 내보내기는 가능
export { v as x };
2. import
export default
로 내보내면 다른 이름으로 바꿔서 받을 수 있음export
로 내보낸 경우에는{}
로 감싸서 받아야 하며as
를 통해 이름을 바꿀 수 있음
App.jsx
1
2
3
4
5
6
7
8
9
10
11
import My, { MyComponent2 as Component, x } from "./MyComponent";
const App = () => {
return (
<>
<My />
<Component />
<span>{x}</span>
</>
)
}
3. 특수한 export, import
import * as <변수명> from "<경로>"
와 같은 형식으로 입력하면 내보내기 하는 모든 모듈을 객체로 받을 수 있습니다.
import * as from ""
1
2
3
4
import * as c from "react";
c.useState(0);
c.useEffect(() => {}, [])
그냥 가져오기만 하는 경우에 import ""
를 사용할 수 있습니다.
import ""
1
2
3
// css를 포함하고 싶을 때
import "./css/reset.css"
4. 가져오면서 바로 내보내기
가져와서 바로 다시 내보내기를 할 때는 export {} from ""
을 사용합니다.
혹은 전체를 그대로 내보낼 때는 export * from ""
을 사용하고, export default
를 그대로 내보낼 때는 export { default } from ""
과 같은 형태를 사용합니다.
제가 사용하는 경우는 유사한 동작을 하는 함수를 역할별로 파일로 분리하고 그 파일들을 다시 하나의 파일에서 모아서 내보낼 때 사용합니다.
이렇게 사용하면 파일을 import
할 때 하나의 경로에서 가져오기를 할 수 있어서 경로가 깔끔해집니다.
실제로 사용했던 예시를 이용해서 설명하겠습니다.
( 내보내기 예시, 가져오기 예시 )
2️⃣ 동적으로 가져오기 ( import() )
import
를 한다고 해서 반드시 사용하는 것은 아니기 때문에 선택적으로 사용하는 경우에 import()
를 이용해서 필요한 시점에 데이터를 불러올 수 있습니다.
import()
의 반환 값은 Promise
객체이기 때문에 then()
, await
같은 방식으로 비동기 처리를 할 수 있습니다.
( import()
가 함수처럼 보이지만 사실 함수는 아니라고 합니다. )
1
2
3
4
5
6
7
8
9
10
11
12
13
// sum.js
export const sum = (...args) = args.reduce((acc, curr) => acc + curr);
// index.js
const $button = document.querySelector("button['type=button']");
$button.addEventListener("click", () => {
// 버튼을 누른 시점에 가져오고, 완료되면 실행
// 단, 파일이 커서 가져오는 데 오래 걸린다면 로딩 처리를 해주는 것이 좋음
import("./sum.js").then(({ sum }) => {
sum(1, 2, 3, 4, 5);
});
});
3️⃣ 코드 스플릿팅 ( Code Splitting )
React.js
에서 import()
를 활용해서 Code Splitting
을 사용하는 방법입니다.
React.js
자체적으로 lazy()
와 <Suspense>
를 이용할 수 있습니다.
( React 공식 문서 코드 스플릿팅 )
1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { useState, Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
📮 레퍼런스
- « 자바스크립트 완벽 가이드 10장 » ( 데이비드 플래너건 지음, 한성용 옮김, 인사이트, 2022 )
- 1-blue - 클로저
- 내보내기 예시 ( GitHub ), 가져오기 예시 ( GitHub )
- React 공식 문서 - 코드 스플리팅