⚡노드 기능 알아보기⚡

gdhi·2023년 12월 27일

Node.js

목록 보기
2/7
post-thumbnail

📖노드 기능 알아보기

노드와 처음 상호작용을 해보고, 노드로 자바스크립트 파일을 실행하는 방법을 알아본다. 또한, 노드가 기본적으로 제공하는 객체와 모듈 사용법에 대해서도 알아본다. 모듈을 사용하면서 중요한 개념인 버퍼와 스트림, 동기와 비동기, 이벤트, 예외 처리에 대해서도 배운다. 새로운 개념이 많이 나오는데, 한 번에 외우려 하지 말고. 잊어버렸을 때마다 다시 찾아서 보면 된다.



📌REPL 사용하기

콘솔에서 즉석으로 실행 하는 것

입력한 코드를 읽고(Read), 해석하고(Eval), 결과물을 반환하고(Print), 종료할 때까지 반복(Loop)한다고 해서 REPL(Read Eval Print Loop)이라고 부른다



📌JS 파일 실행하기

1일차 부터 했던 것. node + 파일이름

function helloWorld() {
    console.log('Hello World');
    helloNode();
}
function helloNode() {
    console.log('Hello Node');
}
helloWorld();



📌모듈로 만들기

특정한 기능을 하는 함수나 변수들의 집합.
예를 들면 수학에 관련된 코드들만 모아서 모듈을 하나 만들 수 있다.

자체로도 하나의 프로그램이면서 다른 프로그램의 부품으로도 사용할 수 있다
👉 여러 프로그램(파일)에서 해당 모듈을 재사용할 수 있다. 자바스크립트에서 코드를 재사용하기 위해 함수, 클래스 등으로 만드는 것과 비슷
👉 구조 분해 할당


📍CommonJS 모듈


07_1_var.js

const odd = '홀수입니다';
const even = '짝수입니다';

module.exports = {
    odd,
    even
};

07_2_func.js

const { odd, even } = require('./07_1_var'); // 07_1_var 참조

function checkOddOrEven(num){
    if(num % 2){ // 홀수면
        return odd;
    }
    return even;
}

module.exports = checkOddOrEven;

07_3_index.js

const { odd, even } = require('./07_1_var.js');
const checkNumber = require('./07_2_func.js');
function checkStringOddOrEven(str) {
    if (str.length % 2) { // 홀수면
        return odd;
    }
    return even;
}
console.log(checkNumber(10));
console.log(checkNumber(9));
console.log('------------');
console.log(checkStringOddOrEven('hello'));
console.log(checkStringOddOrEven('hi'));


07_3_index.js 실행



📍노드에서의 this

노드에서의 this는 브라우저의 this와는 다르다. 브라우저의 자바스크립트와 동일하지만 최상위 scope에 존재하는 thismodule.exports(또는 exports 객체)를 가리킨다. 함수 선언문 내부의 this는 곧 배울 global 객체를 가리킨다.

console.log(this);
console.log(this === module.exports);
console.log(this === exports);

function whatIsThis(){
    console.log('function', this === exports, this === global);
}

whatIsThis();



📍모듈을 불러오는 require

require는 함수고 함수는 객체이므로 require는 객체로서의 속성을 몇 개 가지고 있다. 그 중에서 require.cache(우리가 아는 그 캐시)require.main을 알아본다.

  • 한 번 require 한 파일은 require.cache에 저장
  • 새로 불러오지 않고 require.cache에 있는 것이 재사용 됨 👉 캐시
  • 새로 require를 원하면 require.cache를 삭제하면 된다(❌권장하지 않음❌)
  • require.main은 노드 실행시 첫 모듈을 가리킴
  • require.main 객체랑 require.cache 모듈 객체와 같다
  • module.exports는 순환구조가 되면 조용히 빈(bean 아님)객체로 변경 된다. 에러가 발생 하지 않는다.



require 예제 1

console.log('require가 가장 위에 오지 않아도 됩니다.');

// module.exports가 반드시 파일 최하단에 위치 할 필요가 없다
module.exports = '저를 찾아보세요.';

// require가 반드시 파일 최상단에 위치 할 필요가 없다
require('../Day03/07_1_var');

console.log('require.cache 입니다');
console.log(require.cache);

console.log('------------------')

console.log('require.main 입니다');
console.log(require.main === module);

console.log('------------------')
console.log('require.main.filename 입니다');
console.log(require.main.filename);



require 예제 2, 모듈이 서로 require 할 때


dep1

const dep2 = require('./03_2_dep2');
console.log('require dep2', dep2);
module.exports = () => {
    console.log('dep2', dep2);
};

dep1

const dep1 = require('./03_1_dep1');
console.log('require dep1', dep1);
module.exports = () => {
    console.log('dep1', dep1);
};

dep-run

const dep1 = require('./03_1_dep1');
const dep2 = require('./03_2_dep2');

dep1();
dep2();

👉 순환 참조가 되어 dep1{}로 빈 객체로 표시 됨. 순환 참조가 발생하지 않도록 구조를 잘 잡자.



📍ECMAScript 모듈

ES 모듈이라고도 불리며 공식적인 자바 스크립트 모듈 형식이다. 브라우저에서도 ES 모듈 사용이 가능 해 브라우저, 노드 모두 같은 모듈을 사용 할 수 있다는 장점이 있다. 이전 var.js를 ES 모듈로 바꿔본다.


var.mjs

export const odd = 'MJS 홀수입니다.';
export const even = 'MJS 짝수입니다.';



func.mjs

import { odd, even } from './04_1_var.mjs';

function checkOddOrEven(num){
    if (num % 2) { // 홀수면
        return odd;
    }

    return even;
}

export default checkOddOrEven;



index.mjs

import { odd, even } from './04_1_var.mjs';
import checkNumber from './04_2_func.mjs';

function checkStringOddOrEven(str){
    if (str.length % 2){ // 홀수면
        return odd;
    }

    return even;
}

console.log(checkNumber(10));
console.log(checkNumber(9));
console.log('------------');
console.log(checkStringOddOrEven('hello'));
console.log(checkStringOddOrEven('hi'));



📍다이나믹 임포트

조건부로 모듈을 불러오는 것을 다이나믹 임포트라 하며 ComminJS 모듈ES 모듈의 다이나믹 임포트 사용 방식이 다르다.


ComminJS 모듈

const a = false;

if(a){
    require('../Day03/07_2_func')
}

console.log('성공');



ES 모듈

👉 에러


const a = true;

if(a){
    const m1 = await import('./04_2_func.mjs');
    console.log(m1);
    const m2 = await import('./04_1_var.mjs');
    console.log(m2);
}

👉 최상위에서 importasync 없이 await 사용이 가능하다



📍⭐filename, dirname⭐

노드에서 파일 사이에 모듈 관계가 있는 경우가 많으므로 현재 파일 경로나 파일명을 알아야 하는 경우가 있다. 노드는 __filename, __dirname 키워드로 경로에 대한 정보를 제공하며 파일에 넣어두면 실행 시 현재 파일명과 현재 파일 경로로 바뀐다.

