PostgREST vs GraphQL(Hasura): 현실적인 기술 스택 비교
PostgreSQL을 사용하는 프로젝트에서 API 레이어를 구성할 때, 두 가지 옵션을 비교해봤습니다.
이 비교는 GraphQL이 정말 필요한 상황은 언제인가? 에 대해서 생각해보다가 시작하게 되었습니다.
GraphQL + Hasura + codegen 의 강력함은 프로젝트를 해보면서 알게 되었는데
다만 PostgREST 에서도 유연한 쿼리, 관계 기반 페칭이 잘 되므로(supabase.js 에서 보듯...)
요구사항만 맞는다면 Hasura 혹은 resolver 구성에 드는 비용을 줄이면서도
GraphQL의 이점을 유사하게 구사할 수 있을 것 같았습니다.
그래서 GraphQL + Hasura 에 대항할 수 있는 REST 스펙을 아래와 같이 고민해 보았습니다.
스택 구성 비교
PostgREST 스택
// 기본 구성
PostgreSQL → PostgREST → postgrest-js → openapi-typescript/zod
GraphQL + Hasura 스택
// 기본 구성
PostgreSQL → Hasura → GraphQL → graphql-codegen
핵심 기능 비교
1. 쿼리 작성 방식
PostgREST
// REST 기반 체이닝
const { data } = await db
.from('posts')
.select(`
*,
author:users(name, email),
comments(count)
`)
.eq('published', true)
.order('created_at', { ascending: false })
.limit(10)
Hasura (GraphQL)
query GetPosts {
posts(
where: { published: { _eq: true } }
order_by: { created_at: desc }
limit: 10
) {
id
title
author {
name
email
}
comments_aggregate {
aggregate {
count
}
}
}
}
2. 타입 안전성 구현
PostgREST + OpenAPI
# OpenAPI 스펙에서 타입 생성
npx openapi-typescript http://localhost:3000/ \
--output ./types/database.ts
// 생성된 타입 활용
import { paths, components } from './types/database'
type User = components['schemas']['users']
type Post = components['schemas']['posts']
// Zod 스키마로 런타임 검증 추가
import { z } from 'openapi-zod'
const UserSchema = z.schema(components['schemas']['users'])
Hasura + GraphQL Codegen
# codegen.yml
generates:
./src/generated/graphql.tsx:
plugins:
- typescript
- typescript-operations
- typescript-react-query
// 자동 생성된 훅 사용
import { useGetPostsQuery } from '@/generated/graphql'
const { data, loading, error } = useGetPostsQuery({
variables: { limit: 10 }
})
실제 사용 시나리오별 비교
시나리오 1: 단순 CRUD 작업
PostgREST >> 더 간단
// 한 줄로 처리
await db.from('users').insert({ name: 'John', email: 'john@example.com' })
Hasura
// mutation 정의 필요
const INSERT_USER = gql`
mutation InsertUser($name: String!, $email: String!) {
insert_users_one(object: { name: $name, email: $email }) {
id
}
}
`
await client.mutate({ mutation: INSERT_USER, variables: { ... } })
시나리오 2: 복잡한 관계 데이터 조회
PostgREST
// 깊은 중첩은 복잡해짐
const { data } = await db
.from('organizations')
.select(`
*,
departments!inner(
*,
employees(
*,
manager:employees!manager_id(name),
projects(*)
)
)
`)
Hasura >> 더 직관적이고 편리!
query GetOrgStructure {
organizations {
name
departments {
name
employees {
name
manager {
name
}
projects {
title
status
}
}
}
}
}
시나리오 3: 실시간 구독
PostgREST >> 추가 구성 없이는 불가능
// 별도 Realtime 서버 필요
import { RealtimeClient } from '@supabase/realtime-js'
const client = new RealtimeClient('ws://localhost:4000/socket')
const channel = client.channel('db-changes')
.on('postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'messages' },
(payload) => console.log(payload.new)
)
.subscribe()
Hasura >> 내장 지원
const MESSAGE_SUBSCRIPTION = gql`
subscription OnMessageAdded {
messages(order_by: { created_at: desc }, limit: 1) {
id
content
user {
name
}
}
}
`
// 바로 사용 가능
⭐️ 각 스택이 빛나는 순간은?
PostgREST가 최적인 경우
- 단일 PostgreSQL DB 중심 서비스
// 모든 데이터가 하나의 DB에 있을 때
const dashboard = await db
.from('analytics')
.select('*')
.gte('date', '2024-01-01')
- 빠른 프로토타이핑
// 별도 스키마 정의 없이 바로 시작
// DB 테이블 = API 엔드포인트
- RESTful API 선호 환경
// 기존 REST 클라이언트와 호환
fetch('/api/users?age=gte.18&select=name,email')
- 서버리스/엣지 환경
// Vercel Edge Function
export const runtime = 'edge'
export async function GET() {
// PostgREST는 HTTP 요청만으로 작동
const res = await fetch(process.env.POSTGREST_URL + '/users')
return Response.json(await res.json())
}
Hasura + GraphQL이 최적인 경우
- 다중 데이터소스 통합
# Hasura metadata
remote_schemas:
- name: payment_service
definition:
url: https://payment-api.com/graphql
- name: auth_service
definition:
url: https://auth-api.com/graphql
- 복잡한 권한 관리
{
"permission": {
"role": "user",
"select": {
"filter": {
"_or": [
{ "owner_id": { "_eq": "X-Hasura-User-Id" } },
{ "visibility": { "_eq": "public" } }
]
},
"columns": ["id", "title", "content"],
"computed_fields": ["likes_count"]
}
}
}
- MSA 환경의 API Gateway
# 여러 서비스를 하나의 GraphQL로 통합
type Query {
# PostgreSQL (Hasura)
users: [User!]!
# Redis (Remote Schema)
activeUsers: [ActiveUser!]!
# Elasticsearch (Action)
searchPosts(query: String!): [Post!]!
}
- 모바일 앱 최적화 >> 클라이언트마다, 상황마다 매번 이것저것 다른 필드 요청이 빈번할 때
# 필요한 필드만 정확히 요청
query MobileOptimized {
posts {
id
title
thumbnailUrl # 큰 이미지 제외
# content 제외 - 필요시만 추가 요청
}
}
성능 & 운영 비교
인프라 복잡도
PostgREST
# docker-compose.yml
services:
postgres:
image: postgres:17
postgrest:
image: postgrest/postgrest
depends_on:
- postgres
# 끝! 매우 단순
Hasura
services:
postgres:
image: postgres:17
hasura:
image: hasura/graphql-engine
depends_on:
- postgres
# 메타데이터 관리, 마이그레이션 등 추가 고려사항 있음
번들 사이즈
// PostgREST 클라이언트
import { PostgrestClient } from '@supabase/postgrest-js' // ~15kb
// GraphQL 클라이언트
import { ApolloClient, InMemoryCache } from '@apollo/client' // ~130kb
// 또는
import { createClient } from 'urql' // ~45kb
러닝 커브
PostgREST
- REST API 지식만 필요
- PostgreSQL 함수/뷰 활용하면 확장 가능
- 팀원 온보딩 빠름
Hasura
- GraphQL 개념 이해 필요
- Hasura 특유의 설정 학습 필요...
- 강력한 기능이지만 초기 러닝커브 존재
정리
PostgREST 선택 !!
- 🏢 B2B SaaS, 관리자 대시보드
- 📱 서버 사이드 렌더링 중심 웹앱
- 🚀 MVP, 빠른 프로토타입
- 💾 단일 PostgreSQL DB 서비스
Hasura 선택 !!
- 📱 모바일 앱 백엔드
- 🌐 MSA 환경의 통합 레이어
- 🔄 실시간 협업 기능 중심 서비스
- 🏗️ 여러 데이터소스 조합 필요
'TIL' 카테고리의 다른 글
| [251123 TIL] OpenTelemetry? (with Next.js) (0) | 2025.11.23 |
|---|---|
| [251123 TIL] Next.js instrumentation.ts 정리 (0) | 2025.11.23 |
| [251031 TIL] naver 로그인 구현 with Supabase 3편 (1) | 2025.10.31 |
| [251029 TIL] Cookie, CORS, Site, Origin 총정리 (0) | 2025.10.29 |
| [251026 TIL] 실전적 Apollo Client 구현기 (0) | 2025.10.26 |