노드 학습 5일차
안녕하세요 이번엔 노드 내장 모듈에 대해 공부하고 정리합니다.
지난 번에는 내장 객체에 대해 알아봤습니다. 명칭이 비슷해서 했갈리네요.
글에 앞서 내장 모듈에 대한 정보는 nodeJS 공식 문서에서도 자세히 나와 있습니다
https://nodejs.org/docs/latest/api/
목차
os
운영 체제의 정보를 가지는 모듈입니다.
내장 모듈이기 때문에 직접 만든 모듈처럼 경로를 입력하지 않고 이름만 입력해서 가져올 수 있습니다. 많은 기능들이 있는데 제가 눈여겨 봤던 기능만 정리해봅니다.
- os.uptime() : os가 실행되고 흐른 시간을 확인합니다
- os.cpus() : os의 cpu 정보를 확인합니다.
- os.freemem() : 사용 가능한 메모리의 양을 바이트로 반환합니다.
- os.totalmem() : 시스템 메모리의 총 양을 바이트로 반환합니다.
path
운영체제에 따라 경로를 표현하는 방법이 다른데 이를 핸들링 하는데 도움이 되는 모듈입니다.
공식 문서에서는 운영체제를 Windows와 POSIX 로 구분하여 설명하고 있는데요. 서로 경로를 표현하는 방법이 다르기 때문입니다. 여기서 POSIX는 맥과 리눅스 등을 의미합니다.
운영 체제에 따라 일부 기능은 서로 다른 결과를 반환하기 때문에 이 부분은 문서를 보고 확인하는 것이 좋겠습니다.
path.basename(path[,suffix])
: 경로의 마지막 부분을 반환하며 명시된 확장자는 제거합니다.path.dirname(path)
: 경로의 디렉토리 이름까지를 반환합니다.path.basename()
과는 반대네요path.extname(path)
: 확장자를 반환합니다.path.join([...paths])
: 경로를 연결해서 반환합니다...
같이 상위로 가는 요소가 있다면 이것도 처리해서 반환합니다. 이 때 절대 경로는 무시합니다.path.resolve([...paths])
:join()
과 비슷하지만 절대경로를 포함해서 처리합니다.문자열경로.split(path.sep)
: 경로의 세그먼트 구분 기호를 기준으로 문자열을 나눈 배열을 반환합니다.path.normalize(path)
: 주어진path
를 정규화 합니다.
url
URL을 핸들링 하는데에 도움이 되는 모듈입니다. 기존에는 노드에서 독자적으로 사용하던 url 처리 방식이 있고 WHATWG 방식이 있는데요 현재는 WHATWG 방식을 사용합니다. URL 처리에 알기전에 앞서 URL의 구조에 대해 알아 놓을 필요가 있는데요
URL은 다음의 구조를 가집니다.
하나의 URL에 이렇게 복잡한 구조가! 하지만 url 모듈을 이용한다면 URL에 대해 일일이 파악하고 분석하지 않아도 보기 좋게 정리할 수 있습니다.
url 모듈을 사용해서 로그를 확인해보기 위한 예제 코드입니다.
1
2
3
4
5
6
7
8
9
const url = require('url');
const {URL} = url;
const myURL = new URL('http://www.github.co.kr/book/bookList.aspx?sercate1=001001000#anchor');
console.log('new URL : ', myURL);
console.log('url.format() : ', url.format(myURL));
log에 남은 결과입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
new URL : URL {
href: 'http://www.github.co.kr/book/bookList.aspx?sercate1=001001000#anchor',
origin: 'null',
protocol: 'http:',
username: '',
password: '',
host: 'www.github.co.kr',
hostname: 'www.github.co.kr',
port: '',
pathname: '/book/bookList.aspx',
search: '?sercate1=001001000',
searchParams: URLSearchParams { 'sercate1' => '001001000' },
hash: '#anchor'
}
url.format() : htt://www.github.co.kr/book/bookList.aspx?sercate1=001001000#anchor
myURL
객체를 log에 찍어보니 구조별로 나오는 것을 확인할 수 있습니다.
여기서 searchParams
는 search
의 값을 객체화 한 것 입니다.
searchParams
url의 searchParams에는 사용할 수 있는 다양한 기능이 있습니다.
getAll(key)
: key 에 해당 하는 value를 목록으로 반환합니다.get(key)
: key 에 해당하는 value를 반환합니다. 없다면 null 을 반환합니다.has(key)
: key가 존재하는지 확인합니다keys()
: key를 목록으로 반환합니다.values()
: value를 목록으로 반환합니다.append(key,value)
: key에 value를 추가합니다. 같은 키에 값이 있다면 배열로 추가합니다.set(key,value)
: key에 value를 저장합니다. 기존의 값을 덮어씁니다.delete(key)
: key에 해당하는 값을 지웁니다toString()
: searchParam 객체를 문자열로 반환합니다.
dns
DNS를 다룰 때 사용하는 모듈입니다. 도메인을 통해 IP나 DNS 정보를 얻을 때 사용합니다.
DNS에 관해서 자세한 부분은 다음에 공부하고 일단 DNS RECORD에 대해 간단하게 알고 넘어가겠습니다.
DNS RECORD는 DNS에서 사용되는 데이터베이스 항목입니다. 설명이 어렵네요. 인터넷에서는 설명이 전부 어렵습니다.
보다 쉽게 설명하자면 어떠한 주소와 그 주소에 연결된 정보를 관리하는 정보입니다. DNS RECORD에는 몇 가지 유형이 있습니다.
이 유형에 대해서는 따로 정리해보고 여기서는 DNS RECORD라는 것이 있고, 몇 가지의 종류가 정해져있다는 것만 인지하고 넘어가겠습니다.
lookup(url)
: ip주소를 반환합니다resolve(url,record)
: url의 record에 해당하는 값을 반환합니다.
crypto
노드의 주요 내장 모듈 중 하나인 crypto
입니다. 암호화를 담당하는 모듈입니다.
단방향 암호화 & 양방향 암호화를 지원합니다.
해시 기법
단방향 암호화의 대표적인 방법 입니다. 해시 기법을 사용하는 기본 적인 형식은 다음과 같습니다.
1
2
3
const crypto = require('crypto');
//...기타 작업 생략
crypto.createHash({해시_알고리즘}).update({문자열}).digest({인코딩_알고리즘});
해시 알고리즘은 sha512
가 주로 사용됩니다. md5
, sha1
등 다양한 해시 알고리즘도 있지만 sha512
는 블록체인에서도 사용되는 강력한 해시를 제공하는 알고리즘 입니다.
인코딩 알고리즘에는 base64
가 제일 보편적으로 사용되고 있습니다.
pbkdf2
새로운 암호화 방법입니다.
pbkdf2
의 가장 큰 특징은 salt
입니다. 여기서 의미하는 salt
는 진짜 말그대로 소금과 같은 역할입니다.
암호화를 하려는 문자열에 소금을 쳐서 원래 문자열에 변화를 주어, 보다 해독을 어렵게 만드는 역할을 합니다.
만약 salt
의 값이 바뀌어 버린다면 결과도 달라지기 때문에 salt
의 값은 변해서는 안됩니다.
pbkdf2
방식을 사용하기 위한 구조는 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
const crypto = require('crypto');
//...기타 코드 생략
crypto.randomBytes({바이트_크기},(err,buf)=>{ // salt 를 생성하기 위한 랜덤 바이트 생성
const salt = buf.toString('base64'); // 생성된 랜덤한 바이트를 salt 로 저장
crypto.pbkdf2(
{암호화_할_문자열},{salt},{반복횟수},{출력바이트},{해시_알고리즘},
(err,key)=>{
// 콜백 작업
}
)
})
❓궁금했던점 🤔 매번 다른 salt
를 생성해서 사용하면 어떤 비밀번호에 어떤 salt
를 사용했는지 어떻게 아는거지? 라는 생각이 들었습니다. 생각해보면 salt를 함께 저장하면 되는 것 이었습니다.
양방향 암호화
암호화 및 복호화 둘 다 가능한 암호화 방법입니다.
대칭형 암호화와 비대칭형 암호화 두 가지로 나뉩니다. 정처기 공부할 때 나오던 용어가 여기서…!
대칭형 암호화
키를 사용해서 암호화를 하고 키를 통해 복호화 합니다. 때문에 암호화-복호화 할 때에는 같은 키를 사용해야합니다.
대칭형 암호화의 예제 코드 입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const crypto = require('crypto');
const algorithm = 'aes-256-cbc'; // 알고리즘의 종류는 선택
const key = 'abcdefghijklmnopqrstuvwxyz123456'; // 32바이트
const iv = "abcdefghijklmnop"; // 16 바이트
// key와 iv의 길이는 algorithm에 따라 달라짐.
const cipher = crypto.createCipheriv(algorithm,key,iv);
let result = cipher.update({암호화_할_문자열},{평문의_인코딩},{암호문의_인코딩});
result += cipher.final({암호문의_인코딩});
const decipher = crypto.createDecipheriv(algorithm,key,iv);
let result2 = decipher.update(result,{암호문의_인코딩},{평문의_인코딩});
result2 += decipher.final({평문_인코딩});
예전에는 없었던 iv
가 암호화 하는데 추가되었습니다. 보안 취약점 때문이라고…
여기서 사용되는 key
는 고정된 불변 값으로 모든 비밀번호에 공통으로 사용됩니다. 그렇기 때문에 소스에서 자체적으로 관리하는 것은 보안에 위협이 될 수 있기 때문에 일반적으로는 키를 관리하는 다양한 방법을 사용하고 있습니다.
iv
는 암호화 할 때 마다 새로운 iv
가 생성되어 매번 새로운 암호문이 생성되도록 합니다. 해시 알고리즘의 salt
와 비슷한 역할이라 생각됩니다.
util
각종 편의 기능을 모아 놓은 모듈입니다. 편의 기능의 집합 모듈이다 보니 문서의 양도 굉장히 많네요.
여기서는 강의에서 언급한 deprecate
와 promisify
에 대해서만 정리해보겠습니다.
deprecate
deprecated 된 기능을 처리하는데 사용됩니다. 사용 방법은 다음의 예제와 같습니다.
1
2
3
4
5
6
7
8
9
const util = require('util');
const oldFunction = (x,y) => {
console.log(x+y);
}
const deprecatedFunction = util.deprecate(oldFunction,'deprecated 되었으니 사용하지 마세요');
export default deprecatedFunction;
의아했던점🤔❓ 위의 예제에서 deprecate한 deprecatedFunction()
가 아니라 oldFunction()
을 호출하면 어떻게 되는거지? 라고 생각했습니다만 모듈화된 시점에서 그런 방법은 불가능 했습니다.
제가 보고 있는 강의에서는 모듈화 하지 않고 deprecate한 함수를 바로 아래에서 호출하고 있었기 때문에 이러한 생각이 들었습니다만, 사실상 노드에서는 모듈화 해서 사용하기 때문에 같은 모듈 내에서 deprecate 하기 전의 함수를 그대로 호출할 경우 외에는 없겠습니다.
promisify
콜백 함수로 사용하던 것들을 프로미스로 사용할 수 있습니다.
요즘에는 콜백 함수로 사용하는 것보다 프로미스를 사용하는 것이 더 권장되고 있고 코드도 간결하기 때문에 콜백 함수로 작성된 레거시 코드들을 핸들링 하기 힘든 경우가 많은데, 이에 도움이 됩니다.
promisify()
함수 내에 콜백 함수를 사용하는 함수를 넣어서 사용합니다. 예제는 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const util = require('util');
const crypto = require('crypto');
// 콜백 함수로 핸들링 하던 crypto.randomBytes를 프로미스로 사용하는 방법
const randomBytePromise = util.promisify(crypto.randomBytes);
// crypto.randomBytes 의 기능과 동일하게 바이트 크기를 인수로 전달
randomBytePromise(64)
.then((buf)=>{
console.log(buf.toString('base64'));
})
.catch((err)=>{
console.error(err);
})
프로미스 형식으로 바꿨기 때문에 then()
, catch()
가 사용 가능 할 뿐만 아니라 async/await 문법도 사용 가능 합니다. 단 여기서 주의 해야 할 점이 있습니다⚠️ 콜백의 형식이 (err,data)=>{}
의 형식을 가져야 한다는 것입니다.
worker_threads
싱글 스레드인 노드가 멀티 스레드 방식으로 작업할 수 있도록 하는 모듈입니다.
노드에서 멀티 스레드 작업 방식에 대해 무지하기 때문에 간단한 워크 플로우를 보겠습니다.
- 메인스레드일 경우 -> 워커 스레드 생성 후 작업 분배
- 워커스레드인 경우 -> 분배된 작업 처리
- 메인 스레드가 워커 스레드의 작업이 종료된 것을 확인 후 병합하여 최종 저리
저는 멀티 스레드 작업 방식에 대해 잘 몰라서 특정 작업을 멀티스레드로 처리하겠다고 명시하면 모듈이 알아서 스레드 만들고 작업 분배하고 처리할 줄 알았는데 아니군요.
제가 공부하며 느낀 worker_threads
의 장점과 단점 입니다. 장점
- 빠른 속도
- 효율적인 리소스 사용 단점
- 코드 복잡성 증가
스레드를 만들고 처리하는 작업 또한 시간이 소요되는 작업이기 때문에 스레드 수가 곧 작업 속도와 반드시 비례하지는 않았습니다.
- worker_threads 를 사용하지 않은 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const min = 2;
const max = 10_000_000;
const primes = [];
function generatePrimes(start,range){
let isPrime = true;
const end = start + range;
for (let i = start; i < end; i++){
for(let j = min; j < Math.sqrt(end);j++){
if(i !== j && i % j === 0){
isPrime = false;
break;
}
}
if(isPrime){
primes.push(i);
}
isPrime = true;
}
}
console.time('prime');
generatePrimes(min,max);
console.timeEnd('prime');
console.log(primes.length);
- worker_threads 를 사용한 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
const { isMainThread, Worker, parentPort, workerData} = require('worker_threads')
const min = 2;
let primes = [];
function findPrimes(start,range){
let isPrime = true;
const end = start + range;
for (let i = start; i < end; i++){
for(let j = min; j < Math.sqrt(end);j++){
if(i !== j && i % j === 0){
isPrime = false;
break;
}
}
if(isPrime){
primes.push(i);
}
isPrime = true;
}
}
if(isMainThread){
const max = 10_000_000;
const threadCount = 8;
const threads = new Set();
const range = Math.ceil((max-min)/threadCount);
let start = min;
console.time('primes');
for(let i = 0; i < threadCount - 1; i++){
const wStart = start;
threads.add(new Worker(__filename,{workerData:{start : wStart, range, index:i}}));
start += range;
}
threads.add(new Worker(__filename,{workerData: {start, range : range+((max-min+1)%threadCount),index:7}}))
for(let worker of threads){
worker.on('error',(err)=>{
throw err;
})
worker.on('exit',()=>{
threads.delete(worker);
if(threads.size === 0){
console.timeEnd('primes');
console.log(primes.length);
}
})
worker.on('message',(msg)=>{
primes = primes.concat(msg);
})
}
} else {
console.log('start:',workerData.start,',range:',workerData.range,',index:',workerData.index);
findPrimes(workerData.start, workerData.range);
parentPort.postMessage(primes);
}
child_process
자식 프로세스를 생성하는 모듈입니다. worder_threads
와 비교하자면 worker_threads
는 하나의 프로세스가 다수의 스레드를 사용 하도록 합니다. child_process
는 새로운 자식 프로세스를 실행 시킵니다.
새로운 프로세스를 실행시키기 때문에 아예 다른 언어를 실행 할 수도 있습니다.
프로세스 생성에는 다음의 두 가지 방법이 있습니다.
- 비동기 프로세스 생성
- 동기 프로세스 생성
단 자식 프로세스가 실행하고자 하는 프로그램은 설치가 되어있어야 합니다. 이 모듈이 프로그램을 대신 동작하는 것이 아닌, 해당 프로그램이 동작하도록 요청하는 작업을 하기 때문입니다.
비동기 프로세스 생성
비동기 프로세스는 부모 프로세스가 자식 프로세스의 종료를 기다리지 않고 다음 작업을 수행하는 프로세스 입니다.
비동기 프로세스를 생성하는 방법에는 다음과 같은 방법이 있습니다.
exec(command[, options][, callback])
execFile(file[, args][, options][, callback])
fork(modulePath[, args][, options])
spawn(command[, args][, options])
동기 프로세스 생성
동기 프로세스는 자식 프로세스의 작업이 끝날 때 까지 부모 프로세스가 대기하는 프로세스 입니다.
동기 프로세스 생성 방법은 다음과 같은 방법이 있습니다.
execFileSync(file[, args][, options])
execSync(command[, options])
- `spawnSync(command[, args][, options