본문 바로가기

Back-End/Node.js

[Node.js 교과서] 3. 노드 기능 알아보기

반응형

1. REPL 사용하기

자바스크립트는 스크립트 언어이므로 미리 컴파일을 하지 않아도 즉석에서 코드를 실행할 수 있다. 아마 여러분도 이것을 브라우저 콘솔 탭에서 경험해봤을 것이다. 

이것을 REPL이라고 한다. 입력한 코드를 읽고(Read), 해석하고(Eval), 결과물을 반환하고(Print), 종료할 때까지 이 과정을 반복한다(Loop)라는 의미이다.

REPL

노드도 이와 비슷한 콘솔을 제공한다.

$ node
>

콘솔창에 node를 입력하면 프롬프트가 > 모양으로 바뀌어 자바스크립트 코드를 입력할 수 있게 된다.

간단한 문자열을 입력해보자.

노드 REPL창 사용하기

REPL을 종료하려면 ctrl+c 를 두 번 누르거나, 혹은 REPL창에 .exit을 입력하면 된다.

 

REPL은 한두 줄짜리 코드를 테스트해보는 용도로는 좋지만 긴 코드를 실행하기는 불편하다. 그런 경우는 코드를 자바스크립트 파일로 만들고 파일을 통째로 실행하는 것이 좋다.

 

 

2. 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

 

 

3. 모듈로 만들기

3.1 모듈이란?

노드는 코드를 모듈로 만들 수 있다는 점에서 브라우저의 자바스크립트와 다르다.

모듈이란 특정한 기능을 하는 함수나 변수들의 집합이다. 바나나, 사과, 포도를 모두 과일이라고 칭하는 것처럼 말이다. 바나나, 사과, 포도는 각각 함수나 변수들이고, 과일이 모듈이다.

모듈은 자체로도 하나의 프로그램이면서 다른 프로그램의 부품으로도 사용할 수 있다. 또 모듈로 만들어놓으면 여러 프로그램에 해당 모듈을 재사용할 수도 있어서 편리하다.

 

보통 파일 하나가 모듈 하나가 된다. 파일별로 코드를 모듈화할 수 있어서 관리하기 편하다.

모듈 생성과 모듈의 재사용

 

3.2 모듈 만들기

실제로 모듈을 만들어보자. var.js, func.js, index.js를 같은 폴더 내에 만들자.

  • var.js : odd, even 변수를 export
  • func.js : var.js의 odd, even 변수를 받고 함수를 export
  • index.js : var.js의 odd, even 변수와 func.js의 함수를 받음

3.2.1 모듈 만들기

// var.js
const odd = '홀수입니다';
const even = '짝수입니다';

module.exports = {
    odd,
    even,
};

module.exports에 변수들을 담은 객체를 대입했다. 이제 이 파일은 모듈로서 기능한다. 변수들을 모아둔 모듈이 되는 것이다. 외부 파일에서 이 파일을 불러오면 module.exports에 대입된 값을 사용할 수 있다.

module.exports에는 객체뿐만 아니라 함수변수를 대입할 수도 있다.

3.2.2 모듈 불러오기

// func.js
const { odd, even } = require('./var');

function checkOddOrEven(num) {
    if(num%2) {
        return odd;
    }
    return even;
}

module.exports = checkOddOrEven;

require 함수 안에 불러올 모듈의 경로를 작성한다. 다른 폴더에 있는 파일도 모듈로 사용할 수 있다. 파일 경로에서 .js같은 확장자는 생략할 수 있다.

var.js에서 module.exports에 담겨있던 변수들을 불러오고 숫자의 홀짝을 판별하는 함수를 선언하였다. 그리고 다시 module.exports 함수에 대입하여 모듈로 만들었다.

이렇게 다른 모듈(var.js)을 사용하는 파일을 다시 모듈(func.js)로 만들 수 있다.

여기서 const { odd, even } 은 2.1.5절에서 설명한 구조분해 할당이다. 구조분해 할당을 사용하면 객체와 배열로부터 속성이나 요소를 쉽게 꺼낼 수 있다.
객체 안의 속성을 찾아서 입력한 변수와 동일하면 매칭시킨다. 객체의 여러 단계 안의 속성도 찾을 수 있다.

마지막으로 index.js도 다음과 같이 작성하자.

// 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')); // 홀수입니다

index.js는 var.js와 func.js 모듈 모두 참조한다. 이처럼 모듈 하나(index.js)가 여러 개의 모듈(var.js, func.js)을 사용할 수도 있고, 또 모듈 하나(var.js)가 여러 개의 모듈(func.js, index.js)에 사용될 수도 있다.

