3.5 노드 기능 알아보기

Bor·2021년 12월 25일
0

searchParams.js


URL 생성자를 통해 myURL 주소 객체를 만들었음. myURL 안에는 searchParams 객체가 있다. 이 객체는 search 부분을 조작하는 다양한 메서드 지원. query 문자열보다 searchParams가 유용한 이유는 query의 경우 다음에 배우는 querystring 모듈을 한 번 더 사용해야 하기 때문.


3.5.4 querystring

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

const url = require('url');
const querystring = require('querystring');

const parsedUrl = url.parse('https://www.gilbut.co.kr/?page=3&limit=10&category=nodejs&category=javascript')
const query = querystring.parse(parsedUrl.query);
console.log('querystring.parse():', query);
// querystring.parse(): [Object: null prototype] {
//     page: '3',
//     limit: '10',
//     category: [ 'nodejs', 'javascript' ]
//   }
console.log('querystring.stringify():', querystring.stringify(query));
//querystring.stringify(): page=3&limit=10&category=nodejs&category=javascript

모듈 두 개를 함께 사용. 실제 프로젝트에서도 이렇게 모듈 여러 개를 파일 하나에 불러올 수 있다.

  • querystring.parse(쿼리) : url의 query 부분을 자바스크립트 객체로 분해
  • querystring.stringfy(객체) : 분해된 query 객체를 문자열로 다시 조립

간단하게 객체로 분해되고 문자열로 조립되므로 편리하다.


3.5.5 crypto

다양한 방식의 암호화를 도와주는 모듈. 고객의 비밀번호는 반드시 암호화해야. 비밀번호를 암호화하지 않으면 비밀번호를 저장해둔 DB가 해킹당하는 순간. 고객의 비밀번호도 해커에게 털린다. 안정 장치를 이중으로 만들어 놓는 것.

3.5.5.1 단방향 암호화

  • 단방향 암호화 알고리즘을 사용해서 암호화. 단방향 암호화란 복호화할 수 없는 암호화 방식.
  • 복호화는 암호화된 문자열을 원래 문자열로 돌려놓는 것.
  • 즉, 단방향 암호화는 한 번 암호화하면 원래 문자열을 찾을 수 없다.
  • 복호화할 수 없으므로 암호화라고 표현하는 대신 해시 함수라고 부르기도 한다.
const crypto = require('crypto');

console.log('base64:', crypto.createHash('sha512').update('비밀번호').digest('base64'))
//base64: dvfV6nyLRRt3NxKSlTHOkkEGgqW2HRtfu19Ou/psUXvwlebbXCboxIPmDYOFRIpqav2eUTBFuHaZri5x+usy1g==

console.log('hex:', crypto.createHash('sha512').update('비밀번호').digest('hex'))
//hex: 76f7d5ea7c8b451b773712929531ce92410682a5b61d1b5fbb5f4ebbfa6c517bf095e6db5c26e8c483e60d8385448a6a6afd9e513045b87699ae2e71faeb32d6

console.log('base64:', crypto.createHash('sha512').update('다른비밀번호').digest('base64'))
//base64: H3wpOArA9Md5f6APVAK7kZxe4KLj9bu1OYfDTzObcO5z4+TGEtj8L5T+hF+nEJM+O205Je5uCGS7UTzQ5fgqDw==

비밀번호라는 문자열을 해시를 사용해 바꿈!

  • createHash(알고리즘) : 사용할 해시 알고리즘. md5, sha1, sha256, sha512 등이 가능하지만 md5 와 sha1은 이미 취약점이 발견. 나중에 512도 취약해지면 더 강화된 알고리즘으로 변경해야
  • update(문자열) : 반환할 문자열을 넣음
  • digest(인코딩) : 인코딩할 알고리즘을 넣는다. base64, hex, latin1이 주로 사용. base64가 결과 문자열이짧아 애용된다.

가끔 nopqrst라는 문자열이 qvew로 변환되어 abcdefgh를 넣었을 때와 동일한 출력 문자열로 바뀔 때도 있다. 이런 상황을 충돌했다. 해킹용 컴퓨터의 역할은 어떠한 문자열이 같은 출력 문자열을 반환하는지 찾아내는 것. 여러 입력 문자열이 같은 출력 문자열로 변환. 그래서 비밀번호를 abcdefgh로 설정했어도 nopqrst로 뚫리는 사태가 발생.

