바

데이터베이스 · 로깅

내 앱에 '기억'을 붙이는 방법입니다. 사용자가 쓴 글, 신청 내역, 방문 기록 — 새로고침해도 사라지지 않게 어딘가에 저장해야 합니다. 그게 데이터베이스(DB)예요. 예전엔 개발자만 다뤘지만, 지금은 Claude에게 시키면 됩니다. 대신 '이건 사람이 꼭 지켜야 한다'는 규칙 몇 개가 있어요.

여러 개의 표(테이블)가 클라우드 데이터베이스에 담긴 일러스트

이 페이지의 한 줄 요약

저장은 Claude가 다 해줍니다. 사람이 지킬 건 딱 두 가지 — ① 지우기·고치기는 반드시 나에게 물어보게 할 것, ② 시간은 UTC로 저장하고 볼 때만 한국 시간으로 바꿀 것. 나머지는 읽어두면 좋은 배경지식이에요.
Part 1 / 6

데이터베이스란

앱에 기억을 붙이는 상자

DB는 '표'가 쌓여 있는 곳이에요

데이터베이스는 어렵게 생각할 것 없어요. 엑셀 시트 여러 장을 모아둔 것이라고 보면 됩니다. 시트 한 장이 테이블(table)이고, 테이블은 행(row)과 열(column)로 되어 있어요.

예를 들어 '신청자' 테이블이라면 — 열은 이름 · 이메일 · 신청일시, 행은 신청자 한 명 한 명. 사용자가 폼을 제출할 때마다 이 표에 줄이 하나씩 추가되는 거예요.

실제 배포된 '행사 신청 폼' 웹페이지 — 이름·이메일·소속을 입력하고 '무료로 신청하기' 버튼이 있는 화면
예를 들어 이런 신청 폼(강사가 실제로 만들어 vibe-event-signup.vercel.app에 배포한 것)에 이름·이메일·소속을 적고 '무료로 신청하기'를 누르면…
신청 완료 화면 — 초록 체크 아이콘과 '신청이 접수됐어요!' 메시지
…이렇게 '신청이 접수됐어요!' 화면이 뜨고, 바로 그 순간 우리 DB의 'applications' 표에 이 사람의 줄이 하나 저장돼요. 아래 표가 그 결과예요.

'applications' 테이블 예시

idnameemailcreated_at
1김하늘sky@…2026-07-21 14:03
2이바다sea@…2026-07-21 14:07

각 열엔 '종류(타입)'가 정해져 있어요 — id는 숫자, email은 글자, created_at은 시간. 이 타입 규칙 덕분에 잘못된 값이 들어오는 걸 막아줍니다.

표 구조 그림 — 하나의 표(TABLE)가 여러 열(COLUMN)로 나뉘고 각 열에 INTEGER·TEXT·JSON·DATETIME 같은 타입이 붙어 있다
실제 DB 도구에선 표를 이렇게 봐요 — 하나의 표(TABLE)가 여러 열(COLUMN)로 나뉘고, 열마다 담을 수 있는 '종류(타입)'가 정해져 있어요. (Supabase 공식 문서 그림)

원래는 개발자만 다루던 영역이었어요

표를 쌓는 게 뭐가 어렵냐 싶지만, DB는 직접 하려면 까다로운 것들이 많았어요. 어떤 표를 만들지 설계하고(스키마), 연결을 안전하게 잡고, 표끼리 관계를 맺고, 느려지지 않게 손보고, 망가졌을 때 되돌릴 백업까지 — 이걸 다 신경 써야 해서 보통 백엔드 개발자의 일이었습니다.

예전 — 직접 하던 시절

SQL 문법 배우고, 서버 세팅하고, 연결 풀 관리하고, 스키마 마이그레이션 도구 익히고… 앱 하나 붙이는 데 며칠.

지금 — AI에게 시키면

“신청 폼 내용을 DB에 저장해줘” 한 줄. Claude가 테이블 설계·연결·저장 코드까지 만들어줍니다. 몇 분.

그래서 지금 이 수업이 가능한 거예요

AI 모델이 좋아지면서, 예전엔 개발자만 하던 DB 작업을 비개발자가 말로 시켜서 할 수 있게 됐어요. 단, AI가 다 해준다고 해서 사람이 몰라도 되는 건 아닙니다 — 어디에 뭐가 저장되는지, 무엇이 위험한지는 알고 있어야 해요. 그게 이 페이지의 나머지 내용이에요.
Part 2 / 6