또 checkNumber처럼 모듈로부터 값을 불러올 때 변수 이름을 다르게 지정할 수도 있다.

3.2.3 ES2015 모듈

ES2015가 도입되면서 자바스크립트도 자체 모듈 시스템 문법이 생겼다. 노드의 모듈 시스템과 조금 다르므로 살펴보자.

import { odd, even } from './var';

function checkOddOrEven(num) {
    if(num % 2) {
        return odd;
    }
    return even;
}

export default checkOddOrEven;
  • require ➡ import, from
  • module.exports ➡ export default

 

 

4. 노드 내장 객체 알아보기

노드에서 기본적으로 제공되는 내장 객체와 내장 모듈을 알아보자. 내장되어 있으므로 따로 설치없이 바로 사용할 수 있다. 먼저 노드 내장 객체를 살펴보자.

 

4.1 global

브라우저의 window를 생각하면 된다. 전역 객체이므로 모든 파일에서 접근할 수 있고, global 메서드를 호출할 때 global 글자를 생략해도 무방하다. 사실 require 함수와 console 객체도 global 객체 안에 들어있는 것이라서 원래는 global.require, global.console이다.

노드의 window, document 객체

노드에는 DOM(Document Object Model)이나 BOM(Browser Object Model)이 없어서 window와 document 객체를 노드에서 사용할 수 없다. 사용하면 에러가 발생한다.

 

노드 REPL창에서 global와 global.console을 입력했을 때

다음은 노드 REPL창에서 global과 global.console을 입력했을 때 출력 결과이다.

이처럼 global 객체 안에는 수십 가지의 속성이 담겨져 있다. 모두 알 필요는 없고, 자주 사용하는 속성들만 알아보자.

 

전역 객체라는 점을 이용하여 파일 간에 간단한 데이터를 공유할 때 사용하기도 한다. globalA.js와 globalB.js를 같은 폴더에 생성해보자.

// globalA.js
module.exports = () => global.message;
// globalB.js
const A = require('./globalA');

global.message = '안녕하세요';
console.log(A()); // 안녕하세요

globalA 모듈의 함수는 global.message 값을 반환한다.

globalB.js 에서는 global 객체에 속성명이 message인 값을 globalA 모듈의 함수를 호출한다.

콘솔 결과는 globalB에서 넣은 global.message 값을 globalA에서도 접근할 수 있다는 사실을 보여준다.

 

global 객체의 남용

global 객체의 속성에 값을 대입하여 파일 간에 데이터를 공유할 순 있지만, 너무 남용하면 안된다. 프로그램 규모가 커질수록 어떤 파일에서 global 객체에 값을 대입했는지 찾기 힘들어져서 유지 보수에 어려움을 겪게 되기 때문이다.
다른 파일의 값을 사용하고 싶다면 모듈 형식으로 만들어서 명시적으로 값을 불러와 사용하는 것이 좋다.

 

 

4.2 console

global 객체 안에 들어있는 객체이다. 보통 디버깅을 위해 사용한다.

  • console.log(내용): 평범한 로그를 콘솔에 표시한다. (내용, 내용, 내용, ... )처럼 여러 내용을 동시에 표시할 수도 있다.
  • console.time(레이블): console.timeEnd(레이블)과 대응되어 같은 레이블을 가진 time과 timeEnd 사이의 시간을 측정한다.
  • console.error(에러내용): 에러를 콘솔에 표시한다.
  • console.table(배열): 배열의 요소로 객체 리터럴을 넣으면, 객체의 속성들일 테이블 형식으로 표시된다.
    • ➡ [{key: 'value', key: 'value'}, {key: 'value'} , ... ]와 같은 형태
  • console.dir(객체, 옵션): 객체를 콘솔에 표시할 때 사용한다.
    • 객체: 표시할 객체
    • 옵션
      • colors: true면 콘솔에 색이 추가되어 보기가 편해진다.
      • depth: 객체 안의 객체를 몇 단계까지 보여줄지 결정한다. 기본값은 2.
  • console.trace(레이블): 에러가 어디서 발생했는지 추적할 수 있게 한다. 일반적으로 에러 발생 시 에러 위치를 알려주므로 자주 사용되진 않지만 위치가 나오지 않을 때 사용할만하다.
const string = 'abc';
const number = 1;
const boolean = true;
const obj = {
    outside: {
        inside: {
            key: 'value',
        }
    }
};
console.time('전체 시간');
console.log('평범한 로그','입니다', string, number, boolean);

console.table([{ name: '제로', birth: 1994}, { name: 'nno3onn', birth: 1997 }]);

