Node.js의 모듈 시스템

2024. 12. 10. 23:19Programming Language/Node.js

01. Module이란?

모듈이란

필요한 함수들의 집합이라고 보면 된다.

모듈은 애플리케이션을 구성하는 기본 단위로 기능을 분리하고 재사용할 수 있게 해준다.

쉽게 말해, 모듈은 물건을 만들 때 사용하는 부품과 같다.

여러 개의 모듈을 조합하여 하나의 애플리케이션을 구성할 수 있다.

 

이 모듈은 Nodejs에서 제공해주는 모듈도 있고, 사용자가 모듈을 만들어서 사용할 수 도 있다. 

또한 다른 사용자가 만들어둔 모듈 또한 쉽게 가져다가 사용할 수 도 있다.

이 모듈을 가져와서 사용할때에는 require라는 키워드를 통해서 가져다가 사용할 수 있다.

 

모듈의 종류는 크게 나눠서 

  • Core Module - Node.js에 기본적으로 포함되어 있는 모듈로 예를 들면, http, fs, path 등이 있다.
  • Local Module - 사용자가 직접 작성한 모듈로 특정 기능을 수행하는 코드를 포함하고 있으며, 다른 파일에서 재사용할 수 있다. 이 Local Module은 exports 또는 module.exports 키워드를 사용하여 외부에 공개할 수 있다
  • Third Party Module - 다른 사용자가 만들어둔 모듈을 의미하고 이 모듈은 npm(Node Package Manager)이란 것을 사용해서 가져다 사용해야만 한다.

로 나뉜다.

 

모듈을 불러오는 방법은 

const module = require("moduleName");

과 같이 require키워드를 통해서 가져와 사용할 수 있다.

이 require 키워드를 사용하면 해당 묘듈이 있는 자바스크립트 파일을 읽고 그 파일을 통해서 객체를 반환한다.

이렇게 모듈을 가져와 변수나 상수에 할당해서 사용하면 된다.

 

Core Module

코어 모듈은  Node.js에 기본적으로 포함되어 있는 모듈로써 40개이상의 모듈이 존재한다.

물론 이는 Node.js홈페이지에서 Document에서 확인할 수 있다.

https://nodejs.org/docs/latest/api/

 

Index | Node.js v23.3.0 Documentation

 

nodejs.org

여기서 확인이 가능하다.

그리고 이건 node.js의 버전에 따라서 활용할 수 있는 모듈이 달려질 수 도 있다.(어떤 모듈은 deprecated되어서 사용을 권장하지 않는다던가... 어떤걸로 합쳐진다던가..)

 

몇가지 사용 예시를 보자면 

 

crypto

암호화 및 해싱 기능 제공하는 모듈

const crypto = require('crypto');
const hash = crypto.createHash('sha256').update('password').digest('hex');
console.log(hash);

과 같이 사용할 수 있다.

 

dns

DNS(Domain Name System)를 조회 및 관리를 해주는 모듈

const dns = require('dns');
dns.lookup('example.com', (err, address, family) => {
  console.log(`IP 주소: ${address}`);
});

 

events

위에서 봤던 모듈로 이벤트 기반 애플리케이션을 구현하기 위한 이벤트 관리 기능 제공해주는 모듈

const EventEmitter = require('events');
const emitter = new EventEmitter();
emitter.on('event', () => console.log('이벤트 발생!'));
emitter.emit('event');

 

등등이 있다.

 

이런 내용들은 원한다면 위에 있는 node.js의 document에서 확인하면서 사용이 가능하다.

예를 들어 events의 경우엔 

이렇게 왼쪽 리스트에서 Events를 누르면 

이렇게 해당 모듈에서 사용가능한 함수들에 대해서 나와 있다.

아래로 내려보면 

이렇게 사용하는 방법에 대해서 설명 해주기도 하고 해당 클래스가 갖고 있는 함수에 대해서 

이렇게 설명해주면서 사용하는 길잡이가 되기도 한다.

 

Local Module

