자주 사용되는 모듈2

[Node.js] 자주 사용되는 모듈2


fs

fs모듈은 파일에 대한 연산을 지원하는 모듈이다.
가장 기본적으로 사용되는 메서드는 readFile이다. (비동기. callback 형식.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// fs-module.js
const fs = require("fs");

fs.readFile("./readme.txt", (err, data) => {
		if(err)
				throw err;

		console.log(data);
		console.log(data.toString());
});

// 출력 결과
<Buffer ec 95 88 eb 85 95 ed 95 98 ec 84 b8 ec 9a 94 2e>
안녕하세요.


readFile은 기본적으로 callback 형식으로 수행된다. 이를 promise형식으로 사용하고 싶다면 fs모듈 import 시 아래와 같이 해주면 된다.

1
2
3
4
5
6
7
8
9
const fs = require("fs").promises;

try {
		const data = await fs.readFile("./readme.txt");
		console.log(data);
		console.log(data.toString());
} catch(error) {
		console.error(err);
}

readFile은 기본적으로 비동기적으로 수행된다. 만약 readFile의 순서를 맞추고 싶다면 readFileSync 메서드를 사용하거나, then/catch 혹은 async/await으로 코딩하면 된다.

readFileSync는 메인 쓰레드를 블로킹하기 때문에 추천하지 않는다.(메인 쓰레드가 블로킹되면 뒤의 코드가 실행되지 못하고 대기해야 한다.) Node.js의 장점인 이벤트 기반, 비동기적 프로그래밍을 지향하자!

주의점

readFile의 첫 번째 인자인 파일의 경로는 js 파일을 실행하는 콘솔 기준으로 계산된다.

readFile-example

예를 들어, 위와 같이 readme.txt가 temp 폴더 내에 있고, 이를 읽는 작업을 하는 js 파일(fs-module.js)이 루트 경로에 있을 때, 루트 경로에서 콘솔로 fs-module.js를 실행하면 오류가 발생한다.
이 경우, temp 폴더 내에서 콘솔로 “node ../fs-module” 를 실행해야 오류가 발생하지 않는다.

버퍼와 스트림

버퍼는 메모리 상에 작업할 내용 전부를 저장해둔 뒤 일괄 처리하고, 스트림은 작업할 내용을 작은 단위로 모아서 짧게 짧게 처리하는 방식이다.
readFile은 기본적으로 결과물을 buffer 형태로 반환한다. 읽은 데이터를 문자열 형태로 변환하고 싶다면 toString() 메서드를 사용하면 된다.
작업할 내용의 크기가 작다면 문제가 없지만, 만약 100MB 가량의 데이터를 읽는 작업이 동시에 10회 수행된다고 가정하자. 이 경우 1GB에 달하는 메모리가 이 작업을 위해 사용될 것이다.
이렇게 큰 데이터 용량을 다루는 작업의 경우 버퍼보단 스트림을 사용하는 것이 알맞다. fs모듈에서 스트림을 사용하는 방법은 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const fs = require("fs");

const readStream = fs.createReadStream("./readme.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);
});


읽기 스트림을 사용하기 위해 createReadStream 메서드를 이용한다. 첫 번째 인자로 읽을 파일의 경로를, 두 번째 인자로 옵션이 전달될 수 있다. 옵션으로 전달된 highWaterMark는 스트림의 크기를 의미한다. 기본적으로 스트림의 크기는 64KB이지만, 책에서는 스트림이 데이터를 쪼개어 작업하는 것을 보여주기 위해 16B로 설정했다. 출력 결과는 아래와 같다.

stream-example

이와 동일한 방식으로 파일 쓰기도 가능하다.

1
2
3
4
5
6
7
8
9
10
11
const fs = require("fs");

const writeStream = fs.createWriteStream("./writeme.txt", { highWaterMark: 16 });

writeStream.on("finish", () => {
		console.log("파일 쓰기 완료");
});

writeStream.write("안녕하세요.");
writeStream.write("저는 여러번에 걸쳐 쓰입니다.");
writeStream.end();


