๐Ÿ‹๏ธ ์˜จํ•(on-fit) ๋ฒˆ๊ฐœ ๋ชจ์ž„ ๋“ฑ๋ก ๊ธฐ๋Šฅ ์™„์„ฑ (Next.js + Supabase + Axios)

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

์˜จํ•

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

์˜ค๋Š˜์€ ์˜จํ• ์„œ๋น„์Šค์—์„œ

์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ โ€œ๋ชจ์ž„์„ ๋“ฑ๋กโ€ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์™„์„ฑํ–ˆ๋‹ค.

ํ”„๋ก ํŠธ โ†’ ๋ฐฑ์—”๋“œ โ†’ DB๊นŒ์ง€ ํ๋ฆ„์„ ์ง์ ‘ ์—ฐ๊ฒฐํ•˜๋ฉด์„œ,

Next.js์˜ Route Handler์™€ Supabase, ๊ทธ๋ฆฌ๊ณ  axios์˜ ์—ญํ• ์„ ํ™•์‹คํžˆ ์ดํ•ดํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.


๐ŸŽฏ ๋ชฉํ‘œ

  • ๋ฒˆ๊ฐœ ๋ชจ์ž„ ์ž‘์„ฑ ํผ ๊ตฌํ˜„
  • axios๋กœ ์„œ๋ฒ„(/api/post) ์š”์ฒญ
  • ์„œ๋ฒ„์—์„œ Supabase๋ฅผ ํ†ตํ•ด DB์— ๋ฐ์ดํ„ฐ ์ €์žฅ
  • ํผ ์ดˆ๊ธฐํ™” ๋ฐ ์—๋Ÿฌ ์ฒ˜๋ฆฌ

๐Ÿงฉ 1. ์ „์ฒด ๊ตฌ์กฐ

์˜ค๋Š˜ ๋งŒ๋“  ๊ตฌ์กฐ๋Š” Next.js App Router ๊ธฐ๋ฐ˜์˜ ์™„์ „ํ•œ ํ’€์Šคํƒ ํ๋ฆ„์ด๋‹ค.

๐Ÿ“ฆ on-fit
 โ”ฃ ๐Ÿ“‚ app
 โ”ƒ โ”ฃ ๐Ÿ“‚ post
 โ”ƒ โ”ƒ โ”ฃ ๐Ÿ“‚ create
 โ”ƒ โ”ƒ โ”ƒ โ”— ๐Ÿ“œ page.tsx        โ† ํด๋ผ์ด์–ธํŠธ ํผ (axios ์š”์ฒญ)
 โ”ƒ โ”— ๐Ÿ“‚ api
 โ”ƒ   โ”— ๐Ÿ“‚ post
 โ”ƒ     โ”— ๐Ÿ“œ route.ts         โ† ์„œ๋ฒ„ Route Handler (Supabase DB ์ €์žฅ)
 โ”ฃ ๐Ÿ“‚ lib
 โ”ƒ โ”ฃ ๐Ÿ“œ axios.ts             โ† axios ๊ณตํ†ต ์ธ์Šคํ„ด์Šค
 โ”ƒ โ”— ๐Ÿ“œ supabase-admin.ts    โ† ์„œ๋ฒ„์šฉ Supabase ํด๋ผ์ด์–ธํŠธ
 โ”— ๐Ÿ“‚ components/common       โ† Input, DropBox, Button ๋“ฑ UI ์ปดํฌ๋„ŒํŠธ

๐Ÿงฑ 2. ํ”„๋ก ํŠธ์—”๋“œ (ํผ ํŽ˜์ด์ง€)

์‚ฌ์šฉ์ž๊ฐ€ ์ž‘์„ฑํ•˜๋Š” ํŽ˜์ด์ง€ /post/create

์ด ํผ์˜ ํ•ต์‹ฌ์€ FormData โ†’ axios.post() ์กฐํ•ฉ์ด๋‹ค.

'use client'
import { useState } from 'react'
import { Card, CardHeader, CardContent } from '@/components/common/Card'
import { Input } from '@/components/common/Input'
import { TextArea } from '@/components/common/TextArea'
import DropBox from '@/components/common/DropBox'
import { Button } from '@/components/common/Button'
import { api } from '@/lib/axios'

const sportOption = ['๋ฐฐ๋“œ๋ฏผํ„ด', '์ถ•๊ตฌ', '์•ผ๊ตฌ']
const levelOption = ['๋ธŒ๋ก ์ฆˆ', '์‹ค๋ฒ„', '๊ณจ๋“œ']

