관리 메뉴

진취적 삶

03 노드 기능 알아보기 본문

개발 도서/Node.js 교과서

03 노드 기능 알아보기

hp0724 2023. 7. 14. 11:41

3.1 REPL 사용하기

노드에서는 입력한 코드를 읽고(Read) 해석하고 (Eval) 결과물은 반환하고 (Print) 종료할때까지 반복한다 (Loop)라고 해서 REPL 이라고 한다.

3.2 JS 파일 실행하기

function helloWorld() {
  console.log("hello world");
  helloNode();
}

function helloNode() {
  console.log("hello Node");
}
helloWorld();

3.3 모듈로 만들기

노드는 코드를 모듈로 만들수 있다.

모듈 : 특정한 기능을 하는 함수나 변수들의 집합을 말한다.

노드에서는 두가지 형식의 모듈을 사용 CommonJs, ECMAScript

3.3.1 CommonJs 모듈

ex03_var.js

const odd = "CJS 홀수입니다 ";
const even = "CJS 짝수입니다 ";
module.exports = {
  odd,
  even,
};

ex04_func.js

//구조분해 할당
const { odd, even } = require("./Ex03_var");
function checkOddEven(num) {
  if (num % 2) {
    return odd;
  }
  return even;
}
module.exports = checkOddEven;

ex05_index.js

const { odd, even } = require("./Ex03_var");
const checkNumber = require("./Ex04_func");
function checkStringOddEven(str) {
  if (str.length % 2) {
    return odd;
  }
  return even;
}
console.log(checkNumber(10));
console.log(checkStringOddEven("hello"));

index.js 는 var.js와 func.js 모두 참조한다 . 모듈하나가 여러개의 모듈을 사용할수 있다 .

exports 객체 사용시 유의사항

exports 객체를 사용할 때는 module.exports 와의 참조 관계가 깨지지 않도록 주의해야한다.

module.exports 에는 어떤 값이든 대입해도 되지만 , exports에는 반드시 객체처럼 속성명과 속성값을 대입해야 한다.

노드에서의 this는 브라우저의 this와 조금 다르다 .

console.log(this); //{}
console.log(this === module.exports); //true
console.log(this === exports); //true
function whatIsThis() {
  console.log("function", this === exports, this === global);
}
whatIsThis(); //function false true

순환참조 발생

Ex07_dep1.js

const dep2 = require("./Ex08_dep2");
console.log("require dep2", dep2);
module.exports = () => {
  console.log("dep2", dep2);
};

Ex08_dep2.js

const dep1 = require("./Ex07_dep1");
console.log("require dep1", dep1);
module.exports = () => {
  console.log("dep1", dep1);
};

Ex09_depRun.js

const dep1 = require("./Ex07_dep1");
const dep2 = require("./Ex08_dep2");
dep1();
dep2();

require("./Ex07_dep1") 먼저 실행되고 Ex07_dep1.js에서는 require("./Ex08_dep2") 을 실행하는데

Ex08_dep2.js 에서는 require("./Ex07_dep1") 실행해야 한다. 이과정이 계속 반복 될경우 순환참조가 발생하고 dep1은 함수가 아니라 빈 객체로 표시된다. 순환참조가 생길경우 예기치 못한 동작이 발생할수 있다.

3.3.2 ECMAScript 모듈

공식적인 자바스크립트 모듈 형식이다 .

확장자 생략 불가능이다

Ex10_var.mjs

export const odd = "mjs 홀수입니다 ";
export const even = "mjs 짝수입니다 ";

Ex11_func.mjs

import { odd, even } from "./Ex10_var.mjs";

function checkOddOrEven(num) {
  if (num % 2) {
    return even;
  }
  return even;
}
export default checkOddOrEven;

Ex12_index.mjs

import { odd, even } from "./Ex10_var.mjs";
import checkNumber from "./Ex11_func.mjs";

function checkStringOddOrEven(str) {
  if (str.length % 2) {
    return odd;
  }
  return even;
}
console.log(checkNumber(10)); //짝수

console.log(checkStringOddOrEven("hello")); //홀수

3.3.3 다이내믹 임포트

조건부로 모듈을 불러오는것을 다이내믹 임포트라고 한다

CommonJS 는 가능

