master-project
CHAPTER 15 / 50
읽기 약 2분
FUNCTION
인증 UI: 로그인/회원가입/소셜 로그인
핵심 개념
Supabase Auth + Server Action — 이메일·Google OAuth·Magic Link·세션 관리·middleware 보호.
본문
로그인 페이지
// src/app/(auth)/login/page.tsx
import { Metadata } from 'next'
import Link from 'next/link'
import { LoginForm } from './login-form'
import { GoogleButton } from './google-button'
export const metadata: Metadata = { title: '로그인' }
export default function LoginPage() {
return (
<div className="container max-w-md py-12">
<h1 className="text-2xl font-bold mb-6">로그인</h1>
<GoogleButton />
<div className="my-6 flex items-center gap-3">
<div className="flex-1 h-px bg-border" />
<span className="text-sm text-muted-foreground">또는</span>
<div className="flex-1 h-px bg-border" />
</div>
<LoginForm />
<p className="mt-6 text-sm text-muted-foreground">
계정이 없나요?{' '}
<Link href="/signup" className="text-primary hover:underline">회원가입</Link>
</p>
</div>
)
}Server Action 로그인
// src/app/(auth)/login/actions.ts
'use server'
import { z } from 'zod'
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
})
export async function loginAction(formData: FormData) {
const parsed = schema.safeParse({
email: formData.get('email'),
password: formData.get('password'),
})
if (!parsed.success) return { error: '입력값을 확인하세요.' }
const supabase = await createClient()
const { error } = await supabase.auth.signInWithPassword(parsed.data)
if (error) return { error: error.message }
redirect('/dashboard')
}Login Form (Client)
// src/app/(auth)/login/login-form.tsx
'use client'
import { useFormState, useFormStatus } from 'react-dom'
import { loginAction } from './actions'
export function LoginForm() {
const [state, formAction] = useFormState(loginAction, null)
return (
<form action={formAction} className="space-y-4">
<div>
<label className="block text-sm mb-1">이메일</label>
<input name="email" type="email" required
className="w-full px-3 py-2 border rounded-md" />
</div>
<div>
<label className="block text-sm mb-1">비밀번호</label>
<input name="password" type="password" required
className="w-full px-3 py-2 border rounded-md" />
</div>
{state?.error && <p className="text-sm text-red-500">{state.error}</p>}
<SubmitBtn />
</form>
)
}
function SubmitBtn() {
const { pending } = useFormStatus()
return (
<button disabled={pending} className="w-full bg-primary text-primary-foreground py-2 rounded-md">
{pending ? '로그인 중...' : '로그인'}
</button>
)
}Google OAuth
// src/app/(auth)/login/google-button.tsx
'use client'
import { createClient } from '@/lib/supabase/client'
export function GoogleButton() {
const supabase = createClient()
const handleClick = async () => {
await supabase.auth.signInWithOAuth({
provider: 'google',
options: { redirectTo: `${location.origin}/auth/callback` },
})
}
return (
<button onClick={handleClick} className="w-full border py-2 rounded-md flex items-center justify-center gap-2">
<GoogleIcon /> Google로 로그인
</button>
)
}OAuth Callback
// src/app/auth/callback/route.ts
import { NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get('code')
if (code) {
const supabase = await createClient()
await supabase.auth.exchangeCodeForSession(code)
}
return NextResponse.redirect(`${origin}/dashboard`)
}Middleware (세션 갱신)
// src/middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
let response = NextResponse.next({ request })
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return request.cookies.getAll() },
setAll(cookies) {
cookies.forEach(({ name, value, options }) => {
response.cookies.set(name, value, options)
})
},
},
}
)
await supabase.auth.getUser() // 세션 갱신
return response
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}다음 챕터
CH.16 "대시보드: 메인 화면 + 통계 카드".
AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 마스터 프로젝트의 인증 UI 구현 부분을 분석해서 실전 적용 + 개선 우선순위 3가지를 알려줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
인증 UI 구현 관련 모범 사례·안티패턴 5개를 비교 분석해서 실전 적용를 위한 추천 방안을 알려줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 프로젝트 전체에서 인증 UI 구현 최적화 가능 위치와 리스크를 보고해줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 한국 1인 개발자 시장의 인증 UI 구현 트렌드와 차별화 포인트를 정리해줘.
⭐ 이것만 기억하세요
인증 UI: 로그인/회원가입/소셜 로그인은 이 3가지만 확실히 잡으세요
1.Supabase Auth + Server Action = 모던 패턴
2.OAuth Callback + middleware로 세션 자동 갱신
3.다음 챕터에서 대시보드 구현
공유하기
진행도 15 / 50