export default function Page() {
  const [sport, setSport] = useState(sportOption[0])
  const [level, setLevel] = useState(levelOption[0])
  const [loading, setLoading] = useState(false)

  async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    const fd = new FormData(e.currentTarget)

    const payload = {
      sport: fd.get('sport'),
      title: fd.get('title'),
      description: fd.get('description') || '',
      location: fd.get('location'),
      date: fd.get('date'),
      time: fd.get('time'),
      level: fd.get('level'),
      maxParticipants: Number(fd.get('maxParticipants') || 0),
      currentParticipants: 1,
      status: '๋ชจ์ง‘์ค‘',
      author: 'ํ…Œ์ŠคํŠธ์œ ์ €',
      equipment: fd.get('equipment') || '',
      fee: fd.get('fee') || '',
    }

    try {
      setLoading(true)
      const res = await api.post('/api/post', payload)
      console.log('์„ฑ๊ณต:', res.data.item)
      alert('๋ชจ์ž„์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!')
      e.currentTarget.reset() // ํผ ์ดˆ๊ธฐํ™”
    } catch (err: any) {
      console.log('SERVER ERROR:', err.response?.data)
      alert(`์ƒ์„ฑ ์‹คํŒจ: ${err.response?.data?.error ?? err.message}`)
    } finally {
      setLoading(false)
    }
  }

  return (
    <main className="container mx-auto px-4 py-8 max-w-2xl">
      <Card>
        <CardHeader>
          <h3 className="text-2xl font-semibold">๋ฒˆ๊ฐœ ๋ชจ์ž„ ๋งŒ๋“ค๊ธฐ</h3>
          <p className="text-sm text-muted-foreground">ํ•จ๊ป˜ ์šด๋™ํ•  ์‚ฌ๋žŒ๋“ค์„ ๋ชจ์ง‘ํ•ด๋ณด์„ธ์š”!</p>
        </CardHeader>

        <CardContent>
          <form onSubmit={onSubmit} className="space-y-6">
            <DropBox name="sport" value={sport} onChange={setSport} options={sportOption} />
            <Input name="title" placeholder="์˜ˆ: ๊ฐ•๋‚จ ๋ฐฐ๋“œ๋ฏผํ„ด ์ดˆ๊ธ‰์ž ๋ชจ์ง‘!" required />
            <TextArea name="description" placeholder="๋ชจ์ž„์— ๋Œ€ํ•ด ๊ฐ„๋‹จํžˆ ์†Œ๊ฐœํ•ด์ฃผ์„ธ์š”" />
            <Input name="location" placeholder="์˜ˆ: ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ ์ฒด์œก๊ด€" required />
            <Input name="date" type="date" required />
            <Input name="time" type="time" required />
            <Input name="maxParticipants" type="number" placeholder="์˜ˆ: 6" required />
            <DropBox name="level" value={level} onChange={setLevel} options={levelOption} />
            <Input name="equipment" placeholder="์˜ˆ: ๋ผ์ผ“(๋Œ€์—ฌ ๊ฐ€๋Šฅ), ์šด๋™ํ™”" />
            <Input name="fee" placeholder="์˜ˆ: 15,000์› (์‹œ์„ค๋น„ ํฌํ•จ)" />
            <div className="flex gap-3 pt-4">
              <Button type="button" variant="outline" fullWidth>์ทจ์†Œ</Button>
              <Button type="submit" variant="hero" fullWidth disabled={loading}>
                {loading ? '๋“ฑ๋ก ์ค‘โ€ฆ' : '๋ชจ์ž„ ๋งŒ๋“ค๊ธฐ'}
              </Button>
            </div>
          </form>
        </CardContent>
      </Card>
    </main>
  )
}

์ด ์ฝ”๋“œ์—์„œ๋Š” ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ์ˆ˜์ง‘ํ•ด์„œ axios๋กœ ์„œ๋ฒ„์— POST ์š”์ฒญ์„ ๋ณด๋‚ธ๋‹ค.

FormData๋กœ key/value๋ฅผ ์ถ”์ถœํ•ด์„œ payload๋กœ ์ „์†กํ•˜๋Š” ๊ตฌ์กฐ๋‹ค.


โš™๏ธ 3. ๋ฐฑ์—”๋“œ (Next.js Route Handler)