const a = false;
if (a) {
  require("./Ex04_func");
}
console.log("성공");

ES 모듈은 불가능 IF문 안에서 import 하는것이 불가능하다 .

그러나 import 라는 함수를 사용해서 모듈을 동적으로 불러올수 있다. import는 Promise를 반환하기에 await나 then을 붙여야 한다. async를 사용하지는 않지만 ES 모듈의 최상위 스코프에서는 async 함수 없이도 await 할수 있다.

const a = true;
async function myFunction() {
  if (a) {
    const m1 = await import("./Ex11_func.mjs");
    console.log(m1);
    const m2 = await import("./Ex10_var.mjs");
    console.log(m2);
  }
}
myFunction();

3.3.4 __filename ,__dirname

노드는 filename ,__dirname 이라는 키워드로 경로에 대한 정보를 제공한다.

console.log(__filename); //C:\\Users\\suha hwang\\Desktop\\NodeJs\\03\\Ex15.js
console.log(__dirname); //C:\\Users\\suha hwang\\Desktop\\NodeJs\\03

3.4 노드 내장 객체 알아보기

내장 객체와 내장 모듈은 따로 설치하지 않아도 바로 사용할수 있다.

3.4.1 global

브라우저의 window 같은 전역 객체이며 모든 파일에서 접근할수 있다.

require은 원래 global.require 인데 생략이 가능하다.

노드 콘솔에 로그를 기록하는 console 도 원래는 global.console이다 .

3.4.2 console

console 객체는 보통 디버깅을 위해 사용한다.

개발중 변수에 값이 제대로 들어있는지 확인하거나 에러 발생시 에러 내용을 콘솔에 표시하기 위해서 사용하거나 코드 실행 시간을 알아보려고 할 때도 사용한다.

const string = "abc";
const number = 1;
const boolean = true;
const obj = {
  outside: {
    inside: {
      key: "value",
    },
  },
};
console.time("전체 시간");
console.log(`평범한 로그 `);
console.log(string, number, boolean);
console.error(`에러 메시지는 console.error 에 담아`);

console.table([
  { name: "제로", birth: 1998 },
  { name: "최", birth: 1997 },
]);
console.dir(obj, { colors: false, depth: 2 });
console.dir(obj, { colors: true, depth: 1 });
console.time("시간 측정");

for (let i = 0; i < 10000; i++) {}
console.timeEnd("시간 측정");

function b() {
  console.trace("에러 위치 추적");
}
function a() {
  b();
}
a();
console.timeEnd("전체 시간");
  • console.time (레이블) console.timeEnd(레이블)과 대응되어 같은 레이블을 가진 time과 timeEnd 사이의 시간을 측정
  • console.table (배열) 배열의 요소로 객체 리터럴을 넣으면 ,객체의 속성들이 테이블 형식으로 표현된다.
  • console.dir(객체,옵션) 객체를 콘솔에 표시할때 사용한다. 옵션의 colors를 true로 하면 콘솔에 색이 추가되어 보기 편함, depth는 객체 안의 객체를 몇단계까지 보여줄지 결정 기본값은 2
  • console.trace(레이블) :에러가 어디서 발생했는지 추적

3.4.3 타이머

  • 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("즉시 실행 ");
});
const immediate2 = setImmediate(() => {
  console.log(`실행 안됨`);
});
clearImmediate(immediate2);

/* 실행순서
    즉시 실행 
    1초마다 
    1.5 초마다 
    1초마다 
*/

setImmediate (콜백)과 setTimeout(콜백,0)

setImmediate (콜백)과 setTimeout(콜백,0) 에 담긴 콜백 함수는 이벤트 루프를 거친뒤 즉시 실행된다. 둘의 차이점은 무엇인가?

setImmediate 가 setTimeout보다 먼저 실행된다. setTimeout이 0 으로 설정해도 최소시간이 있기때문이다.

프로미스 기반으로 then 대신 await를 사용하기 위해 ES 모듈을 사용

import { setTimeout, setInterval } from "timers/promises";

await setTimeout(3000);
console.log("3초후 실행");

for await (const startTime of setInterval(1000, Date.now())) {
  console.log("1초마다 실행", new Date(startTime));
}
/*3초후 실행
1초마다 실행 2023-04-14T03:20:46.840Z
1초마다 실행 2023-04-14T03:20:46.840Z
1초마다 실행 2023-04-14T03:20:46.840Z*/

