Y J S

์—ฌํ–‰ ๋ฆฌ๋ทฐ ์‚ฌ์ดํŠธ Supabase ์—ฐ๋™ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

๋ฌธ์ œ ์ƒํ™ฉ

์—ฌํ–‰ ๋ฆฌ๋ทฐ ์‚ฌ์ดํŠธ ๊ฐœ๋ฐœ ์ค‘ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฌธ์ œ๋“ค์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค:

  1. Hydration Mismatch: SSR ํ™˜๊ฒฝ์—์„œ ๋‹ฌ๋ ฅ ๋กœ์บ˜ ๋ถˆ์ผ์น˜
  2. ์—ฌํ–‰ ์ถ”๊ฐ€ ์‹œ user_id ๋ˆ„๋ฝ: ์ƒˆ๋กœ๊ณ ์นจ ํ›„ ๋ฐ์ดํ„ฐ ์‚ฌ๋ผ์ง
  3. ๋ฆฌ๋ทฐ ์ €์žฅ ์‹คํŒจ: RLS ์ •์ฑ… ์œ„๋ฐ˜ ์˜ค๋ฅ˜
  4. ํ”„๋กœํ•„ ์ƒ์„ฑ ์‹คํŒจ: user_id NULL ์ œ์•ฝ ์กฐ๊ฑด ์œ„๋ฐ˜

ํ™˜๊ฒฝ

  • ํ”„๋กœ์ ํŠธ: ์—ฌํ–‰ ๋ฆฌ๋ทฐ ์‚ฌ์ดํŠธ
  • ์Šคํƒ: Next.js 14, TypeScript, Supabase
  • ์ฃผ์š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ: react-calendar, @supabase/ssr

๋ฌธ์ œ 1: Hydration Mismatch (๋‹ฌ๋ ฅ ๋กœ์บ˜)

์ฆ์ƒ

Hydration failed because the server rendered HTML didn't match the client.
Server: "October 2025", Client: "2025๋…„ 10์›”"

์›์ธ

  • SSR ํ™˜๊ฒฝ์—์„œ ์„œ๋ฒ„(en-US)์™€ ํด๋ผ์ด์–ธํŠธ(ko-KR) ๋กœ์บ˜ ๋ถˆ์ผ์น˜
  • react-calendar๊ฐ€ ์„œ๋ฒ„/ํด๋ผ์ด์–ธํŠธ์—์„œ ๋‹ค๋ฅด๊ฒŒ ๋ Œ๋”๋ง

ํ•ด๊ฒฐ

"use client";
import dynamic from "next/dynamic";

const Calendar = dynamic(() => import("react-calendar"), {
  ssr: false,
  loading: () => <div>๋‹ฌ๋ ฅ ๋กœ๋”ฉ ์ค‘...</div>,
});

๋ฌธ์ œ 2: ์—ฌํ–‰ ์ถ”๊ฐ€ ์‹œ user_id ๋ˆ„๋ฝ

์ฆ์ƒ

  • ์—ฌํ–‰ ์ถ”๊ฐ€ ํ›„ ์ƒˆ๋กœ๊ณ ์นจ ์‹œ ๋ชฉ๋ก์—์„œ ์‚ฌ๋ผ์ง
  • trip ํ…Œ์ด๋ธ”์— user_id๊ฐ€ NULL๋กœ ์ €์žฅ๋จ

์›์ธ

  • ํด๋ผ์ด์–ธํŠธ์—์„œ user_id ๋ฏธ์ง€์ •
  • auth.uid() ํŠธ๋ฆฌ๊ฑฐ๊ฐ€ SSR ํ™˜๊ฒฝ์—์„œ ์ž‘๋™ํ•˜์ง€ ์•Š์Œ

ํ•ด๊ฒฐ

  1. ํŠธ๋ฆฌ๊ฑฐ ํ•จ์ˆ˜ ์ˆ˜์ •
CREATE OR REPLACE FUNCTION public.set_trip_user_id()
RETURNS trigger AS $$
BEGIN
  IF NEW.user_id IS NULL AND auth.uid() IS NOT NULL THEN
    NEW.user_id := auth.uid();
  END IF;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
  1. ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ช…์‹œ์  user_id ์ „๋‹ฌ
