본문 바로가기
프로젝트/GitHub Pages

서버 파일 관리 시스템을 정적 데모로 재현한 이야기

by 루까(Luka) 2026. 2. 11.
반응형

왜 만들었나

상업 프로젝트에서 PHP + AJAX 기반의 파일 관리 시스템을 구현한 적이 있다. 서버의 디렉토리를 트리로 탐색하고, CodeMirror로 파일을 열어보고, 생성/이름변경/삭제까지 가능한 관리자 도구였다.

문제는 이걸 포트폴리오에서 보여줄 방법이 없다는 것이었다. 서버가 필요한 시스템이니 GitHub Pages에서는 동작하지 않는다. 스크린샷만으로는 "이런 걸 만들었습니다"라고 말하기에 부족했다.

그래서 프론트엔드만 떼어내서 정적 JSON 데이터로 동작하는 체험형 데모를 만들기로 했다.

라이브 데모: lukaplayground.github.io/demos/file-manager/


구조

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 시: unsavedContentnode.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 같은 단축키가 비활성화되어 코드 입력을 방해하지 않는다.

리사이저블 패널

트리 패널과 에디터 패널 사이의 디바이더를 드래그해서 폭을 조절할 수 있다. mousedownmousemovemouseup 이벤트 체인으로 구현했다.


삽질 기록

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

반응형

 

반응형