현재는 주로 pbkdf2 나 bcrypt, scrypt 라는 알고리즘으로 비밀번호를 암호화함. 그 중 노드에서 지원하는 pbkdf는 기존 문자열에 salt라고 불리는 문자열을 붙인 후 해시 알고리즘을 반복해서 적용.

const crypto = require('crypto');

crypto.randomBytes(64, (err,buf) => {
    const salt = buf.toString('base64');
    console.log('salt:', salt);
    crypto.pbkdf2('비밀번호', salt, 10000, 64, 'sha512', (err,key) => {
        console.log('password:', key.toString('base64'))
    })
})
// salt: uJbwDAQgpxCcgEcfzhuo+7GaYfE69CC/0gAKAiY+FirsIuxQLzxxUBentd1mRmlgMW6txXJc3GxDPNFV/+ZrAw==
// password: P0pokaht2cIiG/6NjN0ipzh0nedPoLnxi/hcc4ybon78eVPBIypnDKHJ9mCwF81kpja+jnGmfIEAKy6TEUNqSA==

먼저 randomBytes() 메서드로 64바이트 길이의 문자열을 만든다. 이것이 salt가 된다. pbkdf2() 메서드에는 순서대로 비밀번호, salt, 반복횟수, 출력 바이트, 해시 알고리즘을 인수로 넣는다. 예시에서는 만 번 적용한다고. 즉, sha512로 변환된 결과값을 다시 sha512로 변환하는 과정을 10만 번 반복. pbkdf2는 간단하지만 bcrypt 나 scrypt보다 취약하므로 더 나은 보안이 필요하면 죠 방식을 사용.

3.5.5.2 양방향 암호화

  • 암호화된 문자열을 복호화할 수 있으며, 키가 사용된다
  • 대칭형 암호화에서 암호를 복호화하려면 암호화할 때 사용한 키와 같은 키를 사용해야

다음은 노드로 양방향 암호화하는 방법. 다음 코드를 완벽하게 이해하려면 암호학 is needed.

const crypto = require('crypto');

const algorithm = 'aes-256-cbc';
const key = 'abcdefghijklmnopqrstuvwxyz1234567890';
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);
  • crypto.createCipheriv(알고리즘, 키, iv) 암호화 알고리즘과 키, iv를 넣는다. 암호화 알고리즘으로 aes-256-cbc를 사용했으며, 다른 알고리즘을 사용해도 된다. aes-256-cbc 알고리즘의 경우 키는 32바이트, iv는 16바이트여야 한다. iv는 암호화할 때 사용하는 초기화 벡터를 의미하지만 내용이 많으므로 AES 암호화에 대해 따로 공부하는 것이 좋다

  • cipher.update(문자열, 인코딩, 출력, 인코딩) : 암호화 대상과 대상의 인코딩, 출력 결과물의 인코딩을 넣는다. 보통 문자열은 utf8 인코딩을, 암호는 base64를 사용

  • cipher.final(출력 인코딩) : 출력 결과물의 인코딩을 넣으면 암호화가 완료

  • crypto.createDecipheriv(알고리즘, 키, iv): 복호화할 때 사용. 암호화할 때 사용했던 알고리즘과 키, iv를 그래도 넣어야

  • decipher.update(문자열, 인코딩, 출력 인코딩) : 암호화된 문장, 그 문장의 인코딩, 복호화할 인코딩을 넣는다.

  • decipher.final(출력 인코딩): 복호화 결과물의 인코딩을 넣는다

3.5.6 util

util이라는 이름처럼 각종 편의 기능을 모아둔 모듈. 계속해서 API가 추가되고 있으며, 가끔 deprecated되어 사라지는 경우도 있다.

deprecated란?

deprecated는 프로그래밍 용어로, '중요도가 떨어져 더 이상 사용되지 않고 앞으로 사라지게 될'것이라는 뜻. 새로운 기능이 나와서 기존 기능보다 더 좋을 때, 기존 기능을 deprecated 처리하곤 한다. 이전 사용자를 위해 기능을 제거하지는 않지만 곧 없앨 예정이므로 사용하지 말라는 의미.

const util = require('util');
const crypto = require('crypto');