어디에 저장하고, 얼마인가

Vercel + Supabase · Neon · Blob 연결과 과금

먼저 그림부터 — Vercel과 DB는 다른 회사예요

헷갈리기 쉬운 부분: 내 웹사이트가 도는 곳(Vercel)과 데이터가 저장되는 곳(DB)은 보통 서로 다른 서비스예요. Vercel은 '앱을 인터넷에 띄워주는 집'이고, DB는 '데이터를 보관하는 창고'입니다. 이 둘을 연결 문자열(connection string) 하나로 이어줘요.

사용자 (브라우저 / 폰)
vercel내 웹앱 — Vercel에 배포
supabaseDB (Supabase / Neon) + 파일 저장소 (Blob)

연결은 클릭 몇 번이면 돼요

Vercel 대시보드의 Storage 탭에서 Supabase·Neon·Blob을 눌러 연결하면, 필요한 연결 정보(환경변수)가 내 프로젝트에 자동으로 꽂힙니다. 키를 복사·붙여넣기 할 필요도 거의 없어요. 나머지 코드는 Claude가 씁니다.

실제로 연결해본 과정 — 클릭만 따라가면 돼요

말로만 들으면 감이 안 오죠. 강사가 실제로 ‘행사 신청 폼’ 앱에 DB를 붙인 그대로예요 — 전부 무료, 카드 등록도 필요 없었어요.

Vercel Storage → Supabase 연결 · 실제 화면
1

Vercel 프로젝트의 Storage 탭에서 'Create Database'

Vercel Storage 탭의 'Connect to a Database' 화면 — Create Database / Connect Database 버튼
2

DB 종류에서 Supabase 선택

DB 프로바이더 목록에서 Supabase가 선택된 화면 (Neon·Blob 등도 함께)
3

무료 플랜($0) 그대로 두고 진행

Supabase 설정 화면 — Supabase Free Plan $0/month 선택
4

DB 이름(my-eventpage-db) 확인하고 'Create' — Free Plan은 DB 500MB·대역폭 5GB까지 무료

Install Integration 확인 단계 — Resource Name 'my-eventpage-db', Supabase Free Plan(500MB DB·5GB 대역폭·1GB 파일 등), Create 버튼
5

몇십 초 뒤 — 'database is ready to use', 만들어졌어요

Database Provisioning 완료 화면 — 'Your Supabase database is ready to use', my-eventpage-db 생성 성공
6

만들면 프로젝트에 연결되고, 환경변수가 자동으로 꽂혀요

DB 연결 완료 화면 — SUPABASE_URL 등 환경변수가 프로젝트에 자동 주입됨
7

신청을 저장할 표(applications)를 만들고

SQL 에디터에서 applications 테이블 생성 성공 (Query executed successfully)
8

실제 신청이 이렇게 쌓입니다

Data Editor에 실제 신청 23건(이름·이메일)이 쌓인 화면

‘표 만들기’랑 ‘저장 코드’는 Claude에게 시키면 돼요. 사람이 한 건 클릭 몇 번 + 무료 플랜 선택뿐이에요.

세 가지 저장소 — 뭘 쓰나요?

supabaseSupabase — 초보자 기본값

PostgreSQL DB + 로그인 인증 + 파일 저장 + 표를 눈으로 보고 고치는 화면까지 한 세트로 줍니다. 처음이라면 이걸 추천해요.

neonNeon — 안 쓸 땐 잠들어 요금 절약

같은 PostgreSQL인데, 트래픽이 없으면 자동으로 ‘잠들었다(scale to zero)’ 깨어나서 비용이 적게 나와요. 실험용 앱 여러 개 굴릴 때 좋아요.

vercelVercel Blob — 파일 전용

이미지·영상·PDF 같은 파일을 담는 창고예요. DB가 아닙니다. “사용자가 올린 사진 저장” 같은 게 필요할 때만 씁니다.

DB에 파일을 넣지 마세요

“프로필 사진” 같은 파일은 Blob(파일 저장소)에 올리고, DB에는 그 파일의 주소(URL)만 적어두는 게 정석이에요. 파일 원본을 DB에 통째로 넣으면 DB가 무거워지고 요금도 급등합니다.

과금 구조 — 실제로 얼마 나오나요

숫자는 2026년 기준 '대략'입니다

