node:vfs — Node.js에 가상 파일 시스템이 필요한 이유

11 min read
Node.jsVFSnode:vfsPlatformatic파일 시스템
node:vfs — Node.js에 가상 파일 시스템이 필요한 이유

Node.js에서 파일 시스템을 모킹해본 적이 있다면, 그 고통을 안다. memfsfs 모듈을 패치하면 파일 읽기/쓰기는 가상으로 돌아가지만, require()import()로 모듈을 불러오는 순간 진짜 파일 시스템을 뒤진다. 모듈 리졸버가 패치된 fs를 무시하기 때문이다. Matteo Collina가 14,000줄짜리 PR로 node:vfs 모듈을 Node.js 코어에 제안한 이유가 여기에 있다.


node:vfs가 해결하려는 4가지 문제

비유하면 Node.js의 파일 시스템은 건물의 배관 같다. 벽 밖에서 보이는 수도꼭지(fs 모듈)만 교체하면 될 것 같지만, 실제로는 벽 안쪽 배관(모듈 리졸버, 네이티브 모듈)까지 전부 연결되어 있어서 하나만 바꿀 수가 없다.

문제설명현재 상태
SEA 에셋 배포Single Executable App에 정적 파일을 포함시키기 어려움별도 번들링 필요
테스트 격리fs 모킹이 모듈 리졸버에 적용되지 않음memfs로 불완전 패치
멀티테넌트 샌드박싱여러 사용자 코드를 격리된 환경에서 실행컨테이너 수준 격리 필요
런타임 코드 생성AI 에이전트가 생성한 코드를 안전하게 실행임시 파일 작성 후 삭제

핵심 — 왜 npm 패키지로는 안 되는가

memfsunionfs 같은 기존 도구가 있지 않냐고 물을 수 있다. 문제는 이것들이 fs API만 감싸는 얇은 래퍼라는 점이다. Node.js의 모듈 시스템(require, import)은 내부적으로 C++ 레벨에서 파일을 읽는다. 사용자 영역의 패치가 닿지 않는 곳이다.

[💡 잠깐! 이 용어는?] SEA(Single Executable Application): Node.js 애플리케이션을 하나의 실행 파일로 패키징하는 기능이다. 별도의 Node.js 설치 없이 바이너리 하나로 배포할 수 있다.


아키텍처 — 3개 레이어로 구성된다

node:vfs는 세 개의 레이어로 설계되어 있다. 비유하면 컴퓨터의 파일 시스템 계층과 비슷하다. 물리 디스크(Provider) → 마운트 포인트(Mounting) → 오버레이(Overlay)로 추상화가 올라간다.

1. Provider 레이어 — 데이터의 원천

파일 데이터가 실제로 어디에 저장되어 있는지를 결정하는 계층이다.

Provider저장소용도
MemoryProvider메모리(Map)테스트, 임시 파일
SEAProviderSEA 바이너리 내부단일 실행 파일 배포
VirtualProviderJavaScript 객체동적 코드 생성
SqliteProviderSQLite DB영속적 가상 FS
RealFSProvider실제 디스크 (샌드박싱)경로 제한된 실제 파일 접근

2. Mounting 레이어 — 경로에 붙이기

리눅스에서 USB를 /mnt/usb에 마운트하는 것과 같다. 가상 파일 시스템을 특정 경로에 연결한다.

vfs-mount-example.js
import { mount, unmount, MemoryProvider } from 'node:vfs'
 
const provider = new MemoryProvider()
 
provider.writeFileSync('/hello.js', `
  export function greet(name) {
    return 'Hello, ' + name
  }
`)
 
mount('/virtual', provider)
 
const { greet } = await import('/virtual/hello.js')
console.log(greet('Node.js')) // "Hello, Node.js"
 
unmount('/virtual')

이 코드에서 /virtual/hello.js는 디스크에 존재하지 않는다. 메모리에만 있다. 그런데 import()가 정상 동작한다. 이것이 코어 모듈이어야만 가능한 이유다. 모듈 리졸버가 마운트된 VFS를 인식하기 때문이다.

3. Overlay 모드 — 실제 FS 위에 덮어씌우기

오버레이 모드는 실제 파일 시스템 위에 가상 레이어를 얹는다. 가상 레이어에 파일이 있으면 그걸 반환하고, 없으면 실제 파일 시스템으로 폴스루한다.