console.log(__filename);
console.log(__dirname);

ES 모듈

console.log(import.meta.url);

console.log('-------------------');
console.log('__filename은 에러');
console.log(__filename);



📌노드 내장 객체 알아보기

브라우저의 window 객체와 비슷하다고 보면 된다. 기본적으로 제공 한다.


📍global 내장 객체

브라우저의 window와 같은 전역 객체. 전역
객체이므로 모든 파일에서 접근할 수 있다. 또한, window.open 메서드를 그냥 open으로 호출할 수 있는 것처럼 global도 생략할 수 있다. 이전 절에서 사용했던 require 함수에서도 global.require에서 global이 생략된 것. 노드 콘솔에 로그를 기록하는 console 객체도 원래는 global.console이다.


globalA

module.exports = () => global.message;

globalB

const A = require('./07_1_globalA');

global.message = '안녕하세요';
console.log(A());



📍console 내장 객체

브라우저에서의 console과 거의 비슷. console 객체는 보통 디버깅을 위해 사용된다. 개발 중 변수에 값이 제대로 들어 있나 확인하기 위해 사용하기도 하고, 에러 발생 시 에러 내용을 콘솔에 표시하기 위해서도 사용하며, 코드 실행 시간을 알아보려고 할 때도
사용. cosole.log 말고 다른 console 도 사용 해본다.

const string = 'abc';
const number = 1;
const boolean = true;
const obj = {
    outside: {
        inside: {
            key: 'value',
        },
    },
};

// console.time(레이블). console.timeEnd(레이블)과 대응되어
// 같은 레이블을 가진 time과 timeEnd 사이의 시간을 측정함
console.time('전체 시간');

console.log('---------------------');

// console.log(내용). 평범한 로그를 콘솔에 표시
console.log('평범한 로그입니다 쉼표로 구분해 여러 값을 찍을 수 있습니다');
console.log(string, number, boolean);

console.log('---------------------');

// console.log(에러 내용). 에러를 콘솔에 표시
console.error('에러 메시지는 console.error에 담아주세요');

console.log('---------------------');

// console.table(배열). 배열의 요소로 객체 리터럴을 넣으면, 객체의 속성들이 테이블 형식으로 표현 됨
console.table([{ name: '제로', birth: 1994 }, { name: 'hero', birth: 1988}]);

console.log('---------------------');

// console.dir(객체, 옵션). 객체를 콘솔에 표시할 때 사용.
// 첫 번째는 인자 객체, 두 번째는 인자 옵션
console.dir(obj, { colors: false, depth: 2 });
console.dir(obj, { colors: true, depth: 1 });

console.log('---------------------');

// console.time(레이블). console.timeEnd(레이블)과 대응되어
// 같은 레이블을 가진 time과 timeEnd 사이의 시간을 측정함
console.time('시간 측정');
for (let i = 0; i < 100000; i++) {
    continue;
}

console.timeEnd('시간 측정');
function b() {
    // console.trace(레이블). 에러가 어디서 발생했는지 추적
    console.trace('에러 위치 추적');
}

function a() {
    b();
}
a();

console.timeEnd('전체 시간');



📍타이머 내장 객체

  • setTimeout(콜백함수, 밀리초) : 밀리초 뒤에 콜백함수 호출

  • setInterval(콜백함수, 밀리초) : 밀리초마다 콜백함수 호출

  • setImmediate(콜백함수) : 콜백함수 즉시 실행

  • clearTimeout(아이디): setTimeout 취소

  • clearInterval(아이디): setInterval 취소

  • clearImmediate(아이디): setImmediate 취소

const timeout = setTimeout(() => {
    console.log('1.5초 후 실행');
}, 1500);

const interval = setInterval(() => {
    console.log('1초마다 실행');
}, 1000);

const timeout2 = setTimeout(() => {
    console.log('실행되지 않습니다');
}, 3000);

setTimeout(() => {
    clearTimeout(timeout2);
    clearInterval(interval);
}, 2500);

const immediate = setImmediate(() => {
    console.log('즉시 실행');
});

console.log(' ');

const immediate2 = setImmediate(() => {
    console.log('실행되지 않습니다');
});


clearImmediate(immediate2);



📍process 내장 객체

현재 실행되고 있는 노드 프로세스에 대한 정보를 담고 있다.

위에서 사용하진 않았지만 process.env 는 application.property와 비슷하며 환경변수 설정할 때 사용한다. process.nextTick(콜백)은 다른 함수들 보다 우선해서 실행한다.
process.exit는 실행중인 노드 프로세스를 종료한다

process.env

나중에 다룰 것.
const secretId = process.env.SECRET_ID;
const secretCode = process.env.SECRET_CODE; 같이 환경변수를 다룰 때 사용



process.nextTick(콜백)

setImmediate(() => {
    console.log('immediate');
});

process.nextTick(() => {
    console.log('nextTick');
});

setTimeout(() => {
    console.log('timeout');
}, 0);

Promise.resolve().then(() => console.log('promise'));

👉

마이크로 태스크의 재귀 호출

process.nextTick으로 받은 콜백 함수나 resolvePromise는 다른 이벤트 루프에서 대기하는 콜백 함수보다도 먼저 실행된다. 그래서 비동기 처리를 할 때 setImmediate보다 process.nextTick을 더 선호하는 개발자도 있다. 하지만 이런 Microtask를 재귀 호출하게 되면 이벤트 루프는 다른 콜백 함수보다 Microtask를 우선하여 처리하므로 콜백 함수들이 실행되지 않을 수도 있다.



process.exit

let i = 1;

setInterval(() => {
    if (i === 5) {
        console.log('종료!');
        process.exit();
    }
    console.log(i);
    i += 1;
}, 1000);



📍기타 내장 객체

  • URL, URLSearchParams : 나중에 배움
  • AbortController, FormData, fetch, Headers, Request, Response, Event, EventTarget : 브라우저에서 사용하던 API가 노드에도 동일하게 생성
  • TextDecoder : Buffer를 문자열로 바꿈
  • TextEncoder : 문자열을 Buffer로 바꿈
  • WebAssembly : 웹 어셈블리 처리 담당



📌노드 내장 모듈 사용하기

노드에서 제공하는 모듈을 사용하면 운영체제 정보에도 접근할 수 있고, 클라이언트가 요청한 주소에 대한 정보도 가져올 수 있다. 자주 사용하는 것들만 알아본다.

📍os 내장 모듈

노드는 os 모듈에 정보가 담겨 있어 정보를 가져
올 수 있다.

const os = require('os');

console.log('운영체제 정보---------------------------------');
console.log('os.arch():', os.arch()); // process.arch와 동일
console.log('os.platform():', os.platform()); // process.platfrom과 동일
console.log('os.type():', os.type()); // 운영체제 종류
// 운영체제 부팅 후 지난 시간(초). process.uptime()은 노드 실행시간
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()); // 컴퓨터 코어 정보
// 컴퓨터 코어 개수. 노드는 하나 밖에 사용하지 않지만 `cluster` 모듈을 사용하여 프로세스를 늘릴 수 있다. `cpus()` 메소드를 사용 할 것임
console.log('os.cpus().length:', os.cpus().length); 