console.dir(obj, { colors: false, depth: 2});
console.dir(obj, { colors: true, depth: 1});

function b() {
    console.trace('에러 위치 추적');
}
function a() {
    b();
}
a();

console.timeEnd('전체 시간');

콘솔 결과

 

 

4.3 타이머

타이머 기능 또한 global 객체 안에 들어있다. 

  • setTimeout(콜백 함수, 밀리초): 주어진 밀리초(1/1,000) 이후에 콜백 함수를 실행한다.
  • setInterval(콜백 함수, 밀리초): 주어진 밀리초마다 콜백 함수를 반복 실행한다.
  • setImmediate(콜백 함수): 콜백 함수를 즉시 실행한다.

위의 타이머 함수들은 모두 아이디를 반환한다. 아이디를 사용하여 타이머를 취소할 수 있다.

  • clearTimeout(아이디): setTimeout을 취소한다.
  • clearInterval(아이디): setInterval을 취소한다.
  • clearImmediate(아이디): setImmediate를 취소한다.
const timeout = setTimeout(() => {
    console.log('1.5초 후 실행');
}, 1500);

const interval = setInterval(() => {
    console.log('1초마다 실행');
}, 1000);

const timeout2 = setTimeout(() => {
    console.log('실행되지 않습니다.');
}, 3000);

setTimeout(() => {
    clearTimeout(timeout2);
    clearInterval(interval);
}, 2500);

const immediate = setImmediate(() => {
    console.log('즉시 실행');
});

const immediate2 = setImmediate(() => {
    console.log('실행되지 않습니다');
})

clearImmediate(immediate2);

콘솔 결과

제일 먼저 실행되는 것은 immediate이다. immediate2는 바로 clearImmediate를 사용해서 취소되므로 실행되지 않는다.

1초 후에는 interval의 콜백이 실행되고, 1.5초 후에는 timeout의 콜백이 실행된다. interval은 1초 간격으로 실행되므로 2초가 지났을 때도 콜백이 실행된다.

2.5초에서 timeout2와 interval을 취소한다. 따라서 이후 아무 로그가 남지 않는다.

실행 콘솔
0 immediate
immediate2
즉시 실행
1 interval 1초마다 실행
1.5 timeout 1.5초마다 실행
2 interval 1초마다 실행
2.5 timeout2
interval
 

 

 

4.4 __filename, __dirname

노드에서는 파일 사이에 모듈 관계가 있는 경우가 많다. 따라서 때로는 현재 파일의 경로나 파일명을 알아야 한다.

  • __filename: 현재 파일명
  • __dirname: 현재 파일 경로
console.log(__filename); // C:\Users\nno3o\Desktop\NodeJS_Book\Ch03_node_skill\learn-globalObject\filename.js
console.log(__dirname); // C:\Users\nno3o\Desktop\NodeJS_Book\Ch03_node_skill\learn-globalObject

 

 

4.5 module, exports, require

4.5.1 module.exports와 exports

이전에 모듈을 만들 때 module.exports만 사용했지만, module 객체 말고 exports 객체로도 모듈을 만들 수 있다.

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

 

module.exports를 사용하면 한 번에 대입할 수 있지만, exports 객체를 사용하면 각각의 변수를 하나씩 넣어주어야 한다.

두 방법이 동일하게 동작하는 이유는 module.exports와 exports가 같은 객체를 참조하기 때문이다. 실제로 console.log(module.exports === exports)를 하면 true가 나온다.

따라서 exports 객체에 add 함수를 넣으면 module.exports에도 add 함수가 들어간다.

 

4.5.2 require

노드는 모듈 로딩 시스템을 갖고 있다. 노드에서 파일과 모듈은 일대일로 대응하고, 각 파일은 별도의 모듈로 처리된다. 그렇기 때문에 여러 곳에서 하나의 파일에 작성된 모듈을 필요로 할 때 동일한 인스턴스를 사용할 수 있도록 한다.

즉, 모듈을 require할 때마다 새로운 인스턴스가 생성되는 것이 아니라 캐싱된 객체 인스턴스를 재사용하는 것이다. 그래서 불필요한 메모리의 사용을 피할 수 있다.

노드에서 한 번 require(=로딩)된 모듈은 require.cache 객체에 캐싱이 된다. key 값으로 해당 모듈 파일의 경로를 갖게 되는데 key 값이 삭제되면 다음 require 요청 시에 다시 재로딩하게 된다.

 

