이 글은 누구를 위한 것인가
- 성공/오류/경고 메시지 컴포넌트를 일관성 없이 만들어온 팀
- Toast를 어떤 상황에 써야 하고 Alert는 언제 쓰는지 모르는 팀
- 알림 컴포넌트를 디자인 시스템에 정리하고 싶은 디자이너
들어가며
"저장되었습니다" Toast, "주의: 이 작업은 되돌릴 수 없습니다" Alert, "입력 오류" 인라인 메시지 — 이 세 가지가 혼용되면 사용자는 혼란스럽다. 상황별 컴포넌트를 명확히 정의해야 한다.
이 글은 bluefoxdev.kr의 알림 시스템 UX 를 참고하여 작성했습니다.
1. 알림 컴포넌트 유형 가이드
[알림 컴포넌트 선택 가이드]
Toast (토스트):
특징: 일시적, 자동 소멸, 화면 우측 하단/상단
용도: 성공 확인 (저장, 복사, 좋아요)
지속 시간: 3-5초 (오류는 더 길게)
주의: 사용자 행동 필요한 경우 사용 금지
Alert Banner (페이지 상단 배너):
특징: 페이지 전체에 영향, 수동으로 닫음
용도: 점검 공지, 중요 공지, 브라우저 비호환
우선순위: error > warning > info > success
Inline Message (인라인):
특징: 폼 필드 아래, 특정 UI 요소 근처
용도: 폼 검증 오류, 필드 설명
규칙: 관련 UI 요소 바로 아래에 배치
Confirm Dialog:
특징: 사용자 확인 필요한 모달
용도: 삭제 확인, 결제 확인
주의: 과용 금지 (인증 피로)
Badge / Dot:
특징: 아이콘/메뉴에 붙는 미니 알림
용도: 읽지 않은 수, 새 항목
규칙: 숫자 99+ 이상은 "99+" 표시
[알림 우선순위]
Error (빨강): 즉시 해결 필요
Warning (노랑): 확인 필요
Info (파랑): 참고 정보
Success (초록): 완료 확인
2. Toast 시스템 구현
import { useState, createContext, useContext, useCallback } from "react";
type ToastType = "success" | "error" | "warning" | "info";
interface Toast {
id: string;
type: ToastType;
message: string;
duration?: number;
}
interface ToastContextValue {
addToast: (toast: Omit<Toast, "id">) => void;
removeToast: (id: string) => void;
}
const ToastContext = createContext<ToastContextValue | null>(null);
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([]);
const addToast = useCallback((toast: Omit<Toast, "id">) => {
const id = Math.random().toString(36).slice(2);
const duration = toast.duration ?? (toast.type === "error" ? 6000 : 3000);
setToasts((prev) => [...prev, { ...toast, id }]);
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, duration);
}, []);
const removeToast = useCallback((id: string) => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, []);
return (
<ToastContext.Provider value={{ addToast, removeToast }}>
{children}
{/* Toast 스택 */}
<div
className="fixed bottom-4 right-4 z-50 flex flex-col gap-2"
role="region"
aria-label="알림"
aria-live="polite"
>
{toasts.map((toast) => (
<ToastItem key={toast.id} toast={toast} onClose={() => removeToast(toast.id)} />
))}
</div>
</ToastContext.Provider>
);
}
const toastStyles = {
success: "bg-green-600 text-white",
error: "bg-red-600 text-white",
warning: "bg-yellow-500 text-white",
info: "bg-blue-600 text-white",
};
const toastIcons = {
success: "✓",
error: "✕",
warning: "⚠",
info: "ℹ",
};
function ToastItem({ toast, onClose }: { toast: Toast; onClose: () => void }) {
return (
<div
className={`flex items-center gap-3 px-4 py-3 rounded-xl shadow-lg min-w-[280px] max-w-sm animate-slide-in-right ${toastStyles[toast.type]}`}
role="alert"
>
<span className="text-lg">{toastIcons[toast.type]}</span>
<span className="flex-1 text-sm font-medium">{toast.message}</span>
<button onClick={onClose} className="opacity-80 hover:opacity-100 text-lg">
✕
</button>
</div>
);
}
export function useToast() {
const ctx = useContext(ToastContext);
if (!ctx) throw new Error("ToastProvider 안에서 사용해야 합니다");
return ctx;
}
// 사용 예시
function SaveButton() {
const { addToast } = useToast();
const handleSave = async () => {
try {
await saveData();
addToast({ type: "success", message: "저장되었습니다" });
} catch (error) {
addToast({ type: "error", message: "저장에 실패했습니다. 다시 시도해주세요." });
}
};
return <button onClick={handleSave}>저장</button>;
}
마무리
알림 시스템의 핵심 원칙: Toast는 "확인용"이고, Alert는 "주의용"이다. Toast로 오류를 알리면 3초 후 사라지기 때문에 사용자가 놓친다. 오류는 인라인 메시지나 Alert Banner로 보여주고, Toast는 "저장 완료" 같은 긍정적 피드백에만 써야 한다.