Supabase의 PostgREST: GraphQL처럼 쿼리하는 SQL
📌 TL;DR (한 줄 요약)
Supabase는 PostgREST를 통해 GraphQL의 선언적 쿼리 스타일을 REST API로 구현하여, SQL JOIN을 직관적인 체이닝 메서드로 사용할 수 있게 합니다.
🎯 핵심 정리
- Supabase JS는 SQL을 직접 쓰지 않고 체이닝 메서드 사용
- PostgREST는 GraphQL의 장점과 REST의 단순함을 결합한 프로토콜
- Foreign Key 기반으로 자동 관계 생성 - 별도 설정 불필요
- 중첩 쿼리로 복잡한 JOIN도 간단하게 표현
- TypeScript 타입 자동 생성으로 완벽한 타입 안정성
1. SQL JOIN의 전통적 방식
전통적인 SQL JOIN
-- 게시글 + 작성자 정보
SELECT
posts.id,
posts.title,
posts.content,
users.name,
users.avatar_url
FROM posts
LEFT JOIN users ON posts.user_id = users.id
WHERE posts.is_published = true
ORDER BY posts.created_at DESC
LIMIT 10;
문제점
- Over-fetching: 필요 없는 컬럼도 모두 가져옴
- Under-fetching: 추가 데이터가 필요하면 별도 쿼리 필요
- 복잡한 중첩: 댓글 + 댓글 작성자까지 가져오려면 복잡해짐
- 타입 안정성 부족: SQL 결과를 수동으로 타이핑
2. PostgREST의 철학
PostgREST란?
PostgreSQL 데이터베이스를 자동으로 RESTful API로 변환해주는 도구.
GraphQL과의 비교
GraphQL 쿼리
query {
posts {
id
title
user {
name
avatar
}
comments {
text
author {
name
}
}
}
}
Supabase (PostgREST) 쿼리
const { data } = await supabase
.from('posts')
.select(`
id,
title,
user:users (
name,
avatar
),
comments (
text,
author:users (
name
)
)
`);
거의 똑같습니다! 🎉
PostgREST의 장점
특징 | PostgREST | GraphQL | 전통 REST |
---|---|---|---|
설정 복잡도 | ✅ 낮음 (FK만) | ⚠️ 높음 (스키마+리졸버) | ⚠️ 중간 (엔드포인트) |
쿼리 유연성 | ✅ 높음 | ✅ 높음 | ❌ 낮음 |
Over-fetching 방지 | ✅ | ✅ | ❌ |
타입 안정성 | ✅ (자동생성) | ✅ (codegen) | ⚠️ (수동) |
학습 곡선 | ✅ 낮음 | ⚠️ 높음 | ✅ 낮음 |
핵심 개념: Foreign Key = 자동 관계
-- Foreign Key만 설정하면 끝!
CREATE TABLE posts (
id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id), -- ← 이것만으로 충분
title TEXT
);
CREATE TABLE comments (
id UUID PRIMARY KEY,
post_id UUID REFERENCES posts(id), -- ← 이것만으로 충분
user_id UUID REFERENCES users(id),
text TEXT
);
// 자동으로 관계 쿼리 가능!
.select('*, users(*), comments(*)')
3. 기본 JOIN 패턴
❌ SQL 문법은 사용할 수 없습니다
// ❌ 이런 건 안 됩니다!
await supabase
.from('posts')
.select('* LEFT JOIN users ON posts.user_id = users.id');
✅ 체이닝 메서드 사용
// ✅ 이렇게 사용합니다!
await supabase
.from('posts')
.select('*, users(*)');
패턴 1: 1:1 관계 (사용자 프로필)
-- 테이블 구조
users profiles
├── id (PK) ├── user_id (FK → users.id)
├── email ├── bio
└── created_at └── avatar_url
const { data } = await supabase
.from('users')
.select(`
id,
email,
profiles (
bio,
avatar_url
)
`);
// 결과:
// [
// {
// id: "user-1",
// email: "oeun@example.com",
// profiles: {
// bio: "프론트엔드 개발자",
// avatar_url: "https://..."
// }
// }
// ]
패턴 2: 1:N 관계 (게시글 + 댓글들)
-- 테이블 구조
posts comments
├── id (PK) ├── id (PK)
├── title ├── post_id (FK → posts.id)
└── user_id ├── text
└── user_id
const { data } = await supabase
.from('posts')
.select(`
id,
title,
comments (
id,
text
)
`);
// 결과:
// [
// {
// id: "post-1",
// title: "Supabase 시작하기",
// comments: [
// { id: "c1", text: "좋은 글이네요" },
// { id: "c2", text: "감사합니다" }
// ]
// }
// ]
패턴 3: 특정 컬럼만 선택
// 전체 선택
.select('*, users(*)')
// 특정 컬럼만
.select(`
id,
title,
users (
name,
email
)
`)
// 단일 컬럼
.select('title, users(name)')
패턴 4: 별칭(Alias) 사용
// Foreign Key가 2개일 때 (발신자/수신자)
const { data } = await supabase
.from('messages')
.select(`
content,
sender:users!sender_id (name, avatar_url),
receiver:users!receiver_id (name, avatar_url)
`);
// 결과:
// {
// content: "안녕하세요",
// sender: { name: "영희", avatar_url: "..." },
// receiver: { name: "철수", avatar_url: "..." }
// }
문법 설명:
sender:users
- 'sender'라는 별칭 사용!sender_id
- 어떤 Foreign Key를 사용할지 명시
4. 고급 JOIN 패턴
패턴 1: 중첩 JOIN (N단계)
-- 테이블 구조
posts → comments → users
// 게시글 + 댓글 + 댓글 작성자
const { data } = await supabase
.from('posts')
.select(`
id,
title,
comments (
id,
text,
users (
name,
avatar_url
)
)
`);
// 결과:
// [
// {
// title: "게시글",
// comments: [
// {
// text: "댓글",
// users: { name: "철수", avatar_url: "..." }
// }
// ]
// }
// ]
패턴 2: 다대다 관계 (게시글 + 태그)
-- 테이블 구조
posts post_tags tags
├── id ├── post_id ├── id
└── title └── tag_id └── name
const { data } = await supabase
.from('posts')
.select(`
id,
title,
post_tags (
tags (
id,
name
)
)
`);
// 결과:
// {
// title: "Next.js 가이드",
// post_tags: [
// { tags: { name: "typescript" } },
// { tags: { name: "react" } },
// { tags: { name: "nextjs" } }
// ]
// }
더 깔끔한 형태로 변환:
const posts = data?.map(post => ({
...post,
tags: post.post_tags.map(pt => pt.tags.name)
}));
// 결과:
// {
// title: "Next.js 가이드",
// tags: ["typescript", "react", "nextjs"]
// }
패턴 3: COUNT 집계
// 각 게시글의 댓글 수
const { data } = await supabase
.from('posts')
.select(`
id,
title,
comments (count)
`);
// 결과:
// [
// {
// id: "post-1",
// title: "게시글",
// comments: [{ count: 5 }]
// }
// ]
더 깔끔하게:
const posts = data?.map(post => ({
...post,
commentCount: post.comments[0]?.count || 0
}));
패턴 4: INNER vs LEFT JOIN
// LEFT JOIN (기본값) - 댓글 없는 게시글도 포함
.select('*, comments(*)')
// INNER JOIN - 댓글 있는 게시글만
.select('*, comments!inner(*)')
예제:
// 댓글이 하나라도 있는 게시글만 가져오기
const { data } = await supabase
.from('posts')
.select(`
*,
comments!inner (
id
)
`);
패턴 5: 필터링과 함께 사용
// JOIN된 테이블에 필터 적용
const { data } = await supabase
.from('posts')
.select(`
*,
comments!inner (
*,
users (name)
)
`)
.eq('comments.approved', true) // 승인된 댓글만
.gte('comments.created_at', '2024-01-01'); // 특정 날짜 이후
// 복수 조건
const { data } = await supabase
.from('posts')
.select('*, users(*)')
.eq('is_published', true)
.eq('users.role', 'author')
.order('created_at', { ascending: false })
.limit(10);
패턴 6: 외부 테이블에 LIMIT 적용
// 각 게시글의 최신 댓글 3개만
const { data } = await supabase
.from('posts')
.select(`
*,
comments (
*,
users (name)
)
`)
.order('comments.created_at', {
foreignTable: 'comments',
ascending: false
})
.limit(3, { foreignTable: 'comments' });
5. TypeScript 타입 안정성
타입 자동 생성
# Supabase CLI 설치
npm install -g supabase
# 타입 생성
npx supabase gen types typescript --project-id "your-project-id" > types/supabase.ts
생성된 타입 사용
// types/supabase.ts (자동 생성)
export interface Database {
public: {
Tables: {
posts: {
Row: {
id: string;
title: string;
user_id: string;
created_at: string;
};
Insert: {
id?: string;
title: string;
user_id: string;
created_at?: string;
};
Update: {
id?: string;
title?: string;
user_id?: string;
created_at?: string;
};
};
users: {
Row: {
id: string;
name: string;
email: string;
};
// ...
};
};
};
}
클라이언트에 타입 적용
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js';
import { Database } from '@/types/supabase';
export const supabase = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
타입 안정성의 힘
const { data } = await supabase
.from('posts') // ← 'posts' 자동완성!
.select('id, title, users(name)')
.eq('is_published', true); // ← 'is_published' 자동완성!
// ↑ 존재하지 않는 컬럼 입력 시 타입 에러!
// data의 타입도 자동 추론!
data?.[0].title // ✅ string
data?.[0].users.name // ✅ string
data?.[0].nonexistent // ❌ 타입 에러!
커스텀 쿼리 타입
// 복잡한 쿼리의 결과 타입 추출
type PostWithAuthorAndComments = Database['public']['Tables']['posts']['Row'] & {
users: Database['public']['Tables']['users']['Row'];
comments: Array
Database['public']['Tables']['comments']['Row'] & {
users: Database['public']['Tables']['users']['Row'];
}
>;
};
const { data } = await supabase
.from('posts')
.select(`
*,
users(*),
comments(*, users(*))
`)
.returns<PostWithAuthorAndComments[]>();
6. 실전 예제
예제 1: 블로그 시스템
요구사항:
- 게시글 목록 (최신순)
- 각 게시글의 작성자 정보
- 각 게시글의 댓글 수
- 각 게시글의 태그 목록
// app/api/posts/route.ts
import { createClient } from '@supabase/supabase-js';
import { Database } from '@/types/supabase';
export async function GET() {
const supabase = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
const { data, error } = await supabase
.from('posts')
.select(`
id,
title,
content,
created_at,
users (
name,
avatar_url
),
comments (count),
post_tags (
tags (
name,
color
)
)
`)
.eq('is_published', true)
.order('created_at', { ascending: false })
.limit(20);
if (error) {
return Response.json({ error: error.message }, { status: 500 });
}
// 데이터 변환
const posts = data.map(post => ({
id: post.id,
title: post.title,
content: post.content,
createdAt: post.created_at,
author: {
name: post.users?.name,
avatar: post.users?.avatar_url
},
commentCount: post.comments[0]?.count || 0,
tags: post.post_tags.map(pt => ({
name: pt.tags?.name,
color: pt.tags?.color
}))
}));
return Response.json({ posts });
}
예제 2: 소셜 피드
요구사항:
- 팔로우하는 사람들의 게시글만
- 최신 댓글 3개 (작성자 포함)
- 좋아요 수
-- 테이블 구조
follows
├── follower_id (현재 사용자)
└── following_id (팔로우 대상)
posts
├── id
├── user_id
└── content
likes
├── post_id
└── user_id
comments
├── post_id
├── user_id
└── text
export async function GET(request: Request) {
const userId = await getUserFromSession(request);
const supabase = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
// 1. 팔로우하는 사용자 ID 가져오기
const { data: followingData } = await supabase
.from('follows')
.select('following_id')
.eq('follower_id', userId);
const followingIds = followingData?.map(f => f.following_id) || [];
// 2. 피드 가져오기
const { data, error } = await supabase
.from('posts')
.select(`
id,
content,
created_at,
users (
id,
name,
avatar_url
),
likes (count),
comments (
id,
text,
created_at,
users (
name,
avatar_url
)
)
`)
.in('user_id', followingIds)
.order('created_at', { ascending: false })
.order('comments.created_at', {
foreignTable: 'comments',
ascending: false
})
.limit(3, { foreignTable: 'comments' })
.limit(20);
if (error) {
return Response.json({ error: error.message }, { status: 500 });
}
return Response.json({ feed: data });
}
예제 3: 이커머스 주문 상세
요구사항:
- 주문 정보
- 주문 항목들 (상품 정보 포함)
- 배송지 정보
- 결제 정보
const { data: order } = await supabase
.from('orders')
.select(`
id,
order_number,
status,
total_amount,
created_at,
users (
name,
email
),
order_items (
quantity,
price,
products (
name,
image_url,
description
)
),
shipping_addresses (
recipient_name,
address,
phone
),
payments (
method,
status,
paid_at
)
`)
.eq('id', orderId)
.single();
// 결과:
// {
// order_number: "ORD-2024-001",
// status: "delivered",
// users: { name: "~~", email: "..." },
// order_items: [
// {
// quantity: 2,
// price: 29000,
// products: { name: "키보드", image_url: "..." }
// }
// ],
// shipping_addresses: { ... },
// payments: { ... }
// }
예제 4: 실시간 채팅 (Realtime + JOIN)
// 실시간 구독 + JOIN
const channel = supabase
.channel('messages')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages'
},
async (payload) => {
// 새 메시지가 들어오면 작성자 정보 포함해서 가져오기
const { data } = await supabase
.from('messages')
.select(`
*,
users (
name,
avatar_url
)
`)
.eq('id', payload.new.id)
.single();
// UI 업데이트
addMessageToChat(data);
}
)
.subscribe();
7. 성능 최적화
최적화 1: 필요한 컬럼만 선택
// ❌ 비효율적 (모든 컬럼)
.select('*, users(*), comments(*)')
// ✅ 효율적 (필요한 것만)
.select(`
id,
title,
users (name, avatar_url),
comments (count)
`)
최적화 2: 인덱스 활용
-- Foreign Key는 자동으로 인덱스 생성되지만,
-- 필터링에 자주 쓰는 컬럼에는 인덱스 추가
CREATE INDEX idx_posts_published ON posts(is_published, created_at DESC);
CREATE INDEX idx_comments_approved ON comments(approved, created_at);
최적화 3: 페이지네이션
// 오프셋 페이지네이션
const page = 1;
const pageSize = 20;
const { data, count } = await supabase
.from('posts')
.select('*, users(name)', { count: 'exact' })
.range(page * pageSize, (page + 1) * pageSize - 1);
// 커서 페이지네이션 (더 효율적)
const { data } = await supabase
.from('posts')
.select('*, users(name)')
.lt('created_at', lastPostCreatedAt) // 커서
.order('created_at', { ascending: false })
.limit(20);
최적화 4: 쿼리 결과 캐싱
// Next.js App Router
export async function getPosts() {
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
const { data } = await supabase
.from('posts')
.select('*, users(name)');
return data;
}
// 캐싱 (5분)
export const revalidate = 300;
최적화 5: N+1 문제 방지
// ❌ N+1 문제 (N번의 추가 쿼리)
const { data: posts } = await supabase.from('posts').select('*');
for (const post of posts) {
// 각 게시글마다 별도 쿼리!
const { data: user } = await supabase
.from('users')
.select('name')
.eq('id', post.user_id)
.single();
post.authorName = user.name;
}
// ✅ 해결: 한 번에 JOIN
const { data: posts } = await supabase
.from('posts')
.select(`
*,
users (name)
`);
8. 한계와 대안
Supabase JS의 한계
1. 복잡한 집계 쿼리
-- ❌ 이런 건 불가능
SELECT
users.name,
COUNT(posts.id) as post_count,
AVG(posts.views) as avg_views,
MAX(posts.created_at) as latest_post
FROM users
LEFT JOIN posts ON users.id = posts.user_id
GROUP BY users.id, users.name
HAVING COUNT(posts.id) > 5;
해결책: PostgreSQL Function (RPC)
CREATE FUNCTION get_active_users()
RETURNS TABLE(
name TEXT,
post_count BIGINT,
avg_views NUMERIC,
latest_post TIMESTAMPTZ
) AS $$
SELECT
users.name,
COUNT(posts.id) as post_count,
AVG(posts.views) as avg_views,
MAX(posts.created_at) as latest_post
FROM users
LEFT JOIN posts ON users.id = posts.user_id
GROUP BY users.id, users.name
HAVING COUNT(posts.id) > 5;
$$ LANGUAGE SQL;
const { data } = await supabase.rpc('get_active_users');
2. UNION, INTERSECT 등
-- ❌ 불가능
SELECT id FROM posts WHERE user_id = '123'
UNION
SELECT id FROM drafts WHERE user_id = '123';
해결책: RPC 또는 여러 쿼리 조합
const [posts, drafts] = await Promise.all([
supabase.from('posts').select('id').eq('user_id', userId),
supabase.from('drafts').select('id').eq('user_id', userId)
]);
const allIds = [...posts.data!, ...drafts.data!];
3. 서브쿼리 (복잡한 경우)
-- ❌ 이런 서브쿼리는 불가능
SELECT *
FROM posts
WHERE views > (
SELECT AVG(views) FROM posts WHERE category = posts.category
);
해결책: RPC 또는 클라이언트에서 처리
4. WINDOW 함수
-- ❌ 불가능
SELECT
*,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) as rank
FROM posts;
해결책: RPC
대안 비교
필요 기능 | 해결 방법 | 복잡도 | 성능 |
---|---|---|---|
간단한 JOIN | Supabase JS | ✅ 낮음 | ✅ 좋음 |
복잡한 집계 | RPC | ⚠️ 중간 | ✅ 좋음 |
UNION, 복잡한 로직 | RPC | ⚠️ 중간 | ✅ 좋음 |
트랜잭션 | RPC | ⚠️ 중간 | ✅ 좋음 |
직접 제어 필요 | pg 라이브러리 |
⚠️ 높음 | ✅ 좋음 |
🎓 결론
PostgREST의 핵심 가치
- GraphQL의 선언적 스타일: 필요한 것만 명시적으로 요청
- REST의 단순함: 별도 서버 설정 불필요
- PostgreSQL의 강력함: SQL의 모든 기능 활용 가능
- 타입 안정성: TypeScript 완벽 지원
언제 Supabase JS를 사용할까?
✅ 추천하는 경우
- CRUD 중심의 애플리케이션
- 1~3단계 정도의 JOIN
- 빠른 프로토타이핑
- 타입 안정성이 중요한 프로젝트
- Next.js/React와 함께 사용
⚠️ RPC 함께 사용 권장
- 복잡한 집계 쿼리
- 트랜잭션 필요
- 비즈니스 로직이 복잡한 경우
- GROUP BY, HAVING 등 고급 SQL 필요
❌ 다른 방법 고려
- 실시간 복잡한 분석 (BI 도구 사용)
- 매우 복잡한 데이터 모델 (GraphQL 고려)
- 레거시 DB 통합
GraphQL vs Supabase PostgREST
측면 | Supabase | GraphQL |
---|---|---|
설정 | ✅ FK만 설정 | ⚠️ 스키마+리졸버 작성 |
타입 생성 | ✅ CLI 한 줄 | ✅ Codegen 필요 |
학습 곡선 | ✅ SQL 아는 사람에게 쉬움 | ⚠️ 새로운 개념 학습 |
유연성 | ⚠️ PostgreSQL 기능 제한 | ✅ 완전한 자유 |
커뮤니티 | ⚠️ 상대적으로 작음 | ✅ 매우 큼 |
에코시스템 | ⚠️ Supabase 전용 | ✅ 다양한 도구 |
실무 조합 추천
// 1. 간단한 CRUD → Supabase JS
const { data: posts } = await supabase
.from('posts')
.select('*, users(name)');
// 2. 복잡한 쿼리 → RPC
const { data: stats } = await supabase
.rpc('get_user_statistics', { user_id: userId });
// 3. 트랜잭션 → RPC
const { data: result } = await supabase
.rpc('create_order_with_items', {
items: [...],
total: 10000
});
// 4. 실시간 → Supabase Realtime
supabase
.channel('posts')
.on('postgres_changes', { ... }, callback)
.subscribe();
핵심 기억사항
- Foreign Key = 자동 관계: 설정만 하면 끝
select()
문법은 GraphQL과 유사: 중첩 가능- 점(
.
)이 아닌 괄호(()
)로 관계 탐색:users(name)
- TypeScript 타입은 자동 생성: CLI 한 줄로 해결
- 복잡한 건 RPC로: SQL의 모든 기능 활용
마이그레이션 가이드
기존 SQL → Supabase JS
-- Before: SQL
SELECT
posts.id,
posts.title,
users.name,
COUNT(comments.id) as comment_count
FROM posts
LEFT JOIN users ON posts.user_id = users.id
LEFT JOIN comments ON posts.id = comments.post_id
WHERE posts.is_published = true
GROUP BY posts.id, users.name
ORDER BY posts.created_at DESC
LIMIT 10;
// After: 간단한 부분은 Supabase JS
const { data } = await supabase
.from('posts')
.select(`
id,
title,
users (name),
comments (count)
`)
.eq('is_published', true)
.order('created_at', { ascending: false })
.limit(10);
// 복잡한 집계는 RPC로
CREATE FUNCTION get_posts_with_stats() ...
GraphQL → Supabase
# Before: GraphQL
query {
posts(where: { published: { _eq: true } }) {
id
title
user {
name
}
comments_aggregate {
aggregate {
count
}
}
}
}
// After: Supabase (거의 같은 구조!)
const { data } = await supabase
.from('posts')
.select(`
id,
title,
users (name),
comments (count)
`)
.eq('is_published', true);
📚 참고 자료
💡 추신: 추가로 궁금할 만한 점
Q1: JOIN 성능이 걱정됩니다
A: Supabase JS의 JOIN은 PostgREST를 통해 실제 PostgreSQL의 JOIN으로 변환됩니다. 따라서 성능은 일반 SQL JOIN과 동일합니다. 오히려 필요한 컬럼만 선택하므로 네트워크 트래픽이 줄어듭니다.
// 이 쿼리는 내부적으로 최적화된 SQL JOIN으로 실행됩니다
.select('id, title, users(name)')
// 실제 실행되는 SQL (단순화):
// SELECT posts.id, posts.title, users.name
// FROM posts
// LEFT JOIN users ON posts.user_id = users.id
Q2: 몇 단계까지 중첩할 수 있나요?
A: 이론적으로는 제한이 없지만, 실무에서는 3~4단계가 적당합니다. 그 이상은 가독성과 성능을 위해 RPC를 고려하세요.
// ✅ 적당함 (3단계)
.select('*, comments(*, users(*))')
// ⚠️ 과도함 (5단계)
.select('*, a(*, b(*, c(*, d(*))))')
// 이럴 땐 RPC 사용 권장
Q3: 같은 테이블을 여러 번 JOIN할 수 있나요?
A: 네, 별칭(alias)을 사용하면 됩니다.
// 발신자와 수신자 (둘 다 users 테이블)
.select(`
content,
sender:users!sender_id(name),
receiver:users!receiver_id(name)
`)
Q4: 관계가 없는 테이블도 JOIN할 수 있나요?
A: 아니요. Supabase JS는 Foreign Key 기반. 관계가 없다면:
- Foreign Key를 추가하거나
- RPC 함수를 사용하거나
- 여러 쿼리를 조합하세요
// Foreign Key 없이 JOIN 필요하다면
const { data } = await supabase.rpc('custom_join_function');
Q5: 조건부 JOIN은 어떻게 하나요?
A: Supabase JS는 조건부 JOIN을 직접 지원하지 않습니다. 클라이언트에서 처리하거나 RPC를 사용하세요.
// 클라이언트에서 처리
const query = supabase
.from('posts')
.select('*');
if (includeAuthor) {
query.select('*, users(*)');
}
const { data } = await query;
Q6: 순환 참조는 어떻게 처리하나요?
A: PostgREST는 순환 참조를 감지하고 자동으로 차단합니다. 필요하다면 명시적으로 깊이를 제한하세요.
// 무한 루프 방지를 위해 명시적으로 선택
.select(`
id,
name,
parent_category:categories!parent_id(id, name)
`)
// 2단계만 가져오기
'TIL' 카테고리의 다른 글
[251009 TIL] Hasura 트랜잭션 처리 (1) | 2025.10.09 |
---|---|
[251009 TIL] PostgREST? (supabase vs hasura) (0) | 2025.10.09 |
[251009 TIL] Supabase RPC 총정리 (0) | 2025.10.09 |
[251006 TIL] Terraform + Neon + Hasura 구축기 (1) | 2025.10.06 |
[250929 TIL] ApolloClient 디버거 직접 만들기 (0) | 2025.09.29 |