전자주보의 데이터베이스를 다른 프로젝트로 합쳤습니다. 여러 서비스가 각자 프로젝트를 쓰다 보니 관리가 번거로워서 하나로 모은 것입니다.
옮기는 것 자체는 잘 됐습니다. 표도, 데이터도, 파일도 다 옮겨졌습니다. 그런데 몇 주 뒤에 문제가 터졌습니다.
이 글은 [교회 전자주보 만들기] 시리즈의 5편입니다. 개발하시는 분께 필요한 이야기입니다.
증상 — 보이는데 못 올린다
관리자가 편집 모드에서 사진을 고르면 이 문구만 떴습니다.
파일 업로드에 실패했습니다. 잠시 후 다시 시도해 주세요.
이상한 점이 있었습니다. 기존에 올려 둔 사진과 영상은 멀쩡히 보였습니다. 설교 썸네일도, 선교지 영상도 잘 나왔습니다. 새로 올리는 것만 안 됐습니다.
“파일이 옮겨졌으니 저장소는 정상”이라고 생각하기 딱 좋은 상황이었습니다.
원인 — 정책은 안 따라온다
로그인한 편집자 계정으로 실제 업로드를 시도해 봤습니다.
403 Unauthorized
new row violates row-level security policy
파일 저장소의 업로드 권한 정책이 없었습니다.
Supabase는 저장소 접근을 행 수준 보안(RLS) 정책으로 통제합니다. 옛 프로젝트에서는 그 정책을 대시보드 SQL 편집기에 직접 붙여넣어 만들었습니다. 그래서 프로젝트 소스에 마이그레이션 파일로 남아 있지 않았고, 이관할 때 아무도 그걸 옮기지 않았습니다.
파일과 DB 행은 도구로 옮겼지만, 정책은 사람이 만들었던 것이라 같이 안 갔습니다.
왜 발견이 늦었나
읽기가 멀쩡했기 때문입니다.
이 저장소는 공개 버킷입니다. 공개 URL로 접근하면 권한 검사를 타지 않습니다. 그래서:
- 읽기 → 정책 없이도 잘 됨 → 기존 사진 정상 표시
- 쓰기 → 정책 없으면 막힘 → 새 업로드만 실패
증상이 “저장소는 살아 있는데 업로드만 안 됨”이라 원인을 엉뚱한 데서 찾게 됩니다. 실제로 앱 코드부터 뒤졌습니다.
고친 방법
정책을 다시 만들되, 이번엔 마이그레이션 파일로 남겼습니다.
핵심은 두 가지입니다.
1. 읽기는 공개로. 성도가 로그인 없이 주보를 보므로 사진·영상도 누구나 볼 수 있어야 합니다.
2. 쓰기는 교회 멤버십이 있는 사람만. 처음엔 “로그인한 사용자”로 하려다 멈췄습니다. 가입이 열려 있어서, 모르는 사람이 가입만 하면 파일을 밀어 넣을 수 있게 됩니다.
그래서 이런 함수를 하나 두고 정책에서 불렀습니다.
create or replace function public.is_any_church_member()
returns boolean
language sql
security definer
set search_path = public
as $$
select exists (
select 1 from public.church_memberships m
where m.user_id = auth.uid() and m.is_active
);
$$;
그리고 정책 네 개 — 읽기(공개), 올리기·덮어쓰기·지우기(교회 멤버만).
네 가지 경우를 다 확인했습니다
정책을 만들고 나서 실제로 시도해 봤습니다. 정책은 “된다”만 확인하면 절반만 검증한 것입니다.
| 누가 | 결과 |
|---|---|
| 교회 편집자 — 사진 | 200 OK |
| 교회 편집자 — 영상 | 200 OK |
| 멤버십 없는 가입자 | 403 차단 |
| 비로그인 | 차단 |
| 공개 URL로 읽기 | 200 OK |
막혀야 할 것이 실제로 막히는지까지 봐야 합니다.
배운 것
1. 대시보드에서 만든 것은 없어진다고 생각하세요
정책, 트리거, 함수, 확장 — 관리 화면에서 클릭 몇 번으로 만든 것들은 코드에 흔적이 없습니다. 이관 도구는 대개 스키마와 데이터를 옮기지 정책까지 챙기지 않습니다.
만든 즉시 마이그레이션 파일로 남기세요. 저희는 이 사달을 겪고 나서야 파일로 만들었습니다.
2. 읽기와 쓰기를 따로 확인하세요
이관 후 점검할 때 “사이트가 뜨는지”만 보면 놓칩니다. 공개 읽기는 권한 검사를 안 타는 경우가 많아서 정책이 통째로 없어도 멀쩡해 보입니다.
- ☐ 로그인 없이 읽기
- ☐ 로그인하고 쓰기
- ☐ 권한 없는 계정으로 쓰기 시도 → 막히는지
3. 에러 메시지가 원인을 감추면 며칠이 갑니다
서버는 처음부터 new row violates row-level security policy라고 정확히 알려 주고 있었습니다. 화면이 그걸 버리고 “잠시 후 다시 시도해 주세요”로 덮었을 뿐입니다.
지금은 실패 사유를 가려서 보여 줍니다. 권한 문제면 “권한을 요청하세요”, 용량이면 “용량을 줄이거나 유튜브 주소를 붙여넣으세요”. 예상 못 한 오류는 서버 원문을 그대로 붙입니다.
이 이야기는 [3편]에서 더 자세히 다뤘습니다.
이관하실 분을 위한 체크리스트
- ☐ 표·데이터 이관 확인
- ☐ RLS 정책 — 표마다, 그리고 저장소 버킷까지
- ☐ 함수·트리거 (특히
security definer함수) - ☐ 인증 설정 — 소셜 로그인 Provider, Redirect 허용목록
- ☐ 저장소 버킷의 공개 여부와 용량 제한
- ☐ 옮긴 뒤 읽기·쓰기·차단 세 가지를 각각 시도
- ☐ 이번에 만든 정책을 마이그레이션 파일로 저장
소셜 로그인 설정도 같이 안 따라옵니다. 저희는 카카오 로그인이 이관 후 통째로 멈춰 있었습니다. 그 이야기는 [4편]에 있습니다.
다음이 마지막 편입니다. 매주 반복되는 일을 줄인 방법과, 실제로 도입하실 때의 순서를 정리했습니다.
→ [6편] 매주 주보 만드는 시간을 20분으로 + 우리 교회 도입 가이드
교회 전자주보 만들기 시리즈
- 1. 교회 전자주보 만들기 — 주일 아침, 주보가 모자랐습니다
- 2. 종이 주보를 그대로 옮기면 안 되는 이유
- 3. “등록이 안 돼요” — 실은 버튼이 안 보였습니다
- 4. 교회에 카카오 로그인 붙이기
- 5. 서버를 옮겼더니 업로드만 막혔습니다 ← 지금 읽고 계신 글
- 6. 매주 주보 만드는 시간을 20분으로
