Node.js 교과서 : 3. 노드 기본 기능 익히기(20~32)

전지연·2025년 10월 15일

GDGoC_2025

목록 보기
3/16
post-thumbnail

REPL과 js 파일 실행하기

REFL

(Read, Evaluate, Print, Loop)

  • 자바스크립트는 스크립트 언어 - 즉석에서 코드 실행 가능
  • REPL이라는 콘솔 제공
  • 스크립트 창에 코드를 입력하면 결괏값을 바로 출력
  • 간단한 코드 테스트하기 적합

js 파일 실행

  • 편집기에 코드를 입력하고 실행
  • 코드를 입력한 후 터미널 창에 cmd를 켜서 코드를 실행한다.

CommonJS 모듈 시스템

  • 모듈 : 특정한 기능을 하는 함수나 변수들의 집합
  • 모듈로 만들면 여러 프로그램에서 재사용 가능
  • module.exports 와 require()를 사용해서 만든 모듈을 주고받을 수 있다.
// var.js
const odd = '홀수';
const even = '짝수'

module.exports = {
    odd,
    even,
};
// func.js
const value = require('./var');  //require : node에서 제공
console.log(value);   // 출력 : { odd: '홀수', even: '짝수' }
  • 중괄호{}를 사용하여 객체 통채로 받지 않고 변수에 할당할 수 있고, function함수 통채로 변수에 할당할 수 있다.
func.jsindex.js

  • import와 export를 만들 수는 있음
// var.js
const odd = '홀수';
const even = '짝수'

export default { odd, even };  // export default 사용
//func.js
import { odd, even } from './var';  // import, from 사용

function checkStringOddOrEven(number) {
    if (number % 2) {
        return odd;
    } else {
        return even;
    }
}

export default checkStringOddOrEven;

exports, this, require, 순환참조

exports

  • module 생략
  • 이 코드에서 export default 없이도 다음과 같이 코드를 작성할 수 있다.
const odd = '홀수';
const even = '짝수'

exports.odd = odd;
exports.even = even;
  • module.export는 export.--과 기본적으로 같고, 객체를 대입한 것과 같음
  • 함수를 넣으면 참조값이 달라짐 -> module.export와 export.--는 달라짐
  • export.--를 작성하고 module.exports에 새로운 객체를 생성하면 참조 관계가 달라짐
  • 한 가지는 module.exports 사용. 두 가지 이상은 exports.-- 사용
  • exports.--와 module.exports는 같이 쓸 수 없다(참조값이 달라지기 때문)

this

  • function 바깥의 this : 지역 변수
  • function 안의 this : 전역 변수
console.log(this);

function a() {
    console.log(this === global);
}

a();

이 코드를 실행하면 다음과 같이 출력된다.

  • 맨 위의 this는 빈 객체로 출력되고, a() 안의 this는 true로 처리됨
  • 맨 위의 this를 다음과 같이 고치면 둘 다 true가 된다.
console.log(this === module.exports);  // 고침

function a() {
    console.log(this === global);
}

a();
  • 전역 this는 module.exports와 같다.

  • 변수를 export할 때 this를 사용할 수도 있다(잘 쓰이진 않음)

const odd = '홀수';
const even = '짝수';

this.odd = odd;
this.even = even;
  • 변수 할당 없이 require를 사용할 수도 있다. 이 경우에는 파일에서 외부 import파일을 실행만 하고 변수를 가져오지 않을 때 사용한다.
require('./var');