가격 정책은 자주 바뀌어요. 아래 표는 감을 잡기 위한 근사치이고, 결제 전 반드시 공식 pricing 페이지에서 확인하세요. 링크는 표 아래에 있어요.
무료로 되는 것유료 시작돈이 새는 지점
VercelHobby 무료 — 개인·비상업 프로젝트, 대역폭 월 100GBPro 월 $20/명 (사용 크레딧 $20 포함)Hobby는 한도를 넘으면 청구 대신 프로젝트가 일시정지 / Pro는 방문 폭증 시 대역폭·함수 실행이 종량제로 초과 과금
SupabaseDB 500MB·스토리지 1GB, 프로젝트 2개Pro 약 $25/월1주일 안 쓰면 무료 DB가 일시정지 / Free는 자동 백업이 없어요 / 용량이 차면 Pro로 올려야 해요
Neon무료 플랜 있음, 안 쓰면 자동 정지월 최소 요금 없는 종량제 — 컴퓨트 약 $0.106/CU-시간, 저장 약 $0.35/GB-월쓴 만큼 나가요. 컴퓨트 사용 시간·저장량이 늘면 같이 늘어요
Vercel BlobHobby 기준 저장 1GB · 전송 월 10GBPro에서 저장 GB·전송량당 소액큰 파일 다운로드가 많으면 전송량(egress)에서 새어나감

과금의 핵심 원리 3가지

  1. 기본요금(월 정액) + 사용량(종량제)이 합쳐져요. “$25면 끝”이 아니라, 많이 쓰면 그 위로 더 붙습니다.
  2. 초보자 프로젝트는 거의 다 무료 티어 안에서 돌아가요. 수강생·지인 몇십 명 수준이면 돈 낼 일이 잘 없어요.
  3. 진짜 무서운 건 “사고성 폭탄” — 무한 반복 코드가 DB를 계속 호출하거나, 큰 파일이 대량 다운로드될 때. 그래서 Pro라면 사용량 알림·상한(spend limit)을 꼭 켜두세요. Hobby는 청구 대신 한도를 넘으면 프로젝트가 일시정지돼요.
Vercel 프로젝트 Usage 화면 — Hobby 플랜, Fast Data Transfer 1.36 MB / 100 GB, Edge Requests 122 / 1M 등 사용량이 무료 한도의 극히 일부
말이 아니라 숫자로 — 이 강의에서 실제로 배포·운영 중인 프로젝트의 Vercel Usage 화면이에요. Hobby(무료) 플랜이고, 데이터 전송 1.36MB / 100GB, 요청 122 / 100만 건처럼 무료 한도의 1%도 안 씁니다. 수강생 수십 명 규모라면 이렇게 돈 낼 일이 거의 없어요.
Claude에게 이렇게 말하세요

이 프로젝트의 Vercel/Supabase 사용량이 무료 티어를 넘지 않는지 확인하는 법을 알려줘. 그리고 요금 폭탄을 막게, 내 플랜(Hobby/Pro)에 맞는 결제 상한(spend limit)과 사용량 알림 설정하는 위치를 단계별로 알려줘.

공식 가격: vercel.com/pricing · supabase.com/pricing · neon.com/pricing

내 앱은 Vercel에 있고, 데이터는 Supabase(표) 또는 Blob(파일)에 따로 저장된다. 무료로 시작하되, 사용량 알림(Pro라면 결제 상한도)을 켜서 요금 폭탄을 막는다 — 여기까지 이해했다면 다음으로.
Part 3 / 6

AI에게 시키는 법 + 절대 규칙

편하게 시키되, 사람이 반드시 지킬 선

저장 붙이기 — 이렇게 시키면 됩니다

거창한 SQL을 몰라도 돼요. “무엇을 저장하고 싶은지”를 평소 말로 설명하면 Claude가 테이블 설계부터 저장 코드까지 만들어줍니다.

1단계 — 저장소 연결

이 프로젝트에 Supabase를 연결하고 싶어. Vercel Storage 탭에서 연결하는 방법과, 연결 정보를 .env에 안전하게 넣는 것까지 단계별로 알려줘. 내가 클릭할 화면을 정확히 짚어줘.

2단계 — 표 만들고 저장 붙이기

신청 폼(이름·이메일·소속)을 제출하면 그 내용이 DB에 저장되게 해줘. - 테이블 이름은 applications - 저장 시각(created_at)도 같이 기록 - 같은 이메일이 두 번 신청하면 막아줘 표 구조를 먼저 설명하고, 내 확인을 받은 다음에 만들어.

3단계 — 잘 저장됐는지 확인