console.log('메모리 정보---------------------------------');
console.log('os.freemem():', os.freemem()); // 사용 가능한 메모리(RAM)
console.log('os.totalmem():', os.totalmem()); // 전체 메모리 용량

결과

$ node 12_os.js
운영체제 정보---------------------------------
os.arch(): x64
os.platform(): win32
os.type(): Windows_NT
os.uptime(): 677570.078
os.hostname(): DESKTOP-DBMGCEN
os.release(): 10.0.19045
경로---------------------------------
os.homedir(): C:\Users\thdta
os.tmpdir(): C:\Users\thdta\AppData\Local\Temp
cpu 정보---------------------------------
os.cpus(): [
  {
    model: 'Intel(R) Core(TM) i5-5200U CPU @ 2.20GHz',
    speed: 2197,
    times: {
      user: 18357937,
      nice: 0,
      sys: 9576984,
      idle: 95935375,
      irq: 1154046
    }
  },
  {
    model: 'Intel(R) Core(TM) i5-5200U CPU @ 2.20GHz',
    speed: 2197,
    times: {
      user: 17435015,
      nice: 0,
      sys: 9184890,
      idle: 97250093,
      irq: 211765
    }
  },
  {
    model: 'Intel(R) Core(TM) i5-5200U CPU @ 2.20GHz',
    speed: 2197,
    times: {
      user: 20321640,
      nice: 0,
      sys: 7443593,
      idle: 96104765,
      irq: 141078
    }
  },
  {
    model: 'Intel(R) Core(TM) i5-5200U CPU @ 2.20GHz',
    speed: 2197,
    times: {
      user: 19499203,
      nice: 0,
      sys: 6287468,
      idle: 98083359,
      irq: 122781
    }
  }
]
os.cpus().length: 4
메모리 정보---------------------------------
os.freemem(): 3156029440
os.totalmem(): 8484786176

👉 꾸진 노트북..



📍❗path

폴더와 파일의 경로를 쉽게 조작하도록 도와주는 모듈. path 모듈이 필요한 이유 중 하나는 운영체제별(Win은 /, POSIX는 \)로 경로 구분자가 다르기 때문.

const path = require('path');

const string = __filename;

console.log('path.sep : ', path.sep); // 경로 구분자. WIN : \, MAC,LINUX: /
console.log('path.delimiter : ', path.delimiter); // 환경변수 구분자 WIN : ;, MAN,LINUX: :

console.log('-----------------------------------');

console.log('path.dirname(string)', path.dirname(string)); // 현재 파일 경로
console.log('path.extname(string) : ', path.extname(string)); // 현재 파일 확장자
console.log('path.basename(string)', path.basename(string)); // 현재 파일명
// 확장자 포함 파일명
console.log('path.basename(string, path.extname(string))', path.basename(string, path.extname(string)));
console.log('-----------------------------------');
// 경로 root, dir, base, ext, name 으로 분리
console.log('path.parse(string) : ', path.parse(string)); 
// path.parse 한 객체를 파일 경로로 합침
console.log('path.format() : ', path.format({ 
    dir: 'C:\\NodeWork\\Day04',
    name: '13_path',
    ext: '.js'
}));
// /나 \를 실수로 여러 번 사용 했거나 혼용했을 때 정상적인 경로로 반환
console.log('path.normalize() : ', path.normalize('C://NodeWork\\\\Day04\\\\13_path.js'));
console.log('-----------------------------------');
// 절대경로인지 상대경로인지 true, false로 반환
console.log('path.isAbsolute(C:\\) : ', path.isAbsolute('C:\\')); 
console.log('path.isAbsolute(./home) : ', path.isAbsolute('./home'));
console.log('-----------------------------------');
// 경로 2개를 넣으면 첫 번째 경로에서 두 번째 경로로 가는 방법을 알림
console.log('path.relative() : ', path.relative('C:\\Nodework\\Day04\\13_path.js', 'C:\\'));
// 여러 인수를 넣으면 하나의 경로로 합침, 상대 경로. '..' : 전 폴더, '.' : 현재 폴더, '/, \' : 폴더나 파일을 가르킴
console.log('path.join() : ', path.join(__dirname, '..', '..', '/NodeWork', '.', '/Day04')); // 상대 경로
// 앞의 경로를 무시함, 절대 경로
console.log('path.resolve() : ', path.resolve(__dirname, '..', 'NodeWork', '.', '/Day04')); // 절대 경로



📍url

인터넷 주소를 쉽게 조작하도록 도와주는 모듈

👉 요즘은 위와 같은 WHATWG 방식만 사용

url 예제1

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

console.log('------------------------------------------');


const parsedUrl = url.parse('http://www.gilbut.co.kr/book/bookList.aspx?sercate1=001001000#anchor');
console.log('url.parse() : ', parsedUrl);
console.log('url.format() : ', url.format(parsedUrl));

url 예제2, searchParams 객체

const { URL } = require('url');

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의 모든 키를 리터럴 객체로 가져옴
console.log('searchParams.keys():', myURL.searchParams.keys());
// searchParams의 모든 값을 리터럴 객체로 가져옴
console.log('searchParams.values():', myURL.searchParams.values());

console.log('---------------------------------------------------');

// 해당 키를 추가. 같은 키의 값이 있으면 유지하고 하나 더 추가
myURL.searchParams.append('filter', 'es3');
myURL.searchParams.append('filter', 'es5');
console.log(myURL.searchParams.getAll('filter'));

console.log('---------------------------------------------------');

// append와 비슷하지만 같은 키의 값들을 모두 지우고 새로 추가
myURL.searchParams.set('filter', 'es6');
console.log(myURL.searchParams.getAll('filter'));

console.log('---------------------------------------------------');

// 해당 키를 제거
myURL.searchParams.delete('filter');
console.log(myURL.searchParams.getAll('filter'));

console.log('---------------------------------------------------');

// 조작한 searchParams 객체를 다시 문자열로 만듦. 문자열을 search에 넣으면 주소 객체에 반영 됨
console.log('searchParams.toString():', myURL.searchParams.toString());
myURL.search = myURL.searchParams.toString();



📍dns

DNS를 다룰 때 사용하는 모듈. 도메인을 통해 IP나 기타 DNS 정보를 얻고자 할 때 사용
도메인 있나 검색해보기

DNS?

IP 주소 및 기타 데이터를 저장하고 이름별로 쿼리할 수 있게 해주는 계층형 분산 데이터베이스. 즉, DNS는 컴퓨터가 서로 통신하는 데 사용하는 숫자 IP 주소로 변환되는, 쉽게 읽을 수 있는 도메인 이름의 디렉터리

서브 도메인?

m.naver.com 같은 것(네이버 모바일 페이지).

import dns from 'dns/promises';

const ip = await dns.lookup('gilbut.co.kr'); // lookup : ip 주소 얻기
console.log('IP : ', ip); // ip 주소

