๐Ÿ‹๏ธโ€โ™€๏ธ ์˜จํ•(on-fit) Supabase + Next.js๋กœ ์šด๋™ ๋ชจ์ž„ ๊ฒŒ์‹œ๊ธ€ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„

์กฐ์ค€ํ˜•ยท2025๋…„ 11์›” 5์ผ

์˜จํ•

๋ชฉ๋ก ๋ณด๊ธฐ
3/16

์˜ค๋Š˜์€ On-Fit ํ”„๋กœ์ ํŠธ์—์„œ

์šด๋™ ๋ฒˆ๊ฐœ(๋ชจ์ž„) ๊ฒŒ์‹œ๊ธ€์„ DB์—์„œ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ–ˆ๋‹ค.

์ด์ „ ๊ธ€์—์„œ๋Š” ๊ฒŒ์‹œ๊ธ€์„ ๋“ฑ๋กํ•˜๋Š” POST API๋ฅผ ๋งŒ๋“ค์—ˆ๊ณ ,

์ด๋ฒˆ์—๋Š” GET ์š”์ฒญ์œผ๋กœ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์™€

๋ฉ”์ธ ํ™”๋ฉด์—์„œ ์นด๋“œ ํ˜•ํƒœ๋กœ ๋ณด์—ฌ์ฃผ๋Š” ์ž‘์—…์„ ํ–ˆ๋‹ค.


1๏ธโƒฃ Supabase ํ…Œ์ด๋ธ”

์šฐ์„  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()
);

2๏ธโƒฃ ์„œ๋ฒ„ ๋ผ์šฐํŠธ(/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 ๊ฒฝ๋กœ๋กœ ์ž๋™ ์—ฐ๊ฒฐ๋œ๋‹ค.


3๏ธโƒฃ Axios๋ฅผ ์„ค์ •

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') ํ•œ ์ค„๋กœ ์„œ๋ฒ„ ์š”์ฒญ์„ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋‹ค.

์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ๋„ ์ธํ„ฐ์…‰ํ„ฐ์—์„œ ํ•œ ๋ฒˆ์— ์žก์•„์ค€๋‹ค.


4๏ธโƒฃ ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ฐ์ดํ„ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ

ํ™ˆ ํ™”๋ฉด(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 ์ปดํฌ๋„ŒํŠธ๋กœ ์นด๋“œ ํ˜•ํƒœ๋กœ ๋ Œ๋”๋งํ–ˆ๋‹ค.


5๏ธโƒฃ useMemo๋กœ ๋ถˆํ•„์š”ํ•œ ๊ณ„์‚ฐ ์ค„์ด๊ธฐ

์ฒ˜์Œ์—” ๋‹จ์ˆœํžˆ items.map()์œผ๋กœ ๋ฐ”๋กœ ๋ Œ๋”๋งํ–ˆ๋Š”๋ฐ,

๋ Œ๋”๋งํ•  ๋•Œ๋งˆ๋‹ค ๋‹ค์‹œ map()์ด ์‹คํ–‰๋˜๋Š” ๊ฒŒ ๋งˆ์Œ์— ๊ฑธ๋ ธ๋‹ค.

๊ทธ๋ž˜์„œ useMemo๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

const fits = useMemo(() => items.map(...), [items])

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด items๊ฐ€ ๋ฐ”๋€Œ์ง€ ์•Š๋Š” ํ•œ

์ด map() ๊ณ„์‚ฐ์€ ๋‹ค์‹œ ์ˆ˜ํ–‰๋˜์ง€ ์•Š๋Š”๋‹ค.

๋ฐ์ดํ„ฐ๊ฐ€ ๋™์ผํ•  ๋•Œ๋Š” ์ด์ „ ๊ฒฐ๊ณผ๋ฅผ ๊ทธ๋Œ€๋กœ ์žฌ์‚ฌ์šฉํ•˜๋ฏ€๋กœ

๋ถˆํ•„์š”ํ•œ ๋ Œ๋”๋ง ๋น„์šฉ์„ ์ค„์ผ ์ˆ˜ ์žˆ์—ˆ๋‹ค.


6๏ธโƒฃ ๊ฒฐ๊ณผ ํ™•์ธ

์ด์ œ ๋ฉ”์ธ ํŽ˜์ด์ง€์— ์ ‘์†ํ•˜๋ฉด

Supabase์— ์ €์žฅ๋œ ์šด๋™ ๋ชจ์ž„ ๊ฒŒ์‹œ๊ธ€์ด ์นด๋“œ๋กœ ๋ Œ๋”๋ง๋œ๋‹ค.

์ฝ˜์†”์—๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ฑ๊ณต ๋กœ๊ทธ๊ฐ€ ์ฐํžŒ๋‹ค.

GET /api/posts 200 OK

๐Ÿงญ ์˜ค๋Š˜์˜ ์ •๋ฆฌ

๊ตฌ๋ถ„์—ญํ• 
/app/api/posts/route.tsSupabase์—์„œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๋Š” ์„œ๋ฒ„ ์—”๋“œํฌ์ธํŠธ
/lib/axios.ts๊ณต์šฉ axios ์ธ์Šคํ„ด์Šค
/app/page.tsxํด๋ผ์ด์–ธํŠธ์—์„œ GET ์š”์ฒญ + UI ๋ Œ๋”๋ง
useMemomap ๊ฒฐ๊ณผ ์บ์‹ฑ, ๋ Œ๋”๋ง ์ตœ์ ํ™”

๐Ÿง  ์˜ค๋Š˜ ๋ฐฐ์šด ์ 

  • route.ts๋Š” Next.js์—์„œ ์ž๋™์œผ๋กœ REST API ์—”๋“œํฌ์ธํŠธ๋กœ ์ธ์‹๋œ๋‹ค.
  • useMemo๋Š” ์„œ๋ฒ„ ์บ์‹œ๊ฐ€ ์•„๋‹ˆ๋ผ ๋ Œ๋”๋ง ์ค‘ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ ์บ์‹œ์ด๋‹ค.
  • Axios ์ธ์Šคํ„ด์Šค๋ฅผ ๋งŒ๋“ค์–ด๋‘๋ฉด API ํ˜ธ์ถœ ๊ตฌ์กฐ๊ฐ€ ํ›จ์”ฌ ๊น”๋”ํ•ด์ง„๋‹ค.
profile
์ฝ”๋ฆฐ์ด

0๊ฐœ์˜ ๋Œ“๊ธ€