์ด์ œ /app/api/post/route.ts ์—์„œ ์„œ๋ฒ„๊ฐ€ Supabase DB์— ๋ฐ์ดํ„ฐ ์‚ฝ์ž…์„ ๋‹ด๋‹นํ•œ๋‹ค.

import { sbAdmin } from '@/lib/supabase-admin'
import { NextResponse } from 'next/server'

export async function POST(req: Request) {
  try {
    const body = await req.json()

    const { data, error } = await sbAdmin
      .from('fits')
      .insert({
        ...body,
        created_at: new Date().toISOString(),
      })
      .select()
      .single()

    if (error) throw error
    return NextResponse.json({ ok: true, item: data })
  } catch (err: any) {
    console.error(err)
    return NextResponse.json(
      { ok: false, error: err.message ?? '์„œ๋ฒ„ ์˜ค๋ฅ˜' },
      { status: 500 }
    )
  }
}

์—ฌ๊ธฐ์„œ sbAdmin์€ ์„œ๋ฒ„ ์ „์šฉ Supabase ํด๋ผ์ด์–ธํŠธ๋กœ,

Service Role Key๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” ์ ˆ๋Œ€ ์“ธ ์ˆ˜ ์—†๋‹ค.


๐Ÿงฐ 4. lib/axios.ts โ€” ๊ณตํ†ต axios ์ธ์Šคํ„ด์Šค

axios๋ฅผ ํ”„๋กœ์ ํŠธ ์ „์—ญ์—์„œ ํ†ต์ผ๋œ ํ˜•ํƒœ๋กœ ์“ฐ๊ธฐ ์œ„ํ•ด ์•„๋ž˜์ฒ˜๋Ÿผ ์„ค์ •ํ–ˆ๋‹ค.

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)
  }
)

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์—๋Ÿฌ ๋กœ๊ทธ๋‚˜ ํ† ํฐ ์ฃผ์ž…, ๊ณตํ†ต ํ—ค๋” ์„ค์ • ๋“ฑ์„ ์‰ฝ๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.


๐Ÿ”„ 5. ๋ฐ์ดํ„ฐ ํ๋ฆ„ (๋‘ ๋‹จ๊ณ„ ํ†ต์‹  ๊ตฌ์กฐ)

์˜ค๋Š˜ ๊ตฌํ˜„ํ•œ ํ๋ฆ„์€ ์ด๋ ‡๊ฒŒ ๋‘ ๋ฒˆ์˜ ํ†ต์‹ ์œผ๋กœ ์ด๋ค„์ง„๋‹ค.

(1) ํด๋ผ์ด์–ธํŠธ โ†’ axios โ†’ /api/post
(2) ์„œ๋ฒ„(Route Handler) โ†’ sbAdmin โ†’ Supabase(PostgreSQL)

๐Ÿ“ 1๋‹จ๊ณ„: ํด๋ผ์ด์–ธํŠธ โ†’ ์„œ๋ฒ„

  • ์‚ฌ์šฉ์ž๊ฐ€ ํผ ์ž‘์„ฑ ํ›„ ์ œ์ถœ
  • axios.post('/api/post') ์‹คํ–‰
  • Next.js ์„œ๋ฒ„(Route Handler)๋กœ ๋ฐ์ดํ„ฐ ์ „์†ก

๐Ÿ“ 2๋‹จ๊ณ„: ์„œ๋ฒ„ โ†’ Supabase

  • ์„œ๋ฒ„(route.ts)๊ฐ€ ์š”์ฒญ ๋ณธ๋ฌธ์„ ๋ฐ›๊ณ 
  • sbAdmin.from('fits').insert() ๋กœ Supabase DB์— ์ €์žฅ
  • ์„ฑ๊ณต ์‹œ { ok: true, item: {...} } ์‘๋‹ต

๐Ÿ”’ ์™œ ๋‘ ๋‹จ๊ณ„๋ฅผ ๊ฑฐ์น˜๋‚˜?

์ด์œ ์„ค๋ช…
๋ณด์•ˆService Role Key๋ฅผ ๋ธŒ๋ผ์šฐ์ €์— ๋…ธ์ถœํ•˜๋ฉด ์•ˆ ๋จ
๊ฒ€์ฆ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ฐ ํ•„ํ„ฐ๋ง ๊ฐ€๋Šฅ
ํ™•์žฅ์„ฑ๋‚˜์ค‘์— ์•Œ๋ฆผ, ์บ์‹œ, ํŒŒ์ผ ์—…๋กœ๋“œ ๋“ฑ ์‰ฝ๊ฒŒ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ

์ฆ‰, ์ด ๊ตฌ์กฐ๋Š” ์•ˆ์ „ํ•˜๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ํ‘œ์ค€์ ์ธ Next.js ๋ฐฑ์—”๋“œ ํŒจํ„ด์ด๋‹ค.


๐Ÿง  6. Supabase SDK๋งŒ ์ผ์„ ๋•Œ์™€ ๋น„๊ต

๋ฐฉ์‹์žฅ์ ๋‹จ์ 
Supabase SDK๋งŒ ์‚ฌ์šฉ๋น ๋ฅด๊ณ  ๊ฐ„๋‹จํ•จService Role ๋ชป ์”€, ๋กœ์ง ์ถ”๊ฐ€ ์–ด๋ ค์›€
axios โ†’ route.ts โ†’ sbAdmin๋ณด์•ˆ/๊ฒ€์ฆ/ํ™•์žฅ์„ฑ ํƒ์›”๊ตฌ์กฐ ํ•œ ๋‹จ๊ณ„ ๋” ๋ณต์žก

โœ… 7. ์˜ค๋Š˜์˜ ๊ฒฐ๊ณผ

โœ”๏ธ /post/create ํŽ˜์ด์ง€์—์„œ ํผ ์ž‘์„ฑ ํ›„ โ€œ๋ชจ์ž„ ๋งŒ๋“ค๊ธฐโ€ ํด๋ฆญ ์‹œ

Supabase DB์— ์ƒˆ๋กœ์šด ๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ

โœ”๏ธ axios โ†’ Route โ†’ Supabase ํ๋ฆ„ ์™„์ „ ์—ฐ๊ฒฐ

โœ”๏ธ ๋กœ๋”ฉ ์ƒํƒœ, ํผ ์ดˆ๊ธฐํ™”, ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๊ตฌํ˜„ ์™„๋ฃŒ


๐Ÿงญ ์˜ค๋Š˜ ๋ฐฐ์šด ํ•ต์‹ฌ ์š”์•ฝ

๊ฐœ๋…์„ค๋ช…
axiosํด๋ผ์ด์–ธํŠธ โ†’ Next.js Route ํ†ต์‹ ์šฉ HTTP ํด๋ผ์ด์–ธํŠธ
Supabase sbAdmin์„œ๋ฒ„(Route) โ†’ PostgreSQL ์—ฐ๊ฒฐ์šฉ SDK
๋‘ ๋‹จ๊ณ„ ํ†ต์‹ (1) axios ์š”์ฒญ โ†’ (2) sbAdmin DB ์š”์ฒญ
๋ณด์•ˆ ๊ตฌ์กฐ๋ธŒ๋ผ์šฐ์ €๋Š” anon key, ์„œ๋ฒ„๋Š” service role key ์‚ฌ์šฉ
FormData<form>์—์„œ key/value ์ถ”์ถœํ•˜๋Š” ๊ธฐ๋ณธ Web API

โœ๏ธ ๋งˆ๋ฌด๋ฆฌ

์˜ค๋Š˜์€ ์ฒ˜์Œ์œผ๋กœ Next.js ๋‚ด๋ถ€ API(Route Handler) ์™€

Supabase(PostgreSQL) ๋ฅผ ์ง์ ‘ ์—ฐ๊ฒฐํ•ด๋ดค๋‹ค.

์ฒ˜์Œ์—” axios์™€ Supabase์˜ ์—ญํ• ์ด ๊ฒน์ณ๋ณด์˜€์ง€๋งŒ,

์ง€๊ธˆ์€ ์™„์ „ํžˆ ์ดํ•ด๋๋‹ค ๐Ÿ‘‡

axios๋Š” โ€œ๋‚ด ์•ฑ ์„œ๋ฒ„๊นŒ์ง€โ€,

sbAdmin(Supabase) ์€ โ€œ์„œ๋ฒ„์—์„œ DB๊นŒ์ง€โ€.

์ด์ œ ์ด ๊ตฌ์กฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ

๋‹ค์Œ ๋‹จ๊ณ„ โ€” ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก/์ƒ์„ธ ์กฐํšŒ ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค ์˜ˆ์ •์ด๋‹ค.

profile
์ฝ”๋ฆฐ์ด

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