3.5 파일 시스템 접근하기
fs 모듈은 파일 시스템에 접근하는 모듈이다.
즉 파일을 생성하거나 삭제하고 읽거나 쓸 수 있다. 또한 폴더도 만들거나 지울 수 있다.
// readme.txt
저를 읽어주세요.
const fs = require('fs');
fs.readFile('./readme.txt', (err, data) => {
if (err) {
throw err;
}
console.log(data);
console.log(data.toString());
});
/* 실행 결과
<Buffer ec a0 80 eb a5 bc 20 ec 9d bd ec 96 b4 ec a3 bc ec 84 b8 ec 9a 94 2e>
저를 읽어주세요.
*/
fs 모듈을 불러온 뒤 읽을 파일의 경로를 지정한다.
여기서 파일의 경로가 현재 파일 기준이 아니라 node 명령어를 실행하는 콘솔 기준이다.
readFile의 결과물은 버퍼라는 형식으로 제공되기 때문에 toString을 통해 문자열로 변환해야 제대로 문자열이 출력된다.
const fs = require('fs');
fs.writeFile('./writeme.txt', '글이 입력됩니다', (err) => {
if (err) {
throw err;
}
fs.readFile('./writeme.txt', (err, data) => {
if (err) {
throw err;
}
console.log(data.toString());
});
});
/* 실행 결과
글이 입력됩니다.
*/
writeFile 메서드에 생성될 파일의 경로와 내용을 입력한다.
파일이 만들어지고, 파일 읽기도 가능하다.
3.6.1 동기 메서드와 비동기 메서드
setTimeout 같은 타이머와 process.nextTick이외에도 노드는 대부분 메서드를 비동기 방식으로 처리한다.
몇몇 메서드는 동기 방식으로 처리할 수 있는데 fs 모듈에 그러한 메소드들이 많이 있다.
// readme2.txt
저를 읽어주세요.
const fs = require('fs');
console.log('시작');
fs.readFile('./readme2.txt', (err, data) => {
if (err) {
throw err;
}
console.log('1번', data.toString());
});
fs.readFile('./readme2.txt', (err, data) => {
if (err) {
throw err;
}
console.log('2번', data.toString());
});
fs.readFile('./readme2.txt', (err, data) => {
if (err) {
throw err;
}
console.log('3번', data.toString());
});
console.log('끝');
/* 실행 결과
시작
끝
2번 저를 여러 번 읽어보세요.
3번 저를 여러 번 읽어보세요.
1번 저를 여러 번 읽어보세요.
*/
비동기 메서드들은 백그라운드에 해당 파일을 읽으라고 요청만 하고 다음 작업으로 넘어간다.
따라서 파일 읽기 요청만 세 번을 보내고 console.log('끝')을 찍는다.
읽기가 완료되면 백그라운드가 다시 메인 스레드에 알리고 그제야 등록된 콜백 함수를 실행한다.
이 방식의 좋은 점은 수 백개의 I/O 요청이 들어와도 백그라운드에 요청 자리를 위임하여 그 후로도 얼마든지 요청을 받을 수 있다.
동기와 비동기, 블로킹과 논블로킹의 차이
동기와 비동기 : 백그라운드 작업 완료 확인 여부
블로킹과 논블로킹 : 함수가 바로 return되는지 여부
순서대로 출력하고 싶다면 다음 메서드를 사용할 수 있다.
const fs = require('fs');
console.log('시작');
let data = fs.readFileSync('./readme2.txt');
console.log('1번', data.toString());
data = fs.readFileSync('./readme2.txt');
console.log('2번', data.toString());
data = fs.readFileSync('./readme2.txt');
console.log('3번', data.toString());
console.log('끝');
/* 실행 결과
시작
1번 저를 여러 번 읽어보세요.
2번 저를 여러 번 읽어보세요.
3번 저를 여러 번 읽어보세요.
끝
*/
콜백 함수를 넣는 대신 직접 return 값을 받아온다.
요청이 수백개 이상 들어올 때 백그라운드가 작업하는 동안 메인 스레드는 아무것도 못하고 대기하고 있어야 되기 때문에 비효율적이고 성능에 문제가 생긴다.
동기 메서드들은 이름 뒤에 Sync가 붙어 있다.
비동기 방식으로 하되 순서를 유지하고 싶다면 이전 readFile의 콜백에 다음 readFile을 넣으면 된다. (콜백 지옥)
const fs = require('fs');
console.log('시작');
fs.readFile('./readme2.txt', (err, data) => {
if (err) {
throw err;
}
console.log('1번', data.toString());
fs.readFile('./readme2.txt', (err, data) => {
if (err) {
throw err;
}
console.log('2번', data.toString());
fs.readFile('./readme2.txt', (err, data) => {
if (err) {
throw err;
}
console.log('3번', data.toString());
console.log('끝');
});
});
});
3.6.2 버퍼와 스트림 이해하기
파일을 읽거나 쓰는 방식에는 크게 두 가지 방식, 즉 버퍼를 이용하거나 스트림을 이용하는 방식이 있다.
버퍼링은 영상을 재생할 수 있을 때까지 데이터를 모으는 동작이고, 스트리밍은 방송인 컴퓨터에서 시청자의 컴퓨터로 영상 데이터를 조금씩 전송하는 동작이다.
노드의 버퍼와 스트림도 비슷한 개념이다.
파일을 읽을 때 메모리에 파일 크기만큼 공간을 마련해두며 파일 데이터를 메모리에 저장한 뒤 사용자가 조작할 수 있도록 한다. 이때 메모리에 저장된 데이터가 버퍼이다.
Buffer는 버퍼를 직접 다룰 수 있는 클래스이다.
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);
/* 실행 결과
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>
*/
from(문자열): 문자열을 버퍼로 바꿀 수 있다. length 속성은 버퍼의 크기를 알린다. 바이트 단위
toString(버퍼): 버퍼를 다시 문자열로 바꿀 수 있다. 이때 base64나 hex를 인수로 넣으면 해당 인코딩으로도 변환 가능
concat(배열): 배열 안에 든 버퍼들을 하나로 합친다.
alloc(바이트): 빈 버퍼를 생성한다. 바이트를 인수로 넣으면 해당 크기의 버퍼가 생성된다.
readFile 방식의 버퍼가 편리하지만 문제점도 있다. 용량이 100MB인 파일이 있으면 읽을 때 메모리에 100MB의 버퍼를 만들어야 하지만 여러명이 읽을 경우 많은 메모리가 사용되어 문제가 발생할 수 있다.
또한 모든 내용을 버퍼에 다 쓴 후에 다음 동작으로 넘어가야하는 문제가 있다.
그래서 버퍼의 크기를 작게 만들고 여러 번에 걸쳐 나눠 보내는 방식이 등장했다.
파일을 읽는 스트림 메서드로는 createReadStream이 있다.
// readme3.txt
저는 조금씩 조금씩 나눠서 전달됩니다. 나눠진 조각을 chunk라고 부릅니다.
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);
});
/* 실행 결과
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은 이벤트 리스너를 붙여서 사용한다.
const fs = require('fs');
const writeStream = fs.createWriteStream('./writeme2.txt');
writeStream.on('finish', () => {
console.log('파일 쓰기 완료');
});
writeStream.write('이 글을 씁니다.\n');
writeStream.write('한 번 더 씁니다.');
writeStream.end();
먼저 createWaterStream으로 쓰기 스트림을 만든다.
첫 번째 인수로 출력 파일명, 두 번째 인수로 옵션을 넣는다.
writeStream에서 제공하는 write메서드로 넣을 데이터를 쓴다.
데이터를 다 쓰면 end 메서드로 종료를 알린다.
스트림끼리 연결하는 것을 파이핑한다고 표현하는데 액체가 흐르는 관 파이프처럼 데이터가 흐른다고 해서 지어진 이름이다.
// readme4.txt
저를 writeme3.txt로 보내주세요.
const fs = require('fs');
const readStream = fs.createReadStream('readme4.txt');
const writeStream = fs.createWriteStream('writeme3.txt');
readStream.pipe(writeStream);
readme4.txt와 똑같은 내용의 writeme3.txt가 생성된다.
미리 읽기 스트림과 쓰기 스트림을 만든 후 두 개의 스트림 사이를 pipe 메서드로 연결하면 데이터가 writeStream으로 넘어간다.
const zlib = require('zlib');
const fs = require('fs');
const readStream = fs.createReadStream('./readme4.txt');
const zlibStream = zlib.createGzip();
const writeStream = fs.createWriteStream('./readme4.txt.gz');
readStream.pipe(zlibStream).pipe(writeStream);
노드에서는 파일을 압축하는 zlib이라는 모듈도 제공한다.
zlib의 createGzip이라는 메서드가 스트림을지원하므로 readStream과 writeStream 중간에서 파이핑을 할 수 있다.
stream 모듈의 pipeline 메서드를 사용해 여러 개의 파이프를 연결하는 방법도 있다.
import { pipeline } from 'stream/promises';
import zlib from 'zlib';
import fs from 'fs';
const ac = new AbortController();
const signal = ac.signal;
setTimeout(() => ac.abort(), 1); // 1ms 뒤에 중단
await pipeline(
fs.createReadStream('./readme4.txt'),
zlib.createGzip(),
fs.createWriteStream('./readme4.txt.gz'),
{ signal },
);
pipeline 메서드를 사용하면 중간에 AbortController를 사용해 원할 때 파이프를 중단할 수 있다.
pipeline의 마지막 인수로 {signal}을 추가한다. 원하는 시점에 ac.abort()를 호출하면 중단한다.
스트림을 사용하면 효과적으로 데이터를 전송할 수 있다. 그렇기 때문에 동영상 같은 큰 파일을 전송할 때 스트림을 사용한다.
3.6.3 기타 fs 메서드
fs는 폴더를 생성하고 삭제도 가능하다.
const fs = require('fs').promises;
const constants = require('fs').constants;
fs.access('./folder', constants.F_OK | constants.W_OK | constants.R_OK)
.then(() => {
return Promise.reject('이미 폴더 있음');
})
.catch((err) => {
if (err.code === 'ENOENT') {
console.log('폴더 없음');
return fs.mkdir('./folder');
}
return Promise.reject(err);
})
.then(() => {
console.log('폴더 만들기 성공');
return fs.open('./folder/file.js', 'w');
})
.then((fd) => {
console.log('빈 파일 만들기 성공', fd);
return fs.rename('./folder/file.js', './folder/newfile.js');
})
.then(() => {
console.log('이름 바꾸기 성공');
})
.catch((err) => {
console.error(err);
});
/* 실행 결과
$ node fsCreate
폴더 없음
폴더 만들기 성공
빈 파일 만들기 성공 3
이름 바꾸기 성공
$ node fsCreate
이미 폴더 있음
*/
fs.access(경로, 옵션, 콜백): 폴더나 파일에 접근할 수 있는지를 체크한다. F_OK는 파일 존재 여부, R_OK는 읽기 권한 여부, W_OK는 쓰기 권한 여부를 체크한다. 파일/폴더나 권한이 없다면 에러가 발생하는데, 파일/폴더가 없을 때의 에러 코드는 ENOENT이다.
fs.mkdir(경로, 콜백): 폴더를 만드는 메서드이다. 이미 폴더가 있다면 에러가 발생하므로 먼저 access 메서드를 호출해서 확인하는 것이 중요하다.
fs.open(경로, 옵션, 콜백): 파일의 아이디(fd 변수)를 가져오는 메서드이다. 파일이 없다면 파일을 생성한 뒤 그 아이디를 가져온다. 가져온 아이디를 사용해 fs.read 또는 fs.write로 읽거나 쓸 수 있다. 두 번째 인수로 어떤 동작을 할 것인지를 설정할 수 있다. 쓰려면 w, 읽으려면 r, 기존 파일에 추가하려면 a.
fs.rename(기존 경로, 새 경로, 콜백): 파일의 이름을 바꾸는 메서드이다. 기존 파일 위치와 새로운 파일 위치를 적으면 된다. 꼭 같은 폴더를 지정할 필요는 없으므로 잘라내기 같은 기능을 할 수도 있다.
폴더 삭제와 관련된 메서드
const fs = require('fs').promises;
fs.readdir('./folder')
.then((dir) => {
console.log('폴더 내용 확인', dir);
return fs.unlink('./folder/newfile.js');
})
.then(() => {
console.log('파일 삭제 성공');
return fs.rmdir('./folder');
})
.then(() => {
console.log('폴더 삭제 성공');
})
.catch((err) => {
console.error(err);
});
fs.readdir(경로, 콜백): 폴더 안의 내용물을 확인할 수 있다.
fs.unlink(경로, 콜백): 파일을 지울 수 있다.
fs.rmdir(경로, 콜백): 폴더를 지울 수 있다.
파일/폴더의 변경 사항을 감시하는 메서드
const fs = require('fs');
fs.watch('./target.txt', (eventType, filename) => {
console.log(eventType, filename);
});
/*
내용물 수정 후
change target.txt
change target.txt
파일명 변경 또는 파일 삭제 후
rename target.txt
*/
내용물을 수정할 때는 change 이벤트가 발생하고 파일명을 변경하거나 파일을 삭제하면 reanme ㅁ이벤트가 발생한다.
rename 이벤트가 발생한 후에는 더 이상 watch가 수행되지 않는다.
'Programming > NodeJS' 카테고리의 다른 글
Node.js 교과서 [요청과 응답] #4 (1) | 2024.02.04 |
---|---|
Node.js 교과서 [이벤트, 예외 처리] #3.4 (0) | 2024.02.04 |
Node.js 교과서 [노드 내장 모듈] #3.2 (0) | 2024.02.04 |
Node.js 교과서 [REPL, 모듈, 노드 내장 객체] #3 (1) | 2024.02.04 |
Node.js 교과서 [Front-end] #2.2 (0) | 2024.02.02 |