사용자가 직접 작성한 모듈로 특정 기능을 수행하는 코드를 포함하고 있으며, 다른 파일에서 재사용할 수 있다. 이 Local Module은 exports 또는 module.exports 키워드를 사용하여 외부에 공개할 수 있다.

이 사용법에 대해서 알아보자면, 먼저 VSCode를 실행시킨 후에 자바스크립트 파일을 하나 생성해주자.

그리고 이제 이안에 모듈을 하나 만들어주자.

이 모듈은 객체를 반환하게 되는데 그 때문에 만드는 형식은 두가지가 있을 수 있다.

객체를 만들어서 객체를 반환하는 방법, 함수로 만든 다음에 이걸 객체로 반환하는 방법으로 순서의 차이만 있을뿐 결과적으로 동일하다고 볼 수 있다.

 

먼저 객체를 만들어서 객체로 반환하는 방법은 

이렇게 하나의 객체에 담아서 객체 자체를 module.exports로 내보내는 방식이다.

 

함수를 객체로 엮어서 내보내는 방식은 

와 같이 사용할 수 있다.

여기서 module.exports는 해당 내용을 모듈로 밖으로 내보내겠다는 의미로 사용된다.

 

이걸 사용해보자면, 또 자바스크립트 파일을 하나 만들고 

이렇게 require를 사용해서 전달인자로 모듈 파일을 찾아오면 이걸 변수 혹은 상수에 저장할 수 있다.

그리고 저장한 변수 혹은 상수를 통해서 해당 모듈이 갖고 있던 함수(기능)을 사용할 수 있게 된다.

이렇게 사용한 결과를 확인할 수 있다.

 

require가 해당 파일을읽어서 module.exports = 의 값을 반환값으로 반환된 그 객체를 사용하는 것이다.

 

 

서드 파티 모듈의 경우는 진행해가면서 사용해보자.

 

02. HTTP 서버 구축하기

core module 중 http라는 모듈을 사용해서 HTTP 서버를 한번 구축해보자.

 

먼저 http 모듈을 https라는 이름의 변수로 가져오자.

그러면 이제 http라는 변수를 통해서 http모듈에서 제공하는 기능들을 사용할 수 있게된다.

 

http에는 createServer라는 것이 있는데 이 createServer() 함수는 요청을 처리할 함수를 인자로 받아서 HTTP 서버를 생성한다.
이 createServer함수의 기본적인 형태는 

와 같이 되어 있는데 

 

먼저 콜백으로 전달 받는 req와 res에 대해서 보자면 

 

req - 요청객체

req 객체는 클라이언트가 서버로 보낸 HTTP 요청에 대한 정보를 담고 있다

자주 사용하는 속성은

  • req.url: 요청한 URL (예: /, /about)
  • req.method: 요청의 HTTP 메서드 (GET, POST 등)
  • req.headers: 요청 헤더 정보
  • req.query: URL 쿼리 파라미터 (예: ?name=John)

 

res - 응답객체

res 객체는 서버가 클라이언트에게 보낼 응답을 작성하는 데 사용된다.

자주 사용하는 메서드는

  • res.writeHead(statusCode, headers): 응답 상태 코드와 헤더를 설정
  • res.end(data): 응답 본문을 전송하고 연결을 종료
  • res.write(data): 응답 본문에 데이터를 추가 (여러 번 사용 가능)

로 구성된다.

 

우리는 createServer 함수의 콜백 함수 내부에 어떤 요청이 어떻게 들어왔을때 어떻게 처리할지에 대해서 작성해주면 된다.

 

에를 들어 간단히 보자면 /라는 url로 들어오는 요청에는 상태 코드는 200을 컨텐츠 타입은 mime 타입으로 text/html를, 마지막으로 Hello, Stranger를 p태그로 반환하도록 하자.

그리고 /node 라는 url로 들어오는 요청에는 상태 코드는 200을 컨텐츠 타입은 mime 타입으로 text/html를, 마지막으로 Hello, Node Student 를 p태그로 반환하도록 해준다면

이렇게 구현할 수 있다.

 

**여기서 상태코드 200은 기본적으로 정상적으로 잘 통신되었음을 의미하고 Content-Type은 서버가 클라이언트에게 어떤 형식으로 데이터를 전달할 것인지를 브라우저에게 알려주는 역할을 한다.

 

