✨ 핵심 기능
- 재사용 가능한
useToast()
훅 - Framer Motion 적용
React Portal
을 사용해 DOM 트리 외부에 렌더링
1️⃣ Toast 컴포넌트
ReactDOM.createPortal
을 이용해 Toast를 document.body에 직접 렌더링
이렇게 하면 부모 컴포넌트의 CSS나 위치에 영향을 받지 않고 자유롭게 위치 지정 가능
return ReactDOM.createPortal(
<div className="...">
<div className={`...`}>
{message}
</div>
</div>,
document.body,
);
2️⃣ useToast Hook
컴포넌트 어디서든 Toast 메시지를 쉽게 트리거할 수 있도록 커스텀 훅 작성
필요한 곳에서 showToast(message)
만 호출하여 사용
import { useState, useCallback } from 'react';
export default function useToast() {
const [message, setMessage] = useState('');
const showToast = useCallback((msg: string) => {
setMessage(msg);
setTimeout(() => setMessage(''), 2000);
}, []);
return { message, showToast };
}
useToast()
훅은 상태(message
) + 제어 함수 (showToast
를 제공하는 컨트롤러 역할Toast
는 그 상태를 뷰로 표현하는 컴포넌트- 토스트를 띄우는 로직과 실제 UI를 분리함으로써 독립적으로 유지 → 재사용성, 유지보수성 증가
3️⃣ 실제 사용 예시
const { message, showToast } = useToast();
const handleCopy = async () => {
await navigator.clipboard.writeText(`https://...`);
showToast('클립보드에 복사되었습니다.');
};
...
{message && <Toast key={message} message={message} />}
동일한 메시지를 여러 번 연속으로 보여주기 위해 key={message}
사용
React에서는 동일한 key를 가진 컴포넌트를 재사용하려 하기 때문에 메시지가 바뀌지 않으면 Toast가 재렌더링되지 않음
이를 방지하고자 메시지 변경 시 강제 리마운트하도록 적용
4️⃣ 최종 코드
//Toast.tsx
import { AnimatePresence, motion } from 'framer-motion';
import { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
interface ToastProps {
message: string;
duration?: number;
}
export default function Toast({ message, duration = 2000 }: ToastProps) {
const [show, setShow] = useState(true);
useEffect(() => {
const timer = setTimeout(() => setShow(false), duration);
return () => clearTimeout(timer);
}, [duration]);
return ReactDOM.createPortal(
<AnimatePresence>
{show && (
<motion.div
className="fixed bottom-6 inset-x-0 mx-auto z-50 w-fit"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ duration: 0.5 }}
>
<div className="bg-black text-white text-sm px-4 py-2 rounded-lg shadow-lg text-center">
{message}
</div>
</motion.div>
)}
</AnimatePresence>,
document.body,
);
}
// useToast.tsx
import { useState, useCallback } from 'react';
export default function useToast() {
const [message, setMessage] = useState('');
const showToast = useCallback((msg: string, duration: number = 2000) => {
setMessage(msg);
setTimeout(() => setMessage(''), duration);
}, []);
return { message, showToast };
}