2026. 7. 5.
Supabase 저장 버튼이 먹통일 때, RLS를 7분 안에 확인하는 순서
AI가 만든 React나 Next.js 화면에서 Supabase 저장 버튼은 눌리는데 테이블이 비어 있다면 UI를 다시 만들기 전에 error, 로그인 상태, RLS 정책, 반환값을 7분 안에 나눠 봅니다.

AI가 만든 React 화면에서 저장 버튼을 눌렀는데 Supabase Table Editor가 계속 비어 있으면, 초보자는 보통 버튼 코드부터 다시 고칩니다. 하지만 저장 실패는 UI보다 Supabase 응답의 error, 로그인 상태, RLS 정책에서 먼저 갈리는 경우가 많습니다. 오늘 목표는 화면을 새로 만들지 않고, 7분 안에 문제가 어느 줄에 있는지 좁히는 것입니다.
supabase.com의 공식 RLS 문서는 브라우저에서 접근되는 테이블에 RLS를 켜고, 정책이 있어야 API 접근이 가능하다고 설명합니다. supabase.com의 JavaScript insert 문서도 insert 호출에서 error를 확인하는 예시를 보여 줍니다. 그래서 저장 버튼이 먹통처럼 보일 때 첫 행동은 디자인 수정이 아니라 응답 error를 화면 밖으로 꺼내는 것입니다.
30초 안에 성공 토스트를 의심합니다
저장 버튼을 누른 뒤 "저장 완료"가 뜬다고 해서 DB 저장이 끝난 것은 아닙니다. AI가 만든 코드에는 insert 결과를 보지 않고 바로 성공 메시지를 띄우는 경우가 있습니다. 먼저 버튼 함수에서 error를 받는지 확인합니다.
const { error } = await supabase
.from("todos")
.insert({ title: inputValue, user_id: user.id });
if (error) {
console.error("Supabase insert error", error);
return;
}
setMessage("저장되었습니다");
Supabase JavaScript insert 문서는 기본 insert 예시에서 error를 받고, 에러가 있으면 처리하는 흐름을 보여 줍니다. 이 줄이 없으면 버튼은 눌렸지만 실패 이유가 사라집니다. 성공 메시지는 error 확인 뒤에만 보여 줘야 합니다.
초보자가 복사해 둘 값은 네 가지입니다.
| 확인값 | 어디서 보는가 | 왜 필요한가 |
|---|---|---|
error.message | 브라우저 콘솔 | 코드 문제인지 권한 문제인지 첫 단서가 됩니다 |
| 테이블 이름 | .from("...") | AI가 다른 테이블에 넣고 있을 수 있습니다 |
| 저장 payload | .insert({...}) | user_id 같은 정책 조건 값이 비었는지 봅니다 |
| 로그인 여부 | Supabase Auth 상태 | RLS가 authenticated만 허용할 수 있습니다 |
이 네 가지가 없으면 AI도 추측으로 고칩니다. 반대로 네 가지가 있으면 "버튼이 안 된다"가 아니라 "insert error가 이렇다"로 문제가 바뀝니다.
2분째에는 anon인지 authenticated인지 나눕니다
Supabase RLS 문서는 요청이 로그인하지 않은 상태면 anon, 로그인한 상태면 authenticated 역할로 매핑된다고 설명합니다. 저장 정책이 to authenticated인데 현재 사용자가 로그인하지 않았다면, 버튼 코드를 여러 번 고쳐도 저장되지 않습니다.
const {
data: { user }
} = await supabase.auth.getUser();
console.log("current user id", user?.id);
여기서 user?.id가 비어 있으면 아직 로그인된 요청이 아닙니다. 이 상태에서 auth.uid() = user_id 같은 정책을 통과하기 어렵습니다. Supabase RLS 문서는 인증되지 않은 요청에서 auth.uid()가 null이 될 수 있다고 설명합니다. 그러면 정책 조건이 맞지 않아 저장이 막힐 수 있습니다.
이때 바로 RLS를 끄지 않습니다. RLS를 끄는 것은 원인 확인이 아니라 보호 장치를 빼는 행동입니다. 먼저 현재 화면이 로그인 뒤에만 저장해야 하는 화면인지, 아니면 익명 사용자도 저장해야 하는 공개 폼인지 결정합니다.
4분째에는 insert policy의 with check를 봅니다
Supabase RLS insert 정책은 새로 들어오는 행이 조건을 만족하는지 with check로 검사합니다. 예를 들어 로그인한 사용자가 자기 할 일만 만들 수 있게 하려면 정책은 대략 이런 방향이 됩니다.
create policy "Users can create own todos"
on todos
for insert
to authenticated
with check ((select auth.uid()) = user_id);
이 정책이 있다면 insert payload에도 user_id가 들어가야 합니다.
const { data: { user } } = await supabase.auth.getUser();
const { error } = await supabase
.from("todos")
.insert({
title: inputValue,
user_id: user?.id
});
반대로 payload에 user_id가 없거나, 로그인하지 않아서 user?.id가 비어 있으면 정책을 통과하지 못합니다. AI에게 "Supabase 저장 고쳐 줘"라고만 말하면 화면 상태나 form state를 바꿀 수 있습니다. 이 단계에서는 정책 조건과 insert payload가 같은 값을 바라보는지만 봅니다.
6분째에는 저장된 행을 반환받아 착각을 줄입니다
Supabase JavaScript insert 문서는 insert된 행이 기본으로 반환되지 않으며, 반환이 필요하면 .select()를 이어 붙이라고 설명합니다. 그래서 data가 비었다고 해서 무조건 저장 실패는 아닙니다. 확인용으로는 잠시 .select()를 붙여 실제로 들어간 행을 봅니다.
const { data, error } = await supabase
.from("todos")
.insert({
title: inputValue,
user_id: user?.id
})
.select();
console.log({ data, error });
여기서 세 가지로 나눕니다.
| 결과 | 판단 | 다음 행동 |
|---|---|---|
error가 있다 | 저장이 거절됐습니다 | message와 code를 복사합니다 |
data에 행이 있다 | 저장은 됐습니다 | 목록 조회나 화면 갱신 문제를 봅니다 |
| 둘 다 애매하다 | 코드가 결과를 숨길 수 있습니다 | insert 함수를 짧게 분리해 다시 로그를 찍습니다 |
이 표가 중요한 이유는 UI 문제와 DB 문제를 섞지 않기 위해서입니다. 저장은 됐는데 목록만 안 바뀌는 문제라면 RLS insert가 아니라 select 정책, 캐시, 상태 갱신 쪽을 봐야 합니다.
AI에게는 네 가지 사실만 주고 고치게 합니다
이제 AI에게 보낼 프롬프트를 짧게 만듭니다. 핵심은 "전체를 다시 만들지 말라"는 경계입니다.
Supabase 저장 버튼은 눌리지만 테이블에 행이 안 보입니다.
테이블: todos
insert payload: { title, user_id }
현재 user id: [콘솔에 찍힌 값 또는 비어 있음]
Supabase insert error: [error.message와 code]
현재 RLS insert policy: [정책 SQL 또는 대시보드 문구]
요청:
1. UI를 다시 만들지 말고 insert 실패 원인을 먼저 분류해 주세요.
2. RLS policy 문제인지, 로그인 상태 문제인지, payload 문제인지 나눠 주세요.
3. 바꿀 파일과 SQL을 각각 분리해서 최소 수정만 제안해 주세요.
4. service_role key를 브라우저 코드에 넣는 방식은 제안하지 마세요.
이 프롬프트는 AI가 버튼 컴포넌트 전체를 갈아엎는 일을 줄입니다. 특히 service_role 키를 클라이언트에 넣지 말라는 문장을 넣어야 합니다. 브라우저 코드는 사용자가 볼 수 있는 영역이기 때문에, 관리자 권한 키를 넣는 방식은 저장 실패 해결책이 아닙니다.
오늘은 RLS를 끄지 말고 증거 4칸을 채웁니다
Supabase 저장 실패는 막막해 보이지만 처음부터 큰 문제가 아닐 수 있습니다. 성공 토스트가 너무 일찍 뜨거나, 로그인 상태가 비어 있거나, insert policy의 with check 조건과 payload가 맞지 않는 식으로 원인이 좁혀집니다.
오늘 확인할 순서는 이것으로 충분합니다.
1. insert 결과에서 error를 콘솔에 찍는다.
2. 현재 사용자가 anon인지 authenticated인지 확인한다.
3. RLS insert policy의 with check 조건과 payload를 비교한다.
4. 확인용으로 .select()를 붙여 저장된 행을 본다.
이 네 칸을 채운 뒤에도 실패하면 그때는 테이블 이름, 컬럼 제약, select 정책, 목록 갱신 문제로 넘어갑니다. 하지만 먼저 할 일은 분명합니다. RLS를 끄기 전에 error와 auth 상태부터 확인합니다.
참고 출처
- Supabase Docs, Row Level Security: https://supabase.com/docs/guides/database/postgres/row-level-security
- Supabase Docs, JavaScript insert: https://supabase.com/docs/reference/javascript/insert
다음으로 읽을 기사
같은 흐름으로 이어 읽기 좋은 기사만 추려 보여줍니다.
첫 번째 댓글을 남겨보세요
여러분의 생각이 다른 독자에게 도움이 됩니다.
댓글 0
이 글을 읽은 독자들의 생각을 나눠보세요.