다음은 이렇게 생성된 서버를 어떤 포트에 지정할 지 변수에 담아주고

 

이제 서버를 실행시키는 listen이란 함수를 사용할 것인데 이 listen() 함수는 http 모듈의 createServer() 함수가 반환하는 객체에 속하는 메서드로써 서버를 실행시켜주는 역할을 한다.

이 listen 함수의 기본적인 모양은 

server.listen(port[, host[, backlog[, callback]]])

으로 매개변수의 순서대로

port (필수) - 서버가 요청을 받을 포트 번호를 설정한다. port의 예로는 3000, 8080 등이 있다.

host (선택) - 서버가 리스닝할 호스트 주소를 설정한다. 기본값은 localhost이다. 예로는 localhost, 0.0.0.0, ::(IPv6)가 있다.

backlog (선택) - 대기열의 크기를 지정한다. 이 값은 서버가 처리할 수 있는 최대 연결 대기 수를 설정한다. 대기 중인 연결이 이 숫자를 초과하면 새로운 연결이 거부한다. 기본값은 시스템에 따라 다르다.

callback (선택) - 서버가 성공적으로 시작되었을 때 실행될 콜백 함수이다. 서버가 포트에서 요청을 대기 시작하면 호출된다. 예를 들어, 서버가 정상적으로 실행되고 있다는 메시지를 출력하는 데 사용된다.

 

를 넣어준다.

여기서 첫번째만 넣고 콜백 함수를 넣으면 host와 backlog는 그냥 기본 값으로 설정된다.

이렇게 사용하는 것을 말하는 것이다.

 

이렇게 하고 실행해보면 

이렇게 내가 서버를 시작할때 보여줄 콘솔이 출력 되고 브라우저를 통해서 localhost:3000/으로 접근하면 

이렇게 Hello, Stranger! 가 출력되는 것을 볼 수 있다.

그러면 /node로 접근하면 

이렇게 Hello, Node Student! 가 출력되는 것을 볼 수 있다.

 

이렇게 간단하게 HTTP 서버를 생성하는 방법을 알아봤다.

 

03. 모듈을 사용하는 이유

재사용성이 좋고, 관련 있는 코드를 모아 둘 수 있고 필요한 부분(함수나 코드)만 가져와서 사용할 수 있다.

 

04. 모듈 생성하기

대부분의 모듈은 만들어진걸 가지고 활용하지만 직접만들어서 사용하는 것도 가능하다.

여기선 HTTPS 모듈을(이미 존재하는 모듈이긴 가지만) 직접 만들어보도록 하자.

 

강의에선 https 모듈이라고 했지만 그냥 단순하게 어떤 모듈을 하나 만드는 과정을 보여주고자 하는듯 하다.

이 내용을 내 해석으로 필요한 부분만 가져다가 그려보자면 먼저 https.js라는 파일에서 요청과 응답을 받을 것이고 응답받은 내용을 출력할 것이다.

그리고 request.js에서는 이 요청을 처리해서 반환하고 이걸 response에서 받아서 응답으로 만들어 보내주는 코드를 하나 만들어보자.

 

먼저 https.js에서는 sendAPI라는 함수를 하나 생성해주자.

그리고 내부에서 먼저 request.js를 통해서 함수를 불러와 url과 파라미터를 보내서 요청을 처리하게 할것이다.

그러기 위헤서 먼저 request.js라는 파일을 하나 생성해주고 getRequest라는 함수를 하나 생성해주고 매개변수로 url과 파라미터를 받아 주고

받은 매개변수들을 객체로 만들어준 다음에 sendToResponse라는 함수를 만들어서

객체를 전달하고 그 내부에서 response.js에게 전달하는 코드를 작성하자.

물론 여기 내부에서 사용할 함수는 아직 없기 때문에 먼저 response.js를 생성해주자.

response.js에서는 request에서 getResponse라는 함수를 하나 생성해주고

매개변수로 객체를 받아서 문자열로 만들어 반환해주도록 하자.

