블로그: 가상 파일 시스템 설계 — 포스트를 디렉토리로
블로그: 가상 파일 시스템 설계에 대해 "왜?"라고 물으면 답할 수 있으신가요?
\n\n> 블로그: 가상 파일 시스템 설계에 대해 "왜?"라고 물으면 답할 수 있으신가요?\n이전 글에서 터미널 에뮬레이터의 입력·출력·히스토리를 만들었습니다. 근데 cd를 쳐도 갈 곳이 없고, ls를 쳐도 보여줄 게 없습니다. 터미널 뒤에 파일 시스템이 없기 때문입니다.
실제 서버가 아니니까 진짜 파일 시스템을 쓸 수는 없고, 블로그 포스트 데이터를 기반으로 브라우저 메모리에 가상 파일 시스템 을 만들기로 했습니다.
구조: 포스트가 파일이 된다
블로그 포스트의 카테고리 경로가 디렉토리가 되고, 글 자체가 .md 파일이 됩니다.
~/posts/
├── 개발/
│ ├── 백엔드/
│ │ ├── 스프링/
│ │ │ ├── 스프링부트/
│ │ │ │ ├── Spring Boot 란.md
│ │ │ │ └── 빈(Bean) 이란.md
│ │ │ └── 스프링 시큐리티/
│ │ │ └── ...
│ │ └── 네티/
│ └── 프론트엔드/
├── 데브옵스/
└── 사이드 프로젝트/
└── 블로그/
└── 이 글.md
사용자는 cd 개발/백엔드/스프링으로 이동하고, ls로 하위 글 목록을 보고, cat 파일명.md로 해당 글로 이동할 수 있습니다.
VirtualFS 클래스
Post 배열을 받아서 트리 구조를 만드는 클래스입니다.
class VirtualFS {
constructor(posts) {
this._root = { name: '~', children: {}, type: 'dir' };
// posts 디렉토리 생성
this._root.children['posts'] = { name: 'posts', children: {}, type: 'dir' };
for (const post of posts) {
const segments = post.path.split('/');
let node = this._root.children['posts'];
// 카테고리 경로를 따라 디렉토리 생성
for (let i = 0; i < segments.length - 1; i++) {
const seg = segments[i];
if (!node.children[seg]) {
node.children[seg] = { name: seg, children: {}, type: 'dir' };
}
node = node.children[seg];
}
// 마지막 세그먼트는 파일
const fileName = segments[segments.length - 1] + '.md';
node.children[fileName] = {
name: fileName,
type: 'file',
post: post,
};
}
}
}
Post의 path가 개발/백엔드/스프링/Spring Boot 란이라면:
개발,백엔드,스프링은 디렉토리 노드Spring Boot 란.md는 파일 노드 (원본 Post 객체 참조)
각 노드는 { name, children, type } 형태로 단순합니다. 파일 노드에는 post 속성으로 원본 데이터를 연결합니다.
경로 해석
Unix 경로를 해석하는 유틸리티입니다. 상대 경로, ., .., 절대 경로를 전부 처리합니다.
function resolve(cwd, target) {
const parts = target.startsWith('~')
? target.split('/')
: [...cwd.split('/'), ...target.split('/')];
const result = [];
for (const part of parts) {
if (part === '' || part === '.') continue;
if (part === '..') {
if (result.length > 1) result.pop(); // ~ 위로는 못 감
} else {
result.push(part);
}
}
return result.join('/') || '~';
}
resolve('~/posts/개발', '..') → '~/posts'
resolve('~/posts', '개발/백엔드') → '~/posts/개발/백엔드'
resolve('~/posts', '~/about') → '~/about'
resolve('~', '../../..') → '~'
..을 아무리 많이 써도 ~(홈) 위로는 올라갈 수 없게 했습니다. 루트 아래로 빠지면 에러가 나니까요.
명령어 구현
cd — 디렉토리 이동
function cd(ctx, args) {
const target = args[0] || '~';
const resolved = resolve(ctx.cwd, target);
const node = ctx.fs.getNode(resolved);
if (!node) {
ctx.push(`cd: ${target}: No such file or directory`);
} else if (node.type !== 'dir') {
ctx.push(`cd: ${target}: Not a directory`);
} else {
ctx.setCwd(resolved);
}
}
실제 bash의 에러 메시지를 그대로 따라했습니다. 존재하지 않는 경로면 No such file or directory, 파일을 cd하려고 하면 Not a directory.
ls — 목록 보기
function ls(ctx, args) {
const target = args[0] || ctx.cwd;
const node = ctx.fs.getNode(resolve(ctx.cwd, target));
if (!node) {
ctx.push(`ls: ${target}: No such file or directory`);
return;
}
if (node.type === 'file') {
ctx.push(node.name);
return;
}
for (const entry of Object.values(node.children)) {
if (entry.type === 'dir') {
ctx.push(`drwxr-xr-x ${entry.name}/`);
} else {
ctx.push(`-rw-r--r-- ${entry.name}`);
}
}
}
drwxr-xr-x 같은 퍼미션 표시는 의미는 없지만, 진짜 ls -l 느낌을 주기 위해 넣었습니다. 디렉토리는 이름 뒤에 /를 붙여서 구분합니다.
cat — 파일 읽기 (글로 이동)
function cat(ctx, args) {
if (!args[0]) {
ctx.push('cat: missing operand');
return;
}
const node = ctx.fs.getNode(resolve(ctx.cwd, args[0]));
if (!node) {
ctx.push(`cat: ${args[0]}: No such file or directory`);
} else if (node.type === 'dir') {
ctx.push(`cat: ${args[0]}: Is a directory`);
} else if (node.post) {
ctx.openPost(node.post);
}
}
cat으로 파일을 읽으면 마크다운을 터미널에 출력하는 대신 해당 글의 상세 페이지로 이동 합니다. 터미널에서 마크다운 원문을 보는 건 가독성이 떨어지니까, 차라리 예쁘게 렌더링된 페이지를 보여주는 게 낫습니다.
탭 자동완성
현재 디렉토리의 자식 노드 이름을 기반으로 자동완성합니다.
getCompletions(partial, cwd) {
const dir = this.getNode(cwd);
if (!dir || dir.type !== 'dir') return [];
return Object.keys(dir.children)
.filter(name => name.startsWith(partial))
.map(name => {
const child = dir.children[name];
return child.type === 'dir' ? name + '/' : name;
});
}
cd 개+ Tab →cd 개발/cat Sp+ Tab →cat Spring Boot 란.md- 후보가 여러 개면 목록 출력
디렉토리는 자동으로 /가 붙어서 연속으로 Tab을 눌러 깊이 들어갈 수 있습니다.
정리
| 구성요소 | 역할 |
|---|---|
| VirtualFS | 포스트 데이터 → 트리 구조 변환 |
| path-utils | 상대/절대 경로 해석 |
| commands | cd, ls, cat, pwd 등 구현 |
가상 파일 시스템이 있으니까 터미널이 진짜 터미널처럼 동작합니다. cd로 돌아다니고, ls로 내용물을 보고, cat으로 글을 열 수 있습니다. 실제 서버가 없어도 이 정도 경험을 만들 수 있다는 게 재밌었습니다.