const dontUseMe = util.deprecate((x, y) => {
    console.log(x + y);
}, '요 함수는 deprecated되었으니 더 이상 사용하지 마세요!');
dontUseMe(1, 2);
// 3
// (node:11788) DeprecationWarning: 요 함수는 deprecated되었으니 더 이상 사용하지 마세요!
// (Use `node --trace-deprecation ...` to show where the warning was created)

const randomBytePromise = util.promisify(crypto.randomBytes);
randomBytePromise(64)
    .then((buffer) => {
        console.log(buf.toString('base64'));
    })
    .catch((err) => {
        console.error(error);
    })
  • util.deprecate: 함수가 deprecated 처리되었음을 알림. 첫 번째 인수로 넣은 함수를 사용했을 때 경고 메세지가 출력. 두 번째 인수로 경고 메세지 내용을 넣으면 된다. 함수가 조만간 사라지거나 변경될 때 알려줄 수 있어 유용
  • util.promisify : 콜백 패턴을 프로미스 패턴으로 바꿈. 바꿀 함수를 인수로 제공하면 된다. 이렇게 바꿔두면 async/await 패턴까지 사용할 수 있어 좋다.

3.5.8 child_process

노드에서 다른 프로그램을 실행하고 싶거나 명령어를 수행하고 싶을 때 사용하는 모듈. 현재 노드 프로세스 외에 새로운 프로레스를 띄워서 명령을 수행하고, 노드 프로세스에 결과를 알려주기 때문.

먼저 명령 프롬프트의 명령어인 dir을 노드를 통해 실행.

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.log(data.toString())
}) // 실행 에러 

exec의 첫 번째 인수로 명령어를 넣는다. 결과는 stdout(표준출력)과 stderr(표준에러)에 붙여둔 data 이벤트 리스너에 버퍼 형태로 전달된다. 성공적인 결과는 표준출력에서, 실패한 결과는 표준에러에서 표시된다.


3.6 파일 시스템 접근하기

fs 모듈은 파일 시스템에 접근하는 모듈. 즉, 파일을 생성하거나 삭제하고, 읽거나 쓸 수 있습니다. 폴더도 만들거나 지울 수 있다. 웹 브라우저에서 자바스트립트를 사용할 때는 일부를 제외하고 파일 시스템 접근이 금지되어 있으므로 노드의 fs 모듈이 낯설 것임.

const fs = require('fs');

fs.readFile('./readme.txt', (err,data)=> {
    if(err){
        console.log(err); 
    }
    console.log(data);
    console.log(data.toString());
})

fs모듈을 불러온 뒤 읽을 파일의 경로를 지정한다. 여기서는 파일의 경로가 현재 파일 기준이 아니라 node 명령어를 실행하는 콘솔 기준. readFile의 결과물은 버퍼(buffer)라는 형식으로 제공됨. 지금은 단순히 버퍼를 메모리의 데이터라고 생각하면 된다. 버퍼는 사람이 읽을 수 있는 형식이 아니므로 toString을 통해서 문자열로 변환. fs는 기본적으로 콜백형식의 모듈이므로 실무에서 사용하기 불편. 따라서 fs모듈을 프로미스 형식으로 바꿔주는 방법을 사용.

readFilePromise.js
const fs = require('fs').promises;
fs.readFile('./readme.txt')
.then((data) => {
    console.log(data);
    console.log(data.toString());
})
.catch((err) => {
    console.log(err);
})

fs 모듈에서 promise 속성을 불러오면 프로미스 기반의 fs모듈을 사용할 수 있께 된다. 앞으로는 프로미스 기반의 fs 모듈을 사용하겠습니다.

writeFile.js
const fs = require('fs').promises;

fs.writeFile('./write.txt','글이 입력됩니당!')
.then(()=>{
    return fs.readFile('./write.txt')
})
.then((data)=>{
    console.log(data.toString());
})
.catch((err)=>{
    console.error(err);
})

writeFile 메서드에 생성될 파일의 경로와 내용을 입력. 도중에 에러가 발생하지 않았다면 가은 폴더 내에 writeme.txt가 생성되었을 것. 파일이 잘 만들어졌고 읽기도 성공 자바스크립트로도 파일 시스템에 간단히 접근 가능.


3.6.1 동기 메서드와 비동기 메서드