이 방식으로 var파일을 가져온 후 console.log로 실행하면 var파일 안에 어떤 것이 내장되어 있는지 확인 가능하다
(cache는 최초로 var파일을 불러온 값을 저장한 후, var를 다시 읽을 때 새로 파일을 불러올 필요 없이 cache에 저장된 값을 불러온다)
(exports는 module.exports이다

순환참조

파일 두개가 서로를 참조하는 현상
-> 노드가 알아서 빈 객체로 바꿔줌

ECMAScript 모듈

: 공식적인 자바스크립트 모듈 형식(ES모듈)

  • 아직까지는 CommonJS 모듈을 많이 쓰지만, ES모듈이 표준으로 정해지면서 점점 ES모듈 사용 비율이 늘어남
  • 파일 형식으로 mjs를 사용

사용 형식은 다음과 같다.

  • mjs(ECMAScript)

  • cjs(CommonJS)

  • mjs에서는 cjs와 다르게 변수 선언 바로 앞에 export를 작성해 줄 수 있다

  • import는 다음과 같다.

import { odd, even } from './var.mjs';
  • 모듈을 export 할 때는 다음과 같다.
function checkOddOrEven(num) {
	if (num % 2) {
    	return odd;
    }
    return even;
}

export default checkOddOrEven;   // 위에서 선언한 모듈을 export한다
  • 모듈을 import할 때는 변수처럼 import하면 된다.
import checkNumber from './func.mjs';

...

console.log(checkNumber(10));    // 사용은 일반 function처럼 사용하면 된다

  • named export : 이름이 정확히 정해진 export
    ex) var.mjs에서 'export const'를 사용하여 export한 odd와 even은 func에서도 odd, even이라 부름
// var.mjs
export const odd = 'MJS 홀수입니다';
export const even = 'MJS 짝수입니다';
// func.mjs
import { odd, even } from './var.mjs';
// 이 파일에서 odd, even으로 이름이 정해짐
  • default export : export된 파일에서 이름을 자유롭게 붙일 수 있음
// var.cjs
function checkOddOrEven(num) {
	if (num % 2) {
    	return odd;
    }
    return even;
}

export default checkOddOrEven;
// func.cjs
import checkNumber from './func.mjs';

...

console.log(checkNumber(10));
// var.cjs에서 checkOddOrEven라 지었어도 func.cjs에서 checkNumber이라 부를수 있음
  • 콘솔에서 실행시킬 때는 파일 형식까지 작성해야 한다

    node index.mjs


  • mjs 확장자 대신 js확장자를 사용 가능
    -> package.json에 type: "module" 속성 추가

  • ECMAScript 모듈은 CommonJS 모듈과 다르게 js, mjs 확장자를 생략할 수 x

  • 폴더 내부의 index.js도 생략x

  • top level await
    CommonJS에서는 asycn로 감싸줘야 사용가능
    MJS파일에서는 await바로 사용가능
CommonJS MJS

다이내믹 임포트

(CommonJS모듈은 가능, ES모듈은 불가)
: if문 안에 require를 넣는것

  • CommonJS모듈은 if문 안에 require가 실행된다.
// dynamic.js
const a = false;
if(a) {
	require('./func');
}
console.log('성공');

실행하면 콘솔에 '성공'이라고 출력된다.

  • ES모듈의 경우에는 실행되지 않는다.
// dynamic.mjs
if(a) {
	import './func.mjs';   // if문 안에서 import는 실행되지 않음
}
  • ES모듈에서 다이내믹 임포트를 하려면 import 함수를 사용해야 한다.
// dynamic.mjs
if(a) {
	const m1 = await import ('./func.mjs');
  // import는 Promise -> await 사용
  // 여기서 top level await까지 적용
}

filename, dirname

  • __filename : 현재 파일명
  • __dirname : 현재 파일 경로

ES모듈에서는 둘 다 사용불가
대신 import.meta.url로 경로를 가져올 수 있다.

// filename.mjs
console.log(import.meta.url);   // 경로 가져옴
console.log(__filename);   // 에러

require, module 객체는 선언없이 사용 가능한 내장 객체

global, console, 타이머

-> 노드 내장 객체

global

: 모든 파일에서 접근 가능한 전역 객체

  • window 처럼 생략 가능
  • global 속성에 값을 대입하면 다른 파일에서도 사용 가능하다.
// globalA.js
module.exports = () => global.message;
// globalB.js
const A = require('./globalA');

global.message = '안녕하세요';
console.log(A());   // globalB를 실행하면 콘솔에 '안녕하세요' 출력
  • 파일 많아지면 어디서 global 넣었는지 헷갈림 -> 지양

console 객체

