알림·알럿 UX 시스템: 상황에 맞는 피드백 컴포넌트 설계

UX 디자인

알림 UXToast 컴포넌트Alert 설계피드백 시스템알림 디자인

이 글은 누구를 위한 것인가

  • 성공/오류/경고 메시지 컴포넌트를 일관성 없이 만들어온 팀
  • 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는 "저장 완료" 같은 긍정적 피드백에만 써야 한다.