setTimeout 같은 타이머와 process.nextTick 외에도, 노드는 대부분의 메서드를 비동시 방식르오 처리. 하지만 몇몇 메서드는 동기 방식으로도 사용할 수 있다. 특히 fs 모듈이 그런 메서드가 많다.

async.js
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('끝!')
결과
시작
끝!
1번 저를 여러번 읽어보세요!!
3번 저를 여러번 읽어보세요!!
2번 저를 여러번 읽어보세요!!

시작과 끝을 제외하고는 순서가 반복 실행할 때마다 달라진다. 비동기 메서드들은 백그라운드에 해당 파일을 읽으라고만 요청하고 다음 작업으로 넘어간다. 따라서 파일 읽기 요청만 세 번 보내고 console.log('끝')을 찍는다. 나중에 읽기가 완료되면 백그라운드가 다시 메인 스레드에게 알리고 그제서야 등록된 콜백 함수를 실행.

이 방식은 상당히 좋다. 수백 개의 I/O 요청이 들어와도 메인 스레드는 백그라운드에 요청 처리를 위임. 그 후로도 얼마든지 요청을 더 받을 수 있다. 나중에 백그라운드가 각각의 요청 처리가 완료되었다고 알리면 그 때 콜백 함수를 처리하면 된다.

백 그라운드에서는 요청 세 개를 거의 동시에 실행. 백그라운드는 어떻게 파일 읽기 작업을 처리할지는 이후에 스레드풀을 다루면서 알아볼 예정.

동기와 비동기, 블로킹와 논 블로킹


동기와 비동기, 블로킹과 논 블로킹이라는 네 개의 용어가 노드에서 혼용.

  • 동기와 비동기: 백그라운드 작업 완료 확인 여부
  • 블로킹과 논 블로킹 : 함수가 바로 return 되는지 여부

노드에서 동기-블로킹 방식과 비동기-논 블로킬 방식이 대부분. 교차는 없다고 봐도 무방. 동기 - 블로킹 방식에서는 백그라운드 작업 완료 여부를 계속 확인하여, 호출한 함수가 바로 return 되지 않고 백그라운드 작업이 끝나야 return. 비동기-논블로킹 방식에서는 호출한 함수가 바로 return 되어 다음 작업으로 넘어가며, 백그라운드 작업 완료 여부는 신경 쓰지 않고 나중에 백그라운드가 알림을 줄 때 비로소 처리.

순서대로 출력하고 싶다면 다음 메서드를 사용할 수도

sync.js
const fs = require('fs');

console.log('시작');
let data = fs.readFileSync('./readme2.tx')
console.log('1번', data.toString());

data = fs.readFileSync('./readme2.tx')
console.log('2번', data.toString());

data = fs.readFileSync('./readme2.tx')
console.log('3번', data.toString());

console.log('끝!')

결과는? 콘솔에 순서대로 찍힘. 코드는 간결하지만 치명적인 단점. readFileSycn 메서드를 사용하면 요청이 수백 개 이상 들어올 때 성능에 문제가 생긴다. Sync 메서드를 사용할 때 이전 작업이 완료되어야 다음 작업을 진행할 수 있음. 즉, 백그라운드가 작업하는 동안 메인 스레드는 아무것도 하지 못하고 대기하고 있어야 하는 것. 메인 스레드가 일을 하지 않고 노는 시간이 생기므로 비효율적. 대부분의 경우 비동기 메서드가 훨씬 더 효율적이다. 비동기 방식으로 하되 순서를 유지하고 싶다면 어떻게 해야할까? 이전 readFile의 콜백에 다음 readFile을 넣으면 된다. 이른다 콜백 지옥이 펼쳐지지만 적어도 순서가 어긋나는 일은 없다.

콜백지옥은 promise나 async/await으로 어느 정도 해결할 수 있다.

asyncOrderPromise.js
const fs = require('fs').promises;

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

// 시작
// 1번 동기처리 테스트
// 2번 동기처리 테스트
// 3번 동기처리 테스트
// 끝

실행결과는 asyncOrder.js와 같다. 지금까지 메서드와 비동기 메서드의 차이를 알아봄. 이제 readFile과 readFileSync에서 받아온 data를 data.toString()으로 변환하는 이유를 알아봄! 결론부터 말하자면 toString 메서드를 사용하는 이유는 data가 버퍼이기 때문에. 버퍼가 무엇인지 알아보자!


0개의 댓글