: 터미널 = 콘솔

  • 브라우저의 console 객체와 매우 유사
  • console.log('--') : 콘솔창에 메시지 출력
  • console.dir({ -- : '--' }) : 객체 로깅
  • console.time('--') , console.timeEnd('--') : time과 timeEnd 사이에 있는 코드가 실행되는 시간을 알 수 있음
  • console.trace : 호출스택 로깅
  • console.table([{ -- : '--'}, { -- : '--' }]);
    테이블처럼 보여짐

타이머 메서드

: set 메서드에 clear 메서드가 대응됨

비동기코드

  • setTimeout(콜백 함수, 밀리초0) : 밀리초 이후에 콜백 함수 실행
  • setInterval(콜백 함수, 밀리초) : (변수에 담아서 사용) 밀리초 마다 콜백 함수 실행
  • setImmediate(콜백 함수) : 콜백 함수 즉시 실행, setTimeout(--, 0)과 비슷하지만 실행 순서 다름
  • clearTimeout(변수) : setTimeout 취소
  • clearInterval(변수) : setInterval 취소
const timeout = setTimeout(() => {
	console.log('1.5초 후 실행');
}, 1500);
// 이하 setInterval, setImmediate 모두 같은 방식으로 작성

setTimeout(() => {
  clearTimeout(timeout);
}, 2500);

setImmediate는 즉시 실행이지만 호출 스택 -> 백그라운드 -> 이벤트 큐 -> 호출 스택으로 이동 후 실행되기 때문에 바로 취소하면 실행되지 않는다.

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

clearImmediate(immediate);   // setImmediate 취소됨

실행 순서 :

process

: 현재 실행중인 노드 프로세스에 대한 정보를 담고 있음

이 코드를 콘솔창에 입력해서 실행한다.

process.--

명령어

process.env

: 시스템 환경 변수들이 들어있는 객체

  • 비밀키(DB비번, 앱 키 등)를 보관하는 용도로도 쓰임
const secretId = process.env.SECRET_ID;
const secretCode = process.env.SECRET_CODE;
  • 일부 환경 변수는 노드 실행 시 영향을 미침
    ex) NODE_OPTIONS(노드 실행 옵션), UV)THREADPOOL_SIZE(스레드풀 개수)
NODE_OPTIONS=--max-old-space-size=8192   //메모리 크기 지정하는 옵션
UV_THREADPOOL_SIZE=8   // 동시에 돌릴 스레드풀 개수 지정(디폴트4)

process.nextTick(콜백)

: 다른 콜백 함수들보다 우선적으로 처리됨

process.exit(코드)

: 현재의 프로세스를 멈춤

  • process.exit(0) : 정상 종료
  • process.exit(1) : 에러 있을때 에러 있음을 알리고 종료

os와 path

os

: 운영체제의 정보

// os.js
const os = require('os');

console.log(os.cpus());

코드를 실행시키면 CPU정보가 출력된다.

os 모듈 메서드

path

const path = require('path');

path.join(__dirname, '..', 'var.js');
path.resolve(__dirname, '..', './var.js');
// 둘 다 파일 경로를 표시(window와 linux간 파일 경로 표기가 다르기 때문에 사용)

// './var.js'에서 .은 파일 경로를 축약한 것
// .이 없으면 절대 경로로 취급된다
  • join : 맨 뒤에 절대 경로를 넣으면 절대 경로가 무시됨
  • resolve : 맨 뒤에 절대 경로를 넣으면 앞의 것들(__dirname, '..')이 무시됨

path 모듈 메서드

노드 내장 모듈(url과 dns) + searchParams

URL처리

WHATWG와 노드의 주소 체계

  • url.js

    URL은 노드 내장 객체이기도 해서 위의 두 줄은 생략 가능하다

콘솔에는 다음과 같이 출력된다.

searchParams

: search를 객체화

  • getAll : 같은 키가 여러 값을 갖는 것
    ex) A=B&A=D&E=F
    { A: ['B', 'D'], E: 'F' }
  • 콘솔 결과는 다음과 같다.

DNS

주소

  • A : ipv4 주소
  • MX : 메일 서버
  • CNAME : www 있는것과 없는 것 서로 연결(cname = 별칭)
  • ANY
  • 도메인 활용 설정(MX, CNAME 등) : 레코드 -> dns.resolve로 조회
  • dns.lookup : ip주소

  • 기타

crypto와 util