이제 response.js부터 순서대로 모듈에서 해당 함수를 밖으로 내보내 줘야지만 request에서는 response를 https.js에서는 request.js에서 함수를 가져다 사용할 수 있게 된다.

이때 사용하는 것이 module이라는 객체인데 ,module을 console으로 출력해보면

그 출력으로 

이런 객체가 나오는데 여기서 exports라는 부분이 해당 모듈에서 어떤걸 내보낼지를 넣어주는 부분이다.

이걸 사용할때는 module.exports = {함수명:함수} 과 같이 사용하는데 response의 경우는 getResponse라는 함수를 객체로 내보내도록 만들어주면 된다.

이때 함수명은 외부 모듈에서 해당 함수를 어떤 이름으로 가져다 사용할지를 지정한다.

이떄 함수명:함수 가 아니라 이렇게 그냥 함수만 전달하면 함수명도 getResponse로 지정된다.

함수명:함수라고 하면 헷갈릴 수 있는데 어떻게 전달해야 의미가 정확하게 전달될지 몰라 그냥 함수명 : 함수라고 말했지만 단순하게 

이렇게 해당 함수에 대한 key값을 지정하는 것을 말하는 것이다.

이걸

{
    getResponse
}

==

{
    getResponse : getResponse
}

로 생각해주면 된다.

 

아무튼 이렇게 exports하고나서 module을 콘솔로 찍어보면

이렇게 exports에 채워져 있는걸 확인할 수 있다.

 

이제 이걸 request에서 가져다 사용해보자면 request.js의 가장 상단 부분에 require을 통해 response의 파일의 path를 전달해주면서 변수 혹은 상수에 담으면

이 변수 혹은 상수를 통해서 response.js에서 exports한 함수를 사용할 수 있게 된다.

이때 왜 확장자명을 안붙이는 이유는, require를 console로 출력해보면 알 수 있다.

이렇게 require에 있는 extensions에서 가장 먼저 js에서 맞는 파일을 찾고 없다면 json을 찾고 그다음에 .node를 붙여서 파일을 찾는다.

그렇기 때문에 가장 먼저 .js파일을 찾아서 자동으로 response.js로 맞는 파일이 있다면 찾아와서 안붙여줘도 사용엔 지장이 없다.

 

아무튼 이렇게 모듈을 불러온 다음에 

sendToResponse에서 response에서 만들어뒀던 getResponse를 호출해서 객체를 전달해주면서 반환값을 return 해주자.

 

그리고 이 request.js도 https.js에서 불러올 모듈이기 때문에 이 또한 exports해주자.

이제 https.js의 가장 상단부에 request를 require를 통해서 호출하고 

sendAPI함수에서 req의 getRequest함수에 url과 param을 전달해주자

그리고 이걸 console을 통해서 호출하면서 전달인자로 임의의 데이터를 한번 넣어 사용해보자.

이제 저장한 후에 node https.js라는 명령어를 터미널에 입력해서 실행시켜보자.

이렇게 request를 타고 response에서 만든 결과를 받아서 출력하는 모습을 볼 수있다.

 

//https.js
const req = require('./request')

const sendAPI = (url, param) => {
    return req.getRequest(url, param);
}

console.log(sendAPI("https://google.com", "인자받은겨?"));


//request.js
const res = require("./response");

const getRequest = (url, data) => {
    const response = {
        url: url,
        data: data
    };

   return sendToResponse(response)
}

const sendToResponse = (response) => {
    return res.getResponse(response)
}

module.exports = {
    getRequest
}


//response.js
const getResponse = (response) => {
    return `url ${response.url} , data: ${response.data}`;
}

module.exports = {
    getResponse
}

 

사실 코드를 보면 의미가 없는 과정이지만 그냥 모듈을 생성하고 exports 하고 모듈을 불러오는 과정들을 살펴봤다.

 

05. 모듈에서 exports하는 방법

모듈에서 exports를 하는방법은 여러가지 방법이 있다.

 

모든 방법을 나열해보자면 

module.exports.PI = 3.14

이렇게 값을 그냥 exports 할 수 도 있고 

