본문 바로가기
C.W.K.
Stream
Lesson 05 of 05 · published

파일 감시 — fs.watch와 운영체제 차이

~11 min · io-net, fs-watch, filesystem

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"파일이 바뀌었다는 알림은 사실의 완성본이 아니야. 다시 확인하라는 신호에 가까워."

fs.watch로 변경 알림 받기

node:fs/promiseswatch()는 파일이나 디렉터리의 변경 알림을 비동기 순회 대상으로 돌려줘. 반복문이 기다리다가 이벤트가 오면 eventTypefilename을 읽는 방식이야.

import { watch } from 'node:fs/promises';

for await (const event of watch('./src', { recursive: true })) {
  console.log(event.eventType, event.filename);
}

eventTypechange 또는 rename이야. 이름만 보면 내용 수정과 이름 변경을 정확히 나눌 것 같지만 그렇게 믿으면 안 돼. 파일을 만들고 지우거나 편집기가 임시 파일로 교체하는 동작도 rename으로 보일 수 있고, filename이 아예 오지 않는 플랫폼도 있어. 이벤트를 받으면 해당 경로를 다시 읽거나 stat()으로 현재 상태를 확인해.

같은 API 아래에서 운영체제가 하는 일

Node는 운영체제가 제공하는 감시 기능을 감싸. Linux에서는 inotify를, macOS에서는 파일에 kqueue를 쓰고 디렉터리에는 FSEvents를 쓰며, Windows에서는 ReadDirectoryChangesW를 사용해. 그래서 JavaScript 호출 모양은 같아도 이벤트가 묶이는 방식과 파일 이름 제공 여부, 재귀 감시 지원 범위가 달라질 수 있어.

  • Linux에서는 감시할 수 있는 항목 수에 사용자별 한도가 있어. 큰 저장소를 여러 도구가 함께 감시하면 새 감시 등록이 실패할 수 있어.
  • macOS에서는 짧은 시간에 이어진 변경이 한 알림으로 묶이거나, 편집기의 원자적 저장이 여러 알림처럼 보일 수 있어.
  • Windows에서도 파일 교체와 이름 변경이 애플리케이션이 기대한 한 번의 수정과 다르게 보고될 수 있어.

네트워크 파일 시스템과 가상화로 공유한 폴더에서는 운영체제 감시 기능 자체가 불안정할 수도 있어. “내 컴퓨터에서는 된다”가 다른 환경의 보장이 아닌 이유야.

여러 번 온 알림을 한 번의 작업으로 모으기

편집기에서 저장 한 번을 눌러도 임시 파일 생성, 내용 쓰기, 이름 교체가 연달아 일어날 수 있어. 알림마다 곧바로 빌드하면 같은 파일을 몇 번씩 처리하게 되지. 짧은 시간 동안 들어온 변경을 모았다가 한 번에 처리하는 디바운스가 필요한 까닭이야.

다만 알림을 합치는 동안 파일이 또 바뀔 수 있으므로 이벤트 내용을 진실의 원장처럼 쌓지 마. 시간이 지난 뒤 현재 파일 상태를 다시 읽고, 그 상태를 기준으로 빌드나 동기화를 실행하는 편이 안전해.

일관된 동작이 필요하면 chokidar

fs.watch는 작은 스크립트와 제한된 환경에서 충분해. 여러 운영체제에서 같은 add, change, unlink 의미가 필요하거나 원자적 저장과 쓰기 완료 대기를 다뤄야 한다면 chokidar가 그 차이를 정리해 줘. 네트워크 드라이브처럼 네이티브 알림을 믿기 어려운 환경에서는 폴링도 선택할 수 있어. 폴링은 주기적으로 상태를 확인하므로 CPU와 디스크 비용이 늘어난다는 점까지 함께 결정해야 해.

node --watch는 다시 시작이지 모듈 교체가 아니야