단방향 암호화(crypto)

멀티스레드로 돌아감
  • 단방향 암호화 대표 : 해시 기법(비밀번호에 많이 사용)

    같은 비밀번호 -> 같은 해시
    해시 -> 비밀번호는 매우 어려움

  • createHash(알고리즘) : 사용할 해시 알고리즘 대입

  • update(문자열) : 변환할 문자열을 넣어줌

  • digest(인코딩) : 인코딩할 알고리즘 넣어줌(base64, hex, latin1 등)

pbkdf2

기존 암호화 알고리즘 위협 -> 새로운 알고리즘 사용

  • 현재는 pbkdf2나, bcrypt, scrypt 알고리즘으로 암호화
  • Node는 pbkdf2나, scrypt 제공

  • crypto.randomBytes로 64바이트 문자열 생성 => salt 역할(salt는 해독이 더 어렵게 만듦)
  • pbkdf2 인수 = {'비밀번호', salt, 반복 횟수, 출력 바이트, 알고리즘}
  • 반복 횟수 조정 -> 암호화하는 데 1초 정도 걸리게 하는 것이 권장
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'));
    });
});

대칭형 암호화(복호화 가능)

  • 암호화-복호화 시 같은 key 사용
const crypto = require('crypto');

const algorithm = 'aes-256-cvc';
const key = 'qwertyuiop123456789';
const iv = '12456789123456789';  // 초기화 백터 공격 방지

// 암호화
const cipher = crypto.createCipheriv(algorithm, key, iv);
let result = cipher.update('암호화할 문장', 'utf8', 'base64');
result += cipher.final('base64');
console.log('암호화', result);

// 복호화
const decipher = crypto.createDecipheriv(algorithm, key, iv);
let result2 = decipher.update(result, 'base64', 'utf8');
result2 += decipher.final('utf8');
console.log('복호화:', result2);
  • 키 관리 잘해야함
  • 프론트-서버 사이에서는 사용할 수 없음

추천하는 암호화 알고리즘
단방향 암호화 : SHA512
대칭형 암호화 : AES
비대칭형 암호화 : RSA

util

: 각종 편의 기능 모아둔 모듈

1. deprecated

  • 공유한 코드를 잘못 만들었을 경우 함부로 삭제할 수는 없음(쓰고있는 사람들이 있기 때문)
  • 때문에 해당 코드는 deprecated로 감싸주면서 쓸 때마다 경고창이 뜨도록 만들어야 함
const dontUseMe = util.deprecated((x, y) => {
  console.log(x + y);
}, 'dontUseMe 함수는 deprecated되었으니 더 이상 사용하지 마세요!');
dontUseMe(1, 2);
  • 이렇게 해놓고 버전을 올릴 때 dontUseMe제거

2. promisify

const randomBytesPromise = util.promisify(crypto.randomBytes);
randomBytesPromise(64)
  .then((buf) => {
    console.log(buf.toString('base64'));
  })
  .catch((error) => {
    console.error(error);
});
  • node 중에서는 promise가 아직 안되고 callback을 계속 쓸 때가 많음
  • callback은 async, await을 사용할 수 없음 -> 불편
  • 그러한 함수를 promisify로 감싸서 Promise 패턴으로 바꿈 -> then, catch를 사용할 수 있게 함
  • BUT 콜백이 (error, date) => {} 형식이어야 함

worker_threads

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

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

if(isMainThread) {   //분기처리 - 메인쓰레드
    // 처음에 메인 실행 - 워커 생성 - 분배 - 메인에서 총집합
    const worker = new Worker(__filename);
    worker.postMessage('message', (value) => console.log('워커로부터', value));
    worker.on('exit', () => console.log('워커 끝~'));
    worker.postMessage('ping')


} else {    // 워커 쓰레드
    parentPort.oon('message', (value) => {
        console.log('부모로부터', value);
        parentPort.postMessage('pong');
        parentPort.close();
    })
}

메인쓰레드인 경우와 워커 쓰레드인 경우를 분기처리
처음에는 메인쓰레드 실행 후 워커 생성해서 분배
워커에서의 결과를 메인으로 총집합

  • 다음 코드와 같이 worker를 선언 후 따로 작동시킴
