본문으로 건너뛰기
강홍재/ James
← Work
EMBA2026 · Solo Builder· Started(First Commit date)

SPARK Live

청중이 폰으로 슬라이드를 같이 넘겨 보며 그 슬라이드에 이름을 남겨 질문하고, 발표자는 공감 많은 질문부터 슬라이드별로 답하는 세미나 Q&A - 문서 4개에서 출발해 사흘, 커밋 252개로 실제 행사에 올렸고, 결과는 질문 5개였다.

  • Next.js
  • TypeScript
  • Supabase Realtime
  • Cloudflare Workers
  • Self-hosted
  • Playwright
  • QA

Setup

Problem

세미나에서 질문은 잘 안 나온다. 손 들고 묻기 어려운 분위기라 궁금한 게 있어도 그냥 넘어가고, 어쩌다 나온 질문도 어느 슬라이드에서 나왔는지 모르면 발표자가 답하기 어렵다. 행사가 끝나면 슬라이드와 질문이 흩어져서 동아리에 남는 게 없다. BRD에 적은 문제는 이 세 개였다. 실시간 Q&A 서비스는 이미 있다. 그런데도 직접 만든 이유는 둘이다. 하나는 SPARK 행사 흐름에 맞추기 위해서고, 다른 하나는 세미나 주제가 "글로 쓸 수 있다면, 만들 수 있다"였기 때문이다. 개발자가 아닌 사람도 BRD·PRD·IA·CLAUDE 문서 4개로 서비스를 만들 수 있다는 걸 세미나에서 결과물로 보여주고, 그 문서를 참가자에게 샘플로 나누는 것까지가 목표였다. 이 앱은 그 주장의 증거물로 만들어졌다.

Context

성균관대 EMBA 창업동아리 SPARK의 AI 세미나(2026-09-28 19:30, 한국컨퍼런스센터)에 쓰려고 만들었다. 첫 커밋이 9월 26일 21:41, 행사가 28일 저녁이었으니 이틀 남짓이었다. 커밋 252개 중 238개가 27일 하루에 몰려 있고, 종류로 나누면 docs 128·feat 64·fix 30·test 27·refactor 3이다. 코드보다 문서 커밋이 많다는 게 이 프로젝트의 성격이다. 문서 4개를 먼저 쓰고, 그 문서에서 PRD 완료 기준(AC) 70개를 뽑고, AC 하나당 Playwright 테스트 하나를 붙이는 순서로 갔다. 운영은 Cloudflare Worker 앞단에 Mac mini 셀프호스팅 Supabase를 임시 Tunnel로 붙인 구성이다. 임시 Tunnel은 재시작마다 주소가 바뀌고 동시 요청 200개 한도가 있다는 걸 알고도 첫 행사는 이 구성으로 가기로 했고, 100명을 넘길 것 같으면 이름 있는 Tunnel로 바꾸기로 적어뒀다.

Users

사용자는 셋이다. 청중은 EMBA 원우, 비개발자, 스마트폰이고 로그인이나 가입 없이 QR 한 번으로 들어온다. 발표자는 노트북과 프로젝터 앞에서 발표 중에는 넘기기만 한다. 운영진은 세션 전후에 슬라이드를 올리고 기록을 확인한다. 실제 행사의 명단은 46명이었다. 솔직하게 적으면, 실제로 쓴 사람은 그보다 훨씬 적다. 참여 흔적이 남은 기기는 8대, 질문을 남긴 기기는 4대였다. 입장만 한 기기는 기록하지 않아서 몇 명이 QR로 들어왔는지는 모른다.

Hypothesis

질문이 안 나오는 이유가 용기나 관심의 부재가 아니라 "지금 보는 슬라이드에, 손 들지 않고, 이름만 남기고" 물을 수 있는 경로의 부재라면, 폰에서 슬라이드를 같이 보며 그 자리에서 묻게 하는 것만으로 세션당 질문 10개는 나온다. BRD의 성공 기준은 동시 접속 50명에서 끝까지 동작(B-1), 세션당 질문 10개 이상(B-2), 참석자 70% 이상 QR 접속(B-3)이었고 셋 다 "확인 필요" 표시를 달고 있었다.

Build