3.4.4 process

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

> process.version
'v18.16.0'
> process.arch
'x64'
> process.platform
'win32'
> process.pid
15808
> process.uptime()
27.5784299
> process.execPath
'C:\\\\Program Files\\\\nodejs\\\\node.exe'
> process.pwd()
Uncaught TypeError: process.pwd is not a function
> process.cwd()
'C:\\\\Users\\\\suha hwang\\\\Desktop\\\\NodeJs\\\\03'

3.4.4.1 process.env

서비스의 중요한 키를 저장하는 공간으로도 사용된다.

서버나 데이터 베이스의 비밀번호를 코드에 직접 입력하는것은 위험하다.

중요한 비밀번호는 process.env 속성으로 대체한다.

const secretId = process.env.SECRET_ID;
const secretCode =process.env.SECRET_CODE;

3.4.4.2 process.nextTick (콜백)

이벤트 루프가 다른 콜백 함수들보다 nextTick 콜백함수를 우선으로 처리하도록 만든다.

setImmediate(() => {
  console.log("immediate");
});
process.nextTick(() => {
  console.log("nextTick");
});
setTimeout(() => {
  console.log("timeout");
}, 0);
Promise.resolve().then(() => console.log("promise"));

nextTick
promise
timeout
immediate

setImmediate 나 setTimout 보다 먼저 실행 Promise resovle 된 Promise보다는 우선시 한다.

process.nextTick 과 Promise를 마이크로 테스크라고 따로 구분해서 부른다 .

3.4.5 기타 내장 객체

URL ,URLSearchParams

AbortController , FormData, fetch,Headers, Request, Response, Event, EventTarget

TextDecoder

TextEncoder

WebAssembly

3.5 노드 내장 모듈 사용하기

3.5.1

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

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("cpu 정보 ");
//cpu 정보
console.log("os.cpus", os.cpus());
//cpu 코어 개수
console.log("os.cpus().length", os.cpus().length);

console.log("메모리 정보 ");
//사용가능 메모리
console.log("os.freemem()", os.freemem()); //5803655168
//총 메모리
console.log("os.totalmem()", os.totalmem()); //16541696000

3.5.2 path

path 모듈이 필요한 이유는 운영체제별로 경로 구분자가 다르기 때문이다.

*윈도 \ 로 구분

*POSIX / 로 구분

const path = require("path");
const string = __filename;
console.log("path.dirname()", path.dirname(string)); //C:\\Users\\suha hwang\\Desktop\\NodeJs\\03
console.log("path.basename()", path.basename(string)); //Ex23.js
console.log(
  "path.normalize()",
  path.normalize("//C:Userssuha hwangDesktopNodeJs\\\\\\\\03))") //\\\\C:Userssuha hwangDesktopNodeJs\\03))\\
);

path . join 과 path.resolve 차이

비슷해보이지만 동작 방식이 다르다. /를 만나면 path.resovle는 절대 경로로 인식해서 앞의 경로 무시 path.join은 상대 경로로 처리

path.join('/a','/b','c') // /a/b/c/
path.resolve('/a','/b','c')// /b/c 

상대경로와 절대 경로

절대 경로는 루트폴더 (C:\) 니 노드 프로세스가 실행되는 위치가 기준이 된다.

상대 경로는 현재 파일이 기준이 된다.

3.5.3 url

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

const myURL = new URL("<https://www.gilbut.co.kr/book/view?bookcode=BN003474>");
console.log("searchParams", myURL.searchParams);
console.log("searchParams getAll()", myURL.searchParams.getAll("bookcode")); // [ 'BN003474' ]
console.log("searchParams get()", myURL.searchParams.get("bookcode")); //BN003474
console.log("searchParams has()", myURL.searchParams.has("bookcode")); //true

console.log("searchParams keys()", myURL.searchParams.keys()); //URLSearchParams Iterator { 'bookcode' }
console.log("searchParams values()", myURL.searchParams.values()); //URLSearchParams Iterator { 'BN003474' }

myURL.searchParams.append("filter", "es3");
myURL.searchParams.append("filter", "es5");
console.log(myURL.searchParams.getAll("filter")); //[ 'es3', 'es5' ]

