1편 요약 — 새로고침하면 다 날아간다고요?
안녕하세요, 13년차 인프라 엔지니어이자 4자녀의 아빠입니다. 지난 1편에서 Claude와 함께 타임박싱 웹앱의 기본 UI를 뚝딱 만들었는데요, 한 가지 치명적인 문제가 있었습니다. 새로고침 한 번이면 오늘 정성스럽게 짠 하루 계획이 증발한다는 거였죠. 마치 아이들이 정리한 레고를 고양이가 밟고 지나간 느낌이랄까요(고양이는 없지만요).
데이터를 어딘가에 저장해야 했습니다. 클라우드 서비스? 물론 편하죠. 하지만 저는 인프라 엔지니어입니다. 집에 Proxmox 서버가 떡하니 돌아가고 있는데, 남의 서버에 제 일정을 맡기는 건 자존심이 허락하지 않았습니다. 셀프호스팅의 길을 선택했고, 그 여정이 바로 이번 2편의 이야기입니다.

PocketBase를 선택한 이유 — 14MB짜리 백엔드의 매력
백엔드를 뭘로 할지 꽤 고민했습니다. Firebase, Supabase 같은 BaaS(Backend as a Service)도 후보에 올랐는데요, 제가 원했던 조건은 명확했습니다. 첫째, 반드시 내 서버에서 돌아갈 것. 둘째, 설정이 5분 안에 끝날 것. 셋째, 인증 시스템이 기본 내장되어 있을 것. 넷째, 백업이 쉬울 것.
PocketBase는 이 네 가지를 전부 만족시키는 녀석이었습니다. Go 언어로 만들어진 단일 바이너리 파일인데, 용량이 고작 14MB 남짓합니다. 이 조그만 파일 하나를 실행하면 REST API, 관리자 대시보드, 인증 시스템이 한 번에 뜹니다. 데이터베이스는 SQLite를 사용하기 때문에 별도의 DB 서버 설치도 필요 없고요. 2026년 3월 기준 최신 버전은 v0.36.x대로, 아직 정식 v1.0 출시 전이지만 사이드 프로젝트용으로는 더없이 훌륭합니다.
서버 시작이 얼마나 간단하냐면, 터미널에서 딱 한 줄이면 됩니다.
./pocketbase serve --http 127.0.0.1:8090이게 전부입니다. 진짜로요. REST API가 8090 포트에서 열리고, 브라우저에서 /_/ 경로로 접속하면 깔끔한 관리자 대시보드가 나옵니다. 처음 접속하면 슈퍼유저 계정 생성 페이지가 뜨는데, 이메일과 비밀번호만 넣으면 바로 사용할 수 있습니다.
Firebase나 Supabase 대비 PocketBase의 가장 큰 장점은 데이터 주권입니다. 모든 데이터가 내 서버의 SQLite 파일 하나에 들어있으니 백업은 파일 복사 한 번이면 끝이고, 나중에 마이그레이션도 부담이 없습니다. 물론 대규모 서비스에는 부적합하지만, 저처럼 가족 몇 명이 쓰는 사이드 프로젝트에는 최적의 선택이었습니다.
Proxmox LXC 컨테이너로 가볍게 인프라 올리기
이제 이 PocketBase를 어디에 올릴지 정해야 합니다. 제 Proxmox 서버에는 이미 여러 VM과 LXC 컨테이너가 돌아가고 있는데요, 타임박싱 앱 정도의 가벼운 서비스라면 LXC 컨테이너가 딱입니다.
LXC(Linux Container)는 호스트 OS의 커널을 공유하는 경량 가상화 기술인데, VM에 비해 메모리와 CPU 오버헤드가 현저히 낮습니다. 쉽게 말하면, VM이 통째로 방 하나를 빌리는 거라면 LXC는 칸막이만 쳐서 공간을 나눠 쓰는 느낌이에요. Proxmox 9.x 버전부터는 OCI 이미지 지원도 추가되어 더욱 유연해졌습니다.
저는 Ubuntu 24.04 LXC 컨테이너를 하나 생성했습니다. 스펙은 1코어, 512MB RAM, 4GB 디스크. 이게 다냐고요? 타임박싱 앱 하나 돌리는 데 이것만으로도 충분합니다. 오히려 남는 리소스가 아까울 정도예요. 홈랩의 좋은 점이 바로 이겁니다. 이런 소소한 실험을 부담 없이 해볼 수 있다는 거죠.

