단일 파일 웹앱이란
단일 파일 웹앱은 HTML, CSS, JavaScript가 모두 하나의 .html 파일 안에 들어있는 애플리케이션이다. 빌드 과정이 없고, 브라우저에서 바로 열 수 있다. 설치도 배포도 필요 없다.
이 접근법은 내부 도구, 개인 유틸리티, 프로토타입 제작에서 특히 강력하다. 이메일에 첨부하거나 USB에 넣어서 전달할 수 있고, 인터넷 없이도 동작한다.
- 사내 계산 도구, 견적서 생성기 같은 내부 유틸리티
- 인터넷 연결 없는 환경에서 쓰는 오프라인 도구
- 빠른 프로토타입 또는 데모용 앱
- 설치 불가 환경(공용 PC, 제한된 네트워크)에서의 도구
아키텍처 설계
단일 파일이라고 해서 구조가 없어야 하는 건 아니다. 오히려 파일 하나에 모든 것을 담기 때문에 구조를 더 신경 써야 한다.
기본 구조
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Portable App</title>
<style>
/* 모든 CSS를 여기에 */
</style>
</head>
<body>
<div id="app"></div>
<script>
// 모든 로직을 여기에
const App = {
state: {},
init() { /* 초기화 */ },
render() { /* UI 업데이트 */ }
};
App.init();
</script>
</body>
</html>모듈화 전략
파일은 하나지만 코드는 모듈로 나눠야 한다. IIFE(즉시 실행 함수)나 객체 리터럴 패턴으로 영역을 분리하면 유지보수가 훨씬 수월해진다. 상태 관리, UI 렌더링, 데이터 저장을 각각 별도 객체로 분리하는 것을 권장한다.
파일 크기가 커지면 개발 중에는 여러 파일로 분리하고, 배포할 때만 하나로 합치는 빌드 단계를 두는 방법도 있다. CSS·JS가 적다면 cat 명령어만으로도 충분하지만, 모듈 import와 번들링이 필요해지면 vite-plugin-singlefile(2026년 4월 기준 2.3.3)을 쓰면 빌드 산출물의 모든 JS와 CSS를 단일 .html로 자동 인라인해 준다. esbuild 기반이라 빌드 속도도 빠르다.
셀프 업데이트 구현
단일 파일 앱에서 가장 흥미로운 부분이다. 앱이 자기 자신의 최신 버전을 서버에서 확인하고, 새 버전이 있으면 다운로드를 안내하는 구조를 만들 수 있다.
버전 번호 내장
앱 내부에 현재 버전을 상수로 선언한다. const APP_VERSION = '1.2.0'; 형태로 관리하면 된다.
서버에 최신 버전 정보 배치
JSON 파일 하나에 최신 버전과 다운로드 URL을 기록해 서버에 올려둔다. 앱이 실행될 때 이 JSON을 fetch로 확인한다.
비교 후 알림 표시
내장 버전과 서버 버전을 비교해서, 차이가 있으면 업데이트 안내 배너를 표시한다. 강제 업데이트가 아니라 사용자 선택에 맡기는 것이 UX 측면에서 좋다.
const APP_VERSION = '1.2.0';
async function checkUpdate() {
try {
const res = await fetch('https://example.com/app-version.json');
const data = await res.json();
if (data.version !== APP_VERSION) {
showUpdateBanner(data.version, data.downloadUrl);
}
} catch (e) {
// 오프라인이면 무시
}
}데이터 영속성 확보
단일 파일 앱에서 데이터 저장은 localStorage가 가장 현실적인 선택이다. 용량은 약 5MB로 제한되지만, 대부분의 내부 도구에는 충분하다.
| 저장 방식 | 용량 | 적합한 상황 |
|---|---|---|
| localStorage | ~5MB | 설정, 소량 데이터, 상태 보존 |
| IndexedDB | 수백MB+ | 대량 구조화 데이터, 바이너리 파일 |
| File System Access API | 디스크 여유분 | 파일 직접 저장/읽기 (2026년 현재 Chrome·Edge·Opera 전용, Firefox·Safari 미지원) |
데이터 백업과 복원 기능도 반드시 넣어야 한다. JSON으로 내보내고 가져오는 기능이면 충분하다. 브라우저를 바꾸거나 PC를 바꿀 때 데이터를 잃어버리는 문제를 예방할 수 있다.
File System Access API의 showSaveFilePicker로 로컬 파일에 바로 저장하는 방식은 편리하지만, 2026년 현재도 Firefox와 Safari에서는 동작하지 않는다. 두 브라우저는 샌드박스화된 OPFS(Origin Private File System)만 제공한다. 따라서 'showSaveFilePicker' in window로 기능을 감지하고, 미지원 환경에서는 <a download>로 파일을 내려받게 하는 폴백을 반드시 함께 둬야 한다.
한계와 대안
단일 파일 웹앱이 만능은 아니다. 규모가 커지면 유지보수가 어려워지고, 팀 협업에는 적합하지 않다.
언제 다른 방식을 선택할 것인가
- 코드가 3,000줄을 넘어가면 모듈 번들러 도입을 고려해야 한다
- 여러 명이 동시에 작업해야 하면 일반 프로젝트 구조가 낫다
- 외부 API 인증이 필요하면 서버가 결국 필요하다
- 복잡한 라우팅이 필요하면 SPA 프레임워크가 적합하다
Electron이나 Tauri로 데스크탑 앱으로 패키징하면 파일 시스템 접근, 시스템 트레이 같은 네이티브 기능을 추가할 수 있다. 단일 파일로 시작한 도구가 성장하면 자연스럽게 넘어갈 수 있는 경로다.
2026년 기준으로는 Tauri 2.0이 새 프로젝트의 기본 선택지로 굳어졌다. OS에 내장된 WebView를 사용해 "Hello World" 번들이 3MB 미만으로, 동일 앱이 100MB대인 Electron보다 훨씬 가볍고 메모리·기동 시간 면에서도 유리하다. 또한 Tauri 2.0은 데스크탑뿐 아니라 iOS·안드로이드까지 하나의 코드베이스로 타깃할 수 있다. 다만 운영체제마다 다른 WebView 렌더링 엔진을 쓰므로 렌더링 일관성이 중요하거나 성숙한 플러그인 생태계·검증된 자동 업데이트 경로가 필요하면 Electron이 여전히 안전한 선택이다.
마무리
단일 파일 웹앱은 "가장 단순한 배포"의 극단에 있다. 복잡한 빌드 파이프라인 없이, 파일 하나를 전달하면 끝이다. 작은 도구, 내부 유틸리티, 빠른 프로토타입에 이 방식은 여전히 유효하다.
중요한 건 이 접근법의 한계를 알고 쓰는 것이다. 작을 때는 단일 파일로, 커지면 구조를 갖춘 프로젝트로 전환하면 된다. 도구의 라이프사이클에 맞게 유연하게 대응하는 것이 실력이다.
덧붙이면 파일 하나라는 제약이 오히려 설계를 단단하게 만들기도 한다. 의존성을 늘릴 수 없으니 정말 필요한 것만 남게 되고, 그 결과 몇 년 뒤에도 그대로 열리는 도구가 된다. 오래 쓸 작은 도구를 만들 때 이 방식이 의외로 잘 맞는다.
그리고 브라우저 저장소에는 용량 한계가 있다는 점을 염두에 둔다. 데이터가 쌓이는 도구라면 내보내기 기능을 함께 만들어 두는 편이 낫다. 파일 하나로 가볍게 시작하되, 빠져나갈 길은 열어 둔다.