다음 예제를 통해 자세히 require 객체의 기능인 cache와 main을 살펴보자.

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);
  • 위의 예제를 통해 알아야 할 점은, require가 반드시 파일 최상단에 위치할 필요가 없다는 것이다. module.exports도 최하단에 위치할 필요가 없다. 아무 곳에나 작성해도 된다.

require.cache

key 값으로 해당 모듈 파일(require.js와 var.js)의 경로를 사용하여 모듈을 캐싱하고 있다. 속성값(value)으로는 각 파일의 모듈 객체가 들어있다. 

따라서 require를 통해 모듈을 로딩하면 파일의 경로를 캐시 키로 사용하기 때문에, 다른 여러 파일에서 동일한 파일을 필요로하는 경우에는 이전에 캐싱되었던 동일한 모듈을 사용한다는 것을 알 수 있다. 그래서 여러 파일에서 같은 모듈을 require할 때마다 새로운 인스턴스가 생성되는 것이 아니라 캐싱된 객체 인스턴스를 재사용하는 것이다. 이는 불필요한 메모리 사용을 피할 수 있다.

  • 속성
    • exports: 해당 파일(모듈)이 module.exports한 객체
    • loaded: 로딩 여부
    • parent: 부모 모듈 관계
    • children: 자식 모듈 관계

require.main

실행한 해당 파일의 모듈을 가리킨다. 현재 node require를 실행했으므로 require.js 모듈 객체를 가리킨다.

require.main은 실행한 현재 파일의 module 객체와 동일하다. 실제로 require.main === module을 콘솔해보면 true로 나온다. 만약 var.js에서 require.main === module을 콘솔해보면 false가 나온다. var.js에서 module은 var.js 모듈 객체를 가리키기 때문이다.

 

4.5.3 순환 참조(circular dependency)

모듈 사용 시 주의할 점이 있다. 모듈 dep1, dep2가 있을 때 서로를 require하면 어떻게 되는지 보자.

// 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('dep2', dep2);
};
// dep-run.js
const dep1 = require('./dep1');
const dep2 = require('./dep2');
dep1();
dep2();

node dep-run을 콘솔창에 입력하여 실행해보자.

코드는 위에서부터 실행되므로 require('./dep1')이 먼저 실행된다. dep1.js에서는 require('./dep2')이 가장 먼저 실행이 된다. 그리고 dep2.js에서는 require('./dep1')이 먼저 실행된다. 이 과정이 계속 반복되는데 실행하면 어떻게 될까?

dep-run.js 실행 결과 콘솔

dep1의 module.exports가 빈 객체로 표시된다. 이런 현상을 순환 참조(circular dependency)라고 한다.

이처럼 순환 참조가 있으면 순환 참조되는 대상을 빈 객체로 만든다.

에러가 발생하지 않고 조용히 빈 객체로 변경되므로 예기치 못한 동작이 발생할 수 있다. 따라서 순환 참조가 발생하지 않도록 구조를 잘 잡는 것이 중요하다.

 

 

4.6 process

현재 실행되고 있는 노드 프로세스에 대한 정보를 담고 있다. 노드 REPL을 통해 살펴보자.

> process.version
'v12.18.3' // 설치된 노드 버전
> process.arch
'x64' // 프로세서 아키텍처 정보
> process.platform
'win32' // 운영체제 플랫폼 정보
> process.pid
35952 // 현재 프로세스의 아이디. 여러 프로세스를 가질 때 구분할 수 있다.
> process.uptime()
21.761594401 // 프로세스가 시작된 후 흐른 시간
> process.execPath
'C:\\Program Files\\nodejs\\node.exe' // 노드의 경로
> process.cwd()
'C:\\Users\\nno3o\\Desktop\\NodeJS_Book\\Ch03_node_skill\\learn-globalObject' // 현재 프로세스가 실행되는 위치
> process.cpuUsage()
{ user: 1171000, system: 421000 } // 현재 cpu 사용량

사용 빈도는 높진 않지만, 운영체제나 실행 환경별로 다른 동작을 하고 싶을 때 사용하는 편이다.

다음은 process의 중요한 속성들을 살펴보자.

 

4.6.1 process.env

시스템의 환경 변수 정보를 담고 있다. 시스템 환경 변수는 노드에 직접 영향을 미치기도 한다.

대표적으로 UV_THREADPOOL_SIZE와 NODE_OPTIONS가 있다.

NODE_OPTIONS=--max-old-space-size=8192

UV_THERADPOOL_SIZE=8

 

 

 

4.6.2 process.nextTick(콜백)

 

 

 

4.6.3 process.exit(코드)

 

 

 

 

5. 노드 내장 모듈 알아보기

 

 

 

 

 

 

반응형