const { data: user } = await supabase.auth.getUser();

await supabase.from("trip").insert({
  title,
  start_date,
  end_date,
  description,
  user_id: user?.data?.user?.id, // fallback
});

๋ฌธ์ œ 3: SSR ์ธ์ฆ ์ปจํ…์ŠคํŠธ ๋ถˆ์ผ์น˜

์ฆ์ƒ

  • ์„œ๋ฒ„์—์„œ auth.getUser()๊ฐ€ null ๋ฐ˜ํ™˜
  • ํด๋ผ์ด์–ธํŠธ ์„ธ์…˜์ด ์„œ๋ฒ„๋กœ ์ „๋‹ฌ๋˜์ง€ ์•Š์Œ

ํ•ด๊ฒฐ

// app/dashboard/page.tsx
import { cookies } from "next/headers";
import { createServerClient } from "@supabase/ssr";

export default async function DashboardPage() {
  const cookieStore = cookies();

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll();
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) => {
            cookieStore.set(name, value, options);
          });
        },
      },
    }
  );

  const { data: user } = await supabase.auth.getUser();
  // ...
}

๋ฌธ์ œ 4: ๋ฆฌ๋ทฐ ์ €์žฅ ์‹คํŒจ (RLS ์ •์ฑ…)

์ฆ์ƒ

{
  code: '42501',
  message: 'new row violates row-level security policy for table "reviews"'
}

์›์ธ

  • reviews ํ…Œ์ด๋ธ”์˜ INSERT ์ •์ฑ…์ด ์ œ๋Œ€๋กœ ์„ค์ •๋˜์ง€ ์•Š์Œ
  • user_id ๊ฐ’์ด ์ •์ฑ… ์กฐ๊ฑด๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š์Œ

ํ•ด๊ฒฐ

-- ๊ธฐ์กด ์ •์ฑ… ์‚ญ์ œ ํ›„ ์žฌ์ƒ์„ฑ
DROP POLICY IF EXISTS "Reviews: user can insert" ON public.reviews;

CREATE POLICY "Reviews: user can insert"
ON public.reviews
FOR INSERT
WITH CHECK (user_id = auth.uid());

๋ฌธ์ œ 5: ํ”„๋กœํ•„ ์ƒ์„ฑ ์‹คํŒจ

์ฆ์ƒ

ERROR: 23502: null value in column "user_id" violates not-null constraint

์›์ธ

  • profiles ํ…Œ์ด๋ธ”์˜ ํŠธ๋ฆฌ๊ฑฐ๊ฐ€ ์ž‘๋™ํ•˜์ง€ ์•Š์Œ
  • RLS ์ •์ฑ…๊ณผ ํŠธ๋ฆฌ๊ฑฐ ๊ฐ„ ํƒ€์ด๋ฐ ๋ฌธ์ œ

ํ•ด๊ฒฐ

-- 1. ํŠธ๋ฆฌ๊ฑฐ ํ•จ์ˆ˜ ์žฌ์ƒ์„ฑ
CREATE OR REPLACE FUNCTION public.set_profiles_user_id()
RETURNS trigger AS $$
BEGIN
  IF auth.uid() IS NOT NULL THEN
    NEW.user_id := auth.uid();
  END IF;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- 2. ํŠธ๋ฆฌ๊ฑฐ ์žฌ์ƒ์„ฑ
DROP TRIGGER IF EXISTS trg_profiles_set_user_id ON public.profiles;
CREATE TRIGGER trg_profiles_set_user_id
  BEFORE INSERT ON public.profiles
  FOR EACH ROW
  EXECUTE FUNCTION public.set_profiles_user_id();

-- 3. RLS ์ •์ฑ… ์ˆ˜์ •
CREATE POLICY "Profiles: user can insert"
  ON public.profiles
  FOR INSERT
  WITH CHECK (auth.uid() IS NOT NULL);