myURL.searchParams.set("filter", "es6");
console.log(myURL.searchParams.getAll("filter")); //[ 'es6' ]

console.log("searchParams toString()", myURL.searchParams.toString()); //bookcode=BN003474&filter=es6

URL과 URLSearchParams 모두 노드 내장 객체 require(’url’) 생략 가능

3.5.4 dns

DNS 를 다룰때 사용하는 모듈

도메인을 통해 IP 나 기타 DNS 정보를 얻고자할때 사용

IP주소는 간단하게 dns.lookup 이나 dns.resovle (도메인) 사용해서 얻을수 있다.

A(IP4 주소), AAA(ipv6주소 ) MX(메일서버) CNAME(별칭)

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("gilbut.co.kr", "CNAME");
console.log("CNAME", cname);

const any = await dns.resolve("gilbut.co.kr", "ANY");
console.log("ANY", any);
IP { address: '49.236.151.220', family: 4 }
A [ '49.236.151.220' ]
MX [
  { exchange: 'aspmx2.googlemail.com', priority: 10 },
  { exchange: 'aspmx.l.google.com', priority: 1 },
  { exchange: 'alt2.aspmx.l.google.com', priority: 5 },
  { exchange: 'aspmx3.googlemail.com', priority: 10 },
  { exchange: 'alt1.aspmx.l.google.com', priority: 5 }
]

ANY [
  { value: 'ns1-1.ns-ncloud.com', type: 'NS' },
  { value: 'ns1-2.ns-ncloud.com', type: 'NS' },
  {
    nsname: 'ns1-1.ns-ncloud.com',
    hostmaster: 'ns1-2.ns-ncloud.com',
    serial: 53,
    refresh: 21600,
    retry: 1800,
    expire: 1209600,
    minttl: 300,
    type: 'SOA'
  }
]
Node.js v18.16.0

3.5.5 crpyto

다양한 방식의 암호화를 도와주는 모듈

고객의 비밀번호는 반드시 암호화 해야한다.

3.5.5.1 단반향 암호화

단방향 암호화 : 복호화 할수 없는 암호화 방식

복호화 : 암호화된 문자열을 원래 문자열로 되돌려놓는 것을 의미

단방향 암호화는 원래 문자열을 찾을수 없다.

단방향 암호화 대신 해시 함수라고 부르기도 한다.

고객의 비밀번호는 복호화될 필요가 없으니깐 해시 함수 사용

해시 기법 : 어떠한 문자열을 고정된 길이의 다른 문자열로 바꿔버리는 방식

const crypto = require("crypto");
console.log(
  "base64",
  crypto.createHash("sha512").update("비밀번호").digest("base64")
); //dvfV6nyLRRt3NxKSlTHOkkEGgqW2HRtfu19Ou/psUXvwlebbXCboxIPmDYOFRIpqav2eUTBFuHaZri5x+usy1g==
console.log(
  "hex",
  crypto.createHash("sha512").update("비밀번호").digest("hex")
);
//hex 76f7d5ea7c8b451b773712929531ce92410682a5b61d1b5fbb5f4ebbfa6c517bf095e6db5c26e8c483e60d8385448a6a6afd9e513045b87699ae2e71faeb32d6

console.log(
  "base64",
  crypto.createHash("sha512").update("다른비밀번호").digest("base64")
); //H3wpOArA9Md5f6APVAK7kZxe4KLj9bu1OYfDTzObcO5z4+TGEtj8L5T+hF+nEJM+O205Je5uCGS7UTzQ5fgqDw==
  • createHash(알고리즘): sha512 이 현시점에서 가장 나음
  • update(문자열) :변환할 문자열넣어라
  • digest(인코딩) : 인코딩할 알고리즘 넣기 base64, hex 등등 base64 결과 문자열이 가장 짧아서 애용

현재는 노드를 지원하는 pbkdf2 알고리즘으로 비밀번호 암호화

기본 문자열에 salt 라고 불리는 문자열 붙인후 해시 알고리즘 반복해서 적용

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

