Supabase에서 RPC 제대로 쓰기 — 트랜잭션부터 호출까지

프론트엔드를 공부하다 보면 "데이터를 읽고 쓰는 것"까지는 금방 익숙해지는데, 막상 결제·재고·포인트처럼 여러 데이터를 한꺼번에 바꿔야 하는 순간부터 코드가 흔들리기 시작합니다. "재고는 줄었는데 주문은 안 생겼다", "같은 좌석이 두 명에게 팔렸다" 같은 버그가 바로 이 지점에서 나옵니다.
이 글은 그 문제를 트랜잭션이라는 개념으로 이해하고, Supabase에서 **RPC(데이터베이스 함수 호출)**로 깔끔하게 푸는 방법을 처음부터 끝까지 정리한 것입니다. 순서는 이렇습니다.
- 왜 보통 방식이 위험한가 (문제 상황)
- 트랜잭션이란 무엇인가
- RPC와 데이터베이스 함수의 정체
- 함수 만들기 — 인자 받고, 결과 돌려주기
- 함수 배포하기 (마이그레이션 / SQL 에디터)
- 앱에서 호출하기 — 인자 넘기고 결과 받기
- 실전 예제 4가지
- 보안: SECURITY DEFINER와 RLS
- 자주 하는 실수와 디버깅
- 정리
1. 왜 보통 방식이 위험한가
상품을 주문하는 코드를 생각해 봅시다. 보통은 이렇게 짜기 쉽습니다.
// (위험한 방식) 앱에서 여러 쿼리를 순서대로 실행 const { data: product } = await supabase .from("products").select("stock").eq("id", productId).single(); if (product.stock < qty) throw new Error("재고 부족"); await supabase.from("products") .update({ stock: product.stock - qty }).eq("id", productId); // ① 재고 차감 await supabase.from("orders") .insert({ user_id, product_id: productId, qty }); // ② 주문 생성 await supabase.from("users") .update({ point: userPoint - usedPoint }).eq("id", user_id); // ③ 포인트 차감
언뜻 멀쩡해 보이지만 두 가지 큰 구멍이 있습니다.
(1) 중간에 실패하면 데이터가 어긋난다
①에서 재고를 깎았는데 ②에서 네트워크가 끊기거나 에러가 나면, 재고는 줄었는데 주문은 없는 상태가 남습니다. 손으로 되돌리는 보상 코드를 짤 수도 있지만, 그 보상 코드마저 실행되기 전에 함수가 죽으면 소용이 없습니다.
(2) 동시에 들어오면 같은 재고를 두 번 판다
재고가 1개 남았을 때 두 사람이 거의 동시에 주문하면, 둘 다 stock = 1을 읽고 → 둘 다 "재고 있음"으로 판단 → 둘 다 주문에 성공합니다. 이걸 **경합(race condition)**이라고 합니다. 읽기와 쓰기가 떨어져 있는 한 막을 수 없습니다.
이 두 문제를 한 번에 푸는 게 바로 트랜잭션입니다.
2. 트랜잭션이란 무엇인가
트랜잭션(transaction)은 여러 개의 작업을 "하나의 묶음"으로 처리하는 것입니다. 핵심 약속은 단 하나입니다.
묶음 안의 작업이 전부 성공하거나, 전부 취소되거나 둘 중 하나다. "절반만 적용된 상태"는 없다.
은행 송금을 떠올리면 쉽습니다. "내 계좌에서 1만 원 빠짐"과 "상대 계좌에 1만 원 들어옴"은 반드시 같이 일어나야 합니다. 내 돈만 빠지고 상대에겐 안 들어가는 일은 절대 없어야 하죠. 이 둘을 한 트랜잭션으로 묶으면, 중간에 무슨 일이 생겨도 둘 다 되거나 둘 다 안 되거나가 보장됩니다.
트랜잭션이 보장하는 성질을 흔히 ACID라고 부르는데, 지금은 이 두 가지만 기억하면 충분합니다.
- 원자성(Atomicity): 전부 되거나 전부 취소. (위 "절반은 없다"가 이것)
- 격리성(Isolation): 동시에 실행되는 트랜잭션끼리 서로 꼬이지 않게 데이터베이스가 정리해 준다. (경합을 막아주는 근거)
여기서 중요한 사실 하나. 데이터베이스(PostgreSQL)는 트랜잭션을 아주 잘합니다. 문제는 우리가 앱(프론트/서버)에서 쿼리를 따로따로 날리면 그 묶음이 깨진다는 거죠. 그래서 **"여러 작업을 데이터베이스 안으로 가져가서 한 덩어리로 실행"**하면 됩니다. 그 방법이 바로 데이터베이스 함수이고, 그걸 앱에서 부르는 통로가 RPC입니다.
3. RPC와 데이터베이스 함수의 정체
RPC = Remote Procedure Call, "원격에 있는 함수를 호출한다"는 뜻입니다. Supabase에서 RPC라고 하면 보통 PostgreSQL 안에 만들어 둔 함수를 앱에서 supabase.rpc()로 호출하는 것을 말합니다.
그림으로 보면 이렇습니다.
[ 앱 코드 ] [ PostgreSQL (Supabase) ] supabase.rpc("create_order", { }) ───호출──▶ create_order 함수 실행 (재고 차감 + 주문 생성 + 포인트 차감을 하나의 트랜잭션으로 처리) 결과(주문 id / 에러) ◀──────반환────
즉, 흩어져 있던 ①②③ 작업을 DB 안의 함수 하나로 옮겨 담는 겁니다. 함수 안에서 일어나는 일은 자동으로 하나의 트랜잭션이 되기 때문에, 중간에 실패하면 함수가 한 모든 변경이 통째로 취소됩니다. 경합도 데이터베이스가 알아서 정리해 줍니다.
용어 정리: PostgreSQL에는
FUNCTION과PROCEDURE가 있는데, Supabase의rpc()는 FUNCTION을 호출합니다. 그래서 이 글에서는 전부create function으로 만듭니다.
4. 함수 만들기 — 인자 받고, 결과 돌려주기
데이터베이스 함수의 기본 골격은 이렇습니다.
create or replace function 함수이름(인자 목록) returns 반환타입 language 언어 as $$ -- 함수 본문 $$;
하나씩 뜯어봅시다.
4-1. 인자(파라미터) 받기
괄호 안에 이름 타입 형태로 적습니다. 관례적으로 파라미터 이름 앞에 p_를 붙여 테이블 컬럼과 헷갈리지 않게 합니다.
create or replace function increment_view_count(p_post_id bigint) ...
여러 개면 콤마로 나열하고, 기본값도 줄 수 있습니다.
create or replace function get_daily_signups(p_days int default 30) ...
4-2. 결과(반환값) 돌려주기
returns 뒤에 함수가 무엇을 돌려줄지를 적습니다. 크게 보면 "값 하나만 줄 거냐, 표(여러 줄)를 줄 거냐"의 차이입니다.
- 값 하나 — 숫자 한 개, 글자 한 개, id 하나처럼 딱 한 개의 값. 이걸 어려운 말로 **스칼라(scalar)**라고 부릅니다. 그냥 "값 하나"라고 생각하면 됩니다. 예: 증가된 조회수
153, 새로 만든 주문 id. - 표(여러 줄) — 엑셀 표처럼 여러 행·여러 열이 있는 결과. 예: "상태별 정산 합계" 목록.
이걸 기준으로 자주 쓰는 형태가 넷입니다.
| 반환 타입 | 언제 쓰나 | 앱에서 받는 모양 |
|---|---|---|
returns void | 돌려줄 게 없을 때(그냥 실행만 하고 끝) | null (빈 값) |
returns bigint 등 | 값 하나(스칼라)를 줄 때 — 숫자/글자/id 한 개 | 그 값 그대로 |
returns table(...) | 표를 줄 때 — 열 이름과 타입을 직접 정해서 | 배열(여러 줄) |
returns setof 테이블명 | 표를 주되, 이미 있는 테이블의 행 모양 그대로 | 배열(여러 줄) |
void는 영어로 "비어 있음"이란 뜻으로, "돌려줄 값이 없다"는 표시입니다.bigint는 "큰 정수"(아주 큰 숫자까지 담는 숫자 타입)라고 보면 됩니다.
4-3. 언어: sql vs plpgsql
language sql: 쿼리 한두 개로 끝나는 단순한 함수. 짧고 빠릅니다.language plpgsql: 변수 선언,if분기, 반복문, 에러 던지기(raise)가 필요한 함수. 트랜잭션 로직은 대부분 이쪽입니다.
4-4. plpgsql 함수의 구조
create or replace function 함수이름(p_x int) returns bigint language plpgsql as $$ declare v_result bigint; -- 변수 선언 (관례: v_ 접두사) begin -- 여기서 쿼리 실행, 분기, 계산 ... select count(*) into v_result from some_table where x = p_x; if v_result = 0 then raise exception '결과 없음'; -- 에러 던지면 트랜잭션 전체 롤백 end if; return v_result; -- 결과 반환 end; $$;
기억할 점 세 가지:
declare블록에서 변수를 선언하고,begin ... end사이에 로직을 씁니다.select ... into 변수로 쿼리 결과를 변수에 담습니다.raise exception을 만나면 함수가 그때까지 한 모든 변경이 자동으로 취소(롤백)됩니다. 이게 "전부 되거나 전부 안 되거나"를 구현하는 핵심 장치입니다.
왜
$$ ... $$로 감싸나요? 함수 본문 안에 따옴표가 많이 들어가서, 본문 전체를 통째로 묶는 특별한 따옴표(달러 인용)를 쓰는 겁니다.$$대신$func$같은 이름표를 붙여도 됩니다.
5. 함수 배포하기
만든 함수를 실제 데이터베이스에 올리는 방법은 두 가지입니다.
방법 A — Supabase 대시보드 SQL 에디터 (빠르게 시도)
대시보드 → SQL Editor에 위 create or replace function ...을 붙여넣고 실행하면 끝입니다. 연습하거나 빠르게 확인할 때 좋습니다.
방법 B — 마이그레이션 파일 (팀 프로젝트 권장)
팀으로 작업한다면 함수도 **코드로 관리(버전 관리)**하는 게 맞습니다. supabase/migrations/ 아래에 SQL 파일로 두면, 누가 언제 어떤 함수를 바꿨는지 git에 남습니다.
# 마이그레이션 파일 생성 (이름은 자유) supabase migration new create_order_function # → supabase/migrations/20260624XXXXXX_create_order_function.sql 생성됨
그 파일에 create or replace function ...을 적고,
supabase db push # 원격 DB에 반영 # 또는 로컬 개발 DB라면 supabase migration up
create or replace로 써두면 같은 함수를 여러 번 배포해도 덮어쓰기가 되어 안전합니다.
6. 앱에서 호출하기 — 인자 넘기고 결과 받기
이제 프론트/서버 코드에서 부릅니다. 기본형은 이렇습니다.
const { data, error } = await supabase.rpc("함수이름", { 인자객체 });
세 가지만 알면 됩니다.
6-1. 인자는 객체로, 키 이름은 함수 파라미터와 똑같이
-- 함수 정의 create function get_daily_signups(p_days int default 30) ...
// 호출: 키 이름(p_days)이 SQL 파라미터 이름과 정확히 일치해야 한다 const { data, error } = await supabase.rpc("get_daily_signups", { p_days: 7 });
여기서 초보자가 가장 많이 틀립니다. SQL 파라미터가 p_days인데 JS에서 { days: 7 }로 넘기면 "그런 인자 없음" 에러가 납니다. 이름을 똑같이 맞추세요.
6-2. 결과 받기
함수가 값 하나를 주느냐 표를 주느냐에 따라 data 모양이 다릅니다.
// (1) 값 하나(스칼라)를 돌려주는 함수 → data 가 그 값 그대로 const { data } = await supabase.rpc("increment_view_count", { p_post_id: 10 }); console.log(data); // 예: 153 (증가된 조회수, 숫자 한 개) // (2) 표(여러 줄)를 돌려주는 함수 → data 가 배열 const { data } = await supabase.rpc("get_daily_signups", { p_days: 7 }); console.log(data); // [{ day: "2026-06-24", count: 12 }, ...] ← 줄들의 목록 // (3) 표인데 "딱 한 줄만 올 것"이 확실하면 .single() 로 배열을 벗겨 객체로 받는다 const { data } = await supabase.rpc("create_order", { ... }).single();
6-3. 에러 처리
error가 있으면 함수 안에서 raise exception이 났거나 실패한 겁니다. 반드시 확인하세요.
const { data, error } = await supabase.rpc("create_order", { ... }); if (error) { // 함수가 raise exception 으로 던진 메시지가 error.message 에 담겨 온다 if (error.message.includes("SOLD_OUT")) { return alert("죄송합니다. 매진되었습니다."); } return alert("주문 처리 중 오류가 발생했습니다."); } // 여기 도달했다면 함수 전체가 성공적으로 "커밋"된 것 // (커밋 = 변경 내용을 데이터베이스에 진짜로 확정 저장하는 것. 반대는 롤백 = 전부 없던 일로 되돌리기) console.log("주문 완료, 주문 id =", data);
7. 실전 예제 4가지
예제 A. 조회수 1 증가 (원자적 카운터)
가장 간단하면서도 RPC가 왜 필요한지 보여주는 예입니다. 앱에서 "읽고 +1 해서 다시 쓰기"를 하면 동시 접속 시 카운트가 누락됩니다. DB 함수로 하면 한 번에 안전하게 올라갑니다.
create or replace function increment_view_count(p_post_id bigint) returns bigint language sql as $$ update posts set view_count = view_count + 1 where id = p_post_id returning view_count; $$;
const { data: newCount } = await supabase .rpc("increment_view_count", { p_post_id: postId });
update ... returning으로 증가된 값을 바로 돌려받습니다. 읽기와 쓰기가 한 문장이라 경합이 없습니다.
예제 B. 주문 생성 + 재고 차감 (트랜잭션의 핵심)
이 글의 출발점이었던 문제를 RPC로 풉니다.
create or replace function create_order( p_user_id uuid, p_product_id uuid, p_qty int ) returns uuid -- 생성된 주문 id 를 돌려줌 language plpgsql as $$ declare v_order_id uuid; begin -- ① 재고가 충분할 때만 차감 (조건이 안 맞으면 0행이 영향받음) update products set stock = stock - p_qty where id = p_product_id and stock >= p_qty; if not found then -- 위 update가 한 행도 못 바꿨다면 = 재고 부족 raise exception 'SOLD_OUT'; -- → 여기서 함수 전체가 롤백된다 end if; -- ② 주문 생성 insert into orders (user_id, product_id, qty, status) values (p_user_id, p_product_id, p_qty, 'paid') returning id into v_order_id; -- ③ 필요하면 포인트 차감 등 추가 작업도 여기서 (전부 같은 트랜잭션) return v_order_id; end; $$;
핵심 두 가지를 짚어볼게요.
where ... and stock >= p_qty: 재고 확인과 차감을 한 문장으로 합쳤습니다. 동시에 두 요청이 와도 데이터베이스가 행을 잠그며 처리하므로, 재고가 음수로 빠지지 않습니다. 경합 해결.raise exception 'SOLD_OUT': 재고가 부족하면 예외를 던지는데, 이 순간 ①에서 했을지도 모를 변경까지 전부 자동 취소됩니다. "재고만 줄고 주문은 없는" 상태가 원천적으로 불가능해집니다.
const { data: orderId, error } = await supabase.rpc("create_order", { p_user_id: userId, p_product_id: productId, p_qty: 2, }); if (error) { return alert(error.message.includes("SOLD_OUT") ? "매진되었습니다." : "주문 실패"); } console.log("주문 완료:", orderId);
이제 앱 코드에서 보상 로직을 짤 필요가 없습니다. 실패하면 데이터베이스가 알아서 깨끗하게 되돌려 줍니다.
예제 C. 집계 — 정산 합계 (returns table)
"전체 결제를 다 읽어서 앱에서 더하기"는 데이터가 많아지면 느려집니다. 합계는 데이터베이스가 제일 잘합니다.
create or replace function settlement_summary(p_seller_id uuid) returns table (status text, total bigint, count bigint) language sql stable -- 데이터를 바꾸지 않는 읽기 전용 함수라는 표시 as $$ select status, sum(amount)::bigint as total, count(*)::bigint as count from settlements where seller_id = p_seller_id group by status; $$;
const { data } = await supabase.rpc("settlement_summary", { p_seller_id: sellerId }); // data: [{ status: "pending", total: 120000, count: 3 }, { status: "completed", ... }]
행을 앱으로 끌어오지 않으니 결제가 아무리 쌓여도 응답 속도가 일정합니다. (읽기 전용 함수에는 stable을 붙여 두면 좋습니다.)
예제 D. 인자를 여러 개 / 배열로 넘기기 (jsonb)
장바구니처럼 여러 상품을 한 번에 주문할 땐, 항목 목록을 jsonb로 통째로 넘기는 방법이 깔끔합니다.
jsonb는 데이터베이스가 JSON을 그대로 담을 수 있는 타입입니다. JS의[{ product_id, qty }, ...]같은 배열·객체를 그대로 인자로 넘기면, 함수 안에서 하나씩 꺼내 쓸 수 있습니다.
create or replace function create_cart_order( p_user_id uuid, p_items jsonb -- [{ "product_id": "...", "qty": 2 }, ...] ) returns uuid language plpgsql as $$ declare v_order_id uuid; v_item jsonb; begin insert into orders (user_id, status) values (p_user_id, 'paid') returning id into v_order_id; -- 배열을 한 개씩 돌면서 재고 차감 + 주문 항목 추가 for v_item in select * from jsonb_array_elements(p_items) loop update products set stock = stock - (v_item->>'qty')::int where id = (v_item->>'product_id')::uuid and stock >= (v_item->>'qty')::int; if not found then raise exception 'SOLD_OUT:%', v_item->>'product_id'; end if; insert into order_items (order_id, product_id, qty) values (v_order_id, (v_item->>'product_id')::uuid, (v_item->>'qty')::int); end loop; return v_order_id; end; $$;
const { data: orderId, error } = await supabase.rpc("create_cart_order", { p_user_id: userId, p_items: [ { product_id: "aaa...", qty: 2 }, { product_id: "bbb...", qty: 1 }, ], });
장바구니에 담긴 상품 중 하나라도 재고가 부족하면 주문 전체가 취소됩니다. 이게 트랜잭션의 힘입니다.
8. 보안: SECURITY DEFINER와 RLS
Supabase는 보통 **RLS(Row Level Security)**로 "누가 어떤 행을 읽고 쓸 수 있는지"를 막아둡니다. 그런데 함수를 호출하는 사람의 권한으로는 막혀서 함수가 제 일을 못 하는 경우가 있습니다. 이때 쓰는 게 security definer입니다.
create or replace function settlement_summary(p_seller_id uuid) returns table (...) language sql security definer -- 함수를 만든 사람(주인)의 권한으로 실행 set search_path = public -- 보안상 반드시 함께 지정 as $$ ... $$;
security invoker(기본값): 함수를 호출한 사람의 권한으로 실행. RLS가 그대로 적용됩니다.security definer: 함수를 만든 사람(주로 관리자)의 권한으로 실행. RLS를 우회할 수 있습니다.
security definer는 강력한 만큼 조심해야 합니다. 두 가지를 꼭 지키세요.
set search_path = public을 같이 적는다. 안 그러면 공격자가 엉뚱한 스키마를 끼워넣어 함수를 악용할 수 있습니다.- 함수 안에서 권한을 직접 확인한다. definer로 RLS를 우회하는 만큼, "이 사람이 정말 이 작업을 할 자격이 있는가"를 함수 첫머리에서 검사해야 합니다.
-- 예: 관리자만 호출할 수 있어야 하는 함수 begin if (select role from users where id = auth.uid()) <> 'admin' then raise exception 'FORBIDDEN'; end if; -- ... 관리자 작업 ... end;
참고: 호출하는 사람을 함수 안에서 알아내려면
auth.uid()(현재 로그인한 사용자 id)를 씁니다. 단,security definer일 때는 호출자 정보가 그대로 들어오는지 환경에 따라 다를 수 있으니, 필요하면 사용자 id를 인자로 받아서 검증하는 방식이 더 확실합니다.
마지막으로, 함수를 anon(비로그인)이나 일반 사용자도 호출하게 하려면 실행 권한을 주어야 합니다.
grant execute on function settlement_summary(uuid) to authenticated; -- 비로그인도 호출해야 하면 anon 에게도: -- grant execute on function some_public_fn() to anon;
9. 자주 하는 실수와 디버깅
- 인자 이름 불일치: SQL은
p_user_id인데 JS에서{ userId }로 넘김 → "function ... does not exist" 비슷한 에러. 이름을 정확히 맞추세요. - 타입 불일치:
uuid자리에 그냥 문자열을 넣었는데 형식이 안 맞으면invalid input syntax for type uuid(코드 22P02). 넘기는 값의 형식을 확인하세요. error를 확인 안 함:supabase.rpc()는 실패해도 throw하지 않고{ error }로 돌려줍니다. 항상if (error)로 확인하세요.- 반환 타입 착각:
returns table인데data를 객체로 기대하면 어긋납니다. table/setof는 항상 배열입니다(행 하나만 원하면.single()). - 함수가 안 보일 때: 새로 만든 함수가 인식 안 되면 대시보드에서 SQL을 한 번 더 실행했는지, 마이그레이션이 실제로 반영(push)됐는지 확인하세요. Supabase가 함수 목록을 잠깐 늦게 갱신해서 몇 초 뒤에야 인식되는 경우도 있습니다.
- 디버깅: 함수 중간값을 보고 싶으면
raise notice '값: %', v_변수;로 로그를 남길 수 있습니다(raise exception과 달리 롤백되지 않습니다).
10. 정리
- 여러 데이터를 한꺼번에 바꿔야 하면 트랜잭션이 필요하고, 그 트랜잭션을 가장 안전하게 다루는 곳은 데이터베이스 안입니다.
- Supabase에서는 데이터베이스 함수(
create function)를 만들고supabase.rpc()로 호출하는 방식으로 이를 구현합니다. 함수 안의 작업은 자동으로 하나의 트랜잭션이라, 전부 성공 아니면 전부 취소가 보장됩니다. - 인자는 객체로, 키 이름은 SQL 파라미터와 똑같이 넘기고, 결과는 반환 타입(스칼라/배열)에 맞게 받습니다.
error는 반드시 확인합니다. raise exception이 롤백의 스위치,where ... and stock >= qty가 경합을 막는 장치,security definer는 강력하니 권한 검사를 잊지 마세요.
처음엔 "왜 굳이 SQL로 함수까지 만들어야 하나" 싶을 수 있는데, 결제·재고·예약처럼 틀어지면 안 되는 데이터를 다루는 순간 RPC가 가장 든든한 도구가 됩니다. 작은 것(조회수 +1)부터 한 번 만들어 호출해 보면 금방 손에 익을 겁니다.
부록. 어려운 말, 한 줄로
본문에 나온 낯선 용어들을 쉬운 말로 정리했습니다. 읽다가 막히면 여기서 찾아보세요.
| 용어 | 한 줄 설명 |
|---|---|
| 트랜잭션(transaction) | 여러 작업을 한 묶음으로 처리해서 "전부 성공 아니면 전부 취소"를 보장하는 것 |
| 원자성(atomicity) | 트랜잭션의 핵심 성질. "쪼개지지 않는다" = 절반만 적용되는 일이 없다 |
| 격리성(isolation) | 동시에 실행되는 작업들이 서로 꼬이지 않게 데이터베이스가 정리해 주는 성질 |
| 커밋(commit) | 변경 내용을 데이터베이스에 진짜로 확정 저장하는 것 |
| 롤백(rollback) | 변경을 전부 없던 일로 되돌리는 것. 커밋의 반대 |
| 경합(race condition) | 두 요청이 거의 동시에 같은 데이터를 건드려 결과가 어긋나는 현상(예: 같은 재고를 두 번 판매) |
| 스칼라(scalar) | "값 하나". 숫자 한 개, 글자 한 개, id 하나처럼 딱 한 개의 값 |
| RPC | Remote Procedure Call. 멀리 있는(여기선 DB 안의) 함수를 불러서 결과만 받는 것 |
| 함수(function) | 미리 만들어 둔 작업 묶음. 이름으로 불러서 실행하고 결과를 받는다 |
| plpgsql | PostgreSQL에서 변수·조건문·반복문·에러던지기까지 쓸 수 있는 함수 작성 언어 |
| RLS(Row Level Security) | "누가 어떤 행을 읽고 쓸 수 있는지"를 데이터베이스가 행 단위로 막아주는 보안 기능 |
| security definer | 함수를 호출한 사람이 아니라 함수를 만든 사람의 권한으로 실행하는 옵션(RLS를 우회할 수 있어 조심) |
| jsonb | 데이터베이스가 JSON(배열·객체)을 그대로 담을 수 있는 타입 |
| uuid | a1b2...처럼 길고 고유한 식별자 형식. 흔히 행의 id로 쓴다 |
| bigint | 아주 큰 정수까지 담는 숫자 타입 |
| void | "비어 있음". 돌려줄 값이 없다는 표시 |
| raise exception | 함수 안에서 에러를 일부러 던지는 것. 이 순간 트랜잭션이 롤백된다 |
| returning | insert/update 한 결과(예: 방금 만든 행의 id)를 바로 돌려받는 SQL 문법 |
| auth.uid() | 지금 로그인한 사용자의 id를 알려주는 Supabase 내장 함수 |
| 스키마(schema) | 테이블·함수들을 담아두는 폴더 같은 묶음. Supabase 기본은 public |
| search_path | "이름만 적었을 때 어느 스키마에서 찾을지" 순서. security definer 함수에선 public으로 고정해 두는 게 안전 |
| 마이그레이션(migration) | DB 구조 변경(테이블·함수 등)을 SQL 파일로 기록해 버전 관리하는 것 |





댓글
댓글을 작성하려면 이 필요합니다.