[ ch04 ] - 노드의 기본 기능
4-1. 주소 문자열과 요청 파라미터 다루기
4-2. 이벤트 이해하기
4-3. 파일 다루기
4-4. 로그 파일 남기기
4-1. 주소 문자열과 요청 파라미터 다루기
url 모듈을 이용해 주소 문자열을 객체로 만들면 문자열 안에 있던 정보를 나누어 객체 속성으로 보관합니다. 따라서 요청 프로토콜이 http인지 https인지를 구별하거나 요청 파라미터를 확인하고 싶다면 url 객체가 갖고 있는 속성 값을 확인하면 됩니다.
const url = require('url');
//주소 문자열을 URL 객체로 만들기
const curURL = url.parse('https://search.naver.com/search.naver?ie=utf8&where=nexearch&query=steve%20jobs');
// URL 객체를 주소 문자열로 만들기
const curStr = url.format(curURL);
console.log('주소 문자열 : %s', curStr);
console.dir(curURL);
// 요청 파라미터 구분하기
const querystring = require('querystring');
const param = querystring.parse(curURL.query);
console.log('요청 파라미터 중 query의 값 : %s', param.query);
console.log('원본 요청 파라미터 : %s', querystring.stringify(param));
parse() : 주소 문자열을 파싱하여 URL 객체를 만들어 줍니다.
format() : URL 객체를 주소 문자열로 변환합니다.
https://로 시작하는 주소 문자열은 parse()메소드를 사용해 URL 객체로 만들어 졌다가 format()메소드를 사용해 다시 원래의 주소 문자열로 변환되었습니다.
요청 파라미터는 &기호로 구분되는데 querystring 모듈을 사용하면 요청 파라미터를 쉽게 분리할 수 있습니다.
stringify() 메소드는 객체 안에 있는 요청 파라미터를 다시 하나의 문자열로 바꿀 때 사용합니다.
4-2. 이벤트 이해하기
노드 대부분 이벤트를 기반으로 하는 비동기 방식으로 처리합니다. 그리고 비동기 방식으로 처리하기 위해 서로 이벤트를 전달합니다. 노드에서는 이런 이벤트를 보내고 받을 수 있도록 EventEmitter라는 것이 만들어져 있습니다.
EventEmitter 객체의 on()과 emit() 메소드를 사용할 수 있습니다. on() 메소드는 이벤트 리스너를 설정하는 역할을 하는데 객체로 전달된 이벤트를 받아 처리합니다. once() 메소드는 이벤트를 딱 한번 받아서 처리할 수 있습니다. 실행하고 나면 자동으로 제거됩니다. 이벤트를 다른 쪽으로 전달하고 싶을 땐 emit()을 사용합니다.
process.on('exit', function(){
console.log('exit 이벤트 발생함.');
});
setTimeout(function(){
console.log('2초 후에 시스템 종료 시도함.');
process.exit();
}, 2000);
console.log('바로 실행.');
process.on('tick', (count)=> {
console.log('tick 이벤트 발생 : %s', count);
});
setTimeout( () => {
console.log('2초 후에 tick 이벤트 전달 시도');
process.emit('tick', '2');
}, 2000);
[ch04_test4.js]
const calculator = require("./calc3");
const calc1 = new calculator();
calc1.emit("stop");
console.log(calculator.title + "에 stop 이벤트 전달");
[calc3.js]
const util = require("util");
const EventEmitter = require("events").EventEmitter;
const calculator = function() {
const self = this;
this.on("stop", () => {
console.log("calculator에 stop event 전달됨");
});
};
util.inherits(calculator, EventEmitter);
//Calc 객체가 EventEmitter를 상속받음
calculator.prototype.add = (a, b) => {
return a + b;
};
module.exports = calculator;
module.exports.title = "calculator 계산기 : ";
에러 :
4번째 줄에 화살표 함수를 썼을 때 에러가 났었습니다. 그냥 function () {} 함수로 써줘야 에러가 안났습니다.
처음 코드에서 에러가 났던 이유 : ' Object.setPrototypeOf called on null or undefined ~~'
자바스크립트 화살표 함수를 사용해서는 안되는 경우가 몇가지 있었어요. '프로토타입'으로 쓰일 때 화살표 함수로 써버리면 내부의 this가 함수를 호출한 instance를 가리키지 않고 상위 컨텍스트인 전역 객체 window를 가리킨다고 하더군요..
참고 : https://velog.io/@yhe228/화살표-함수에서-this
4-3. 파일 다루기
노드 파일 시스템은 파일을 다루는 기능과 디렉터리를 다루는 기능으로 구성되어 있고, 동기식 IO와 비동기식 IO 기능을 함께 제공합니다.
메소드
readFile(filename, [encoding], [callback]) : 비동기식 IO로 파일을 읽어 들입니다.
readFileSync(filename, [encoding]) : 동기식 IO로 파일을 읽어 들입니다.
writeFile(filename, data, encoding='utf8', [callback]) : 비동기식 IO로 파일을 씁니다.
writeFileSync(filename, data, encoding='utf8') : 동기식 IO로 파일을 씁니다.
const fs = require('fs');
//파일을 동기식 IO로 읽기
const data = fs.readFileSync('./package.json', 'utf8');
console.log(data);
const fs = require('fs');
//파일을 비동기식 IO로 읽기
fs.readFile('./package.json', 'utf8', (err, data) => {
//읽어 들인 데이터 출력.
console.log(data);
});
console.log('프로젝트 폴더 안의 package.json 파일을 읽도록 요청.');
const fs = require('fs');
//파일을 비동기식 IO로 쓰기
fs.writeFile('./output.txt', 'Hello World', (err) => {
if (err) {
console.log('Error : ', err);
}
console.log('output.txt 파일에 데이터 쓰기 완료.');
});
메소드
open(path, flags, [mode], [callback]) : 파일을 엽니다.
read(fd, buffer, offset, length, position, [callback]) : 지정한 부분의 파일 내용을 읽어 들입니다.
write(fd, buffer, offset, length, position, [callback]) : 파일의 지정한 부분에 데이터를 씁니다.
close(fd, [callback]) : 파일을 닫아 줍니다.
플래그
r : 읽기에 사용. 파일이 없으면 예외 발생.
w : 쓰기에 사용. 파일이 없으면 만들어지고 파일이 있으면 이전 내용 모두 삭제.
w+ : 읽기와 쓰기에 모두 사용. 파일이 없으면 만들어지고 파일이 있으면 이전 내용 모두 삭제.
a+ : 읽기와 추가에 모두 사용. 파일이 없으면 만들어지고 파일이 있으면 이전 내용에 새로운 내용 추가.
const fs = require('fs');
fs.open('./output.txt', 'w', (err,fd) => {
if(err) throw err;
const buf = new Buffer.from('안녕!\n');
fs.write(fd, buf, 0, buf.length, null, (err, written, buffer) => {
if(err) throw err;
console.log(err, written, buffer);
fs.close(fd, () => {
console.log('파일 열고 데이터 쓰고 파일 닫기 완료.');
});
});
});
(node:12208) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead.
이런 문구가 떠서 검색해봤더니 Buffer() 대신에 Buffer.from() 이나 Buffer.alloc()를 사용하라는 글을 발견했습니다.
const fs = require('fs');
fs.open('./output.txt', 'r', (err,fd) => {
if(err) throw err;
const buf = new Buffer.alloc(10);
console.log('버퍼타입 : %s', Buffer.isBuffer(buf));
fs.read(fd, buf, 0, buf.length, null, (err, bytesRead, buffer) => {
if(err) throw err;
const inStr = buffer.toString('utf8', 0, bytesRead);
console.log('파일에서 읽은 데이터 : %s', inStr);
console.log(err, bytesRead, buffer);
fs.close(fd, () => {
console.log('파일 열고 읽기 완료.');
});
});
});
Buffer 객체는 바이너리 데이터를 읽고 쓰는데 사용합니다. 새로운 버퍼 객체를 만들기 위해서는 new 연산자를 사용하며, 그 안에 들어갈 바이트(byte) 데이터의 크기만 지정하면 됩니다.
write() 메소드를 사용해 문자열을 버퍼에 쓰거나 처음부터 문자열을 사용해 버퍼 객체를 만들 수도 있습니다. 파일을 실행하면 파일에서 읽은 데이터가 콘솔창에 출력됩니다.
Buffer 객체를 사용하는 방법
두 개의 버퍼를 서로 다른 방식으로 만듭니다. 하나는 빈 버퍼를 먼저 만들고 그 안에 문자열을 넣었으며, 다른 하나는 버퍼를 만들면서 문자열을 파라미터로 전달하였습니다. 이 두개의 버퍼에 대해 toString() 메소드를 호출하여 결과 문자열을 확인해 보면 문자열이 똑같이 들어 있는 것을 알 수 있습니다.
// 버퍼 객체를 크기만 지정하여 만든 후 문자열을 씁니다.
const output = '안녕 1!';
const buffer1= new Buffer.alloc(10);
const len = buffer1.write(output,'utf8');
console.log('첫 번째 버퍼의 문자열 : %s', buffer1.toString());
// 버퍼 객체를 문자열을 이용해 만듭니다.
const buffer2= new Buffer.from('안녕 2!', 'utf8');
console.log('두 번째 버퍼의 문자열 : %s', buffer2.toString());
// 타입을 확인
console.log('버퍼 객체의 타입 : %s', Buffer.isBuffer(buffer1));
// 버퍼 객체에 들어 있는 문자열 데이터를 문자열 변수로
const byteLen = Buffer.byteLength(output);
const str1 = buffer1.toString('utf8', 0, byteLen);
const str2 = buffer2.toString('utf8');
// 첫 번째 버퍼 객체의 문자열을 두 번째 버퍼 객체로 복사
buffer1.copy(buffer2, 0,0, len);
console.log('두 번째 버퍼에 복사한 후의 문자열 : %s', buffer2.toString('utf8'));
// 두 개의 버퍼를 붙여 줍니다.
const buffer3 = Buffer.concat([buffer1, buffer2]);
console.log('두 개의 버퍼를 붙인 후의 문자열 : %s', buffer3.toString('utf8'));
다만 크기를 먼저 지정하면 나머지 공간이 그대로 버퍼에 남아 있게 됩니다. 변수에 들어 있는 것이 버퍼 객체인지 아닌지 확인할 때는 isBuffer()메소드를 사용합니다. 하나의 버퍼 객체를 다른 버퍼 객체로 복사할 때는 copy() 메소드를 사용하며, 두 개의 버퍼를 하나로 붙여서 새로운 버퍼 객체를 만들 때는 concat() 메소드를 사용합니다.
버퍼에 대해 이해했다면, 파일을 읽었을 때 콜백 함수로 전달된 버퍼를 이용해 문자열을 만드는 부분도 이해가 될 것 입니다.
스트림 단위로 파일 읽고 쓰기
파일을 읽고 쓸 때는 데이터 단위가 아닌 스트림 단위로 처리할 수도 있습니다. 스트림은 데이터가 전달되는 통로와 같은 개념입니다.
createReadStream(path, [options]) : 파일을 읽기 위한 스트림 객체를 만듭니다.
createWriteStream(path, [options]) : 파일을 쓰기 위한 스트림 객체를 만듭니다.
옵션
flags, encoding, autoClose 속성이 들어 있는 자바스크립트 객체를 전달 할 수 있습니다.
// output.txt.파일의 내용을 읽어 들인 후 output2.txt파일로 쓰는 코드
const fs = require('fs');
const infile = fs.createReadStream('./output.txt', {flags:'r'});
const outfile = fs.createWriteStream('./output2.txt', {flags:'w'});
infile.on('data', (data) => {
console.log('읽어 들인 데이터 : ', data.toString('utf8'));
outfile.write(data);
});
infile.on('end', () => {
console.log('파일 읽기 종료.');
outfile.end(() => {
console.log('파일 쓰기 종료.');
});
});
읽어 들인 데이터 : <Buffer 68 65 6c 6c 6f 20 77 6f 72 6c 64 0d 0a>
이렇게 나온 부분 때문에 7번째 라인에 data.toString('utf8'); 추가 함.
읽어 들인 데이터 : hello world 정상적으로 출력 됨.
앞서 살펴본 기능은 두 개의 스트림을 붙여 주면 더 간단하게 만들 수 있습니다.
pipe() 메소드는 두개의 스트림을 붙여주는 역할을 합니다.
ReadStream 타입의 객체와 WriteStream 타입의 객체를 붙여주면 스트림 간에 데이터를 알아서 전달합니다.
// output.txt.파일의 내용을 읽어 들인 후 output2.txt파일로 쓰는 코드
const fs = require('fs');
const inname = './output.txt';
const outname = './output2.txt';
fs.exists(outname, (exists) => {
if (exists) {
fs.unlink(outname, (err) => {
if(err) throw err;
console.log('기존파일 [', outname, '] 삭제함.' );
});
}
const infile = fs.createReadStream(inname, {flags:'r'});
const outfile = fs.createWriteStream(outname, {flags:'w'});
infile.pipe(outfile);
console.log('파일복사 [', inname, '] -> [', outname, ']' );
});
기존에 만들어 놓은 파일 output2.txt 파일이 있으면 중복 될 수 있으므로 같은 이름을 가진 파일을 다시 만들기 전에 먼저 삭제하도록 unlink() 메소드를 사용했습니다. 파일을 실행해 보면 pipe() 메소드로 두 개의 스트림 객체를 연결하기만 했는데도 파일 내용이 복사 된 것을 알 수 있습니다.
이렇게 스트림을 서로 연결하는 방법은 웹 서버를 만들고 사용자의 요청을 처리할 때 유용합니다.
http 모듈로 요청받은 파일 내용을 읽고 응답하기
파일에서 스트림을 만든 후 클라이언트로 데이터를 보낼 수 있는 스트림 객체를 pipe() 메소드로 연결해줍니다. 두 객체의 연결이 가능한 이유는 파일에서 데이터를 읽어오기위해 만든 것도 스트림객체이고, 데이터를 쓰기 위해 웹서버에서 클라이언트 쪽에 만든 것도 스트림 객체이기 때문입니다. 따라서 읽기 스트림과 쓰기 스트림은 pipe() 메소드를 사용해 연결할 수 있습니다.
//http 모듈을 사용해 사용자로부터 요청을 받았을 때 파일의 내용을 읽어 응답으로 보내는 코드.
const fs = require('fs');
const http = require('http');
const server = http.createServer((req, res) =>{
const instream = fs.createReadStream('./output.txt');
instream.pipe(res);
});
server.listen(7001, '127.0.0.1');
const fs = require('fs');
fs.mkdir('./docs', 0666, (err) =>{
if(err) throw err;
console.log('새로운 docs 폴더를 만들었습니다.');
fs.rmdir('./docs', (err) => {
if(err) throw err;
console.log('새로운 docs 폴더를 삭제했습니다.');
});
});
4-3. 로그 파일 남기기
console 객체의 log() 또는 error () 메소드 등을 호출하면 로그를 출력할 수 있습니다.
그런데 프로그램의 크기가 커질수록 로그의 양도 많아지고 로그를 보관했다가 나중에 확인해야 하는 경우도 생깁니다. 로그를 보관하려며 화면에 출력하는 것만으로는 부족합니다. winston 모듈로 로그를 남기는 방법을 알아봅니다.
const winston = require('winston'); //로그처리 모듈
const winstonDaily = require('winston-daily-rotate-file');//로그 일별 처리 모듈
const moment = require('moment');
function timeStampFormat() {
return moment().format('YYYY-MM-DD HH:mm:ss.SSS ZZ');
// ex) '2020-02-25 20:20:20.500 +0900'
};
const logger = winston.createLogger({
transports : [
new (winstonDaily)({
name : 'info-file',
filename: './log/server',
datePattern: 'YYYY-MM-DD',
colorize:false,
maxsize: 50000000,
maxFiles: 1000,
level: 'info',
showlevel: true,
json:false,
timestamp: timeStampFormat,
}),
new (winston.transports.Console)({
name : 'debug-console',
colorize: true,
level: 'debug',
showlevel: true,
json:false,
timestamp: timeStampFormat,
})
],
exceptionHandlers: [
new (winstonDaily)({
name : 'exception-file',
filename: './log/exception',
datePattern: 'YYYY-MM-DD',
colorize:false,
maxsize: 50000000,
maxFiles: 1000,
level: 'error',
showlevel: true,
json:false,
timestamp: timeStampFormat,
}),
new (winston.transports.Console)({
name : 'exception-console',
colorize: true,
level: 'debug',
showlevel: true,
json:false,
timestamp: timeStampFormat,
})
]
});
에러 1 :no such file or directory, mkdir 'log\server._yyyy-02-Tu.2/25\' at Object.mkdirSync (fs.js:840:3)
const logger = new (winston.Logger)({
^
TypeError: winston.Logger is not a constructor
at Object.<anonymous> (E:\nodejs_doit\ch04_test15.js:10:16)
at Module._compile (internal/modules/cjs/loader.js:1157:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1177:10)
at Module.load (internal/modules/cjs/loader.js:1001:32)
at Function.Module._load (internal/modules/cjs/loader.js:900:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:74:12)
at internal/main/run_main_module.js:18:47
10번째 줄 const logger = new (winston.Logger)({
수정 : const logger = winston.createLogger({
에러2 : no such file or directory, mkdir 'log\server._yyyy-02-Tu.2/25\' at Object.mkdirSync (fs.js:840:3)
internal/fs/utils.js:230
throw err;
^
Error: ENOENT: no such file or directory, mkdir 'log\server._yyyy-02-Tu.2/25\' at Object.mkdirSync (fs.js:840:3)
at E:\nodejs_doit\node_modules\file-stream-rotator\FileStreamRotator.js:654:20
at Array.reduce (<anonymous>)
at mkDirForFile (E:\nodejs_doit\node_modules\file-stream-rotator\FileStreamRotator.js:642:27)
at Object.FileStreamRotator.getStream (E:\nodejs_doit\node_modules\file-stream-rotator\FileStreamRotator.js:519:5)
at new DailyRotateFile (E:\nodejs_doit\node_modules\winston-daily-rotate-file\daily-rotate-file.js:80:57)
at Object.<anonymous> (E:\nodejs_doit\ch04_test15.js:12:5)
at Module._compile (internal/modules/cjs/loader.js:1157:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1177:10)
at Module.load (internal/modules/cjs/loader.js:1001:32) {
errno: -4058,
syscall: 'mkdir',
code: 'ENOENT',
path: 'log\\server._yyyy-02-Tu.2/25\\'
}
15, 38번째줄 : datePattern: '_yyyy-MM-dd.log',
수정 : datePattern: 'YYYY-MM-DD',
winston 모듈로 만드는 로거(Logger, 로그를 출력하는 객체를 말할 때 사용하는 용어)는 transports라는 속성 값으로 여러 개의 설정 정보를 전달할 수 있습니다. 이름이 info-file인 설정 정보는 매일 새로운 파일에 로그를 기록하도록 설정하고, info 수준의 로그만 기록하도록 설정했습니다.
로그파일의 크기가 50MB를 넘어가면 자동으로 새로운 파일로 생성되며, 이때 자동으로 분리되어 생성되는 파일의 개수는 최대 1000개까지 가능합니다. 이런 정보돌은 name, level, maxsize, maxFiles 등의 속성으로 설정합니다. 콘솔 창에 출력되는 로그는 debug 수준까지 출력되도록 설정하고 컬러도 적용했습니다.
예제의 코드를 다른 프로그램에서 사용하고 싶다면 파일 이름이나 설정 부분을 약간씩 수정하면서 사용하세요.
로그 수준(Log Level)
로그 수준이란 어떤 정보까지 출력할 것인지 결정하는 것입다. 오류만 보여줄 것인지 아니면 사소한 정보까지 모두 보여 줄 것인지를 로그 수준으로 결정합니다. winston모듈에서 사용하는 로그 수준은 단게별로 구성되며, 하위 수준은 상위 수준을 모두 포함하여 출력합니다.
debug:0 > info:1 > notice:2 > warning:3 > error:4 > crit:5 > alert:6 > emerg:7