YepBuddy - (4) 헬스장 데이터를 불러보자! Supabase REST API를 직접 구성해봤다
2025-10-02
왜 직접 API를 구성했는가?
Supabase는 기본적으로 REST Endpoint를 자동으로 만들어 주지만, 이 프로젝트에서는 더 세밀한 제어가 필요했다.
- 헬스장 데이터를 단순히 불러오는 것뿐 아니라 검색(q), 필터(topic), 정렬(desc) 등을 직접 제어하고 싶었다.
- 프론트와 백 간의 데이터 구조를 명확히 통일하기 위해
/api경로에 Route Handler를 두고 REST API를 구현했다.
결국 목표는 Supabase + ORM(Drizzle)을 이용한 Spring-like REST API였다.
구현 과정
api/proteins/route.ts 파일 생성
- Next.js App Router에서는 파일 경로가 곧 API 경로가 된다.
src/app/api/proteins/route.ts처럼 만들면/api/proteins엔드포인트가 생긴다.
route 파일 로직
API는 데이터만 주고받는 통로가 아니다. YepBuddy에서는 Drizzle ORM + Supabase로 Next.js 안에 자체 REST 서버를 구성했다.
서버 런타임 지정
export const runtime = "nodejs";
- App Router는 기본적으로 Edge 런타임에서 실행된다.
- Drizzle ORM은 Node.js 환경 전용이므로, 이 한 줄을 넣어야 DB 커넥션이 정상 동작한다.
- 빼면
"params.id must be awaited"같은 런타임 에러가 난다.
필수 import
import { NextResponse } from "next/server";
import db from "@/app/db";
import { sql } from "drizzle-orm";
- NextResponse: Next.js에서 서버 응답을 만드는 객체
- db: Drizzle로 연결된 Supabase Postgres 인스턴스
- sql: SQL 쿼리를 실행하는 tagged template literal
- ORM(Drizzle)이 DB 접근을, NextResponse가 응답 객체를 담당한다.
(ORM = Object–Relational Mapping, 관계형 DB와 코드 사이를 이어 주는 매개체)
GET 요청
export async function GET() {
const result = await db.execute(sql`
SELECT DISTINCT ON (protein_id)
protein_id,
observed_date,
price,
available,
sale
FROM protein_prices_daily
ORDER BY protein_id, observed_date DESC
`);
const items = Array.isArray((result as any)?.rows) ? (result as any).rows : (result as any);
return NextResponse.json({ ok: true, data: { items } });
}
db.execute(sql\...`)`: Drizzle로 SQL을 직접 실행한다.DISTINCT ON (protein_id): 각protein_id당 가장 최신 행 한 건만 가져온다.ORDER BY protein_id, observed_date DESC: protein_id별로 최신 날짜 순이 되도록 정렬해, 각 단백질의 최근 가격만 사용한다.- 응답은
NextResponse.json()형태로 통일한다.
fetch → React Query
프론트에서는 fetch 후 React Query로 사용한다. (구현은 생략)
결과
- Supabase 자동 API보다 쿼리 제어가 훨씬 유연해졌다.
- 프론트엔드에서 DB 구조를 그대로 재활용할 수 있었다.
- Spring 하듯 React(Next) 안에서 백엔드를 설계하는 구조를 만들 수 있었다.