파일을 읽거나 쓰는 방식에는 두가지 방식이 있다.
흔히 말해 버퍼링
이란 영상을 재생할 수 있을때까지 데이터를 모으는 동작을 말하며, 스트리밍
은 방송을 시청자 컴퓨터에 전송하는 동작이다. 여기너 나오는 버퍼
와 스트림
또한 비슷한 개념으로서 사용된다.
Node.js에는 fs
모듈이 있다. fs
모듈은 파일과 관련된 메소드들이 존재한다. 여기서 readFile
메소드는 텍스트를 버퍼로 읽어들이는 기능을 한다.
fs.readFile(`${__dirname}/../readme.txt`, (err, data) => {
if (err) {
throw err
}
console.log(data)
console.log(data.toString())
})
/*
<Buffer 52 65 61 64 6d 65 20 70 6c 65 61 73 65>
Readme please
*/
Node.js에는 Buffer를 다룰 수 있는 Buffer
클래스가 있다. Buffer객체의 메소드들을 살펴보자
메소드 | 기능 |
---|---|
from(문자열) | 문자열을 버퍼로 바꿀 수 있다. 변경된 버퍼의 length 속성은 버퍼의 크기를 알리며 이는 바이트 단위이다. |
toString(encodingType) | 버퍼를 다시 문자열로 바꾼다. base64,hex등 인코딩 타입을 지정해줄 수 있다. |
concat(배열) | 배열안에 든 버퍼들을 하나로 합친다. |
alloc(바이트) | 빈 버퍼를 생성한다. 바이트를 인수로 넣으면 해당 크기의 버퍼가 생성된다. |
const buffer = Buffer.from('Hello world')
console.log('from() : ', buffer)
console.log('length : ', buffer.length)
console.log('toString() : ', buffer.toString())
const array = [Buffer.from('Node.js '), Buffer.from('buffer '), Buffer.from('concat '), Buffer.from('array')]
const buffer2 = Buffer.concat(array)
console.log('concat(), toString() : ', buffer2.toString())
const buffer3 = Buffer.alloc(5)
console.log('length : ', buffer3.length)
위의 readFile
방식에는 단점이 있다. 용량이 100MB인 파일이 있다면 해당 100MB의 버퍼를 만들어야한다. 서버에서 해당 작업을 10개만 해도 1GB의 메모리를 점유하게 되고, 이는 결코 적은 용량의 메모리 사용량이 아니다.
이 단점을 해결하고자 버퍼의 크기를 작게 만든 후 여러번 나누어 보내는 방식이 생겼다. 100MB를 한번에 보내는것이 아닌 1MB를 100번 나누어 보내는것과 같은 방식으로 말이다. 파일을 읽는 스트림
메소드로는 fs
의 createReadStream
이 있다.
createReadStream
은 읽기 스트림을 생성하게 되는데, 첫번째 매개변수는 인수로 읽을 파일 경로를 넣고, 두번쨰 인수는 옵션 객체로서, highWaterMark
옵션에는 버퍼의 크기를 정할 수 있다. 기본값은 64kb라고한다.
또한 Read Stream은 Event Emitter를 활용한다.(https://velog.io/@hoplin/Node.js-Event-Stream) 일반적으로 data
,end
,error
이벤트를 사용하게 된다.
자세한건 document
를 참고하자.
createReadStream
을 활용한 파일 읽기를 해본다. data
이벤트가 발생하면 배열에 버퍼를 저장하고, end
이벤트가 발생하면 문자열로 합친다.
예시를 작성해 본다. 예시에서는 16바이트로 하여 나누어지는것을 확인한다.(예시로 사용한 텍스트가 짧기때문)
// readme.txt
this is example readme file. to test create read stream from fs module
// Code
import * as fs from 'fs'
const stream = fs.createReadStream(`${__dirname}/../readme.txt`, { highWaterMark: 16 })
const buffers = new Array<Buffer>();
stream.on('data', (chunk: Buffer) => {
buffers.push(chunk);
console.log(`data : `, chunk, chunk.length)
})
stream.on('error', (err) => {
console.log(err)
})
stream.on('end', () => {
console.log(Buffer.concat(buffers).toString())
})
/*
data : <Buffer 74 68 69 73 20 69 73 20 65 78 61 6d 70 6c 65 20> 16
data : <Buffer 72 65 61 64 6d 65 20 66 69 6c 65 2e 20 74 6f 20> 16
data : <Buffer 74 65 73 74 20 63 72 65 61 74 65 20 72 65 61 64> 16
data : <Buffer 20 73 74 72 65 61 6d 20 66 72 6f 6d 20 66 73 20> 16
data : <Buffer 6d 6f 64 75 6c 65> 6
this is example readme file. to test create read stream from fs module
*/
이번에는 반대로 파일을 작성할때 스트림
을 사용해본다. 이를 위해서는 createWriteStream
을 사용한다. createReadStream
과 동일하게, Event Emitter를 사용한다. 대표적으로 finish
이벤트를 사용하며, 이는 데이터를 다 썼을때 발생되게 된다.
데이터를 다 썼다고 알려주기 위해서는 end()
메소드를 호출해준다.
const stream2 = fs.createWriteStream(`${__dirname}/../writeme.txt`)
stream2.on('finish', () => {
console.log("Complete to write file")
})
stream2.write("write line 1\n")
stream2.write("write line 2")
stream2.end()
// writeme.txt
write line 1
write line 2
createReadStream
으로 파일을 읽은 후 해당 스트림을 전달받아 createWriteStream
으로 파일을 쓸 수 도 있다. 스트림끼리 연결하는것을 파이핑
이라고 한다.
위에서 생성한 writeme.txt
파일을 읽어 writeme2.txt
파일에 쓰기를 해본다. 파이핑을 하기 위해서는 스트림의 pipe(스트림)
메소드를 사용하면 된다.
import * as fs from 'fs'
const rStream = fs.createReadStream(`${__dirname}/../writeme.txt`)
const wStream = fs.createWriteStream(`${__dirname}/../writeme2.txt`)
rStream.pipe(wStream)
물론 현재는 fs모듈의 copyFile()
메소드를 사용하여 파일을 복사할 수 있기도 하다.
import * as fs from 'fs/promises'
fs.copyFile(`${__dirname}/../writeme.txt`, `${__dirname}/../writeme3.txt`)
.then(() => {
console.log("Complete to copy")
})
.catch((err) => {
console.error(err)
})
pipe는 스트림 사이에 여러번 연결할 수 있다. 예를 들어 노드에서 zlib는 파일을 압축하는 모듈이다. 이 모듈의 createGzip
이라는 메소드는 스트림을 지원한다. createReadStream
, createWriteStream
중간에 파이프를 놓으면, 버퍼가 전송되다 gzip 압축을 거친 후 파일로 작성된다.
import * as fs from 'fs'
import * as zlib from 'zlib'
const rStream = fs.createReadStream(`${__dirname}/../readme.txt`)
const zStream = zlib.createGzip();
const wStream = fs.createWriteStream(`${__dirname}/../readmezip.gz`)
rStream.pipe(zStream).pipe(wStream)