웹 브라우저에 사용되는 자바스크립트는 운영체제의 정보를 가져올 수 없지만, 노드는 os 모듈에 정보가 담겨 있어 정보를 가져올 수 있다.
const os = require('os');
일반적인 웹 서비스를 제작할 때는 사용 빈도가 높지 않지만 운영체제별로 다른 서비스를 제공하고 싶을 때 os 모듈을 사용한다.
경로 구분자는 윈도 타입(\)과 POSIX 타입(/)으로 구분된다.
운영체제별로 경로 구분자가 다르기 때문에 폴더와 파일의 경로를 쉽게 조작하도록 도와준다.
const path = require('path');
인터넷 주소를 쉽게 조작하도록 도와주는 모듈이다.
WHATWG 방식의 url과 예전부터 노드에서 사용하던 방식의 url이 있으며 요즘은 WHATWG 방식만 사용한다.
const url = require('url');
url 모듈 안에 URL 생성자가 있다.
URL은 노드 내장 객체이기도 하여 requrie 할 필요는 없다.
해당 생성자에 주소를 넣어 객체로 만들면 주소가 부분별로 정리된다.
const myURL = new URL('http://www.gilbut.co.kr/book/bookList.aspx?sercate1=001001000#anchor');
주소가 host 부분 없이 pathname만 오는 경우 new URL('/book/bookList.apsx', 'https://www.gilbut.co.kr') 두 번째 인수로 host를 적어주어야 한다.
생성된 객체에는 username, password, origin, searchParams 속성이 존재한다.
console.log('new URL():', myURL);
/*new URL(): URL {
href: 'http://www.gilbut.co.kr/book/bookList.aspx?sercate 1 = 001001000 #anchor',
origin: 'http://www.gilbut.co.kr' ,
protocol: 'http:',
username: '',
password: '',
host: 'www.gilbut.co.kr',
hostname: 'www.gilbut.co.kr',
port: '',
pathname: '/book/bookList.aspx',
search: '?sercate 1 = 001001000 ',
searchParams: URLSearchParams { 'sercate 1 ' => '001001000 ' },
hash: '#anchor'
}*/
• url.format(객체): 분해되었던 url 객체를 다시 원래 상태로 조립한다.
console.log('url.format():', url.format(myURL));
• searchParams 객체
search는 물음표(?)로 시작하고, 그 뒤에 키=값 형식으로 데이터를 전달한다.
new URLSearchParams(myURL.search)로도 같은 결괏값을 얻을 수 있다.
const myURL = new URL('http://www.gilbut.co.kr/?page=3&limit=10&category=nodejs&category=javascript');
도메인을 통해 IP나 기타 DNS 정보를 얻고자 할 때 사용한다.
import dns from 'dns/promises';
ip 주소는 간단하게 dns.lookup 이나 dns.resolve(도메인)으로 얻을 수 있다.
const ip = await dns.lookup('gilbut.co.kr');
console.log('IP', ip);
// IP { address: ' 49 . 236 . 151 . 220 ', family: 4 }
다양한 방식의 암호화를 도와주는 모듈이다.
const crypto = require('crypto');
단방향 암호화
복호화할 수 없는 암호화 방식 → 해시 함수라고 불린다
어떠한 문자열을 고정된 길이의 다른 문자열로 바꿔버리는 방식인 해시 기법을 사용한다.
console.log('base64:', crypto.createHash('sha512').update('비밀번호').digest('base64'));
// base64: dvfV6nyLRRt3NxKSlTHOkkEGgqW2HRtfu19Ou/psUXvwlebbXCboxIPmDYOFRIpqav2eUTBFuHaZri5x+usy1g==
• createHash(알고리즘): 사용할 해시 알고리즘을 넣는다. 현재는 sha512 정도로 충분하다.
• update(문자열): 변환할 문자열을 넣는다.
• digest(인코딩): 인코딩할 알고리즘을 넣는다. base64가 결과 문자열이 가장 짧아서 주로 사용된다. 결과물로 변환된 문자열을 반환한다.
양방향 암호화
암호화된 문자열을 복호화할 수 있으며, 키(열쇠)라는 것을 사용해 복호화한다.
• crypto.createCipheriv(알고리즘, 키, iv): 암호화 알고리즘과 키, iv를 넣는다.
• cipher.update(문자열, 인코딩, 출력 인코딩): 암호화할 대상과 대상의 인코딩, 출력 결과물의 인코딩을 넣는다.
• cipher.final(출력 인코딩): 출력 결과물의 인코딩을 넣으면 암호화가 완료된다.
• crypto.createDecipheriv(알고리즘, 키, iv): 복호화할 때 사용합니다. 암호화할 때 사용했던 알고리즘과 키, iv를 그대로 넣어야 한다.
• decipher.update(문자열, 인코딩, 출력 인코딩): 암호화된 문장, 그 문장의 인코딩, 복호화할 인코딩을 넣는다. createCipheriv의 update()에서 넣은 순서와 반대로 넣는다.
• decipher.final(출력 인코딩): 복호화 결과물의 인코딩을 넣는다.
각종 편의 기능을 모아둔 모듈이다.
const util = require('util');
• util.deprecate: 함수가 deprecated 처리되었음을 알린다. 첫 번째 인수로 넣은 함수를 사용했을 때 경고 메시지가 출력된다. 두 번째 인수로 경고 메시지 내용을 넣으면 된다. 함수가 조만간 사라지거나 변경될 때 알려줄 수 있어 유용하다.
• util.promisify: 콜백 패턴을 프로미스 패턴으로 바꾼다. 바꿀 함수를 인수로 제공하면 된다. 이렇게 바꿔두면 async/await 패턴까지 사용할 수 있다.
노드에서 멀티 스레드 방식으로 작업할 수 있다.
const {
Worker, isMainThread, parentPort,
} = require('worker_threads');
isMainThread : 현재 코드가 메인 스레드 에서 실행되는지, 아니면 우리가 생성한 워커 스레드에서 실행되는지 구분된다.
if (isMainThread) { // 부모일 때
const worker = new Worker(__filename); // 현재 파일을 new Worker을 워커 스레드에서 실행시킴
worker.on('message', message => console.log('from worker', message));
worker.on('exit', () => console.log('worker exit')); // 워커와 연결이 종료될 때 실행됨
worker.postMessage('ping'); // worker.postMessage - 워커에 데이터를 보냄
} else { // 워커일 때
parentPort.on('message', (value) => { // parentPort.on('message') - 부모로부터 메세지를 받음
console.log('from parent', value);
parentPort.postMessage('pong'); // parentPort.postMessage - 부모에게 메세지를 보냄
parentPort.close(); // 부모와의 연결 종료
});
}
/*
from parent ping
from worker pong
worker exit
*/
여러 개의 워커 스레드에 데이터를 넘김
const {
Worker, isMainThread, parentPort, workerData,
} = require('worker_threads');
if (isMainThread) { // 부모일 때
const threads = new Set();
threads.add(new Worker(__filename, { // 워커 스레드 1
workerData: { start: 1 },
}));
threads.add(new Worker(__filename, {
workerData: { start: 2 }, // 워커 스레드 2
}));
for (let worker of threads) { // 워커 1 메세지, 워커 2 메세지
worker.on('message', message => console.log('from worker', message));
worker.on('exit', () => {
threads.delete(worker);
if (threads.size === 0) { // 워커 스레드가 모두 종료되었을 때
console.log('job done');
}
});
}
} else { // 워커일 때
const data = workerData;
parentPort.postMessage(data.start + 100);
}
/*
from worker 101
from worker 102
job done
*/
노드에서 다른 프로그램을 실행하고 싶거나 명령어를 수행하고 싶을 때 사용하는 모듈이다.
= 다른 언어의 코드를 실행하고 결괏값을 받을 수 있다.
- dir 실행
const exec = require('child_process').exec;
const process = exec('넣고 싶은 명령어'); // dir
// 결과는 stdout(표준출력)과 stderr(표준에러)에 붙여둔 data 이벤트 리스너에 버퍼 형태로 전달됨
process.stdout.on('data', function(data) { // 외부 명령어의 표준 출력(stdout)을 감지
console.log(data.toString());
}); // 실행 결과
process.stderr.on('data', function(data) { // 외부 명령어의 표준에러(stderr)를 감지
console.error(data.toString());
}); // 실행 에러
/*
chcp 65001
*/
- python 실행
const spawn = require('child_process').spawn; // spawn - 파이썬 코드를 실행하는 명령어
const process = spawn('python', ['test.py']); // spawn 첫번쩨 인수로 명령어, 두번째 인수로 옵션 배열을 넣음
// 결과는 exec과 마찬가지로 stdout, stderr의 데이터로 나옴
process.stdout.on('data', function(data) {
console.log(data.toString());
}); // 실행 결과
process.stderr.on('data', function(data) {
console.error(data.toString());
}); // 실행 에러
• async_hooks: 비동기 코드의 흐름을 추적할 수 있는 실험적인 모듈이다.
• dgram: UDP와 관련된 작업을 할 때 사용한다.
• net: HTTP보다 로우 레벨인 TCP나 IPC 통신을 할 때 사용한다.
• perf_hooks: 성능 측정을 할 때 console.time보다 더 정교하게 측정한다.
• querystring: URLSearchParams가 나오기 이전에 쿼리스트링을 다루기 위해 사용했던 모듈이다.
• string_decoder: 버퍼 데이터를 문자열로 바꾸는 데 사용한다.
• tls: TLS와 SSL에 관련된 작업을 할 때 사용 한다.
• tty: 터미널과 관련된 작업을 할 때 사용 한다.
• v8: v8 엔진에 직접 접근할 때 사용 한다.
• vm: 가상 머신에 직접 접근할 때 사용 한다.
• wasi: 웹어셈블리를 실행할 때 사용하는 실험적인 모듈이다..
const fs = require('fs');
console.log('시작');
fs.readFile('./readme2.txt', (err, data) => {
if (err) {
throw err;
}
console.log('1번', data.toString());
});
fs.readFile('./readme2.txt', (err, data) => {
if (err) {
throw err;
}
console.log('2번', data.toString());
});
fs.readFile('./readme2.txt', (err, data) => {
if (err) {
throw err;
}
console.log('3번', data.toString());
});
console.log('끝');
/*
시작
끝
2번 저를 여러 번 읽어보세요.
3번 저를 여러 번 읽어보세요.
1번 저를 여러 번 읽어보세요.
*/
비동기 메서드들은 백그라운드에 해당 파일을 읽으라고만 요청하고 다음 작업으로 넘어간다.
→ 파일 읽기 요청만 세 번을 보낸 후 끝 출력
백그라운드에서는 요청 세 개를 거의 동시에 실행한다.
동기 | 순차적으로 실행 이전 작업이 완료될 때까지 대기 |
백그라운드 작업 완료 확인 여부 |
비동기 | 다른 작업을 기다리지 않고 백그라운드에서 병렬로 실행 | |
블로킹 | 결과가 나올때까지 제어를 반환하지 않고 대기 | 함수가 바로 return 되는지 여부 |
논블로킹 | 결과가 나오지 않아도 제어를 즉시 반환하여 다른 작업을 실행 |
비동기 fs 메서드를 사용하면 백그라운드가 동시에 작업할 수도 있고, 메인 스레드는 다음 작업을 처리할 수 있다.
async/await을 이용해 비동기 방식을 이용하여 순차적으로 실행시킬 수도 있다.
console.log('시작');
fs.readFile('./readme2.txt')
.then((data) => {
console.log('1번', data.toString());
return fs.readFile('./readme2.txt');
})
.then((data) => {
console.log('2번', data.toString());
return fs.readFile('./readme2.txt');
})
.then((data) => {
console.log('3번', data.toString());
console.log('끝');
})
.catch((err) => {
console.error(err);
});
노드는 파일을 읽을 때 메모리에 파일 크기만큼 공간을 마련해 두며 파일 데이터를 메모리에 저장한 뒤 사용자가 조작할 수 있도록 한다.
메모리에 저장된 데이터가 바로 버퍼이다.
Buffer클래스
from(문자열): 문자열을 버퍼로 바꾼다.
• toString(버퍼): 버퍼를 다시 문자열로 바꾼다.
• concat(배열): 배열 안에 든 버퍼들을 하나로 합친다.
• alloc(바이트): 바이트를 인수로 넣으면 해당 크기의 버퍼가 생성된다.
파일을 읽는 createReadStream 메서드
const readStream = fs.createReadStream('./readme3.txt', { highWaterMark: 16 });
// 첫 번째 인수는 파일 경로, 두 번째 인수는 옵션 객체
// highWaterMark라는 옵션이 버퍼의 크기(바이트 단위)를 정할 수 있는 옵션
readStream.on('data', (chunk) => { // data 이벤트 리스너 - 파일을 읽기 시작할 때 발생
data.push(chunk);
console.log('data :', chunk, chunk.length);
});
readStream.on('end', () => { // end 이벤트 리스너 - 파일을 다 읽으면 발생
console.log('end :', Buffer.concat(data).toString());
});
readStream.on('error', (err) => { // error 이벤트 리스너 - 파일을 읽는 도중 에러 발생
console.log('error :', err);
});
readStream은 이벤트 리스너를 붙여서 사용한다.
const writeStream = fs.createWriteStream('./writeme2.txt');
writeStream.on('finish', () => {
console.log('파일 쓰기 완료');
});
writeStream.write('이 글을 씁니다.'); // write 메서드로 넣을 데이터를 작성
writeStream.write('한 번 더 씁니다.');
writeStream.end(); // 데이터를 다 썼다면 종료를 알림 -> finish 이벤트 발생
파일 복사
- writeFileSync 사용 → 파일 용량만큼 메모리가 필요하다.
const data1 = fs.readFileSync('./big.txt');
fs.writeFileSync('./big2.txt', data1);
- 스트림 사용 → 큰 파일을 조각내어 작은 버퍼 단위로 옮긴다.
const readStream = fs.createReadStream('readme4.txt');
const writeStream = fs.createWriteStream('writeme3.txt');
readStream.pipe(writeStream); // on, writeStream.write를 하지 않아도 됨
미리 읽기 스트림과 쓰기 스트림을 만들어둔 후 두 개의 스트림 사이를 pipe 메서드로 연결하면 저절로 데이터가 넘어간다.
스트림을 사용해 파일을 복사하는 방법은 큰 파일을 조각내어 작은 버퍼 단위로 옮기기 때문에 writeFileSync 방식보다 메모리 차지가 적다.
pipeline메서드를 이용해 여러 개의 파이프를 연결할 수 있다.
import { pipeline } from 'stream/promises';
import zlib from 'zlib';
import fs from 'fs';
const ac = new AbortController(); // AbortController을 이용해 원할 때 파이프 중단 가능
const signal = ac.signal;
setTimeout(() => ac.abort(), 1); // 1ms 뒤에 중단
await pipeline(
fs.createReadStream('./readme4.txt'),
zlib.createGzip(),
fs.createWriteStream('./readme4.txt.gz'),
{ signal },
);
이때 pipeline의 마지막 인수로 { signal }을 추가하해 원하는 시점에 ac.abort()를 호출하면 중단할 수 있다.
파일을 생성하고 삭제할 수도 있으며 폴더를 생성하고 삭제할 수도 있다.
const fs = require('fs').promises;
const constants = require('fs').constants;
fs.access('./folder', constants.F_OK | constants.W_OK | constants.R_OK)
• fs.access(경로, 옵션, 콜백): 폴더나 파일에 접근할 수 있는지를 체크한다. 두 번째 인수로 상수들(constants를 통해 가져옴)을 넣는다. F_OK는 파일 존재 여부, R_OK는 읽기 권한 여부, W_OK는 쓰기 권한 여부를 체크한다. 파일/폴더나 권한이 없다면 에러가 발생하는데, 파일/폴더가 없을 때의 에러 코드는 ENOENT이다.
return fs.mkdir('./folder');
• fs.mkdir(경로, 콜백): 폴더를 만드는 메서드이다. 이미 폴더가 있다면 에러가 발생하므로 먼저 access 메서드를 호출해서 확인해야 한다.
return fs.open('./folder/file.js', 'w');
• fs.open(경로, 옵션, 콜백): 파일의 아이디(fd 변수)를 가져오는 메서드이다. 파일이 없다면 파일을 생성한 뒤 그 아이디를 가져온다. 가져온 아이디를 사용해 fs.read 또는 fs.write로 읽거나 쓸 수 있다. 두 번째 인수로 어떤 동작을 할 것인지를 설정할 수 있다. 쓰려면 w, 읽으려면 r, 기존 파일에 추가하려면 a이다.
return fs.rename('./folder/file.js', './folder/newfile.js');
• fs.rename(기존 경로, 새 경로, 콜백): 파일의 이름을 바꾸는 메서드이다. 기존 파일 위치와 새로운 파일 위치를 적으면 된다.
fs.readdir('./folder')
• fs.readdir(경로, 콜백): 폴더 안의 내용물을 확인할 수 있다. 배열 안에 내부 파일과 폴더명이 나온다.
return fs.unlink('./folder/newfile.js');
• fs.unlink(경로, 콜백): 파일을 지울 수 있다. 파일이 없다면 에러가 발생하므로 먼저 파일이 있는지를 꼭 확인해야 한다.
return fs.rmdir('./folder');
• fs.rmdir(경로, 콜백): 폴더를 지울 수 있다. 폴더 안에 파일들이 있다면 에러가 발생하므로 먼저 내부 파일을 모두 지우고 호출해야 한다.
• copyfile로 간단하게 파일 복사
fs.copyFile('readme4.txt', 'writeme4.txt')
.then(() => {
console.log('복사 완료');
})
.catch((error) => {
console.error(error);
});
• watch 메서드로 파일/폴더의 변경 사항을 감시
fs.watch('./target.txt', (eventType, filename) => {
console.log(eventType, filename);
});
내용물을 수정할 때는 change 이벤트가 발생하고, 파일명을 변경하거나 파일을 삭제하면 rename 이벤트가 발생한다.
rename 이벤트가 발생한 후에는 더 이상 watch가 수행되지 않는다
비동기 메서드들은 백그라운드에서 실행되고, 실행된 후에는 다시 메인 스레드의 콜백 함수나 프로미스의 then 부분이 실행된다.
→ fs 메서드를 여러 번 실행해도 백그라운드에서 동시에 처리되는데, 바로 스레드 풀 때문이다.
기본적인 스레드 풀의 개수에 따라 작업을 처리하며 SET UV_THREADPOOL_SIZE에 따라 처리 양이 달라진다.
const EventEmitter = require('events');
events 모듈을 사용하면 된다.
• on(이벤트명, 콜백): 이벤트 이름과 이벤트 발생 시의 콜백을 연결한다. 이렇게 연결하는 동작을 이벤트 리스닝이라고 한다.
• addListener(이벤트명, 콜백): on과 기능이 같다.
• emit(이벤트명): 이벤트를 호출하는 메서드이다. 이벤트 이름을 인수로 넣으면 미리 등록해 뒀던 이벤트 콜백이 실행된다.
• once(이벤트명, 콜백): 한 번만 실행되는 이벤트이다. myEvent.emit('event3')을 두 번 연속 호출해도 콜백이 한 번만 실행된다.
• removeAllListeners(이벤트명): 이벤트에 연결된 모든 이벤트 리스너를 제거한다.
• removeListener(이벤트명, 리스너): 이벤트에 연결된 리스너를 하나씩 제거한다.
• off(이벤트명, 콜백): 노드 10 버전에서 추가된 메서드로, removeListener와 기능이 같다.
• listenerCount(이벤트명): 현재 리스너가 몇 개 연결되어 있는지 확인한다.
예외란 보통 처리하지 못한 에러를 말한다.
- try/catch 문
setInterval(() => {
console.log('시작');
try {
throw new Error('서버를 고장내주마!');
} catch (err) {
console.error(err);
}
}, 1000);
- 노드 자체에서 잡아주는 에러
const fs = require('fs');
setInterval(() => {
fs.unlink('./abcdefg.js', (err) => {
if (err) {
console.error(err);
}
});
}, 1000);
에러가 발생하지만 노드 내장 모듈의 에러는 실행 중인 프로세스를 멈추지 않는다.
에러를 throw하면 노드 프로세스가 멈춰버리기 때문에 이 경우는 try/catch문으로 throw한 에러를 잡아야 한다.
예측 불가능한 에러 처리
process.on('uncaughtException', (err) => { // process 객체에 uncaughtException 이벤트 리스너를 단다.
console.error('예기치 못한 에러', err);
});
setInterval(() => {
throw new Error('서버를 고장내주마!');
}, 1000);
setTimeout(() => {
console.log('실행됩니다');
}, 2000);
처리하지 못한 에러가 발생했을 때 uncaughtException 이벤트 리스너가 실행되고 프로세스가 유지된다.
그러나 uncaughException 이벤트 발생 후 다음 동작이 제대로 동작하는지를 보증하지 않는다.
uncaughtException은 단순히 에러 내용을 기록하는 정도로 사용하고, 에러를 기록한 후 process.exit()으로 프로세스를 종료하는 것이 적절하다.
빈칸 채우기
코드 작성 문제 1
setInterval(() => {
throw new Error('서버를 고장내주마!');
}, 1000);
setTimeout(() => {
console.log('누구맘대로!');
}, 2000);
정말 예측이 불가능한 에러가 발생한 상황에서 에러 처리를 하려면?
process.on('uncaughtException',(err) => {
console.error("예기치 못한 에러 발생", err);
});
코드 작성 문제 2
const {
Worker, isMainThread, parentPort,
} = require('worker_threads');
if ( ) { // 현재 스레드가 메인 스레드인지 아니면 워커 스레드인지 확인
const worker = new Worker(__filename);
worker.on('message', message => console.log('from worker', message));
worker.on('exit', () => console.log('worker 종료'));
// 워커 스레드에 'hello' 메세지 전달
} else {
parentPort.on('message', (value) => {
console.log('from parent', value);
// 부모 스레드에 'I love you' 메세지 전달
parentPort.close();
});
}
const {
Worker, isMainThread, parentPort,
} = require('worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
worker.on('message', message => console.log('from worker', message));
worker.on('exit', () => console.log('worker 종료'));
worker.postMessage('hello');
} else {
parentPort.on('message', (value) => {
console.log('from parent', value);
parentPort.postMessage('I love you');
parentPort.close();
});
}
출처: 조현영 , 『Node.js 교과서』 개정판 3판, 길벗, 3.5장 ~ 3.9장
[Node.js 1] 7장 MySQL (1) | 2023.11.24 |
---|---|
[Node.js 1] 6장 익스프레스 웹 서버 만들기 (0) | 2023.11.17 |
[노드1] 4장~5장 http 모듈로 서버 만들기 & 패키지 매니저 (0) | 2023.11.10 |
[Node.js 1] 1장 ~ 3장 노드 시작하기 (0) | 2023.10.13 |
[Node.js 1] 1장 ~ 3장 자바스크립트 복습 (0) | 2023.10.06 |