방금 만든 신청 폼으로 테스트 데이터를 하나 넣고, DB에 제대로 들어갔는지 조회해서 보여줘. 확인 끝나면 그 테스트 데이터는 지워도 되는데, 지우기 전에 나한테 먼저 물어봐.

절대 규칙 — 지우기·고치기는 사람이 결정한다

AI는 시키면 정말로 지워버려요. “테스트 데이터 정리해줘” 한마디에 진짜 사용자 데이터까지 날아갈 수 있어요. 그래서 실제 데이터가 든 프로젝트에는 이 규칙을 CLAUDE.md에 박아두고 AI가 항상 지키게 만드는 게 핵심이에요. (아래 블록을 그대로 복사해서 내 프로젝트 최상단 CLAUDE.md에 붙여넣으세요.)

복사 → 내 프로젝트 CLAUDE.md에 붙여넣기

# 백엔드/DB 작업 규칙 (반드시 지킬 것) ## 이 프로젝트는 실제 사용자 데이터를 다룬다 DB에는 진짜 데이터가 들어간다. 되돌릴 수 없는 작업은 사람이 결정한다. ## 반드시 나에게 먼저 물어보고, 승인 없이는 실행하지 말 것 - 데이터·테이블 삭제: DELETE / TRUNCATE / DROP - 기존 데이터 일괄 수정: UPDATE - 스키마 변경(컬럼 삭제·타입 변경·제약조건 변경), 마이그레이션 실행 - 관리자/인증 계정 조작 승인 요청은 이 형식으로: 작업: (무엇을) / 영향: (어느 테이블·몇 건) / 되돌리기: (가능/불가) / 백업: (했는지) → "이대로 진행할까요?" ## 물어보지 않고 해도 되는 것 - 조회(SELECT), 새 기능 코드 작성·배포, 로그 확인 - 새 데이터 추가(INSERT). 단, 실서비스에 넣는 내 테스트 데이터는 반드시 '테스트'로 표시한다(아래 참고) ## 시간은 무조건 이렇게 (아래 6번 참고) - DB에는 UTC로 저장(timestamptz). 코드에서 +9시간을 더하지 않는다. - 화면·집계에서만 Asia/Seoul로 변환한다. - 내가 말하는 시각(예: "오전 10시 마감", "매일 9시 실행")은 한국 시간이다. 코드·DB·예약(cron)에 넣을 땐 UTC로 변환하고, 애매하면 먼저 되묻는다. (cron은 UTC 기준) ## 테스트 데이터 구분 / 비밀 - 초보·소규모 단계에선 실서비스 DB 하나만 써도 된다(개발/운영 DB 분리는 스케일 커질 때). - 대신 내 테스트 데이터는 반드시 구분한다: · 계정 있는 앱: 사용자 표에 is_test 칸을 두고 내 테스트 계정만 표시 + 숨은 경로에서 테스트 계정 로그인 · 계정 없는 앱: 방문자마다 랜덤 익명 ID를 쿠키/로컬스토리지에 저장하고 데이터에 함께 기록 - 연결 문자열·키는 .env에만 둔다. 코드·깃에 절대 커밋하지 않는다. ## 매 작업 끝 - 무엇을 바꿨고 되돌리는 법이 뭔지 한 줄로 보고한다.

이 규칙이 실제로 서비스를 지킨 사례

운영 중인 서비스 “QANDA 득템 챌린지”는 CLAUDE.md 맨 위에 “삭제·수정은 반드시 사용자 동의” 규칙을 박아뒀어요. 덕분에 AI가 데이터 정리를 하려다가도 먼저 멈추고 물어봅니다. 실데이터가 있는 프로젝트라면 선택이 아니라 필수예요.
내 프로젝트 CLAUDE.md에 “삭제·수정·스키마 변경은 먼저 물어보기” 규칙을 붙여넣었다. 이제 Claude는 위험한 작업 전에 항상 나에게 확인한다.
Part 4 / 6

로깅 — DB냐 Mixpanel이냐

'누가 무엇을 했는지' 기록하는 두 갈래

로깅이 왜 필요한가요

앱을 띄우면 궁금해져요 — 사람들이 어디서 들어와서, 어느 버튼을 누르고, 어디서 나가는가? 이 '행동 기록'을 남기는 게 로깅이에요. 방법은 크게 둘 — ① 내 DB에 직접 기록하거나, ② Mixpanel 같은 전문 분석 도구로 보내거나.

