·기술 회고·5 분·리뷰 필요
스택 결정 전부: 무엇을, 왜, 대안은, 그리고 프로덕션 전환점
버팀목 MVP에서 내린 모든 기술 스택 결정을 근거·대안·프로덕션 전환점과 함께 정리. 핵심 패턴은 '로컬 기본값을 인터페이스 뒤에 두고, 프로덕션 키는 한 파일만 갈아끼우기'.
AI 버전
work.md §16이 스택을 확정했고, 그 위에서 "외부 계정 0개 즉시 실행"을 위해 각 항목을 인터페이스 뒤의 로컬 구현으로 채웠다. 각 결정의 왜와 프로덕션 전환점:
프레임워크 — Next.js 16 (App Router)
풀스택(UI+API+RSC). 사양 확정 항목. React 19, Tailwind v4. dev/build 모두 Turbopack.
DB — Drizzle + PGlite (임베디드 PostgreSQL)
- 왜: 사양은 PostgreSQL 확정. PGlite는 WASM PostgreSQL이라 "PostgreSQL"을 지키면서 외부 DB 서버 0개로 즉시 실행. Drizzle에
drizzle-orm/pglite드라이버 존재. - 대안: better-sqlite3(스펙 이탈 — SQLite), 도커 Postgres(외부 의존).
- 전환점:
lib/db/index.ts만drizzle-orm/node-postgres+DATABASE_URL로 교체. 스키마/쿼리 동일. - 주의: 단일 연결 WASM이라 동시성·손상에 취약 → 별도 항목(
pglite-embedded-db)에 상세. 마이그레이션은 drizzle-kit 대신 런타임CREATE TABLE IF NOT EXISTS(lib/db/init.ts)로 첫 부팅 즉시 동작.
인증 — 세션 쿠키 dev 로그인 (인터페이스 뒤)
- 왜: 사양은 Better Auth + 카카오/애플. 하지만 소셜 로그인은 진짜 키가 필요 → "멈추지 말 것" 지침에 따라 HMAC 서명 세션 쿠키 + 이름/이메일 dev 로그인을
lib/auth인터페이스 뒤에 둠. 세션 180일(§15 "재로그인 매끄럽게"). - 대안: Better Auth를 처음부터(키 없이는 소셜 미동작, 설정 마찰), 매니지드(Clerk 등 — 신원을 외부에 맡김 = 신뢰 우려).
- 전환점:
getCurrentUser/login/logout시그니처 유지하며 교체. 이후 실제로 카카오 OAuth를 직접 연동(kakao-oauth-debug참고) — 사양의 "카카오는 제너릭 OAuth로"를 그대로. - 보안: dev 로그인은 프로덕션에서
NODE_ENV가드로 완전 비활성(ed3600c).
스토리지 — 로컬 파일 드라이버 (인터페이스 뒤)
- 왜: §13 "공개 URL 금지, 주인만".
lib/storage의StorageDriver인터페이스 +LocalStorageDriver(.data/media) + 인증 게이트 라우트(/api/items/[id]/media)로 쿠키 세션 통제. 경로 탈출 방지 포함. - 전환점:
S3StorageDriver(R2/S3 비공개 버킷 + 서명 URL) 추가,put/get/delete만 지키면 라우트 그대로.
PWA — 직접 작성한 Service Worker (Serwist 아님)
- 왜: 사양은 Serwist 또는 수동. Turbopack 호환·완전 제어를 위해 수동
public/sw.js(앱 셸 캐시·push·notificationclick) 선택. 신뢰성 우선. - 대안: Serwist(Turbopack 엣지케이스 우려), next-pwa(아카이브/미유지).
- 매니페스트 + iOS '홈 화면에 추가' 안내 포함.
웹 푸시 — VAPID 자동 생성
- 왜: VAPID 키는 자가 생성 가능(외부 계정 불필요).
lib/env.ts가.data/secrets.json에 자동 생성·보관 → 안드로이드/데스크톱 푸시가 실제로 작동. - 전환점: 프로덕션은
VAPID_*환경변수로 고정(키별 개별 적용 —ed3600c).
스케줄러 — node-cron + instrumentation
- 왜: 영속 Node 런타임에서 매분 평가 → reminder 시각 매칭 시 Web Push.
instrumentation.register()에서 1회 기동. - 주의(미래): 인메모리 dedupe라 다중 인스턴스/재시작엔 부정확 → 영속 마커 필요(PROGRESS에 기록).
오디오 — 서버 트랜스코딩 베스트-에포트
- 왜: §12 webm→mp4(아이폰 재생 보장). 단 ffmpeg가 이 머신에 없음 → 있으면 변환, 없으면 원본 저장 + 기록(
lib/audio.ts). MIME은 하드코딩 금지,MediaRecorder.isTypeSupported()로 후보 순차 선택.
아이콘 — 순수 Node PNG 인코더
- 왜: 외부 이미지 툴(sharp/ffmpeg/imagemagick) 없이 아이콘 생성 필요.
scripts/gen-icons.mjs가 zlib+자체 CRC32로 PNG를 직접 인코딩(따뜻한 클레이 디스크 안 크림빛 하트). 192/512/maskable/apple-touch. - 같은 인코더 패턴을 후에 샘플 사진 생성(
gen-sample-photo.mjs)과 데이터 내보내기 ZIP(lib/zip.ts, store 방식)에도 재사용.
한 줄 패턴
로컬 기본값을 인터페이스 뒤에 두면, 프로덕션은 키를 넣고 구현 한 파일만 교체하는 일이 된다. 사양의 "멈추지 말고 기록하고 진행"이 이 패턴으로 구현됐다.
리뷰 필요
내 시각이 아직 안 들어간 entry.