
내 ID가 왜 달라요? (auth.uid() vs user_id)
로그인한 유저 ID를 가져오려고 했는데 `auth.uid()`가 에러를 뱉거나 엉뚱한 값을 줍니다. RLS(Row Level Security)에서 `auth.uid()`를 올바르게 사용하는 법과 `security definer` 함수의 비밀.

로그인한 유저 ID를 가져오려고 했는데 `auth.uid()`가 에러를 뱉거나 엉뚱한 값을 줍니다. RLS(Row Level Security)에서 `auth.uid()`를 올바르게 사용하는 법과 `security definer` 함수의 비밀.
로버트 C. 마틴(Uncle Bob)이 제안한 클린 아키텍처의 핵심은 무엇일까요? 양파 껍질 같은 계층 구조와 의존성 규칙(Dependency Rule)을 통해 프레임워크와 UI로부터 독립적인 소프트웨어를 만드는 방법을 정리합니다.

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

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

프링글스 통(Stack)과 맛집 대기 줄(Queue). 가장 기초적인 자료구조지만, 이걸 모르면 재귀 함수도 메시지 큐도 이해할 수 없습니다.

RLS(Row Level Security) 정책이나 트리거(Trigger)를 짤 때 가장 많이 쓰는 함수가 auth.uid()입니다.
그런데 이 함수가 가끔 에러를 뱉거나 null을 리턴해서 사람을 미치게 합니다.
-- ❌ DB 툴(DBeaver 등)에서 실행하면 에러: function auth.uid() does not exist
SELECT auth.uid();
특히 Database Function을 만들거나 외부 도구(TablePlus)에서 테스트할 때 자주 발생합니다.
auth.uid()는 일반적인 PostgreSQL 내장 함수가 아닙니다.
Supabase(정확히는 PostgREST)가 API 요청을 받을 때, 헤더에 있는 JWT 토큰을 해석해서 잠시 메모리에 심어두는 값(request.jwt.claim.sub)을 꺼내오는 래퍼(Wrapper) 함수입니다.
즉, API를 통하지 않고 직접 DB에 접속하거나, 토큰 없이 요청하면 auth.uid()는 작동하지 않거나 null을 반환합니다.
BEFORE INSERT 트리거에서 "누가 글을 썼는지" 자동으로 기록하고 싶습니다.
CREATE FUNCTION handle_new_post() RETURNS TRIGGER AS $
BEGIN
-- ⚠️ 위험: 관리자 모드나 Batch 작업에선 터질 수 있음
NEW.user_id := auth.uid();
RETURN NEW;
END;
$ LANGUAGE plpgsql;
API로 호출할 땐 잘 되지만, 나중에 관리자 페이지에서 데이터를 일괄 입력(Bulk Insert)할 때 auth.uid()가 null이라서 에러가 터집니다(Not Null 제약조건 위반).
해결책: 방어 로직을 넣어야 합니다.
BEGIN
-- user_id가 비어있을 때만 채워줌 (관리자가 직접 넣으면 그거 씀)
IF NEW.user_id IS NULL THEN
NEW.user_id := auth.uid();
END IF;
RETURN NEW;
END;
RLS에서 auth.uid()는 필수입니다.
"내 글은 나만 볼 수 있다"를 구현하려면 이렇게 씁니다.
-- ✅ SELECT 정책 (내가 쓴 글만 보임)
create policy "Individuals can view their own posts"
on posts for select
using ( auth.uid() = user_id );
하지만 여기서 실수하는 게, Insert 정책입니다.
"내 아이디로만 글을 쓸 수 있다"를 강제해야 합니다.
-- ✅ INSERT 정책 (내 아이디로만 생성 가능)
create policy "Individuals can create posts"
on posts for insert
with check ( auth.uid() = user_id );
USING은 기존 행을 조회/수정할 때, WITH CHECK는 새로운 행을 넣을 때 검사합니다.
Supabase의 유저 정보는 auth라는 별도 스키마(auth.users)에 있습니다.
하지만 우리는 보통 public 스키마에 profiles 테이블을 만들어서 씁니다.
이때 함수 내부에서 auth.users를 조회하려고 하면 권한 에러가 납니다. (일반 유저는 auth 스키마 접근 불가)
해결책: security definer 옵션을 사용하세요.
이 옵션을 주면, 함수가 실행되는 동안만 함수 생성자(Superuser)의 권한을 빌려씁니다.
-- 유저 가입 시 자동으로 profiles에도 row 생성
create function public.handle_new_user()
returns trigger as $
begin
insert into public.profiles (id, email)
values (new.id, new.email);
return new;
end;
$ language plpgsql security definer; -- 👈 핵심!
주의: security definer는 "양날의 검"입니다. 아무나 이 함수를 호출해서 관리자 권한을 쓸 수 없도록, RLS나 권한 부여(GRANT EXECUTE)를 잘 해야 합니다.
auth.jwt() 활용 깊이 들여다보기단순 ID 말고, 유저의 메타데이터(예: is_admin 같은 Custom Claims)가 필요하다면?
auth.jwt() 함수를 쓰면 토큰 전체를 JSON으로 받을 수 있습니다.
-- 관리자(admin) 그룹인지 확인
select (auth.jwt() -> 'app_metadata' ->> 'role') = 'admin';
이걸 활용하면 RLS 정책에서 "관리자는 모든 글을 볼 수 있다" 같은 로직도 짤 수 있습니다.
auth.uid()는 API 요청 문맥(Context) 안에서만 산다. 트리거에선 null 체크를 필수적으로 하고, 권한이 더 필요하면 security definer를 써라.
Supabase 설정을 보면 anon 키와 service_role 키 두 가지가 있습니다.
가끔 "에러가 나서 귀찮다"는 이유로 프론트엔드 코드에 service_role 키를 넣는 분들이 계십니다.
절대 안 됩니다. 해커가 그 키를 보면 DB의 모든 데이터를(심지어 auth.users의 암호화된 비밀번호 해시까지) 덤프 떠갈 수 있습니다.
service_role 키는 오직 Edge Functions나 백엔드 서버에서만 은밀하게 사용하세요.
sub)와 유효기간(exp), 메타데이터가 들어있습니다.sudo와 같습니다.app_metadata에 role: admin 같은 정보를 심어서 권한 관리를 할 수 있습니다.For most developers coming from Firebase, Supabase's auth.uid() feels familiar but behaves strangely in edge cases.
You might see errors like:
function auth.uid() does not existnull value in column "user_id" violates not-null constraintThese errors happen because auth.uid() is NOT a static value. It is a dynamic value extracted from the request context.
To understand the error, you must understand the architecture. Supabase uses PostgREST to turn your PostgreSQL database into a REST API.
Authorization: Bearer <JWT_TOKEN>.set_config('request.jwt.claim.sub', 'user_uuid', true).auth.uid() acts as a getter for that configuration variable.The implication:
If you connect to the DB directly (via port 5432) using postgres user, there is no HTTP request. Therefore, there is no JWT, and auth.uid() returns NULL.
Triggers are powerful. You want to automatically assign ownership to every new row.
CREATE FUNCTION assign_owner() RETURNS TRIGGER AS $
BEGIN
NEW.user_id := auth.uid();
RETURN NEW;
END;
$ LANGUAGE plpgsql;
CREATE TRIGGER on_create_post
BEFORE INSERT ON posts
FOR EACH ROW EXECUTE FUNCTION assign_owner();
Scenario A (User): A user creates a post via valid API. auth.uid() is 123. NEW.user_id becomes 123. Success.
Scenario B (Admin): You want to seed the database or fix some data using the SQL Editor or a script. You run INSERT INTO posts.... There is no JWT. auth.uid() is NULL. The INSERT fails.
The Fix: Always program defensively.
BEGIN
-- Allow manual override. If I provide a user_id, respect it.
-- If I don't provide one (NULL), try to get it from auth.uid().
IF NEW.user_id IS NULL THEN
NEW.user_id := auth.uid();
END IF;
-- Validation (Optional)
IF NEW.user_id IS NULL THEN
RAISE EXCEPTION 'User ID is required';
END IF;
RETURN NEW;
END;
Row Level Security (RLS) is the firewall of your database.
Writing policies often involves auth.uid().
"Users can see their own data."
CREATE POLICY "Select Own Data" ON posts
FOR SELECT
USING (auth.uid() = user_id);
"Users can create their own data." Many developers blindly copy the SELECT policy. But for INSERT, you need TWO checks:
CREATE POLICY "Insert Own Data" ON posts
FOR INSERT
WITH CHECK (auth.uid() = user_id);
If you use USING for INSERT, it does nothing. You must use WITH CHECK.
Ideally, you should enable PL/pgSQL Security Definer functions if the logic gets complex, but for simple ownership, the policy above is standard.
Sometimes you need to break the rules.
For example, when a user signs up, you want to create a Profile row.
But standard users might not have permission to INSERT into the profiles table directly, or auth.users table is globally unaccessible.
Solution: Use security definer.
This tells Postgres: "Run this function with the privileges of the User who defined/created the function (usually the superuser/admin), NOT the user who is calling it."
It is equivalent to sudo in Linux.
-- Trigger on auth.users (System table)
-- When a user is created in Auth, create a profile in Public
create function public.on_auth_user_created()
returns trigger as $
begin
insert into public.profiles (id, full_name, avatar_url)
values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');
return new;
end;
$ language plpgsql security definer; -- 👈 Run as Admin!
Warning: Since this runs as Admin, RLS is bypassed. Be very careful.
If you make a function delete_user(id) with security definer, ANY logged-in user could call it and delete anyone if you don't add checks inside the function (IF auth.uid() != id THEN RAISE...).
auth.jwt() vs auth.uid()auth.uid() returns a simple UUID.
auth.jwt() returns the entire JSON object of the token.
This is incredible for Role Based Access Control (RBAC).
Instead of querying a separate user_roles table (which is slow and complex in RLS), you can embed the role inside the JWT App Metadata.
Example:
app_metadata: { "role": "admin" } in the user's Auth object (server-side).create policy "Admins can do anything"
on posts
for all
using (
auth.jwt() -> 'app_metadata' ->> 'role' = 'admin'
);
This is lightning fast because it requires zero DB joins. The data is already in the memory (the token).
Access Token in Supabase. It contains user identity (sub), expiration (exp), and metadata (app_metadata).sudo in Linux.auth.users, the function can't either.auth.uid().sub (subject/user_id), iss (issuer), exp (expiry). Custom claims can hold role, plan_type, etc.