Node.js 교재 · 6편 · 파일 다루기

fs 모듈 — 파일 읽기·쓰기, 동기 vs 비동기

브라우저가 막아둔 영역. Node 가 가장 빛나는 도구 fs 를 손에 익히자.

동기와 비동기 파일 I/O 가 나란히 흐르는 컨셉 아이소메트릭 일러스트

드디어 Node 의 진짜 무대 — 파일 시스템. 브라우저 자바스크립트는 보안 때문에 파일에 접근 못 한다. Node 는 막힘이 없다. 그래서 자동화 스크립트·정적 사이트 빌더·CLI 도구가 다 Node 로 만들어졌다.

관문은 fs 모듈 한 가지. 이 모듈은 같은 동작에 대해 3가지 API 를 제공한다. 동기·비동기 콜백·Promise. 어느 걸 골라야 하는지 헷갈리니, 이번 편에서 정리.

1. 3가지 API — 같은 동작 세 가지 모양

같은 "파일 읽기" 동작이 세 가지 형태로 존재한다.

import fs from 'node:fs'; import fsp from 'node:fs/promises'; // ① 동기 (Sync) const data = fs.readFileSync('./hello.txt', 'utf-8'); // ② 비동기 콜백 (옛 방식) fs.readFile('./hello.txt', 'utf-8', (err, data) => { if (err) throw err; console.log(data); }); // ③ Promise (현재 권장) const data = await fsp.readFile('./hello.txt', 'utf-8');

결과는 셋 다 같지만 — 이벤트 루프를 어떻게 다루느냐가 다르다. 그래서 어떤 API 를 쓰는가가 서버 성능을 좌우한다.

API패키지동작권장
동기node:fs...Sync호출 즉시 막힘. 끝날 때까지 다른 일 X스크립트·CLI 시작·설정 읽기
콜백node:fs (Sync 없는 함수)백그라운드 시작 + 끝나면 콜백 호출레거시. 신규는 비추
Promisenode:fs/promisesawait 로 받음. 콜백과 같은 비동기기본. 모든 신규 코드

2. 가장 많이 쓰는 메서드 6가지

실전에서 99% 를 차지하는 함수 6개. node:fs/promises 기반으로 보면 된다.

import fs from 'node:fs/promises'; import path from 'node:path'; // 읽기 const text = await fs.readFile('./hello.txt', 'utf-8'); const bin = await fs.readFile('./image.png'); // 인코딩 없으면 Buffer // 쓰기 (덮어쓰기) await fs.writeFile('./out.txt', '안녕\n', 'utf-8'); // 이어쓰기 (append) await fs.appendFile('./log.txt', `${new Date().toISOString()}\n`); // 존재 확인 + 메타 (없으면 throw) const stat = await fs.stat('./out.txt'); console.log(stat.size, stat.mtime); // 디렉토리 만들기 (재귀) await fs.mkdir('./build/css', { recursive: true }); // 디렉토리 목록 const files = await fs.readdir('./src'); console.log(files); // ['index.js', 'utils.js', ...]

모든 함수는 실패하면 throw. try/catch 또는 .catch() 로 감싸자. 콜백 API 의 (err, data) => … 형태는 옛날 방식.

인코딩 옵션readFile 두 번째 인자가 인코딩. 'utf-8' 주면 문자열, 안 주면 Buffer (바이너리). 이미지·PDF 같은 바이너리는 인코딩 없이 받아 그대로 다른 함수에 전달.

3. 동기 API 는 언제 쓰나 — 두 가지 케이스만

비동기가 기본이지만, 동기 (...Sync) 가 옳은 경우 두 가지.

① 프로세스 시작 시 설정 읽기

서버가 일을 시작하기 에 필요한 설정. 어차피 이게 끝나야 다음이 의미가 있다.

// server-start.js import fs from 'node:fs'; const config = JSON.parse( fs.readFileSync('./config.json', 'utf-8') ); const app = createApp(config); app.listen(config.port);

여기서 비동기를 쓰면 코드 흐름만 복잡해지고 얻는 게 없다. 어차피 서버는 설정 없이 시작 못 함.

② 짧은 CLI 스크립트

한 번 돌고 끝나는 자동화 스크립트. 동시 요청이 없으므로 막혀도 손해 없음.

// rename-all.js — 디렉토리 안 파일 이름 일괄 변경 import fs from 'node:fs'; import path from 'node:path'; const dir = process.argv[2]; const files = fs.readdirSync(dir); for (const f of files) { const ext = path.extname(f); const base = path.basename(f, ext); fs.renameSync( path.join(dir, f), path.join(dir, `${base}-new${ext}`) ); }

이런 1회성 스크립트는 동기가 읽기 쉬워서 더 낫다. await 줄줄이 나열할 필요 없음.

4. 서버에선 절대 동기를 쓰지 마라

가장 큰 함정. HTTP 서버의 요청 핸들러 안에서 readFileSync 를 호출하면 어떤 일이?

실제 영향 — Node 는 단일 스레드 이벤트 루프. readFileSync 가 10ms 걸리면 그 10ms 동안 다른 모든 요청이 멈춘다. 1000 RPS 부하라면 누적 지연이 폭발한다. 옛 Pages Router 시절 getServerSideProps 안에 동기 fs 박은 사례가 가장 흔한 사고. 이래서 비동기가 기본인 것.

서버 코드에선 무조건 node:fs/promisesawait 형태. 의식하지 않아도 되도록 처음부터 fs/promises 만 import 하는 습관을 들이자.

5. 흔한 패턴 — 존재 확인 + 안전한 쓰기

실전에서 자주 만나는 두 패턴.

존재하면 읽고 없으면 생성

import fs from 'node:fs/promises'; async function readOrInit(file, defaults) { try { return JSON.parse(await fs.readFile(file, 'utf-8')); } catch (e) { if (e.code === 'ENOENT') { await fs.writeFile(file, JSON.stringify(defaults, null, 2)); return defaults; } throw e; } } const settings = await readOrInit('./settings.json', { theme: 'dark' });

e.code === 'ENOENT' 가 "파일 없음" 의 표준 에러 코드. fs.exists 같은 별도 함수 쓰지 말고 try/catch 패턴이 표준이다 (exists 는 race condition 이 있어 deprecated).

임시 파일 → 본 파일 교체 (원자적 쓰기)

async function atomicWrite(file, data) { const tmp = `${file}.tmp.${Date.now()}`; await fs.writeFile(tmp, data, 'utf-8'); await fs.rename(tmp, file); // 원자적 교체 }

쓰기 도중 프로세스가 죽어도 원본은 안전. 데이터 저장이 중요한 경우 표준 패턴.

요약 — 6편 좌표

여기까지 정리. fs 는 3가지 API — 동기(...Sync) · 콜백 · Promise. 현재 표준은 node:fs/promises + await. 동기는 프로세스 시작 설정과 1회성 스크립트에만, 서버 핸들러엔 절대 금지. 자주 쓰는 메서드는 readFile·writeFile·appendFile·stat·mkdir·readdir 6개. 존재 확인은 try/catch + ENOENT 패턴, 중요 데이터는 임시 파일 → rename 원자적 쓰기. 다음 편에선 path 와 os 모듈로 경로 다루기.

다음 편 예고 — path와 os 모듈

크로스 플랫폼 경로 다루기와 시스템 정보 읽기. 7편.

© 2026 주나이테크(주) @JUNAITECH