What I did
  • 문서 4개(BRD·PRD·IA·CLAUDE)를 먼저 쓰고, PRD 완료 기준 AC-01~AC-70을 Playwright 테스트 69개로 옮겼다. 테스트 이름이 AC 번호로 시작하고("AC-05 발표자가 다음을 누르면 현재 슬라이드를 보던 청중 화면이 2초 안에 같은 번호로 바뀐다"), 2초 기준은 tests/helpers.ts의 상수 하나로 고정했다
  • 청중 화면 - QR로 입장, 발표자가 넘기면 2초 안에 같은 슬라이드, 보고 있는 슬라이드에 이름을 적고 질문, 반응 3종. 발표자 화면 - 비밀 링크로 열고 넘기기, 새 질문 알림, 폰 리모컨 탭. 운영진 - 슬라이드 업로드·숨김·순서·영상, 명단 입장, 자료 배포, 만족도 조사
  • Q&A 정리 - 질문을 슬라이드 번호 순으로 묶고 각 묶음 안에서 공감 수(반응 3종의 합)가 많은 순으로 보여주고, 답변 글을 남길 수 있게 했다. 어느 슬라이드를 언제 넘겼는지의 흐름 기록은 DB 트리거가 남기게 해서 넘기기 함수나 상태 API를 고쳐도 기록이 빠지지 않게 했다
  • 실시간 - Supabase Realtime 비공개 broadcast 채널 + presence. 끊기면 1.5초×2^n(최대 10초)으로 재접속하고 그동안은 3초 HTTP 폴링으로 버틴다. 알림은 DB 트리거가 realtime.send로 보낸다
  • 슬라이드 - PDF를 브라우저에서 pdf.js로 가로 1600px JPEG(품질 0.85)로 변환해 올린다. 한글 CMap·표준 글꼴·wasm은 postinstall로 복사. PPTX 변환 서버(Gotenberg)는 GOTENBERG_URL이 있을 때만 켜지고 운영에는 없어서, PPTX는 PowerPoint에서 PDF로 내보내 올린다
  • 배포 - Vercel에서 Cloudflare Workers(vinext 빌드)로 옮기고, spark-live.pages.dev는 요청을 Worker로 넘기는 Pages 프록시로 뒀다. 백엔드는 Mac mini의 셀프호스팅 Supabase(v0.8.2, Envoy 게이트웨이)를 cloudflared 임시 Tunnel로 노출. Tunnel 주소가 바뀌면 재빌드 없이 Worker 비밀값만 바꾸는 스크립트를 뒀다
  • 부하 - AC-32 테스트에서 청중 50명이 동시에 입장하고 넘기기 3회 모두 이미지까지 2초 안, 10명 동시 질문 2초 안, 반응 150개 직후 넘기기 2초 안을 검증. Realtime 기본 한도(초당 100 이벤트)는 50명이 반응을 몰아 누르면 넘쳐서 넘김·질문·알림이 5~60초 동안 조용히 버려지는 걸 확인하고 5000/1000/500으로 올리는 스크립트를 만들었다
  • 운영 기록 - STATUS.md에 배포 24버전마다 되돌리기 명령과 사후 점검(홈·/events·/new 200, 없는 세션 404), DB 변경 여부, 마이그레이션·삭제 전 백업 7개 경로, 리허설 3회, 행사 전 체크리스트와 당일 롤백 계획을 남겼다
Product decisions
  • 익명 질문을 받지 않고 이름을 필수로 - 가능하면 본명을 쓰도록 이끈다. 질문이 안 나오는 걸 익명으로 풀 수도 있었지만, 동아리 세미나에서 남는 기록의 가치는 누가 무엇을 물었는지에 있다고 봤다. 슬라이드 v6에서 "익명" 문구까지 교체했다
  • 청중 회원가입·로그인·참가 신청을 만들지 않고, 운영진·발표자 계정도 비밀 링크로 대신했다. 대신 기기 ID를 브라우저가 정하므로 새 ID로 10초 간격과 반응 1회 제한을 우회할 수 있다는 한계를 문서에 그대로 적어뒀다
  • 발표자 비밀키를 주소창에서 숨겼다 - 프로젝터나 화면 공유로 주소가 보여도 발표자 권한이 새지 않게. 키는 sha256 해시만 저장하고 referrer도 보내지 않는다
  • 데이터 삭제를 코드 규칙이 아니라 권한으로 막았다 - service_role에서 DELETE와 TRUNCATE 권한을 회수하는 마이그레이션을 넣었고, DELETE 문과 cascade 삭제가 코드에 없다. 리허설 질문도 지워지지 않으니 리허설은 별도 세션으로 한다
  • PPTX 변환 서버 없이 운영 - 변환은 발표자가 PowerPoint에서 PDF로 내보내는 한 단계로 대신했다. 행사 이틀 전에 변환 서버를 하나 더 운영하는 리스크보다 안내 한 줄이 쌌다
  • 행사 전날 17:28에 변경 중단을 선언했다 - 코드·DB·설정 모두 그대로. 이후 배포는 "변경 중단의 예외"라고 STATUS에 명시하고 넣었다
  • 리허설에서 나온 요구를 당일 구현 - 발표자가 콘솔에서 5번까지 갔다가 2번으로 돌아오자 청중이 1~2번만 볼 수 있게 된 걸 보고, 청중은 넘긴 가장 먼 슬라이드까지 볼 수 있게 바꿨다
  • 문서에 없는 건 추측하지 않고 "확인 필요"로 표시한 뒤 물었다 - 문서의 "확인 필요"를 적힌 대로 구현한 것 16건, 문서에 없어서 정한 것 11건을 STATUS에 목록으로 남겼다. 코드에도 "확인 필요" 주석이 16곳 남아 있다