module.exports.funcEx = function funcEx () {
    return "Exported Function funcEx";
}

와 같이 단일 함수에 대해서 exports 하는 방법도 있다.

exports.funcEx = function funcEx() {
    return "Exported Function funcEx";
}

이때는 module이라는 키워드를 생략해서 사용도 가능하다.

module.exports = function funcEx() {
    return "funcEx exported";
}

이렇게 그냥 default로 함수 하나만을 exports하는 방법도 있다.

 

각각 모듈을 불러오는 방법은 

default로 함수를 지정한 경우는 이렇게 

바로 사용도 가능하다.

 

그러나 기존에 사용했던 방식이 가장 좋은 방식이고 

이렇게 사용하는 경우는 구조 분해 할당으로 

해당 함수만 빼와서 사용하는 방법도 가능하다.

 

06. CommonJS와 ECMA script 모듈의 차이

여태까지 사용했던 모듈은 Node.js에서  기본 모듈로 사용하는 CommonJS 모듈이였다.

CommonJS 모듈은 module.exports로 모듈을 내보내고 require를 통해서 모듈을 불러와서 사용하는 방식을 사용했다.

 

그렇다면 ECMA script 모듈이란 무엇일까

 

ECMA script 모듈

ECMAScript 모듈(ESM)은 자바스크립트의 공식 모듈 시스템으로, ES6(ECMAScript 2015)에서 도입되었다.

CommonJS와 다른 특징은 export로 모듈을 내보내고 import를 통해서 모듈을 불러와서 사용한다는 점이다.

 

기존에 CommonJS의 경우는 브라우저에서는 사용할 수 없고, Node.js에서만 사용이 가능했던것과는 달리 ECMA script 모듈은 브라우저와 Node.js에서 모두 사용이 가능하다.

 

ECMA script 모듈에서 export 하는 방법은 

// math.js

function add(a, b) {
    return a + b;
}

function subtract(a, b) {
    return a - b;
}

// 여러 함수를 묶어서 export
export { add, subtract };

와 같이 사용하면 된다.

 

이것 또한 함수 각각에 export를 붙여 export 할 수 도 있다.

// math.js

export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}

 

이를 사용하는 방법은 

// app.js

import { add, subtract } from './math.js';

console.log(add(2, 3));      // 5
console.log(subtract(5, 3)); // 2

와 같이 해당 js파일에서 import를 사용해서 모듈을 불러와 사용한다.

 

브라우저에서 ECMA Script를 사용하는 방법도 있는데 이는 export된 js파일을 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ESM Example</title>
</head>
<body>
    <script type="module">
        import { add } from './math.js';
        console.log(add(2, 3));  // 5
    </script>
</body>
</html>

이렇게 불러와 사용해주면 된다.

type="module" 속성은 해당 스크립트가 ECMAScript 모듈임을 알려서 사용이 가능해진다.

 

07. Node.js에서 ECMA Script 모듈 사용하기

우리가 만들었던 Node.js의 모듈을 ECMA Script 모듈로 변경해보자.

먼저 이전에 만들었던 모듈을 복사해 ecmaS라는 폴더로 다시 만들어주자.

 

가장 먼저 https.js를 수정해주자면 

require대신에 import를 해주는 방식으로 수정하자면 export 할 request부분이 

이렇게 export 될것이기 때문에 import 할때 그 함수명을 같게 해서 받아줘야 한다.

그리고 req를 통해 getRequest함수를 사용했으나 이건 그냥 그대로 사용하면 된다.

request.js를 수정하기 전에 https.js를 실행해보면 

이렇게 에러가 발생한다.

이건 ES 모듈을 로드하려면 package.json파일에 "type":"module"로 모듈을 설정하거나 파일의 확장자를 .mjs로 수정하라는 말이다.

먼저 가장 간단한 파일확장자를 .mjs로 수정해보면

이렇게 수정하고 

request, response 모두 import와 export로 수정해주자.

이제 실행을 한번 해보면

실행이 잘 되는 모습을 볼 수 있다.

 

08. 모듈 캐싱에 대해서

