유대선
프로젝트로
·기술 회고·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.tsdrizzle-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/storageStorageDriver 인터페이스 + 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.