vfs-overlay-example.js
import { mount, MemoryProvider } from 'node:vfs'
 
const overlay = new MemoryProvider()
overlay.writeFileSync('/app/config.json', JSON.stringify({
  database: 'test-db',
  debug: true
}))
 
mount('/app', overlay, { overlay: true })
 
// /app/config.json → 가상 파일 반환 (오버레이)
// /app/index.js → 실제 디스크 파일 반환 (폴스루)

비유하면 투명 필름에 수정 사항을 적어서 원본 문서 위에 올려놓는 것이다. 수정한 부분만 필름에서 읽고, 나머지는 원본을 그대로 본다.


코어에 들어가야 하는 5가지 이유

이유설명
모듈 해석require()/import()가 VFS를 인식하려면 C++ 레벨 통합 필요
비공개 API모듈 캐시, 내부 경로 해석 등 공개되지 않은 API 접근 필요
전역 fs 패치모든 fs 호출이 VFS를 거치려면 런타임 수준 후킹 필요
네이티브 모듈.node 파일(C++ 애드온)도 VFS에서 로드해야 함
모듈 캐시같은 경로의 모듈이 VFS와 실제 FS에서 충돌하지 않도록 관리

실전 패턴 — 테스트 격리

using 키워드를 활용하면 테스트 종료 시 자동으로 VFS가 정리된다. 파일 시스템 상태가 테스트 간에 누출되지 않는다.

[💡 잠깐! 이 용어는?] using 키워드: ECMAScript의 Explicit Resource Management 제안(TC39 Stage 3)이다. using으로 선언한 리소스는 블록을 벗어날 때 자동으로 [Symbol.dispose]()가 호출되어 정리된다.

vfs-testing-pattern.js
import { describe, it, assert } from 'node:test'
import { mount, MemoryProvider } from 'node:vfs'
 
describe('config loader', () => {
  it('기본 설정을 반환한다', async () => {
    const provider = new MemoryProvider()
    provider.writeFileSync('/app/config.json', JSON.stringify({
      port: 3000,
      env: 'test'
    }))
 
    using cleanup = mount('/app', provider, { overlay: true })
 
    const { loadConfig } = await import('/app/config-loader.js')
    const config = loadConfig()
 
    assert.strictEqual(config.port, 3000)
    assert.strictEqual(config.env, 'test')
    // cleanup 자동 호출 — 마운트 해제, 모듈 캐시 초기화
  })
 
  it('환경 변수 오버라이드를 적용한다', async () => {
    const provider = new MemoryProvider()
    provider.writeFileSync('/app/config.json', JSON.stringify({
      port: 8080,
      env: 'staging'
    }))
 
    using cleanup = mount('/app', provider, { overlay: true })
 
    const { loadConfig } = await import('/app/config-loader.js')
    const config = loadConfig()
 
    assert.strictEqual(config.port, 8080)
    // 이전 테스트의 상태가 전혀 남아있지 않다
  })
})

각 테스트가 독립된 가상 파일 시스템에서 실행되므로, 테스트 순서에 의존하는 문제가 원천적으로 사라진다.


실전 패턴 — AI 에이전트 코드 생성

AI 에이전트가 코드를 생성하고 실행해야 하는 시나리오에서 VFS는 샌드박스 역할을 한다. 생성된 코드가 실제 파일 시스템에 접근하지 못하도록 격리할 수 있다.

vfs-ai-agent-sandbox.js
import { mount, MemoryProvider } from 'node:vfs'
 
async function executeGeneratedCode(code) {
  const provider = new MemoryProvider()
 
  provider.writeFileSync('/sandbox/generated.js', code)
  provider.writeFileSync('/sandbox/package.json', JSON.stringify({
    name: 'sandbox',
    type: 'module'
  }))
 
  using cleanup = mount('/sandbox', provider)
 
  try {
    const module = await import('/sandbox/generated.js')
    return { success: true, result: module.default }
  } catch (error) {
    return { success: false, error: error.message }
  }
  // cleanup 자동 호출 — 가상 파일 전부 제거
}
 
const result = await executeGeneratedCode(`
  export default function calculate() {
    return Array.from({ length: 10 }, (_, i) => i * i)
  }
`)

