기능 개발에 몰두하다 보면 보안과 백업은 늘 후순위로 밀립니다. "나만 쓰는 건데 뭐"라고 생각하기 쉽지만, 서버가 날아가면 가족들의 몇 달치 기록이 통째로 사라집니다. 시리즈 마지막 편에서는 인프라 엔지니어답게 운영 관점을 정리하고, 프로젝트 전체를 돌아봅니다.
1. 환경 정보
| 항목 | 버전/스펙 |
|---|---|
| Backend | PocketBase (Self-hosted, SQLite) |
| Web Server | Nginx |
| Infra | Proxmox VE — LXC 컨테이너 |
| SSL | Let's Encrypt |
| Frontend | HTML + Vanilla JS (PWA) |
2. PocketBase 관리자 접근 제한
2-1. 문제 상황
PocketBase 관리자 대시보드(/pb/_/)가 외부에 그대로 열려있었습니다. 관리자 비밀번호가 있긴 하지만, 대시보드 자체가 노출되는 것만으로도 위험합니다. 브루트포스 공격에 노출되고, PocketBase 취약점이 발견될 경우 바로 타격을 받을 수 있습니다.
2-2. 해결 — Nginx IP 제한
Nginx location 블록으로 관리자 페이지를 내부망에서만 접근 가능하도록 제한했습니다.
# 관리자 페이지 — 내부망만 허용
location ^~ /pb/_/ {
allow 127.0.0.1;
allow 192.168.0.0/16;
allow 10.0.0.0/8;
deny all;
proxy_pass http://127.0.0.1:8090/_/;
# ... 프록시 헤더
}
# API — 모든 로그인 유저 허용
location ^~ /pb/ {
proxy_pass http://127.0.0.1:8090/;
# ... 프록시 헤더
}
여기서 핵심은 location 블록의 순서입니다. /pb/_/ 블록이 /pb/ 위에 있어야 우선 적용됩니다. Nginx의 ^~ 접두어 매칭은 가장 긴 매칭을 먼저 선택하기 때문에, /pb/_/가 /pb/보다 구체적이므로 관리자 경로 요청이 정확히 IP 제한 블록에 걸립니다. 외부에서 /pb/_/에 접근하면 403 Forbidden이 반환됩니다.
참고: Nginx location 블록 우선순위는 exact(
=) → preferential prefix(^~) → regex(~,~*) → prefix 순서입니다. 자세한 내용은 DigitalOcean 가이드를 참고하세요.

2-3. proxy_pass 경로 삽질
처음에 proxy_pass http://127.0.0.1:8090/;로 했더니 PocketBase가 404를 반환했습니다. /pb/_/ 요청이 PocketBase에는 /로 전달되니까요. proxy_pass http://127.0.0.1:8090/_/;로 경로를 맞춰줘야 합니다.
Nginx의 proxy_pass에서 URI가 포함되어 있으면(/ 포함) location에서 매칭된 부분이 치환됩니다. 이 동작을 정확히 이해하지 않으면 404가 나오기 쉽습니다.
3. 자동 백업 — Proxmox 바인드 마운트
3-1. 왜 중요한가
PocketBase는 SQLite 파일 하나(data.db)에 모든 데이터가 들어있습니다. 이 파일만 백업하면 전체 복원이 가능합니다. 하지만 LXC 컨테이너 안에만 존재하면, 컨테이너가 날아갈 때 같이 사라집니다.
참고로 PocketBase v0.16 이후부터는 대시보드(Settings > Backups)에서 내장 백업/복원 기능을 제공합니다. S3 호환 스토리지에 자동 백업도 가능하지만, 관리자 대시보드를 내부망으로 제한한 상황이라 cron 기반 파일 복사가 더 간편했습니다.
3-2. Proxmox 호스트에 바인드 마운트
NAS에 NFS 마운트가 이상적이지만, 환경상 연결이 어려웠습니다. 대안으로 Proxmox 호스트에 직접 바인드 마운트했습니다.
# Proxmox 호스트에서 실행
mkdir -p /backup/timebox
pct set <CT_ID> -mp0 /backup/timebox,mp=/mnt/backup
chmod 777 /backup/timebox # 호스트에서 실행해야 함!
pct set 명령으로 컨테이너에 마운트 포인트를 추가합니다. 컨테이너 내부에서는 /mnt/backup/으로 접근할 수 있고, 실제 데이터는 Proxmox 호스트의 /backup/timebox/에 저장됩니다. 컨테이너가 죽어도 데이터는 살아남습니다.

