본문 바로가기
JavaScript/Node.js

Node) 노드 기능 알아보기 - REPL, 모듈 사용하기

by 박채니 2022. 12. 27.
안녕하세요, 코린이의 코딩 학습기 채니 입니다.
[Node.js 교과서]의 책을 참고하여 포스팅한 개인 공부 내용입니다.

 

노드 기능 알아보기

 

REPL 사용하기

 

REPL

  • 입력한 코드를 읽고 (Read)
  • 해석하고 (Eval)
  • 결과물을 반환하고 (Print)
  • 종료할 때까지 반복 (Loop)

노드의 REPL을 사용하기 위해 터미널을 열어줍니다. (ctrl + `)

 

$ node

Welcome to Node.js v18.12.1.
Type ".help" for more information.

프롬프트가 > 모양으로 바뀌었다면 자바스크립트 코드를 입력할 수 있습니다.

 

> const str = 'Hello world, Hello node';
undefined
> console.log(str);
Hello world, Hello node

잘 출력되는 것을 확인할 수 있으며, REPL을 종료하려면 ctrl + c를 두 번 누르거나, .exit를 입력해 줍니다.


JS 파일 실행하기

 

helloWorld.js

function helloWorld() {
  console.log("Hello World");
  helloNode();
}

function helloNode() {
  console.log("Hello Node");
}

helloWorld();

코드를 작성하고 콘솔에서 node [자바스크립트 파일 경로]로 실행합니다.

이 때, 확장자 (.js)는 생략할 수 있습니다.

 

$ node helloWorld

Hello World
Hello Node

정상적으로 실행 되는 것을 확인할 수 있습니다.

 

* 파일/폴더 이름 제한

- 파일이나 폴더의 이름에 쓸 수 없는 문자들은 /, \, |, <, >, :, ", ?, * 등이 있습니다.

 


모듈로 만들기

 

모듈이란?

  • 특정한 기능을 하는 함수나 변수들의 집합
  • 자체로도 하나의 프로그램이면서 다른 프로그램의 부품으로도 사용할 수 있음
  • 하나의 모듈(파일 하나)을 여러 프로그램에서 재사용 가능
  • CommonJS 모듈 / ECMAScript 모듈 형식을 사용

 

CommonJS 모듈

 

표준 자바스크립트 모듈은 아니지만, 표준이 나오기 이전부터 쓰였기 때문에 널리 쓰이는 모듈입니다.

모듈 생성 시, 모듈이 될 파일과 모듈을 불러와서 사용할 파일이 필요합니다.

 

var.js

const odd = "CJS 홀수입니다.";
const even = "CJS 짝수입니다.";

module.exports = {
  odd,
  even,
};

변수 두 개를 선언하고, module.exports에 변수들을 담은 객체를 대입했습니다.

var.js는 변수들을 모은 모듈이 되었고, 다른 파일에서도 사용 가능합니다.

 

func.js

const { odd, even } = require("./var");

function checkOddOrEven(num) {
  // 홀수라면
  if (num % 2) {
    return odd;
  }
  return even;
}

module.exports = checkOddOrEven;

require() 함수 안에 불러올 모듈의 경로를 적어주었고, js 혹은 json 같은 확장자는 생략할 수 있습니다. (index.js도 생략 가능)

구조 분해 할당을 이용해 변수들을 가져온 것을 확인할 수 있으며, 아래와 같이 사용할 수도 있습니다.

const obj = require('./var');
console.log(obj.odd);
console.log(obj.even);

 

module.exports에 짝/홀을 구분하는 함수를 대입해주었으며, module.exports에는 객체 뿐만 아니라 변수 혹은 함수를 대입할 수 있습니다.

 

index.js

const { odd, even } = require("./var");
const checkNumber = require("./func"); // 함수명을 다르게 가져옴

function checkStringOddOrEven(str) {
  if (str.length % 2) {
    return odd;
  }
  return even;
}

console.log(checkNumber(10));
console.log(checkStringOddOrEven("hello"));

@콘솔출력값
$ node commonJS
CJS 짝수입니다.
CJS 홀수입니다.

var와 func 두 개의 모듈을 참조하고 있는 것을 확인할 수 있습니다.

또한, 모듈로부터 값을 불러올 때는 변수 이름을 다르게 지정할 수도 있습니다.

 

이처럼 여러 파일에 걸쳐 재사용하는 함수나 변수를 모듈로 만들면 편리하지만, 모듈이 많아지고 모듈 간의 관계가 얽히게 되면 구조를 파악하기 어렵다는 단점도 있습니다.

 

module.export가 아닌 exports 객체로도 모듈을 만들어보겠습니다.

var.js

exports.odd = "CJS 홀수입니다.";
exports.even = "CJS 짝수입니다.";

각 변수들을 exports 객체에 하나씩 넣었으며, 이 또한 잘 실행됩니다.

module.exports와 exports는 같은 객체를 참조하기 때문입니다. 따라서 module.exports에도 해당 변수들이 들어가 있습니다.

 

단!

exports에는 반드시 객체처럼 속성명과 속성값을 대입해야 함. (함수 불가)

exports와 module.exports 관계

 

require 더 파해치기

require는 함수(객체)이기 때문에 속성을 여러가지 가지고 있습니다.

 

require.js

console.log("require가 가장 위에 오지 않아도 됨");

module.exports = "저를 찾아보세요~";

require("./var");

console.log("require.cache입니다.");
console.log(require.cache);
console.log("require.main입니다.");
console.log(require.main);
console.log(require.main === module);
console.log(require.main.filename);

@콘솔출력값
$ node commonJS/require

require가 가장 위에 오지 않아도 됨
require.cache입니다.
[Object: null prototype] {
  '/Users/parkchaeeun/project/playground-cepark/commonJS/require.js': Module {
    id: '.',
    path: '/Users/parkchaeeun/project/playground-cepark/commonJS',
    exports: '저를 찾아보세요~',
    filename: '/Users/parkchaeeun/project/playground-cepark/commonJS/require.js',
    loaded: false,
    children: [ [Module] ],
    paths: [
      '/Users/parkchaeeun/project/playground-cepark/commonJS/node_modules',
      '/Users/parkchaeeun/project/playground-cepark/node_modules',
      '/Users/parkchaeeun/project/node_modules',
      '/Users/parkchaeeun/node_modules',
      '/Users/node_modules',
      '/node_modules'
    ]
  },
  '/Users/parkchaeeun/project/playground-cepark/commonJS/var.js': Module {
    id: '/Users/parkchaeeun/project/playground-cepark/commonJS/var.js',
    path: '/Users/parkchaeeun/project/playground-cepark/commonJS',
    exports: { odd: 'CJS 홀수입니다.', even: 'CJS 짝수입니다.' },
    filename: '/Users/parkchaeeun/project/playground-cepark/commonJS/var.js',
    loaded: true,
    children: [],
    paths: [
      '/Users/parkchaeeun/project/playground-cepark/commonJS/node_modules',
      '/Users/parkchaeeun/project/playground-cepark/node_modules',
      '/Users/parkchaeeun/project/node_modules',
      '/Users/parkchaeeun/node_modules',
      '/Users/node_modules',
      '/node_modules'
    ]
  }
}
require.main입니다.
Module {
  id: '.',
  path: '/Users/parkchaeeun/project/playground-cepark/commonJS',
  exports: '저를 찾아보세요~',
  filename: '/Users/parkchaeeun/project/playground-cepark/commonJS/require.js',
  loaded: false,
  children: [
    Module {
      id: '/Users/parkchaeeun/project/playground-cepark/commonJS/var.js',
      path: '/Users/parkchaeeun/project/playground-cepark/commonJS',
      exports: [Object],
      filename: '/Users/parkchaeeun/project/playground-cepark/commonJS/var.js',
      loaded: true,
      children: [],
      paths: [Array]
    }
  ],
  paths: [
    '/Users/parkchaeeun/project/playground-cepark/commonJS/node_modules',
    '/Users/parkchaeeun/project/playground-cepark/node_modules',
    '/Users/parkchaeeun/project/node_modules',
    '/Users/parkchaeeun/node_modules',
    '/Users/node_modules',
    '/node_modules'
  ]
}
true
/Users/parkchaeeun/project/playground-cepark/commonJS/require.js

require는 반드시 파일 최상단에 위치할 필요 없고, module.exports 역시 최하단에 위치할 필요 없다는 것을 알 수 있습니다.

 

require.cache

  • require.js와 var.js의 파일 이름이 속성명, 모듈 객체가 속성값으로 들어있음
  • 한 번 require한 파일은 cache에 저장되기 때문에 다음에 require할 때 새로 불러오지 않고 cache에 있는 것을 재사용
  • 만일, 새로 require하고 싶다면 require.cache 속성을 제거 (권장하지 않음!)

 

require.main

  • 노드 실행 시 첫 모듈을 가리킴
  • node commonJS/require로 실행했으므로, require.js가 require.main이 됨
  • 만일, node commonJS/require로 실행 후 var.js에서 require.main === module를 실행하면 false 반환!

 

모듈 사용 시 주의해야 할 점

dep1.js

const dep2 = require("./dep2");
console.log("require dep2", dep2);

module.exports = () => {
  console.log("dep2", dep2);
};

 

dep2.js

const dep1 = require("./dep1");
console.log("require dep1", dep1);

module.exports = () => {
  console.log("dep1", dep1);
};

 

dep-run.js

const dep1 = require("./dep1");
const dep2 = require("./dep2");

dep1();
dep2();

@콘솔출력값
$ node dep/dep-run

require dep1 {}
require dep2 [Function (anonymous)]
dep2 [Function (anonymous)]
dep1 {}
(node:1742) Warning: Accessing non-existent property 'Symbol(nodejs.util.inspect.custom)' of module exports inside circular dependency
(Use `node --trace-warnings ...` to show where the warning was created)
...

dep1과 dep2가 서로를 require하고 있는 구조인데, 출력값을 확인하니 dep1의 module.export가 함수가 아닌 빈 객체로 표시되는 것을 확인할 수 있습니다.

이를 순환 참조라고 부릅니다.

 

순환 참조가 있을 경우, 순환 참조되는 대상을 빈 객체로 만들어, 예기치 못한 동작이 발생할 수 있으므로 구조를 잘 잡아야합니다.

 

ECMAScript 모듈

 

공식적인 자바스크립트 모듈 형식으로, 브라우저와 노드 모두에 같은 모듈 형식을 사용할 수 있다는 장점을 가지고 있습니다.

 

var.mjs

export const odd = "MJS 홀수입니다.";
export const even = "MJS 짝수입니다.";

 

func.mjs

import { odd, even } from "./var.mjs";

function checkoddorEven(num) {
  // 홀수라면
  if (num % 2) {
    return odd;
  }
  return even;
}

export default checkoddorEven;

 

index.mjs

import { odd, even } from "./var.mjs";
import checkNumber from "./func.mjs";

function checkStringOddOrEven(str) {
  if (str.length % 2) {
    return odd;
  }
  return even;
}

console.log(checkNumber(10));
console.log(checkStringOddOrEven("hello"));

@콘솔출력값
$ node ecmascript/index.mjs

MJS 짝수입니다.
MJS 홀수입니다.

mjs 확장자로 변경되었고, js 확장자에서 import 사용 시 Cannot use import ... 에러가 발생하게 됩니다.

또한, import 시 파일 경로에서 js, mjs 확장자 생략 및 index.js도 생략할 수 없습니다.

 

차이점 CommonJS 모듈 ECMAScript 모듈
문법 require('./a');
module.exports = A;
const A = require('./a');
exports.C = D;
const E = F; exports.E = E;
const { C, E } = require('./b');
import './a.mjs';
export default A;
import A from './a.mjs';
export const C = D;
const E = F; export { E };
import { C, E } from './b.mjs';
확장자 js
cjs
js(package.json에 type: "module" 필요)
mjs
확장자 생략 가능 불가능
다이내믹 임포트 가능 불가능
인덱스 생략 가능 불가능
top level await 불가능 가능
_filename,
_dirname,
require, module.exports,
exports
사용 가능 사용 불가능 (import.meta.url 사용)
서로 간 호출 가능

 

다이내믹 임포트
  • 조건부로 모듈을 불러오는 것

dynamic.js (commonJS)

const a = false;
if (a) {
  require("../commonJS/func");
}

console.log("성공");

@콘솔출력값
$ node ecmascript/dynamic

성공

if문이 falsef라서 require()은 실행되지 않았습니다.

 

dynamic.mjs (ECMAScript)

const a = false;

if (a) {
  import "./func.mjs";
}
console.log("성공");

@콘솔출력값
$ node ecmascript/dynamic.mjs

file:///Users/parkchaeeun/project/playground-cepark/ecmascript/dynamic.mjs:4
  import "./func.mjs";
         ^^^^^^^^^^^^

ES 모듈은 if문 안에서 import하는 것이 불가능하기 때문에, 이럴 때 다이내믹 임포트를 사용합니다.

 

dynamic.mjs

const a = true;

if (a) {
  const m1 = await import("./func.mjs");
  console.log(m1);
  const m2 = await import("./var.mjs");
  console.log(m2);
}

@콘솔출력값
$ node ecmascript/dynamic.mjs

[Module: null prototype] { default: [Function: checkoddorEven] }
[Module: null prototype] { even: 'MJS 짝수입니다.', odd: 'MJS 홀수입니다.' }

import 함수를 이용해 모듈을 동적으로 불러옵니다.

import는 Promise를 반환하므로 await 혹은 then을 붙여줍니다. ES 모듈의 최상위 스코프에서는 async 함수 없이도 await할 수 있습니다.

export default의 경우, default라는 속성 이름으로 import 되고, module.exports 한 것도 default라는 이름으로 import 됩니다.

 

__filename, __dirname

 

  • 파일 사이에 모듈 관계가 있는 경우가 많으므로, 현재 파일의 경로나 파일명을 알아야 하는 경우 사용

filename.js

console.log(__filename);
console.log(__dirname);

@콘솔출력값
$ node ecmascript/filename

/Users/parkchaeeun/project/playground-cepark/ecmascript/filename.js
/Users/parkchaeeun/project/playground-cepark/ecmascript

경로가 문자열로 반환되고, /나 \와 같은 경로 구분자 문제도 있기 때문에 path 모듈과 함께 씁니다.

 

ES 모듈에서는 import.meta.url 로 경로를 가져옵니다.

filename.mjs

console.log(import.meta.url);
console.log("__filename은 에러!");
console.log(__filename);

@콘솔출력값
$ node ecmascript/filename

/Users/parkchaeeun/project/playground-cepark/ecmascript/filename.js
/Users/parkchaeeun/project/playground-cepark/ecmascript
parkchaeeun@bagchaeeun-ui-Macmini playground-cepark % node ecmascript/filename.mjs
file:///Users/parkchaeeun/project/playground-cepark/ecmascript/filename.mjs
__filename은 에러!
file:///Users/parkchaeeun/project/playground-cepark/ecmascript/filename.mjs:3
console.log(__filename);
            ^

ReferenceError: __filename is not defined in ES module scope
...