const a = await dns.resolve('gilbut.co.kr', 'A'); // resolve : ip 주소 얻기
console.log('A : ', a); // ipv4 주소. AAAA : ipv6 주소

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



📍crypto, 암호화

다양한 방식의 암호화를 도와주는 모듈. 특히 비밀번호 암호화를 할 때 사용 됨. node.js는 이걸 사용해서 암호화 함

단방향 암호화

복호화할 수 없는 암호화 방식으로 비밀번호는 보통 단방향 암호화 알고리즘을 사용해서 암호화를 함. 암호화 대신 해시 함수라고 불리기도 한다.

  • key 로 바로 암호화
  • salt 사용해서 암호화

양방향 암호화

암호화된 문자열을 복호화할 수 있으며, 암호화된 암호를 복호화 하려면 암호화할 때 사용한 키가 사용됨.


단방향 암호화 예제1

const crypto = require('crypto');

// createHash(알고리즘) : 사용할 해시 알고리즘을 넣음
// update(문자열) : 변환할 문자열을 넣음
// digest(인코딩) : 인코딩할 알고리즘을 넣음. hash64가 결과 문자열이 가장 짧아 자주 사용
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'));



단방향 암호화 예제2. pbkdf2 알고리즘(salt 사용)

const crypto = require('crypto');

// 64바이트 길이의 랜덤 문자열 생성 👉 salt가 된다
crypto.randomBytes(64, (err, buf) => {
    console.log('buf : ', buf); // buf가 궁금함
    console.log('buf.toString() : ', buf.toString());
    const salt = buf.toString('base64');
    console.log('salt : ', salt);
    // 비밀번호, salt, 반복 횟수(10만번), 출력 바이트, 해시 알고리즘
    // 👉 sha512로 변환된 결과값을 다시 sha512로 변환하는 과정을 10만번 반복
    crypto.pbkdf2('비밀번호', salt, 100000, 64, 'sha512', (err, key) => {
        console.log('key : ', key); // key가 궁금함
        console.log('key.toString() : ', key.toString());
        console.log('password : ', key.toString('base64'));
    })
})

👉 익명함수 안에 익명함수. 복잡해진다..

👉 password에 10만번 소금 쳐짐



양방향 암호화

const crypto = require('crypto');

// 사용 가능한 암호화 알고리즘
console.log('사용 가능한 암호화 알고리즘 : ', crypto.getCiphers());

console.log('------------------------------------------------------');

// 암호화 알고리즘
const algorithm = 'aes-256-cbc';
// aes-256-cbc 알고리즘 key는 32바이트
const key = 'abcdefghijklmnopqrstuvwxyz123456';
// aes-256-cbc 알고리즘 iv는 16바이트
const iv = '1234567890123456';

// 암호화 알고리즘과 키, iv 를 넣는다
const cipher = crypto.createCipheriv(algorithm, key, iv);
// cipher.update : 암호화할 대상과 대상의 인코딩, 출력 결과물의 인코딩을 넣는다
// 보통 문자열은 utf8 인코딩, 암호는 base64를 사용
let result = cipher.update('암호화할 문장', 'utf8', 'base64');
// cipher.final : 출력 결과물의 인코딩을 넣으면 암호화 완료
result += cipher.final('base64');
console.log('암호화 : ', result);

// 복호화 할 때 사용하며 암호화할 때 사용한 알고리즘, key, iv를 넣어야 한다
const decipher = crypto.createDecipheriv(algorithm, key, iv);
// decipher.update : 암호화된 문장, 그 문장의 인코딩, 복호화할 인코딩을 넣는다
// cipher.update 에서 'utf8, base64' 순으로 넣었다면 
// decipher.update는 'base64, utf8' 순으로 넣는다.
let result2 = decipher.update(result, 'base64', 'utf8');
// 복호화 결과물의 인코딩을 넣는다
result2 += decipher.final('utf8');
console.log('복호화 : ', result2);
사용 가능한 암호화 알고리즘 :  [
  'aes-128-cbc',
  'aes-128-cbc-hmac-sha1',
  'aes-128-cbc-hmac-sha256',
  'aes-128-ccm',
  'aes-128-cfb',
  'aes-128-cfb1',
  'aes-128-cfb8',
  'aes-128-ctr',
  'aes-128-ecb',
  'aes-128-gcm',
  'aes-128-ocb',
  'aes-128-ofb',
  'aes-128-xts',
  'aes-192-cbc',
  'aes-192-ccm',
  'aes-192-cfb',
  'aes-192-cfb1',
  'aes-192-cfb8',
  'aes-192-ctr',
  'aes-192-ecb',
  'aes-192-gcm',
  'aes-192-ocb',
  'aes-192-ofb',
  'aes-256-cbc',
  'aes-256-cbc-hmac-sha1',
  'aes-256-cbc-hmac-sha256',
  'aes-256-ccm',
  'aes-256-cfb',
  'aes-256-cfb1',
  'aes-256-cfb8',
  'aes-256-ctr',
  'aes-256-ecb',
  'aes-256-gcm',
  'aes-256-ocb',
  'aes-256-ofb',
  'aes-256-xts',
  'aes128',
  'aes128-wrap',
  'aes192',
  'aes192-wrap',
  'aes256',
  'aes256-wrap',
  'aria-128-cbc',
  'aria-128-ccm',
  'aria-128-cfb',
  'aria-128-cfb1',
  'aria-128-cfb8',
  'aria-128-ctr',
  'aria-128-ecb',
  'aria-128-gcm',
  'aria-128-ofb',
  'aria-192-cbc',
  'aria-192-ccm',
  'aria-192-cfb',
  'aria-192-cfb1',
  'aria-192-cfb8',
  'aria-192-ctr',
  'aria-192-ecb',
  'aria-192-gcm',
  'aria-192-ofb',
  'aria-256-cbc',
  'aria-256-ccm',
  'aria-256-cfb',
  'aria-256-cfb1',
  'aria-256-cfb8',
  'aria-256-ctr',
  'aria-256-ecb',
  'aria-256-gcm',
  'aria-256-ofb',
  'aria128',
  'aria192',
  'aria256',
  'camellia-128-cbc',
  'camellia-128-cfb',
  'camellia-128-cfb1',
  'camellia-128-cfb8',
  'camellia-128-ctr',
  'camellia-128-ecb',
  'camellia-128-ofb',
  'camellia-192-cbc',
  'camellia-192-cfb',
  'camellia-192-cfb1',
  'camellia-192-cfb8',
  'camellia-192-ctr',
  'camellia-192-ecb',
  'camellia-192-ofb',
  'camellia-256-cbc',
  'camellia-256-cfb',
  'camellia-256-cfb1',
  'camellia-256-cfb8',
  'camellia-256-ctr',
  'camellia-256-ecb',
  'camellia-256-ofb',
  'camellia128',
  'camellia192',
  'camellia256',
  'chacha20',
  'chacha20-poly1305',
  'des-ede',
  'des-ede-cbc',
  ... 31 more items
]



📍util

