์ค๋์ On-Fit ํ๋ก์ ํธ์์
์ด๋ ๋ฒ๊ฐ(๋ชจ์) ๊ฒ์๊ธ์ DB์์ ๋ถ๋ฌ์ค๋ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ค.
์ด์ ๊ธ์์๋ ๊ฒ์๊ธ์ ๋ฑ๋กํ๋ POST API๋ฅผ ๋ง๋ค์๊ณ ,
์ด๋ฒ์๋ GET ์์ฒญ์ผ๋ก ๋ชฉ๋ก์ ๊ฐ์ ธ์
๋ฉ์ธ ํ๋ฉด์์ ์นด๋ ํํ๋ก ๋ณด์ฌ์ฃผ๋ ์์ ์ ํ๋ค.
์ฐ์ Supabase Dashboard์์
์ด๋ ๋ชจ์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ํ ์ด๋ธ์ ์๋ก ๋ง๋ค์๋ค.
create table public.fits (
id uuid primary key default gen_random_uuid(),
sport text not null,
title text not null,
description text,
location text not null,
date_time timestamptz not null,
level text not null,
max_participants int not null,
current_participants int default 1,
status text default '๋ชจ์ง์ค',
author text,
equipment text,
fee text,
inserted_at timestamptz default now()
);
/api/posts)Next.js์ Route Handlers๋ฅผ ์ด์ฉํด์
์๋ฒ์์ Supabase ๋ฐ์ดํฐ๋ฅผ ์ง์ ์กฐํํ๋ API๋ฅผ ๋ง๋ค์๋ค.
// app/api/posts/route.ts
import { NextResponse } from 'next/server'
import { sbAdmin } from '@/lib/supabase-admin'
export async function GET() {
try {
const { data, error } = await sbAdmin
.from('fit')
.select('*')
.order('inserted_at', { ascending: false })
if (error) throw error
return NextResponse.json({ ok: true, items: data })
} catch (err: any) {
console.error('SERVER ERROR:', err)
return NextResponse.json({ ok: false, error: err.message }, { status: 500 })
}
}
์ด ์ฝ๋๋ก /api/posts์ GET ์์ฒญ์ ๋ณด๋ด๋ฉด
DB์์ ๋ชจ๋ ์ด๋ ๋ชจ์ ๋ฐ์ดํฐ๋ฅผ JSON์ผ๋ก ๋ฐ์ ์ ์๋ค.
Next.js์ route.ts ํ์ผ์
ํ์ผ๋ช ์ด ๊ณง ์๋ํฌ์ธํธ๊ฐ ๋๋ ๊ตฌ์กฐ๋ผ์
/api/posts ๊ฒฝ๋ก๋ก ์๋ ์ฐ๊ฒฐ๋๋ค.
Axios๋ฅผ ๋งค๋ฒ ์ง์ importํ๋ ๋์ ,
๊ณต์ฉ ์ธ์คํด์ค๋ฅผ ๋ง๋ค์ด๋์๋ค.
// lib/axios.ts
import axios from 'axios'
export const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || '',
headers: { 'Content-Type': 'application/json' },
})
api.interceptors.response.use(
(res) => res,
(err) => {
console.error('API Error:', err.response?.data || err.message)
return Promise.reject(err)
}
)
์ด๋ ๊ฒ ๋ง๋ค์ด๋๋ฉด
api.get('/api/posts') ํ ์ค๋ก ์๋ฒ ์์ฒญ์ ๊ฐ๋จํ๊ฒ ๋ณด๋ผ ์ ์๋ค.
์๋ฌ๊ฐ ๋ฐ์ํ์ ๋๋ ์ธํฐ์ ํฐ์์ ํ ๋ฒ์ ์ก์์ค๋ค.
ํ ํ๋ฉด(page.tsx)์์
useEffect๋ฅผ ์ฌ์ฉํด Supabase ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋๋ก ํ๋ค.
'use client'
import { useEffect, useMemo, useState } from 'react'
import { api } from '@/lib/axios'
import FitCard from '@/components/main/FitCard'
import { toKstDate, toKstTime } from '@/lib/utils'
export default function Home() {
const [items, setItems] = useState([])
useEffect(() => {
const fetchData = async () => {
try {
const res = await api.get('/api/posts')
if (res.data.ok) setItems(res.data.items)
} catch (err) {
console.error('๋ถ๋ฌ์ค๊ธฐ ์คํจ:', err)
}
}
fetchData()
}, [])
// ๋ฐ์ดํฐ ๊ฐ๊ณต
const fits = useMemo(
() =>
items.map((r) => ({
id: r.id,
sport: r.sport,
title: r.title,
location: r.location,
date: toKstDate(r.date_time),
time: toKstTime(r.date_time),
currentParticipants: r.current_participants,
maxParticipants: r.max_participants,
level: r.level,
status: r.status,
author: r.author ?? '์ต๋ช
',
})),
[items]
)
return (
<main className="container mx-auto px-4 py-10">
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3">
{fits.map((fit) => (
<FitCard key={fit.id} {...fit} />
))}
</div>
</main>
)
}
์ด์ /api/posts์์ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์
FitCard ์ปดํฌ๋ํธ๋ก ์นด๋ ํํ๋ก ๋ ๋๋งํ๋ค.
์ฒ์์ ๋จ์ํ items.map()์ผ๋ก ๋ฐ๋ก ๋ ๋๋งํ๋๋ฐ,
๋ ๋๋งํ ๋๋ง๋ค ๋ค์ map()์ด ์คํ๋๋ ๊ฒ ๋ง์์ ๊ฑธ๋ ธ๋ค.
๊ทธ๋์ useMemo๋ฅผ ์ฌ์ฉํ๋ค.
const fits = useMemo(() => items.map(...), [items])
์ด๋ ๊ฒ ํ๋ฉด items๊ฐ ๋ฐ๋์ง ์๋ ํ
์ด map() ๊ณ์ฐ์ ๋ค์ ์ํ๋์ง ์๋๋ค.
๋ฐ์ดํฐ๊ฐ ๋์ผํ ๋๋ ์ด์ ๊ฒฐ๊ณผ๋ฅผ ๊ทธ๋๋ก ์ฌ์ฌ์ฉํ๋ฏ๋ก
๋ถํ์ํ ๋ ๋๋ง ๋น์ฉ์ ์ค์ผ ์ ์์๋ค.
์ด์ ๋ฉ์ธ ํ์ด์ง์ ์ ์ํ๋ฉด
Supabase์ ์ ์ฅ๋ ์ด๋ ๋ชจ์ ๊ฒ์๊ธ์ด ์นด๋๋ก ๋ ๋๋ง๋๋ค.
์ฝ์์๋ ๋ค์๊ณผ ๊ฐ์ด ์ฑ๊ณต ๋ก๊ทธ๊ฐ ์ฐํ๋ค.
GET /api/posts 200 OK
| ๊ตฌ๋ถ | ์ญํ |
|---|---|
/app/api/posts/route.ts | Supabase์์ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๋ ์๋ฒ ์๋ํฌ์ธํธ |
/lib/axios.ts | ๊ณต์ฉ axios ์ธ์คํด์ค |
/app/page.tsx | ํด๋ผ์ด์ธํธ์์ GET ์์ฒญ + UI ๋ ๋๋ง |
useMemo | map ๊ฒฐ๊ณผ ์บ์ฑ, ๋ ๋๋ง ์ต์ ํ |
route.ts๋ Next.js์์ ์๋์ผ๋ก REST API ์๋ํฌ์ธํธ๋ก ์ธ์๋๋ค.useMemo๋ ์๋ฒ ์บ์๊ฐ ์๋๋ผ ๋ ๋๋ง ์ค ๊ณ์ฐ ๊ฒฐ๊ณผ ์บ์์ด๋ค.