Skip to Content
PatternsAuthentication

Authentication

사용자 인증 및 권한 관리 구현 패턴입니다.

개요

Authentication 패턴은 사용자 식별, 인증, 권한 부여를 처리하는 검증된 구현 방법을 제공합니다. JWT 토큰 기반 인증, 소셜 로그인 통합, 권한 기반 라우팅, 세션 관리 등 프로덕션 환경에서 필요한 모든 인증 시나리오를 다룹니다.

사용 사례:

  • 로그인/로그아웃 플로우 구현
  • 보호된 라우트 및 페이지
  • 사용자 세션 관리
  • 역할 기반 접근 제어 (RBAC)
  • 소셜 로그인 (Google, GitHub 등)

사용하지 말아야 할 때:

  • 공개 웹사이트 (인증 불필요)
  • 단순 정적 사이트
  • 백엔드 인증만으로 충분한 경우

기본 패턴

1. 로그인 폼 구현

사용자명과 비밀번호로 로그인하는 기본 패턴입니다.

"use client"; import { useState } from "react"; import { Input, Button, Label, Alert } from "@vortex/ui-foundation"; export default function LoginForm() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(""); setLoading(true); try { const response = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }), }); if (!response.ok) { throw new Error("로그인에 실패했습니다"); } const { token } = await response.json(); localStorage.setItem("authToken", token); window.location.href = "/dashboard"; } catch (err) { setError( err instanceof Error ? err.message : "로그인 중 오류가 발생했습니다" ); } finally { setLoading(false); } }; return ( <form onSubmit={handleSubmit} className="space-y-4 w-full max-w-md"> {error && ( <Alert variant="destructive" role="alert"> {error} </Alert> )} <div className="space-y-2"> <Label htmlFor="email">이메일</Label> <Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="example@email.com" required autoComplete="email" /> </div> <div className="space-y-2"> <Label htmlFor="password">비밀번호</Label> <Input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="••••••••" required autoComplete="current-password" /> </div> <Button type="submit" className="w-full" disabled={loading}> {loading ? "로그인 중..." : "로그인"} </Button> </form> ); }

2. 인증 Context 구현

애플리케이션 전역에서 인증 상태를 관리하는 패턴입니다.

