그동안 배운 바닐라 자바스크립트, 리액트는 브라우저 상에서 작동하는 것이 위주였기 때문에 파일 시스템에 접근할 일이 없었습니다. 그래서 노드의 파일 시스템 모듈은 노드만의 특징이라고 할 수 있는 기능입니다.
노드의 파일 시스템(fs, File System)
은 파일 시스템에 접근할 수 있는 모듈입니다. 접근이라 함은 다시말해 파일을 생성, 삭제, 수정, 읽기/쓰기가 가능하다는 것을 의미합니다.
기본적인 fs 모듈
의 사용법입니다. 많은 메소드가 있지만 파일을 읽는 readFile()
을 예로 들어보겠습니다. 모듈이기 때문에 require로 불러옵니다.
readFile()
의 사용법은 다음과 같습니다. 첫 번째 인자로는 경로, 두 번째 인자로는 콜백 함수를 사용합니다. 이 콜백은 첫 번째 인자로 읽기를 실패할 경우의 에러를, 두 번째 인자로 읽기를 성공했을 때 데이터를 불러옵니다.
.readFile(파일 경로, 콜백);
실습을 위해 현재 위치에 hello.txt파일을 만들고 'Hello Node!'라는 문구를 입력해놨습니다. 이제 readFile 메소드로 읽어보겠습니다.
const fs = require('fs');
fs.readFile('./src/hello.txt', (error, data) => {
if (error) {
throw error;
}
console.log(data);
});
data
인자는 분명 읽기 성공했을 때의 값이라고 했는데 console.log(data)의 결과가 우리가 생각하던 것과는 다릅니다. 이것은 Buffer
라는 부르는데, 버퍼는 일종의 메모리 데이터입니다. 이 결과로 알 수 있는건 readFile()메소드에서 콜백이 성공을 반환하면 버퍼를 출력한다라는 것을 알 수 있습니다. 우리가 읽을 수 있으려면 toString
으로 변환해주면 됩니다.
console.log(data.toString());
여담으로 한 가지 재밌는 이야기를 하자면 저 버퍼는 아스키 코드 값으로 저장되어있습니다. 따라서 버퍼의 값들을 아스키 코드표의 16진수와 대조해보면 Hello Node!라는 문구를 읽을 수 있습니다.
추가적으로 fs 모듈
이 콜백 형식을 갖는 모듈이라 프로미스를 적용해서 이용하는 것이 훨씬 더 간편하게 이용할 수 있는 방법입니다. 위의 코드를 프로미스 형식으로 바꿔보면 다음과 같이 만들어집니다.
const fs = require('fs').promises;
fs.readFile('./src/hello.txt')
.then(data => {
console.log(data.toString());
})
.catch(error => {
console.error(error);
});
위에서 버퍼
를 소개했었습니다. 그런데 이 버퍼는 한가지 문제점이 있습니다. 만약 전송하는 파일이 10MB라면 메모리에 10MB의 버퍼를 만들고 이용합니다. 파일이 100GB라면 무려 100GB 크기의 버퍼를 만들어야하죠. 또한 1GB짜리 버퍼를 10명이 송수신한다고 해도 총 10GB의 메모리를 쓰게 됩니다. 이것은 서버 환경을 불안정 하게 만들 수 있는 원인이 될 수 있습니다. 그리고 내용을 버퍼에 다 써야만 다음 동작을 수행하기에, 한 번에 읽기, 쓰기 등을 한다면 그 용량을 다 처리해야만 넘어가기에 효율적이지도 못합니다.
그래서 우리는 스트림
이라는 방식을 이용했습니다. 스트림
은 데이터를 잘게 나누어 여러번 보내는 방식입니다. 그러면 버퍼 하나를 통째로 읽는것을 기다리지 않고 전송되는 대로 표시할 수 있다는 큰 장점이 있죠.
그래서 우리가 유튜버들이 영상을 올린 것을 로딩할 때는 버퍼링이라고 하고, 실시간 방송을 할 때 송출되는 것을 스트리밍이라고 하는 것 입니다. 올린 영상을 볼 때는 하나의 큰 영상을 읽어나가면서 로딩되고, 로딩된 부분만 시청할 수 있었습니다. (물론 현재 유튜브는 버퍼링이 거의 느껴지지 않아서 예시가 좋지 못할 수도 있습니다.) 그리고 실시간 방송은 송출자가 찍는 영상을 네트워크 연결만 되어있다면 계속 전송되어서 실시간으로 볼 수 있는 것이죠.
잔말이 많았는데 아무튼 중요한 것은 스트림
은 버퍼를 잘개 쪼개어 연속적으로 여러번 전송하는 방식이다라는 것 입니다. fs 모듈
의 스트림 읽기
메소드는 createReadStream()
가 있습니다.
createReadStream()
메소드는 다음과 같이 사용합니다. 첫 번째 인자로 경로를 사용하고 두 번째 인자는 옵션입니다.
.createReadStream('경로');
위에서 버퍼로 본 예제를 스트림 방식으로 읽어보겠습니다.
const fs = require('fs');
const readStream = fs.createReadStream('./src/hello.txt', {highWaterMark: 4});
const data = [];
readStream.on('data', chunk => {
data.push(chunk);
console.log(chunk, chunk.length);
});
readStream.on('end', () => {
console.log('result: ', Buffer.concat(data).toString());
});
readStream.on('error', err => {
console.error(err);
});
const readStream = fs.createReadStream('./src/hello.txt', {highWaterMark: 4});
Read Stream을 생성하는 구문입니다. 두 번째 인자 옵션에 highWaterMark: 4
라는게 등장했는데, 이것은 버퍼의 크기를 정하는 옵션입니다. 단위는 Byte
이고, 미설정시 default값은 64KB입니다. 이 예제에서 스트림이 버퍼를 나눠서 보낸다는 것을 알려드리기 위해 4Byte단위로 나누도록 만들었습니다.
readStream.on('data', chunk => {
data.push(chunk);
console.log(chunk, chunk.length);
});
readStream.on('end', () => {
console.log('result: ', Buffer.concat(data).toString());
});
readStream.on('error', err => {
console.error(err);
});
Read Stream은 이벤트 리스너
를 붙여서 사용합니다. data, end, error
가 있는데, data
는 파일 읽기를 성공한 경우, end
는 파일 읽기가 종료된 경우, error
는 파일 읽기 과정에 오류가 난 경우에 호출되는 이벤트입니다.
data
이벤트에서는 chunk
라는 인자를 이벤트 콜백에서 이용하는데, chunk
는 스트림을 위해 나눠진 조각들 입니다. 여기서는 4Byte씩 나누기로 했으니, chunk는 4바이트짜리 조각이겠네요.
end
이벤트에서는 파일 읽기가 종료되면 파일 내용을 출력하는 기능을 수행합니다. 청크로 쪼개놨기 때문에 concat()메소드로 나뉘어진 문자들을 이어붙였습니다.
결과를 확인해보면 다음과 같이 조각들이 4Byte로 잘 나뉘었고 마지막에 잘 이어붙여져서 출력되었음을 알 수 있습니다.
너무 읽기만 봤으니 읽기의 짝꿍 쓰기도 소개하고 마치도록 하겠습니다. 쓰기도 마찬가지로 fs.createWriteStream()
을 이용해서 쓰기 스트림
을 이용합니다.
.createWriteStream('쓰기 할 파일 경로');
쓰기 스트림을 생성하고 write메소드를 통해 문자를 씁니다.
const fs = require('fs');
const writeStram = fs.createWriteStream('./src/hello.txt');
writeStram.write('Write Stream으로 쓰여진 문장\n');
writeStram.end();
실행 결과를 보면 다음과 같이 제대로 쓰여졌음을 확인할 수 있습니다.
참고로 방금 본 쓰기기는 완전히 덮어쓰기라, 기존 내용에 이어서 쓰고 싶다면 .appendFile
을 이용합니다.