토스트(스낵바) 직접 만들기

들어가며

화면을 만드는데 디자인 시안에 'snackbar(스낵바)'라는 게 계속 나왔다.
화면 아래에 잠깐 떴다 사라지는 안내 띠인데, 솔직히 처음 보는 단어였다.
'토스트'는 들어봤어도 직접 만들어본 적은 없었고, "토스트랑 스낵바는 같은 건가, 다른 건가?"부터 막혔다.

처음엔 "라이브러리 하나 가져다 쓰면 되겠지" 싶었는데, 파고들수록 alert이랑 뭐가 다른지,
어떻게 저절로 사라지는지, 위치는 어떻게 바꾸는지 궁금한 게 줄줄이 나왔다.
결국 직접 만들어보면서 하나씩 알게 됐다. 토스트를 처음 건드리는 사람 입장에서 막혔던 지점들을 순서대로 정리해둔다.

한 줄 요약

토스트/스낵바는 화면에 잠깐 떴다 사라지는 안내 띠다.
숨겨둔 요소를 잠깐 보여줬다가 setTimeout으로 도로 숨기는 게 전부고, 스낵바는 거기에 바로가기(액션) 버튼이 붙은 진화형이다.
마크업/CSS가 8할이라 처음 만들기에도 부담이 없다.

아래 코드는 그대로 복사해 .html 파일로 저장하면 브라우저에서 바로 돌아간다.


1. 토스트가 뭐냐

화면에 잠깐 떴다가 알아서 사라지는 안내 띠. 보통 화면 아래쪽에 뜬다.

alert / confirm 이랑 뭐가 다른가

평소 쓰던 alert(), confirm() 팝업이랑 헷갈리기 쉬운데 용도가 다르다.

alert / confirm토스트 / 스낵바
생김새화면 가운데 팝업 박스화면 가장자리(보통 하단) 가로 띠
닫는 법사용자가 "확인"을 눌러야 사라짐2~3초 뒤 알아서 사라짐 (X는 선택)
화면 막기누를 때까지 막음 (모달)안 막음. 그 위로 계속 조작 가능
쓰는 상황"변경하시겠습니까?" 같은 질문/결정"변경되었습니다" 같은 결과 통보

흐름은 보통 이렇게 짝이 된다:
작업 → alert 로 한 번 확인받고 → 처리 → 토스트로 "됐어요" 알려주고 슥 사라짐.


2. 토스트 vs 스낵바 (이름이 둘인 이유)

뿌리는 같은데 할 수 있는 게 다르다.

일반 토스트 (Toast)스낵바 (Snackbar)
출신안드로이드 원조 (2008년경)머티리얼 디자인 (2014년)
내용텍스트(+아이콘)만텍스트 + 액션 버튼 + 닫기(X)
상호작용없음 (그냥 떴다 사라짐)있음 (바로가기 누르기 / X로 닫기)
  • 토스트(2008) = 알리고 끝. 손댈 게 없다.
  • 스낵바(2014) = 토스트의 진화형. 액션 버튼("실행취소", "바로가기")이 붙고 X로 닫을 수도 있다.
  • 그래서 스낵바는 토스트의 상위호환이다. 옵션(액션·X)을 빼면 그냥 토스트처럼 동작한다.

웹에서는 둘을 엄격히 안 나누고 그냥 "토스트"라고 뭉뚱그려 부르는 경우가 많다.
문서에 snackbar(=토스트) 라고 같이 적혀 있어도 놀라지 말 것 — 같은 걸 가리키는 두 이름이다.

참고: 스낵바 형태(다크 띠 · 하단 · 텍스트+액션 · 자동소멸)는 구글 머티리얼 디자인이 정립한 패턴이다.


3. 표준 타입 — success / error / warning / info

토스트 라이브러리(Toastify, iziToast, react-toastify 등)는 거의 다 4가지 타입을 표준으로 쓴다.
타입마다 색과 아이콘이 약속돼 있어서, 사용자가 색만 봐도 성공인지 오류인지 안다.

type아이콘쓰는 상황
success초록작업 성공 ("저장되었습니다")
error빨강실패/오류 ("저장에 실패했습니다")
warning주황주의 필요 ("입력값을 확인해 주세요")
info파랑일반 정보 ("새 버전이 있습니다")
  • 타입을 안 주면 그냥 중립(다크) 안내 토스트.
  • 색은 보통 왼쪽 accent 보더 + 같은 색 아이콘으로 표현한다. (배경 전체를 색칠하는 라이브러리도 있음)

