웹 기반 코드 에디터입니다. ZIP 파일을 업로드하고, 코드를 수정한 후 수정된 내용을 다운로드할 수 있습니다.
개발기간: 2025.12.17 - 2025.12.21
graph TB
subgraph "UI Layer"
A[AppLayout] --> B[FileToolbar]
A --> C[FileTree]
A --> D[Tabs]
A --> E[FileViewer]
E --> F[MonacoEditor]
E --> G[ImageViewer]
end
subgraph "State Management"
H[FileContext]
end
subgraph "Utils"
I[fileUtils]
J[monacoWorker]
end
B -->|업로드/다운로드| H
C -->|파일 선택| H
D -->|탭 관리| H
F -->|파일 편집| H
F -->|언어 감지| I
H -->|파일 데이터| C
H -->|선택된 파일| F
H -->|열린 탭| D
style H fill:#e1f5ff
style F fill:#fff4e1
style I fill:#f0f0f0
주요 흐름:
- 파일 업로드: FileToolbar → JSZip 파싱 → FileContext 저장
- 파일 선택: FileTree 클릭 → FileContext 업데이트 → 탭 추가 → MonacoEditor 렌더링
- 파일 편집: MonacoEditor 변경 → editedFiles 상태 업데이트
- 파일 다운로드: editedFiles + 원본 파일 병합 → 새 ZIP 생성 → 다운로드
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = event.target.files?.[0];
if (!selectedFile) return;
if (!selectedFile.name.endsWith(".zip")) {
alert("zip 파일만 업로드 가능합니다");
return;
}
try {
const zip = new JSZip();
const parsedZip = await zip.loadAsync(selectedFile);
setFiles(parsedZip.files);
} catch (error) {
console.error("압축 해제 실패:", error);
} finally {
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
}
};event.target.files?.[0]로 선택한 파일 중 첫 번째 파일을 가져옵니다.zip.loadAsync로 업로드된 zip 파일을 파싱하고, 파싱된 파일 객체를setFiles에 저장합니다.- 마지막으로
fileInputRef.current.value값을 초기화하여 같은 이름의 zip 파일을 연속으로 올려도 onChange 이벤트가 정상적으로 동작하게 합니다.
const TreeNodeItem = ({ node, depth }: TreeNodeItemProps) => {
const [isOpen, setIsOpen] = useState(false);
const { openFile } = useFileContext();
const handleClick = () => {
if (node.isDirectory) {
setIsOpen(!isOpen);
} else {
openFile(node.path);
}
};
return (
<>
<TreeItem depth={depth} onClick={handleClick}>
{node.isDirectory && (
<>
{isOpen ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
<Folder size={16} />
</>
)}
{!node.isDirectory && <File size={16} />}
{node.name}
</TreeItem>
{/* 폴더가 열려있으면 자식들 재귀적으로 렌더링 */}
{node.isDirectory &&
isOpen &&
node.children.map((child) => (
<TreeNodeItem key={child.path} node={child} depth={depth + 1} />
))}
</>
);
};TreeNodeItem함수로 전달된 node의 isDirectory 필드에 따라 알맞은 아이콘을 렌더링한다- 만약 그 노드가 디렉토리이고 열려있다면 그 노드의 자식 노드들을 재귀적으로 depth를 1씩 증가시켜서 렌더링한다
- 클릭된 파일의 path는
openFile(node.path)에 저장되어 tabs와 monaco editor에서 사용된다
editor.onDidChangeModelContent(() => {
const newValue = editor.getValue();
const currentFile = selectedFileRef.current;
if (currentFile && !currentFile.isImage) {
updateFileContent(currentFile.name, newValue);
}
});- monaco editor의 content변경이되면 이벤트 리스너
onDidChangeModelContent가 감지하여editor.getValue()로 content를 읽어오고 수정된 파일명과 새로운 content를 따로 저장해둔다
const downloadFiles = async () => {
if (Object.keys(files).length === 0) {
alert('업로드된 파일이 없습니다.');
return;
}
const zip = new JSZip();
for (const [path, file] of Object.entries(files)) {
if (file.dir) continue;
if (editedFiles[path]) {
zip.file(path, editedFiles[path]);
} else {
const content = await file.async('arraybuffer');
zip.file(path, content);
}
}- 모든 파일을 순회하여 파일 수정단계에 수정된 파일인지
editedFiles[path]로 체크하고 편집된 파일이라면 수정본을, 편집된적이 없는 파일이라면 원본을 zip에 저장한다 - 이후
zip.generateAsync로 zip파일을 생성하여 수정본을 다운로드 할수있게된다
| 분류 | 기술 스택 |
|---|---|
| 빌드 도구 | |
| 언어 & 프레임워크 | |
| 스타일링 & UI | |
| 코드 에디터 | |
| 파일 처리 | |
| 코드 품질 | |
| 배포 |
code-editor/
├── public/ # 정적 파일
├── src/
│ ├── components/ # React 컴포넌트
│ │ ├── editor/ # 에디터 관련 컴포넌트
│ │ │ ├── FileViewer.tsx # 파일 뷰어 (이미지/에디터 분기)
│ │ │ ├── ImageViewer.tsx # 이미지 뷰어
│ │ │ ├── MonacoEditor.tsx # Monaco 에디터 래퍼
│ │ │ └── Tabs.tsx # 탭 UI
│ │ ├── files/ # 파일 관리 컴포넌트
│ │ │ ├── FileToolbar.tsx # 업로드/다운로드 툴바
│ │ │ └── FileTree.tsx # 파일 트리 네비게이션
│ │ └── AppLayout.tsx # 전체 레이아웃
│ ├── contexts/ # Context API
│ │ └── FileContext.tsx # 파일 상태 관리
│ ├── styles/ # 스타일 관련
│ │ ├── GlobalStyles.tsx # 전역 스타일
│ │ ├── theme.ts # 테마 설정
│ │ └── emotion.d.ts # Emotion 타입 정의
│ ├── utils/ # 유틸리티 함수
│ │ ├── fileUtils.ts # 파일 관련 헬퍼
│ │ └── monacoWorker.ts # Monaco 워커 설정
│ ├── App.tsx # 루트 컴포넌트
│ └── main.tsx # 앱 진입점
├── eslint.config.js # ESLint 설정
├── tsconfig.json # TypeScript 설정
├── vite.config.ts # Vite 설정
└── package.json # 프로젝트 의존성
형식: <type>: <subject>
Type 종류:
feat: 새로운 기능 추가fix: 버그 수정docs: 문서 수정style: 코드 포맷팅, 세미콜론 누락 등 (코드 변경 없음)refactor: 코드 리팩토링chore: 빌드 작업, 패키지 매니저 설정 등
예시
feat: 업로드 버튼 구현
fix: 파일 업로드 없이 다운로드되는 버그 수정
docs: README 아키텍처 다이어그램 추가
style: 코드 포맷팅 적용
refactor: getLanguage 함수를 fileUtils로 분리
chore: vite 설정 업데이트
배포 브랜치: main (Vercel 자동 배포)
형식: <type>/<description>
Type 종류:
feat/: 새로운 기능 개발fix/: 버그 수정docs/: 문서 작업style/: 스타일 관련 작업refactor/: 리팩토링chore/: 기타 작업
예시
feat/file-upload
fix/download-button-bug
docs/readme
style/button-component
refactor/utils
chore/setup-eslint
