본문 바로가기

Web/Node js

[Node.js 교과서] 3.5 노드 내장 모듈 사용하기

url

url 처리에는 크게 두 가지 방법이 있다. 기존 노드에서 사용하던 방식과 WHATWG 방식이 존재한다. 

주소의 각 부분 명칭은 다음과 같다.

href
protocol auth href path hash
hostname port pathname search
http:                                    // user :            pass         @sub.host.com             :8000                /p/a/t/h      ?query=string      #hash
protocol username password hostname port pathname search hash
host
origin
href

 

주소 비교

실제 주소를 비교해보면 기존 url과 WhatWG 방식의 차이점을 알 수 있다. 

new URL(): URL {
  href: 'http://www.gilbut.co.kr/book/bookList.aspx?sercate1=001001000#anchor',
  origin: 'http://www.gilbut.co.kr',
  protocol: 'http:',
  username: '',
  password: '',
  host: 'http://www.gilbut.co.kr',
  hostname: 'http://www.gilbut.co.kr',
  port: '',
  pathname: '/book/bookList.aspx',
  search: '?sercate1=001001000',
  searchParams: URLSearchParams { 'sercate1' => '001001000' },
  hash: '#anchor'
}
url.format(): http://www.gilbut.co.kr/book/bookList.aspx?sercate1=001001000#anchor
------------------------------
url.parse(): Url {
  protocol: 'http:',
  slashes: true,
  auth: null,
  host: 'http://www.gilbut.co.kr',
  port: null,
  hostname: 'http://www.gilbut.co.kr',
  hash: '#anchor',
  search: '?sercate1=001001000',
  query: 'sercate1=001001000',
  pathname: '/book/bookList.aspx',
  path: '/book/bookList.aspx?sercate1=001001000',
  href: 'http://www.gilbut.co.kr/book/bookList.aspx?sercate1=001001000#anchor'
}
url.format(): http://www.gilbut.co.kr/book/bookList.aspx?sercate1=001001000#anchor

 

주의점

WHATWG 방식은 pathname만 오는 주소의 경우 처리할 수 없다. ex) /book/bookList

그러나 WHATWG 방식은 search 부분을 searchParams라는 특수한 객체로 반환하므로 유용하다.

결과를 확인해보면 다음과 같다.

 

searchParams: URLSearchParams {
  'page' => '3',
  'limit' => '10',
  'category' => 'nodejs',
  'category' => 'javascript' }
searchParams.getAll(): [ 'nodejs', 'javascript' ]
searchParams.get(): 10
searchParams.has(): true
searchParams.keys(): URLSearchParams Iterator { 'page', 'limit', 'category', 'category' }
searchParams.values(): URLSearchParams Iterator { '3', '10', 'nodejs', 'javascript' }
[ 'es3', 'es5' ]
[ 'es6' ]
[]
searchParams.toString(): page=3&limit=10&category=nodejs&category=javascript

 

 querystring

WHATWG 방식의 url 대신 기존 노드의 url을 사용할 때, search 부분을 사용하기 쉽게 객체로 만드는 모듈이다.

 

querystring.parse(): [Object: null prototype] {
  page: '3',
  limit: '10',
  category: [ 'nodejs', 'javascript' ]
}
querystring.stringify(): page=3&limit=10&category=nodejs&category=javascript

 

crypto

다양한 방식의 암호화를 도와주는 모듈이다. 비밀번호가 api에 그대로 노출될 경우 위험할 수 있다.

단방향 암호화

단방향 암호화는 복호화 할 수 없는 암호화 방식이다. 해시 함수라고 부르기도 한다. 굳이 복호화 할 필요 없는 것들 (예를 들어 고객의 비밀번호를 데이터베이스에 저장할 때)에 사용된다. 주로 해시 기법을 사용하고 다음과 같이 사용한다. 

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'));

비밀번호를 sha512 알고리즘을 사용해 해시하는 코드이다. 현재는 주로 pbkdf2, bcrypt, scrypt라는 알고리즘으로 비밀번호를 암호화한다고 한다. 

 

const crypto = require('crypto');

crypto.randomBytes(64, (err, buf) => {
  const salt = buf.toString('base64');
  console.log('salt:', salt);
  crypto.pbkdf2('비밀번호', salt, 100000, 64, 'sha512', (err, key) => {
    console.log('password:', key.toString('base64'));
  });
});

 

이 코드는 pbkdf2 알고리즘을 가져온 것이고 비밀번호에 sha512 해시를 10만번 반복하는 코드이다. 내부적으로 스레드 풀을 사용해 멀티 스레딩으로 동작하므로 블로킹이 발생하지 않는다. 

 

양방향 암호화

양방향 암호화는 암호화된 문자열을 복호화 할 수 있으며 키가 사용된다.

const crypto = require('crypto');

const cipher = crypto.createCipher('aes-256-cbc', '열쇠');
let result = cipher.update('암호화 할 문장', 'utf8', 'base64');
result += cipher.final('base64');
console.log('암호화:', result);

const decipher = crypto.createDecipher('aes-256-cbc', '열쇠');
let result2 = decipher.update(result, 'base64', 'utf8');
result2 += decipher.final('utf8');
console.log('복호화:', result2);

 

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();
  });
}

isMainThread를 통해 현재 코드가 메인 스레드에서 실행 중인지 혹은 생성한 워커 스레드에서 실행되는지 구분된다. 메인 스레드에서는 new Worker를 통해 현재 파일(__filename)을 워커 스레드에서 실행시킨다.

부모에서는 worker 생성 후 worker.postMessage로 워커에 데이터를 보낼 수 있다. parentPort.on('message') 이벤트 리스너로 부모로부터 메시지를 받고 parentPort.postMessage로 부모에게 메시지를 보낸다.

워커에서 on 메서드를 사용할 때는 직접 워커를 종료해야 한다. (parentPort.close) 

 

다음은 여러 개의 워커 스레드에 데이터를 넘기는 과정이다. 

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);
}

new Worker를 생성할 때 workerData 속성으로 원하는 데이터를 보낼 수 있다. 워커에서는 wokrkerData로 데이터를 받을 수 있다. 해당 코드는 각 부모로부터 숫자를 받아 100을 더한 후 돌려준다. 돌려주는 순간 워커가 종료되어 woker.on('exit')이 실행된다.

워커 두개가 모두 종료되면 job done이 로깅된다.

 

const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

const min = 2;
let primes = [];

function findPrimes(start, end) {
  let isPrime = true;
  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 = 10000000;
  const threadCount = 8;
  const threads = new Set();
  const range = Math.floor((max - min) / threadCount);
  let start = min;
  console.time('prime');
  for (let i = 0; i < threadCount - 1; i++) {
    const end = start + range - 1;
    threads.add(new Worker(__filename, { workerData: { start, range: end } }));
    start += range;
  }
  threads.add(new Worker(__filename, { workerData: { start, range: max } }));
  for (let worker of threads) {
    worker.on('error', (err) => {
      throw err;
    });
    worker.on('exit', () => {
      threads.delete(worker);
      if (threads.size === 0) {
        console.timeEnd('prime');
        console.log(primes.length);
      }
    });
    worker.on('message', (msg) => {
      primes = primes.concat(msg);
    });
  }
} else {
  findPrimes(workerData.start, workerData.range);
  parentPort.postMessage(primes);
}
이 코드는 소수를 구하는 코드를 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());
}); // 실행 에러

이 코드는 실행할 경우 현재 폴더의 파일 목록이 표시될 것이다.