์ฌํ ๋ฆฌ๋ทฐ ์ฌ์ดํธ Supabase ์ฐ๋ ํธ๋ฌ๋ธ์ํ
๋ฌธ์ ์ํฉ
์ฌํ ๋ฆฌ๋ทฐ ์ฌ์ดํธ ๊ฐ๋ฐ ์ค ๋ค์๊ณผ ๊ฐ์ ๋ฌธ์ ๋ค์ด ๋ฐ์ํ์ต๋๋ค:
- Hydration Mismatch: SSR ํ๊ฒฝ์์ ๋ฌ๋ ฅ ๋ก์บ ๋ถ์ผ์น
- ์ฌํ ์ถ๊ฐ ์ user_id ๋๋ฝ: ์๋ก๊ณ ์นจ ํ ๋ฐ์ดํฐ ์ฌ๋ผ์ง
- ๋ฆฌ๋ทฐ ์ ์ฅ ์คํจ: RLS ์ ์ฑ ์๋ฐ ์ค๋ฅ
- ํ๋กํ ์์ฑ ์คํจ: 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 ํ๊ฒฝ์์ ์๋ํ์ง ์์
ํด๊ฒฐ
- ํธ๋ฆฌ๊ฑฐ ํจ์ ์์
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;
- ํด๋ผ์ด์ธํธ์์ ๋ช ์์ 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 ์ด๊ธฐ ๋ฐ์ดํฐ์ ํด๋ผ์ด์ธํธ ์ํธ์์ฉ ๋ถ๋ฆฌ ํ์