🔔 JavaScript UI 패턴

토스트 알림
(Toast Notification)

화면 구석에 잠깐 나타났다 사라지는 알림 메시지. 성공·오류·경고 상황을 사용자에게 자연스럽게 전달하는 토스트 알림을 직접 만들어봐요.

🔔 Pure CSS+JS ⚡ 복사 즉시 사용 🎨 4가지 타입 📋 복사 가능한 코드
① Concept

토스트 알림이란?

Toast(토스트)는 빵이 튀어나오는 토스터에서 이름을 따왔어요. 화면 모서리에서 '툭' 튀어나왔다가 3초쯤 뒤 자연스럽게 사라지는 작은 메시지 박스입니다.

💡

토스트 vs 모달 vs 스낵바: 토스트와 스낵바는 거의 같은 개념이에요. Google Material Design에서는 스낵바, iOS·웹 일반에서는 토스트라고 부릅니다. 둘 다 사용자의 작업을 방해하지 않는 비침투적(non-intrusive) 알림이에요. 모달은 반대로 확인 버튼을 눌러야 사라지는 침투적 알림입니다.

언제 써야 하나요?

저장 완료, 복사됨, 전송 성공처럼 사용자가 방금 한 행동의 결과를 빠르게 알려줄 때 씁니다. 사용자가 계속 작업하도록 흐름을 끊지 않아요.

🚫

언제 쓰면 안 되나요?

삭제 확인처럼 사용자의 결정이 필요한 상황, 긴 오류 메시지, 법적 동의 등은 토스트보다 모달이나 인라인 에러가 적합합니다.

⏱️

적정 표시 시간은?

짧은 메시지는 2~3초, 긴 메시지는 4~5초가 일반적이에요. 오류 메시지는 사용자가 읽을 시간을 줘야 하므로 더 길게 설정하거나 수동 닫기만 허용하기도 해요.

📍

위치는 어디에?

가장 일반적인 위치는 오른쪽 하단이에요. 모바일에서는 하단 가운데가 엄지손가락에 가까워 더 자연스러워요. UX 가이드라인에 맞게 선택하세요.

구분토스트/스낵바모달 다이얼로그인라인 메시지
작업 방해 없음 차단 없음
자동 사라짐 O X X
사용 상황 행동 결과 피드백 확인·선택 요구 폼 유효성 오류
② Demo

직접 눌러서 토스트를 띄워보세요

아래 버튼을 누르면 화면 오른쪽 하단에 토스트가 나타나고 3초 뒤 자동으로 사라집니다. × 버튼으로 직접 닫을 수도 있어요.

버튼을 눌러보세요 — 화면 오른쪽 하단에 토스트가 나타납니다

🎯

데모 체크리스트: ① 버튼마다 색이 다른 왼쪽 테두리 ② 3초 후 자동 소멸 ③ 하단의 진행 바(프로그레스 바) ④ × 버튼 클릭으로 즉시 닫기 ⑤ 여러 개 누르면 쌓이는 큐(queue) 처리를 확인해보세요.

③ Code

복사해서 바로 쓰는 전체 코드

HTML + CSS + JS 모두 포함한 완성 코드예요. 별도 라이브러리 없이 순수 코드만으로 동작합니다.

1 — HTML 구조

<body> 안 어딘가에 컨테이너 한 줄만 추가하면 됩니다.

index.html
<!-- body 안에 딱 한 줄 -->
<div id="toast-container"></div>

<!-- 버튼 예시 -->
<button onclick="showToast('저장되었습니다.', 'success')">저장</button>

2 — CSS

컨테이너, 토스트 아이템, 슬라이드·페이드 애니메이션, 진행 바를 포함합니다.

style.css
/* 토스트 컨테이너: 화면 오른쪽 하단 고정 */
#toast-container {
  position: fixed;
  bottom: 24px;
  right: 24px;
  z-index: 9999;
  display: flex;
  flex-direction: column;
  gap: 10px;
  pointer-events: none;
}