/*
salt P+utTxHxBBrBA76Omdh7mPKMfSHcShWrP8KVvUtcjLYXLNwXVKHbXRUuBCajyMqqAhVD+oCmrgzz6ARmRcXCPQ==
password UFQx17hafm75G+8zGmpk+K1anPfOLegE/5k0yW+/nwJl2VEXu8lEPBas3SCfIRBpb3BTHcwZBOehRvvNiqH1Gg==
*/

randomBytes() 메서드로 64 바이트 길이의 문자열 만들기 이것이 salt

pbkdf2() 메서드에는 비밀번호,salt ,반복 회숫 ,출력 바이트 ,해시 알고리즘 넣는다 .

salt를 잘 보관해야 비밀번호를 찾을수 있다.

간단하게 암호화 하고싶을경우 npm 패키지인 crypto -js를 추천

3.5.6 util

util은 각종 편의 기능을 모아둔 모듈

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

const dontUseMe = util.deprecate((x, y) => {
  console.log(x + y);
}, "dont useem 사용하지 마세요 ");
dontUseMe(1, 2);

const randomBytesPromise = util.promisify(crypto.randomBytes);
randomBytesPromise(64)
  .then((buf) => {
    console.log(buf.toString("base64"));
  })
  .catch((error) => {
    console.error(error);
  });
  • util.deprecate : 함수가 deprecate 처리가 되었음을 알린다. 인수로 넣은 함수를 사용했을때 경고 , 두번째 인수는 경고 메시지 넣으면 됨
  • util.promisify : 콜백 패턴을 프로미스 패턴으로 바꾼다.

3.5.7 worker_threads

const { Worker, isMainThread, parentPort } = require("worker_threads");
if (isMainThread) {
  //부모일때
  //__filename을 워커에서 실행
  const worker = new Worker(__filename);
  //worker.on 메시지 받기
  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 worker pong
worker 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);
}

new Wokrer 호출할때 workerData 속성으로 원하는 데이터를 보낼수있다.

워커에서는 workerData를 data로 받아서 100을 더해 돌려준다.

돌려주는 순간 워커가 종료되어 worker.exit 실행

소수의 개수를 구하는 작업을 워커 스레드를 통해 구현하자 .

const min = 2;
const max = 10000000;
const primes = [];

