입력한 코드를 읽고(Read), 해석하고(Eval), 결과물을 반환하고(Print), 종료할 때까지 반복한다(loop)고 해서 REPL이라고함. 그러나 긴 코드의 경우 자바스크립트 파일로 만든 뒤 통째로 실행하는 것이 좋다.
콘솔에서 node [자바스크립트 path]로 실행. 확장자 생략가능!
코드를 모듈로 만들 수 있다는 점에서 브라우저 자바스크립트와 다름. 모듈이란 특정한 기능을 하는 함수나 번들의 집합. ex) 수학에 관련한 코드만 모아서 모듈을 하나 만들 수도. 자체로도 하나의 프로그램이면서 다른 프로그램의 부품으로도 사용가능.
변수 두개 선언 이후 module.exports에 변수 담은 객체를 대입. 변수들을 모아둔 모듈이 됨.
require 함수 안에 불러올 모듈의 경로. 파일 경로에서 js 나 json과 같은 확장자는 생략가능. const {odd, even}은 ES2015+ 문법. var.js에서 담겨있던 객체를 불러와 func.js 에서 사용.
index.js는 var.js와 func.js를 모두 참조. 모듈 하나가 여러 개의 모듈을 사용하는 것. 또한 모듈로부터 값을 불러올 때 변수 이름을 다르게 지정할 수도. func.js의 checkOddEven이 checkNumber 이라는 이름으로 사용됨.
ES2015모듈
ES2015가 도입되며 자바스크립트도 자체 모듈 시스템 문법이 생김.
import {odd, even} from './var'; function checkOddEven(num) { if(num % 2) { return odd; } return even; } export default checkOddEven;
require과 module.export 가 import, export default로 바뀌었다. 상당한 부분에서 차이가 존재. 노드에서도 9버젼에서부터 ES2015의 모듈 시스템 사용가능. 하지망 파일의 확장자를 mjs로 지정해야하는 제한. mjs 대신 js 확장자를 사용하면서 모듈을 사용하려면 package.json에 type:"module" 속성을 넣으면 된다.
방금 사용한 require, module 객체는 별도 선언 없이 사용가능. 왜? 내장 객체이기 때문에.
브라우저의 window와 같은 전역 객체여서 모든 파일에서 접근 가능.
winodw.open 에서 open으로도 호출이 가능한 것처럼 global도 생략가능(앞선 require에서도 global.require이 생략)
노드의 window, document 객체
노드에 DOM이나 BOM이 없으므로 window, document 객체는 노드에서 사용불가. (에러 발생)
전역 객체라는 점을 이용해 파일간 간단한 데이터를 공유할 때 사용하기도 한다.
const A = require('./globalA')
global.message = '안녕~ 이제는 안녕~~'
console.log(A())
// 예측 : 위에서 require로 가져온 A를 실행하면 A에서 global.message 가 실행되서 msg 출력될 것이다.
globalA 모듈의 함수는 global.message 값을 반환. globalB.js에서는 global 객체에 속성명이 message인 값을 대입하고 globalA 모듈의 함수를 호출. 콘솔 결과는 globalB에서 넣은 global.message 값을 globalA에서도 접근할 수 있음.
warning! global객체의 남용
global 객체에 값을 대입해서 공유할 수 있으나 남용하면 어떤 파일에서 객체에 값을 대입했는지 찾기 힘들어져 유지 보수가 어렵다. 다른 파일의 값을 가져다 쓰려면 모듈 형식으로 만들어서 명시적으로 값을 불러올 것!
지금까지 사용했던 console도 노드에서는 window 대신 global 객체 안에 들어 있으며, 브라우저에서의 console과 유사하고 디버깅을 위해 사용한다.
타이머 기능을 제공하는 함수 setTimeout, setInterval, setImmediate는 노드에서 window 대신 global 객체 안에 들어있다.
이 타이머 함수들은 모두 아이디를 반환. 아이디를 사용하여 타이머를 취소할 수도.
다음은 위 메서드들을 사용한 코드
setImmediate(콜백)과 setTimeout(콜백, O)
setImmediate(콜백)과 setTimeout(콜백, 0)에 담긴 콜백 함수는 이벤트 루프를 거친 뒤 즉시 실행. 둘의 차이점은? 특수한 경우에 setImmediate는 setTimeout(콜백, 0)보다 먼저 실행됨. 파일 시스템 접근, 네트워킹 같은 I/O 작업의 콜백 함수 안에서 타이머를 호출. 하지만 setImmediate가 항상 setTimeout(콜백, 0)보다 먼저 호출되지 않는다는 사실만 알아뒁. 헷갈리지 않도록 setTimeout(콜백, 0)은 사용하지 않는 것을 권장한다.
console.log(__filename); //c:\coding\TIL\node\node_3장\3.4노드내장객체\노드기능.js
console.log(__dirname); //c:\coding\TIL\node\node_3장\3.4노드내장객체
\ 나 / 같은 경로 구분자 문제도 있으므로 보통은 이를 해결해주는 path모듈과 함께 씁니다.
지금까지는 모듈을 만들 때 module.exports만 사용했는데, module 객체말고 exports 객체로도 모듈을 만들 수 있다.
var.js
exports.odd = '홀수입니다'
exports.even = '짝수입니다'
module.exports 로 한 번에 대입하는 대신, 각각의 변수를 exports 객체에 하나씩 넣었음. 동일하게 동작하는 이유는 module.exports 와 exports가 같은 객체를 참조하기 때문. 실제로 console.log(module.exports === exports)를 하면 true가 나옴. 따라서 exports 객체에 add 함수를 넣으면 module.exports 에도 add 함수가 들어감.
exports 객체 사용 시
- exports 객체를 사용할 때는 module.exports와의 참조 관계가 깨지지 않도록 주의해야. module.exports에는 어떤 값이든 대입해도 되지만, exports에는 반드시 객체처럼 속성명과 속성값을 대입해야 한다. exports에 다른 값을 대입하면 객체의 참조 관계가 끊겨 더 이상 모듈로 기능하지 않는다.
- exports를 사용할 때는 객체만 사용할 수 있으므로 func.js와 같이 module.exports에 함수를 대입한 경우에는 exports로 바꿀 수 없음. exports와 module.exports에는 참조 관계가 있으므로 한 모듈에 exports 객체와 module.exports를 동시에 사용하지 않는 것이 좋다.
노드에서 this는 무엇일까요?
노드에서 this를 사용할 때 주의해야 할 점이 있다.
console.log(this); // {} console.log(this === module.exports) // true console.log(this === exports) // true function WhatIsThis() { console.log('function', this === exports, this === global); } WhatIsThis(); //function false true
다른 부분은 브라우저의 자바스크립트와 동일하지만 최상위 스코프에 존재하는 this는 module.exports(또는 exports 객체)를 가리킨다. 또한, 함수 선언문 내부의 this는 global 객체를 가리킨다.
이번에는 모듈을 불러오는 require.js에 대해 알아보자.
console.log('require가 가장 위에 오지 않아도 됩니다');
module.exports = '저를 찾아보세요';
require('./var')
console.log('require.cache입니다.')
console.log(require.cache); //require.cache입니다.
[Object: null prototype] {
'c:\\coding\\TIL\\node\\node_3장\\require.js': Module {
id: '.',
path: 'c:\\coding\\TIL\\node\\node_3장',
exports: '저를 찾아보세요',
filename: 'c:\\coding\\TIL\\node\\node_3장\\require.js',
loaded: false,
children: [ [Module] ],
paths: [
'c:\\coding\\TIL\\node\\node_3장\\node_modules',
'c:\\coding\\TIL\\node\\node_modules',
'c:\\coding\\TIL\\node_modules',
'c:\\coding\\node_modules',
'c:\\node_modules'
]
},
'c:\\coding\\TIL\\node\\node_3장\\var.js': Module {
id: 'c:\\coding\\TIL\\node\\node_3장\\var.js',
path: 'c:\\coding\\TIL\\node\\node_3장',
exports: { odd: '홀수입니다', even: '짝수입니다' },
filename: 'c:\\coding\\TIL\\node\\node_3장\\var.js',
loaded: true,
children: [],
paths: [
'c:\\coding\\TIL\\node\\node_3장\\node_modules',
'c:\\coding\\TIL\\node\\node_modules',
'c:\\coding\\TIL\\node_modules',
'c:\\coding\\node_modules',
'c:\\node_modules'
]
}
console.log('require.main입니다.')
console.log(require.main === module); //true
console.log(require.main.filename); // c:\coding\TIL\node\node_3장\require.js
경로는 다를 수도. 예제에서 알아야 할 점은 require가 반드시 파일 최상단에 위치할 필요가 없고 module.exports도 최하단에 위치할 필요가 없다는 점. 아무 곳에서나 사용해도 된다.
require.cache 객체에 require.js나 var.js 같은 파일 이름이 속성명으로 들어 있는 것을 볼 수 있다. 속성값으로는 각 파일의 모듈 객체가 들어 있다. 한 번 require한 파일은 require.cache에 저장되므로 다음 번에 require할 때는 새로 불러오지 않고 require.cache에 있는 것이 재사용된다.
만약 새로 require하길 원한다면, require.cache의 속성을 제거하면 된다. 다만, 프로그램의 동작이 꼬일 수 있으므로 권장하지 않는다. 속성을 자세히 살펴보면, module.exports했던 부분(exports)이나 로딩 여부(loaded), 부모(parent), 자식(children) 모듈 관계를 찾을 수 있다.
require.main은 노드 실행 시 첫 모듈을 가리킨다. 현재 node require로 실행했으므로 require.js가 require.main이 된다. require.main 객체의 모양은 require.cache의 모듈 객체와 같다. 현재 파일이 첫 모듈인지 알아보려면 require.main === module을 해보면 된다. node require로 실행한 경우, var.js에서 require.main === module 을 실행하면 false가 반환될 것이다. 첫 모듈의 이름을 알아보려면 require.main.filename으로 확인하면 된다.
모듈을 사용할 때 주의해야할 점이 있다. 만약 두 모듈 dep1과 dep2가 있고 이 둘이 서로를 reqire 한다면?
dep1.js
const dep2 = require('./dep2'); console.log('require dep2', dep2); module.exports = () => { console.log('dep1', dep1) }
dep2.js
const dep1 = require('./dep1'); console.log('require dep1', dep1); module.exports = () => { console.log('dep1', dep1) }
dep-run.js를 만들어 두 모듈을 실행해보겠습니다.
dep-run.js
const dep1 = require('./dep1'); const dep2 = require('./dep2'); dep1() dep2()
코드 위에서부터 실행되므로 require('./dep1')이 먼저 실행된다. dep1.js 에서는 제일 먼저 require('./dep2')가 실행되는데 다시 dep2.js에서는 require('./dep1')이 실행된다.
놀랍게도 dep1의 module.exports가 함수가 아니라 빈 객체로 표시됨. 이러한 현상을 순환 참조라고 부른다. 이렇게 순환 참조가 있을 경우에는 순환 참조되는 대상을 빈 객체로 만듦. 이때 에러가 발생하지 않고 조용히 빈 객체로 변경되므로 예기치 못한 동작이 발생할 수 있음. 따라서 순환 참조가 발생하지 않도록 구조를 잘 잡는 것이 중요.
process 객체는 현재 실행되고 있는 노드 프로세스에 대한 정보를 담고 있다. process 객체 안에는 다양한 속성. 하나씩 REPL에 따라 입력. 결과값은 사용자의 컴퓨터마다 차이.
REPL에 process.env를 입력하면 매우 많은 정보가 출력. 시스템의 환경 변수. 시스템 환경 변수 외에도 임의로 환경 변수를 저장할 수 있다. process.env 는 서비스의 중요한 키를 저장하는 공간으로도 사용. 서버나 DB의 비밀번호, 각종 API 키를 코드에 직접 입력하는 것은 위험. 혹여 서비스가 해킹 당해 코드가 유출되면 비밀번호가 코드에 남아서 추가 피해 발생 가능성. 따라서 중요한 비밀번호는 다음과 같이 process.env의 속성으로 대체
const secretId = process.env.SECRET_ID;
const secretCode = process.env.SECRET_CODE;
이제 process.env에 직접 SECRET_ID와 SECRET_CODE를 넣으면 된다.
이벤트 루프가 다른 콜백 함수들보다 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를 마이크로태스크(microtask)라고 따로 구분지어 부른다.
마이크로태스크의 재귀 호출
process.nextTick으로 받은 콜백 함수나 resolve된 Promise는 다른 이벤트 루프에서 대기하는 콜백함수보다도 먼저 실행된다. 그래서 비동기 처리시 setImmediate보다 process.nextTick을 더 선호하는 개발자도 있다. 하지만 이런 마이크로태스크를 재귀호출하면 이벤트 루프는 다른 콜백함수보다 마이크로태스크를 우선 처리해 콜백함수들이 실행되지 않을 수도.
실행 중인 노드 프로세스를 종료. 서버 환경에서 이 함수를 사용 시 서버가 멈춤. 그러므로 특수한 경우를 제외하고 서버에서 잘 사용하지 않는다. 하지만 서버 외의 독립적인 프로그램에서는 수동으로 노드를 멈추기 위해 사용.
let i = 1;
setInterval(()=>{
if (i === 5) {
console.log('종료!');
process.exit();
}
console.log(i);
i += 1;
}, 1000)
process.exit 메서드는 인수로 코드 번호를 줄 수 있다. 인수를 주지 않거나 0을 주면 정상 종료, 1을 주면 비정상 종료. 에러가 발생해서 종료하는 경우엔느 1을 넣으면 된다. 여기까지가 자주 쓰이는 내장 객체! 타이머와 콘솔, 프로세서, 모듈은 기본적인 기능이지만 앞으로도 계속 사용된다.