버퍼와 스트림 이해하기
노드는 파일을 읽을 때 메모리에 파일 크기만큼 공간을 마련해두며 파일 데이터를 메모리에 저장한 뒤 사용자가 조작항 수 있도록 합니다.
이때 메모리에 저장된 데이터가 바로 버퍼입니다.
buffer.js
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);
콘솔
$ node buffer
from(): <Buffer ec a0 80 eb a5 bc 20 eb b2 84 ed 8d bc eb a1 9c 20 eb b0 94 ea bf 94 eb b3 b4 ec 84 b8 ec 9a 94>
length: 32
toString(): 저를 버퍼로 바꿔보세요
concat(): 띄엄 띄엄 띄어쓰기
alloc(): <Buffer 00 00 00 00 00>
Buffer 객체는 여러 가지 메서드를 제공합니다.
from(문자열): 문자열을 버퍼로 바꿀 수 있습니다. length 속성은 버퍼의 크기를 알립니다. 바이트 단위입니다.toString(버퍼): 버퍼를 다시 문자열로 바꿀 수 있습니다. 이제 base64나 hex를 인수로 넣으면 해당 인코딩도 변환 가능힙니다.concat(배열): 배열 안에 든 버퍼들을 하나로 합칩니다.alloc(바이트): 빈 버퍼를 생성합니다. 바이트를 인수로 넣으면 해당 크기의 버퍼가 생성됩니다.
readFile 방식의 버퍼과 편리하기는 하지만 문제점도 있습니다. 서버처럼 몇 명이 이용할지 모르는 환경에서는 메모리 문제가 발생할 수 있습니다.또한, 모든 내용을 버퍼에 다 쓴 후에야 다음 동작으로 넘어가므로 조작을 연달아 할 때 매번 용량을 버퍼로 처리해야 다음 단계로 넘어갈 수 있습니다.
그래서 버퍼의 크기를 작게 만든 후 여러 번으로 나눠 보내는 방식이 스트림입니다.
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);
});
콘솔
$ node createReadStream
data : <Buffer ec a0 80 eb 8a 94 20 ec a1 b0 ea b8 88 ec 94 a9> 16
data : <Buffer 20 ec a1 b0 ea b8 88 ec 94 a9 20 eb 82 98 eb 88> 16
data : <Buffer a0 ec 84 9c 20 ec a0 84 eb 8b ac eb 90 a9 eb 8b> 16
data : <Buffer 88 eb 8b a4 2e 20 eb 82 98 eb 88 a0 ec a7 84 20> 16
data : <Buffer ec a1 b0 ea b0 81 ec 9d 84 20 63 68 75 6e 6b eb> 16
data : <Buffer 9d bc ea b3 a0 20 eb b6 80 eb a6 85 eb 8b 88 eb> 16
data : <Buffer 8b a4 2e> 3
end : 저는 조금씩 조금씩 나눠서 전달됩니다. 나눠진 조각을 chunk라고 부릅니다.
먼저 createReadStream으로 읽기 스트림을 만듭니다. 첫 번째 인수로 읽을 파일 경로를 넣습니다. 두 번째 인수는 옵션 객체인데 highWaterMark라는 옵션이 버퍼의 크기(바이트 단위)를 정할 수 있는 옵션입니다. 기본값은 64KB입니다.
readStream은 이벤트 리스너를 붙여서 사용합니다. 보통 data, end, error 이벤트를 사용합니다. 파일을 읽는 도중에 에러가 발생하면 error를 파일 읽기가 시작되면 data 이벤트가 발생합니다. 파일을 다 읽으면 end 이벤트가 발생합니다.
createWriteStream.js
const fs = require("fs");
const writeStream = fs.createWriteStream("./writeme2.txt");
writeStream.on("finish", () => {
console.log("파일 쓰기 완료");
});
writeStream.write("이 글을 씁니다.h");
writeStream.write("한 번 더 씁니다.");
writeStream.end();
콘솔
$ node createWriteStream
파일 쓰기 완료
만약 createWriteStream으로 쓰기 스트림을 만듭니다. 첫 번째 인수로는 출력 파일명을 입력합니다. 두 번째 인수는 옵션입니다.
파일 쓰기가 종료되면 finish 이벤트 리스너가 호출됩니다.
writeStream에서 제공하는 write 메서드로 넣을 데이터를 씁니다. 여러 번 호출헐 수 있습니다. 데이터를 다 썼다면 end 메서드로 종료를 알립니다. 이때 finish 이벤트가 발생합니다.
readme4.txt
저를 writeme3.txt로 보내주세요.
pipe.js
const fs = require('fs')
const readStream = fs.createReadStream('readme4.txt');
const writeStream = fs.createWriteStrem('write3.txt');
readStream.pipe(writeStream);
콘솔
$ node pipe
readme4.txt와 똑같은 내용의 writeme3.txt가 생성되었을 것입니다. 미리 읽기 스트림과 쓰기 스트림을 만들어둔 후 두 개의 스트림 사이클 pipe 메서드로 연결하면 저절로 데이터가 witeStream으로 넘어갑니다. 따로 on('data')니 writeStream.write를 하지 않아도 알아서 되므로 편리합니다.
pipe는 스트림 사이에 여러 번 연결할 수 있습니다.
gzip.js
const zlib = require("zlib");
const fs = require("fs");
const readStream = fs.createReadStream("./readme4.txt");
const zlibStream = zlib.createGzip();
const writeSteam = fs.createWriteStream("./readme4.txt.gz");
readStream.pipe(zlibStream).pipe(writeSteam);
노드에서는 파일을 압축하는 zlib이라는 모듈도 제공합니다. 다만 zlib의 createGzip는 메서드가 스트림을 지원하므로 readStream고 writeStream 중간에서 파이핑을 할 수 있습니다. 버퍼 데이터가 전달되다가 gzip 압축을 거친 후 파일로 써집니다.
콘솔
$ node gzip
이렇게 전체 파일을 모두 버퍼에 저장하는 readFile 메서드와 부분으로 나눠 만든 createReadStream 메서드를 알아봤습니다. 이 두 메서드의 메모리 사용량이 얼마나 다른지 실제로 확인해보겠습니다.
createBigFile.js
const fs = require("fs");
const file = fs.createWriteStream("./big.txt");
for (let i = 0; i <= 10000000; i++) {
file.write(
"안녕하세요. 엄청나게 큰 파일을 만들어 볼 것입니다. 각오 단단히 하세요!\n"
);
}
file.end();
콘솔
$ node createBigFile
readFile 메서드를 사용하여 big.txt를 big2.txt로 복사해보겠습니다.
buffer-memory.js
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);
콘솔
$ node buffer-memory
before: 29126656
buffer: 1030877184
대용량의 파일을 복사하기 위해 메모리에 파일을 모두 올려둔 후 writeFileSync를 수행했기 때문입니다.
이번에는 스트림을 사용하여 파일을 big3.txt로 복사해보겠습니다.
stream-memory.js
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);
});
콘솔
$ node stream-memory
before(): 29102080
stream: 39264256
큰 파일을 조각내어 작은 버퍼 단위로 옮겼기 때문입니다. 이렇게 스트림을 사용하면 효과적으로 데이터를 전송할 수 있습니다.
'프로그래밍 언어 > NODE JS' 카테고리의 다른 글
| 스레드 풀 알아보기 (0) | 2025.05.04 |
|---|---|
| 기타 fs 메서드 알아보기 (0) | 2025.05.01 |
| 동기 메서드와 비동기 메서드 (0) | 2025.04.26 |
| 파일 시스템 접근하기 (0) | 2025.04.22 |
| 기타 모듈들 (0) | 2025.04.19 |