각종 편의 기능을 모아둔 모듈. 계속해서 API가 추가되고 있으며 deprecated(중요도가 떨어짐)되어 사라지는 경우도 있음

const util = require('util');
const crypto = require('crypto');
const { error } = require('console');

// util.deprecate : 함수가 deprecated 처리되었음을 알림
const dontUseMe = util.deprecate((x, y) => {
    console.log(x + y);
}, 'dontUseMe 함수는 deprecated 되었으니 더 이상 사용하지 마세요!');
dontUseMe(1, 2);

// util.promisify : 콜백 패턴을 프로미스 패턴으로 바꿈
const randomBytesPromise = util.promisify(crypto.randomBytes);
randomBytesPromise(64)
.then((buf) => {
    console.log(buf.toString('base64'));
})
.catch((error) => {
    console.log(error);
})



📍🔥worker_threads🔥

노드에서 멀티 스레드 방식으로 작업하는 모듈


워커 스레드 예제1

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

if (isMainThread){ // 부모일 때
    // 워커 스레드 생성
    const worker = new Worker(__filename);
    // worker.on : 이벤트 리스너로 워커 스레드에서 message를 보내면
    // 메세지를 받는 리스너 생성 👉 직접 종료해야함 ❗
    worker.on('message', message => console.log('from worker', message));
    worker.on('exit', () => console.log('worker exit')); // 종료
    // worker.postMessage: 워커에 데이터를 보냄
    worker.postMessage('ping');
}
else{ // 워커일 때
    // 부모가 메세지를 보내면 받는 리스너
    parentPort.on('message', (value) => {
        console.log('from parent', value);
        parentPort.postMessage('pong');
        // 끊어지는 이벤트 발생
        parentPort.close();
    });
}

👉 워커스레드 생성이 될 때 안보이는 코드 하나가 더 생겨 else로 빠진다고 생각하면 된다. 여러개 생기면 그 개수만큼 else로 빠지는 것임



워커 스레드 예제2. 여러개의 데이터 넘기기

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

if (isMainThread){ // 부모일 때
    const threads = new Set();
    // 워커를 생성하면서 workerDate 속성으로 데이터를 보냄
    threads.add(new Worker(__filename, {
        workerData: { start: 1 }
    }));
    // 워커를 생성하면서 workerDate 속성으로 데이터를 보냄
    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;
    // 각각 부모로부터 숫자를 받아 100을 더해 부모한테 보냄
    parentPort.postMessage(data.start + 100);
}



워커 스레드 예제3. 소수의 개수 구하기, 워커스레드 사용 ❌

const min = 2;
const max = 10000000; // 천만
const primes = []; // 배열에 담는다

function findPrimes(start, range){ // 2, 10000000
    let isPrime = true;
    const end = start + range; // 10000002
    for(let i = start; i < end; i++){ // 2부터 10000002 까지
        for(let j = min; j <Math.sqrt(end); j++){ // 2부터 루트 10000002 까지
            if(i !==j && i % j===0){ // i와 j가 다르고 나눴을 때 0이 나온다 👉 소수 아님
                isPrime = false; // 소수 아님 반환하고 가까운 for문 탈출
                break;
            }
        }
        if(isPrime){
            primes.push(i); // 소수 추가
        }
        isPrime = true; // 소수 맞음 반환
    }
}

console.time('prime'); // 시작 부터
findPrimes(min, max);
console.timeEnd('prime'); // 종료 까지 시간 구함
console.log(primes.length); // 소수 갯수 구함

👉 대략 17초 걸림



워커 스레드 예제4. 소수의 개수 구하기, 워커스레드 사용 ⭕

// 워크 스레드 사용을 위한 임포트
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

// 최소수 2로 시작
const min = 2;
// 소수 값을 담으려고 하는 배열
let primes = [];


// 소수를 구하는 함수 시작, 범위(매개변수) 시작~시작+범위의 소수를 구한다
function findPrimes(start, range) { // 2, 10000000
    let isPrime = true;
    const end = start + range; // 10000002
    for (let i = start; i < end; i++) { // 2부터 10000002 까지
        // 2부터 루트 10000002(대략 3,162) 까지
        for (let j = min; j < Math.sqrt(end); j++) {
            // i와 j가 다르고 나눴을 때 0이 나온다 👉 소수 아님
            if (i !== j && i % j === 0) {
                // 소수 아님 반환하고 가까운 for문 탈출
                isPrime = false;
                break;
            }
        }
        if (isPrime) {
            primes.push(i); // 소수 추가
        }
        isPrime = true; // 소수 맞음 반환
    }
}

if (isMainThread) {
    const max = 10000000; // 천만
    // 스레드 8개, 2~천만까지 나누는 건 일정하게 나눠 실행가능하지만
    // 숫자가 아닌 다른 경우는 나눠 일하는 게 일정하지 않을 수 있다
    // 싱글스레드보다 멀티스레딩이 오래 걸리는 경우도 발생하므로
    // 비용 등을 고려해 멀티스레딩을 해야한다.
    const threadCount = 8;
    const threads = new Set();
    // (10000000 - 2) / 8 = 1,249,999.75 👉 올림(floor)해서 125만
    const range = Math.floor((max - min) / threadCount);

    let start = min; // start = 2
    console.time('prime');
    for (let i = 0; i < threadCount - 1; i++) {  // 0~6
        const wStart = start;
        threads.add(new Worker(__filename, {
            workerData: {
                start: wStart, range
            }
        }));
        // [0]2, 1250002 / [1]1250002, 2500002 /.../[6]7500002, 87500002
        start += range;
    }
    threads.add(new Worker(__filename, {
        workerData: {
            // 마지막 [7] 은 따로 계산
            start, range: range + ((max - min + 1) % threadCount)
        }
    }));
    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);
}

👉 확실히 줄어들었다.



📍child_process

노드에서 다른 프로그램을 실행하고 싶거나 명령어를 수행하고 싶을 때 사용하는 모듈. 파이썬 같은 다른 언어의 코드를 실행하고 결과값을 받을 수 있음.



📍기타 모듈들

모듈설명
async_hooks비동기 코드의 흐름을 추적하는 실험적인 모듈
dgramUDP와 관련된 작업을 수행할 때 사용
netTCP나 IPC 통신과 같이 HTTP보다 로우 레벨인 작업을 할 때 사용
perf_hooks성능 측정을 위해 console.time보다 정교한 모듈
querystringURLSearchParams가 나오기 이전에 쿼리스트링을 다루기 위해 사용되었으며, 요즘은 URLSearchParams를 권장
string_decoder버퍼 데이터를 문자열로 변환하는데 사용
tlsTLS와 SSL에 관련된 작업을 수행할 때 사용
tty터미널과 관련된 작업을 할 때 사용
v8v8 엔진에 직접 접근하기 위해 사용
vm가상 머신에 직접 접근하기 위해 사용
wasi웹 어셈블리를 실행할 때 사용하는 실험적인 모듈



📌파일 시스템 접근하기

