Menu

Node.js fs/promises: async/await 파일 시스템 API

fs/promises는 Node.js fs 모듈의 Promise 버전입니다. readFile, writeFile, readdir, stat, mkdir, rm, rename이 모두 await할 수 있는 Promise를 반환하고, 에러는 try/catch로 잡습니다.

이 페이지에는 실행 가능한 에디터가 있습니다. 편집하고 실행하면 결과를 바로 볼 수 있습니다.

fs/promises는 Node.js fs 모듈의 Promise 기반 버전입니다. 그 함수들(readFile, writeFile, readdir, stat, mkdir, rm, rename 등)은 콜백을 받는 대신 Promise를 반환하므로, await로 기다리고 에러는 try/catch로 잡습니다. require('fs/promises')나 import fs from 'fs/promises'로 가져옵니다.

[ 'write docs', 'ship it' ]이 출력됩니다. 각 await가 작업 하나를 기다린 뒤에 다음 줄이 실행되므로, 덧붙이기는 항상 첫 쓰기 뒤에 일어납니다. ES 모듈에서는 최상위에서 await가 동작하고, CommonJS에서는 코드를 async 함수로 감싸세요.

가장 많이 쓰는 함수

이름은 콜백 API와 같고 콜백만 빠집니다. 그중 두 가지는 readFile과 writeFile 페이지에서 자세히 다룹니다.

함수하는 일resolve되는 값
readFile(path, 'utf8')파일 전체 읽기내용
writeFile(path, data)파일 만들기 또는 교체undefined
appendFile(path, data)끝에 덧붙이기undefined
readdir(path)폴더 목록이름 배열
stat(path)크기, 날짜, 파일인지 폴더인지Stats 객체
mkdir(path, { recursive: true })폴더 만들기처음 만든 폴더 또는 undefined
rename(from, to)이동 또는 이름 바꾸기undefined
rm(path, { recursive: true, force: true })파일이나 폴더 삭제undefined

readdir와 stat으로 폴더 목록 보기

readdir는 이름만 반환합니다. 크기를 알거나 파일과 폴더를 구분하려면 각각에 stat을 호출하거나, { withFileTypes: true }를 넘겨 이미 자기 종류를 아는 항목을 받으세요.

반복문은 README.md 6 bytes, notes.txt 3 bytes, src folder를 출력합니다. stat은 없는 경로에 ENOENT로 reject되므로, 세부 정보도 필요할 때는 빠른 존재 확인이 됩니다.

이름 바꾸기, 이동, 삭제

rename은 (같은 드라이브 안에서) 이름 바꾸기와 이동을 모두 합니다. rm은 삭제하는데, 내용이 있는 폴더에는 recursive: true가, 경로가 이미 없을 때 실패하지 않으려면 force: true가 필요합니다.

파일이 새 이름으로 drafts에서 published로 옮겨지고, rm 뒤에 blog 폴더에는 published만 남습니다. 두 번째 rm에서 force: true를 빼면 ENOENT로 reject됩니다.

try/catch로 에러 처리하기

실패한 작업은 Promise를 reject합니다. await를 쓰면 그 reject가 다른 예외처럼 잡을 수 있는 예외가 됩니다. 무엇이 잘못됐는지는 에러의 code가 알려 줍니다.

첫 호출은 파일을 찾지 못해 []를 반환하고, 두 번째는 Ada를 출력합니다. 알아 둘 코드는 ENOENT(파일 없음), EEXIST(이미 있음), EISDIR(파일이어야 할 곳에 폴더), ENOTEMPTY(폴더에 내용이 남아 있음), EACCES(권한 없음)입니다.

await도 .catch()도 하지 않은 Promise가 실패하면 처리되지 않은 reject가 되고, Node.js는 에러와 함께 종료됩니다. 모든 호출을 await하거나, 그것을 처리하는 호출자에게 Promise를 반환하세요.

Promise.all로 여러 파일 한 번에 읽기

await를 쓴 for 반복문은 파일을 하나씩 차례로 읽습니다. 읽기끼리 서로 의존하지 않는다면 모두 시작하고 Promise.all로 한 번만 기다리세요. 디스크가 그것들을 동시에 처리합니다.

Promise.all은 입력 배열의 순서를 유지하므로, 어느 읽기가 먼저 끝나든 결과는 [ 'alpha', 'beta', 'gamma' ]이고 합친 길이는 14입니다. 배열의 'b.txt'를 'x.txt'로 바꾸면 await 전체가 ENOENT로 reject됩니다.

일부 읽기가 실패해도 모든 결과가 필요하다면 Promise.allSettled를 쓰세요. 둘 다 Promise 페이지에서 다룹니다.

흔한 실수

두 모듈을 헷갈리는 것. require('fs').readFile은 콜백이 필요하고, require('fs/promises').readFile은 Promise를 반환합니다. 콜백 버전을 콜백 없이 호출하면 The "cb" argument must be of type function이 발생합니다.

await를 잊는 것. const text = fs.readFile(...)은 대기 중인 Promise이고, text.split은 text.split is not a function을 던집니다.

한 파일에 병렬로 쓰는 것. 같은 경로에 대한 쓰기 여러 개를 Promise.all로 돌리면 내용이 섞일 수 있습니다. 그런 쓰기는 차례로 실행하세요.

자주 묻는 질문

fs.promises와 fs/promises는 무엇이 다른가요?

다르지 않습니다. require('fs').promises와 require('fs/promises')는 같은 객체입니다. 요즘은 fs/promises(또는 node:fs/promises) 경로로 가져오는 것이 일반적입니다.

fs/promises에 existsSync가 있나요?

없습니다. Promise API에는 경로가 없으면 reject되는 access가 있지만, 보통은 필요한 작업을 바로 호출하고 ENOENT를 잡는 방식을 씁니다. 간단한 확인이라면 일반 fs 모듈의 existsSync도 여전히 쓸 수 있습니다.

fs/promises는 콜백 API보다 느린가요?

Promise 오버헤드 때문에 아주 조금 느리지만, 디스크에 걸리는 시간에 비하면 무시할 만합니다. 가독성으로 고르세요. 새 코드는 대부분 Promise API를 씁니다.

콜백 방식의 fs 함수를 Promise로 바꾸려면?

파일 함수는 fs/promises에 다 있으므로 그럴 일은 드뭅니다. 다른 콜백 API라면 util.promisify(fn)이 Promise를 반환하는 버전을 만들어 줍니다.

Coddy 프로그래밍 언어 일러스트

Coddy로 코딩 배우기

시작하기