파일 읽기, 쓰기 스트림을 서로 연결하여 파일을 복사하는 것과 같이 사용할 수도 있다. 이렇게 스트림을 서로 연결하는 방식을 “파이핑” 이라고 표현한다.

1
2
3
4
5
6
const fs = require("fs");

const readStream = fs.createReadStream("./readme.txt");
const writeStream = fs.createWriteStream("./writeme.txt");

readStream.pipe(writeStream); // 파이핑

노드 8.5버전 전까지 이 방법으로 파일을 복사했다. 현재 버전에서는 파일 복사를 위해 fs.copyFile(복사할 파일 경로, 새로 만들어질 파일 경로) 메서드를 사용한다.

기타 fs모듈의 메서드들

fs.access(경로, 옵션, 콜백)

폴더 혹은 파일의 접근 권한을 확인하는 메서드이다.
두 번째 인자는 확인할 권한 종류이다. fs모듈의 constants 프로퍼티에 담겨있는 상수값들을 사용할 수 있다.

  • constants.F_OK
    파일 존재 여부

  • constants.W_OK
    파일 쓰기 권한 여부

  • constants.R_OK
    파일 읽기 권한 여부

1
2
3
4
5
6
7
8
9
10
const fs = require("fs");
const constants = require("fs").constants;

fs.access("./readme.txt", constants.F_OK | constants.W_OK | constants.R_OK)
.then(() => {
		// 파일 접근 가능할 시 수행할 코드
})
.catch(() => {
	// 파일 접근 권한이 없어 실패 시 수행할 코드
});

fs.midkr(경로, 콜백)

폴더 생성 메서드이다. 이미 폴더가 존재하면, 오류가 발생하므로 fs.access 메서드를 통해 존재 여부를 미리 확인해야 한다.

fs.open(경로, 옵션, 콜백)

파일의 아이디 (file descriptor)를 가져오는 메서드이다.
두 번째 인자인 옵션으로 파일에 어떠한 작업을 할 지 설정할 수 있다.
(쓰기 : w, 읽기 : r, 기존 파일에 추가 : a)
w 옵션은 파일이 없을 때 파일을 생성하지만, r 이나 a는 파일이 존재해야 한다.

fs.rename(기존 경로, 새 경로, 콜백)

파일명을 바꾸는 메서드이다.
기존 경로와 새 경로가 다른 폴더여도 된다. 이 덕분에 잘라내기와 같은 효과를 볼 수도 있다.

fs.readdir(경로, 콜백)

폴더 하위의 내용물을 확인하는 메서드이다.
폴더 내의 파일/폴더명이 배열에 담겨 반환된다.

fs.unlink(경로, 콜백)

파일을 지우는 메서드이다.
파일이 존재하지 않을 경우 오류가 발생한다.

fs.rmdir(경로, 콜백)

폴더를 지우는 메서드이다.
폴더가 없거나 지우려는 폴더가 빈 폴더가 아닐 경우 오류가 발생한다.

fs.watch(경로, 콜백)

파일/폴더의 변경사항을 감시할 수 있는 메서드이다.

1
2
3
4
5
const fs = require("fs");

fs.watch("./readme.txt", (eventType, filename) => {
		console.log(eventType, filename);
});

위 js파일을 실행하고 readme.txt 내용을 수정하거나 파일명 변경, 삭제하면 로그가 출력된다.
파일명을 변경하거나 삭제한 뒤에는 더 이상 해당 파일을 감시할 수 없다.


crypto

다양한 방식의 암호화를 지원하는 모듈이다. 단방향 암호화 (해시) 및 양방향 대칭/비대칭 암호화 모두 지원한다.

단방향 암호화

흔히 해싱이라고 부르는 단방향 암호화는 입력으로 주어지는 문자열을 특정 길이의 문자열로 변환하는 작업을 말한다. 말 그대로 변환된 문자열의 복호화는 불가능하다.

1
2
3
4
const crypto = require("crypto");

const password = "test";
const hashedPassword = crypto.createHash("sha256").update(password).digest("base64");

