
Supabase 데이터가 안 보여요 (RLS의 배신)
DB에 데이터가 분명히 있는데, 프론트엔드에서는 빈 배열(`[]`)만 옵니다. Supabase 초보자가 가장 많이 겪는 RLS(Row Level Security) 정책 위반 문제와 해결법을 정리해봤습니다.

DB에 데이터가 분명히 있는데, 프론트엔드에서는 빈 배열(`[]`)만 옵니다. Supabase 초보자가 가장 많이 겪는 RLS(Row Level Security) 정책 위반 문제와 해결법을 정리해봤습니다.
로버트 C. 마틴(Uncle Bob)이 제안한 클린 아키텍처의 핵심은 무엇일까요? 양파 껍질 같은 계층 구조와 의존성 규칙(Dependency Rule)을 통해 프레임워크와 UI로부터 독립적인 소프트웨어를 만드는 방법을 정리합니다.

프론트엔드 개발자가 알아야 할 4가지 저장소의 차이점과 보안 이슈(XSS, CSRF), 그리고 언제 무엇을 써야 하는지에 대한 명확한 기준.

DB 설계의 기초. 데이터를 쪼개고 쪼개서 이상 현상(Anomaly)을 방지하는 과정. 제1, 2, 3 정규형을 쉽게 설명합니다.

Debug에선 잘 되는데 Release에서만 죽나요? 범인은 '난독화'입니다. R8의 원리, Mapping 파일 분석, 그리고 Reflection을 사용하는 라이브러리를 지켜내는 방법(@Keep)을 정리해봤습니다.

