setInterval(() => {
console.log('시작');
try{
throw new Error('down server!!');
}catch(err){
console.error(err);
}
}, 1000);
파일 시스템 접근하기
fs 모듈은 파일 시스템에 접근하는 모듈이다. 즉, 파일을 생성하거나 삭제하고, 읽거나 쓸 수 있다.
폴더도 만들거나 지울 수 있다.
const fs = require('fs');
fs.readFile('./read.txt', (err, data) => {
if(err){
throw err;
}
console.log(data);
console.log(data.toString());
});
fs는 기본적으로 콜백 형식의 모듈이므로 실무에서 사용하기 불편하다. 따라서 fs 모듈을 프로미스 형식으로 바꿔주는 방법을 사용한다.
readFilePromise
const fs = require('fs').promises;
fs.readFile('./read.txt')
.then((data) => {
console.log(data);
console.log(data.toString());
})
.catch((err) => {
console.error(err);
});
writeFile
const fs = require('fs').promises;
fs.writeFile('./write.txt', 'text!!!')
.then(() => {
return fs.readFile('./write.txt');
})
.then((data) => {
console.log(data.toString());
})
.catch((err) => {
console.error(err);
});
동기 메서드와 비동기 메서드
setTimeout 같은 타이머와 process.nextTick 외에도, 노드는 대부분의 메서드를 비동기 방식으로 처리한다.
하지만 몇몇 메서드는 동기 방식으로 사용할 수 있다.
const fs = require('fs');
console.log('시작');
fs.readFile('./read.txt', (err,data) => {
if(err){
throw err;
}
console.log('1번', data.toString());
});
fs.readFile('./read.txt', (err,data) => {
if(err){
throw err;
}
console.log('2번', data.toString());
});
fs.readFile('./read.txt', (err,data) => {
if(err){
throw err;
}
console.log('3번', data.toString());
});
console.log('끝');
시작, 끝이 먼저 출력된 뒤 파일을 읽어온 결과가 출력된다. 나중에 읽기가 완료되면 백그라운드가 다시 메인 스레드에 알린다. 메인 스레드는 그제야 등록된 콜백 함수를 실행한다.
백그라운드에서는 요청 세 개를 동시에 실행한다.
순서를 보장하는 동기식으로도 가능하다.
const fs = require('fs');
console.log('시작');
let data = fs.readFileSync('./read.txt');
console.log('1번', data.toString());
data = fs.readFileSync('./read.txt');
console.log('2번', data.toString());
data = fs.readFileSync('./read.txt');
console.log('3번', data.toString());
console.log('끝');
비동기 방식으로 동작하는데 순서를 유지하고 싶다면 어떻게 해야 할까?
const fs = require('fs');
console.log('시작');
fs.readFile('./read.txt', (err,data) => {
if(err){
throw err;
}
console.log('1번', data.toString());
fs.readFile('./read.txt', (err,data) => {
if(err){
throw err;
}
console.log('2번', data.toString());
fs.readFile('./read.txt', (err,data) => {
if(err){
throw err;
}
console.log('3번', data.toString());
console.log('끝');
});
});
});
콜백 지옥이 발생할 수 있다. 이를 Promise나 async/await 으로 어느 정도 해결할 수 있다.
const fs = require('fs').promises;
console.log('시작');
fs.readFile('./read.txt')
.then((data) => {
console.log('1번', data.toString());
return fs.readFile('./read.txt');
})
.then((data) => {
console.log('2번', data.toString());
return fs.readFile('./read.txt');
})
.then((data) => {
console.log('3번', data.toString());
return fs.readFile('./read.txt');
})
.catch((err) => {
console.error(err);
});
버퍼와 스트림
파일을 읽거나 쓰는 방식에는 크게 두 가지 방식, 즉 버퍼를 이용하는 방식과 스트림을 이용하는 방식이 있다.
버퍼는 데이터를 모으는 공간을 이용하는 방식이다.
const buffer = Buffer.from('버퍼로 바꾸기');
console.log('from(): ', buffer);
console.log('length: ', buffer.length);
console.log('from(): ', buffer.toString());
const array = [Buffer.from('띄엄 '), Buffer.from('띄엄 '), Buffer.from('띄어쓰기')];
const buffer2 = Buffer.concat(array);
console.log('concat(): ', buffer2.toString());
const buffer3 = Buffer.alloc(5);
console.log('alloc(): ', buffer3);
readFile 방식의 버퍼가 편리하기는 하지만 문제점도 있다. 만약 용량이 100MB인 파일이 있으면 읽을 때 메모리에 100MB의 버퍼를 만들어야 한다.
그래서 버퍼의 크기를 작게 만든 후 여러 번으로 나눠 보내는 방식이 등장했다. 이것이 스트림이다.
const fs = require('fs');
const readStream = fs.createReadStream('./read.txt', { highWaterMark: 16});
const data = [];
readStream.on('data', (chunk) => {
data.push(chunk);
console.log('data: ',chunk, chunk.length);
});
readStream.on('end', () => {
console.log('end: ', Buffer.concat(data).toString());
});
readStream.on('error', (err) => {
console.log('error: ', err);
});
createReadStream으로 읽기 스트림을 만든다. 첫 번째 인수로 읽을 파일의 경로, 두 번째 인수는 옵션으로 highWaterMark라는 옵션으로 버퍼 크기를 정할 수 있다.
const fs = require('fs');
const writeStream = fs.createWriteStream('./write2.txt');
writeStream.on('finish', () =>{
console.log('파일 쓰기 완료');
});
writeStream.write('글을 쓴다.\n');
writeStream.write('글을 쓴다.\n');
writeStream.end();
writeStream으로 파일을 쓸 수 있다.
이러한 스트림을 연결하는 것을 '파이핑 한다'라고 표현한다.
const fs = require('fs');
const readStream = fs.createWriteStream('read.txt');
const writeStream = fs.createWriteStream('write3.txt');
readStream.pipe(writeStream);
pipe는 스트림 사이에 여러 번 열결 할 수 있다.
스트림을 사용하면 메모리를 적게 차지하고 버퍼를 사용하면 메모리를 많이 사용한다.
따라서 용량이 큰 데이터는 스트림을 자주 사용한다.
스레드풀
fs 모듈의 비동기 메서드들을 사용했다. 비동기 메서드들은 백그라운드에서 실행되고, 실행된 후에는 다시 메인 스레드의 콜백 함수나 프로미스의 then 부분이 실행된다. 이때 fs 메서드를 여러 번 실행해도 백그라운드에서 동시에 처리되는데, 바로 스레드풀이 있기 때문이다.
const crypto = require('crypto');
const pass = 'pass';
const salt = 'salt';
const start = Date.now();
crypto.pbkdf2(pass, salt, 100000, 128, 'sha512', () => {
console.log('1: ', Date.now() - start);
});
crypto.pbkdf2(pass, salt, 100000, 128, 'sha512', () => {
console.log('2: ', Date.now() - start);
});
crypto.pbkdf2(pass, salt, 100000, 128, 'sha512', () => {
console.log('3: ', Date.now() - start);
});
crypto.pbkdf2(pass, salt, 100000, 128, 'sha512', () => {
console.log('4: ', Date.now() - start);
});
crypto.pbkdf2(pass, salt, 100000, 128, 'sha512', () => {
console.log('5: ', Date.now() - start);
});
crypto.pbkdf2(pass, salt, 100000, 128, 'sha512', () => {
console.log('6: ', Date.now() - start);
});
실행할 때마다 시간과 순서가 달라진다. 스레드풀이 작업을 동시에 처리하므로 여섯 개의 작업 중에서 어느 것이 먼저 처리될지 모른다. 1~4의 실행 시간은 비슷하고 5~6은 차이가 있다. 이는 스레드풀의 개수가 네 개이기 때문이다.
이벤트
스트림을 사용할 때 on('data', 콜백) 또는 on('end', 콜백)을 사용했다. 바로 data라는 이벤트와 end라는 이벤트가 발생할 때 콜백 함수를 호출하도록 이벤트를 등록한 것이다.
이벤트를 직접 만들 수도 있다. 이벤트를 만들고, 호출하고, 삭제해보자.
const EventEmitter = require('events');
const myEvent = new EventEmitter();
myEvent.addListener('event1', () => {
console.log('이벤트 1');
});
myEvent.on('event2', () => {
console.log('이벤트 2');
});
myEvent.on('event2', () => {
console.log('이벤트 2 추가');
});
myEvent.once('event2', () => {
console.log('이벤트 3');
});
myEvent.emit('event1');
myEvent.emit('event2');
myEvent.emit('event3');
myEvent.emit('event3');
myEvent.on('event4', () => {
console.log('이벤트 4');
});
myEvent.removeAllListeners('event4');
myEvent.emit('event4');
once를 사용해서 3을 두 번 실행하면 한 번은 실행되지 않는다.
예외 처리
노드에서는 예외 처리가 정말 중요하다. 예외란 보통 처리하지 못한 에러를 가리킨다.
멀티 스레드 프로그램에서는 스레드 하나가 멈추면 그 일을 다른 스레드가 대신한다. 하지만 노드의 메인 스레드는 하나뿐이므로 그 하나를 소중히 다뤄야 한다.
강제로 error를 발생시키는 것이다.
노드에서 직접 잡아주는 error도 있다.
const fs = require('fs');
setInterval(() => {
fs.unlink('./abcdefg.js', (err) => {
if(err){
console.log(err);
}
});
}, 1000);
존재하지 않는 파일을 지우려 하여 발생하는 error이다.
프로미스 에러는 catch하지 않아도 알아서 처리된다.
const fs = require('fs').promises;
setInterval(() => {
fs.unlink('./abcdefg.js')
},1000);
uncaughtException 이벤트 리스너를 달아야 처리하지 못한 에러가 발생해도 이벤트 리스너가 실행되고 프로세스가 유지된다.