3.5 노드 내장 모듈 사용하기
노드는 웹 브라우저에서 사용되는 자바스크립트보다 더 많은 기능을 제공한다.
운영체제 정보에도 접근할 수 있고 클라이언트가 요청한 주소에 대한 정보도 가져올 수 있다.
3.5.1 os
웹 브라우저에 사용되는 자바스크립트는 운영체제의 정보를 가져올 수 없지만, 노드는 os 모듈 정보가 담겨 있어 정보를 가져올 수 있다.
// os.js
const os = require('os');
console.log('운영체제 정보---------------------------------');
console.log('os.arch():', os.arch());
console.log('os.platform():', os.platform());
console.log('os.type():', os.type());
console.log('os.uptime():', os.uptime());
console.log('os.hostname():', os.hostname());
console.log('os.release():', os.release());
console.log('경로------------------------------------------');
console.log('os.homedir():', os.homedir());
console.log('os.tmpdir():', os.tmpdir());
console.log('cpu 정보--------------------------------------');
console.log('os.cpus():', os.cpus());
console.log('os.cpus().length:', os.cpus().length);
console.log('메모리 정보-----------------------------------');
console.log('os.freemem():', os.freemem());
console.log('os.totalmem():', os.totalmem());
os.arch(): process.arch와 동일하다.
os.platform(): process.platform과 동일하다.
os.type(): 운영체제의 종류를 보여준다.
os.uptime(): 운영체제 부팅 이후 흐른 시간(초)을 보여준다. process.uptime()은 노드의 실행 시간
os.hostname(): 컴퓨터의 이름을 보여준다.
os.release(): 운영체제의 버전을 보여준다.
os.homedir(): 홈 디렉터리 경로를 보여준다.
os.tmpdir(): 임시 파일 저장 경로를 보여준다.
os.cpus(): 컴퓨터의 코어 정보를 보여준다.
os.freemem(): 사용 가능한 메모리(RAM)를 보여준다.
os.totalmem(): 전체 메모리 용량을 보여준다.
os 모듈은 주로 컴퓨터 내부 자원에 빈번하게 접근하는 경우 사용된다. 즉 일반적인 웹 서비스를 제작할 때는 사용 빈도가 높지 않다.
하지만 운영체제 별로 다른 서비스를 제공하고 싶을 때 os 모듈이 유용하다.
3.5.1 path
폴더와 파일의 경로를 쉽게 조작하도록 도와주는 모듈이다.
path 모듈이 필요한 이유는 운영체제 별로 경로 구분자가 다르기 때문이다.
윈도 타입과 POSIX 타입으로 구분되는데 POSIX는 유닉스 기반의 운영체제들로 맥과 리눅스가 속해 있다.
윈도 - C:\Users\ZeroCho 처럼 \로 구분
POSIX - /home/zerocho 처럼 /로 구분
// path.js
const path = require('path');
const string = __filename;
console.log('path.sep:', path.sep);
console.log('path.delimiter:', path.delimiter);
console.log('------------------------------');
console.log('path.dirname():', path.dirname(string));
console.log('path.extname():', path.extname(string));
console.log('path.basename():', path.basename(string));
console.log('path.basename - extname:', path.basename(string, path.extname(string)));
console.log('------------------------------');
console.log('path.parse()', path.parse(string));
console.log('path.format():', path.format({
dir: 'C:\\users\\zerocho',
name: 'path',
ext: '.js',
}));
console.log('path.normalize():', path.normalize('C://users\\\\zerocho\\\path.js'));
console.log('------------------------------');
console.log('path.isAbsolute(C:\\):', path.isAbsolute('C:\\'));
console.log('path.isAbsolute(./home):', path.isAbsolute('./home'));
console.log('------------------------------');
console.log('path.relative():', path.relative('C:\\users\\zerocho\\path.js', 'C:\\'));
console.log('path.join():', path.join(__dirname, '..', '..', '/users', '.', '/zerocho'));
console.log('path.resolve():', path.resolve(__dirname, '..', 'users', '.', '/zerocho'));
path.sep: 경로의 구분자입니다. 윈도는 \, POSIX는 /이다.
path.delimiter: 환경 변수의 구분자입니다. process.env.PATH를 입력하면 여러 개의 경로가 이 구분자로 구분 되어 있다. 윈도는 세미콜론(;)이고, POSIX는 콜론(:)이다.
path.dirname(경로): 파일이 위치한 폴더 경로를 보여준다.
path.extname(경로): 파일의 확장자를 보여준다.
path.basename(경로, 확장자): 파일의 이름(확장자 포함)을 표시한다. 파일의 이름만 표시하고 싶다면 basename의 두 번째 인수로 파일의 확장자를 넣으면 된다.
path.parse(경로): 파일 경로를 root, dir, base, ext, name으로 분리한다.
path.format(객체): path.parse()한 객체를 파일 경로로 합친다.
path.normalize(경로): /나 \를 실수로 여러 번 사용했거나 혼용했을 때 정상적인 경로로 변환한다.
path.isAbsolute(경로): 파일의 경로가 절대경로인지 상대경로인지를 true나 false로 알린다.
path.relative(기준경로, 비교경로): 경로를 두 개 넣으면 첫 번째 경로에서 두 번째 경로로 가는 방법을 알린다.
path.join(경로, …): 여러 인수를 넣으면 하나의 경로로 합친다. 상대경로인 ..(부모 디렉터리)과 .(현 위치)도 알아서 처리한다. [상대경로]
path.resolve(경로, …): path.join()과 비슷하지만 차이가 있다. [절대 경로]
path.join('/a', '/b', 'c'); /* 결과: /a/b/c/ */
path.resolve('/a', '/b', 'c'); /* 결과: /b/c */
가끔 윈도에서 POSIX 스타일 경로를 사용할 때가 있고, 그 반대일 경우도 있다.
이러한 경우 윈도에서는 path.posix.sep나 path.posix.join과 같이 사용하면 되고, POSIX에서는 path.win32.sep이나 path.sin32.join과 같이 사용하면 된다.
노드는 require.main파일을 기준으로 상대경로로 인식한다.
3.5.3 url
인터넷 주소를 쉽게 조작하도록 도와주는 모듈이다.
url처리에는 두 가지 방식이 있다. 하나는 노드 버전 7에서 추가된 WHATWG 방식의 url이고, 다른 하나는 예전부터 노드에서 사용하던 방식의 url이다.
브라우저에서도 WHATWG 방식을 사용하므로 호환성이 좋아 요즘은 WHATWG 방식을 사용한다.
// url.js
const url = require('url'); ---- ➊
const { URL } = url;
const myURL = new URL('http://www.gilbut.co.kr/book/bookList.aspx?sercate1=001001000#anchor');
console.log('new URL():', myURL);
console.log('url.format():', url.format(myURL));
• url.format(객체): 분해되었던 url 객체를 다시 원래 상태로 조립한다.
주소가 host 부분 없이 pathname 부분만 오는 경우 WHATWG방식으로 처리할 수 없다.
이럴 때는 new URL(''/book/bookList.apsx", "https://www.gilbut.co.kr") 처럼 두 번째 인수에 host를 적어준다.
search 부분은 보통 주소를 통해 데이터를 전달할 때 사용된다.
?로 시작하고 구 뒤에 키=값 형식으로 데이터를 전달한다.
여러 키가 있을 경우 &로 구분한다.
// searchParms.js
const myURL = new URL('http://www.gilbut.co.kr/?page=3&limit=10&category=nodejs&category=javascript');
console.log('searchParams:', myURL.searchParams);
console.log('searchParams.getAll():', myURL.searchParams.getAll('category'));
console.log('searchParams.get():', myURL.searchParams.get('limit'));
console.log('searchParams.has():', myURL.searchParams.has('page'));
console.log('searchParams.keys():', myURL.searchParams.keys());
console.log('searchParams.values():', myURL.searchParams.values());
myURL.searchParams.append('filter', 'es3');
myURL.searchParams.append('filter', 'es5');
console.log(myURL.searchParams.getAll('filter'));
myURL.searchParams.set('filter', 'es6');
console.log(myURL.searchParams.getAll('filter'));
myURL.searchParams.delete('filter');
console.log(myURL.searchParams.getAll('filter'));
console.log('searchParams.toString():', myURL.searchParams.toString());
myURL.search = myURL.searchParams.toString();
URL과 URLSearchParams 모두 내장 객체이므로 require('url')를 생략했다.
getAll(키): 키에 해당하는 모든 값을 가져온다. category 키에는 nodejs와 javascript라는 두 가지 값이 들어 있다.
get(키): 키에 해당하는 첫 번째 값만 가져온다.
has(키): 해당 키가 있는지 없는지를 검사한다.
keys(): searchParams의 모든 키를 반복기(iterator)(ES2015 문법) 객체로 가져온다.
values(): searchParams의 모든 값을 반복기 객체로 가져온다.
append(키, 값): 해당 키를 추가한다. 같은 키의 값이 있다면 유지하고 하나 더 추가한다.
set(키, 값): append와 비슷하지만 같은 키의 값들을 모두 지우고 새로 추가한다.
delete(키): 해당 키를 제거한다.
toString(): 조작한 searchParams 객체를 다시 문자열로 만든다. 이 문자열을 search에 대입하면 주소 객체에 반영된다.
3.5.4 dns
DNS를 다룰 때 사용하는 모듈이다. 주로 도메인을 통해 IP나 기타 DNS 정보를 얻고자 할 때 사용한다.
// dns.mjs
import dns from 'dns/promises';
const ip = await dns.lookup('gilbut.co.kr');
console.log('IP', ip);
const a = await dns.resolve('gilbut.co.kr', 'A');
console.log('A', a);
const mx = await dns.resolve('gilbut.co.kr', 'MX');
console.log('MX', mx);
const cname = await dns.resolve('www.gilbut.co.kr', 'CNAME');
console.log('CNAME', cname);
const any = await dns.resolve('gilbut.co.kr', 'ANY');
console.log('ANY', any);
ip주소는 간단하게 dns.lookup이나 dns.resolve로 얻을 수 있따.
A, AAAA, NS, SOA, CNAME, MX 등은 레코드라고 부르는다. 해당 레코드에 대한 정보는 dns.resolve으로 조회하면 된다.
3.5.5 crypto
다양한 방식의 암호화를 도와주는 모듈이다.
3.5.5.1 단방향 암호화
비밀번호는 보통 단방향 암호화 알고리즘을 사용한다.
단방향 암호화란 복호화할 수 없는 암호화 방식을 뜻한다.
즉, 단방향 암호화는 한번 암호화하면 원래 문자열을 찾을 수 없다. 복호화할 수 없으므로 암호화 대신 해시 함수라고 부르기도 한다.
단방향 알고리즘은 주로 해시 기법을 사용한다.
해시 기법이란 어떠한 문자열을 고정된 길이의 다른 문자열로 바꿔버리는 방식이다.
입력의 문자열 길이는 다르지만, 출력 문자열의 길이는 네 자리로 고정되어 있다.
// hash.js
const crypto = require('crypto');
console.log('base64:', crypto.createHash('sha512').update('비밀번호').digest('base64'));
console.log('hex:', crypto.createHash('sha512').update('비밀번호').digest('hex'));
console.log('base64:', crypto.createHash('sha512').update('다른 비밀번호').digest('base64'));
createHash(알고리즘): 사용할 해시 알고리즘을 넣는다. md5, sha1, sha256, sha512 등이 가능하지만, md5와 sha1은 이미 취약점이 발견되었다. 현재는 sha512 정도로 충분하지만, 나중에 sha512마저도 취약해지면 더 강화된 알고리즘으로 바꿔야 한다.
update(문자열): 변환할 문자열을 넣는다.
digest(인코딩): 인코딩할 알고리즘을 넣는다. base64, hex, latin1이 주로 사용되는데, 그중 base64가 결과 문자열이 가장 짧아서 애용된다. 결과물로 변환된 문자열을 반환한다.
가끔 nopqrst라는 문자열이 qvew로 변환되어 abcdefgh를 넣었을 때와 똑같은 출력 문자열로 바뀔 때도 있다.
이런 상황을 충동이 발생했다고 표현한다.
현재는 주로 pbkdf2나 bcrypt, scrypt라는 알고리즘을 비밀번호를 암호화하고 있다.
pbkdf2는 간단히 말해서 기존 문자열에 salt라고 불리는 문자열을 붙인 후 해시 알고리즘을 반복해서 적용하는 방식이다.
const crypto = require('crypto');
crypto.randomBytes(64, (err, buf) => {
const salt = buf.toString('base>64');
console.log('salt:', salt);
crypto.pbkdf2('비밀번호', salt, 100000, 64, 'sha>512', (err, key) => {
console.log('password:', key.toString('base>64'));
});
});
/* 실행 결과
salt: OnesIj8wznyKgHva1fmulYAgjf/OGLmJnwfy8pIABchHZF/Wn2AM2Cn/9170Y1AdehmJ0E5CzLZULps+daF6rA==
password: b4/FpSrZulVY28trzNXsl4vVfhOKBPxyVAvwnUCWvF1nnXS1zsU1Paq2p68VwUfhB0LDD44hJOf+tLe3HMLVmQ==
*/
먼저 randomByte()메서드로 64바이트 길이의 문자열을 만든다. 이것이 salt가 된다.
순서대로 인수를 넣고 반복해서 적용한다.
10만번 반복해도 1초 정도 밖에 걸리지 않는다.
컴퓨터 성능에 좌우되므로 알아서 잘 반복횟수를 조절한다.
싱글 스레드 프로그래밍을 할 때 1초동안 블로킹이 되지는 않고 내부적으로 스레드 풀을 사용해 멀티 스레딩으로 동작한다.
pbkdf2는 간단하지만 bcrypt나 scrypt보다 취약하므로 더 나은 보안이 필요하다면 다른 방식을 사용하면 된다.
3.5.5.2 양방향 암호화
암호화된 문자열을 복호화할 수 잇으며 키 라는 것이 사용된다.
대칭형 암호화에서는 암호를 복호화하려면 암호화할 때 사용한 키와 같은 키를 사용해야 한다.
const crypto = require('crypto');
const algorithm = 'aes-256-cbc';
const key = 'abcdefghijklmnopqrstuvwxyz123456';
const iv = '1234567890123456';
const cipher = crypto.createCipheriv(algorithm, key, iv);
let result = cipher.update('암호화할 문장', 'utf8', 'base64');
result += cipher.final('base64');
console.log('암호화:', result);
const decipher = crypto.createDecipheriv(algorithm, key, iv);
let result2 = decipher.update(result, 'base64', 'utf8');
result2 += decipher.final('utf8');
console.log('복호화:', result2);
/* 실행 결과
암호화: iiopeG2GsYlk6ccoBoFvEH2EBDMWv1kK9bNuDjYxiN0=
복호화: 암호화할 문장
*/
crypto.createCipheriv(알고리즘, 키, iv): 암호화 알고리즘과 키, iv를 넣는다.
aes-256-cbc 알고리즘의 경우 키는 32바이트여야 하고, iv는 16바이트여야 한다.
iv는 암호화할 때 사용하는 초기화 벡터를 의미한다.
사용 가능한 알고리즘 목록은 crypto.getCiphers()를 호출하면 볼 수 있다.
cipher.update(문자열, 인코딩, 출력 인코딩): 암호화할 대상과 대상의 인코딩, 출력 결과물의 인코딩을 넣는다. 보통 문자열은 utf8 인코딩을, 암호는 base64를 많이 사용한다.
cipher.final(출력 인코딩): 출력 결과물의 인코딩을 넣으면 암호화가 완료된다..
decipher.update(문자열, 인코딩, 출력 인코딩): 암호화된 문장, 그 문장의 인코딩, 복호화할 인코딩을 넣는다. createCipheriv의 update()에서 utf8, base64 순으로 넣었다면 createDecipheriv의 update()에서는 base64, utf8 순으로 넣으면 된다.
decipher.final(출력 인코딩): 복호화 결과물의 인코딩을 넣는다.
3.5.6 util
util이라는 이름처럼 각종 편의 기능을 모아둔 모듈이다. 계속해서 API가 추가되고 있으며 가끔 deprecated되어 사라지는 경우가 있다.
const util = require('util');
const crypto = require('crypto');
const dontUseMe = util.deprecate((x, y) => {
console.log(x + y);
}, 'dontUseMe 함수는 deprecated되었으니 더 이상 사용하지 마세요!');
dontUseMe(1, 2);
const randomBytesPromise = util.promisify(crypto.randomBytes);
randomBytesPromise(64)
.then((buf) => {
console.log(buf.toString('base64'));
})
.catch((error) => {
console.error(error);
});
/* 실행 결과
3
(node:7264) DeprecationWarning: dontUseMe 함수는 deprecated되었으니 더 이상 사용하지 마세요!
(Use `node --trace-deprecation ...` to show where the warning was created)
60b4RQbrx1j130x4r95fpZac9lmcHyitqwAm8gKsHQKF8tcNhvcTfW031XaQqHlRKzaVkcENmIV25fDVs3SB7g==
*/
util.deprecate: 함수가 deprecated 처리되었음을 알린다. 함수가 조만간 사라지거나 변경될 때 알려줄 수 있어 유용하다.
util.promisify: 콜백 패턴을 프로미스 패턴으로 바꾼다. 바꿀 함수를 인수로 제공하면 된다.
3.5.7 worker_threads
멀티 스레드 방식으로 작업할 수 있다.
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 exit'));
worker.postMessage('ping');
} else { // 워커일 때
parentPort.on('message', (value) => {
console.log('from parent', value);
parentPort.postMessage('pong');
parentPort.close();
});
}
/* 실행 결과
from parent ping
from parent pong
worker exit
*/
isMainThread를 통해 현재 코드가 메인 스레드에서 실행되는지 아니면 생성한 워커 스레드에서 실행되는지 구분된다.
메인 스레드에서는 new Worker를 통해 현재 파일을 워커 스레드에서 실행시키고 있다.
부모에서는 워커 생성 후 worker.postMessage로 워커에 데이터를 보낼 수 있다.
워커는 parentPort.on 이벤트 리스너로 부모로부터 메시지를 받고, parentPort.postMESSAGE로 부모에게 메세지로 보낸다.
부모는 worker.on('message')를 받는다.
parentPort.close()하면 부모의 연결이 종료된다.
종료될 때는 worker.on('exit)이 실행된다.
const {
Worker, isMainThread, parentPort, workerData,
} = require('worker_threads');
if (isMainThread) { // 부모일 때
const threads = new Set();
threads.add(new Worker(__filename, {
workerData: { start: 1 },
}));
threads.add(new Worker(__filename, {
workerData: { start: 2 },
}));
for (let worker of threads) {
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
*/
new Worker를 호출할 때 두 번째 인수의 workerData 속성으로 원하는 데이터를 보낼 수 있따.
현재 두 개의 워커가 돌아가고 있으며, 각각 부모로부터 숫자를 받아서 100을 더해 돌려준다.
워커 두 개가 모두 종료되면 job done이 로깅된다.
워커 스레드를 여덟게 사용했다고 해서 여덟 배 빨라지는 것은 아니다. 스레드를 생성하고 스레드 사이에서 통신하는데 상당한 비용이 발생하므로 이 점을 고려해서 멀티 스레딩을 해야한다.
3.5.8 child_process
노드에서 다른 프로그램을 실행하고 싶거나 명령어를 수행하고 싶을 때 사용하는 모듈이다.
이 모듈을 통해 다른 언어의 코드를 실행하고 결괏값을 받을 수 있다.
const exec = require('child_process').exec;
const process = exec('dir');
process.stdout.on('data', function(data) {
console.log(data.toString());
}); // 실행 결과
process.stderr.on('data', function(data) {
console.error(data.toString());
}); // 실행 에러
실행하면 현재 폴더의 파일 목록들이 표시된다.
결과는 stdout과 stderr에 붙여둔 data 이벤트 리스너에 버퍼 형태로 전달된다.
cmd나 파워쉘에서 한글이 제대로 표시되지 않는 경우에는 다음 명령을 통해 터미널을 utf8로 바꾼뒤 실행한다.
$chcp 65001
파이썬 프로그램도 실행 가능하다.
// test.py
print('hello python')
// spawn.js
const spawn = require('child_process').spawn;
const process = spawn('python', ['test.py']);
process.stdout.on('data', function(data) {
console.log(data.toString());
}); // 실행 결과
process.stderr.on('data', function(data) {
console.error(data.toString());
}); // 실행 에러
파이썬 코드를 실행하는 명령어인 python test.py를 노드 spawn을 통해 실행한다.
첫 번째 인수로 명령어를, 두 번째 인수로 배열을 넣으면 된다.
exec와 spawn의 차이점은 exec은 셸을 실행해서 명령어를 수행하고, spawn은 새로운 프로세스를 띄우면서 명령어를 실행한다.
spawn에서도 세 번째 인수로 shell:true를 제공하면 exec 처럼 셸을 실행해서 명령어를 수행한다.
3.5.9 기타 모듈들
async_hooks: 비동기 코드의 흐름을 추적할 수 있는 실험적인 모듈이다.
dgram: UDP와 관련된 작업을 할 때 사용한다.
net: HTTP보다 로우 레벨인 TCP나 IPC 통신을 할 때 사용한다.
perf_hooks: 성능 측정을 할 때 console.time보다 더 정교하게 측정한다.
querystring: URLSearchParams가 나오기 이전에 쿼리스트링을 다루기 위해 사용했던 모듈이다.
string_decoder: 버퍼 데이터를 문자열로 바꾸는 데 사용한다.
tls: TLS와 SSL에 관련된 작업을 할 때 사용한다.
tty: 터미널과 관련된 작업을 할 때 사용한다.
v8: v8 엔진에 직접 접근할 때 사용한다.
vm: 가상 머신에 직접 접근할 때 사용한다.
wasi: 웹어셈블리를 실행할 때 사용하는 실험적인 모듈이다.
'Programming > NodeJS' 카테고리의 다른 글
Node.js 교과서 [이벤트, 예외 처리] #3.4 (0) | 2024.02.04 |
---|---|
Node.js 교과서 [파일 시스템] #3.3 (0) | 2024.02.04 |
Node.js 교과서 [REPL, 모듈, 노드 내장 객체] #3 (1) | 2024.02.04 |
Node.js 교과서 [Front-end] #2.2 (0) | 2024.02.02 |
Node.js 교과서 [ES2015+] #2 (0) | 2024.02.02 |