Node.js에서 모듈 캐싱이란, 모듈을 require(CommonJS)나 import(ESM)로 처음 로드할 때, 그 모듈의 실행 결과를 메모리에 저장해 두고, 다음에 같은 모듈을 요청하면 저장된 값을 재사용하는 메커니즘이다.

즉, 같은 모듈은 한번만 실행되고 그 결과를 재사용 한다는 의미이다.

 

예를 들어보면 

// counter.js

let count = 0;

console.log("모듈이 실행되었습니다!");

module.exports = {
    increment: () => ++count,
    getCount: () => count,
};

모듈의 내부에 함수 2개를 선언하는데 하나는 값을 증가시키는 increment, 하나는 값을 출력만 하는 getCount 함수를 만들었다.

 

이걸 가져다가 모듈을 두번 부르면서 사용해보면

//main.js

const counter1 = require('./counter');
console.log(counter1.increment()); // 1
console.log(counter1.increment()); // 2

const counter2 = require('./counter');
console.log(counter2.getCount());  // 2 (캐싱된 결과를 사용)

이런 결과를 확인할 수 있는데, 모듈을 두번 불렀기에 counter1과 counter2는 서로 다른 모듈이라고 생각할 수 있겠지만, 값을 봤을때 증가한 값이 다른 모듈에서 불러왔을때도 동일하게 출력되는 걸  볼 수있다.

 

이렇게 동일한 모듈을 불렀을때 

const counter2 = require('./counter');

이 부분에서는 require이 실행되지 않고 counter1에 있는 내용을 그대로 사용하게 된다.

 

이 캐쉬된 정보를 확인하려면 require.cache라는 것을 console로 호출해주면 확인이 가능하다.

여기서 main.js는 어디서 require해서 가져와지지 않았기 때문에 loaded에 false가 들어가 있고 counter.js에서는 loaded가 true가 되어 있는 것을 볼  수 있다.

이 load에 true가 되어 있는게 cache되어 있다는 것이다.

 

09. index.js 에 대해서

index.js는 Node.js에서 디렉터리의 기본 진입점(entry point)으로 사용되는 파일명이다.

그 말은 Node.js가 특정 디렉터리를 require하거나 import할 때, 자동으로 찾는 파일이라는 것이다.

 

index.js는 Node.js에서 모듈을 디렉터리로 관리할 때, 디렉터리 안에 index.js 파일이 있으면 그 파일이 자동으로 로드한다

그러면 마치 디렉터리 자체가 모듈처럼 동작할 수 있게 한다.

Node.js는 디렉터리에서 어떤 파일을 로드해야 할지 명시적으로 지정하지 않으면, index.js를 기본 파일로 간주한다.

 

예를 들어 우리가 만들었던 https모듈을 예시로 들어보자면

이렇게 내부에 폴더를 하나 생성하고 그안에 request.js, response.js를 넣어준다

그리고 그 내부에 index.js파일을 하나 생성해주자.

이 index.js는 request.js, response.js를 하나로 묶어 밖으로 export할 수 있다.

이렇게 만든 모듈을 https.js에서 사용할때는 

res는 그냥 사용법을 알려주기 위해 추가해봤다. 기존과는 조금 다른 형태의 https.js이다.

이렇게 사용하던 걸 

이렇게 디렉터리를 지정해주면 index.js를 모듈로 가져온다.

 

사용할때에는 

이렇게 사용하면 된다.

 

또한 index.js는 다양한 방식으로 구현이 가능한데 특정 함수만 exports하도록도 

이렇게 구현도 가능하다.

 

이렇게 index.js를 만들면 어쩔땐 편하게 모듈을 불러와 사용할 수 있을 지 모르지만 모듈 시스템을 복잡하게 만들 수 있는데, 우선 디렉터리 이름만 보고 어떤 파일이 로드되는지 명확히 알기 어렵다.

또한 여러 디렉터리에 동일한 이름(index.js)이 존재하면 디버깅 시 혼란스러울 수 있다.

그리고 너무 많은 파일을 index.js에서 다루면 복잡성이 증가한다.

 

그렇기에 이렇게 구현하는 방식은 지양하는게 좋다고 한다.