/* 토스트 아이템 기본 */
.toast {
  pointer-events: all;
  display: flex;
  align-items: flex-start;
  gap: 12px;
  background: #fff;
  border-radius: 10px;
  padding: 14px 16px;
  box-shadow: 0 4px 20px rgba(0,0,0,.13);
  border-left: 4px solid #4f46e5;
  min-width: 280px;
  max-width: 360px;
  position: relative;
  animation: toastIn .3s cubic-bezier(.34,1.56,.64,1) both;
}
.toast.hiding { animation: toastOut .3s ease forwards; }

/* 타입별 색상 */
.toast-success { border-left-color: #16a34a; }
.toast-error   { border-left-color: #e11d48; }
.toast-warning { border-left-color: #d97706; }
.toast-info    { border-left-color: #4f46e5; }

.toast-icon  { font-size: 1.15rem; flex-shrink: 0; margin-top: 1px; }
.toast-body  { flex: 1; }
.toast-title { font-size: .88rem; font-weight: 700; color: #111827; margin-bottom: 2px; }
.toast-msg   { font-size: .82rem; color: #6b7280; line-height: 1.55; }
.toast-close {
  background: none; border: none; cursor: pointer;
  color: #9ca3af; font-size: 1rem; padding: 0;
  flex-shrink: 0; transition: color .15s;
}
.toast-close:hover { color: #374151; }

/* 하단 진행 바 */
.toast-progress {
  position: absolute;
  bottom: 0; left: 0;
  height: 2px;
  border-radius: 0 0 0 6px;
  animation: toastProgress 3s linear forwards;
}
.toast-success .toast-progress { background: #16a34a; }
.toast-error .toast-progress   { background: #e11d48; }
.toast-warning .toast-progress { background: #d97706; }
.toast-info .toast-progress    { background: #4f46e5; }

/* 애니메이션 keyframes */
@keyframes toastIn {
  from { opacity: 0; transform: translateX(60px); }
  to   { opacity: 1; transform: translateX(0); }
}
@keyframes toastOut {
  from { opacity: 1; transform: translateX(0); }
  to   { opacity: 0; transform: translateX(60px); }
}
@keyframes toastProgress {
  from { width: 100%; }
  to   { width: 0%; }
}

3 — JavaScript

showToast(message, type) 함수 하나로 어디서든 호출할 수 있어요.

script.js
/**
 * showToast(message, type, duration)
 * type: 'success' | 'error' | 'warning' | 'info'
 * duration: ms (기본값 3000)
 */
function showToast(message, type = 'info', duration = 3000) {
  const config = {
    success: { icon: '✅', title: '성공' },
    error:   { icon: '❌', title: '오류' },
    warning: { icon: '⚠️', title: '경고' },
    info:    { icon: 'ℹ️', title: '알림' },
  };
  const { icon, title } = config[type] || config.info;

  // 토스트 요소 생성
  const toast = document.createElement('div');
  toast.className = `toast toast-${type}`;
  toast.innerHTML = `
    <span class="toast-icon">${icon}</span>
    <div class="toast-body">
      <div class="toast-title">${title}</div>
      <div class="toast-msg">${message}</div>
    </div>
    <button class="toast-close" onclick="dismissToast(this.parentElement)">×</button>
    <div class="toast-progress"
         style="animation-duration:${duration}ms"></div>
  `;

  const container = document.getElementById('toast-container');
  container.appendChild(toast);

  // duration 후 자동 닫기
  const timer = setTimeout(() => dismissToast(toast), duration);
  toast._timer = timer;
}

function dismissToast(toast) {
  if (!toast || toast.classList.contains('hiding')) return;
  clearTimeout(toast._timer);
  toast.classList.add('hiding');
  toast.addEventListener('animationend', () => toast.remove(), { once: true });
}
④ Advanced

더 다양하게 응용하기

위치 변경, 지속 시간 조절, 큐 처리 등 실무에서 바로 쓸 수 있는 응용 패턴이에요.

① 위치 변경

#toast-container의 CSS 속성만 바꾸면 돼요.

버튼을 선택하면 다음 토스트부터 해당 위치에 표시됩니다. 위의 ② 데모 버튼을 눌러 확인해보세요.

style.css — 위치 옵션
/* 오른쪽 하단 (기본) */
#toast-container { bottom: 24px; right: 24px; }

/* 왼쪽 하단 */
#toast-container { bottom: 24px; left: 24px; }

/* 상단 가운데 */
#toast-container {
  top: 24px;
  left: 50%;
  transform: translateX(-50%);
  align-items: center;
}

② 지속 시간 조절

showToast()의 세 번째 인수로 밀리초를 전달해요.

script.js — 지속시간
// 기본 (3초)
showToast('저장되었습니다.', 'success');

// 1.5초 — 짧은 확인 메시지
showToast('링크가 복사되었습니다.', 'success', 1500);

// 5초 — 오류처럼 읽을 시간이 필요한 경우
showToast('서버와 연결할 수 없습니다. 네트워크를 확인해주세요.', 'error', 5000);

// 0 (또는 Infinity) — 수동으로만 닫히게 하기
showToast('중요: 결제가 실패했습니다.', 'error', 0);

③ 큐(queue) 처리 — 동시에 쌓이는 갯수 제한

토스트가 너무 많이 쌓이면 사용자가 읽기 어려워요. 최대 갯수를 제한하는 패턴입니다.

script.js — 최대 갯수 제한
const MAX_TOASTS = 3; // 동시에 최대 3개

function showToast(message, type = 'info', duration = 3000) {
  const container = document.getElementById('toast-container');
  const existing = container.querySelectorAll('.toast:not(.hiding)');

  // 한도 초과 시 가장 오래된 토스트 제거
  if (existing.length >= MAX_TOASTS) {
    dismissToast(existing[0]);
  }

  // ... 이하 기존 showToast 코드와 동일
}
⚠️

접근성(a11y) 팁: 토스트에 role="alert" 속성을 추가하면 스크린 리더가 내용을 자동으로 읽어줘요. 오류·경고는 aria-live="assertive", 일반 정보는 aria-live="polite"를 사용하면 더욱 좋습니다.

⑤ Prompt Tip

AI 프롬프트 팁

AI에게 토스트 알림을 요청할 때 사용하기 좋은 프롬프트 예시예요. 복사해서 바로 사용해보세요.

✦ 바이브코더 프롬프트

아래 프롬프트를 Claude, ChatGPT, Cursor에 붙여넣어 보세요.

내 웹페이지에 토스트 알림 시스템을 추가해줘.

요구사항:
- showToast(message, type, duration) 함수 하나로 어디서든 호출 가능
- type: 'success', 'error', 'warning', 'info' 4가지
- 각 타입별로 아이콘과 테두리 색이 달라야 함
- 오른쪽 하단에 fixed 위치로 표시
- 3초 후 자동 소멸 + × 버튼으로 즉시 닫기 가능
- 오른쪽에서 슬라이드인 / 오른쪽으로 슬라이드아웃 애니메이션
- 하단에 남은 시간을 보여주는 progress bar 포함
- 동시에 최대 3개까지만 표시 (초과 시 오래된 것 제거)
- 외부 라이브러리 없이 순수 HTML + CSS + JS로 구현
- role="alert" 접근성 속성 포함

기존 코드에 추가 형식으로 작성해줘.
⑥ FAQ

자주 묻는 질문

토스트 알림을 구현할 때 헷갈리는 부분들이에요.

mouseenter 이벤트에서 clearTimeout(toast._timer)로 타이머를 멈추고, mouseleave에서 setTimeout을 다시 시작하면 됩니다. CSS의 animation-play-state: paused를 progress bar에 적용하면 진행 바도 함께 멈출 수 있어요.
React라면 useState로 toast 배열을 관리하고 useEffect로 자동 제거를 처리해요. Vue는 ref([])로 같은 방식으로 구현합니다. 또는 react-hot-toast, vue-toastification 같은 검증된 라이브러리를 사용하면 더 빠르게 적용할 수 있어요.
모바일에서 소프트 키보드가 올라오면 fixed 요소의 위치가 밀릴 수 있어요. bottom 값을 env(safe-area-inset-bottom)으로 대체하거나, 키보드가 열렸을 때 토스트를 상단으로 이동시키는 방법이 있습니다. 또는 입력 중에는 토스트를 잠시 숨기는 전략도 실무에서 많이 씁니다.