3. REPL 사용하기
자바스크립트는 스크립트 언어이므로 미리 컴파일하지 않아도 즉석에서 코드를 실행할 수 있다.
노드도 콘솔을 제공하는데 입력한 코드를 읽고(Read), 해석하고(Eval), 결과물을 반환하고(Print), 종료할 때까지 반복한다(Loop)고 해서 REPL이다.
노드의 REPL을 사용하려면 윈도에서는 CMD, 맥이나 리눅스에서는 터미널을 열고 node를 입력한다.
VS에서는 Ctrl+'키를 누르면 터미널을 켤 수 있다.
프롬포트 모양이 >로 바뀌었다면 자바스크립트 코드를 입력할 수 있다.
입력한 코드를 REPL이 읽고(Read) 해석한(Eval) 뒤 바로 결과물을 출력(Print)했다. 그리고 종료되기 전까지 입력을 기다린다.(Loop)
REPL은 여러 줄의 코드를 실행하기에는 불편한 점이 있다.
3.2 JS 파일 실행하기
function helloWorld() {
console.log('Hello World');
helloNode();
}
function helloNode() {
console.log('Hello Node');
}
helloWorld();
콘솔에서 node [자바스크립트 파일 경로]로 실행한다.
REPL이 아닌 콘솔에서 입력해야 한다.
콘솔에서 REPL로 들어가는 명령어가 node이고, 노드를 통해 파일을 실행하는 명령어는 node [자바스크립트 파일 경로] 이다.
3.3 모듈로 만들기
노드는 코드를 모듈로 만들 수 있다는 점에서 브라우저의 자바스크립트와 다르다.
모듈이란 특정한 기능을 하는 함수나 변수들의 집합을 말한다.
모듈로 만들어두면 어려 프로그램에 해당 모듈을 재사용할 수 있다.
노드에서는 두 가지 형식의 모듈을 사용하는데 하나는 CommonJS 모듈과 ECMAScript 모듈이다.
3.3.1 CommonJS 모듈
CommonJS 모듈은 표준 자바스크립트 모듈은 아니지만 노드 생태계에서 가장 널리 쓰이는 모듈이다.
모듈을 만들 때는 모듈이 될 파일과 모듈을 불러와서 사용할 파일이 필요하다.
// var.js
const odd = 'CJS 홀수입니다';
const even = 'CJS 짝수입니다';
module.exports = {
odd,
even,
};
module.exports에 변수들을 담은 객체를 대입했다.
이제 이 파일은 모듈로서 기능한다.
아래 코드는 var.js를 참조하는 func.js 파일이다.
// func.js
const { odd, even } = require('./var');
function checkOddOrEven(num) {
if (num % 2) { // 홀수이면
return odd;
}
return even;
}
module.exports = checkOddOrEven;
require 함수 안에 불러올 모듈의 경로를 적는다.
require 함수로 var.js에 있던 값들을 불러와 사용한다.
다른 모듈을 사용하는 파일을 다시 모듈로 만들 수 있다.
아래 index.js는 var.js와 func.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'));
모듈로부터 값을 불러올 때 변수 이름을 다르게 지정할 수도 있다.
위에서는 checkOddOrEven을 checkNumber라는 이름으로 사용되고 있다.
여러 파일에 걸쳐 재사용되는 함수나 변수를 모듈로 만들면 편리하나 많아지면 복잡해진다는 단점도 있다.
module 객체 말고 exports 객체로도 모듈을 만들 수 있다.
// var.js
exports.odd = 'CJS 홀수입니다';
exports.even = 'CJS 짝수입니다';
결과는 동일하다.
module.exports와 exports가 같은 객체를 참조하기 때문에 동일하게 동작한다.
노드에서 this는 브라우저의 this와는 조금 다르다.
console.log(this);
console.log(this === module.exports);
console.log(this === exports);
function whatIsThis() {
console.log('function', this === exports, this === global);
}
whatIsThis();
/*
실행 결과
{}
true
true
function false true
*/
다른 부분은 브라우저의 자바스크립트와 동일하지만 최상위 스코프에 존재하는 this는 module.exports를 가리킨다.
require는 함수이고, 함수 객체이므로 reuquire는 객체로써 속성 몇 개를 가지고 있다.
// 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 === module);
console.log(require.main.filename);
require가 반드시 파일의 최상단에 위치할 필요가 없다.
한번 require한 파일은 require.cache에 저장되므로 다음 번에 require할 때는 새로 불러오지 않고 재사용한다.
만약 새로 require하길 원하면 require.cache의 속성을 제거하면 된다. (프로그램 동작이 꼬일 수 있음)
require.main은 노드 실행 시 첫 모듈을 가리킨다.
현재 node require로 실행했으므로 require.js가 require.main이 된다.
현재 파일이 첫 모듈인지 알아보려면 require.main === module을 사용한다.
첫 모듈의 이름을 알아보려면 rquire.main.filename으로 확인하면 된다.
만약 두 모듈 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('dep1', dep1);
};
// dep-run.js
const dep1 = require('./dep1');
const dep2 = require('./dep2');
dep1();
dep2();
dep1. module.exports가 함수가 아니라 빈 객체로 표시된다.
이러한 현상을 순환 참조라고 부른다.
이렇게 순환 참조되는 대상을 빈 객체로 만든다. 이때 에러가 발생하지 않고 조용히 빈 객체로 변경된다.
3.3.2 ECMAScript 모듈
ECMAScript는 공식적인 자바스크립트 모듈 형식이다.
ES 모듈이 표준으로 정해지면서 점점 ES 모듈을 사용하는 비율이 늘어나고 있다.
브라우저에서도 ES모듈을 사용할 수 있어 브라우저와 노드 모두에 같은 모듈 형식을 사용할 수 있다.
// 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'));
require와 exports, modul.export가 importm export, export default로 바뀌었다.
파일도 js 대신 mjs 확장자로 변경되었다.
Common 모듈과는 다르게 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 | 사용 가능 | 사용 불가능 |
서로간 호출 | 가능 | 가능 |
3.3.3 다이내믹 임포트
// dynamic.js
const a = false;
if (a) {
require('./func');
}
console.log('성공');
/* 실행결과
성공
*/
dynamic.js에서 require(./func)는 if문이 false이기 때문에 실행되지 않는다.
이렇게 조건부로 모듈을 불러오는 것을 다이내믹 임포트라고 한다.
// dynamic.mjs
const a = false;
if (a) {
import './func.mjs';
}
console.log('성공');
/* 실행 결과
file:///C:/Users/speak/WebstormProjects/nodejs-book/ch3/3.3/dynamic.mjs:3
import './func.mjs';
^^^^^^^^^^^^
SyntaxError: Unexpected string
*/
하지만 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);
}
/* 실행 결과
[Module: null prototype] { default: [Function: checkOddOrEven] }
[Module: null prototype] { even: 'MJS 짝수입니다', odd: 'MJS 홀수입니다' }
*/
import라는 함수를 사용해서 모듈을 동적으로 불러올 수 있다.
ES 모듈의 최상위 스코프에서는 async 함수 없이도 await할 수 있다.
결괏값을 보면 export default의 경우 import 할 때도 default 라는 속성 이름으로 import된다.
CommonJS 모듈에서 module.exports 한 것도 default라는 이름으로 import 된다.
3.3.4 __filename, __dirname
파일에 __filename과 __dirname을 넣어두면 실행 시 현재 파일명과 현재 파일 경로로 바뀐다.
// filename.js
console.log(__filename);
console.log(__dirname);
윈도가 아니라면 \대신 /로 폴더 경로가 구분될 수 있다.
경로가 문자열로 반환되기도 하고, \나 /같은 경로 구분자 문제도 있으므로 보통은 이를 해결해주는 path 모듈과 함께 쓴다.
ES 모듈에서는 __filename과 __dirname을 사용할 수 없다.
3.4 노드 내장 객체 알아보기
노드에서는 기본적인 내장 객체와 내장 모듈을 제공한다.
3.4.1 global
브라우저 window와 같은 전역 객체이며 모든 파일에 접근할 수 있다.
window.open 메서드에서 open으로 호출할 수 있는 것처럼 global도 생략할 수 있다.
require 함수도 원래는 global.require이고, console도 global.console이다.
global 객체 안에는 수십 가지의 속성이 담겨 있다.
전역 객체라는 점을 이용해 파일 간에 간단한 데이터를 공유할 때 사용하기도 한다.
// globalA.js
module.exports = () => global.message;
// globalB.js
const A = require('./globalA');
global.message = '안녕하세요';
console.log(A());
/* 실행 결과
안녕하세요
*/
globalB에서 넣은 global.message 값을 globalA에서도 접근할 수 있다.
3.4.2 console
console도 노드에서는 window 대신 global 객체 안에 들어 있다.
console 객체는 보통 디버깅을 위해 상요한다.
개발 중 변수에 값이 제대로 들어 있는지 확인하기 위해 사용하기도 하고, 에러 발생 시 에러 내용을 알기 위해서도 사용하며 코드 실행 시간을 알아보려고 할 때도 사용한다.
// console.js
const string = 'abc';
const number = 1;
const boolean = true;
const obj = {
outside: {
inside: {
key: 'value',
},
},
};
console.time('전체 시간');
console.log('평범한 로그입니다 쉼표로 구분해 여러 값을 찍을 수 있습니다');
console.log(string, number, boolean);
console.error('에러 메시지는 console.error에 담아주세요');
console.table([{ name: '제로', birth: 1994 }, { name: 'hero', birth: 1988}]);
console.dir(obj, { colors: false, depth: 2 });
console.dir(obj, { colors: true, depth: 1 });
console.time('시간 측정');
for (let i = 0; i < 100000; i++) {}
console.timeEnd('시간 측정');
function b() {
console.trace('에러 위치 추적');
}
function a() {
b();
}
a();
console.timeEnd('전체 시간');
console.time(레이블): console.timeEnd(레이블)과 대응되어 같은 레이블을 가진 time과 timeEnd 사이의 시간을 측정한다.
console.log(내용): 평범한 로그를 콘솔에 표시한다. console.log(내용, 내용, …)처럼 여러 내용을 동시에 표시할 수도 있다.
console.error(에러 내용): 에러를 콘솔에 표시한다.
console.table(배열): 배열의 요소로 객체 리터럴을 넣으면, 객체의 속성들이 테이블 형식으로 표현된다.
console.dir(객체, 옵션): 객체를 콘솔에 표시할 때 사용된다. 첫 번째 인수로 표시할 객체를 넣고, 두 번째 인수로 옵션을 넣는다. 옵션의 colors를 true로 하면 콘솔에 색이 추가되어 보기가 한결 편해진다. depth는 객체 안의 객체를 몇 단계까지 보여줄지를 결정한다. 기본값은 2이다.
console.trace(레이블): 에러가 어디서 발생했는지 추적할 수 있게 한다.
3.4.3 타이머
타이머 기능을 제공하는 함수인 setTimeout, setInterval, setImmediate는 노드에서 global 객체 안에 있다.
setTimeout(콜백 함수, 밀리초): 주어진 밀리초(1,000분의 1초) 이후에 콜백 함수를 실행한다.
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의 콜백이 실행된다.
코드 실행 2초 후에는 interval의 콜백이 다시 실행된다.
코드 실행 2.5초 후에는 clearTimeout과 clearInterval이 각각 timeout2와 interval을 취소한다.
코드 실행 3초 후에는 아무 로그도 남지 않는다.
타이머는 콜백 기반 API이지만 프로미스 방식을 사용할 수도 있다.
import { setTimeout, setInterval } from 'timers/promises';
await setTimeout(3000);
console.log('3초 뒤 실행');
for await (const startTime of setInterval(1000, Date.now())) {
console.log('1초마다 실행', new Date(startTime));
}
프로미스 기반이므로 then 대신 await을 사용하기 위해 ES모듈을 사용한다.
3.3.4 process
process 객체는 현재 실행되고 있는 노드 프로세스에 대한 정보를 담고 있다.
일반적으로 운영체제나 실행 환경별로 다른 동작을 하고 싶을 때 사용한다.
3.4.4.1 process.env
REPL에 process.env를 입력하면 매우 많은 정보가 출력된다.
이 정보들은 시스템의 환경 변수이다.
대표적인 것으로 UV_THREADPOOL_SIZE와 NODE_OPTIONS가 있다.
NODE_OPTIONS=--max-old-space-size=8192
UV_THREADPOOL_SIZE=8
--max-old-space-size=8192는 노드의 메모리를 8GB까지 사용할 수 있게 한다.
UV_THREADPOOL_SIZE는 노드에서 기본적으로 사용하는 스레드 풀의 스레드 개수를 조절할 수 있게 한다.
시스템 환경 변수 외에도 여러분이 임의로 환경 변수를 지정할 수 있다.
process.env는 서비스의 중요한 키를 저장하는 공간으로도 사용되기 때문에 중요한 비밀번호는 속성으로 대체한다.
const secretId = process.env.SECRET_ID;
const secretCode = process.env.SECRET_CODE;
3.4.4.2 process.nextTick(콜백)
이벤트 루프가 다른 콜백 함수들보다 nextTick의 콜백 함수를 우선으로 처리하도록 만든다.
setImmediate(() => {
console.log('immediate');
});
process.nextTick(() => {
console.log('nextTick');
});
setTimeout(() => {
console.log('timeout');
}, 0);
Promise.resolve().then(() => console.log('promise'));
/* 실행 결과
nextTick
promise
timeout
immediate
*/
process.nextTick은 setImmediate나 setTimeout보다 먼저 실행된다.
코드 맨 밑에 Promise를 넣은 것은 resolve된 Promise도 nextTick처럼 다른 콜백들보다 우선시 되기 때문이다.
그래서 process.nextTick과 Promise를 마이크로태스크라고 따로 구분해서 부른다.
'Programming > NodeJS' 카테고리의 다른 글
Node.js 교과서 [파일 시스템] #3.3 (0) | 2024.02.04 |
---|---|
Node.js 교과서 [노드 내장 모듈] #3.2 (0) | 2024.02.04 |
Node.js 교과서 [Front-end] #2.2 (0) | 2024.02.02 |
Node.js 교과서 [ES2015+] #2 (0) | 2024.02.02 |
Node.js 교과서 #1 (0) | 2024.01.13 |