임시 파일을 디스크에 쓰고 지우는 기존 방식과 비교하면, 디스크 I/O가 없고 정리 실패 위험도 없다. using 키워드가 블록 종료 시점에 확실하게 정리해준다.


@platformatic/vfs — 코어 머지 전의 대안

node:vfs PR이 코어에 머지되기까지는 시간이 걸린다. 그동안 Platformatic 팀이 npm 패키지로 동일한 인터페이스를 제공하고 있다. 코어 API와 최대한 호환되도록 설계되어 있어서, 나중에 전환 비용이 적다.

terminal
npm install @platformatic/vfs
platformatic-vfs-usage.js
import { mount, MemoryProvider } from '@platformatic/vfs'
 
const provider = new MemoryProvider()
provider.writeFileSync('/data/hello.txt', 'Hello from VFS')
 
mount('/data', provider)

다만 npm 패키지로는 모듈 리졸버 통합이 불가능하다는 한계가 있다. fs.readFile은 가상으로 동작하지만, import('/data/module.js')는 실제 파일 시스템을 참조한다. 이 차이가 코어 통합의 필요성을 다시 한번 보여준다.


정리

  • Node.js에는 모듈 리졸버까지 포함하는 가상 파일 시스템이 필요하다. memfs 같은 npm 패키지는 fs API만 감싸기 때문에 불완전하다.
  • node:vfs는 Provider(데이터 원천) → Mounting(경로 연결) → Overlay(폴스루) 3계층 아키텍처로 설계되었다.
  • 테스트 격리, SEA 에셋 번들링, 멀티테넌트 샌드박싱, AI 코드 실행이 주요 사용 사례다.
  • using 키워드와 조합하면 리소스 정리가 자동화되어 테스트 격리가 깔끔해진다.
  • 코어 머지 전까지는 @platformatic/vfs로 미리 경험해볼 수 있다.

14,000줄짜리 PR이라는 규모가 말해주듯, 이건 단순한 유틸리티가 아니라 Node.js의 파일 시스템 인프라를 근본적으로 확장하는 작업이다.


참고:

관심 있을 만한 포스트

Naver FE News 2026년 4월 — 49MB 웹 페이지부터 Temporal Stage 4까지

Naver FE News 2026년 4월호에서 프론트엔드 개발자가 주목할 6가지 소식을 선별해 정리한다.

Naver FE NewsTemporal

envscan — .env.example을 손으로 관리하지 말자

코드에서 process.env 참조를 스캔해 .env.example을 자동 생성하는 envscan의 접근 방식과 기존 도구들과의 차이를 정리한다.

Node.js환경 변수

Bun이 빠른 건 맞다 — 그런데 당신의 이벤트 루프가 문제다

Bun으로 바꿔도 p99가 개선되지 않는 이유. 런타임 선택보다 먼저 봐야 할 진짜 병목 지점들.

BunNode.js

Bun vs Node.js vs Deno — 뭐가 다른지, 그래서 뭘 쓰면 좋은지 (2026 기준)

런타임 3대장 비교: 호환성(Node), 속도/번들(Bun), 올인원/보안(Deno). 팀/프로덕트 상황별 선택 기준과 체크리스트까지 정리.

BunNode.js

Action-Reducer-State의 귀환 — 프론트엔드 패턴이 서버를 점령한 이유

프론트엔드에서 익숙한 Redux의 Action-Reducer-State 패턴을 서버 사이드에 적용한 당근마켓의 이벤트 소싱 라이브러리 Ventyd를 분석한다.

이벤트 소싱Redux

SVG 아이콘 — 코드 배포 없이 프로덕트 팀이 직접 관리하는 법

CSS mask-image와 S3를 조합해 개발자 개입 없이 아이콘을 교체하는 패턴을 소개한다.

SVGCSS

VS Code 1.116 — 에이전트 디버깅, 포그라운드 터미널, 내장 Copilot

2026년 4월 VS Code 1.116이 에이전트 경험, 터미널, Chat UX, 내장 브라우저를 개선한 핵심 변경사항을 정리한다.

VS Code1.116

VS Code 에이전트 — 실전 개발에서 쓸 수 있게 만드는 세 가지 축

VS Code 1.110이 도입한 컨텍스트 관리, 에이전트 제어, 확장성 기능이 AI 에이전트를 실무에 투입 가능하게 만든 방식을 분석한다.

VS CodeAI Agent