QA considerations
  • 완료 기준이 테스트와 1:1인가 - PRD의 AC 70개 중 68개가 Playwright 테스트 69개에 이름으로 박혀 있다. 빠진 AC-54·55는 구현하지 않은 F-14(질문 사전 승인)의 것이라, 테스트 목록이 곧 구현 범위다. 다만 CI는 없어서 테스트는 로컬 pnpm test로만 돈다
  • 50명이 동시에 반응을 눌러도 슬라이드 넘김이 전달되는가 - Realtime 한도 초과분은 에러 없이 조용히 버려진다는 게 제일 위험했다. 한도를 올리고, 로컬 50명 흉내에서 넘기기(이미지까지) 0.5~1초·질문 도착 0.2초, 실기기 부하에서 60·100대는 약 1.1초 실패 0, 150대부터 429/500 일부 실패를 기록했다
  • 흔들리는 테스트를 흔들린다고 적었는가 - 4워커 병렬로 두 번 돌려 65/69, 실패는 매번 다른 4개, 따로 돌리면 전부 통과. 원인은 부하 시 시간 기준과, 오래 켜 둔 개발 서버가 옛 코드를 그리던 것(trace로 확인)이었다. 69/69라고 적지 않았다
  • 행사 당일 되돌릴 수 있는가 - 배포 24버전마다 wrangler rollback 명령과 직전 버전 해시를 적고, DB 함수는 백업 SQL을 psql로 되돌리는 계획을 따로 뒀다. 마이그레이션·삭제 전 dump 7개
  • 리허설 데이터가 운영 DB를 오염시키지 않는가 - 리허설은 별도 세션으로 하고, 점검·리허설 세션 26개(질문 95, 반응 597, 슬라이드 행 308, 흐름 기록 76)를 행사 전에 지웠다. 삭제 금지 원칙의 예외라 PRD 5.6에 예외 한 줄을 추가하고 지웠다
  • 행사 직전 상태가 리허설 잔재가 아닌가 - 현재 슬라이드 번호가 19로 남아 있던 적이 있어서 "[진행] 전에 현재 번호가 1인지 확인"을 체크리스트에 넣었다. 슬라이드 전수 확인(1600×900, 글자 깨짐·잘림 없음), macOS 자동 업데이트 끔, 잠자기 끔, 운영 암호가 개발용 값이 아님을 401로 확인
  • 고치기 전에 실패를 먼저 봤는가 - 수정에 앞서 고치기 전 코드에서 테스트가 실패하는 걸 확인한 기록을 STATUS에 여러 번 남겼다(AC-42 등). 통과하는 테스트는 아무것도 증명하지 않을 수 있기 때문
  • 입력 경계 - PPTX는 매직 바이트로 검사해 변환 서버를 보호, 요청 본문 16KB 제한, 발표자 요청 6초 제한. 알려진 구멍도 적었다: 운영진 암호 시도 제한이 Workers에서는 isolate가 나뉘어 1분에 13번 틀려도 429가 안 난다, 질문 1000개를 넘으면 max_rows 때문에 목록이 잘린다

Outcome

Metrics
  • 커밋 252개, 2026-09-26 ~ 09-29 (KST). 09-26 7건, 09-27 238건, 09-29 7건. docs 128 / feat 64 / fix 30 / test 27 / refactor 3
  • 문서 1,183줄(PRD 449, STATUS 305, IA 166, README 136, BRD 78, CLAUDE 49), 추적 파일 175개, 마이그레이션 13개
  • PRD 완료 기준 70개, Playwright 테스트 69개(spec 17개), 기준 커버 68개. 전체 실행 2회 65/69, 개별 실행 전부 통과. CI 없음
  • 배포 24버전, 되돌리기 명령 22회 기록, 변경 전 백업 7개, 리허설 3회
  • 부하: 로컬 50명 흉내 넘기기 0.5~1초·질문 0.2초, 실기기 60·100대 약 1.1초 실패 0, 150대부터 일부 429/500
  • 행사(2026-09-28, 세션 XV357Y): 명단 46명, 참여 흔적 기기 8대, 질문 5개(기기 4대, 답변 1), 반응 5개(기기 4대), 만족도 조사 응답 1건. 19:35 시작, 20:42 마지막 넘기기(50장 중 29번)
  • 성공 기준 대비: B-2 질문 10개 이상은 5개로 못 미침. B-1 최대 동시 접속과 B-3 QR 접속률은 접속 기기 수를 DB에 남기지 않아 판단 못 함(참여 기기 기준 최소 17%)
