파일 시스템(fs)과 비동기 I/O
파일 시스템(fs)과 비동기 I/O
파일을 읽고 쓰는 건 Node.js가 가장 자주 하는 비동기 작업 중 하나예요. node:fs 모듈은 표준 POSIX 함수 패턴을 본떠서 파일 시스템과 상호작용하게 해 주고요. 같은 작업을 동기, 콜백, Promise의 세 가지 방식으로 쓸 수 있어서, 상황에 맞게 고르면 돼요. 이 글에서는 각 방식의 차이와, Promise API를 쓸 때 주의할 점을 정리해 볼게요.
출처: File system — Node.js 공식 문서
세 가지 방식
node:fs는 거의 모든 파일 시스템 작업에 대해 동기·콜백·Promise 세 가지 형태를 제공해요. CommonJS와 ESM 어느 쪽이든 쓸 수 있고요.
Promise 방식은 작업이 끝날 때 fulfilled되는 Promise를 반환해요. try/catch로 예외를 잡을 수 있지요.
import { unlink } from 'node:fs/promises';
try {
await unlink('/tmp/hello');
console.log('successfully deleted /tmp/hello');
} catch (error) {
console.error('there was an error:', error.message);
}
콜백 방식은 마지막 인자로 완료 콜백을 받아요. 콜백의 첫 번째 인자는 항상 예외용으로 예약돼 있어서, 성공하면 null 또는 undefined가 오고 실패하면 에러가 와요.
import { unlink } from 'node:fs';
unlink('/tmp/hello', (err) => {
if (err) throw err;
console.log('successfully deleted /tmp/hello');
});
동기 방식은 파일 작업이 끝날 때까지 이벤트 루프와 이후 자바스크립트 실행을 막아요. 에러는 즉시 throw되니까 try/catch로 잡거나 상위로 전파되게 두면 돼요.
성능(실행 시간·메모리 할당)이 최우선인 상황이라면 콜백 기반 API가 Promise API보다 낫다고 문서도 강조해요.
Promise API와 스레드풀
fs/promises API는 이벤트 루프 스레드 밖, Node.js의 스레드풀을 이용해 파일 작업을 수행해요. 이 작업들은 동기화되지도 스레드 안전하지도 않아요. 같은 파일을 여러 번 동시에 수정하면 데이터가 깨질 수 있으니 주의해야 해요.
FileHandle
fsPromises.open()은 파일 디스크립터를 감싸는 FileHandle 객체를 만들어요. FileHandle은 EventEmitter 인스턴스이고요. FileHandle을 만들었으면 반드시 filehandle.close()로 닫아야 해요. 직접 닫지 않으면 자동으로 닫으려 시도하면서 프로세스 경고를 내기도 하므로, 메모리 누수를 막으려면 항상 명시적으로 닫는 게 좋아요. 'close' 이벤트는 파일 핸들이 닫혀서 더 못 쓸 때 발생해요.
import { open } from 'node:fs/promises';
let filehandle;
try {
filehandle = await open('thefile.txt', 'r');
} finally {
await filehandle?.close();
}
FileHandle에서 createReadStream()·createWriteStream()으로 스트림을 만들 수도 있어요. start/end 값을 주면 파일 전체가 아니라 특정 바이트 구간만 읽을 수 있고요. autoClose 기본값은 true라서 'error'나 'end'가 나면 파일 디스크립터가 자동으로 닫혀요.
더 알아보기
- 파일을 스트림으로 읽고 쓰기 — Stream
- 파일 데이터를 담는 버퍼 — Buffer
- fs의 각 메서드 상세 참조 — File system (fs)