내 DB에 로깅mixpanelMixpanel
시작 난이도이미 DB 있으면 즉시가입·SDK 심기 필요(그래도 쉬움)
데이터 소유100% 내 것, 외부로 안 나감외부 회사 서버에 저장됨
분석 화면직접 만들어야 함(쿼리·그래프)깔때기·리텐션·코호트 기본 제공
비용DB 요금에 포함(로그 쌓이면 무거워짐)월 이벤트 수 기준, 무료 티어 넉넉
서비스 로직과 결합쉬움 — 로그로 기능도 만들 수 있음분석 전용, 서비스에 되먹이긴 번거로움
개인정보/규제내가 관리·책임(장점이자 부담)제3자 제공 → 동의·정책 명시 필요
Mixpanel 대시보드 예시 — 깔때기(퍼널), 리텐션, 카테고리별 사용자 수 등 분석 차트가 카드로 모여 있는 화면
Mixpanel은 이렇게 생겼어요 — '분석 화면 기본 제공'이란 이런 뜻이에요. 깔때기·리텐션 같은 그래프를 직접 안 만들고 클릭으로 봅니다. (Mixpanel 공식 제품 화면)

내 DB에 로깅이 유리할 때

  • 로그가 서비스 기능이기도 할 때 (예: “내 활동 내역” 화면, 응모 횟수 집계)
  • 데이터를 밖으로 내보내면 안 되는 민감한 서비스
  • 이벤트 종류가 단순하고, 정밀 분석까진 필요 없을 때

Mixpanel이 유리할 때

  • “어디서 이탈하나”(깔때기), “다시 오나”(리텐션)를 보고 싶을 때
  • 분석 화면 만들 시간이 아까울 때 — 클릭 몇 번이면 그래프가 나옴
  • 마케팅·A/B 테스트처럼 행동 분석이 본론일 때

바이브코딩 단계라면 — 결론은 '그냥 DB에 남기세요'

솔직한 결론부터 — 지금 단계에선 Mixpanel 없이 DB 로깅 하나로 충분해요.오히려 DB에 쌓아두면 “AI야, 이 데이터로 이런 대시보드 만들어줘” 한마디로 원하는 화면을 뚝딱 만들 수 있어서, 연동·설정이 필요한 Mixpanel보다 편하거든요. 결제·신청처럼 정확해야 하는 기록은 어차피 DB에 남겨야 하고요. Mixpanel의 강점(깔때기·리텐션 정밀 분석)은 유저가 많아지고 분석이 본론이 될 때 얹어도 늦지 않아요.
실제로 만들어봤어요 — Mixpanel 없이, 내 DB로

위에서 말한 그대로예요. ‘행사 신청 폼’ 앱에 이벤트 로깅(방문·입력 시작·신청 완료)을 심고, Claude에게 “이 데이터로 분석 대시보드 만들어줘”라고 했더니 나온 화면이에요. 퍼널 · 전환율 · 일별 추이 · 소속 분포 — 딱 Mixpanel이 해주던 것들이죠.

직접 만든 행사 신청 분석 대시보드 — 방문 420·입력 158·신청 23·전환율 5.5%, 전환 퍼널, 일별 추이, 소속 분포, 최근 신청 목록

방문 420 → 입력 158 → 신청 23 (전환율 5.5%). 전부 내 Supabase에 쌓인 이벤트를 직접 집계한 거예요 — 외부 분석 도구 0개, 추가 비용 0원, 데이터도 100% 내 것.

Part 5 / 6

DB를 편하게 보는 법

표를 직접 뒤지지 말고, 나에게 맞는 화면으로

왜 DB를 직접 열어 보면 안 되나요

Supabase 화면에서 표를 직접 볼 수도 있지만, 그건 날것이에요 — 원하는 정렬도, 보기 좋은 요약도 없고, 잘못 눌러 데이터를 고칠 위험도 있어요. 매번 표를 뒤지는 대신, 내가 편한 방식으로 정리된 화면을 따로 두는 게 좋아요. 두 가지 방법이 있어요.

Supabase Data Editor에 실제 신청 데이터(이름·이메일)가 행으로 쭉 쌓인 화면
이게 바로 그 '날것' 화면이에요 — 실제 신청이 이렇게 표로 쌓입니다. 쓸 만하지만, 원하는 정렬·요약·검색은 직접 해야 하고 실수로 값을 고칠 위험도 있어요. 그래서 아래 두 방법을 씁니다.

방법 A — 나만의 관리자 웹(admin 페이지)을 만든다

