자바스크립트 완벽 가이드 10장 정리 ( module )
포스트
취소

자바스크립트 완벽 가이드 10장 정리 ( module )

해당 포스트는 자바스크립트 완벽 가이드라는 교재로 스터디를 하면서 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.jsrequire()함수도 비슷하게 동작한다고 하네요. )

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 즉, CommonJsmodule.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

  1. export default로 내보는 것은 하나만 가능
  2. 선언과 동시에 export로 내보낼 수 있음
  3. 선언 이후에 내보낼 때는 (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

  1. export default로 내보내면 다른 이름으로 바꿔서 받을 수 있음
  2. 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>
  );
}

📮 레퍼런스

  1. « 자바스크립트 완벽 가이드 10장 » ( 데이비드 플래너건 지음, 한성용 옮김, 인사이트, 2022 )
  2. 1-blue - 클로저
  3. 내보내기 예시 ( GitHub ), 가져오기 예시 ( GitHub )
  4. React 공식 문서 - 코드 스플리팅
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

이펙티브 타입스크립트 5장 ( Item 38 ~ 44 )

자바스크립트 완벽 가이드 12장 정리 ( Iterator & Generator )