
Supabase 이메일 인증 구현 - 11편. 회원가입과 비밀번호 찾기
Supabase 이메일 인증으로 안전한 회원가입 시스템을 구축합니다.
이 글에서는 이메일 템플릿, 인증 콜백, 재발송 기능, 비밀번호 찾기를 다룹니다.
📧 가짜 이메일 문제
회원가입을 만들고 테스트하다가 문제를 발견했습니다.
아무 이메일이나 입력해도 가입이 됩니다. asdf@asdf.com 같은 존재하지 않는 이메일도요.
이러면 문제가 생깁니다.
- 견적서를 보내도 안 도착함
- 비밀번호 찾기가 안 됨
- 스팸 계정이 늘어남
Claude에게 물었습니다.
이메일 인증을 추가해야 할 것 같아.
Supabase에 이메일 인증이 내장되어 있다고 했습니다.
🔧 이메일 인증 활성화
Supabase 대시보드에서 설정합니다.
Authentication → Providers → Email → "Confirm email" → ON
이제 회원가입하면 인증 메일이 발송됩니다.
이메일 템플릿 수정
기본 템플릿이 영어입니다. 한국어로 바꿨습니다.
Authentication → Email Templates → Confirm signup
<h2>회원가입 인증</h2>
<p>안녕하세요! 아래 버튼을 클릭하여 이메일을 인증해주세요.</p>
<p>
<a href="{{ .ConfirmationURL }}"
style="background: #2563eb; color: white; padding: 12px 24px;
border-radius: 8px; text-decoration: none;">
이메일 인증하기
</a>
</p>
<p>이 링크는 24시간 동안 유효합니다.</p>
예쁜 버튼도 만들었습니다.
🔄 인증 콜백 처리
사용자가 이메일 링크를 클릭하면 어디로 가야 할까요?
Supabase가 특정 URL로 리다이렉트합니다. 그 URL을 처리하는 코드가 필요합니다.
Redirect URL 설정
Supabase 대시보드 → Authentication → URL Configuration
Site URL: https://your-domain.com
Redirect URLs:
- https://your-domain.com/auth/callback
- http://localhost:3000/auth/callback (개발용)
콜백 라우트 생성
// app/auth/callback/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
const requestUrl = new URL(request.url)
const code = requestUrl.searchParams.get('code')
const next = requestUrl.searchParams.get('next') ?? '/mypage'
if (code) {
const supabase = await createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
return NextResponse.redirect(new URL(next, requestUrl.origin))
}
}
return NextResponse.redirect(
new URL('/auth/error?message=인증에 실패했습니다', requestUrl.origin)
)
}
이메일 링크의 code를 세션으로 교환합니다. 성공하면 마이페이지로, 실패하면 에러 페이지로.
📝 가입 완료 페이지
회원가입 후 바로 마이페이지로 가면 안 됩니다. 이메일 인증을 안 했으니까요.
안내 페이지를 만들었습니다.
// app/[locale]/(auth)/signup/complete/page.tsx
import { Mail } from 'lucide-react'
export default function SignupCompletePage() {
return (
<div className="text-center py-12">
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Mail className="w-8 h-8 text-blue-600" />
</div>
<h1 className="text-2xl font-bold mb-2">
이메일을 확인해주세요
</h1>
<p className="text-gray-600 mb-6">
입력하신 이메일 주소로 인증 메일을 보냈습니다.<br />
메일함을 확인하고 인증을 완료해주세요.
</p>
<div className="bg-gray-50 p-4 rounded-lg text-sm text-gray-500">
<p>이메일이 오지 않았나요?</p>
<ul className="mt-2 space-y-1">
<li>• 스팸 메일함을 확인해주세요</li>
<li>• 1-2분 정도 기다려주세요</li>
<li>• 이메일 주소가 정확한지 확인해주세요</li>
</ul>
</div>
</div>
)
}
🔄 인증 메일 재발송
이메일이 안 올 수도 있습니다. 스팸함에 갔거나, 기다리다 만료됐거나.
재발송 기능을 추가했습니다.
// app/actions/auth.ts
export async function resendVerificationEmail(email: string) {
const supabase = await createClient()
const { error } = await supabase.auth.resend({
type: 'signup',
email,
})
if (error) {
return { error: '메일 발송에 실패했습니다' }
}
return { success: true }
}
'use client'
import { useState } from 'react'
import { resendVerificationEmail } from '@/app/actions/auth'
export function ResendButton({ email }: { email: string }) {
const [isLoading, setIsLoading] = useState(false)
const [message, setMessage] = useState('')
const handleResend = async () => {
setIsLoading(true)
const result = await resendVerificationEmail(email)
if (result.error) {
setMessage(result.error)
} else {
setMessage('인증 메일을 다시 보냈습니다')
}
setIsLoading(false)
}
return (
<div>
<button
onClick={handleResend}
disabled={isLoading}
className="text-blue-600 underline"
>
{isLoading ? '발송 중...' : '인증 메일 재발송'}
</button>
{message && <p className="text-sm mt-2">{message}</p>}
</div>
)
}
🔑 비밀번호 찾기
비밀번호를 잊어버린 사용자도 있습니다.
비밀번호 재설정 요청
// app/actions/auth.ts
export async function requestPasswordReset(email: string) {
const supabase = await createClient()
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`,
})
if (error) {
return { error: '메일 발송에 실패했습니다' }
}
return { success: true }
}
비밀번호 재설정 페이지
// app/auth/reset-password/page.tsx
'use client'
import { createClient } from '@/lib/supabase/client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
export default function ResetPasswordPage() {
const router = useRouter()
const supabase = createClient()
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const { error } = await supabase.auth.updateUser({ password })
if (error) {
setError('비밀번호 변경에 실패했습니다')
return
}
alert('비밀번호가 변경되었습니다')
router.push('/login')
}
return (
<form onSubmit={handleSubmit}>
<h1>새 비밀번호 설정</h1>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="새 비밀번호 (8자 이상)"
minLength={8}
/>
{error && <p className="text-red-500">{error}</p>}
<button type="submit">비밀번호 변경</button>
</form>
)
}
이메일 링크를 클릭하면 이 페이지로 옵니다. 새 비밀번호를 입력하면 끝.
⚠️ 삽질했던 것들
인증 메일이 안 옴
Supabase 무료 플랜은 시간당 4통 제한이 있습니다. 테스트하다가 다 써버렸습니다.
개발 중에는 "Confirm email"을 끄고, 배포할 때 커스텀 SMTP(Resend)를 연결하면 됩니다.
인증 링크 클릭 시 에러
Redirect URL이 등록 안 되어 있었습니다.
Supabase → Authentication → URL Configuration에서 localhost와 배포 URL 모두 등록해야 합니다.
이미 가입된 이메일
같은 이메일로 다시 가입하려 하면 에러가 납니다.
if (error?.message.includes('already registered')) {
return { error: '이미 가입된 이메일입니다' }
}
에러 메시지를 한국어로 바꿔줬습니다.
💡 UX 개선
Claude가 제안한 것들입니다.
로딩 상태 표시
<button disabled={isSubmitting}>
{isSubmitting ? (
<span className="flex items-center gap-2">
<Loader2 className="animate-spin" />
처리 중...
</span>
) : (
'회원가입'
)}
</button>
비밀번호 강도 표시
function PasswordStrength({ password }: { password: string }) {
const strength = calculateStrength(password)
return (
<div className="h-1 bg-gray-200 rounded">
<div
className={cn(
'h-full rounded transition-all',
strength < 2 && 'bg-red-500 w-1/4',
strength === 2 && 'bg-yellow-500 w-2/4',
strength === 3 && 'bg-green-400 w-3/4',
strength >= 4 && 'bg-green-600 w-full'
)}
/>
</div>
)
}
비밀번호를 입력하면 강도가 시각적으로 표시됩니다.
📋 이번 편 요약
| 항목 | 내용 |
|---|---|
| 다룬 기능 | 이메일 인증, 재발송, 비밀번호 찾기 |
| 핵심 파일 | app/auth/callback/route.ts, Email Templates |
| Supabase 설정 | Confirm email ON, Redirect URLs |
| UX 개선 | 로딩 상태, 비밀번호 강도 표시 |
다음 편에서는 미들웨어로 페이지를 보호합니다.
💬 이메일 인증 관련 질문 있으면 댓글로 남겨주세요!
'바이브코딩' 카테고리의 다른 글
| 13. 고객과 관리자 권한 분리 - Supabase RBAC 역할 관리 (0) | 2026.01.20 |
|---|---|
| 12. 인증과 권한 체크 구현 - Next.js 미들웨어 페이지 보호 (0) | 2026.01.19 |
| 10. 회원가입부터 로그아웃까지 - Supabase Auth 로그인 구현 (0) | 2026.01.17 |
| 16. 제품 선택부터 이메일 발송까지 - React Hook Form 문의 폼 구현 (0) | 2026.01.17 |
| 09. 한국어/일본어 지원-next-intl 다국어 설정 (0) | 2026.01.16 |