const { Worker, isMainThread, parentPort } = require('worker_threads');

if(isMainThread) {   //분기처리 - 메인쓰레드
    // 처음에 메인 실행 - 워커 생성 - 분배 - 메인에서 총집합
    const threads = new Set();

    // 워커쓰레드 2개
    threads.add(new Worker(__filename, {
        workerData: {start : 1},
    }));
    threads.add(new Worker(__filename, {
        workerData: {start : 2},
    }));

    for(let worker of threads) {
        worker.postMessage('message', (value) => console.log('워커로부터', value));
        worker.on('exit', () => {

            // worker 하나 끝날때마다 삭제
            threads.delete(worker);
            if(threads.size === 0) {    // 모든 worker 종료
                console.log('워커 끝~')
            }
        });
    }
    worker.postMessage('ping')


} else {    // 워커 쓰레드
    parentPort.on('message', (value) => {
        console.log('부모로부터', value);
        parentPort.postMessage('pong');
        parentPort.close();
    })
}
  • workerData를 import해서 워커 쓰레드를 다음과 같이 작성할 수 있다.
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

if(isMainThread) { ... }

else {    // 워커 쓰레드
    const data = workerData;   // 데이터 받아옴
    parentPort.postMessage(data.start + 100);   // 데이터에 + 100
}

예제: 2부터 천만까지의 수 중 소수 찾는법

  1. 싱글스레드의 경우
// prime.js
const min = 2;
const max = 10000000;   //천만
const primes = [];

function generatePrimes(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');
generatePrimes(min, max);
console.timeEnd('prime');
console.log(primes.length);

이 경우에 코드를 실행시키면 콘솔창에 다음과 같이 출력된다.

이런 경우에 멀티스레드를 사용하면 좀 더 빠르게 계산할 수 있다.

  1. 스레드를 8개로 하여 계산할 경우
// prime-worker.js
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;    // 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}}));
    }
    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 = primes.concat(msg);
        })
    }
} else {
    findPrimes(workerData.start, workerData.range);
    parentPort.postMessage(primes);
}

이 경우에 코드를 실행시키면 콘솔창에 다음과 같이 출력된다.

시간이 6.3초에서 0.87초로 줄어들었다.

1/8로 줄어들지는 않고, 분배하는 계산을 하거나 컴퓨터 사양에 따라 동시에 도는 쓰레드의 종류가 다르기 때문

child_process

: 다른 언어 호출

// exec.js
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());
  • process.stdout : 콘솔
  • process.stderr : 노드 말고 다른 프로그램 호출

exec가 아닌 spawn을 이용해서 다른 언어를 호출할 수도 있다(여기선 파이썬)

// spawn.js
const spawn = require('child_process').spawn;

// spawn을 이용해서 파이썬 파일 호출
const process = spawn('python', ['test.py']);

process.stdout.on('data', function(data) {
    console.log(data.toString());
});

process.stderr.on('data', function(data) {
    console.error(data.toString());
});

파일 시스템 사용하기

fs

: 파일, 폴더 생성, 삭제, 읽기, 쓰기 기능

  • 파일 읽기

이 코드에서 promises를 사용하고 싶으면 fs에서 제공하는 promises를 가져오면 된다.

  • 파일 만들기

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

  • 동기 = 순서대로, 비동기 = 따로 작동
  • 서버 시작 전 파일 만들거나 비번 삭제 -> 서버 세팅 시는 동기 사용
  • 그 외의 경우에는 기본적으로 비동기 사용
  • 비동기 코드
  • 동기 코드

버퍼와 스트림

  • 버퍼 : 일정한 크기로 모아두는 데이터
    일정한 크기가 되면 한 번에 처리
  • 스트림 : 데이터의 흐름
    일정한 크기로 나눠서 여러 번에 걸쳐서 처리

버퍼로 데이터 전송

  • buffer로 데이터를 한 번에 보낼수도, 버퍼를 나눠서 array 형태로 만들 수도 있다.
  • 마지막 줄처럼 빈 버퍼를 만들 수도 있다(5byte)

스트림으로 데이터 전송

결과는 다음과 같다.

0개의 댓글