fs 모듈은 파일 시스템에 접근하는 모듈. 즉, 파일이나 폴더를 생성하거나 삭제하고, 읽거나 쓸 수 있다.

readme.txt

저를 읽어주세요.

readFile.js

const fs = require('fs');

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

👉 버퍼로 제공 됨

readFilePromise.js

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

fs.readFile('./01_readme.txt')
    .then((data) => {
        console.log(data);
        console.log(data.toString());
    })
    .catch((err) => {
        console.error(err);
    });

👉 fs 모듈을 프로미스 형식으로 바꿈. 프로미스 속성을 불러오면 프로미스 기반의 fs 모듈을 사용할 수 있게 된다.

writeFile.js

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

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

👉 생성 됨



📍동기 메소드와 비동기 메소드

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

비동기 메소드 예제

const fs = require('fs');

console.log('시작');

fs.readFile('./01_readme.txt', (err, data) => {
    if(err){
        throw err;
    }
    console.log('1번', data.toString());
});

fs.readFile('./01_readme.txt', (err, data) => {
    if(err){
        throw err;
    }
    console.log('2번', data.toString());
});

fs.readFile('./01_readme.txt', (err, data) => {
    if(err){
        throw err;
    }
    console.log('3번', data.toString());
});

console.log('끝');

👉 실행할 때마다 달라질 수 있는데 비동기이기 때문



비동기 메소드 순서 억지로 맞추기

const fs = require('fs');

console.log('시작');
fs.readFile('./01_readme.txt', (err, data) => {
    if(err){
        throw err;
    }
    console.log('1번', data.toString());
    fs.readFile('./01_readme.txt', (err, data) => {
        if(err){
            throw err;
        }
        console.log('2번', data.toString());
        fs.readFile('./01_readme.txt', (err, data) => {
            if(err){
                throw err;
            }
        console.log('3번', data.toString());
        console.log('끝');
        });
    });
});

👉 순서대로 실행



동기 메소드 예제

const fs = require('fs');

console.log('시작');
let data = fs.readFileSync('./01_readme.txt');
console.log('1번', data.toString());
data = fs.readFileSync('./01_readme.txt');
console.log('2번', data.toString());
data = fs.readFileSync('./01_readme.txt');
console.log('3번', data.toString());
console.log('끝');



프로미스나 async/await로 콜백지옥 해결하기

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

console.log('시작');
fs.readFile('./01_readme.txt')
  .then((data) => {
    console.log('1번', data.toString());
    return fs.readFile('./01_readme.txt');
  })
  .then((data) => {
    console.log('2번', data.toString());
    return fs.readFile('./01_readme.txt');
  })
  .then((data) => {
    console.log('3번', data.toString());
    console.log('끝');
  })
  .catch((err) => {
    console.error(err);
  });



📍버퍼와 스트림 이해하기

파일을 읽거나 쓰는 방식에는 버퍼를 이용하는 방식과 스트림을 이용하는 방식이 있다. 일상생활에서도 영상을 로딩 중일 때는 버퍼링한다고 하고, 영상을 실시간으로 송출할 때는 스트리밍한다고 한다.
버퍼링은 영상을 재생할 수 있을 때까지 데이터를 모으는 동작이고, 스트리밍은 방송인의 컴퓨터에서 시청자의 컴퓨터로 영상 데이터를 조금씩 전송
하는 동작이다. 스트리밍하는 과정에서 버퍼링을 할 수도 있다. 전송이 너무 느리면 화면을 내보내기까지 최소한의 데이터를 모아야 하고, 영상
데이터가 재생 속도보다 빨리 전송되어도 미리 전송받은 데이터를 저장할공간이 필요하기 때문.

노드의 버퍼와 스트림도 비슷한 개념인데 앞에서 readFile 메서드를 사용할 때 읽었던 파일이 버퍼 형식으로 출력되었다. 노드는 파일을 읽을
때 메모리에 파일 크기만큼 공간을 마련해두며, 파일 데이터를 메모리에 저장한 뒤 사용자가 조작할 수 있도록 해준다.
메모리에 저장된 데이터가 바로 버퍼이다

readFile() 방식의 버퍼가 편리하기는 하지만 문제점도 있는데, 예를들어 용량이 100MB인 파일이 있으면 읽을 때 메모리에 100MB의 버퍼를 만들어야
한다. 이 작업을 동시에 열 개만 해도 1GB에 달하는 메모리가 사용된다. 특히 서버 같이 몇 명이 이용할지 모르는 환경에서는 메모리 문제가 발생할 수 있다. 또한, 모든 내용을 버퍼에 다 쓴 후에야 다음 동작으로 넘어가므로 파일 읽기, 압축, 파일 쓰기 등의 조작을 연달아 할 때 매번 전체 용량을 버퍼로 처리해야 다음 단계로 넘어갈 수 있다.그래서 버퍼의 크기를 작게 만들어서 여러 번에 나눠서 보내는 방식이 등장했다. 예를 들면 버퍼 1MB를 만든 후 100MB 파일을 백 번에 걸쳐 보내는 것. 메모리 1MB로 100MB 파일을 전송할 수 있습니다.
이를편리하게 만든 것이 스트림이다


버퍼 예제

const buffer = Buffer.from('저를 버퍼로 바꿔보세요');

// 문자열을 버퍼로 바꿈
console.log('from() : ', buffer);
// 버퍼의 크기, 한글은 글자당 3바이트, 띄어쓰기 1 👉 32
console.log('length : ', buffer.length);
// 버퍼를 다시 문자열로 바꿈
console.log('toString() : ', buffer.toString());

const array = [Buffer.from('띄엄 '), Buffer.from('띄엄 '), Buffer.from('띄어쓰기')];
// 배열 안에 든 버퍼들을 하나로 합침
const buffer2 = Buffer.concat(array);
console.log('concat() : ', buffer2.toString());

// 빈 버퍼를 생성, 바이트를 인수로 넣으면 해당 크기의 버퍼가 생성
// 👉 동적 할당
const buffer3 = Buffer.alloc(5);
console.log('alloc() : ', buffer3);



스트림 예제

const fs = require('fs');

const readStream = fs.createReadStream('./09_readme.txt', {
    highWaterMark: 16 // 스트림을 16개씩 끊는다
});

const data = [];

// 이벤트 리스너
readStream.on('data', (chunk) => {
    data.push(chunk);
    console.log('data : ', chunk, chunk.length);
});

readStream.on('end', () => {
    console.log('end : ', Buffer.concat(data).toString());
});

readStream.on('error', (err) => {
    console.log('error : ', err);
});



스트림으로 파일 쓰기

const fs = require('fs');

const writeStream = fs.createWriteStream('./13_writeme2.txt');
writeStream.on('finish', () => {
    console.log('파일 쓰기 완료');
});

writeStream.write('이 글을 씁니다. \n');
writeStream.write('한 번 더 씁니다.');
writeStream.end();



스트림 파일 복사하기

const fs = require('fs');

const readStream = fs.createReadStream('01_readme.txt');
const writeStream = fs.createWriteStream('15_writeme3.txt');
readStream.pipe(writeStream);