function findPrimes(start, range) {
  let isPrime = true;
  const end = start + range; // 2+1000000
  for (let i = start; i < end; i++) {
    //sqrt를 하는이유는 예를들어 100이 가장클때 100의제곱근 10 보다 큰수는 안 나눠떨어지니깐 검색 속도를 향상시킨다.
    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");
findPrimes(min, max);
console.timeEnd("prime");
console.log(primes.length);

멀티 스레딩으로 소수구하기 속도가 확실히 빠르다.

멀티 스레딩을 할 때는 일을 나눠서 처리 하돌록 하는게 제일 어렵다.

const {
  Worker,
  isMainThread,
  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 = 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 wStart = start;
    threads.add(
      new Worker(__filename, { workerData: { start: wStart, range } })
    );
    start += range;
  }
  threads.add(
    new Worker(__filename, { workerData: { start, range: max - start } })
  );
  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 { //worker
  findPrimes(workerData.start, workerData.range);
  parentPort.postMessage(primes);
}

메인 스레드에서는 워커 스레드로부터 전달받은 소수들을 primes 배열에 저장하고, 모든 워커 스레드가 종료될 때까지 기다립니다. 이 때, worker.on() 메서드를 사용하여 각각의 워커 스레드의 이벤트를 처리합니다.

3.5.8 child_process

노드에서 다른 프로그램을 실행하고 싶거나 명령어를 수행하고 싶을 때 사용하는 모듈

이 모듈을 통해 다른 언어의 코드를 실행하고 결과값을 받을수 있다.

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

3.6 파일 시스템 접근하기

fs 모듈은 파일 시스템에 접근하는 모듈이다.

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

readFile 의 결과물을 버퍼 형식으로 제공

버퍼를 메모리의 데이터라고 생각하면 편하다.

fs는 기본적으로 콜백형식의 모듈이므로 실무에서 사용하기 어렵다. fs 모듈을 프로미스 형식으로 바꿔주는 방법을 사용

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

fs.readFile("./readme.txt")
  .then((data) => {
    console.log(data);
    console.log(data.toString());
  })
  .catch((err) => {
    console.error(err);
  });
const fs = require("fs");
fs.writeFile("./write.txt", "글 입력", (err) => {
  if (err) {
    throw err;
  }
  fs.readFile("./write.txt", (err, data) => {
    if (err) {
      throw err;
    }
    console.log(data.toString());
  });
});

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

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("끝");

비동기 메서드들은 백그라운드에 해당 파일을 읽으라고만 요청하고 다음 작업으로 넘어간다.

파일 읽기 요청만 3 번 보내고 console.log(’끝’)찍는다.

나중에 읽기가 완료되며 백그라운드가 다시 메인스레드에 알리고 메인스레드는 그제서야 등록된 콜백함수를 실행

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

노드에서는 동기 -블로킹 방식과 비동기-논블로킹 방식이 대부분이다.

동기-블로킹: 백그라운드 작업 완료 여부를 계속 확인하여, 호출함 함수가 바로 return 되지 않고 백그라운드 작업이 끝나야 return 한다.

비동기-논블로킹 : 호출한 함수가 바로 return 되어 다음 작업으로 넘어가고 ,백그라운드 작업 완료 여부는 신경 쓰지 않고 나중에 백그라운드가 알림을 줄 때 비로소 처리한다.

const fs = require("fs");

console.log("start");
let data = fs.readFileSync("./readme2.txt");
console.log("1번", data.toString());
data = fs.readFileSync("./readme2.txt");
console.log("2번", data.toString());
data = fs.readFileSync("./readme2.txt");
console.log("3번", data.toString());

readFileSync 메서드를 사용하면 요청이 수백 개 이상 들어올때 성능에 문제가 생긴다.

sync 메서드를 사용할 때는 이전 작업이 완료되어야 다음 작업을 진행할수 있음

백그라운드가 작업하는 동안 메인 스레드는 아무것도 못하고 대기해야함

동기 메서드들은 이름뒤에 Sync 가 붙어 있어 구분하기 쉽다.

대부분의 경우에 비동기 메서드가 훨씬더 효율적이다.

비동기 방식으로 하되 순서 유지 하는 법

무한 콜백 하면됨

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

무한 콜백은 async /await 로 바꿀수 있음

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

3.6.2 버퍼와 스트림 이해하기

data 를 data.tostring() 으로 변환하는 이유?

toString 메서드를 사용하는 이유는 data가 버퍼이기 때문이다.

파일을 읽거나 쓰는 방식에는 크게 두가지 방식이 있다. 버퍼를 이용하거나 스트림을 이용하는 방식이 있다.

영상을 로딩하고 있을때 버퍼링, 영상을 실시간으로 송출할때는 스트리밍

노드는 파일을 읽을 때 메모리에 파일 크기만큼 공간을 마련해두고 파일 데이터를 메모리에 저장한뒤 사용자가 조작할수 있도록 한다. 이때 메모리에 저장된 데이터가 바로 버퍼이다.

const buffer = Buffer.from("저를 버퍼로 바꿔라 ");
console.log("from", buffer);
console.log("length", buffer.length); //length 27
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); //alloc <Buffer 00 00 00 00 00>
  • from( 문자열) 문자열을 버퍼로 바꿀수 있다.
  • toString() 버퍼를 다시 문자열로 바꿀수 있다.
  • concat(배열) 배열안에 든 버퍼들을 하나로 합친다.
  • alloc(바이트) 빈 버퍼를 생성한다. 바이트를 인수로 넣으면 해당 크기의 버퍼가 생성된다.

readFile의 경우 용량이 100MB인 파일 읽으려면 메모리에 100MB 버퍼를 만들어야 한다.

동시에 열개만 해도 1GB 모든 내용을 버퍼에 다 쓴후에야 다음 동작으로 넘어간다.

파일 읽기 ,압축, 파일 쓰기 등의 조작을 연달아 할 때 매번 전체 용량을 버퍼로 처리해야 다음 단계로 넘어갈수 있다.

버퍼의 크기를 작게 만들고 여러번에 결쳐 나눠보내는 방식 등장

이를 편리하게 만든것이 스트림이다.

const fs = require("fs");
const readStream = fs.createReadStream("./readme3.txt", { highWaterMark: 16 });
const data = [];

readStream.on("data", (chuck) => {
  data.push(chuck);
  console.log("data :", chuck, chuck.length);
});

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

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

creatReadStream 으로 읽기 스트림을 만든다.

highWaterMark 버퍼의 크기를 정할수 있다.

const fs = require("fs");
const writeStream = fs.createWriteStream("./write2.txt");
writeStream.on("finish", () => {
  console.log("파일 쓰기 완료");
});
writeStream.write("이 글을 씁니다\\n");
writeStream.write("이 글을 씁니다\\n");
writeStream.end();

createWriteStream 으로 쓰기 스트림을 만든다.

finish 이벤트 리스너도 붙인다. 파일 쓰기가 종료되면 콜백 함수가 호출됨

createReadStream 으로 파일을 읽고 그 스트림을 전달받아 createWrtieStream 으로 파일을 쓸수도 있다. 파일 복사와 비슷 스트림끼리 연결하는것을 파이핑 한다고 표현

const fs = require("fs");

const readStream = fs.createReadStream("./readme4.txt");
const writeStream = fs.createWriteStream("./write3.txt");
readStream.pipe(writeStream);

파일 복사

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

3.6.3 기타 fs메서드 알아보기

fs는 파일 시스템을 조작하는 다양한메서드 제공

파일을 생성하고 삭제할수도 있으며 폴더를 생성하고 삭제할수 도있음

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);
  });
  • fs.access(경로,옵션,콜백) :폴더나 파일에 접근할수 있는지 체크한다. 두번째 인수로 상수를 넣는다 F_OK 파일 존재 여부 R_OK는 읽기 권한 여부 W_OK 쓰기 권한 여부 체크 파일 /폴더나 권한이 없다면 에러 발생
  • fs.mkdir(경로 ,콜백) 폴더를 만드는 메서드 이미 폴더가 있으면 에러 따라서 access 메서드 통해서 확인
  • fs.open(경로,옵션,콜백) : 파일의 아이디 가져오는 메서드 가져온 아이디를 사용해 fs.wrtie ,fs.read, 가능 두번째 인수에 어떤 동작할지 넣으면 됨 w, r, a 중에 넣으면된다.
  • fs.rename(기존경로, 새로운 경로, 콜백) : 파일의 이름을 바꾸는 메서드
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.log(err);
  });
  • fs.readdir(경로,콜백) 폴더 안의 내용물을 확인할수 있다.
  • fs.unlink(경로, 콜백) :파일을 지울수있다. 파일이 없다면 에러가 발생
  • fs.rmdir(경로 ,콜백) :폴더을 지울수 있다.