내 앱 안에 /admin 같은 비공개 페이지를 하나 두고, 거기서 내가 보고 싶은 대로 — 최신순 정렬, 오늘 신청자 수, 상태별 필터 — 보여주게 만드는 거예요. 데이터를 실수로 고칠 걱정 없이 '보기 전용'으로 만들 수 있어요.

Claude에게 이렇게 말하세요

/admin 페이지를 만들어줘. applications 테이블을 최신순으로 보여주고, 맨 위에 '오늘 신청 수 / 전체 신청 수'를 크게 표시해줘. - 보기 전용(여기서 데이터 수정·삭제는 안 되게) - 비밀번호로 잠가서 나만 볼 수 있게 - 시간은 한국 시간(KST)으로 표시

방법 B — 스프레드시트로 자동 복사(cron)

“나는 그냥 구글 시트에서 보고 싶다”면 — 일정 시간마다 자동으로 DB 내용을 시트에 복사하게 할 수 있어요. 이 '정해진 시간에 자동 실행'을 cron(크론) 작업이라고 해요. 시트에 쌓이면 정렬·필터·피벗·차트 다 익숙한 방식으로 볼 수 있죠.

applications— 매일 09:00(KST) 자동 복사 · 구글 시트
ABC
1nameemailcreated_at (KST)
2이바다sea@…2026-07-21 14:07
3김하늘sky@…2026-07-21 14:03
Sheet1신청자

DB 내용이 이렇게 시트에 최신순으로 쌓여요 — 익숙한 정렬·필터로 봅니다. (스프레드시트 목업)

Claude에게 이렇게 말하세요

매일 아침 9시에 applications 테이블 전체를 구글 스프레드시트에 자동으로 복사하는 cron 작업을 만들어줘. 시트에는 이름·이메일·신청시각(KST)만, 최신순으로. 설정에 필요한 것(구글 시트 연동 방법 등)을 단계별로 안내해줘.

※ Vercel에는 Cron Jobs기능이 있어서, “매일 9시에 이 작업 실행”을 앱 안에 넣어둘 수 있어요. Claude가 설정 파일까지 만들어줍니다.

어느 쪽을 고르나요

손이 조금 더 가도 웹에서 실시간으로 보고 싶다 → 방법 A. 엑셀/시트가 훨씬 편하고 실시간까진 필요 없다 → 방법 B. 둘을 같이 써도 돼요(관리자 웹 + 매일 백업용 시트).
Part 6 / 6

안 하면 크게 다치는 것

RLS·시간(UTC) 문제와 그 밖의 함정

Supabase RLS — 안 켜면 내 데이터가 전부 공개돼요

RLS가 꺼진 테이블은 누구나 읽고, 고치고, 지울 수 있어요

RLS(Row Level Security)는 “누가 어느 행(row)을 볼 수 있는지” 정하는 DB의 출입 규칙이에요.

RLS가 꺼진 테이블은 브라우저에 들어 있는 공개용 키(sb_publishable_... 또는 옛 anon)만 있으면 누구든 읽고, 수정하고, 삭제할 수 있어요.이 키는 원래 누구나 볼 수 있게 설계된 키라서, 개발자 도구만 열어도 보여요. Supabase 대시보드에 ‘RLS disabled’ 경고가 뜨는 테이블은 사실상 공개 상태라는 뜻이에요.

기억할 규칙 2개

  • 테이블을 만들면 RLS부터 켜요. 브라우저에서 접근하는 테이블이라면 예외 없이요.
  • Claude에게 ‘RLS 정책도 같이 만들어줘’라고 말해요. 켜기만 하면 아무도 못 읽으니, “누가 무엇을 할 수 있는지” 정책이 같이 있어야 앱이 돌아가요.
Claude에게 이렇게 말하세요 — 테이블 만들 때마다

Supabase 테이블을 만들 때마다 RLS(Row Level Security)를 반드시 켜줘. RLS 정책도 같이 만들어줘. - 로그인한 사용자는 자기 데이터만 읽고 쓰게 해줘. - 누구나 봐도 되는 데이터가 아니면 공개 읽기는 허용하지 마. - 지금 있는 테이블 중에 RLS가 꺼진 게 있는지도 확인해서 목록으로 알려줘. - secret(service_role) 키는 서버 코드에서만 쓰고, NEXT_PUBLIC_ 이름으로 노출하지 마.

새 키 이름: 공개용은 sb_publishable_..., 비밀용은 sb_secret_.... 옛 anon / service_role 키는 올해 말쯤 중단될 예정이에요. 비밀용 키는 RLS를 건너뛰는 강한 키라 서버에서만 써요.

