왜 만들었나
상업 프로젝트에서 PHP + AJAX 기반의 파일 관리 시스템을 구현한 적이 있다. 서버의 디렉토리를 트리로 탐색하고, CodeMirror로 파일을 열어보고, 생성/이름변경/삭제까지 가능한 관리자 도구였다.
문제는 이걸 포트폴리오에서 보여줄 방법이 없다는 것이었다. 서버가 필요한 시스템이니 GitHub Pages에서는 동작하지 않는다. 스크린샷만으로는 "이런 걸 만들었습니다"라고 말하기에 부족했다.
그래서 프론트엔드만 떼어내서 정적 JSON 데이터로 동작하는 체험형 데모를 만들기로 했다.
구조
demos/file-manager/
├── index.html # HTML + inline CSS (단일 파일)
├── file-tree-data.js # JSON 트리 데이터
└── file-manager.js # 앱 로직 전체
빌드 도구 없이 순수 HTML/CSS/JS로 구성했다. CodeMirror 5는 CDN으로 로드한다.
실제 시스템 vs 데모
| 구분 | 실제 시스템 | 데모 |
|---|---|---|
| 데이터 소스 | 서버 파일시스템 (scandir) |
정적 JSON (FILE_TREE_DATA) |
| 파일 읽기 | PHP API → AJAX 응답 | JSON 노드의 content 속성 |
| 파일 저장 | file_put_contents → 서버 저장 |
node.content 업데이트 (메모리) |
| 보안 필터 | PHP chkExt() 서버 측 검증 |
JS validateFilename() 클라이언트 측 |
| 미리보기 | PHP로 이미지/텍스트 분기 | iframe.srcdoc + CSS 자동 연결 |
핵심은 같은 UX를 유지하면서 서버 의존성을 제거한 것이다.
핵심 구현 포인트
1. JSON 트리 데이터 설계
const FILE_TREE_DATA = {
name: "demos",
type: "directory",
children: [
{
name: "sample",
type: "directory",
children: [
{
name: "index.html",
type: "file",
language: "htmlmixed",
content: `<!DOCTYPE html>...`
},
{
name: "style.css",
type: "file",
language: "css",
content: `body { ... }`
}
]
}
]
};
실제 시스템에서는 PHP의 scandir()이 재귀적으로 디렉토리를 탐색해서 이 구조를 만들어준다. 데모에서는 이걸 JS 파일에 하드코딩했다.
language 속성은 CodeMirror의 모드명과 일치시켜서 별도 매핑 없이 바로 사용할 수 있게 했다.
2. 상태 관리 - unsavedContent 버퍼
처음에는 편집 내용을 바로 node.content에 반영했다. 그런데 이러면 미리보기가 항상 최신 편집 내용을 보여주게 된다. "수정 → 저장 → 미리보기"라는 흐름이 무의미해진다.
그래서 버퍼 패턴을 도입했다:
const state = {
unsavedContent: null, // 편집 중 임시 저장
hasUnsavedChanges: false, // 변경 여부 추적
savedFiles: new Set(), // 저장 완료 파일 표시용
};
- 편집 시:
editorInstance.on('change')→state.unsavedContent에 저장 - Save 시:
unsavedContent→node.content로 반영 - Preview 시:
node.content(저장된 내용)만 표시, 미저장이면 경고
function saveFile() {
node.content = state.unsavedContent; // 커밋
state.unsavedContent = null;
state.hasUnsavedChanges = false;
state.savedFiles.add(node.id);
}
트리에서도 상태를 시각적으로 구분한다:
- 주황색 ● — 미저장 변경
- 초록색 ● — 저장 완료
3. CSS 자동 연결 미리보기
HTML 파일을 미리보기할 때, 같은 폴더에 CSS 파일이 있으면 자동으로 연결해준다.
function showPreview(node) {
let linkedCss = '';
if (node.name.endsWith('.html')) {
const parent = findParent(state.tree, node.id);
if (parent && parent.children) {
const cssFile = parent.children.find(c =>
c.type === 'file' && c.name.endsWith('.css')
);
if (cssFile && cssFile.content) {
linkedCss = cssFile.content;
}
}
}
// <link> 태그를 인라인 <style>로 교체
let finalContent = content;
if (linkedCss) {
finalContent = content.replace(
/<link[^>]*href=["']([^"']+\.css)["'][^>]*>/gi,
`<style>${linkedCss}</style>`
);
}
iframe.srcdoc = finalContent;
}
실제 서버에서는 CSS 파일이 상대경로로 자연스럽게 로드되지만, iframe.srcdoc에서는 외부 파일 참조가 안 된다. 그래서 <link> 태그를 <style> 태그로 치환하는 방식으로 해결했다.
4. 보안 필터 데모
실제 시스템의 chkExt() 함수를 JS로 재현했다:
const BLOCKED_EXTENSIONS = [
'.php', '.phtml', '.exe', '.bat', '.cmd',
'.sh', '.bash', '.jsp', '.asp', '.aspx',
'.cgi', '.pl', '.htpasswd', '.env', '.ini',
];
const BLOCKED_PATTERNS = [
{ pattern: /\.\./, desc: 'Path traversal (..)' },
{ pattern: /^\.ht/, desc: '.ht* server config' },
{ pattern: /<\?php/i, desc: 'PHP tag injection' },
];
Security Demo 버튼을 누르면 9개의 테스트 파일명을 순차적으로 검증하면서 차단/허용 애니메이션을 보여준다. 실제 시스템에서 서버가 어떤 기준으로 파일을 걸러내는지 시각적으로 확인할 수 있다.
5. CodeMirror 통합
CodeMirror 5를 CDN으로 로드하고, 6가지 언어 모드를 지원한다:
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.18/codemirror.min.js"></script>
<script src=".../mode/xml/xml.min.js"></script>
<script src=".../mode/css/css.min.js"></script>
<script src=".../mode/javascript/javascript.min.js"></script>
<script src=".../mode/htmlmixed/htmlmixed.min.js"></script>
<script src=".../mode/php/php.min.js"></script>
<script src=".../mode/markdown/markdown.min.js"></script>
편집 모드 전환 시 readOnly 옵션만 토글하고, 에디터 좌측에 파란색 보더를 추가해서 편집 중임을 시각적으로 표시한다.
.fm-editor-content .CodeMirror:not(.CodeMirror-readonly) {
border-left: 3px solid var(--accent);
}
테마도 포트폴리오의 다크/라이트 모드와 연동된다. 라이트 모드는 default, 다크 모드는 material-darker 테마를 사용한다.
UI/UX 디테일
Save 버튼 하이라이트
미저장 변경이 있으면 Save 버튼이 주황색으로 바뀌면서 깜빡인다:
.fm-toolbar-btn.highlight {
border-color: var(--warning);
color: var(--warning);
animation: pulse-save 1.5s ease-in-out infinite;
}
@keyframes pulse-save {
0%, 100% { box-shadow: none; }
50% { box-shadow: 0 0 8px rgba(245, 158, 11, 0.4); }
}
키보드 단축키
| 키 | 기능 |
|---|---|
Ctrl+S / Cmd+S |
저장 |
E |
편집 모드 토글 |
P |
미리보기 토글 |
F2 |
이름 변경 |
Delete |
삭제 |
CodeMirror 편집 중에는 E, P 같은 단축키가 비활성화되어 코드 입력을 방해하지 않는다.
리사이저블 패널
트리 패널과 에디터 패널 사이의 디바이더를 드래그해서 폭을 조절할 수 있다. mousedown → mousemove → mouseup 이벤트 체인으로 구현했다.
삽질 기록
CodeMirror 5 vs 6
처음에는 최신 버전인 CodeMirror 6을 사용하려고 했다. 그런데 v6은 ESM 기반이라 CDN <script> 태그로는 로드할 수 없다. 번들러가 필요한데, 이 프로젝트는 빌드 도구 없이 순수 정적 파일로 가야 했다. 결국 CDN으로 바로 쓸 수 있는 v5를 선택했다.
자기 참조 문제
트리 데이터에 포트폴리오 전체 파일 구조를 넣었더니, index.html의 미리보기에서 데모 페이지 자체로의 링크가 나타났다. 클릭하면 iframe 안에서 파일 매니저가 또 열리는 무한 루프 같은 상황이 됐다.
해결: 트리 루트를 demos/ 폴더로 제한해서, 데모와 관련된 파일만 보여주도록 변경했다.
미리보기에서 CSS가 안 먹는 문제
iframe.srcdoc로 HTML을 렌더링하면 <link href="style.css">가 동작하지 않는다. iframe의 srcdoc은 실제 URL이 없으므로 상대경로 참조가 불가능하다.
해결: 같은 폴더의 CSS 파일을 찾아서 <link> 태그를 <style> 태그로 치환하는 방식으로 우회했다.
마무리
서버 의존적인 프로젝트를 포트폴리오에서 보여주는 건 항상 고민이다. 스크린샷은 밋밋하고, 영상은 인터랙션이 없다.
이번에 시도한 "프론트엔드만 떼어내서 정적 데모로 재현" 방식은 꽤 괜찮은 타협점이었다. 방문자가 직접 파일을 수정하고 저장하고 미리보기까지 해볼 수 있으니, 실제 시스템의 UX를 거의 그대로 전달할 수 있다.
물론 한계도 있다. 실제 서버와의 AJAX 통신, 파일 업로드, 권한 관리 같은 부분은 보여줄 수 없다. 하지만 "이 사람이 이런 수준의 UI를 만들 수 있구나"를 전달하는 데는 충분하다고 생각한다.
사용 기술: Vanilla JavaScript, CodeMirror 5, CSS Variables, GitHub Pages
소스 코드: GitHub
라이브 데모: lukaplayground.github.io/demos/file-manager
File Manager Demo
lukaplayground.github.io
'프로젝트 > GitHub Pages' 카테고리의 다른 글
| 개발자 포트폴리오 사이트 무료로 만들기 - GitHub Pages 완벽 가이드 (0) | 2026.01.18 |
|---|