노드 8.5 버전 이후에는 createReadStream 과 createWriteStream 을 pipe 하지 않아도 파일 복사 가능

const fs = require("fs").promises;
fs.copyFile("readme4.txt", "writeme4.txt")
  .then(() => {
    console.log("복사 완료 ");
  })
  .catch((error) => {
    console.error(error);
  });
const fs = require("fs");

fs.watch("./target.txt", (eventType, fileName) => {
  console.log(eventType, fileName);
});
  • 내용물 수정 할때는 change 이벤트 발생
  • 파일명 변경하거나 파일을 삭제하면 rename 이벤트가 발생한다.

3.6.4 스레드 풀 알아보기

fs모듈의 비동기 메서드들을 사용해봄 비동기 메서드들은 백그라운에서 실행되고 ,실행된 후에는 다시 메인 스레드의 콜백 함수나 프로미스의 then 부분이 실행된다. 이때 fs 메서드를 여러번 실행해도 백그라운드에서 동시에 처리되는데 ,바로 스레드 풀이 있기 때문이다.

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: 1537
2: 1541
1: 1550
3: 1555
5: 3099
7: 3101
8: 3120
6: 3223

스레드 풀이 작업을 동시에 처리하므로 여덟개의 작업중에서 어느 것이 먼저 처리될지 모른다.

컴퓨터의 코어개수마다 차이가 있지만 스레드가 4개씩 묶여서 작업을 동시에 처리

스레드풀의 개수 조절 같은경우 SET UV_THREADPOOL_SIZE =1 로 하면됨

3.7 이벤트 이해하기