Supabase 대시보드에서 보면 users 테이블에 데이터가 100개가 꽉 차 있습니다.
그런데 Flutter 앱에서 select()를 하면 빈 깡통([])만 돌아옵니다.
에러도 안 납니다. 그냥 데이터가 "없는 척" 합니다.
final data = await supabase.from('users').select();
print(data); // []
"아니 에러라도 뱉던가!" 이 소름 돋는 현상의 범인은 RLS (Row Level Security)입니다. Supabase는 기본적으로 "허락받지 않은 자에게는 데이터가 안 보인다"는 철학을 가집니다.
RLS를 켜는 순간(Enable RLS), 모든 요청은 기본적으로 차단(Block)됩니다.
방화벽과 같습니다. 구멍을 뚫어주지 않으면 아무도 못 지나갑니다.
개발자가 "누구나 읽을 수 있음(Enable Read Access to Everyone)" 정책을 추가해줘야 비로소 보입니다.
Postgres의 권한 체계는 단순합니다.
RLS가 켜져 있으면, 쿼리 실행 결과에서 조건에 맞지 않는 행(Row)을 필터링해서 줍니다. 접근 금지가 아니라, "너한테 보여줄 데이터는 0건이야"라고 거짓말을 하는 겁니다. (보안상의 이유로 존재 자체를 숨김) 그래서 에러가 나는 게 아니라, 그냥 빈 결과가 나오는 것입니다.
가장 흔한 실수입니다. 데이터를 넣는(INSERT) 정책만 만들고, 읽는(SELECT) 정책을 안 만든 경우입니다.
-- ❌ 반쪽짜리 정책 (쓰기만 가능)
CREATE POLICY "Users can insert their own profile"
ON public.profiles FOR INSERT
WITH CHECK (auth.uid() = user_id);
이렇게 하면 회원가입 후 내 프로필을 만들 수는 있는데, 조회하려고 하면 빈 배열이 뜹니다. "내가 쓴 글인데 왜 내가 못 봐?"라는 상황이 벌어집니다.
해결책: SELECT 정책을 반드시 추가해야 합니다.
-- ✅ 읽기 정책 추가
CREATE POLICY "Users can view their own profile"
ON public.profiles FOR SELECT
USING (auth.uid() = user_id);
만약 "내 프로필은 나만 보고, 남의 프로필은 이름만 보여주고 싶다"면?
정책을 두 개 만들어야 합니다. 복잡해지죠? 그래서 보통은 profiles 테이블은 public 읽기 허용으로 풀고 민감한 정보(이메일, 폰번호)는 별도 테이블(private_profiles)로 분리합니다.
anon vs authenticated 역할"로그인 안 한 사람도 게시글을 보게 하고 싶어요."
이럴 땐 Target roles 설정을 조심해야 합니다. 정책 생성 시 기본적으로 authenticated (로그인 유저)만 선택되어 있는 경우가 많습니다.
authenticated: 로그인한 사용자만 적용 (JWT 토큰 보유).anon: 로그인 안 한 사용자(Guest)만 적용.public (기본값): 모두에게 적용.로그인 여부 상관없이 보여주고 싶다면 Role을 비워두거나 public으로 설정해야 합니다.
TO authenticated라고 명시된 정책은 비로그인 유저에게는 아예 존재하지 않는 것처럼 동작합니다.
USING vs WITH CHECK정책을 만들 때 두 가지 조건문이 있습니다. 헷갈리면 보안 구멍이 생깁니다.
UPDATE의 경우 (둘 다 씀):
USING: "내가 쓴 글인가?" (대상 선정) -> auth.uid() == user_idWITH CHECK: "내용을 이상하게 바꾸진 않나?" (변경 후 유효성) -> new.role != 'admin'만약 WITH CHECK 없이 USING만 쓰면?
"내 글(USING 통과)"을 수정해서 "user_id를 남의 것으로 바꿈"이 가능해집니다. 그러면 글의 소유권이 넘어가 버립니다.
RLS는 강력하지만 가끔은 방해가 됩니다.
예를 들어, "회원가입 시 profiles 테이블에 행을 추가"하는 트리거가 있다고 칩시다.
회원가입 직후에는 아직 사용자가 로그인이 안 된 상태(anon)일 수도 있고, 권한 체크가 꼬일 수 있습니다.
이럴 때 쓰는 것이 Postgres Function과 SECURITY DEFINER 옵션입니다.
-- 이 함수는 호출한 놈(Invoker)의 권한이 아니라,
-- 만든 놈(Definer: 보통 Admin/Postgres)의 권한으로 실행됨.
CREATE OR REPLACE FUNCTION create_user_profile()
RETURNS TRIGGER
SECURITY DEFINER
AS $
BEGIN
INSERT INTO public.profiles (user_id) VALUES (new.id);
RETURN new;
END;
$ LANGUAGE plpgsql;
SECURITY DEFINER를 쓰면 RLS를 우회해서(Bypass) 관리자 권한으로 데이터를 조작할 수 있습니다.
단, 남용하면 보안 구멍이 되므로 꼭 필요한 시스템 로직에만 써야 합니다.
RLS 정책도 결국은 SQL WHERE 절입니다.
auth.uid() = user_id라는 정책은 WHERE user_id = '...' 쿼리를 매번 실행하는 것과 같습니다.
즉, user_id 컬럼에 인덱스(Index)가 없으면 성능이 박살 납니다.
데이터가 적을 땐 모르지만, 10만 건이 넘어가면 SELECT * FROM posts가 엄청 느려집니다.
모든 행을 다 뒤지면서(Full Scan) "이게 니꺼냐?" 하고 물어봐야 하니까요.
한 번은 어드민 대시보드(users 테이블 전체 조회)를 만드는데, "분명 어드민 계정으로 로그인했는데 데이터가 0건"인 상황을 겪었습니다.
알고 보니 제가 만든 RLS 정책이 문제였습니다.
-- 기존 정책: 자기 자신만 볼 수 있음
CREATE POLICY "Users can see own data" ON users
FOR SELECT USING (auth.uid() = id);
이 정책 때문에 어드민조차 "자기 자신(데이터 1건)"밖에 못 봤던 겁니다.
Supabase는 is_admin 같은 매직 플래그가 없습니다. 어드민도 결국 authenticated 유저일 뿐입니다.
해결책은 정책에 OR 조건을 추가하는 것이었습니다.
CREATE POLICY "Users can see own data OR Admin sees all" ON users
FOR SELECT USING (
auth.uid() = id
OR
(SELECT role FROM profiles WHERE id = auth.uid()) = 'admin'
);
이렇게 하면 어드민은 모든 데이터를 볼 수 있게 됩니다. 교훈: RLS는 "예외"가 없습니다. 어드민을 위한 뒷문(Backdoor)도 명시적으로 정책(Policy)으로 뚫어줘야 합니다.
Supabase SQL 에디터에서 권한 문제를 테스트하려면 이렇게 하세요.
-- 익명 유저로 빙의 (로그인 안 한 상태 테스트)
SET ROLE anon;
SELECT * FROM posts; -- 빈 배열이면 anon 정책 문제
-- 로그인 유저로 빙의
-- (JWT 토큰 흉내는 어렵지만, RLS 정책 조건에 내 UUID를 하드코딩해서 테스트)
또는 Supabase 대시보드의 "Policy Tester" 기능을 활용하면 특정 유저 ID로 쿼리를 시뮬레이션할 수 있습니다.
Q: I enabled RLS but still see everything.
A: Check if you are using the service_role key in your backend. Only the anon and public keys are subject to RLS. The Service Role key bypasses RLS entirely.
Q: Can I use RLS strictly for one column?
A: No, RLS is Row Level. For Column Level Security (e.g., hiding just the email address), you need to move that column to a separate table (e.g., private_profiles) or use a View.
Q: My policy logic is too complex.
A: If you have 10 OR conditions, performance will tank. Wrap complex logic in a Postgres Function and call that function in your policy. USING ( is_admin(auth.uid()) ).