백그라운드 리패치 오류나 네트워크 연결에 여러 번 실패할시 토스트로 피드백을 주려고 했다.
컴포넌트를 import해 사용하는 것보다 util함수로 만들어 window.alert처럼 메시지만 넣어 간단히 호출하게 하면 팀원들이 사용하기 편할 것 같았다.
전역 상태 관리 라이브러리로 zustand를 사용하고 있어 이를 활용해 토스트 오픈, 애니메이션 상태를 관리하고자 했다.
1️⃣ 전역 상태 설정
import { create } from 'zustand';
const useToastStore = create((set) => ({
isOpen: false,
message: '',
isFadingOut: false, // 애니메이션 상태
show: (message: string) => {
set({ isOpen: true, message, isFadingOut: false });
setTimeout(() => set({ isFadingOut: true }), 2500); // 2.5초 후 fadeout 시작
setTimeout(() => set({ isOpen: false, isFadingOut: false }), 3000); // 3초 후 toast 숨김
},
close: () => set({ isOpen: false, message: '', isFadingOut: false }),
}));
export default useToastStore;
setTimeout으로 특정 시간이 지나면 페이드 아웃하고, 더 지나면 사라지게 할 것이다.
2️⃣ util 함수
import useToastStore from '../stores/ui/useToastStore';
const toast = (message) => {
const store = useToastStore.getState();
// 이미 토스트가 표시 중이라면 새로운 요청 무시
if (store.isOpen) {
return;
}
store.show(message);
};
// 토스트를 강제 종료하는 함수
export const closeToast = () => {
const store = useToastStore.getState();
store.close();
};
export default toast;
토스트가 중복 생성되지 않도록 isOpen 상태를 확인해 show를 호출한다.
3️⃣ Toast 렌더링용 DOM 노드 생성
<body>
<div id="root"></div>
<div id="modal-portal"></div>
<div id="toast-portal"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
레이어를 분리하기 위해 createPortal을 사용할 것이다.
createPortal(children, domNode, key?)
일부 JSX와 렌더링할 DOM 노드를 전달해 사용한다.
그럼 React는 사용자가 전달한 JSX에 대한 DOM 노드를 사용자가 제공한 DOM 노드 안에 넣는다.
이런식으로 JSX를 순간 이동시킬 수 있다.
이를 위해 id가 toast-portal인 노드를 추가했다.
4️⃣ Toast component
import * as S from './style';
import { createPortal } from 'react-dom';
import { AnimatePresence } from 'framer-motion';
import useToastStore from '@/stores/ui/useToastStore';
const Toast = () => {
const { isOpen, message, isFadingOut } = useToastStore();
if (!isOpen) return null;
const toastRoot = document.getElementById('toast-portal');
return createPortal(
<AnimatePresence>
{isOpen && (
<S.Toast
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: isFadingOut ? 0 : 1, y: isFadingOut ? 50 : 0 }}
exit={{ opacity: 0, y: 50 }}
transition={{ duration: 0.5 }}
role="alert"
aria-live="assertive"
>
{message}
</S.Toast>
)}
</AnimatePresence>,
toastRoot,
);
};
export default Toast;
framer-motion을 사용해 애니메이션을 설정했다.
렌더링되면 y 값이 50에서 0으로 바뀌게 해 밑에서 올라오는 효과를 줬다.
isFadingOut이 true가 되면 다시 50으로 이동한다.
스타일은 따로 분리해놨다.
import styled from 'styled-components';
import { motion } from 'framer-motion';
export const Toast = styled(motion.div)`
position: fixed;
z-index: 2;
right: 16px;
bottom: 16px;
display: flex;
align-items: center;
justify-content: center;
padding: 16px 24px;
border-radius: 5px;
color: #555555;
text-align: center;
white-space: nowrap;
box-shadow: 0 2px 5px rgb(0, 0, 0, 20%);
`;
그리고 App.jsx에 Toast 컴포넌트를 추가해놨다
function App() {
// ...
<Suspense fallback={<DeferredLoader />}>
<Toast />
<Router />
</Suspense>
// ...
}

모달을 단순히 컴포넌트로 사용해왔는데 이런 방식으로 구현하니 편하게 사용할 수 있었다.
alert와 confirm도 직접 만들어 사용할 계획이다.
참고자료