스트림 파일 압축하기

const zlib = require('zlib');
const fs = require('fs');

const readStream = fs.createReadStream('./01_readme.txt');
const zlibStream = zlib.createGzip();
const writeStream = fs.createWriteStream('./01_readme.txt.gz');

readStream.pipe(zlibStream).pipe(writeStream);



파이프라인으로 압축하기

import { pipeline } from "stream/promises";
import zlib from "zlib";
import fs from "fs";

await pipeline(
    fs.createReadStream('./01_readme.txt'),
    zlib.createGzip(),
    fs.createWriteStream('./01_readme2.txt.gz')
);



파이프라인으로 중단하기

import { pipeline } from "stream/promises";
import zlib from "zlib";
import fs from "fs";

const ac = new AbortController();
const signal = ac.signal;

setTimeout(() => ac.abort(), 1); // 1ms 뒤에 중단

await pipeline(
    fs.createReadStream('./01_readme.txt'),
    zlib.createGzip(),
    fs.createWriteStream('./01_readme3.txt.gz'),
    { signal }
);

👉 { signal } 을 마지막에 추가하고 원하는 시점에 ac.abort() 호출



용량이 큰 파일 만들기

const fs = require('fs');
const file = fs.createWriteStream('./big.txt');

for (let i = 0; i <= 10000000; i++) {
  file.write('안녕하세요. 엄청나게 큰 파일을 만들어 볼 것입니다. 각오 단단히 하세요!\n');
}
file.end();

👉 실행은 안함. 1기가 정도의 파일 생성 됨



버퍼 메모리 확인하기

const fs = require('fs');

console.log('before: ', process.memoryUsage().rss);

const data1 = fs.readFileSync('./big.txt');
// 복사
fs.writeFileSync('./big2.txt', data1);

console.log('buffer : ', process.memoryUsage().rss);

👉 용량을 더 많이 쓰게 된다.



스트림 메모리 확인하기

const fs = require('fs');

console.log('before : ', process.memoryUsage().rss);

const readStream = fs.createReadStream('./big.txt');
const writeStream = fs.createWriteStream('./big3.txt');

readStream.pipe(writeStream);

readStream.on('end', () => {
    console.log('stream : ', process.memoryUsage().rss);
});

👉 큰 파일을 조각내어 작은 버퍼 단위로 옮기기 때문에 용량을 적게 차지한다.



📍폴더 생성/삭제/복사 fs 메소드

fs는 파일 시스템을 조작하는 다양한 메서드를 제공한다. 여태 단순히 파일 읽기/쓰기를 했지만, 파일을 생성하고 삭제할 수도 있으며 폴더를 생성하고 삭제할 수도 있다

메서드설명
fs.access(경로, 옵션, 콜백)파일/폴더 접근 여부 확인. F_OK(존재), R_OK(읽기 권한), W_OK(쓰기 권한) 상수 사용. ENOENT 코드 반환 가능.
fs.mkdir(경로, 콜백)폴더 생성 메서드. 이미 존재 시 에러 발생. access로 먼저 확인이 필요.
fs.open(경로, 옵션, 콜백)파일 아이디(fd) 가져오기. 파일 없으면 생성 후 아이디 반환. 옵션으로 동작 지정 (w: 쓰기, r: 읽기, a: 추가).
fs.rename(기존 경로, 새 경로, 콜백)파일 이름 변경 메서드. 다른 폴더로 이동 가능.
fs.readdir(경로, 콜백)폴더 내용 확인. 배열로 파일과 폴더명 반환.
fs.unlink(경로, 콜백)파일 삭제. 없으면 에러 발생. access로 먼저 확인이 필요.
fs.rmdir(경로, 콜백)폴더 삭제. 폴더 안에 파일이 있으면 에러 발생. 내부 파일 먼저 삭제 후 호출 필요.

폴더 생성하기

const fs = require('fs').promises;
const constants = require('fs').constants;

fs.access('./folder', constants.F_OK | constants.W_OK | constants.R_OK)
    .then(() => {
        return Promise.reject('이미 폴더 있음');
    })
    .catch((err) => {
        if (err.code === 'ENOENT') {
            console.log('폴더 없음');
            return fs.mkdir('./folder');
        }
        return Promise.reject(err);
    })
    .then(() => {
        console.log('폴더 만들기 성공');
        return fs.open('./folder/file.js', 'w');
    })
    .then((fd) => {
        console.log('빈 파일 만들기 성공', fd);
        return fs.rename('./folder/file.js', './folder/newfile.js');
    })
    .then(() => {
        console.log('이름 바꾸기 성공');
    })
    .catch((err) => {
        console.error(err);
    });

👉 한 번더 실행했을 때



폴더 삭제하기

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

fs.readdir('./folder')
    .then((dir) => {
        console.log('폴더 내용 확인', dir);
        return fs.unlink('./folder/newFile.js');
    })
    .then(() => {
        console.log('파일 삭제 성공');
        return fs.rmdir('./folder');
    })
    .then(() => {
        console.log('폴더 삭제 성공');
    })
    .catch((err) => {
        console.error(err);
    });



폴더 복사하기

const { error } = require('console');

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

fs.copyFile('../Day06/01_readme.txt', 'wirteme4.txt')
    .then(() => {
        console.log('복사 완료');
    })
    .catch((error) => {
        console.error(error);
    });



파일/폴더 변경 사항 감시하기

👉 깡통 target.txt 생성

const fs = require('fs');

// 계속 실행 됨
fs.watch('./target.txt', (eventType, filename) => {
    console.log(eventType, filename);
});

👉 이름 변경



📍⚡스레드 풀 알아보기⚡

이전 섹션에서는 파일 시스템 모듈의 비동기 메소드를 활용하여 작업할 때, 이 메소드들이 백그라운드에서 실행되고 완료된 후에는 메인 스레드의 콜백 함수나 프로미스의 then 부분이 실행되는 특성을 경험했다. 이러한 동작은 스레드 풀이라는 메커니즘을 통해 이루어진다.

스레드 풀은 fs 모듈 외에도 crypto, zlib, dns.lookup 등과 같은 모듈에서 활용되고 있다. 특히, crypto 모듈의 crypto.pbkdf2 메소드를 통해 스레드 풀의 존재를 확인할 수 있는데, 이 메소드는 비동기적으로 실행되면서 백그라운드에서 스레드 풀을 활용한다.

crypto.pbkdf2로 스레드 풀 사용하기

const crypto = require('crypto');

const pass = 'pass';
const salt = 'salt';
const start = Date.now();

crypto.pbkdf2(pass, salt, 1000000, 128, 'sha512', () => {
    console.log('1:', Date.now() - start);
});

crypto.pbkdf2(pass, salt, 1000000, 128, 'sha512', () => {
    console.log('2:', Date.now() - start);
});

crypto.pbkdf2(pass, salt, 1000000, 128, 'sha512', () => {
    console.log('3:', Date.now() - start);
});

crypto.pbkdf2(pass, salt, 1000000, 128, 'sha512', () => {
    console.log('4:', Date.now() - start);
});