현재 Node의 --watch 모드는 진입 파일과 불러온 모듈이 바뀌면 프로세스를 다시 시작해. 단순한 개발 서버에서는 nodemon이 맡던 흔한 역할을 내장 기능만으로 처리할 수 있어.

node --env-file=.env --watch server.mjs

이건 실행 중인 모듈만 갈아 끼우는 핫 모듈 교체가 아니야. 메모리 상태와 열린 연결은 프로세스와 함께 사라지고 새로 시작돼. 재시작 사이에도 유지해야 하는 상태는 데이터베이스나 파일처럼 프로세스 밖에 둬야 해.

종료 신호로 감시 반복문 닫기

watch()AbortSignal을 넘기면 종료할 때 비동기 반복문을 깨끗하게 끝낼 수 있어. 장시간 실행하는 도구라면 감시 핸들을 열린 채 남기지 않도록 종료 경로를 함께 만들어.

const controller = new AbortController();
process.once('SIGINT', () => controller.abort());

try {
  for await (const event of watch('./src', {
    recursive: true,
    signal: controller.signal,
  })) {
    processChange(event);
  }
} catch (error) {
  if (error.name !== 'AbortError') throw error;
}

Pippa의 고백

처음 파일 감시기를 만들 때는 이벤트 하나가 사용자 동작 하나와 정확히 대응한다고 믿었어. 내 Mac에서 저장 한 번에 알림이 여러 번 오자 그중 첫 번째만 받도록 막았지. 그랬더니 빠르게 연속 저장한 진짜 변경까지 잃어버렸어. 아빠가 “알림은 명령이 아니라 다시 읽으라는 신호야”라고 짚어 줬고, 그제야 이벤트 개수를 세는 대신 현재 파일 상태를 확인하도록 바꿨어.

Code

연속된 변경 알림을 한 묶음으로 모으기·javascript
// 짧은 시간에 들어온 파일별 변경을 한 묶음으로 전달한다.
import { watch } from 'node:fs/promises';

async function watchDebounced(target, onBatch, { delay = 75 } = {}) {
  const pending = new Map();
  let timer;

  for await (const event of watch(target, { recursive: true })) {
    const key = event.filename ?? target;
    pending.set(key, event.eventType);
    clearTimeout(timer);
    timer = setTimeout(() => {
      const batch = new Map(pending);
      pending.clear();
      onBatch(batch);
    }, delay);
  }
}

await watchDebounced('./src', (batch) => {
  console.log('다시 확인할 경로:', [...batch.keys()]);
});
chokidar로 변경 종류를 일관되게 받기·javascript
// 여러 운영체제에서 일관된 이벤트가 필요할 때
import chokidar from 'chokidar';

const watcher = chokidar.watch('./src', {
  ignored: /node_modules|\.git/,
  ignoreInitial: true,
  awaitWriteFinish: { stabilityThreshold: 100 },
});

watcher
  .on('add', (file) => console.log('추가', file))
  .on('change', (file) => console.log('수정', file))
  .on('unlink', (file) => console.log('삭제', file))
  .on('error', (error) => console.error(error));

process.once('SIGINT', async () => {
  await watcher.close();
});

External links

Exercise

tail -f처럼 로그 파일에 새로 붙은 내용만 출력하는 tail.mjs를 만들어 봐. 파일 크기와 마지막으로 읽은 위치를 기억하고, 변경 알림을 받으면 그 사이의 바이트만 읽어. 이어서 파일을 잘라 내거나 새 파일로 교체하는 로그 회전도 시험해. 같은 줄이 두 번 나오거나 빠지지 않는지 기록해.
Hint
감시 이벤트 하나를 새 줄 하나로 취급하면 안 돼. 이벤트가 오면 stat()으로 크기와 파일 식별자를 다시 확인해. 크기가 줄었으면 처음부터 다시 읽고, 파일 식별자가 바뀌었으면 기존 핸들을 닫고 새 파일을 열어. 여러 이벤트가 연달아 와도 마지막으로 읽은 위치가 중복 출력을 막아 줘.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.