Node.js에 가상 파일 시스템이 필요한 이유
Node.js는 실제 파일 시스템에 강하게 묶여 있어요. 가상 파일 시스템(VFS)이 있다면 테스트, 샌드박싱, 엣지 배포가 훨씬 쉬워집니다.

실제 파일 시스템 없이 Node.js 애플리케이션을 실행해 보면 얼마나 금방 무너지는지 바로 알 수 있어요. require()는 파일을 읽고, fs.readFile()도 파일을 읽어요. 템플릿 엔진도, 설정 로더도, 로깅 라이브러리도 전부 파일을 읽거나 써요. Node.js 생태계 전체가 POSIX 계열 파일 시스템이 존재하고, 쓰기가 가능하며, 코드와 데이터에 접근하는 기본 수단이라고 가정하고 있는 셈이죠.
로컬 디스크가 달린 서버에서 돌아가던 2010년에는 이 방식이 문제없었어요. 하지만 파일 시스템이 아예 없거나, 읽기 전용이거나, 신뢰할 수 없는 환경에서도 JavaScript를 돌리고 싶어지면서 점점 골칫거리가 되고 있어요. 엣지 런타임, WebAssembly 샌드박스, 서버리스 함수, 브라우저 기반 개발 환경이 대표적이죠.
파일 시스템 가정이 깨지는 지점
엣지 런타임. Cloudflare Workers, Deno Deploy, Vercel Edge Functions는 사용자와 가까운 곳에서, 최소한의 인프라로 JavaScript를 실행해요. 이런 환경은 쓰기 가능한 파일 시스템을 제공하지 않거나, 요청 사이에 유지되지 않는 제한적인 인메모리 파일 시스템만 제공하는 경우가 많아요. 그래서 fs.writeFileSync를 호출하는 Node.js 모듈은 그대로 크래시가 나고, fs.existsSync로 설정 파일 존재 여부를 확인하는 모듈은 동작이 예측 불가능해지죠.
WebAssembly. Node.js를 WebAssembly 샌드박스 안에서 돌리려면 파일 시스템 연산을 Wasm 런타임의 가상 파일 시스템(보통 WASI)에 매핑해야 해요. 그런데 이 매핑이 완벽하지 않아요. 파일 권한, 심볼릭 링크, 플랫폼별 경로가 깔끔하게 옮겨지지 않거든요. VFS 추상화가 있으면 이 매핑을 임시방편이 아니라 명시적인 구조로 만들 수 있어요.
테스트. 설정 파일을 읽거나, 템플릿을 불러오거나, 로그를 쓰는 코드를 단위 테스트하려면 두 가지 중 하나를 해야 해요. fs 모듈을 목(mock)으로 바꾸거나(라이브러리마다 fs API를 다르게 써서 쉽게 깨져요), 임시 디렉터리에 테스트 픽스처를 만들어야 하죠(느리고, 불안정하고, 디스크에 찌꺼기가 남아요). VFS가 있다면 테스트를 전부 메모리 안에서, 결정적으로, 실제 파일 시스템에 손대지 않고 돌릴 수 있어요.
보안 샌드박싱. 플러그인, 사용자 스크립트, CI/CD 빌드 단계처럼 신뢰할 수 없는 코드를 실행할 때는 파일 시스템 접근을 통제하고 싶어요. 지금은 이를 위해 OS 수준의 샌드박싱(컨테이너, 네임스페이스)을 쓰거나 fs 모듈을 몽키 패칭해야 하죠. VFS가 있으면 세밀한 파일 시스템 정책을 걸 수 있어요. 예를 들어 이 코드는 /app은 읽을 수 있지만 /etc는 못 읽고, /tmp에는 쓸 수 있지만 /app에는 쓸 수 없게 하는 식이에요.
VFS는 어떤 모습일까
가상 파일 시스템은 새로운 개념이 아니에요. 운영체제는 1980년대부터 VFS 계층을 써 왔고, 리눅스의 VFS는 ext4, NFS, procfs가 같은 API 뒤에서 함께 동작하게 해 줘요. Node.js에 적용할 아이디어도 같아요. 파일 시스템 인터페이스(fs.readFile, require() 등)와 실제 구현을 분리하자는 거죠.
// Hypothetical VFS API
import { createVFS, MemoryFS, ReadOnlyFS, OverlayFS } from 'node:vfs';
// In-memory filesystem for testing
const testFS = new MemoryFS({
'/app/config.json': '{"port": 3000}',
'/app/templates/index.html': '<h1>Hello</h1>',
});
// Read-only view of the real filesystem
const readOnly = new ReadOnlyFS('/');
// Overlay: reads from real FS, writes go to memory
const sandbox = new OverlayFS(readOnly, new MemoryFS());
// Run code with a specific filesystem
const vfs = createVFS(sandbox);
vfs.run(() => {
// Inside this context:
// fs.readFileSync('/etc/hosts') → reads real file
// fs.writeFileSync('/tmp/log.txt') → writes to memory overlay
// require('./module') → resolves against the VFS
const app = require('./app');
app.start();
});
핵심은 VFS가 명시적인 fs 호출뿐 아니라 require(), import(), __dirname, process.cwd()를 통한 모든 파일 시스템 접근을 가로챈다는 점이에요. 이게 실용적인 이유이기도 해요. 애플리케이션 코드를 건드리지 않고도 전체 파일 시스템 접근 경로를 통째로 바꿀 수 있거든요.
모듈 해석 문제
Node.js와 파일 시스템이 가장 깊게 얽혀 있는 부분은 모듈 해석이에요. require('express')를 쓰면 Node.js는 디렉터리 트리를 거슬러 올라가며 node_modules/express 디렉터리를 찾고, package.json을 읽고, 심볼릭 링크를 해석하고, main이나 exports 필드를 따라가요. require 한 번에 파일 시스템 호출이 수십 번 일어나는 셈이죠.
Yarn PnP(Plug'n'Play)가 이 문제를 부분적으로 해결했어요. node_modules를 모듈 이름을 zip 아카이브에 매핑하는 매니페스트 파일로 대체했죠. 디렉터리 탐색이 없어서 빠르고, 호이스팅으로 인한 예상치 못한 동작도 없어서 더 결정적이에요. 다만 Node의 모듈 해석을 몽키 패칭해야 했고, 그 과정에서 node_modules가 존재한다고 가정하던 도구들이 깨졌어요.
제대로 된 VFS가 있다면 Yarn PnP 방식을 일급 기능으로 만들 수 있어요. 모듈 해석이 VFS를 거치면, VFS가 zip 아카이브, 인메모리 모듈, 원격 URL, 또는 전통적인 node_modules 디렉터리 탐색 등 어떤 매핑이든 구현할 수 있어요. 환경마다 다른 전략을 쓰되, 애플리케이션 코드에서는 같은 API를 쓰면 되는 거죠.
선행 사례
다른 런타임들은 이 문제의 여러 변형을 이미 풀어냈어요.
- Deno는 기본적으로 URL에서 모듈을 불러와 관리되는 디렉터리에 캐싱해요.
node_modules도 없고 파일 시스템 기반 모듈 해석도 없어요. 모듈을 어디에, 어떻게 저장할지는 런타임이 결정하죠. - Bun은 하드링크를 활용한 전역 모듈 캐시를 써서 파일 시스템 오버헤드를 줄여요. Node.js보다 모듈 해석이 훨씬 최적화되어 있어요.
- Go의
io/fs인터페이스(Go 1.16에서 추가)는 OS 파일 시스템, 임베디드 파일(embed.FS), zip 아카이브, 인메모리 파일 시스템이 모두 구현하는 인터페이스를 정의해요. 표준 라이브러리 함수들이fs.FS인터페이스를 받기 때문에 실제 파일 없이도 테스트할 수 있어요. - Java의 NIO
FileSystem추상화는 플러그형 파일 시스템 프로바이더를 지원해요. 테스트용 인메모리 구현이나 zip 기반 파일 시스템도 포함되어 있죠. - .NET의
IFileSystem패턴은 테스트 용이성을 위해 .NET 애플리케이션에서 널리 쓰여요. 다만 런타임 기능이라기보다는 커뮤니티 관례에 가까워요.
Go의 접근 방식이 특히 참고할 만해요. Open, Read, Stat 같은 작은 인터페이스를 정의하고 표준 라이브러리 전반에서 쓰면서, Go는 기존 코드를 깨뜨리지 않고도 파일 시스템 추상화를 아주 쉽게 만들었어요. Node.js도 VFS 인터페이스를 정의한 뒤 표준 라이브러리와 모듈 로더를 점진적으로 여기에 맞춰 옮기면 같은 길을 갈 수 있어요.
호환성이라는 과제
Node.js VFS에서 가장 큰 장애물은 구현이 아니라 생태계예요. npm에는 패키지가 200만 개 넘게 있고, 그중 상당수가 POSIX 시맨틱, 실제 경로, 쓰기 가능한 디렉터리를 가정하고 파일 시스템에 접근해요.
기존 패키지를 깨뜨리는 VFS는 나오자마자 실패할 거예요. 옵트인 방식이어야 하고 하위 호환성을 지켜야 해요. VFS를 명시적으로 만들지 않으면 지금과 완전히 똑같이 동작해야 하죠. VFS를 만들었을 때는 파일 시스템 호출을 투명하게 가로채서, 제대로 작성된 패키지는 코드 수정 없이 동작하게 해야 해요.
‘제대로 작성된’이라는 조건이 핵심이에요. cp나 rm을 셸로 호출하거나, libc의 open()을 직접 부르는 네이티브 애드온을 쓰거나, /proc이나 다른 OS 전용 파일 시스템에 기대는 패키지는 VFS와 함께 동작하지 않아요. 이건 넘기 어려운 경계예요. 코드가 그 경계를 넘어 기저 시스템에 직접 손을 대는 순간, 어떤 추상화든 새기 마련이니까요.
실제로 제안되고 있는 내용
Node.js VFS 논의는 탁상공론이 아니에요. Node.js 이슈 트래커에는 구체적인 제안들이 올라와 있어요. 현재 논의는 몇 가지 핵심 아이디어를 중심으로 흘러가고 있어요.
첫째, fs 모듈이 위임하는 FileSystemProvider 인터페이스예요. 기본 프로바이더는 실제 파일 시스템이고, 커스텀 프로바이더는 같은 인터페이스를 구현해 인메모리, 읽기 전용, 오버레이, 원격 파일 시스템을 만들 수 있어요.
둘째, 모듈 로더와의 통합이에요. require()와 import()가 VFS를 통해 모듈을 해석하면, node_modules 디렉터리에 의존하지 않는 모듈 해석 전략을 쓸 수 있어요.
셋째, 정책 계층이에요. VFS가 접근 제어를 강제해서, 정의된 범위 밖의 경로를 읽거나 쓰지 못하게 막을 수 있어요. Deno의 --allow-read, --allow-write 플래그와 비슷한 샌드박싱 기능을 Node.js가 기본으로 갖게 되는 셈이죠.
이게 Node.js 24에 들어갈지, 26에 들어갈지, 아니면 아예 들어가지 않을지는 Node.js 팀의 여력과, 파일 시스템 접근 방식의 파괴적 변경을 커뮤니티가 얼마나 받아들일지에 달려 있어요. 그래도 압박은 현실이에요. 엣지 런타임은 커지고 있고, WebAssembly 배포 대상도 늘고 있고, ‘어디서나 JavaScript’와 ‘실제 파일 시스템이 있는 서버의 Node.js’ 사이의 간극은 점점 경쟁력의 약점이 되고 있어요.
지금 당장 할 수 있는 것
런타임 VFS를 기다리지 않아도 파일 시스템 추상화의 이점은 누릴 수 있어요. 새로 짜는 코드라면 파일 시스템 접근을 인터페이스 뒤로 감싸세요. fs.readFile을 직접 호출하는 대신, 코드가 의존하는 FileStore 인터페이스를 만드는 거예요. 운영에서는 실제 파일 시스템으로, 테스트에서는 인메모리 맵으로 구현하면 돼요. 평범한 의존성 역전이지만 화려하지 않아도 효과적이고, 어떤 런타임에서든 잘 맞아요.
기존 코드라면 memfs와 unionfs 같은 라이브러리를 써 볼 수 있어요. 이들은 Node의 fs 모듈을 패치해서 인메모리와 오버레이 파일 시스템을 제공해요. 완벽하지는 않아요. 네이티브 애드온과 자식 프로세스는 이들을 우회하거든요. 그래도 순수 JavaScript 코드에서는 잘 동작하고, 테스트가 훨씬 쉬워져요.
파일 시스템은 Node.js 인프라에서 아직 깔끔한 추상화 경계가 없는 마지막 큰 조각이에요. 스트림에는 인터페이스가 있고, HTTP에도 인터페이스가 있고, 모듈 로더에도 훅이 있어요. 파일 시스템도 같은 대우를 받으면 Node.js는 서버에 묶인 런타임이 아니라 진짜로 이식 가능한 런타임이 될 거예요. 그만큼 밀어붙일 가치가 있는 변화라고 생각해요.