๋ฌธ์ œ 6: ์™ธ๋ž˜ํ‚ค ๊ด€๊ณ„ ์˜ค๋ฅ˜

์ฆ์ƒ

{
  code: 'PGRST200',
  message: "Could not find a relationship between 'trip' and 'destinations'"
}

์›์ธ

  • destinations ํ…Œ์ด๋ธ”์˜ ์™ธ๋ž˜ํ‚ค ๊ด€๊ณ„๊ฐ€ ๊นจ์ง
  • Supabase ์Šคํ‚ค๋งˆ ์บ์‹œ ๋ฌธ์ œ

ํ•ด๊ฒฐ

-- destinations ํ…Œ์ด๋ธ” ์žฌ์ƒ์„ฑ
DROP TABLE IF EXISTS public.destinations CASCADE;

CREATE TABLE public.destinations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  trip_id UUID NOT NULL REFERENCES public.trip(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  description TEXT,
  day INT CHECK (day > 0),
  order_num INT,
  created_at TIMESTAMPTZ DEFAULT now()
);

๋ฌธ์ œ 7: ์ƒํƒœ ๋™๊ธฐํ™” ๋ฌธ์ œ

์ฆ์ƒ

  • ์ƒˆ ์—ฌํ–‰ ์ถ”๊ฐ€ ํ›„ ๋‹ฌ๋ ฅ์— ์ฆ‰์‹œ ๋ฐ˜์˜๋˜์ง€ ์•Š์Œ

ํ•ด๊ฒฐ

const handleTravelAdded = (newTrip: Trip) => {
  setTravels((prev) => [...prev, newTrip]);
  // ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ ์ฆ‰์‹œ ๊ฐฑ์‹ 
};

๊ฒ€์ฆ

  • SSR ํ™˜๊ฒฝ์—์„œ ๋‹ฌ๋ ฅ ๋กœ์บ˜ ์ผ์น˜ ํ™•์ธ
  • ์—ฌํ–‰/๋ฆฌ๋ทฐ ์ถ”๊ฐ€ ํ›„ ์ƒˆ๋กœ๊ณ ์นจํ•ด๋„ ๋ฐ์ดํ„ฐ ์œ ์ง€ ํ™•์ธ
  • RLS ์ •์ฑ…์œผ๋กœ ์ธํ•œ ๊ถŒํ•œ ์˜ค๋ฅ˜ ํ•ด๊ฒฐ ํ™•์ธ
  • ํ”„๋กœํ•„ ์ž๋™ ์ƒ์„ฑ ์ •์ƒ ์ž‘๋™ ํ™•์ธ

๋ฐฐ์šด ์ 

  • SSR ํ™˜๊ฒฝ ์ฃผ์˜์‚ฌํ•ญ: ๋กœ์บ˜, ์ธ์ฆ ์ปจํ…์ŠคํŠธ, ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ๋ถˆ์ผ์น˜ ๋ฌธ์ œ๊ฐ€ ๋นˆ๋ฒˆ
  • RLS ์ •์ฑ…๊ณผ ํŠธ๋ฆฌ๊ฑฐ: ํƒ€์ด๋ฐ ๋ฌธ์ œ์™€ ์˜์กด์„ฑ ๊ด€๋ฆฌ๊ฐ€ ์ค‘์š”
  • Supabase ์ธ์ฆ: ์„œ๋ฒ„-ํด๋ผ์ด์–ธํŠธ ๊ฐ„ ์„ธ์…˜ ๋™๊ธฐํ™” ํ•„์š”
  • ์™ธ๋ž˜ํ‚ค ๊ด€๊ณ„: ์Šคํ‚ค๋งˆ ์บ์‹œ ๋ฌธ์ œ ์‹œ ํ…Œ์ด๋ธ” ์žฌ์ƒ์„ฑ ๊ณ ๋ ค
  • ์ƒํƒœ ๊ด€๋ฆฌ: SSR ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ์™€ ํด๋ผ์ด์–ธํŠธ ์ƒํ˜ธ์ž‘์šฉ ๋ถ„๋ฆฌ ํ•„์š”

์ฐธ๊ณ