Sonner 작업하기
초기 응답
이 스킬이 특정 질문 없이 처음 호출되면, 다음 내용만 응답하세요:
Sonner를 설정하고, 스타일링하고, 문제를 해결하는 데 도움을 드릴 준비가 되었습니다. 제 지식은 작성자 Emil Kowalski로부터 왔습니다.
사용자가 질문하기 전까지는 다른 정보를 제공하지 마세요.
Sonner 토스트 라이브러리를 위한 가이드 스킬입니다. 작업이 Sonner와 관련된 경우 — 연결, 토스트 렌더링, 스타일링, 또는 수정 — 이 파일을 먼저 참고하여 답변하세요. <Toaster /> 및 toast()의 전체 속성 표는 API.md에 있습니다. 정확한 속성 이름, 유형 또는 기본값이 필요할 때 읽어보세요.
설정
두 가지 요소만 있으면 됩니다:
<Toaster />하나, 한 번만 마운트 — 가능한 한 루트에 가깝게 배치합니다 (Next.js의 경우:layout.tsx— 서버 컴포넌트 내에서도 작동합니다). 페이지별 또는 조건부로 렌더링하지 마세요; 두 번째 마운트된 Toaster는 모든 토스트를 중복으로 만듭니다.- 클라이언트 코드에서
toast()호출 — 이벤트 핸들러, 효과, 콜백. 일반 함수이며, 훅이나 프로바이더가 필요하지 않지만 서버에서는 아무 작업도 수행하지 않습니다: 서버 액션에서는 결과를 반환하고, 이를 수신하는 클라이언트 코드에서toast()를 호출하세요.
import { Toaster } from 'sonner'; // 한 번, layout에서
import { toast } from 'sonner'; // 클라이언트 측 어디서든
올바른 호출 선택하기
| 원하는 기능 | 호출 방법 |
|---|---|
| 일반 메시지 | toast('제목') — 두 번째 줄을 위해 { description } 추가 |
| 성공 / 에러 / 정보 / 경고 아이콘 | toast.success('…'), toast.error('…') 등 |
| 상태를 직접 관리하면서 스피너 표시 | toast.loading('…'), 그 다음 id로 업데이트 |
| 프로미스에 연결된 로딩 → 성공/에러 | toast.promise(promise, { loading, success, error }) — success/error는 각각 resolve된 값 또는 에러를 받는 함수 허용 |
| 동작을 수행하는 버튼 | { action: { label, onClick } } — onClick이 event.preventDefault()를 호출하지 않으면 토스트를 닫음; cancel은 보조 변형 |
| 사용자 정의 JSX, 기본 토스트 셸 | toast(<jsx />) |
| 사용자 정의 JSX, 스타일 없음 | toast.custom((t) => <jsx />) — headless, t는 닫기에 사용할 id 제공 |
레시피
토스트 업데이트 — 같은 id로 toast()를 다시 호출하세요; 전달한 속성만 변경됩니다. toast.success(…, { id })로 전환하면 유형도 변경됩니다. 이것이 toast.promise 없이 로딩 → 성공 흐름이 작동하는 방식입니다:
const id = toast.loading('업로드 중…');
toast.success('업로드됨', { id });
유지 — { duration: Infinity }. 닫기 — toast.dismiss(id), 또는 모든 토스트를 닫으려면 toast.dismiss(). 활성 토스트 읽기 — React에서는 useSonner(), 그 외에서는 toast.getActiveToasts().
텍스트 내 링크 또는 컴포넌트 — 제목 또는 설명에 함수 전달: toast(() => <a href="…">보기</a>).
여러 Toaster — 각각에 id를 지정하고 toast('…', { toasterId: 'canvas' })로 타겟팅합니다. toasterId가 없으면 모든 Toaster가 토스트를 렌더링합니다.
닫기 콜백 — onDismiss는 닫기 버튼 또는 스와이프 시 실행; onAutoClose는 타임아웃 시 실행됩니다. 이들은 별개이며, 단일 "닫힘" 콜백은 없습니다.
스타일링 — 단계별 가이드
변경 사항에 필요한 만큼만 단계를 올라가세요. 너무 일찍 최상위 단계로 가는 것은 괜찮지만 (권장되는 최종 상태), 중간 단계에 머무르는 것은 좋지 않습니다.
- 기본값 — Toaster에
richColors를 추가하여 다채로운 성공/에러 표시,invert로 테마 반전. - 인라인 조정 — 모든 토스트에 적용하려면 Toaster에
toastOptions={{ style: {…} }}, 또는 각toast()호출에style. - 파트별 클래스 —
toastOptions={{ classNames: { toast, title, description, actionButton, cancelButton, closeButton } }}. Sonner의 주입된 스타일이 캐스케이드에서 우선하므로 모든 클래스는!important가 필요합니다 (Tailwind:!text-red-900). 여러 항목에 important를 표시해야 한다면 중단하고 headless로 가세요. - Headless —
toast.custom()과 사용자 정의 JSX를 사용하여 Sonner의 위치 지정, 쌓기, 스와이프 기능을 유지합니다. 디자인 시스템 토스트에 권장되는 방법: 자체toast()추상화로 래핑하세요. (unstyled: true도 중간 해결책으로 존재하지만, headless가 동일한 노력으로 더 많은 제어를 제공합니다.)
아이콘 — Toaster의 icons 속성으로 유형별 기본값을 바꾸거나, 토스트별로 icon을 사용하고, null로 제거합니다.
테마 — theme은 기본적으로 'light'이며 OS 설정을 추적하지 않습니다. theme="system"을 전달하거나, 테마 프로바이더와 연결하세요: next-themes의 <Toaster theme={resolvedTheme} />.
문제 해결
| 증상 | 원인 → 해결책 |
|---|---|
| 토스트가 나타나지 않음 | <Toaster />가 마운트되지 않았거나, 마운트 해제됨 (조건부 렌더링, 페이지별 배치). 루트에 하나를 마운트하세요. 서버 액션에서 호출하는 경우: toast()는 클라이언트 전용입니다 — 액션의 결과로 클라이언트에서 호출하세요. |
| 같은 토스트가 두 번 나타남 | 두 개의 Toaster가 마운트됨 (layout 및 page) — 하나만 유지하세요. 또는 React StrictMode의 개발 모드 이중 호출로 인해 useEffect에서 toast()가 실행된 경우 — 이벤트 핸들러에서 호출하거나, 안정적인 id를 전달하여 두 번째 호출이 중복 대신 업데이트되도록 하세요. |
| Tailwind/CSS 클래스가 적용되지 않음 | 기본 스타일이 덮어씁니다. !important를 표시하거나, unstyled / headless를 사용하세요 (위 단계별 가이드 참조). |
| 토스트가 완전히 스타일 없이 렌더링됨 (Astro, view transitions에서 흔함) | Sonner의 주입된 스타일시트가 손실됨 — layout에서 명시적으로 가져오기: import 'sonner/dist/styles.css'. |
| Shadow DOM 내에서 스타일 없음 | 스타일이 document.head에 들어가므로 Shadow Root에는 들어가지 않습니다. [data-sonner-toaster]가 포함된 스타일 태그의 텍스트를 Shadow Root에 복사하세요. |
| 모달/오버레이 뒤에 토스트가 있거나 잘림 | 조상 요소가 쌓임 컨텍스트를 생성함 (transform, filter, overflow) 또는 오버레이가 Toaster보다 높은 z-index를 가짐. <Toaster />를 다이얼로그/포털 컨테이너 외부, 문서 루트로 이동하세요. |
| 다크 모드 무시됨 | theme이 기본적으로 'light'임 — theme="system"을 설정하거나 resolve된 테마를 전달하세요 (위 테마 참조). |
| 성공/에러가 회색으로 보임 (녹색/빨간색 아님) | 기본값입니다. Toaster에 richColors를 추가하세요. |
| 토스트가 닫히지 않음 | duration: Infinity, dismissible: false, 또는 toast.promise의 프로미스가 해결되지 않음 — 로딩 토스트가 영원히 기다림. |
toast.promise가 로딩에서 멈춤 | 첫 번째 인자로 프로미스 (또는 프로미스를 반환하는 함수)가 필요하며, 프로미스가 실제로 resolve/reject되어야 함. |
| 스와이프로 닫기가 잘못된 방향으로 / 작동하지 않음 | 방향은 position에서 파생됩니다. Toaster의 swipeDirections로 재정의하세요. |
| 토스트가 모든 Toaster에 나타남 | 여러 Toaster는 타겟팅이 필요함: 각 Toaster에 id를 지정하고 toast() 호출에 toasterId를 전달하세요. |
| 모바일에서 토스트가 화면 가장자리에 너무 가까움 | offset (데스크탑, 기본 32px) 및 mobileOffset (<600px, 기본 16px) — 숫자, CSS 문자열, 또는 측면별 객체. |