해싱을 할 땐 세 가지 과정을 거친다.

  1. createHash(알고리즘)
    사용할 해싱 알고리즘을 선택한다. sha256, sha512 등이 있다.
    md5나 sha1 계열의 알고리즘은 이미 취약점이 드러났으므로 사용하지 않는 것이 좋다.

  2. update(문자열)
    해싱할 문자열을 인자로 받는다. 이 시점에서 패스워드가 해싱 알고리즘에 의해 해싱된다.

  3. digest(인코딩)
    해싱된 문자열을 인코딩할 알고리즘을 설정한다. base64, hex, latin1 등이 있다.
    base64가 결과 문자열이 가장 짧아서 자주 사용된다.


pbkdf2

crypto 모듈에서 자주 사용되는 단방향 암호화는 pbkdf2이다. pbkdf2는 간단하게 말해서 salt라는 랜덤 문자열을 기존 문자열에 추가한 뒤 해시 알고리즘을 반복적으로 적용하는 방식이다.

1
2
3
4
5
6
7
8
9
10
const crypto = require("crypto");

crypto.randomBytes(64, (err, buf) => {
		const salt = buf.toString("base64");
		const password = "test";

		crypto.pbkdf2(password, salt, 100000, 64, "sha512", (err, key) => {
				console.log("password : ", key.toString("base64"));
		});
});

crypto.pbkdf2 메서드는 순서대로 해싱할 문자열, salt, 해싱 알고리즘 반복 횟수, 결과(key)의 길이, 해싱 알고리즘, 콜백을 인자로 받는다.
결과인 key는 buffer형태로 반환되므로 이를 문자열로 변환하려면 toString(인코딩) 메서드를 사용해야 한다.
pbkdf2보다 조금 더 보안적으로 강력한 bcrypt, scrypt 등의 메서드들이 존재한다. 상황에 맞게 사용하면 된다.

양방향 암호화

양방향 암호화는 복호화가 가능하다. 대신 암호화, 복호화를 위한 key가 있어야 한다. 양방향 암호화는 사용할 암호 알고리즘에 따라 사용 방법이 다르며, 단방향 암호화보다 복잡하다.
따라서 예시로 대칭 암호 알고리즘인 AES 암호 알고리즘을 사용하는 코드를 보며 흐름만 살펴보고 넘어가겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const crypto = require("crypto");

const algorithm = "aes-256-cbc";
const key = "abcdefghijklmnopqrstuvwxyz123456";
const iv = "1234567890123456";
const password = "test";

const cipher = crypto.createCipheriv(algorithm, key, iv);
let result = cipher.update(password, "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);

AES 암호 알고리즘은 key가 32B이어야 하고, iv가 16B이어야 한다. iv는 initialization vector의 약자로 AES 암호 알고리즘을 시작할 때 필요한 문자열이다. iv에 대한 자세한 내용은 AES 암호 알고리즘을 따로 공부하면 알 수 있다.

양방향 암호화는 세 가지 과정을 거친다.

  1. createCipheriv(암호 알고리즘, 사용할 키, iv)
    암호화할 때 사용할 cipher를 준비한다.

  2. update(암호화할 문자열, 입력 문자열 인코딩, 출력 문자열 인코딩)
    문자열을 인자로 주어 해당 문자열을 암호화한다.
    cipher.final이 호출되기 전까지 cipher.update 메서드로 다른 데이터를 암호화할 수 있다.

  3. final(출력 문자열 인코딩)
    cipher.update를 통해 암호화된 데이터를 주어진 인코딩 방식으로 인코딩하여 반환한다.
    cipher.final이 호출되면 해당 cipher 인스턴스로 더 이상 update할 수 없다.

복호화 과정은 암호화 순서와 동일하며, 주어지는 인자의 순서가 다르다.


worker_threads

노드에서 멀티 쓰레딩을 할 수 있도록 지원하는 모듈이다.
멀티 쓰레딩은 제대로 하기 위해서 숙련도가 필요하고, 내가 관심을 갖고 있는 백엔드 분야에서는 사용할 일이 빈번할 듯 하여 제대로 공부한 뒤 따로 포스팅을 할 계획이다.

0%