[Node.js] 파일 관리 - path, File System 모듈
*Node.js 프로그래밍 입문(고경희)를 읽고 작성한 글입니다.
버퍼와 스트림 개념 이해하기
백엔드 프로그래밍에서 서버와 클라이언트 간 데이터 전송을 효율적으로 하기 위해선 데이터 전체를 한 뭉탱이로 가져오는 것보다 잘게 쪼개서 여러 번에 걸쳐 가져오는 것이 효율적이다. 이런 개념을 버퍼라고 한다. 버퍼를 이용한 데이터 읽고 쓰는 처리 개념이 스트림이다. 그리고 스트림 방식은 pipe를 통해 하나의 처리 흐름으로 조정할 수 있다.
pipe는 data 이벤트가 발생했을 때 가져오고 기록하고, 발생하면 또 가져오고 기록하는 과정을 한 꺼번에 처리하도록 연결해준다.
const fs = require('fs');
const readStream = fs.createReadStream('./readMe.txt', 'utf8');
const writeStream = fs.createWriteStream('./writeMe.txt', 'utf8')
readStream.on('data', (chunk) => {
console.log("new chunk received");
writeStream.write(chunk);
});
- chunk는 데이터를 읽어오는 작은 단위를 말한다. stream에서 받는 chunk는 보통 Node.js의 Buffer 객체라는데 동일한 크기인지는 더 찾아봐야겠다.
- data는 readable stream의 이벤트로 데이터를 읽을 수 있을 때마다 발생한다.
- utf-8은 encoding 값으로, 반환값을 어떤 형태로 받아올 것이냐를 말한다. utf-8은 사람이 읽을 수 있는 텍스트 형태다.
- 버퍼에 있는 값은 이진값이라 00 4e 52 64 이렇게 떠서 인간이 이해하기가 어렵다. 버퍼는 data.toString()으로 변환해 읽을 수 있다.
위 코드는 data가 있는 한 계속 진행되는데 - 데이터를 받아오다가 버퍼가 꽉 차면 비우고 console에 new chunk received가 뜨고 writeStream을 한다. 아직 data가 남아있다면 위 과정을 반복한다.
data를 받고 - 쓰고 , 받고 -쓰고... 하지 않고 받아서 전달 - 작성하는, 하나의 흐름으로 만들기 위해서 pipe를 사용한다.
readStream.pipe(writeStream);
- readable stream에서 데이터를 읽어온다.
- 읽은 데이터를 writable stream으로 기록한다.
- writable stream에 다 기록할 때까지 readable stream에서 읽고 쓰기를 반복한다.
- 읽을 데이터/ 쓸 데이터가 없으면 pipe 함수가 자동 종료된다.
readStream.pipe(writeStream);과 duplex stream과의 차이는?
pipe()는 단방향으로 두 개의 스트림을 연결해 읽고 -> 쓰기가 가능하도록 하는 함수다. 주로 데이터 전달을 용도로 사용된다.
duplex stream은 양방향 통신에 사용되며 하나의 스트림 객체로써 데이터를 받아서 보내는 상황에 사용된다.
- 네트워크 소켓
- Transform stream - 압축, 인코딩
fs.writeFile flags - 기존 값을 지우는 게 기본값
fs.writeFile 는 파일이 존재하면 기존 내용을 삭제하고 새로운 내용을 추가한다. 파일이 없다면 생성한다. 그런데 기존 내용을 삭제하지 말고 그 밑에 붙여야 하는 경우도 있으니 flag 옵션으로 지우지 말라고 말해주자
flag값 | 설명 |
a | append : 내용 추가를 위해 파일을 연다. 파일이 없으면 만든다. |
ax | a와 동일하지만 파일이 이미 있으면 실패한다. |
a+ | 파일을 읽고 내용을 추가하기 위해 파일을 연다. 파일이 없으면 만든다. |
ax+ | a+와 동일하지만 파일이 없으면 실패한다. |
as | sync : 동기 처리로 내용을 추가하기 위해 파일을 연다. |
w | (덮어)쓰기 위해 파일을 연다. 파일이 없으면 만든다. |
wx | |
w+ | 내용을 읽고 쓰기 위해 파일을 연다. 파일이 없으면 만든다. |
wx+ |
w+ 로 내용을 읽으면 duplex 느낌인가? fs.readFile 대신 write으로 한 번에 읽고 써도 되나?
안된다. writeFile은 기본적으로 쓰기 전용 함수이고, 그 내부에서 readFile로 읽기도 허용하게 해주겠다는 의미
const fs = require('fs');
fs.open('example.txt', 'w+', (err, fd) => {
if (err) throw err;
// 파일에 쓰기
fs.write(fd, 'Hello, world!', (err) => {
if (err) throw err;
// 파일 읽기 (버퍼 필요)
const buffer = Buffer.alloc(20); // 읽을 공간
fs.read(fd, buffer, 0, 20, 0, (err, bytesRead, buf) => {
if (err) throw err;
console.log('읽은 내용:', buf.toString('utf8', 0, bytesRead));
fs.close(fd, () => {});
});
});
});
flag 대신 fs.appendFile(파일, 내용, [옵션], 콜백) 으로 기존 파일에 내용 추가도 가능하다.
Write로 추가할 내용 주의할 점
` ` template literals은 소스에 입력하는 공백이나 줄바꿈이 그대로 적용되지만, 일반 문자열에서는 이스케이프 문자 \n을 사용해 줄바꿈을 지정해야 한다.
const fs = ('fs');
let content = `
.*"*..*"*.
'*.*♧*.*'
.*"*♣*"*.
'*.*][*.*'
네잎클로버의행운..♡
`;
fs.appendFile("./text.txt", "\n\n hello world!", (err) => {
if(err){
console.log(err);
}
fs.writeFile('./text.txt', content, {flag: 'a'});
});
fs.writeFile( file, content, [options], callback ) 옵션
사용할 수 있는 옵션들 flag, encoding, mode, signal 중 mode, signal은 프로그램 운영 및 관리에 중요한 옵션들이라 추가로 정리해보려 한다.
- mode : 파일 권한 지정으로 read, write, execute 등에 대한 권한을 8진수로 read(4), write(2), exe(1)로 표현해 지정한다.
- signal : 파일 내용 기록 시 시간이 오래 걸리거나 interrupt 발생 등의 상황에 중간에 파일 쓰기를 취소할 수 있는 옵션이다.
callback: writeFile, appenFile의 콜백은 err를 매개변수로 사용한다.
지금까지는 비동기/동기 구분 없이 함수를 글을 썼는데 프로그래밍하기 전에 동기처리와 비동기 처리 중 어떤 것을 사용해 프로그램을 만들지 결정하고 그에 맞는 fs모듈을 사용해줘야 한다. 비동기에서 실행 순서 조절하는 방법으로 callback, promise를 이용할 수 있다.
동기 처리는 대부분의 비동기 fs모듈 뒤에 Sync를 붙여주면 된다.
- fs.existsSync는 비동기 함수를 더 이상 사용하지 않아 existsSync만 이용한다.