#3 노드 기능 알아보기
# REPL 사용하기
Read 읽고, Eval 평가하고, Print 출력하고, Loop 종료할 때까지 반복에서 앞글자만 따서 이야기 함.
콘솔에 node라고 입력하면 된다.
종료하는 방법으로는 컨트롤 + C 두번 혹은, .exit을 입력하여 종료.
# JS 파일 실행
해당 파일 생성후 콘솔에 node {해당파일} 입력하기 확장자는 생략.
# 모듈로 만들기
노드는 두가지 모듈 사용 CommonJS와 ECMAScript.
- CommonJS: 이건 표준 자바스크립트 모듈은 아니지만 표준 나오기 이전부터 쓰여서 널리 쓰임.
- ECMAScript(ES 모듈): 공식적인 자바스크립트의 모듈. ES 모듈이 표준으로 정해지면서, 브라우저와 노드 모두에서 같은 모듈 형식을 사용할 수 있다는 장점.
# CommonJS
// var.js
const odd = 'cjs odd'
const even = 'cjs even'
// 이 방법은 객체만 가능, func.js처럼 함수를 넣은 경우에는 불가.
exports.even = 'CJS 짝수';
exports.odd = 'CJS 홀수';
// module.exports = {
// odd,
// even
// }
두개의 변수를 선언하고 modlue.exports를 통해 대입.
이렇게하면 이제 모듈로써 기능함.
아래 코드는 참조하는 방식
// func.js
// 불러올 파일의 경로엥서 확장자(js, json)은 생략 가능.
// index.js도 생략 가능
const { odd, even } = require('./var'); // 불러올 파일의 경로
function checkOddOrEven(num) {
if (num % 2) {
return odd;
}
return even;
}
module.exports = checkOddOrEven;
// index.js
const { odd, even } = require('./var');
const checkNumber = require('./func');
function checkStringOddOrEven(str) {
if (str.length % 2) {
return odd;
}
return even;
}
console.log(checkNumber(10));
console.log(checkStringOddOrEven('hello'));
require 함수안에 불러올 모듈의 경로를 적어줌.
- exports 객체 사용시 주의사항
객체만 사용할 수 있으므로, 함수를 대입한 경우에는 이 방식으로 사용할 수 없음.
- 노드의 this는 좀 다름.
대부분의 this는 브라우저와 동일하게 global을 가르키나, 최상위 스코프는 module.exports를 가르킨다.
// require.js
console.log('require가 가장 위에 오지 않아도 됩니다.');
module.exports = '저를 찾아보세요.';
require('./var');
console.log('require.cache입니다.'); // 1
console.log(require.cache); // 2
console.log('require.main입니다.'); // 3
console.log(require.main === module); // 4
console.log(require.main.filename); // 5
1. 그대로 출력
2. 노드에서는 한번 require한 파일은 require.cache에 저장되므로 다음 번에 require할 때는 새로 불러오지 않고 캐시에 있는 것 사용.
3. 그대로 출력
4. require.main은 노드 실행 시 첫 모듈을 가르킨다. 따라서 현재는 require.main임.
따라서 현재 require.main과 require.cache(module, 캐시에서 불러왔기 때문에)는 동일.
만약 var.js에서 해당 코드를 실행하면 false로 반환
5. 현재 파일 네임 알려줌.
만약 모듈 불러올 때 순환참조하면 어떻게 될까?
// dep1.js
const dep2 = require('./dep2');
console.log('require dep2', dep2);
module.exports = () => {
console.log('dep2', dep2);
};
// dep2.js
const dep1 = require('./dep1');
console.log('require dep1', dep1);
module.exports = () => {
console.log('dep1', dep1);
};
//dep-run.js
const dep1 = require('./dep1');
const dep2 = require('./dep2');
dep1();
dep2();
dep1의 module.exports가 함수가 아니라 빈 객체로 표시.
순환참조의 경우에는 순환참조되는 대상을 빈 객체로 만들어버림. 에러가 아닌 워닝을 발생시킴.
# ECMAScript
// var.mjs
export const odd = 'cjs odd'
export const even = 'cjs even'
// func.mjs
import { odd, even } from './var';
function checkOddOrEven(num) {
if (num % 2) { // 홀수면
return odd;
}
return even;
}
export default checkOddOrEven;
// index.mjs
import { odd, even } from './var.mjs';
import checkNumber from './func.mjs';
// 🟡 const { odd, even } = require('./var');
// 🟡 const checkNumber = require('./func');
function checkStringOddOrEven(str) {
if (str.length % 2) {
return odd;
}
return even;
}
console.log(checkNumber(10));
console.log(checkStringOddOrEven('hello'));
require와 exports, module.exports가 각각 import, export ,export default로 변경되었다.
파일 확장자도 js에서 mjs로 변경되었음.
확장자를 js로 하고 import 사용시 에러 발생합니다.
만약 확장자 js를 사용하면서 ES모듈을 사용하려면 package.json에 type: "module" 속성을 넣으면 된다.
CommonJS 모듈과는 다르게 파일 경로에서 js, mjs 같은 확장자는 생략할 수 없음. 폴더 내부에서 index.js도 생략 불가.
# 다이내믹 임포트
CommonJS에서는 가능하나 ES 모듈에서는 불가.
조건부로 모듈을 불러오는 것을 의미
// dynamic.js
const a = false;
if (a) {
require('./func');
}
console.log('성공');
// 출력결과
성공
// dynamic.mjs
const a = false;
if (a) {
import './func.mjs' // SyntaxError: Unexpected string
}
console.log('성공');
위에서 require('./func');는 실행되지 않음. 이처럼 조건에 따라 모듈을 불러옴.
하지만 ES 모듈이 아래처럼은 가능함.
// dynamic.mjs
const a = true;
if (a) {
const m1 = await import('./func.mjs');
console.log(m1);
const m2 = await import('./var.mjs');
console.log(m2);
}
import는 Pormise를 반환하기에 await이나 then을 붙여야 한다.
위 코드에서는 async를 사용하지 않아도 되는데, 그 이유는 최상위 스코프에서는 async 없이도 await 할 수 있다.
# __filename, __dirname
노드에서는 파일 사이에 모듈 관계가 있는 경우가 많아서 혅 ㅐ파일의 경로나 파일명을 알아야하는 경우가 있다.
이 두개의 키워드로 경로에 대한 정보를 제공한다.
console.log(__filename); // C:\User\Desktop\Karrot\NodeJS\var.js
console.log(__dirname); // C:\User\Desktop\Karrot\NodeJS
참고로 ES모듈에서는 위 두개 사용할 수 없다. 대신 import.meta.url로 경로를 가져온다.
// filename.mjs
console.log(import.meta.url); // C:\User\Desktop\Karrot\NodeJS\var.js
console.log('__filename은 에러');
console.log(__filename);
CommonJS 모듈에서 사용했던 require 함수나 module 객체는 따로 선언하지 않았어도 사용할 수 있음. 이게 왜 되냐면 노드에서 기본저긍로 제공하는 내장 객체이기 때문
# global
브라우저의 window같은 전역객체
전역객체라는 특성을 활용파일 간에 간단한 데이터를 공유할 때 사용하기도 합니다.
// globalA.js
module.exports = () => global.message;
// globalB.js
const A = require('./globalA');
global.message = '안녕하세요.';
console.log(A());
// 출력결과
$ node globalB
안녕하세요.
# 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: 1994 }, { name: 'hero', birth: 1988}]);
console.dir(obj, { colors: false, depth: 2 });
console.dir(obj, { colors: true, depth: 1 });
console.time('시간측정');
for (let i = 0; i < 100000; i++) {}
console.timeEnd('시간측정');
function b() {
console.trace('에러 위치 추적');
}
function a() {
b();
}
a();
console.timeEnd('전체시간');
- console.time(레이블): console.timeEnd(레이블)과 대응되어 같은 레이블을 가진 time과 timeEnd 사이의 시간을 측정.
- console.log(내용): 평범한 로그를 콘솔에 표시합니다. console.log(내용, 내용 ...)처럼 내용을 동시 표시도 가능.
- console.error(에러 내용): 에러를 콘솔에 표시
- console.table(배열): 배열의 요소로 객체 리터럴을 넣으면, 객체의 속성들이 테이블 형식으로 표현.
- console.dir(객체, 옵션): 객체를 콘솔에 표시할 때 사용. 첫번째에 객체를 넣고 두번째에 인수를 넣는다. 옵션의 colors를 true로 하면 콘솔에 색이 추가되어 보기가 한결 편해집니다. depth는 객체 안의 객체를 몇 단계까지 보여줄지 결정한다. 기본값은 2.
- console.trace(레이블): 에러가 어디서 발생했는지 추적할 수 있게 한다. 보통은 에러 발생시 위치를 알려주므로 주로 사용하진 않지만, 위치가 나오지 않는다면 사용할만 하다.
# 타이머
- setTimeout(콜백 함수, 밀리초): 주어진 밀리초(1000분의 1초) 이후에 콜백 함수를 실행합니다.
- setInterval(콜백 함수, 밀리초): 주어진 밀리초 이후에 콜백 함수를 실행합니다.
- setImmediate(콜백 함수): 콜백 함수를 즉시 실행.
이 타이머 함수들은 모두 아이디를 반환. 아이디를 사용하면 타이머를 취소할 수도 있음.
- clearTimeout(아이디): setTimeout을 취소.
- clearInterval(아이디): setInterval을 취소.
- clearImmediate(아이디): setImmediate을 취소.
const timeout = setTimeout(() => { // 1.5초에 한번 실행
console.log('1.5초 후 실행');
}, 1500);
const interval = setInterval(() => { // 1초, 2초 실행
console.log('1초마다 실행');
}, 1000);
const timeout2 = setTimeout(() => { // 실행 안돼
console.log('실행되지 않습니다');
}, 3000);
setTimeout(() => { // 2.5초에 두개 취소
clearTimeout(timeout2);
clearInterval(interval);
}, 2500);
const immediate = setImmediate(() => { // 0초에 실행
console.log('즉시 실행');
});
const immediate2 = setImmediate(() => { // 즉시 취소되어서 미실행
console.log('실행되지 않습니다');
});
clearImmediate(immediate2);
이건 하나하나 분석해보기
setImmediate(콜백)과 setTimeout(콜백, 0)
- 공통점: 이벤트 루프 거친 뒤 즉시 실행.
- 차이점: 특수한 경우에 setImmediate는 setTimeout(콜백, 0)보다 먼저 실행된다.
- 예를들면: 파일 시스템 접근, 네트워킹 같은 I/O 작업의 콜백 함수 안에서 타이머를 호출하는 경우. 하지만 항상 먼저 호출되는건 아님.
헷갈리지 않도록 setTimeout(콜백, 0) 사용하지 않는거 권장.
타이머는 콜백 기반 API지만 프로미스 방식을 사용 가능하다.
// timerPromise.mjs
import { setTimeout, setInterval } from 'timers/promises'
await setTimeout(3000);
for await (const startTime of setInterval(1000, Date.now())) {
console.log('1초마다 실행', new Date(startTime));
}
// 출력결과
3초 뒤 실행
1초마다 실행
1초마다 실행
1초마다 실행
1초마다 실행
1초마다 실행
프로미스 기반이므로 then 대신 await을 사용하기 위해 ES 모듈 사용했으며, timers/promises라는 모듈에서 setTimeout과 setInterval을 새롭게 제공
# Process
현재 실행되고 있는 노드 프로세스에 대한 정보를 담고 있음.
# Process.env
데이터베이스에 중요한 정보 그대로 저장해서 털리면 큰 문제 되니까 env로 저장하자.
다만 process.env에 시크릿 아이디와 시크릿 코드를 직접 넣으면 절대 안된다. 넣는 방법은 운영체제마다 차이가 있다.
const secretId = process.env.SECRET_ID;
const secretCode = process.env.SECRET_CODE;
# Process.nextTick(콜백)
이벤트 루프가 다른 콜백 함수들보다 nextTick의 콜백 함수를 우선적으로 처리.
Promise의 resolve도 다른 콜백들보다 우선시 된다.
그래서 process.nextTick과 Promise를 마이크로태스크라고 따로 부름
setImmediate(() => {
console.log('immediate');
});
process.nextTick(() => {
console.log('nextTick');
});
setTimeout(() => {
console.log('timeout');
}, 0);
Promise.resolve().then(() => console.log('promise'));
// 출력결과
nextTick
promise
timeout
immediate
마이크로태스크도 재귀호출이 된다.
# Process.exit
실행중인 노드를 종료. 서버환경에서 이 함수를 사용하면 서버가 멈추므로 특수한 상황을 제외하고 서버에서 잘 사용하지 않음.
# 기타 내장 객체
- URL, URLSearchParams
- AbortController, FormData, fetch, Headers, Request, Response, Event, EventTarget: 브라우저에서 사용하던 API가 노드에도 동일하게 생성
- TextDecoder: Buffer를 문자열로 바꿈.
- TextEncoder: 문자열을 Buffer로 바꿈.
- WebAssembly: 웹어셈블리 처리를 담당.
# 노드 내장 모듈 사용
모두 공식 문서에 잘 나와 있으나 중요한 것들만 정리.
노드의 모듈은 노드 버전마다 차이가 있다.
클라이언트에 요청한 주소에 대한 정보를 가져올 수 있고, os 정보도 접근할 수 있다.
# os
두개로 불러올 수 있음. require('os'); 또는 require('node.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('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());
console.log('os.cpus().length:', os.cpus().length);
console.log('메모리 정보-----------------------------------');
console.log('os.freemem():', os.freemem());
console.log('os.totalmem():', os.totalmem());
운영체제별로 다른 서비스를 제공하고 싶을 때나 내부자원에 빈번하게 접근하는 경우에 사용합니다.
# path
폴더와 파일의 경로를 쉽게 조작하도록 도와주는 모듈. path별로 os별로 경로 구분자가 다르기 때문. 크게 윈도 타입과 POSIX타입으로 구분. POSIX는 유닉스 기반의 운영체제로 리눅스랑 맥이 여기에 속함.
- 윈도: C:\User\Karrot ... \로 구분
- POSIX: /home/Karrot ... /로 구분
const path = require('path');
const string = __filename;
console.log('path.sep:', path.sep);
console.log('path.delimiter:', path.delimiter);
console.log('------------------------------');
console.log('path.dirname():', path.dirname(string));
console.log('path.extname():', path.extname(string));
console.log('path.basename():', path.basename(string));
console.log('path.basename - extname:', path.basename(string, path.extname(string)));
console.log('------------------------------');
console.log('path.parse()', path.parse(string));
console.log('path.format():', path.format({
dir: 'C:\\users\\zerocho',
name: 'path',
ext: '.js',
}));
console.log('path.normalize():', path.normalize('C://users\\\\zerocho\\\path.js'));
console.log('------------------------------');
console.log('path.isAbsolute(C:\\):', path.isAbsolute('C:\\'));
console.log('path.isAbsolute(./home):', path.isAbsolute('./home'));
console.log('------------------------------');
console.log('path.relative():', path.relative('C:\\users\\zerocho\\path.js', 'C:\\'));
console.log('path.join():', path.join(__dirname, '..', '..', '/users', '.', '/zerocho'));
console.log('path.resolve():', path.resolve(__dirname, '..', 'users', '.', '/zerocho'));
join과 resolve는 비슷하지만 조금 다름.
join('/a', '/b', 'c'); 결과: /a/b/c/ -> 상대경로로 처리
resolve('/a', '/b', 'c'); 결과 /b/c -> 절대경로로 인식해서 앞의 경로를 무시
# url
인터넷 주소를 쉽게 조작하도록 도와주는 모듈
WHATWG(웹 표준을 정하는 단체의 이름)
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));
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.keys():', myURL.searchParams.keys());
console.log('searchParams.values():', myURL.searchParams.values());
myURL.searchParams.append('filter', 'es3');
myURL.searchParams.append('filter', 'es5');
console.log(myURL.searchParams.getAll('filter'));
myURL.searchParams.set('filter', 'es6');
console.log(myURL.searchParams.getAll('filter'));
myURL.searchParams.delete('filter');
console.log(myURL.searchParams.getAll('filter'));
console.log('searchParams.toString():', myURL.searchParams.toString());
myURL.search = myURL.searchParams.toString();
//출력결과
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' ]
[]
# dns
DNS를 다룰 때 사용하는 모듈
주로 도메이능ㄹ 통해 IP나 기타 DNS 정보를 얻고자 할 떄 사용
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);
lookup이나 resolve(도메인)으로 얻을 수 있음 ip주소 간단하게.
MX, CNAME 등은 레코드라고 부르는데, resolve(도메인, 레코드 이름)으로 조회하면 된다.
# crypto
다양한 방식으로 암호화하는 모듈. 몇가지 메소드는 익혀두면 실제서비스에도 아주 유용. 고객의 비밀번호는 반드시 암호화 그렇지 않으면 해킹당하면 진짜 대사고 ^_^
- 단방향 암호화: 한번 암호화하면 복호화할 수 없음. 해시 함수라고도 불림.
- 고객의 비밀번호는 복호화 할 필요가 없음. 암호화해서 DB에 저장하고 로그인할 때마다 입력받은 비밀번호를 같으 암호화 알고리즘으로 암호화한 후 DB에서 비교하면 돼. 원래 비번은 암호화해서 아무데도 저장않고 비교함.
- 양방향 암호화: 대칭형 암호화. 문자열 복호화도 가능.
# util
각종 편의 기능을 모아둔 모듈. API가 지속적으로 추가되고 있으며, deprecated되어 사라지는 것도 존재.
자주 사용하는 두가지 소개
const util = require('util');
const crypto = require('crypto');
const dontUseMe = util.deprecate((x, y) => {
console.log(x + y);
}, 'dontUseMe 함수는 deprecated되었으니 더 이상 사용하지 마세요!');
dontUseMe(1, 2);
const randomBytesPromise = util.promisify(crypto.randomBytes);
randomBytesPromise(64)
.then((buf) => {
console.log(buf.toString('base64'));
})
.catch((error) => {
console.error(error);
});
- deprecated 처리되었음을 알림,
- 콜백 패턴을 프로마이즈 패턴으로 바꿈. 이렇게 바꿔두면 async/await도 사용할 수 있어서 좋음.
# worker_threads
노드에서 멀티 스레드 방식으로 작업하는 방법임 해당 모듈로 가능
const {
Worker, isMainThread, parentPort,
} = require('worker_threads');
if (isMainThread) { // 부모일 때
const worker = new Worker(__filename); // ✅ 1. 워커 스레드 생성
worker.on('message', message => console.log('from worker', message)); // 🟡 리스너 등록
worker.on('exit', () => console.log('worker exit')); ✅ 6. 종료 리스너 받아서 처리
worker.postMessage('ping'); // ✅ 2. 메시지 보냄
} else { // 워커일 때
parentPort.on('message', (value) => { // ✅ 3. 메시지 받음
console.log('from parent', value);
parentPort.postMessage('pong'); // ✅ 4. 부모로 메시지 보냄
parentPort.close(); ✅ 4. 부모 종료 실행
});
}
// 실행결과
from parent ping
from worker pong
worker exit
// 실행정리
- 부모에서는 워커스레드 생성 후 woker.postMessage로 워커에 데이터를 보냄.
- 워커는
parentPort.on('message') 이벤트 리스너로부터 메시지를 받고
parentPort.postMessage로 부모에게 메시지를 보냄.
- 부모는 worker.on('message')로 메시지 받음
참고로 한번만 받으려면 once('message')사용
- 워커에서 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); // ✅ 현재 두개의 스레드 돌아가고 있으며 각각 스레드에서 100씩 더함
}
// 출력결과
from worker 101
from worker 102
job done
좀 더 실질적인 소수의 개수를 구하는 작업을 워커 스레드 통해 해보자.
const min = 2;
const max = 10000000;
const 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;
}
}
console.time('prime');
findPrimes(min, max);
console.timeEnd('prime');
console.log(primes.length);
// 결과
prime: 8.475s
664579
pc 성능에 따라 다르지만 시간 상당히 소요
이번에는 멀티스레딩으로 해결해보겠음.
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.ceil((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: 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);
}
// 결과
prime: 1.752s
664579
스레드에 일을 나눠주어서 따로 처리하게 함. 멀티 스레딩을 할때는 일을 나눠서 처리하도록 하는게 제일 어려워
8배 빨라지는거 아님 스레드 생성하고 통신하는데도 비용이 들어감
# child_process
노드에서 다른 프로그램을 실행하고 싶거나 명령어를 수행하고 싶을 때 사용하는 모듈.
이 모듈을 통해 다른 언어의 코드를 실행하고 결괏값을 받을 수 있음.
const exec = require('child_process').exec;
var process = exec('dir');
process.stdout.on('data', function(data) {
console.log(data.toString());
}); // 실행 결과
process.stderr.on('data', function(data) {
console.error(data.toString());
}); // 실행 에러
리눅스 MyShell구현.(여담이지만 운영체제 과제가 마이 쉘 구현이였음 그때 도대체 어떻게 한걸까;)
exec('ls') 입력해보기
# 파일 시스템 접근
fs 모듈은 파일 시스템에 접근하는 모듈. 즉 파일 CRUD가능. 웹 브라우저는 js 사용할 떄 일부를 제외하고 파일 시스템 금지되어 있어서 js 모듈이 낯설거임.
// readme.txt
저를 읽어주세요.
// readFile.js
const fs = require('fs');
fs.readFile('./readme.txt', (err, data) => {
if (err) {
throw err;
}
console.log(data);
console.log(data.toString());
});
// 출력결과
<Buffer ec a0 80 eb a5 ...> // 콘솔 데이터
저를 읽어주세요. // 콘솔 스트링
fs 모듈 불러온 뒤 파일 경로를 지정. 파일의 경로가 현재 파일 기준이 아니라 node 명령어를 실행하는 콘솔기준이라는 점에 유의
지금은 크게 상관 없으나 폴더 내부에 들어있는 파일을 실행할 때 경로 문제가 발생할 수 있음.
파일을 읽은 후에 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);
});
fs모듈에서 promise 속성을 불러오면 프로미스 기반의 fs 모듈을 사용할 수 있게 됩니다.
const fs = require('fs');
fs.writeFile('./writeme.txt', '글이 입력됩니다', (err) => {
if (err) {
throw err;
}
fs.readFile('./writeme.txt', (err, data) => {
if (err) {
throw err;
}
console.log(data.toString());
});
});
# 동기 메서드와 비동기 메서드
setTimeout과 같은 타이머와 process.nextTick 외에도 노드는 대부분의 메서드를 비동기 방식으로 처리.
하지만 몇몇 메서드는 동기 방식으로도 사용 가능. 특히 fs 모듈이 그러한 메서드를 많이 갖고 있어. 또한 언제 어떤 메서드를 사용해야 하는지 알아보자.
파일 하나를 여러번 읽어보자.
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('끝');
// 출력결과
시작
끝
2번 저를 여러번 읽어보세요.
3번 저를 여러번 읽어보세요.
1번 저를 여러번 읽어보세요.
시작과 끝을 제외하고는 순서가 다를 수 있어.
비동기 메서들은 백그라운드에 해당 파일을 읽으라고만 요청하고 바로 다음 작업으로 넘어감.
const fs = require('fs');
console.log('시작');
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());
console.log('끝');
// 출력결과
시작
1번 저를 읽어주세요.
2번 저를 읽어주세요.
3번 저를 읽어주세요.
끝
이건 뭐 너무나도 당연하다.
비동기 방식으로 하되 순서를 유지하고 싶다면?
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('끝');
});
});
});
콜백 지옥이 펼쳐지나 순서가 어긋나는 일은 없다.
콜백 지옥은 Promise나 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);
});
# 버퍼와 스트림 이해하기
파일을 쓰는 방식에는 크게 두가지가 있음.
- 버퍼를 이용
- 스트림을 이용
버퍼링은 영상을 재생할 수 있을 때까지 데이터를 모으는 동작
스트리밍은 방송인의 컴퓨터에서 시청자의 컴퓨터로 영상 데이터를 조금씩 전송하는 동작
스트리밍 과정에서 버퍼링 할 수도 있음.
전송이 너무 느리면 화면을 내보내기까지 최소한의 데이터를 모아야 하고
영상 데이터가 재생속도보다 너무 빨리 전송되어도 미리 전송받은 데이터를 저장할 공간이 필요하다.
노드에서 버퍼를 직접 다룰 수 있는 클래스가 있다.
const buffer = Buffer.from('저를 버퍼로 바꿔보세요');
console.log('from():', buffer);
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);
// 출력결과
from(): <Buffer ec a0 ...>
legnth: 32
toString(): 저를 버퍼로 바꿔보세요.
concat(): 띄엄 띄엄 띄어쓰기
alloc(): <Buffer 00 ..>
Buffer 객체는 여러가지 메서드를 제공
- from(문자열): 문자열을 버퍼로 바꿀 수 있음. length 속성은 버퍼의 크기를 알리는 바이트 단위
- toString(버퍼): 다시 문자열로 바꿔준다. 이때 인코딩도 가능하다.
- concat(배열): 배열을 하나로 합친다.
- alloc(바이트): 빈 버퍼를 생성 바이트를 인수로 넣으면 해당 크기의 버퍼가 생성. 바이트를 인수로 넣으면 해당 크기의 버퍼가 생성된다.
readFile 방식의 버퍼가 편리하기는 하지만 문제점도 있다. 만약 용량이 100MB인 파일이 있으면 읽을 때 메모리에 100MB의 버퍼를 만들어야 한다. 이 작업을 동시에 열개만해도 1GB에 달하는 메모리가 사용된다. 이 작업을 동시에 10개만 해도 .. 터짐.
게다라 읽기, 압축, 파일쓰기 등의 조작을 연달아 할 때 매번 전체 용량을 버퍼로 처리해야 하므로 매번 전체 용량의 버퍼를 다 써야함.
이걸 해결하고자 버퍼의 크기를 작게 만들고 나눠 보내는 방식이 등장 이것이 바로 스트림
파일 읽는 메서드로는 createReadStream이 있음
// readme3.txt
저는 조금씩 나눠서 전달됩니다. 나눠진 조각을 chunk라고 부릅니다.
// createReadStream.js
const fs = require('fs');
const readStream = fs.createReadStream('./readme3.txt', { highWaterMark: 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);
});
// 출력결과
<Buffer ec a0 80 ...> 16
<Buffer ec a0 80 ...> 16
<Buffer ec a0 80 ...> 16
<Buffer ec a0 80 ...> 16
<Buffer ec a0 80 ...> 16
<Buffer ec a0 80 ...> 16
저는 조금씩 나눠서 전달됩니다. 나눠진 조각을 chunk라고 부릅니다.
먼저 createReadStream으로 읽기 스트림을 만든다. 첫번째 인수로 읽을 파일 경로를 넣는다.
두번쨰 인수는 옵션 객체인데, hughWaterMark라는 옵션이 버퍼의 크기를 정할 수 있는 옵션이다.
readStream은 보통 data, error, end를 사용한다. 각 스트림에 이벤트 리스너를 붙여서 사용한다.
이번에는 써보자
const fs = require('fs');
const writeStream = fs.createWriteStream('./writeme2.txt');
writeStream.on('finish', () => {
console.log('파일 쓰기 완료');
});
writeStream.write('이 글을 씁니다.\n');
writeStream.write('한 번 더 씁니다.');
writeStream.end();
쓰는것도 닮았다
이번에는 읽고 그 스트림을 전달받아 다른데에 써보자.
뭐 파일 복사 기능인데 스트림끼리 연결하는 것을 '파이핑하다'라고 표현한다. 액체가 관을 따라 흐른다고 해서 지어진 이름이다.
const fs = require('fs');
const readStream = fs.createReadStream('readme4.txt');
const writeStream = fs.createWriteStream('writeme3.txt');
readStream.pipe(writeStream);
결과는 뭐 알겠지?
다음 코드는 파일을 읽은 후 gzip 방식으로 압축하는 코드
const zlib = require('zlib');
const fs = require('fs');
const readStream = fs.createReadStream('./readme4.txt');
const zlibStream = zlib.createGzip();
const writeStream = fs.createWriteStream('./readme4.txt.gz');
readStream.pipe(zlibStream).pipe(writeStream);
스트림 모듈의 pipeline 메서드를 사용해 여러개의 파이프를 연결하는 방법도 있다.
import { pipeline } from 'stream/promises';
import zlib from 'zlib';
import fs from 'fs';
await pipeline(
fs.createReadStream('./readme4.txt'),
zlib.createGZip(),
fs.createWriteStream('./readme4.txt'),
)
파이프라인 사용하면 좋은 점이 중간에 AbortController를 사용해 원할 때 파이프를 중단할 수 있음.
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('./readme4.txt'),
zlib.createGZip(),
fs.createWriteStream('./readme4.txt'),
{ signal },
);
원하는 시점에 abort 호출하면 중단된다.
아래는 1기가 파일 작성하는 코드
const fs = require('fs');
const file = fs.createWriteStream('./big.txt');
for (let i =0;, i <= 100000000; i++) {
file.write('엄청나게 큰 파일 업로드');
}
file.end()
big.txt를 big2.txt로 복사해보자.
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);
// 출력결과
before: 18137088 // 약 18MB
buffer: 1018133952 // 1GB이상
메모리에 파일을 모두 올려둔 후 writeFileSync를 수행했음.
이제는 스트림을 사용해 파일 big3.txt로 복사해보자.
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);
});
// 출력결과
before: 18087936
stream: 62472192
# 기타 fs 메서드 알아보기
지금까지는 단순 파일이었다면 이제는 폴더도 컨트롤 해보자
const fs = require('fs').promises;
const constants = require('fs').constants;
// F_OK, W_OD, R_OK 파일 존재여부, 읽기 권한 여부, 쓰기 권한 여부
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'); // 파일 가져오기, 파일 없다면 새로 만듦
// w쓰기 r읽기 a추가
})
.then((fd) => {
console.log('빈 파일 만들기 성공', fd);
return fs.rename('./folder/file.js', './folder/newfile.js'); // 파일 이름 바꿈
// 기존 파일의 위치와 새로운 파일의 위치 적기, 꼭 같은 폴더를 지정할 필요 없어서 잘라내기 같은 기능 가능
})
.then(() => {
console.log('이름 바꾸기 성공');
})
.catch((err) => {
console.error(err);
});
// 출력결과
$node fsCreate
폴더 없음
폴더 만들기 성공
빈 파일 만들기 성공
이름 바꾸기 성공
$node fsCreate
이미 폴더 있음
이번에는 내용 확인이랑 삭제같은 메소드 알아보기
const fs = require('fs');
fs.readdir('./folder', (err, dir) => {
if (err) {
throw err;
}
console.log('폴더 내용 확인', dir);
fs.unlink('./folder/newfile.js', (err) => {
if (err) {
throw err;
}
console.log('파일 삭제 성공');
fs.rmdir('./folder', (err) => {
if (err) {
throw err;
}
console.log('폴더 삭제 성공');
});
});
});
// 출력결과
폴더 내용 확인[ 'newfile.js' ]
파일 삭제 성공
폴더 삭제 성공
readdir은 폴더 안의 내용물을 확인할 수 있다. 배열 안에 내부 파일과 폴더명이 나온다.
없어진 파일 한번 더 지우면 ENOENT에러 발생
createReadStream과 createWriteStream은 pipe하지 않아도 파일을 복사할 수 있다.
const fs = require('fs');
fs.copyFile('readme4.txt', 'writeme4.txt')
.then(() => {
console.log('복사 완료');
})
.catch((error) => {
console.error(error);
});
// 출력결과
복사완료
read4me의 내용이 writeme4로 완전히 복사된다.
마지막으로 파일/폴더 변경 사항을 감시할 수 있는 watch 메서드에 대해서 알아보자
const fs = require('fs');
fs.watch('./target.txt', (eventType, filename) => {
console.log(eventType, filename);
});
//출력결과
// 내용물 수정 후
change target.txt
change target.txt
// 파일명 변경 또는 파일 삭제 후
rename target.txt
change이벤트가 2번 발생하니 실무에서 주의
rename 이후에는 이름이 달라져서 더이상 watch로 트래킹할 수 없으니 주의
# 스레드 풀 알아보기
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: 1548
2: 1583
1: 1590
3: 1695
6: 3326
5: 3463
7: 3659
8: 3682
스레드 풀이 동시에 처리해서 뭐가 먼저 실행될지 모름
다만 하나의 규칙을 발견할 수는 있는데 5-8그룹이 묶여있고 1-4그룹도 묶여있음. 그리고 시간도 더 소요된다.
왜냐하면 기본적인 스레드의 갯수가 4개이기 때문이다. 처음 4개의 작업이 동시에 실행되고, 그것들이 종료되면 4개 더 실행된다.
코어가 만약 PC에 더 많다만 다른 결과가 가능하다.
# 이벤트 알아보기
스트림을 배울때 우리는 on('data', 콜백), end('data', 콜백)을 사용했다. 바로 data라는 이벤트가 end라는 이벤트가 발생할 때 콜백 함수를 호출하도록 등록한 것
createReadStream 같은 경우 내부적으로 알아서 data와 end 이벤트를 호출하지만 우리가 직접 이벤트를 만들수도 있음.
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'));
이벤트 만드려면 myEvent 객체를 먼저 만들어야 함.
그냥 코드 읽다보면 감이 온다.
자 지금까지 배운 것만으로도 이벤트 처리하기엔 충분하다
이제는 에러를 처리하는 방법을 알아보자
# 예외 처리하기
노드에서는 예외처리 중요함. 예외 처리 못하면 노드 프로세스를 멈추게 할수도 있음.
노드는 스레드 하나 뿐이라 그거 잘 관리해야함. 잘못하면 전체 멈춤
에러 로그가 기록되더라도 프로그램을 돌아가야 함.
문법상의 에러는 없다고 가정하겠음. 실제 배포에서 그거 있는건 큰일남. 좋은 에디터로 문법 검사 툴을 사용해보자.
에러가 발생할 부분을 try - catch로 감싼다.
setInterval(() => {
console.log('시작');
try {
throw new Error('서버를 고장내주마!');
} catch (err) {
console.error(err);
}
}, 1000);
// 출력결과
시작
Error: 서버를 고장내주마!
...
시작
Error: 서버를 고장내주마!
...
무한반복
setInterval을 한 이유는 서버가 멈췄는지 여부를 체크하기 위해
프로세스 관련 에러로 멈추면 해당 메서드도 멈출 것임 해당 코드는 에러를 throw에서 강제로 던지고 있음.
이번에는 노드 자체에서 에러 잡아주는 코드 알아보자.
const fs = require('fs');
setInterval(() => {
fs.unlink('./abcdefg.js', (err) => {
if (err) {
console.error(err);
}
});
}, 1000);
이번에는 unlink로 존재하지 않는 파일을 지우고 있다. 에러가 발생하지만 다행히 노드 내장 모듈의 에러는 실행중인 프로세스를 멈추지 않는다. 에러로그를 기록해두고 나중에 원인을 찾아 숮어하면 된다.
throw하는 경우에는 반드시 try/catch문으로 에러를 잡아야 한다.
노드 16부터는 promise의 에러는 반드시 캐치해야 한다. 그렇지 않으면 종료된다.
process.on('uncaughtException', (err) => {
console.error('예기치 못한 에러', err);
});
setInterval(() => {
throw new Error('서버를 고장내주마!');
}, 1000);
setTimeout(() => {
console.log('실행됩니다');
}, 2000);
// 출력결과
예기치 못한 Error: 서버를 고장내주마!
...
실행합니다.
예기치 못한 Error: 서버를 고장내주마!
예기치 못한 Error: 서버를 고장내주마!
// 계속 반복
위 코드는 정말 예측 불가능한 에러를 처리하는 방법을 알아보는 예제이다.
process 객체에 uncauthtException 이벤트 리스너를 달았다. 처리하지 못한 에러가 발생하면 이벤트 리스너가 실행되고 프로세스가 유지된다. 실행 후 1초만에 setInterval에서 에러가 발생해 프로세스가 멈추기 때문이다. 하지만 uncauhtException 이벤트 리스너가 연결되어 있으므로 프로세스가 멈추지 않는다.
uncaughtException사용하면 에러가 발생했어도 계속 싱핼된다.
하지만 이 코드는 최후의 수단으로 사용하라고 권장한다.
uncaughtException 이벤트 발생 후 다음 동작이 제대로 동작하는지를 보증하지 않는다.
따라서 얘는 에러 내용을 기록하는 정도로 사용한 후 process.exit()으로 종료하는게 좋음.
서버는 에러와의 싸움.
에러가 발생할 때 에러를 철저히 로깅하는 습관을 갖자
'Node.js' 카테고리의 다른 글
[Node.js] #6 익스프레스 웹 서버 만들기 (0) | 2023.07.23 |
---|---|
[Node.js] #5 패키지 매니저 (0) | 2023.07.23 |
[Node.js] #4 http 모듈로 서버 만들기 (0) | 2023.07.22 |
[Node.js] #2 알아둬야 할 자바스크립트 (0) | 2023.07.17 |
[Node.js] #1 핵심개념 이해하기 (0) | 2023.07.16 |