4. 구조 — 4요소 (+ 타입 아이콘)

스낵바는 요소 4개로 끝난다.

  1. 컨테이너 = 어두운 배경 띠 (검정 bg + 흰 텍스트로 대비)
  2. 텍스트 = 안내 문구 (필수). 예) "저장되었습니다."
  3. 액션 = 파란 바로가기 링크/버튼 (선택). 예) "실행취소"
  4. 닫기 X = 아이콘 (선택). 누르면 즉시 사라짐. 없으면 2~3초 뒤 자동 소멸.

여기에 타입이 있으면 텍스트 앞에 타입 아이콘(✔/✖/⚠/ⓘ)이 하나 더 붙는다.
마크업으로 치면 div 하나 안에 (아이콘) + 텍스트 + (링크) + (X) 넣는 정도다.


5. 직접 만들기 (1) — HTML

토스트 박스를 화면에 미리 하나 만들어 둔다. 평소엔 CSS로 숨겨두고, 필요할 때 보였다 숨겼다 한다.

<div id="snackbar" class="snackbar">
  <span class="snackbar__text"></span>
</div>

내용(아이콘·텍스트·액션·X)은 보여줄 때 JS가 채워 넣는다.


6. 직접 만들기 (2) — CSS

핵심은 opacity:0(평소 숨김) + .show일 때 opacity:1(보임). 클래스 하나로 켰다 껐다 한다.
타입은 modifier 클래스(snackbar--success 등)로 색만 바꿔준다.

/* (1) 토스트 박스: 평소엔 안 보이는 상태로 대기 */
.snackbar {
  position: fixed;                 /* 스크롤해도 화면에 고정 */
  left: 50%;                       /* 가로 중앙 기준점 */
  bottom: 30px;                    /* 화면 아래쪽 */
  transform: translateX(-50%);     /* 자기 폭의 절반만큼 왼쪽으로 = 진짜 가운데 */
  width: 90%; max-width: 420px; box-sizing: border-box;
  display: flex; align-items: center; gap: 10px;
  min-height: 44px; padding: 14px 16px; border-radius: 10px;
  background: #222; color: #fff;   /* 다크 배경 + 흰 글씨 */
  font-size: 14px;
  opacity: 0;                      /* ★ 평소엔 투명 = 안 보임 */
  pointer-events: none;            /* 안 보일 땐 클릭도 막기 */
  transition: opacity .3s, transform .3s;  /* 부드럽게 나타나고 사라지게 */
}

/* (2) 'show' 클래스가 붙으면 → 보인다 */
.snackbar.show {
  opacity: 1;
  pointer-events: auto;
  transform: translateX(-50%) translateY(0);
}

.snackbar__icon { flex-shrink: 0; font-size: 16px; }   /* 타입 아이콘 */
.snackbar__text { flex: 1; }                            /* 텍스트는 남는 공간 다 차지 */
.snackbar__action {                                     /* 액션(바로가기) = 파란 링크 */
  color: #5b9dff; font-weight: bold; text-decoration: none; white-space: nowrap;
}

