Hasura의 특별한 점
일반 GraphQL 서버 (Apollo Server 등):
GraphQL → 직접 작성한 리졸버 → SQL 쿼리
(여기서 DataLoader 필요!)
Hasura:
GraphQL → Hasura 엔진 → 최적화된 SQL 자동 생성
(DataLoader 불필요! 이미 내장됨)
Hasura가 자동으로 해주는 것들
1. 자동 JOIN 최적화
query GetCampaign {
campaign(where: { id: { _eq: "abc-123" } }) {
id
title
applications {
id
status
influencer {
id
username
platforms {
name
follower_count
}
}
}
}
}
Hasura가 자동 생성하는 SQL:
-- 한 번의 쿼리로!
SELECT
campaign.id,
campaign.title,
applications.id AS applications_id,
applications.status,
influencer.id AS influencer_id,
influencer.username,
platforms.name AS platform_name,
platforms.follower_count
FROM campaign
LEFT JOIN LATERAL (
SELECT * FROM campaign_application
WHERE campaign_id = campaign.id
) applications ON true
LEFT JOIN LATERAL (
SELECT * FROM influencer_profile
WHERE id = applications.influencer_id
) influencer ON true
LEFT JOIN LATERAL (
SELECT * FROM influencer_platform
WHERE influencer_id = influencer.id
) platforms ON true
WHERE campaign.id = 'abc-123';
즉, N+1 문제가 애초에 발생하지 않아요! ✨
2. 배치 쿼리 자동 처리
query GetMultipleCampaigns {
campaign1: campaign_by_pk(id: "abc-1") { ...fields }
campaign2: campaign_by_pk(id: "abc-2") { ...fields }
campaign3: campaign_by_pk(id: "abc-3") { ...fields }
}
Hasura가 생성하는 SQL:
-- 한 번에 배치 처리
SELECT * FROM campaign
WHERE id IN ('abc-1', 'abc-2', 'abc-3');
DataLoader가 하는 일을 Hasura 엔진이 알아서 해줘요!
Hasura + Next.js 15 App Router 구조
아키텍처
┌─────────────────────────────────────┐
│ Next.js 15 App Router (Frontend)
│ ├─ Server Components
│ ├─ Client Components
│ └─ Apollo Client
└──────────────┬──────────────────────┘
│ GraphQL over HTTP
↓
┌─────────────────────────────────────┐
│ Hasura v2 (GraphQL Engine)
│ ├─ Auto-generated Schema
│ ├─ Query Optimizer
│ ├─ Permission System
│ └─ Subscription Engine
└──────────────┬──────────────────────┘
│ SQL
↓
┌─────────────────────────────────────┐
│ PostgreSQL Database
│ └─ 설계한 스키마 + 인덱스 │
└─────────────────────────────────────┘
Setup 예시
1. Hasura 설정
# docker-compose.yml
version: '3.6'
services:
postgres:
image: postgres:15
environment:
POSTGRES_PASSWORD: password
hasura:
image: hasura/graphql-engine:v2.36.0
ports:
- "8080:8080"
environment:
HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:password@postgres:5432/aibee
HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
HASURA_GRAPHQL_ADMIN_SECRET: "admin-secret"
2. Hasura에서 관계 설정
Hasura Console에서 클릭 몇 번이면 끝:
campaign
├─ applications (array relationship)
└─ influencer (object relationship)
└─ platforms (array relationship)
이렇게 설정하면 자동으로 nested query 가능!
3. Next.js 15 App Router에서 사용
Server Component (SSR)
// app/campaigns/[id]/page.tsx
import { getClient } from '@/lib/apollo-client-rsc'
import { gql } from '@apollo/client'
const GET_CAMPAIGN = gql`
query GetCampaign($id: uuid!) {
campaign_by_pk(id: $id) {
id
title
status
applications(where: { status: { _eq: "approved" } }) {
id
influencer {
username
platforms {
platform {
name
}
follower_count
}
}
}
}
}
`
export default async function CampaignPage({
params
}: {
params: { id: string }
}) {
const client = getClient()
const { data } = await client.query({
query: GET_CAMPAIGN,
variables: { id: params.id },
})
return (
<div>
<h1>{data.campaign_by_pk.title}</h1>
{/* ... */}
</div>
)
}
// lib/apollo-client-rsc.ts (Server Component용)
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client'
import { registerApolloClient } from '@apollo/experimental-nextjs-app-support/rsc'
export const { getClient } = registerApolloClient(() => {
return new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: process.env.NEXT_PUBLIC_HASURA_URL,
headers: {
'x-hasura-admin-secret': process.env.HASURA_ADMIN_SECRET!,
},
}),
})
})
Client Component (CSR)
// app/campaigns/[id]/applications.tsx
'use client'
import { gql, useQuery } from '@apollo/client'
const GET_APPLICATIONS = gql`
query GetApplications($campaignId: uuid!) {
campaign_application(
where: { campaign_id: { _eq: $campaignId } }
order_by: { created_at: desc }
) {
id
status
influencer {
username
platforms_aggregate {
aggregate {
sum {
follower_count
}
}
}
}
}
}
`
export default function Applications({ campaignId }: { campaignId: string }) {
const { data, loading } = useQuery(GET_APPLICATIONS, {
variables: { campaignId },
})
if (loading) return <div>Loading...</div>
return (
<ul>
{data.campaign_application.map(app => (
<li key={app.id}>{app.influencer.username}</li>
))}
</ul>
)
}
// lib/apollo-client.tsx (Client Component용)
'use client'
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client'
import { ApolloNextAppProvider } from '@apollo/experimental-nextjs-app-support/ssr'
function makeClient() {
return new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: process.env.NEXT_PUBLIC_HASURA_URL,
headers: {
'x-hasura-admin-secret': process.env.HASURA_ADMIN_SECRET!,
},
}),
})
}
export function ApolloWrapper({ children }: { children: React.ReactNode }) {
return (
<ApolloNextAppProvider makeClient={makeClient}>
{children}
</ApolloNextAppProvider>
)
}
// app/layout.tsx
import { ApolloWrapper } from '@/lib/apollo-client'
export default function RootLayout({ children }) {
return (
<html>
<body>
<ApolloWrapper>
{children}
</ApolloWrapper>
</body>
</html>
)
}
Hasura의 강력한 기능들
1. 집계 쿼리 자동 생성
query CampaignStats {
campaign_by_pk(id: "abc-123") {
title
applications_aggregate {
aggregate {
count
avg {
rating
}
}
nodes {
influencer {
username
}
}
}
}
}
생성되는 SQL:
-- 효율적으로 집계
SELECT
campaign.title,
COUNT(applications.id),
AVG(applications.rating)
FROM campaign
LEFT JOIN campaign_application applications
ON applications.campaign_id = campaign.id
WHERE campaign.id = 'abc-123'
GROUP BY campaign.id;
2. 필터링과 정렬
query TopInfluencers {
influencer_profile(
where: {
platforms: {
follower_count: { _gte: 10000 }
}
}
order_by: { created_at: desc }
limit: 10
) {
username
platforms_aggregate {
aggregate {
sum {
follower_count
}
}
}
}
}
Hasura가 알아서 최적의 인덱스를 활용해요!
3. 실시간 Subscription
subscription WatchCampaignApplications($campaignId: uuid!) {
campaign_application(
where: { campaign_id: { _eq: $campaignId } }
) {
id
status
influencer {
username
}
}
}
'use client'
import { gql, useSubscription } from '@apollo/client'
const WATCH_APPLICATIONS = gql`
subscription WatchApplications($campaignId: uuid!) {
campaign_application(
where: { campaign_id: { _eq: $campaignId } }
) {
id
status
}
}
`
export default function LiveApplications({ campaignId }) {
const { data } = useSubscription(WATCH_APPLICATIONS, {
variables: { campaignId },
})
// 실시간 업데이트!
return <div>{data?.campaign_application.length} applications</div>
}
DataLoader가 필요 없는 이유 정리
상황 | 일반 GraphQL | Hasura |
---|---|---|
N+1 문제 | DataLoader 필수 | 자동 해결 (LATERAL JOIN) |
배치 쿼리 | DataLoader로 구현 | 자동 배치 처리 |
복잡한 JOIN | 수동 최적화 | 자동 최적화 |
인덱스 활용 | 쿼리 튜닝 필요 | 자동 활용 |
그럼 사용자가 신경 써야 할 것은?
✅ DB 인덱스 설계 (여전히 중요!)
-- Hasura가 아무리 똑똑해도 인덱스가 없으면 느려요
CREATE INDEX idx_campaign_application_campaign_status
ON campaign_application(campaign_id, status);
CREATE INDEX idx_influencer_platform_influencer
ON influencer_platform(influencer_id);
✅ Hasura Permission 설정
# campaign 테이블 권한 예시
- role: user
permission:
columns: ['id', 'title', 'status']
filter:
business_profile:
user_id:
_eq: X-Hasura-User-Id # JWT에서 가져옴
✅ 쿼리 복잡도 제한
# Hasura 설정
HASURA_GRAPHQL_QUERY_DEPTH_LIMIT: 5 # nested 5단계까지만
✅ Apollo Client 캐시 전략
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
campaign_application: {
keyArgs: ['where', 'order_by'],
merge(existing, incoming) {
return incoming
},
},
},
},
},
})
실무 팁
1. Hasura CLI로 마이그레이션 관리
# Hasura CLI 설치
npm install --save-dev hasura-cli
# 마이그레이션 생성
hasura migrate create init --from-server --database-name default
# 메타데이터 export
hasura metadata export
2. CodeGen으로 타입 안정성
npm install -D @graphql-codegen/cli
# codegen.yml
schema: http://localhost:8080/v1/graphql
documents: './app/**/*.tsx'
generates:
./lib/graphql/generated.ts:
plugins:
- typescript
- typescript-operations
- typescript-react-apollo
// 자동 생성된 타입 사용
import { useGetCampaignQuery } from '@/lib/graphql/generated'
const { data } = useGetCampaignQuery({
variables: { id: campaignId }
})
// data가 완전히 타입 안전!
3. Performance Monitoring
// Apollo Client에 로깅 추가
import { ApolloLink } from '@apollo/client'
const loggerLink = new ApolloLink((operation, forward) => {
console.log(`GraphQL Request: ${operation.operationName}`)
const start = Date.now()
return forward(operation).map(response => {
console.log(`Took ${Date.now() - start}ms`)
return response
})
})
결론
Hasura를 사용하면:
- ✅ DataLoader 불필요 (자동 최적화)
- ✅ 리졸버 작성 불필요 (자동 생성)
- ✅ N+1 문제 자동 해결
- ✅ 하지만 DB 인덱스는 여전히 필수
'TIL' 카테고리의 다른 글
[250929 TIL] tanstack vs apollo 쿼리캐시 비교 (0) | 2025.09.29 |
---|---|
[250929 TIL] (cache)typePolicies, Apollo Link (0) | 2025.09.29 |
[250927 TIL] Hasura(v2) + Next.js + Apollo 기본 (0) | 2025.09.27 |
[250907 TIL] 실전적인 axios client 구성 (0) | 2025.09.07 |
[250827 TIL] Biome tailwind 클래스 자동정렬 (0) | 2025.08.27 |