시간(UTC) — 비개발자가 가장 많이 데는 곳

실화 — 사용자가 뭔가 할 때마다 시간이 8~9시간씩 밀렸다

운영 서비스에서 실제로 벌어진 일이에요. 신청 시각이 자꾸 엉뚱하게 찍히길래 “+9시간 더하자”로 고쳤는데, 다른 곳에서 또 +9를 더하고, 마이그레이션에서 또 더해서 — 손댈 때마다 시간이 계속 밀렸어요. 원인은 하나예요.

왜 이런 일이 생기나

DB는 시간을 보통 UTC(세계 표준시)로 저장해요. 한국(KST)은 UTC보다 9시간 빠릅니다. 그래서 화면에 “9시간 늦게” 보이면, 초보자는 저장된 값에 +9를 더해 '고치고' 싶어져요.

문제는 — 저장할 때 한 번, 보여줄 때 한 번, 나중에 마이그레이션에서 또 한 번, 이렇게 여기저기서 +9를 중복으로 더하면시간이 계속 밀립니다. “볼 때마다 8시간씩 늘어난다”는 게 정확히 이 증상이에요.

이렇게 하면 안 돼요 — 밀림의 원인

// 저장하면서 시간을 강제로 +9 — 이러면 이중 변환으로 밀린다
const d = new Date(created_at);
d.setHours(d.getHours() + 9);   // ✗ 손으로 오프셋을 더함
await save({ created_at: d.toISOString() });  // ✗ 게다가 다시 UTC로 되돌려 저장

하나의 규칙만 지키면 끝

  1. 저장은 언제나 UTC로. 컬럼 타입은 timestamptz, 값은 new Date()(=UTC) 그대로. 코드에서 +9를 더하지 않는다.
  2. 보여줄 때만 한국 시간으로 변환한다. (Asia/Seoul 타임존으로 포맷)
  3. “며칠에 몇 건” 같은 날짜 집계도 한국 시간 기준으로 묶는다. (UTC로 묶으면 자정 근처 데이터가 하루 어긋남)
  4. 내가 말하는 시각은 항상 한국 시간이다. “오전 10시에 마감”, “매일 9시에 실행” 같은 지시를 코드·DB·예약에 넣을 땐 AI가 UTC로 변환해서 넣는다. 헷갈리면 먼저 되묻는다.

가장 헷갈리는 곳 — 내가 시간을 ‘말할 때’

위 1~3번은 ‘이미 저장된 시간’ 이야기예요. 그런데 반대로 내가 시간을 지시할 때도 함정이 있어요. 비개발자는 당연히 한국 시간으로 말하는데(“오전 10시에 닫아줘”), AI가 그 ‘10시’를 그대로 UTC로 넣으면 또 9시간이 어긋납니다. 그래서 이렇게 되게 시켜두는 게 좋아요.

나: “응모 마감을 오전 10시로 해줘”

AI(이렇게 되묻거나 변환): “말씀하신 10시는 한국 시간 기준 맞죠? DB엔 UTC로 변환해서 01:00(UTC)로 저장할게요.”

특히 예약 실행(cron)이 위험해요 — Vercel Cron 같은 건 대부분 UTC 기준이라, “매일 아침 9시(KST)”는 cron으로는 0 0 * * *(UTC 0시)로 적어야 맞습니다. 타임존이 안 정해지면 AI가 UTC로 도는 걸 알려주고 확인받게 하세요.

// 저장: 손대지 않는다 (UTC 그대로)
await save({ created_at: new Date().toISOString() });

// 표시: 볼 때만 한국 시간으로
new Intl.DateTimeFormat("ko-KR", {
  timeZone: "Asia/Seoul",
  dateStyle: "medium",
  timeStyle: "short",
}).format(new Date(row.created_at));
Supabase Data Editor의 applications 표 — created_at 열이 timestamptz 타입이고 값이 전부 '2026-07-21 05:52…+00'처럼 UTC로 저장돼 있음
글이 아니라 실제 DB로 볼까요? 위 규칙대로 만든 프로젝트의 표예요. created_at 열 타입이 timestamptz이고, 값이 전부 '2026-07-21 05:52…+00' — 끝의 +00이 'UTC로 저장됐다'는 표시예요. 그런데 대시보드 화면(Part 4)에선 같은 신청이 '07/21 14:52'로 보여요. 저장은 05:52(UTC) 그대로 두고 보여줄 때만 +9시간 한 14:52(KST)로 바꾼 것 — 코드에서 +9를 더해 저장하지 '않은' 게 핵심이에요.
Claude에게 이렇게 말하세요