"use client"; import { createContext, useContext, useState, useEffect, ReactNode, } from "react"; interface User { id: string; email: string; name: string; role: string; } interface AuthContextType { user: User | null; login: (email: string, password: string) => Promise<void>; logout: () => void; isAuthenticated: boolean; isLoading: boolean; } const AuthContext = createContext<AuthContextType | undefined>(undefined); export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState<User | null>(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { // 초기 로드 시 토큰으로 사용자 정보 가져오기 const token = localStorage.getItem("authToken"); if (token) { fetchUser(token); } else { setIsLoading(false); } }, []); const fetchUser = async (token: string) => { try { const response = await fetch("/api/auth/me", { headers: { Authorization: `Bearer ${token}` }, }); if (response.ok) { const userData = await response.json(); setUser(userData); } else { localStorage.removeItem("authToken"); } } catch (error) { console.error("Failed to fetch user:", error); } finally { setIsLoading(false); } }; const login = async (email: string, password: string) => { const response = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }), }); if (!response.ok) { throw new Error("로그인에 실패했습니다"); } const { token, user: userData } = await response.json(); localStorage.setItem("authToken", token); setUser(userData); }; const logout = () => { localStorage.removeItem("authToken"); setUser(null); window.location.href = "/login"; }; return ( <AuthContext.Provider value={{ user, login, logout, isAuthenticated: !!user, isLoading, }} > {children} </AuthContext.Provider> ); } export function useAuth() { const context = useContext(AuthContext); if (context === undefined) { throw new Error("useAuth must be used within an AuthProvider"); } return context; }

고급 패턴

3. 보호된 라우트 구현

인증된 사용자만 접근 가능한 페이지를 구현하는 패턴입니다.

"use client"; import { useEffect } from "react"; import { useRouter } from "next/navigation"; import { useAuth } from "@/contexts/AuthContext"; export function ProtectedRoute({ children }: { children: React.ReactNode }) { const { isAuthenticated, isLoading } = useAuth(); const router = useRouter(); useEffect(() => { if (!isLoading && !isAuthenticated) { router.push("/login"); } }, [isAuthenticated, isLoading, router]); if (isLoading) { return <div>로딩 중...</div>; } if (!isAuthenticated) { return null; } return <>{children}</>; } // 사용 예시 export default function DashboardPage() { return ( <ProtectedRoute> <div>보호된 대시보드 콘텐츠</div> </ProtectedRoute> ); }

4. 역할 기반 접근 제어 (RBAC)

사용자 역할에 따라 UI를 조건부로 렌더링하는 패턴입니다.

"use client"; import { useAuth } from "@/contexts/AuthContext"; interface RoleGuardProps { allowedRoles: string[]; children: React.ReactNode; fallback?: React.ReactNode; } export function RoleGuard({ allowedRoles, children, fallback = null, }: RoleGuardProps) { const { user } = useAuth(); if (!user || !allowedRoles.includes(user.role)) { return <>{fallback}</>; } return <>{children}</>; } // 사용 예시 export default function AdminPanel() { return ( <div> <h1>대시보드</h1> <RoleGuard allowedRoles={["admin", "manager"]}> <button>사용자 관리</button> </RoleGuard> <RoleGuard allowedRoles={["admin"]} fallback={<p>관리자 권한이 필요합니다</p>} > <button>시스템 설정</button> </RoleGuard> </div> ); }

5. 소셜 로그인 통합

Google, GitHub 등 OAuth 제공자를 통한 소셜 로그인 패턴입니다.

"use client"; import { Button } from "@vortex/ui-foundation"; export default function SocialLogin() { const handleGoogleLogin = () => { window.location.href = "/api/auth/google"; }; const handleGithubLogin = () => { window.location.href = "/api/auth/github"; }; return ( <div className="space-y-3"> <Button variant="outline" className="w-full" onClick={handleGoogleLogin}> <svg className="w-5 h-5 mr-2" viewBox="0 0 24 24"> {/* Google Icon SVG */} </svg> Google로 계속하기 </Button> <Button variant="outline" className="w-full" onClick={handleGithubLogin}> <svg className="w-5 h-5 mr-2" viewBox="0 0 24 24"> {/* GitHub Icon SVG */} </svg> GitHub으로 계속하기 </Button> </div> ); }

Best Practices

✅ 권장 사항

  1. 보안 토큰 저장

    • 민감한 토큰은 httpOnly 쿠키에 저장 (XSS 방어)
    • localStorage는 XSS 공격에 취약하므로 주의
  2. 토큰 갱신 구현

    • Access Token (짧은 유효기간) + Refresh Token (긴 유효기간)
    • 토큰 만료 시 자동 갱신 로직 구현
  3. 에러 처리

    • 명확한 에러 메시지 제공
    • 401/403 에러 시 로그인 페이지로 리다이렉트
    • 네트워크 오류 재시도 로직
  4. 접근성

    • 스크린 리더용 에러 메시지 (role="alert")
    • 키보드 네비게이션 지원
    • 로딩 상태 명확히 표시
  5. 사용자 경험

    • 로그인 성공 시 이전 페이지로 리다이렉트
    • “로그인 유지” 옵션 제공
    • 비밀번호 재설정 플로우

⚠️ 피해야 할 것

  1. 보안 취약점

    • 비밀번호를 로그에 기록하지 말 것
    • 토큰을 URL에 포함하지 말 것
    • HTTPS 없이 인증 정보 전송하지 말 것
  2. 성능 문제

    • 모든 컴포넌트에서 인증 상태 체크하지 말 것
    • 불필요한 인증 API 호출 최소화
  3. UX 문제

    • 로그인 실패 시 구체적인 오류 정보 노출 (보안 위험)
    • 로딩 상태 없이 리다이렉트

보안 고려사항

XSS 방어

// ❌ 나쁜 예: innerHTML 사용 <div dangerouslySetInnerHTML={{ __html: userData.name }} /> // ✅ 좋은 예: React가 자동으로 escape 처리 <div>{userData.name}</div>

CSRF 방어

// CSRF 토큰을 헤더에 포함 const response = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json", "X-CSRF-Token": getCsrfToken(), }, body: JSON.stringify({ email, password }), });

비밀번호 보안

// ✅ 최소 8자, 대소문자/숫자/특수문자 포함 검증 const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; if (!passwordRegex.test(password)) { setError( "비밀번호는 최소 8자 이상이며, 대소문자, 숫자, 특수문자를 포함해야 합니다" ); return; }

Foundation 예제

범용 로그인 시스템

Foundation 컴포넌트로 구현한 중립적인 로그인 UI입니다.

import { Button, Input, Card, Label } from "@vortex/ui-foundation"; export default function FoundationLogin() { return ( <Card className="w-full max-w-md p-6"> <h2 className="text-2xl font-bold mb-6">로그인</h2> <form className="space-y-4"> <div> <Label htmlFor="email">이메일</Label> <Input id="email" type="email" placeholder="example@email.com" /> </div> <div> <Label htmlFor="password">비밀번호</Label> <Input id="password" type="password" placeholder="••••••••" /> </div> <Button type="submit" className="w-full"> 로그인 </Button> </form> </Card> ); }

전체 예제 보기 →


iCignal 예제

Analytics Dashboard 로그인

iCignal Blue 브랜드를 적용한 데이터 분석 플랫폼 로그인입니다.

import "@vortex/ui-icignal/theme"; import { Button, Input, Card, Alert } from "@vortex/ui-icignal"; export default function ISignalLogin() { return ( <Card className="w-full max-w-md p-6 border-2 border-blue-500"> <div className="flex items-center gap-2 mb-6"> <div className="w-8 h-8 bg-blue-500 rounded" /> <h2 className="text-2xl font-bold">iCignal Analytics</h2> </div> <Alert variant="info" className="mb-4"> 데이터 분석 플랫폼에 오신 것을 환영합니다 </Alert> <form className="space-y-4"> <Input type="email" placeholder="이메일" className="border-blue-200" /> <Input type="password" placeholder="비밀번호" className="border-blue-200" /> <Button variant="primary" className="w-full bg-blue-500 hover:bg-blue-600" > 로그인 </Button> </form> </Card> ); }

전체 예제 보기 →


Cals 예제

예약 시스템 로그인

Cals Pink 브랜드를 적용한 예약 관리 시스템 로그인입니다.

import "@vortex/ui-cals/theme"; import { Button, Input, Card, Badge } from "@vortex/ui-cals"; export default function CalsLogin() { return ( <Card className="w-full max-w-md p-6 border-2 border-pink-500"> <div className="flex items-center justify-between mb-6"> <h2 className="text-2xl font-bold">Cals 예약 시스템</h2> <Badge variant="available">온라인</Badge> </div> <form className="space-y-4"> <div> <label className="text-sm font-medium text-gray-700">이메일</label> <Input type="email" placeholder="example@cals.com" className="border-pink-200 focus:border-pink-500" /> </div> <div> <label className="text-sm font-medium text-gray-700">비밀번호</label> <Input type="password" placeholder="••••••••" className="border-pink-200 focus:border-pink-500" /> </div> <Button variant="primary" className="w-full bg-pink-500 hover:bg-pink-600" > 예약 시스템 로그인 </Button> </form> <p className="text-sm text-center mt-4 text-gray-600"> 예약 관리, 고객 관리, 일정 관리를 한 곳에서 </p> </Card> ); }

전체 예제 보기 →


CodeSandbox

CodeSandbox 예제는 곧 제공될 예정입니다.

로컬에서 실행하기

  1. 프로젝트 생성

    npx @vortex/cli init my-auth-project --template vite-react cd my-auth-project
  2. 컴포넌트 추가

    # Foundation npx @vortex/cli add button input card label alert --package foundation # iCignal npx @vortex/cli add button input card alert --package icignal # Cals npx @vortex/cli add button input card badge --package cals
  3. 코드 복사

    • 위 예제 코드를 src/App.tsx 또는 해당 컴포넌트 파일에 복사
  4. 실행

    pnpm dev

관련 패턴

Last updated on