/* ===== 표준 타입: 왼쪽 accent 보더 + 같은 색 아이콘 ===== */
.snackbar--success { border-left: 4px solid #2ecc71; }
.snackbar--success .snackbar__icon { color: #2ecc71; }
.snackbar--error   { border-left: 4px solid #e74c3c; }
.snackbar--error   .snackbar__icon { color: #e74c3c; }
.snackbar--warning { border-left: 4px solid #f39c12; }
.snackbar--warning .snackbar__icon { color: #f39c12; }
.snackbar--info    { border-left: 4px solid #3498db; }
.snackbar--info    .snackbar__icon { color: #3498db; }

7. 직접 만들기 (3) — JS (보여주고 숨기기)

새로 배우는 건 사실상 setTimeout 한 줄뿐이다.
setTimeout(할 일, 3000) = "3000밀리초(=3초) 뒤에 이거 해줘" 라고 알람을 예약하는 함수.

var hideTimer = null;  // 자동 숨김 타이머 기억용 (새 토스트 뜨면 이전 거 취소)

// 타입별 아이콘 (HTML 엔티티)
var ICONS = { success:'&#10004;', error:'&#10006;', warning:'&#9888;', info:'&#9432;' };

function showSnackbar(message, options) {
  options = options || {};
  var bar = document.getElementById('snackbar');

  // 클래스 초기화 후, 타입 modifier 다시 붙이기
  bar.className = 'snackbar';
  if (options.type) bar.classList.add('snackbar--' + options.type);  // success/error/warning/info

  // 내용 그리기: (타입 아이콘) + 텍스트 + (액션) + X
  var html = '';
  if (options.type && ICONS[options.type]) {
    html += '<span class="snackbar__icon">' + ICONS[options.type] + '</span>';
  }
  html += '<span class="snackbar__text">' + message + '</span>';
  if (options.action) {                                              // 액션 있을 때만 링크 추가
    html += '<a class="snackbar__action" href="' + (options.href || '#') + '">'
            + options.action + '</a>';
  }
  html += '<button class="snackbar__close">&times;</button>';
  bar.innerHTML = html;

  // 보여주고 → 3초 뒤 자동 숨김
  bar.classList.add('show');
  clearTimeout(hideTimer);
  hideTimer = setTimeout(hideSnackbar, 3000);
}

// 숨기기 = show 클래스 떼기 (X 누를 때도 이거 호출)
function hideSnackbar() {
  clearTimeout(hideTimer);
  document.getElementById('snackbar').classList.remove('show');
}

동작 정리:
1. 클래스 초기화 → 타입 modifier 붙이기 (색 결정)
2. 내용 그리고 classList.add('show') → 보여줘
3. setTimeout(..., 3000) → 3초 뒤 숨겨 (= 자동 소멸의 정체)
4. X를 누르면 → 기다리지 말고 바로 hideSnackbar()

setTimeout3000은 밀리초다. 1000 = 1초. 그래서 3000이 3초.


8. 호출 예시 (타입 / 액션 조합)

옵션만 바꾸면 같은 함수로 토스트·스낵바·타입별 알림이 다 나온다.

showSnackbar('저장되었습니다.', { type:'success' });               // 초록 성공
showSnackbar('저장에 실패했습니다.', { type:'error' });             // 빨강 오류
showSnackbar('입력값을 다시 확인해 주세요.', { type:'warning' });    // 주황 경고
showSnackbar('새 버전이 있습니다.', { type:'info' });               // 파랑 정보
showSnackbar('항목을 삭제했습니다.', { action:'실행취소', href:'#' }); // 스낵바(액션, 타입 없음)
showSnackbar('링크가 복사되었습니다.');                             // 중립 토스트
  • type 안 주면 → 중립(다크) 토스트
  • action 주면 → 파란 바로가기 링크 (= 스낵바)
  • 둘은 자유롭게 섞을 수 있다 ({ type:'success', action:'보기', href:'...' })

9. 2~3줄 처리

긴 메시지는 따로 모드가 없어도 자동으로 줄바꿈된다. 비결은 높이를 고정하지 않는 것.

.snackbar {
  /* height: 44px;  ← 고정하면 안 됨 */
  min-height: 44px;   /* 최소만 잡고, 높이는 내용 따라 자동으로 늘어남 */
}
  • align-items: center 덕분에 2~3줄이 돼도 아이콘·액션·X가 세로 가운데에 예쁘게 놓인다.
  • 끊기는 위치를 내가 정하고 싶으면 메시지에 <br>을 넣는다. (단, innerHTML로 넣을 때만 먹는다)
showSnackbar('파일을 업로드하지 못했습니다.<br>잠시 후 다시 시도해 주세요.', { type:'error' });

주의: <br>이나 HTML을 innerHTML로 넣을 땐, 사용자 입력·서버 값을 그대로 넣으면 깨지거나 보안문제(XSS)가 날 수 있다. 고정 안내문구엔 괜찮다.


10. 위치 바꾸기 — 모디파이어(멀티클래스)

스낵바는 보통 하단이지만, 위치는 결국 CSS 값일 뿐이라 어디든 갈 수 있다.
기본 클래스 + 위치별 수식 클래스(modifier) 를 같이 붙이는 방식(멀티클래스)이 정석이다.

.snackbar          { bottom: 30px; }                          /* 기본 = 하단 */
.snackbar--top     { top: 30px; bottom: auto; }                /* 위 */
.snackbar--center  { top: 50%; bottom: auto; transform: translate(-50%, -50%); } /* 가운데 */
<div class="snackbar">...</div>                  <!-- 기본 하단 -->
<div class="snackbar snackbar--top">...</div>     <!-- 위 -->
<div class="snackbar snackbar--center">...</div>  <!-- 가운데 -->
  • 위치를 바꿀 땐 기존 bottom 값을 bottom: auto로 풀어줘야 충돌이 없다.
  • 참고: 위는 시스템/에러 알림 느낌, 가운데는 콘텐츠를 가려서 거의 안 쓴다. 안내 스낵바는 하단이 표준.

11. X 정렬 — 모디파이어 + 자동 규칙

2~3줄일 때 X를 위로 붙이고 싶을 때가 있다. 이것도 모디파이어 한 줄.

.snackbar--xtop .snackbar__close { align-self: flex-start; }
  • align-items = 컨테이너가 자식들 전체의 세로 정렬을 정함
  • align-self = 개별 자식이 자기 혼자만 다르게 정렬

JS에서 옵션 따라 그 클래스를 붙였다 뗀다. 여기에 자동 규칙까지 넣을 수 있다:

// xtop 안 정해주면 → "액션 없을 때만 위로" 자동 적용
var xtop = (options.xtop !== undefined) ? options.xtop : !options.action;
bar.classList.toggle('snackbar--xtop', !!xtop);

classList.toggle('클래스', 조건) = 조건이 참이면 붙이고, 거짓이면 뗀다. 부를 때마다 다시 판단하므로 각 토스트가 독립적이다.

호출X 위치누가 정했나
액션O, xtop 안 줌가운데자동
액션O, xtop:true내가 직접 (자동 무시)
액션X, xtop 안 줌자동
액션X, xtop:false가운데내가 직접 (자동 무시)

평소엔 자동, 예외만 손으로 덮어쓰기.


12. 터치영역 — 업계기준 44px

X 같은 누르는 요소는 터치영역 최소 44×44px(애플 HIG) / 48px(머티리얼)로 잡는 게 표준이다.
보이는 × 글자는 작아도, 클릭되는 영역은 넓게.

.snackbar__close {
  flex-shrink: 0;
  width: 44px; height: 44px;       /* ★ 터치영역 44px */
  display: flex;                   /* × 글자를… */
  align-items: center;             /* …세로 가운데 */
  justify-content: center;         /* …가로 가운데 */
  margin: -10px -8px;              /* 커진 만큼 음수마진으로 당겨 레이아웃 보정 (안 밀리게) */
  background: none; border: none; color: #aaa; font-size: 18px;
  line-height: 1; padding: 0; cursor: pointer;
}

포인트 — width/height로 영역을 키우고, margin 음수로 그만큼 당겨서 다른 요소가 안 밀리게 한다.
"자동으로 사라지니까 X 작아도 되지" 가 아니라, 지금 당장 닫고 싶은 사용자를 위해 넉넉하게 잡아야 한다.


13. 전체 코드 (복사해서 바로 실행)

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>토스트/스낵바 연습</title>
<style>
  body { font-family: -apple-system, "Apple SD Gothic Neo", sans-serif;
    max-width: 480px; margin: 0 auto; padding: 30px 20px; line-height: 1.6; background: #f5f5f5; }
  .btn { display: block; width: 100%; box-sizing: border-box; margin: 8px 0; padding: 14px;
    font-size: 15px; border: 1px solid #ccc; border-radius: 8px; background: #fff; cursor: pointer; }

  /* ===== 토스트(스낵바) 본체 ===== */
  .snackbar {
    position: fixed; left: 50%; bottom: 30px; transform: translateX(-50%);
    width: 90%; max-width: 420px; box-sizing: border-box;
    display: flex; align-items: center; gap: 10px;
    min-height: 44px; padding: 14px 16px; border-radius: 10px;
    background: #222; color: #fff; font-size: 14px;
    opacity: 0; pointer-events: none;
    transition: opacity .3s, transform .3s;
  }
  .snackbar.show { opacity: 1; pointer-events: auto; transform: translateX(-50%) translateY(0); }
  .snackbar__icon { flex-shrink: 0; font-size: 16px; }
  .snackbar__text { flex: 1; }
  .snackbar__action { color: #5b9dff; font-weight: bold; text-decoration: none; white-space: nowrap; }
  .snackbar__close {
    flex-shrink: 0; width: 44px; height: 44px;
    display: flex; align-items: center; justify-content: center; margin: -10px -8px;
    background: none; border: none; color: #aaa; font-size: 18px; line-height: 1; padding: 0; cursor: pointer;
  }
  /* 표준 타입 */
  .snackbar--success { border-left: 4px solid #2ecc71; } .snackbar--success .snackbar__icon { color: #2ecc71; }
  .snackbar--error   { border-left: 4px solid #e74c3c; } .snackbar--error   .snackbar__icon { color: #e74c3c; }
  .snackbar--warning { border-left: 4px solid #f39c12; } .snackbar--warning .snackbar__icon { color: #f39c12; }
  .snackbar--info    { border-left: 4px solid #3498db; } .snackbar--info    .snackbar__icon { color: #3498db; }
  /* 위치 / X정렬 모디파이어 */
  .snackbar--top     { top: 30px; bottom: auto; }
  .snackbar--center  { top: 50%; bottom: auto; transform: translate(-50%, -50%); }
  .snackbar--xtop .snackbar__close { align-self: flex-start; }
</style>
</head>
<body>

  <button class="btn" onclick="showSnackbar('저장되었습니다.', { type:'success' })">success</button>
  <button class="btn" onclick="showSnackbar('저장에 실패했습니다.', { type:'error' })">error</button>
  <button class="btn" onclick="showSnackbar('입력값을 다시 확인해 주세요.', { type:'warning' })">warning</button>
  <button class="btn" onclick="showSnackbar('새 버전이 있습니다.', { type:'info' })">info</button>
  <button class="btn" onclick="showSnackbar('항목을 삭제했습니다.', { action:'실행취소', href:'#' })">스낵바 (액션)</button>
  <button class="btn" onclick="showSnackbar('파일을 업로드하지 못했습니다. 잠시 후 다시 시도해 주세요.', { type:'error' })">긴 메시지 + X 자동 위</button>

  <div id="snackbar" class="snackbar">
    <span class="snackbar__text"></span>
  </div>

<script>
  var hideTimer = null;
  var ICONS = { success:'&#10004;', error:'&#10006;', warning:'&#9888;', info:'&#9432;' };

  function showSnackbar(message, options) {
    options = options || {};
    var bar = document.getElementById('snackbar');

    // 클래스 초기화 → 타입 modifier
    bar.className = 'snackbar';
    if (options.type) bar.classList.add('snackbar--' + options.type);

    // X 정렬: 안 정해주면 "액션 없을 때만 위로" 자동
    var xtop = (options.xtop !== undefined) ? options.xtop : !options.action;
    if (xtop) bar.classList.add('snackbar--xtop');

    // 내용 그리기
    var html = '';
    if (options.type && ICONS[options.type]) html += '<span class="snackbar__icon">' + ICONS[options.type] + '</span>';
    html += '<span class="snackbar__text">' + message + '</span>';
    if (options.action) html += '<a class="snackbar__action" href="' + (options.href || '#') + '">' + options.action + '</a>';
    html += '<button class="snackbar__close">&times;</button>';
    bar.innerHTML = html;

    // 보여주고 → 3초 뒤 자동 숨김
    bar.classList.add('show');
    clearTimeout(hideTimer);
    hideTimer = setTimeout(hideSnackbar, 3000);
  }

  function hideSnackbar() {
    clearTimeout(hideTimer);
    document.getElementById('snackbar').classList.remove('show');
  }
</script>

</body>
</html>

14. 헷갈렸던 점 메모

  • 토스트와 스낵바는 같은 거냐? → 뿌리는 같고 능력이 다르다. 스낵바 = 액션/닫기까지 되는 토스트(상위호환).
  • 표준 타입 → success / error / warning / info 4종. 색·아이콘이 약속돼 있다. 거의 모든 토스트 라이브러리가 이 체계.
  • 스낵바는 보통 어디? → 하단이 표준(머티리얼). 위/가운데도 CSS로 가능하지만, 안내용은 하단.
  • alert vs 토스트 → alert은 "물어보기", 토스트는 "알려주기". 역할 헷갈리지 말 것.
  • 자동 소멸의 정체setTimeout 한 줄. 라이브러리든 직접 만들든 속은 다 이 패턴(요소 보여주고 → 타이머로 숨기기).
  • 타입/위치/X정렬 → 전부 모디파이어 클래스 + JS classList로 조합. 컴포넌트 하나로 다 커버.
  • 터치영역은 44px → 누르는 건 무조건 업계기준으로.
profile
프론트는 순항중 ¿¿

0개의 댓글