시간 처리 규칙을 아래로 통일해줘. 코드 전체를 이 기준으로 점검하고, 어긋난 곳을 고쳐줘: 1. DB 저장은 항상 UTC(timestamptz), 코드에서 +9시간 같은 오프셋을 절대 더하지 않는다. 2. 화면 표시와 날짜별 집계에서만 Asia/Seoul로 변환한다. 3. 내가 시각을 말하면(예: "오전 10시 마감", "매일 9시 실행") 그건 한국 시간이다. 그걸 코드·DB·예약(cron)에 넣을 땐 UTC로 변환해서 넣고, 애매하면 먼저 나에게 되물어. (cron은 UTC 기준이라 "KST 9시"는 "0 0 * * *"로 적어야 함) 4. 지금 이미 시간이 밀려 저장된 데이터가 있는지 먼저 조회해서 보여주고, 고치는 건(마이그레이션) 내 승인을 받은 다음에 해.

이미 밀려서 저장된 데이터가 있다면

함부로 “전부 -9시간” 돌리지 마세요. 어떤 값은 밀렸고 어떤 값은 멀쩡할 수 있어서, 일괄 보정이 오히려 더 망칩니다. 먼저 조회해서 실태를 확인하고, 보정 마이그레이션은 백업 후, 승인받고 실행하세요 (Part 3 규칙 그대로).

그 밖에 미리 알면 좋은 함정

테스트 데이터, 진짜 데이터와 섞이지 않게

DB를 하나(실서비스)만 쓰기로 했다면, 내가 테스트하며 만든 데이터가 진짜 사용자 데이터에 섞여들어가요. 나중에 “테스트한 것만 지워줘”가 안 되면 곤란하죠. 그래서 처음부터 '이건 테스트다'라는 표시를 남겨두는 게 핵심이에요. 내 앱에 로그인(계정)이 있느냐 없느냐로 방법이 갈려요.

A. 계정(로그인)이 있는 앱

  • 테스트 계정에 '테스트용' 표시를 남기기 — DB의 사용자 표에 is_test 같은 칸을 두고 내 계정만 표시. 나중에 “is_test인 것만 지워줘”가 됩니다.
  • 나만 아는 숨은 테스트 로그인 — 구글 로그인을 붙였어도 매번 구글로 들어가긴 번거로우니, 남들에겐 안 보이는 경로(예: /test-login)에서 내 테스트 계정으로만 로그인하게 만들어 두기.
  • 테스트로 만든 자료는 지워달라고 할 때 지우면 됩니다 (삭제는 Part 3 규칙대로 먼저 확인).

B. 계정이 없는 앱 (예: 심리테스트처럼 널리 뿌리는 것)

로그인이 없으면 ‘누가 만든 데이터인지’ 잡을 앵커가 없어요. 이럴 땐 브라우저에 랜덤한 식별값(익명 ID)을 하나 만들어 쿠키·로컬스토리지에 저장해 둡니다. 같은 사람이 다시 오면 같은 값이라 ‘같은 방문자’로 묶을 수 있고, 그 값을 데이터에 함께 기록하면 “이 익명 ID(=내 브라우저)가 만든 건 테스트”라고 구분·삭제할 수 있어요.

Claude에게 이렇게 말하세요

실서비스 DB 하나만 쓸 거야. 대신 내 테스트 데이터가 진짜 데이터와 섞이지 않게 해줘: - (계정 있는 앱) 사용자 표에 is_test 칸을 만들고 내 테스트 계정만 test로 표시. 구글 로그인은 붙이되, 남들에겐 안 보이는 숨은 경로에서 내 테스트 계정으로 로그인할 수 있게 해줘. - (계정 없는 앱) 방문자마다 랜덤 익명 ID를 만들어 쿠키/로컬스토리지에 저장하고, 저장되는 데이터에 그 ID를 같이 기록해줘. - 나중에 "테스트 데이터만 지워줘"라고 하면 지울 수 있게 (지우기 전엔 나에게 먼저 확인).

시간은 UTC로 저장, 볼 때만 한국 시간으로 — 이 한 줄이 오늘의 핵심이에요. 여기에 삭제·수정은 먼저 물어보기(Part 3)와 테스트 데이터 구분하기까지 챙기면, 비개발자도 실데이터를 안전하게 다룰 수 있어요.