Nginx 리버스 프록시 설정 — 외부에서 안전하게 접속하기
PocketBase가 8090 포트에서 돌아가고 있지만, 외부에서 접속하려면 제대로 된 웹 서버가 앞단에 서야 합니다. 여기서 Nginx가 등장합니다. Nginx를 리버스 프록시로 설정하면 SSL 인증서 처리, 정적 파일 서빙, 그리고 PocketBase로의 요청 라우팅을 한 번에 해결할 수 있습니다.
구조는 이렇습니다. 외부에서 https://timebox.example.com:9443으로 접속하면 Nginx가 요청을 받습니다. 일반 페이지 요청은 정적 HTML/CSS/JS 파일을 직접 서빙하고, /pb/ 경로로 들어오는 API 요청만 PocketBase(localhost:8090)로 프록시합니다.
server {
listen 9443 ssl http2;
server_name timebox.example.com;
ssl_certificate /etc/letsencrypt/live/timebox.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/timebox.example.com/privkey.pem;
root /var/www/timebox;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location ^~ /pb/ {
proxy_pass http://127.0.0.1:8090/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}여기서 주의할 점이 몇 가지 있습니다. 먼저, SSL 인증서는 Let's Encrypt로 무료 발급받았습니다. certbot으로 간단하게 처리할 수 있죠. 그리고 WebSocket 업그레이드 헤더(Upgrade, Connection)를 꼭 넣어줘야 합니다. PocketBase의 실시간 구독 기능이 WebSocket을 사용하거든요. 지금 당장은 안 쓰더라도 나중을 위해 미리 설정해두는 게 좋습니다.
proxy_set_header X-Real-IP와 X-Forwarded-For도 잊지 마세요. 이걸 빼먹으면 PocketBase 로그에 모든 접속자의 IP가 127.0.0.1로 찍히는 슬픈 상황이 벌어집니다. 리버스 프록시 설정에서 가장 흔한 실수 중 하나인데, 의외로 많은 분들이 놓치시더라고요.
데이터 설계 — 하루를 통째로 JSON에 담다
이제 서버 인프라는 준비됐으니 데이터를 어떻게 저장할지 고민할 차례입니다. 타임박싱은 하루를 30분(또는 15분) 단위로 쪼개서 계획하는 기법인데요, 셀 하나하나를 개별 레코드로 저장하면 하루에 수십~수백 개의 레코드가 생깁니다. 쿼리도 복잡해지고 성능도 걱정되죠.
그래서 과감하게 하루 전체를 하나의 JSON으로 저장하는 방식을 택했습니다. PocketBase의 timebox_daily 컬렉션 구조는 이렇습니다. date 필드에 날짜(YYYY-MM-DD 형식), payload 필드에 페이지 전체 상태를 JSON 문자열로, 그리고 user 필드에 사용자 관계를 저장합니다.
payload에는 HTML 테이블의 모든 td 요소의 className, style, innerHTML을 캡처해서 통째로 담습니다. 복원할 때는 그대로 다시 넣으면 되니까요. 엔지니어적 관점에서 보면 좀 무식한 방법이긴 한데, 실용성은 최고입니다. 스키마 변경에 유연하고, 구현이 단순하며, 디버깅도 쉽습니다.
{
"v": 3,
"in": {},
"td": {
"t0": {"c": "h-cell"},
"t1": {"c": "slot pc-2", "h": "<span class=\"slot-label\">업무 시작</span>"}
}
}여기서 v 필드는 데이터 버전 번호입니다. 레이아웃을 바꿀 때마다 td 구조가 달라지기 때문에, 이전 버전 데이터를 로드할 때 호환성 처리를 위해 넣어둔 것이죠. v1은 30분 단위 초기 버전, v2는 15분 단위에 PLAN/DONE 분리, v3는 오버레이형 통합 최신 버전입니다. 이런 버전 관리 필드는 작은 프로젝트에서도 처음부터 넣어두면 나중에 정말 고마워집니다.

동기화 전략 — 디바운스가 핵심이다
데이터를 저장할 구조는 정했는데, 언제 저장할 것이냐가 문제입니다. 사용자가 타임슬롯에 색칠할 때마다 저장하면 API 호출이 폭탄처럼 쏟아집니다. 드래그 한 번에 수십 개의 셀이 바뀌거든요.
여기서 디바운스(Debounce) 패턴이 등장합니다. 마지막 변경 후 2초 동안 추가 변경이 없으면 그때 한 번만 저장하는 방식이에요. sync-wrapper.js라는 파일이 이 모든 동기화 로직을 담당하는데, 핵심 동작은 이렇습니다. 페이지 로드 시 PocketBase에서 해당 날짜 데이터를 불러오고, 사용자가 수정하면 2초 디바운스 후 자동 저장합니다. 저장 실패 시에는 localStorage에 백업하고, 오프라인 상태에서는 localStorage만 사용합니다.
이 작은 파일이 처음에는 100줄 정도였는데, 상단 바 생성, 인증 상태 관리, 날짜 변경 처리, 데이터 버전 호환성 등 기능이 추가되면서 지금은 800줄이 넘어가 있습니다. 코드가 유기체처럼 자라는 걸 보면 소프트웨어 개발의 묘미를 느끼게 됩니다(그리고 약간의 공포도요).
멀티 유저 구현 — 와이프가 쓰고 싶다고 했다
처음에는 혼자 쓸 생각이었습니다. 그런데 와이프가 옆에서 보더니 "나도 쓸 수 있어?"라고 묻더군요. 4자녀 가정에서 부부의 시간 관리는 생존 전략이니까요. PocketBase에 인증 시스템이 내장되어 있으니 활용하지 않을 이유가 없었습니다.
로그인 페이지를 만들었습니다. 이메일과 비밀번호로 로그인하거나 회원가입할 수 있고, PocketBase의 authWithPassword와 create API를 사용합니다. 로그인 성공하면 pb.authStore에 토큰이 저장되고, 이후 모든 API 호출에 자동으로 인증 헤더가 붙습니다. 별도의 JWT 처리나 세션 관리 코드를 짤 필요가 없으니 정말 편하죠.
// 로그인
await pb.collection('users').authWithPassword(email, password);
// 회원가입
await pb.collection('users').create({
email, password, passwordConfirm: password, name
});사용자별 데이터 분리는 PocketBase의 API Rules로 간단하게 해결했습니다. List와 View 규칙에 user = @request.auth.id를 설정하면 각 사용자는 자기 데이터만 볼 수 있습니다. Create 규칙은 @request.auth.id != ""로 로그인한 사용자만 데이터를 만들 수 있게 하고, Update와 Delete도 마찬가지로 본인 데이터만 수정/삭제 가능하도록 설정했습니다.
재미있었던 건 기존 데이터 마이그레이션이었습니다. 멀티유저 도입 전에 이미 제 데이터가 쌓여있었거든요. user 필드가 비어있는 레코드들을 제 계정에 연결하는 작업이 필요했는데, 간단한 스크립트로 처리했습니다. 사소해 보이지만, 이런 마이그레이션 작업은 백엔드를 다루다 보면 항상 만나게 되는 일이에요. 작은 프로젝트에서도 예외는 없습니다.

보안 설정 — 셀프호스팅이라고 방심하면 안 됩니다
집에서 돌리는 서비스라고 해도 외부에서 접속 가능하게 열어두면 보안은 필수입니다. 몇 가지 기본적인 조치를 해뒀는데요, 먼저 PocketBase는 localhost(127.0.0.1)에서만 리스닝하도록 설정해서 외부에서 직접 접근할 수 없게 했습니다. 모든 외부 트래픽은 반드시 Nginx의 SSL을 통해서만 들어오도록 한 거죠.
Let's Encrypt SSL 인증서로 HTTPS를 적용했고, PocketBase의 API Rules를 통해 인증되지 않은 사용자의 데이터 접근을 원천 차단했습니다. 또한 Nginx 설정에서 불필요한 서버 정보 노출을 막기 위해 server_tokens off;를 추가했습니다. 완벽한 보안은 아니지만, 개인 프로젝트 수준에서는 충분한 조치입니다.
한 가지 팁을 드리자면, PocketBase 관리자 대시보드(/_/ 경로)는 프록시 설정에서 외부 접근을 차단하거나 별도의 인증을 추가하는 것을 추천합니다. 관리자 대시보드가 인터넷에 그대로 노출되면 브루트포스 공격의 대상이 될 수 있으니까요.
이번 편에서 배운 것들
PocketBase는 사이드 프로젝트용 백엔드로 정말 훌륭합니다. 설정 5분, 단일 바이너리, SQLite 기반이라 백업도 간편하죠. 하루 전체를 JSON으로 저장하는 방식은 단순하지만 실용적이고 스키마 변경에 유연합니다. 디바운스 패턴은 실시간 자동 저장에 필수적인 기법이고, 데이터 버전 필드는 아무리 작은 프로젝트라도 처음부터 넣어두면 두고두고 도움이 됩니다. 그리고 무엇보다, 멀티유저 기능은 가족이 "나도 쓸래"라고 한 마디 하는 순간 필요해집니다.
다음 편에서는 타임박싱 앱에 템플릿 기능, 주간 리뷰, 다크모드 같은 편의 기능을 추가한 이야기를 다뤄보겠습니다. 매일 쓰는 앱이 되려면 이런 디테일이 중요하더라고요. 그럼 다음 편에서 뵙겠습니다!
👉 #1화 — 타임박싱이란? 앱 만들게 된 이유
👉 #2화 — PocketBase + Nginx + Proxmox 서버 인프라 구축
👉 #3화 — 멀티유저 인증과 데이터 동기화
👉 #4화 — 전면 리디자인: 오버레이형 통합 레이아웃
👉 #5화 — 모바일 최적화: 터치와의 전쟁
👉 #6화 — PWA: 웹앱을 앱처럼 쓰는 마법
👉 #7화 — 가족끼리 일정 공유
👉 #8화 — 가족끼리 일정 공유
'IT > AI' 카테고리의 다른 글
| [AI] Claude와 함께 타임박싱 웹앱 만들기 #5 — 모바일 최적화: 터치와의 전쟁 (0) | 2026.03.23 |
|---|---|
| [AI] Claude와 함께 타임박싱 웹앱 만들기 #4 — 전면 리디자인: 오버레이형 통합 레이아웃 (1) | 2026.03.22 |
| [AI] Claude와 함께 타임박싱 웹앱 만들기 #3 — 반복 템플릿, 주간 리뷰 대시보드, 그리고 다크모드까지 (1) | 2026.03.20 |
| [AI] Claude와 함께 타임박싱 웹앱 만들기 #1 — 종이 플래너를 웹으로 옮긴 인프라 엔지니어의 바이브 코딩 도전기 (1) | 2026.03.18 |
| [AI] 월 구독료 0원의 반란: 홈랩 엔지니어가 Proxmox에 Ollama + Open WebUI로 나만의 ChatGPT를 만든 이유 (0) | 2026.03.15 |
| [AI] 13년 차 엔지니어가 분석한 '클로드 코드(Claude Code)': 터미널에 강림한 완벽한 AI 에이전트 (1) | 2026.03.02 |