crypto.pbkdf2(pass, salt, 1000000, 128, 'sha512', () => {
    console.log('5:', Date.now() - start);
});

crypto.pbkdf2(pass, salt, 1000000, 128, 'sha512', () => {
    console.log('6:', Date.now() - start);
});

crypto.pbkdf2(pass, salt, 1000000, 128, 'sha512', () => {
    console.log('7:', Date.now() - start);
});

crypto.pbkdf2(pass, salt, 1000000, 128, 'sha512', () => {
    console.log('8:', Date.now() - start);
});

👉 스레드풀이 4개 있어서 4개 실행 후 다시 4개가 실행 된다.



📌이벤트 이해하기

스트림을 배울 때 on('data', 콜백) 또는 on('end', 콜백)을 사용했다. 바로 data라는 이벤트와 end라는 이벤트가 발생할 때 콜백 함수를 호출하도록 이벤트를 등록한 것이다. createReadStream 같은 경우는 내부적으로 알아서 dataend 이벤트를 호출하지만, 우리가 직접 이벤트를 만들 수도 있다.

메소드설명
on(이벤트명, 콜백)이벤트 이름과 해당 이벤트 발생 시 실행할 콜백을 연결하는 메소드. 이벤트 리스닝에 활용됨.
addListener(이벤트명, 콜백)on 메소드와 동일한 기능을 제공하는 메소드.
emit(이벤트명)지정한 이벤트를 호출하는 메소드. 등록된 콜백들이 실행됨.
once(이벤트명, 콜백)한 번만 실행되는 이벤트를 설정하는 메소드.
removeAllListeners(이벤트명)지정한 이벤트에 연결된 모든 이벤트 리스너를 제거하는 메소드.
removeListener(이벤트명, 리스너)지정한 이벤트에서 특정 리스너를 제거하는 메소드.
off(이벤트명, 콜백)removeListener와 동일한 기능을 제공하는 메소드.
listenerCount(이벤트명)현재 해당 이벤트에 몇 개의 리스너가 연결되어 있는지 확인하는 메소드.

📍이벤트 생성/호출/삭제하기

const EventEmitter = require('events');

const myEvent = new EventEmitter();

myEvent.addListener('event1', () => {
    console.log('이벤트 1');
});

myEvent.on('event2', () => {
    console.log('이벤트 2');
});

myEvent.on('event2', () => {
    console.log('이벤트 2 추가');
});

myEvent.once('event3', () => {
    console.log('이벤트 3');
}); // 한 번만 실행됨

myEvent.emit('event1'); // 이벤트 호출
myEvent.emit('event2'); // 이벤트 호출

myEvent.emit('event3');
myEvent.emit('event3'); // 실행 안 됨

myEvent.on('event4', () => {
    console.log('이벤트 4');
});
myEvent.removeAllListeners('event4');
myEvent.emit('event4'); // 실행 안 됨

const listener = () => {
    console.log('이벤트 5');
};
myEvent.on('event5', listener);
myEvent.removeListener('event5', listener);
myEvent.emit('event5'); // 실행 안 됨

console.log(myEvent.listenerCount('event2'));

👉 스트림에서 봤던 on('data')on('end')에 대해서도 어느 정도 감이 온다. 겉으로 이 이벤트를 호출하는 코드는 없지만, 내부적으로는 chunk를 전달할 때마다 data 이벤트를 emit하고 있습니다. 완료되었을 경우에는 end 이벤트를 emit한 것

이제 직접 이벤트를 만들 수 있으므로 다양한 동작을 직접 구현할 수 있으며, 웹 서버를 구축할 때 사용할 수 있다



📌예외 처리하기

지금까지 배운 개념들만으로도 서버를 만들기에 충분하지만 서버를 운영할 때 코드에 에러가 발생하는 것은 치명적이므로 에러를 처리하는 방법을 배워야 한다.

노드에서는 예외 처리가 매우 중요하다. 예외는 주로 처리하지 못한 에러를 나타내며, 이러한 예외는 노드 프로세스의 실행을 중지시킨다. 노드는 메인 스레드가 하나뿐이므로 메인 스레드의 에러로 인한 중지는 전체 서버의 중단을 의미한다. 따라서 예외를 처리하는 방법을 알고 있어야 하며 예외가 발생하더라도 에러 로그를 기록하고 작업을 계속할 수 있도록 하는 것이 중요하다.

에러 메시지원인 및 해결 방법
node: command not found노드 경로가 환경 변수에 추가되지 않아 발생. 경로를 환경 변수에 추가하거나 노드를 재설치.
ReferenceError: 모듈 is not definedrequire로 모듈을 불러오지 않았거나 오타. 모듈을 정확히 require하고 오타를 확인.
Error: Cannot find module 모듈명필요한 모듈이 설치되지 않음. npm i 명령어로 모듈을 설치.
Error [ERR_MODULE_NOT_FOUND]존재하지 않는 모듈을 불러오려 할 때 발생. 모듈 이름 및 경로 확인.
Error: Can't set headers after they are sent응답을 중복으로 보냄. 응답을 한 번만 보내도록 코드 수정.
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed- JavaScript heap out of memory메모리 부족으로 스크립트가 정상 작동하지 않음. 코드 검토 및 노드 메모리 확장 (--max-old-space-size).
UnhandledPromiseRejectionWarning: Unhandled promise rejection프로미스 처리하지 않은 에러. 항상 catch를 붙여 에러 처리.
EADDRINUSE 포트 번호이미 사용 중인 포트. 다른 포트 사용하거나 해당 프로세스 종료.
EACCES 또는 EPERM노드 권한 부족. 파일/폴더 권한 확인 및 sudo 사용.
EJSONPARSEJSON 파일 구문 오류. 파일 구문 확인.
ECONNREFUSED연결 실패. 서버 주소 및 상태 확인.
ETARGET패키지 버전이 존재하지 않음. 올바른 버전 기입.
ETIMEOUT응답이 시간 내에 오지 않음. 서버 상태 확인.
ENOENT폴더나 파일이 존재하지 않음. 경로 및 대소문자 확인.

윈도에서 프로세스 종료하기

맥/리눅스에서 프로세스 종료하기


📍try/catch throw

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



📍promise

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

setInterval(() => {
    fs.unlink('./abcd.js').catch(console.error);
}, 1000);



📍예기치 못한 에러처리

process.on('uncaughtException', (err) => {
    console.error('예기치 못한 에러', err);
});

setInterval(() => {
    throw new Error('서버를 고장내주마');
}, 1000);

setInterval(() => {
    console.log('-------------------------------------------');
    console.log('실행됩니다');
    console.log('-------------------------------------------');
}, 2000);

👉 제일 좋은 방법같지만, 노드 공식문서엔 최후의 수단으로 사용할 것으로 권고한다. 이유는 에러 발생시 다음 동작이 제대로 동작하는지를 보장하지 않기 때문이다.

사실 에러처리는 날 때 마다 검색해서 공부하는 것이 좋고 이렇게 다 알필요는 없다.



0개의 댓글