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
✅ 권장 사항
-
보안 토큰 저장
- 민감한 토큰은 httpOnly 쿠키에 저장 (XSS 방어)
- localStorage는 XSS 공격에 취약하므로 주의
-
토큰 갱신 구현
- Access Token (짧은 유효기간) + Refresh Token (긴 유효기간)
- 토큰 만료 시 자동 갱신 로직 구현
-
에러 처리
- 명확한 에러 메시지 제공
- 401/403 에러 시 로그인 페이지로 리다이렉트
- 네트워크 오류 재시도 로직
-
접근성
- 스크린 리더용 에러 메시지 (
role="alert") - 키보드 네비게이션 지원
- 로딩 상태 명확히 표시
- 스크린 리더용 에러 메시지 (
-
사용자 경험
- 로그인 성공 시 이전 페이지로 리다이렉트
- “로그인 유지” 옵션 제공
- 비밀번호 재설정 플로우
⚠️ 피해야 할 것
-
보안 취약점
- 비밀번호를 로그에 기록하지 말 것
- 토큰을 URL에 포함하지 말 것
- HTTPS 없이 인증 정보 전송하지 말 것
-
성능 문제
- 모든 컴포넌트에서 인증 상태 체크하지 말 것
- 불필요한 인증 API 호출 최소화
-
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 예제는 곧 제공될 예정입니다.
로컬에서 실행하기
-
프로젝트 생성
npx @vortex/cli init my-auth-project --template vite-react cd my-auth-project -
컴포넌트 추가
# 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 -
코드 복사
- 위 예제 코드를
src/App.tsx또는 해당 컴포넌트 파일에 복사
- 위 예제 코드를
-
실행
pnpm dev
관련 패턴
- Form Validation - 인증 폼 검증
- Error Handling - 인증 에러 처리
- Security Patterns - 보안 강화
Last updated on