const EventEmitter = require("events");
const myEvent = new EventEmitter();
myEvent.addListener("event1", () => {
  console.log("이벤트1 ");
});
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(이벤트명, 콜백) :이벤트 이름과 이벤트 발생 시의 콜백을 연결한다. 이렇게 연결하는 동작을 이벤트 리스닝이라고 한다.
  • addListenr(이벤트명,콜백) :on 기능과 같다
  • emit(이벤트명) : 이벤트를 호출하는 메서드
  • once(이벤트명) :한번만 실행되는 이벤트
  • removeAllListeners(이벤트명): 이벤트에 연결된 모드 인벤트 리스너 제거
  • removeListener(이벤트명,리스너) : 이벤트에 연결된 리스너를 하나씩 제거
  • off(이벤트명 ,콜백) : removeListenr 와 기능이 같다
  • listenerCount(이벤트명) :현재 리스너가 몇개 연결되어 있는지 확인

3.8 예외 처리하기

노드의 메인 스레드는 하나 뿐이므로 그 하나를 소중히 해야한다.

메인 스레드가 에러로 인해 멈추면 .전체 서버도 멈춘다.

const fs = require("fs");
setInterval(() => {
    //존재하지 않는 파일 지우기 
  fs.unlink("./abcdefg.js", (err) => {
    if (err) {
      console.error(err);
    }
  });
}, 1000);

throw 하면 노드 프로세스가 멈춘다. 따라서 throw 하는 경우네는 반드시 try/catch 문으로 throw 한 에러를 잡아야한다.

const fs = require("fs").promises;
setInterval(() => {
  fs.unlink("./abcdefg.js").catch(console.error);
}, 1000);
process.on("uncaughtException", (err) => {
  console.error("예기치 못한 에러", err);
});
setInterval(() => {
  throw new Error("서버를 고장내주마 ");
}, 1000);
setTimeout(() => {
  console.log("실행 ");
}, 2000);

uncaughtException 이벤트 리스너로 모든 에러를 처리할수 있을것 같지만

노드 공식문서에는 uncaughtException 이벤트를 최후의 수단으로 사용할것을 명시하고 있다.

uncaughtException 은 단순히 에러 내용을 기록하는 정도로 사용하고 에러를 기록한 후 process.exit()으로 프로세스를 종료하는것이 좋다.

에러가 발생했을때 철저히 기록하는 습관을 들이고 ,주기적으로 로그를 확인하면서 보완해 나가야한다.

3.8.1 자주 발생하는 에러드

  • node :command not found : 환경 변수가 제대로 설정되어 있지 않은것
  • ReferenceError :모듈 is not defined :모듈을 require했는지 확인해야함
  • Error: cannot find module 모듈명 :해당 모듈을 설치안함
  • 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 meory : 코드를 실행할때 메모리가 부족해서 스크립트가 정상적으로 실행되지 않은것 코드가 잘못 구현되었을 확률이 높다. 코드가 정상일 경우 노드의메모리를 늘려야 한다.
  • UnhandedPromiseRejectionWarnin :unhandled promise rejection : 프로미스 사용시 catch 메서드를 붙이지 않으면 발생
  • EADDRINUSE 포트 번호 :해당 포트번호에 이미 다른 프로세스 사용중 다른 포트번호 사용해야한다.
  • EACCES 또는 EPERM : 노드가 작업을 수행하는데 권한이 충분치 않음
  • EJSONPARSE : JSON 파일에 문법 오류
  • ECONNREFUSED :요청을 보냈을나 연결이 성립되지 않을때 발생 .요청 받는 서버의 주소확인 서버 꺼져있는지 확인
  • ETARGET: package.json 에 기록한 패키지 버전이 존재하지 않을때 발생
  • ETIMEOUT : 요청을 보냈을나 응답이 시간 내에 오지 않을때 발생
  • ENOENT : no such file orr directory : 지정한 폴더나 파일이 존재하지 않는경우

'개발 도서 > Node.js 교과서' 카테고리의 다른 글

06 익스프레스 웹 서버 만들기  (0) 2023.07.14
05 패키지 매니저  (0) 2023.07.14
04 http 모듈로 서버 만들기  (0) 2023.07.14
02 알아둬야 할 자바스크립트  (0) 2023.07.14
01 노드 시작하기  (0) 2023.07.14