Result / Learning

행사에 올라갔고, 끝까지 돌았고, 결과는 질문 5개였다. 목표는 10개였다. 세션은 19:35에 시작해 20:42에 50장 중 29번에서 멈췄고, 참여 흔적이 남은 기기는 명단 46명 중 8대였다. 만족도 조사 응답은 1건인데, 조사 카드가 세션 종료 뒤에 뜨도록 만들어놓고 세션을 다음 날 14:20에 종료해서 조사가 행사 뒤에야 열린 탓이 크다. 성공 기준 셋 중 B-2는 미달이고 B-1과 B-3은 판단할 수 없다. 접속 기기 수를 실시간으로만 보고 DB에 남기지 않아 최댓값이 없고, 입장만 한 기기는 기록하지 않아서다. 만든 쪽은 문서대로 됐다. 문서 4개에서 AC 70개가 나왔고 그중 68개가 테스트로 박혔고, 부하 테스트와 리허설 3회와 배포 24번을 거쳐 행사 전날 17:28에 변경을 멈췄다. 행사 중 앱 장애는 기록에 없다. "글로 쓸 수 있다면, 만들 수 있다"는 세미나 주제의 증거물로서는 역할을 했다. 그런데 이 두 문단을 나란히 놓으면 이 프로젝트의 진짜 결론이 보인다. 만드는 것과 쓰이게 하는 것은 다른 문제였다. 동작은 검증했지만 참여는 설계하지 않았다. 50명 부하 테스트는 통과했는데 실제로 들어온 기기는 8대였고, 그 8대가 왜 46명 중 8대였는지를 설명할 데이터가 없다.

Retrospective
  • B-1·B-3을 성공 기준으로 적어놓고 측정 수단을 만들지 않았다. presence는 실시간으로만 보이고 최댓값을 DB에 남기지 않았으며, 입장만 한 기기는 기록하지 않았다. 완료 기준 70개를 테스트로 박은 사람이 성공 기준 3개 중 2개를 측정 불가로 둔 것이다. 기능의 AC와 사업의 KPI를 같은 엄격함으로 다루지 않았다.
  • 만족도 조사를 세션 종료에 묶어놓고 세션을 다음 날 종료했다. 행사장에서 응답을 받을 유일한 창이 닫힌 채로 지나갔고 응답은 1건이다. 발표자가 넘기기를 멈춘 시점과 운영진이 종료를 누르는 시점이 다르다는 걸 리허설 3번 동안 한 번도 겪지 못했다. 리허설은 기능을 검증했지 행사 진행 순서를 검증하지 않았다.
  • CI가 없다. 테스트 69개가 로컬에서만 돌고, 배포 24번 중 테스트를 돌리고 배포했는지를 보장하는 장치가 없다. 사후 점검(200/404 확인)을 매번 손으로 적은 건 CI가 없어서 생긴 비용이다. 하루에 238 커밋을 치는 속도에서는 CI 셋업이 아깝게 보였고, 그 판단은 행사 뒤에 봐도 틀렸다.
  • 청중이 "들어와서 질문하기까지"의 경로를 설계하지 않았다. QR 접속률이 최소 17%에 그쳤는데, QR을 언제 어디에 띄웠고 접속을 몇 번 권했는지는 기록이 없다. 행사 전 체크리스트에는 서버·슬라이드·암호 항목은 있었지만 청중에게 접속을 권하는 순서는 없었다. 앱은 2초 안에 슬라이드를 넘겼지만, 그 앱에 사람을 들어오게 하는 쪽은 아무도 설계하지 않았다.
Tech stack
  • Next.js 16.3.6 (vinext 1.0.0-beta.12)
  • React 19.2.8
  • TypeScript
  • Cloudflare Workers + Pages 프록시
  • Supabase 셀프호스팅 v0.8.2 (Postgres · Realtime · Storage)
  • cloudflared 임시 Tunnel
  • pdfjs-dist 6.3
  • Playwright 1.63
  • wrangler 4.140
  • Mac mini