OPEN HYPER STEP
← 목록으로 (master-project)
MASTER-PROJECT · 15 / 50
master-project
CHAPTER 15 / 50
읽기 약 2
FUNCTION

인증 UI: 로그인/회원가입/소셜 로그인


핵심 개념

Supabase Auth + Server Action — 이메일·Google OAuth·Magic Link·세션 관리·middleware 보호.

본문

로그인 페이지

TSX📋 코드 (26줄)
// 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 로그인

TSX📋 코드 (24줄)
// 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)

TSX📋 코드 (33줄)
// 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

TSX📋 코드 (18줄)
// 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

TSX📋 코드 (14줄)
// 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 (세션 갱신)

TS📋 코드 (27줄)
// 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