3-3. 크론으로 매일 백업
# 매일 새벽 3시 백업, 7일 보관
0 3 * * * cp /opt/pocketbase/pb_data/data.db \
/mnt/backup/data_$(date +\%Y\%m\%d).db \
&& find /mnt/backup/ -name "data_*.db" -mtime +7 -delete
cp로 SQLite 파일을 날짜별로 복사하고, 7일 이상 된 백업은 자동 삭제합니다. 단순하지만 가족용 앱에는 충분합니다. 여기에 Proxmox 자체의 스냅샷 스케줄까지 설정해두면 이중 백업이 됩니다.
참고: 대용량(2GB 이상) SQLite DB라면
cp대신sqlite3 .backup명령을 사용하는 것이 안전합니다. 파일 복사 중 쓰기가 발생하면 데이터가 깨질 수 있습니다. 자세한 내용은 PocketBase 프로덕션 가이드를 참고하세요.
3-4. 삽질 — 바인드 마운트 권한 문제
컨테이너 안에서 /mnt/backup/에 파일을 쓰려는데 권한 에러가 발생했습니다. chmod를 실행해도 Operation not permitted. 바인드 마운트된 디렉토리의 권한은 컨테이너 안이 아니라 Proxmox 호스트에서 변경해야 합니다.
LXC 컨테이너는 unprivileged 모드에서 UID/GID 매핑이 달라지기 때문에, 호스트에서 chmod 777을 해주거나 정확한 UID 매핑을 설정해야 합니다. 홈랩이라 777로 간단히 처리했지만, 실서비스라면 적절한 UID 매핑을 권장합니다.
4. Nginx 캐시 전략
개발 중에 파일을 자주 바꾸다 보니 캐시가 적이 되었습니다. 수정한 JS가 반영이 안 되거나, 새 기능을 넣었는데 옛날 코드가 실행되는 상황이 반복됐습니다.
4-1. 파일 유형별 캐시 설정
# JS, CSS, HTML, JSON — 항상 최신 파일
location ~* \.(js|css|png|ico|json)$ {
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
# 폰트 — 7일 캐시 (거의 안 바뀜)
location ~* \.(woff2)$ {
expires 7d;
add_header Cache-Control "public, immutable";
}
immutable은 폰트처럼 절대 안 바뀌는 파일에만 적용해야 합니다. JS에 걸면 업데이트가 반영되지 않아서 고생합니다.

4-2. 최종 해결책 — 버전 쿼리
Nginx 설정만으로는 부족했습니다. 브라우저 캐시, PWA Service Worker 캐시 등 여러 레이어에서 캐싱이 발생하기 때문입니다. 결국 버전 쿼리 스트링으로 정착했습니다.
<script src="/sync-wrapper.js?v=4.7"></script>
파일을 업데이트할 때마다 버전을 올립니다.
sed -i 's|sync-wrapper.js?v=4.7|sync-wrapper.js?v=4.8|' /var/www/timebox/index.html
쿼리 스트링이 바뀌면 브라우저가 완전히 새로운 리소스로 인식합니다. 원시적이지만 확실한 방법입니다.
5. 개인정보 관련 — 가족용 앱이라도 챙길 건 챙기자
5-1. 관리자가 볼 수 있는 것
PocketBase 관리자는 모든 유저의 이메일, 이름, 타임박싱 데이터를 볼 수 있습니다. 비밀번호는 bcrypt 해시로 저장되어 원본 비밀번호를 복원할 수 없습니다. 관리자라 해도 비밀번호는 알 수 없으니, 가족이 "내 비밀번호 뭐였지?" 하면 초기화해주는 수밖에 없습니다.
5-2. 회원가입 제한
기본 설정에서는 누구나 회원가입이 가능합니다. 가족/지인끼리만 쓸 거라면 PocketBase 관리자에서 users 컬렉션의 Create rule을 비워서 가입을 막고, 관리자가 직접 계정을 만들어주는 게 안전합니다. URL을 알게 된 외부인이 가입해서 데이터를 볼 수 있는 걸 방지할 수 있습니다.
6. 전체 프로젝트 회고
6-1. 최종 파일 구성
/var/www/timebox/
├── index.html # 메인 앱
├── sync-wrapper.js # 동기화 + UI 바
├── login.html # 로그인
├── review.html # 주간 리뷰
├── manifest.json # PWA
├── sw.js # Service Worker
├── fonts/ # Pretendard + MaruBuri
└── icons/ # PWA 아이콘
6-2. 기술 스택
| 영역 | 선택 | 이유 |
|---|---|---|
| 프론트엔드 | 순수 HTML/CSS/JS | 프레임워크 학습 비용 없이 바로 시작 |
| 백엔드 | PocketBase | Go 바이너리 하나로 인증 + DB + API |
| 웹서버 | Nginx | SSL 터미네이션 + 리버스 프록시 |
| 인프라 | Proxmox LXC | 홈랩에서 가볍게 운영 |
| SSL | Let's Encrypt | 무료 인증서 자동 갱신 |
6-3. 완성된 기능 목록
1~8화를 거치며 완성된 기능입니다.
- 오버레이형 타임그리드 (클릭=DONE, 드래그=PLAN, 길게 누르기=편집)
- 15분 단위 자유 시간 설정
- BIG 3 + Brain Dump + 메모
- 현재 시간 빨간 표시줄
- 반복 템플릿 (PLAN + 텍스트 전부 저장)
- 어제 PLAN 복사
- 주간 리뷰 대시보드
- 멀티 유저 (PocketBase 인증)
- 공유 일정 (전체/특정 유저)
- 다크 모드
- 모바일 반응형 + 드로어
- PWA (앱 설치 + 오프라인)
- 자동 백업
6-4. 숫자로 돌아보기
| 항목 | 수치 |
|---|---|
| 총 개발 대화 | 수백 턴 |
| index.html | 약 650줄 |
| sync-wrapper.js | 약 900줄 |
| 데이터 버전 | v1 → v2 → v3 (3번 리팩토링) |
| 캐시 때문에 날린 시간 | 셀 수 없음 |

6-5. 인프라 엔지니어가 웹 개발을 하면
서버 올리고 Nginx 설정하는 건 익숙한 영역이었습니다. 하지만 CSS 레이아웃, 터치 이벤트, 브라우저 캐시 같은 프론트엔드 영역은 완전히 다른 세상이었습니다. 특히 모바일 터치 이벤트는 다시 하고 싶지 않을 정도로 까다로웠습니다.
그래도 Claude와 대화하면서 만드니까 "막히면 멈추는" 구간이 거의 없었습니다. 막히면 바로 물어보고, 코드를 받고, 안 되면 디버깅 과정을 공유하고, 다시 물어보고. 이 사이클이 빨라서 3일 만에 여기까지 올 수 있었습니다.
6-6. 앞으로 해볼 것
- 친구 맺기 / 그룹 시스템
- 월간 캘린더 뷰
- 데이터 백업/내보내기 (JSON)
- PDF 내보내기
종이 플래너에서 시작한 프로젝트가 PWA 앱까지 왔습니다. 아직 갈 길이 남았지만, 매일 가족이 실제로 쓰고 있는 앱이라는 게 가장 뿌듯합니다.
7. 시리즈를 마치며 — 시작하려는 분들에게
이 시리즈는 "비개발자가 AI와 함께 웹앱을 만든 기록"입니다. 코드의 품질보다는 "실제로 돌아가는 걸 만드는 과정"에 초점을 맞췄습니다. 완벽하지 않아도 매일 쓸 수 있는 수준이면 충분합니다.
비슷한 프로젝트를 시작하려는 분들에게 드리는 조언입니다.
처음부터 완벽하게 만들려고 하지 마세요. 종이에 그려보고, 가장 기본적인 기능부터 시작하세요. 이 앱도 15분 슬롯 클릭 하나에서 시작했습니다.
데이터 버전 필드를 처음부터 넣으세요. 나중에 레이아웃을 바꿀 때 구원받습니다. 마이그레이션 없이 구버전 데이터를 새 포맷으로 변환할 수 있습니다.
모바일은 반드시 실기기에서 테스트하세요. 에뮬레이터와 실제 터치 동작은 다릅니다. 특히 iOS Safari는 예상 밖의 동작이 많습니다.
캐시는 적입니다. 버전 쿼리 스트링을 처음부터 도입하세요. 나중에 "왜 수정한 게 안 보이지?" 하는 시간을 아낄 수 있습니다.
AI에게 "왜?"를 물어보세요. 코드만 받으면 디버깅할 때 막힙니다. 왜 이렇게 작성했는지 이해해야 문제가 생겼을 때 방향을 잡을 수 있습니다.
8편에 걸친 시리즈를 읽어주셔서 감사합니다.
참고 링크
- PocketBase 공식 문서 — Going to Production
- PocketBase 백업 및 복원 가이드
- Nginx Location 블록 우선순위 — DigitalOcean
- Nginx Reverse Proxy 공식 문서
👉 #1화 — 타임박싱이란? 앱 만들게 된 이유
👉 #2화 — PocketBase + Nginx + Proxmox 서버 인프라 구축
👉 #3화 — 멀티유저 인증과 데이터 동기화
👉 #4화 — 전면 리디자인: 오버레이형 통합 레이아웃
👉 #5화 — 모바일 최적화: 터치와의 전쟁
👉 #6화 — PWA: 웹앱을 앱처럼 쓰는 마법
👉 #7화 — 가족끼리 일정 공유
'IT > AI' 카테고리의 다른 글
| [AI] Claude와 함께 타임박싱 웹앱 만들기 #11 — 리뷰 대시보드 강화 + Plan 레이어 보존 (0) | 2026.03.30 |
|---|---|
| [AI] Claude와 함께 타임박싱 웹앱 만들기 #10 — 가족 대시보드, 주간 목표, 알림 (0) | 2026.03.29 |
| [AI] Claude와 함께 타임박싱 웹앱 만들기 #9 — 소셜 시스템 + 인라인 캘린더 (0) | 2026.03.27 |
| [AI] Claude와 함께 타임박싱 웹앱 만들기 #7 — 가족끼리 일정 공유 (0) | 2026.03.25 |
| [AI] Claude와 함께 타임박싱 웹앱 만들기 #6 — PWA: 웹앱을 앱처럼 쓰는 마법 🪄 (0) | 2026.03.24 |
| [AI] Claude와 함께 타임박싱 웹앱 만들기 #5 — 모바일 최적화: 터치와의 전쟁 (0) | 2026.03.23 |