console.log์ฒ๋ผ ์ธ ์ ์๋ toast๋ฅผ ๋ง๋ค๊ธฐ ์ํด ๊ณ ๋ฏผํ ๊ณผ์ ์ ๋ํ ๊ธฐ๋ก์ ๋๋ค.
๊ณฐํฐ๋ทฐ ์๋น์ค์์๋ ์ข ๋ ๋์ ์ฌ์ฉ์ ๊ฒฝํ์ ์ํด ํ ์คํธ ์ปดํฌ๋ํธ๋ฅผ ๋์ ํ๊ธฐ๋ก ๊ฒฐ์ ํ์ต๋๋ค. ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํด์ ํ ์คํธ๋ฅผ ๋์ ํ ์๋ ์์์ง๋ง, ํ ์คํธ๋ฅผ ๊ตฌํํด๋ณด๋ ๊ฒ์ ์ด์ ๋ถํฐ ์ ๋ง ํด๋ณด๊ณ ์ถ์๋ ์์ ์ด๋ผ์ ์ง์ ๊ตฌํํ๊ฒ ๋์์ต๋๋ค.
ํ ์คํธ๋ฅผ ๊ตฌํํ๊ธฐ์ ์์ ์ ๊ฐ ์๊ฐํ๋ ํ ์คํธ์ ์ ์๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
console.log
๋ฅผ ํธ์ถํ ์ ์๋ ๊ณณ์ด๋ผ๋ฉด ํ ์คํธ ๋ํ ํธ์ถํ ์ ์์ด์ผ ํฉ๋๋ค.1๋ฒ, 2๋ฒ ์กฐ๊ฑด์ ๋ง์กฑ์ํค๋ ๊ฒ์ ํจ์์ ํ๋ผ๋ฏธํฐ์ css๋ง ์กฐ๊ธ ์กฐ์ ํ๋ฉด ๋๋ ๊ทธ๋ค์ง ์ด๋ ต์ง ์์ต๋๋ค. ํ์ง๋ง 3๋ฒ ์กฐ๊ฑด์ ์ข ๊น๋ค๋ก์ด๋ฐ์. ํ ์คํธ๋ ๊ฒฐ๊ตญ ๋ด๋ถ๋ฅผ ๋ณด๋ฉด ์ปดํฌ๋ํธ์ธ๋ฐ, ์ปดํฌ๋ํธ์ ๋๋๋ง์ ์ปดํฌ๋ํธ๋ ํ ์ด ์๋ ๊ณณ์์ ์ ์ดํ ์ ์์ด์ผ ํ๊ธฐ ๋๋ฌธ์ด์ฃ . ๊ทธ๋์ ์ด๋ฅผ ๊ตฌํํ๊ธฐ ์ํด ์๋์ ๊ฐ์ ๊ตฌ์กฐ๋ก ํ ์คํธ๋ฅผ ์ค๊ณํ์ต๋๋ค.
ToastContainer์ ToastItem์ ๊ทธ๋ฅ ์ปดํฌ๋ํธ๊ฐ ๋ถ๋ชจ ์์ ๊ด๊ณ๋ผ์ ์ต์ํ์ง๋ง eventManger๊ฐ ์ฝ๊ฐ ์์ํ ํ
๋ฐ์. eventManger๋ฅผ ๋์
ํ ์ด์ ๋ ์ฌ์ฉ์ฒ์ ์๊ด ์์ด toast.info("ํ ์คํธ์ ๋ด์ฉ")
๊ณผ ๊ฐ์ ๋ฌธ๋ฒ์ผ๋ก ํ๋ฉด์ ๋๋๋ง๋๋ ์์๋ฅผ ๊ด๋ฆฌํ๊ธฐ ์ํจ์
๋๋ค.
ToastContainer์์ ํ ์คํธ์ ์ถ๊ฐ, ์ญ์ ์ ๋ํ ์ด๋ฒคํธ๋ฅผ ๊ตฌ๋ ํ๊ณ ์๊ธฐ ๋๋ฌธ์ ํ๋ก์ ํธ์ ์ด๋ ๊ณณ์์๋ toast ์ด๋ฒคํธ dispatch ํจ์๋ฅผ ์คํํด์ UI๋ฅผ ์ ์ดํ ์ ์๊ฒ ๋๋๊ฒ์ด์ฃ .
import { EventManager } from '@foundation/Toast/type';
export const eventManager: EventManager = {
list: new Map(), // ์ด๋ฒคํธ ๋ฆฌ์ค๋ ์ ์ฅ
emitQueue: new Map(), // ์ด๋ฒคํธ ์ง์ฐ์ ์ํ ํ
// ์๋ก์ด ์ด๋ฒคํธ ๋ฆฌ์ค๋ ๋ฑ๋ก
on(event, callback) {
this.list.has(event)
? this.list.get(event)!.push(callback)
: this.list.set(event, [callback]);
return this;
},
// ์ฝ๋ฐฑ์ด ์๋๊ฒฝ์ฐ ํด๋นํ๋ ๋ฆฌ์ค๋ ์ ๊ฑฐ, ์๋ ๊ฒฝ์ฐ ์ด๋ฒคํธ์ ๋ํ ๋ชจ๋ ๋ฆฌ์ค๋ ์ ๊ฑฐ
off(event, callback) {
if (callback) {
const cb = this.list.get(event)?.filter((cb) => cb !== callback);
cb && this.list.set(event, cb);
return this;
}
this.list.delete(event);
return this;
},
//๋๊ธฐ์ค์ธ ์ด๋ฒคํธ๋ฅผ ์ทจ์์ํฌ ๋ ํ์ํจ
cancelEmit(event) {
const timers = this.emitQueue.get(event);
if (timers) {
timers.forEach(clearTimeout);
this.emitQueue.delete(event);
}
return this;
},
// ์ด๋ฒคํธ ๋ฐ์์ํค๊ธฐ
// ํ์
๋ฌธ์ ๋ ์ผ๋จ ๋ณด๋ฅ์ค! (๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ด๋ถ๋ ํด๊ฒฐ ์๋์ด์์)
emit(event, ...args: never[]) {
this.list.has(event) &&
this.list.get(event)!.forEach((callback) => {
const timer = setTimeout(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
callback(...args);
}, 0);
this.emitQueue.has(event)
? this.emitQueue.get(event)!.push(timer)
: this.emitQueue.set(event, [timer]);
});
},
};
ํ ์คํธ ์ด๋ฒคํธ๋ฅผ ๊ด๋ฆฌํ๊ธฐ ์ํด ์์๊ฐ์ eventManger๋ฅผ ๊ตฌํํ์ต๋๋ค. on, off ๋ถ๋ถ์ ์ผ๋ฐ์ ์ธ ์ด๋ฒคํธ ๋งค๋์ ๊ตฌ์กฐ์ ๋์ผํ๋ ์ค๋ช
ํ์ง ์๊ณ ๋์ด๊ฐ๊ฒ ์ต๋๋ค.
์ฌ๊ธฐ์ ์ค์ฌ์ ์ผ๋ก ๋ด์ผํ ๊ฒ์ emitQueue
์
๋๋ค. ์ด๋ฏธ list์์ ์ด๋ฒคํธ๋ฅผ ๊ด๋ฆฌํ๊ณ ์๋๋ฐ ์ ๋ณ๋์ emitQueue
๊ฐ ํ์ํ ๊น์? ๋ฐ๋ก ์ด๋ฒคํธ์ ์ฝ๋ฐฑํจ์๊ฐ ๋๊ธฐํจ์๊ฐ ์๋ ๊ฒฝ์ฐ๋ฅผ ๋๋นํ๊ธฐ ์ํจ์
๋๋ค.
emit์ผ๋ก ์ด๋ฒคํธ dispatch์ ์ด๋ฒคํธ list์์ ์ฝ๋ฐฑํจ์๋ฅผ setTimeout
์ ๋ฃ์ด์ ๋น๋๊ธฐ์ ์ผ๋ก ์คํํ๊ณ ์์ต๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ์ด๋ฒคํธ ๋ฃจํ ๋ด์์ ์ฝ๋ฐฑ ํจ์๋ฅผ ๋ค์ ์ฃผ๊ธฐ๋ก ๋ฐ์ด๋ด์ ํ์ฌ ์คํ์ค์ธ ๋๊ธฐ ์์
์ด ๋ชจ๋ ์ข
๋ฃ๋ ์ดํ์ ์ฝ๋ฐฑ์ด ์คํ๋ ์ ์๋๋ก ํฉ๋๋ค. ์ด ๋ emitQueue
๋ฅผ ์ฌ์ฉํด์ ํ์ด๋จธ์ ๋ํ ์ ๋ณด๋ฅผ ๋ณ๋์ ํ๋ก ๊ด๋ฆฌํ๊ณ , cancelEmit
์์ ๋๊ธฐ์ค์ธ ์ด๋ฒคํธ๋ฅผ ์ทจ์ํ ์ ์์ต๋๋ค.
์์ง์ ํ ์คํธ๋ฅผ ๋์ฐ๊ณ ์ ๊ฑฐํ ๋ ๋๊ธฐ ํจ์๋ง ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ emitQueue
์ cancelEmit
์ ํ์ํ์ง ์์ต๋๋ค. ํ์ง๋ง ์ถํ ํ ์คํธ์์ Promise์ ์ํ๋ฅผ ํ์ํ๋ ๊ธฐํ์ด ์ถ๊ฐ๋ ๊ฒ์ ์ผ๋ํ๊ณ ์๊ธฐ ๋๋ฌธ์ ์ด ๋ถ๋ถ์ ์ถ๊ฐํ์ต๋๋ค.
ToastContainer์์๋ ์๋ useToastContainer
ํ
์ ์ฌ์ฉํด ํ ์คํธ์ ์์ฑ, ์ญ์ ์ด๋ฒคํธ๋ฅผ ๊ตฌ๋
ํด ์ด์ ๋ง๋ ๋์์ ์ํํ ์ ์๋๋ก ํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ toastList๋ฅผ ํตํด ํ์ฌ ํ๋ฉด์ ๋์์ง ํ ์คํธ๋ฅผ ๊ด๋ฆฌํฉ๋๋ค.
const useToastContainer = () => {
const [toastList, setToastList] = useState(new Map<string, ToastProps>());
// ํ ์คํธ ์ถ๊ฐ
const addToast = (props: ToastProps) => {
setToastList((prev) => new Map(prev).set(props.toastId, props));
};
// ํ ์คํธ ์ญ์
const deleteToast = (id: string) => {
setToastList((prev) => {
const newMap = new Map(prev);
newMap.delete(id);
return newMap;
});
};
useEffect(() => {
eventManager.on(ToastEvent.Add, addToast);
eventManager.on(ToastEvent.Delete, deleteToast);
// ์ปดํฌ๋ํธ ์ธ๋ง์ดํธ ์ ๋ฆฌ์ค๋ ํด์
return () => {
eventManager.off(ToastEvent.Add, addToast);
eventManager.off(ToastEvent.Delete, deleteToast);
};
}, []);
const toastListToArray = () => {
return Array.from(toastList);
};
const getToastPositionGroupToRender = () => {
const list = toastListToArray();
const positionGroup = new Map<ToastPosition, ToastProps[]>();
list.forEach(([_, toastProps]) => {
const position = toastProps.position || 'topRight';
positionGroup.has(position)
? positionGroup.get(position)!.push(toastProps)
: positionGroup.set(position, [toastProps]);
});
return positionGroup;
};
return { getToastPositionGroupToRender };
};
export default useToastContainer;
export const ToastContainer = () => {
const { getToastPositionGroupToRender } = useToastContainer();
const positionGroup = getToastPositionGroupToRender();
return Array.from(positionGroup).map(([position, toasts]) => (
<div
key={position}
css={[
css`
position: fixed;
display: flex;
flex-direction: column;
row-gap: 0.5rem;
z-index: 9999;
`,
ToastPositionStyle[position],
]} >
{toasts.map((toastProps) => (
<ToastItem key={toastProps.toastId} {...toastProps} />
))}
</div>
));
};
ํ ์คํธ ์ปจํ ์ด๋์์๋ ์ ํ ์ ์ฌ์ฉํด์ ํ์ฌ ๋๋๋งํด์ผํ๋ ํ ์คํธ์ ์ ๋ณด๋ฅผ position๋ณ Map ํํ๋ก ๋ฐ์์ต๋๋ค. ๊ทธ๋ฆฌ๊ณ ๊ฐ ํฌ์ง์ ์ ๋ง๋ ์ปจํ ์ด๋ ๋ฐ์ค๋ฅผ ์์ฑํด์ ์๋ง์ ์์น์ ToastItem์ ๋์์ค๋๋ค.
ํ ์คํธ๋ ์ฌ์ฉ์์๊ฒ ๊ฐ๋จํ ์ ๋ณด๋ฅผ ํ์ํด์ฃผ๊ธฐ ์ํ UI๋ก ์ผ์ ์๊ฐ์ด ์ง๋๋ฉด ์ฌ๋ผ์ ธ์ผ ํฉ๋๋ค.
์ด ๊ธฐ๋ฅ์ ๊ตฌํํ๊ธฐ ์ํ ์ ํ์ง๋ ๋ ๊ฐ์ง๊ฐ ์๋๋ฐ์. ์ฒซ ๋ฒ์งธ๋ setTimeout
์ ์ฌ์ฉํด ์ผ์ ์๊ฐ ์ดํ ํ ์คํธ๊ฐ ๋์์ ์ ๊ฑฐ๋๋๋ก ํ๋ ๊ฒ์ด๊ณ , ๋ ๋ฒ์งธ๋ animationend ์ด๋ฒคํธ๋ฅผ ํ์ฉํ๋ ๊ฒ ์
๋๋ค.
์ด ์ค์ ์ ๋ animationend
์ด๋ฒคํธ๋ฅผ ํ์ฉํ๋ ๋ฐฉ์์ ์ ํํ์ต๋๋ค. toast ํ๋จ์ ๋ํ๋๋ ํ๋ก๊ทธ๋์ค๋ฐ๋ฅผ ํ์ํ๊ธฐ ์ํด์๋ ์ด์ ๋ํ ์ ๋๋ฉ์ด์
์ ์ผ์ ์๊ฐ๋งํผ ๋ณด์ฌ์ค์ผํฉ๋๋ค. ๋ฐ๋ผ์ ๋ณ๋์ ํ์ด๋จธ๋ฅผ ๋๋ ๊ฒ ๋ณด๋ค ํ๋์ ์ ๋๋ฉ์ด์
์๊ฐ์ ์ข
์์ฑ๋๋๋ก ๊ตฌํํ๋ ๊ฒ์ด ๋ ํจ์จ์ ์ด๋ผ๊ณ ์๊ฐํ๊ธฐ ๋๋ฌธ์
๋๋ค.
์๋ ์ฝ๋๋ ์ค๋ช ์ ์ํด ํ์ํ ๋ถ๋ถ๋ง ๋จ๊ธฐ๊ณ ํ ์คํธ๋ฅผ ๊ฐ์ํํ ์ฝ๋์ ๋๋ค.
export const ToastProgressBarAnimation = keyframes`
from {
transform: scaleX(1);
}
to {
transform: scaleX(0);
}
`;
const ToastItem: React.FC<ToastProps> = ({
toastId,
text,
autoClose = 3000,
closeOnClick = true,
type = 'default',
pauseOnHover = true,
}) => {
const toastRef = useRef<HTMLDivElement>(null);
const [isExiting, setIsExiting] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const handleExitingAnimationEnd = () => {
// ๋ค์ ๋ด์ฉ์์ ์ค๋ช
};
const handleProgressAnimationEnd = () => {
autoClose && setIsExiting(true);
};
const handleClick = () => {
closeOnClick && handleProgressAnimationEnd();
};
const handleMouseEnter = () => {
pauseOnHover && autoClose && setIsPaused(true);
};
const handleMouseLeave = () => {
if (pauseOnHover && autoClose) {
setIsPaused(false);
}
};
return (
<div ref={toastRef}>
<Box
onClick={handleClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onAnimationEnd={handleExitingAnimationEnd}
css={css`
animation: ${isExiting
? css`${ToastFadeOutUpAnimation} 0.8s forwards`
: 'none'};
`}
>
{text}
<div
onAnimationEnd={handleProgressAnimationEnd}
css={css`
transform-origin: left;
animation: ${autoClose
? css`${ToastProgressBarAnimation} ${autoClose}ms linear forwards`
: 'none'};
animation-play-state: ${isPaused ? 'paused' : 'running'};
`}
/>
</Box>
</div>
);
};
export default ToastItem;
ํ ์คํธ์ ์๊ฐ์ด ๊ฒฝ๊ณผํจ์ ๋ฐ๋ผ progressBar๋ฅผ ํ์ํด์ฃผ๊ธฐ ์ํด ToastProgressBarAnimation ์ keyframe์ผ๋ก ์ถ๊ฐํ์ต๋๋ค. ToastProgressBarAnimation์ width ์์ฑ์ด ์๋๋ผ scaleX ์์ฑ์ ์ฌ์ฉํ๋๋ฐ์. width์์ฑ์ element์ ์ค์ ํฌ๊ธฐ๋ฅผ ๋ณํ์ํค๊ธฐ ๋๋ฌธ์ ์ ๋๋ฉ์ด์ ์ด ์งํ๋๋ ๋์ reflow ๊ณผ์ ์ด ๋ฐ์ํ๊ฒ ๋ฉ๋๋ค. ๋ฐ๋ฉด scaleX ์์ฑ์ transform ์์ฑ์ ์ผ๋ถ๋ก, ์ค์ ํฌ๊ธฐ๋ฅผ ๋ณํ์ํค์ง ์์ ์ฑ ๋ณํ๋ง ์ผ์ด๋ฉ๋๋ค. ๋ํ GPU ๊ฐ์์ ํ์ฉํ ์ ์๊ธฐ ๋๋ฌธ์ ์ฑ๋ฅ์ ์ผ๋ก ๋ ์ฐ์ํฉ๋๋ค.
onAnimationEnd๋ฅผ ์ฌ์ฉํด์ ์ ๋๋ฉ์ด์
์ข
๋ฃ์ ์ข
๋ฃ ์ํ๋ฅผ ๋ํ๋ด๋ isExiting ์ํ๋ฅผ true๋ก ์
๋ฐ์ดํธ ํด์คฌ์ต๋๋ค. ํ ์คํธ๋ฅผ ๋ฐ๋ก ์ ๊ฑฐํ์ง ์๊ณ isExiting
๋ฅผ ๋ฐ๋ก ๋ ์ด์ ๋ ์๋์์ ์์ธํ ์ค๋ช
ํ๊ฒ ์ง๋ง, ์์ฝํ์๋ฉด ํ ์คํธ๊ฐ ์์ํ ์ฌ๋ผ์ง๋ ์ ๋๋ฉ์ด์
์ ์ฃผ๊ธฐ ์ํจ์
๋๋ค.
animation-play-state
์์ฑ๊ณผ onMouseEnter, onMouseLeave ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ์ฌ์ฉํด์ ํ ์คํธ์ ๋ง์ฐ์ค ํธ๋ฒ์ ํ๋ก๊ทธ๋์ค๋ฐ ์ ๋๋ฉ์ด์
์ด ์ ์งํ๋๋ก ๊ตฌํํ์ต๋๋ค.
ํ ์คํธ์ ์ง์์๊ฐ์ด ์ข ๋ฃ๋์์ ๋ ๊ฐ์๊ธฐ ์ปดํฌ๋ํธ๊ฐ "ํ~" ์ฌ๋ผ์ ธ๋ฒ๋ฆฌ๋ฉด ๋ถ์์ฐ์ค๋ฝ๊ณ ์ข์ง ์์ ์ฌ์ฉ์ ๊ฒฝํ์ ์ ๊ณตํ๋ค๊ณ ์๊ฐํฉ๋๋ค. ๊ทธ๋์ ํ ์คํธ๊ฐ ์์ฐ์ค๋ฝ๊ฒ "์ค์ค์ฅ~" ํ๋ฉฐ ์ฌ๋ผ์ง ์ ์๋๋ก keyframe์ ์ฌ์ฉํด์fade-out ์ ๋๋ฉ์ด์ ์ ๋ฃ์์ต๋๋ค.
animation: ${isExiting
? css`${ToastFadeOutUpAnimation} 0.8s forwards`
: 'none'};
ํ๋ก๊ทธ๋์ค๋ฐ ์ ๋๋ฉ์ด์
์ด ์ข
๋ฃ๋๋ฉด isExiting
์ํ๊ฐ true๊ฐ ๋๊ณ , ToastFadeOutUpAnimation ์ ๋๋ฉ์ด์
์ด ์คํ๋๊ฒ ๋ฉ๋๋ค. ์ด ์ ๋๋ฉ์ด์
์ ์๋์ ๊ฐ์ด ๊ตฌ์ฑ๋์ด์์ต๋๋ค.
export const ToastFadeOutUpAnimation = keyframes`
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-1.5rem);
}
`;
// ์ ์ ๋๋ฉ์ด์
์ ๋ํ animationend ์ด๋ฒคํธ ์ฝ๋ฐฑํจ์
const handleExitingAnimationEnd = () => {
eventManager.emit(ToastEvent.Delete, toastId);
};
ํ ์คํธ ์์ดํ
์ด ์ ์ ํฌ๋ช
ํด์ง๋ฉด์ ์๋ก ์ฌ๋ผ์ง๋๋ก fade-out ์ ๋๋ฉ์ด์
์ ๋ฃ์์ต๋๋ค..
๊ทธ๋ฆฌ๊ณ ์ด ์ ๋๋ฉ์ด์
์ ๋ํ animationend
์ด๋ฒคํธ๊ฐ ๊ฐ์ง๋๋ฉด ์ค์ ๋์์๋ toast๊ฐ ์ ๊ฑฐ๋๋๋ก ๊ตฌํํ๋๋ฐ์. ์ฌ๊ธฐ์ ์ด์ง ์ด์๊ฐ ์๊ฒผ์ต๋๋ค.
ํ์ฌ ๊ฐ position์ ๋ฐ๋ฅธ toastContainer์ ์์น๋ fixed ์์ฑ์ผ๋ก ๊ด๋ฆฌ๋์ง๋ง, ๋ด๋ถ์ ์๋ ํ ์คํธ๋ค์ ๋ฆฌ์คํธ๋ flexbox ์์ฑ์ผ๋ก ๊ด๋ฆฌ๋๊ณ ์์ต๋๋ค. top ์์ฑ์ ๊ฐ์ง ํ ์คํธ๋ค์ ๋ฐฐ์ด์์๋ ์๊ฐ ์์์ ๋ฐ๋ผ ์์ชฝ์ ์๋ toast๊ฐ ๋จผ์ ์ ๊ฑฐ๋๋๋ฐ์. ์ด ๋๋ฌธ์ fade-out ์ดํ DOM์์ ํ ์คํธ ์ ๊ฑฐ์ flexbox์ ๋์ด๊ฐ ์ค์ด๋ค๋ฉด์ ๋ ๋ ํธ๋ฆฌ์ ๊ฐ ์์์ ํฌ๊ธฐ์ ์์น๋ฅผ ๊ณ์ฐํ๋ reflow๊ณผ์ ์ด ์ผ์ด๋๊ฒ ๋ฉ๋๋ค. ๋๋ฌธ์ ํ ์คํธ์ ๋์ด๊ฐ ๋ณํ๋ ๋์๊ณผ ์ ๋๋ฉ์ด์ ์ด ํฉ์ณ์ ธ์ ์ ๋ฐ ๋๋ ๋๊ธฐ๋ ํ์์ด ๋ฐ์ํ๊ฒ ๋์์ฃ .
์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด requestAnimationFrame
์ ๋์
ํ์ต๋๋ค. requestAnimationFrame
์ ๋ธ๋ผ์ฐ์ ์ ๋ ๋๋ง ์ฌ์ดํด์ ๋ง์ถฐ ํจ์๋ฅผ ์คํํด์ ๋ ์ด์์ ๊ณ์ฐ์ด๋ DOM ์กฐ์ ์ฐ์ฐ์ ํจ์จ์ ์ผ๋ก ์ํํ ์ ์๊ฒ ๋์์ค๋๋ค.
์๋์ ๊ฐ์ ๋ฐฉ์์ผ๋ก requestAnimationFrame
ํจ์๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
const handleAnimationEnd = () => {
requestAnimationFrame(() =>
eventManager.emit(ToastEvent.Delete, toastId)
);
};
requestAnimationFrame
์ ์ ์ฉํ์์๋ ๋ถ๊ตฌํ๊ณ ์ฌ์ ํ ๋๋ ๋๊ธฐ๋ ๋ฏํ ๋๋์ด ์ฌ๋ผ์ง์ง ์์์ต๋๋ค.
์๋ํ๋ฉด ๊ทผ๋ณธ์ ์ผ๋ก flexbox์ ์ฌ์ด์ฆ๊ฐ ์ค์ด๋ค๋ฉด์ ํ ์คํธ ์์น๊ฐ ์๋ก ์ฌ๋ผ๊ฐ๋ค๋ ์ฌ์ค์ ๋ณํ์ง ์์๊ธฐ ๋๋ฌธ์ธ๋ฐ์.
export function collapseToast(
node: HTMLDivElement,
done: () => void,
duration = 20000
) {
const { scrollHeight, style } = node;
requestAnimationFrame(() => {
style.height = scrollHeight + 'px';
style.transition = `all ${duration}ms`;
requestAnimationFrame(() => {
style.height = '0';
setTimeout(done, duration);
});
});
}
์ ์ ํธ์ ์์ฑํด์, ํ ์คํธ์ ๋์ด๋ฅผ ๋ณ๊ฒฝํ๋ ๋์๋ ์ ๋๋ฉ์ด์
์ผ๋ก ์ฒ๋ฆฌ๋๋๋ก ๊ตฌํํ์ต๋๋ค. height ์์ฑ์ requestAnimationFrame
์ ํตํด ์ ์ฉํ๊ธฐ ๋๋ฌธ์ ๋ธ๋ผ์ฐ์ ๋๋๋ง ์ฌ์ดํด์ ๋ง๊ฒ ์ค๋ฌด์คํ ๋ฐ์ค ์ฌ์ด์ฆ ๋ณ๊ฒฝ์ด ์ผ์ด๋ฉ๋๋ค.
const handleExitingAnimationEnd = () => {
collapseToast(toastRef.current!, () => {
eventManager.emit(ToastEvent.Delete, toastId);
});
};
handleExitingAnimationEnd
ํจ์์ collapseToast
์ ํธ์ ์ ์ฉํ ๋ชจ์ต์
๋๋ค.
ํ๋ฒ ๋ ์์ฝํ์๋ฉด ์๋์ ๊ฐ์ ๊ณผ์ ์ด ์ผ์ด๋์ ์ ๊ฑฐ ์ ๋๋ฉ์ด์ ์ด ๋ถ๋๋ฝ๊ฒ ๋ณํฉ๋๋ค.
const generateUniqueId = () => {
return Date.now().toString(36) + Math.random().toString(36).substring(4);
};
const emitToast = (type: ToastType, toastProps: ToastFunctionProps) => {
const id = generateUniqueId();
eventManager.emit(ToastEvent.Add, {
...toastProps,
toastId: id,
type,
});
};
export const toast = {
default: (text: string, toastOptions?: ToastOptions) =>
emitToast('default', { text: text, ...toastOptions }),
info: (text: string, toastOptions?: ToastOptions) =>
emitToast('info', { text: text, ...toastOptions }),
success: (text: string, toastOptions?: ToastOptions) =>
emitToast('success', { text: text, ...toastOptions }),
warning: (text: string, toastOptions?: ToastOptions) =>
emitToast('warning', { text: text, ...toastOptions }),
error: (text: string, toastOptions?: ToastOptions) =>
emitToast('error', { text: text, ...toastOptions }),
};
์ด์ ํ ์คํธ๋ฅผ console.log
์ ๊ฐ์ ํ์์ผ๋ก ํธ์ถํ๋ ํจ์๋ฅผ ๋ง๋๋ ์ผ๋ง ๋จ์์ต๋๋ค. ์ด๋ฅผ ์ํด toast ์ ํธ ํจ์๋ฅผ ๊ฐ์ฒด ํํ๋ก ์ ์ธํ๊ณ , ๊ฐ ํ์
์ ๋ง๋ ํจ์๋ฅผ ์์ฑํ์ต๋๋ค.
์ด์ ์๋์ ๊ฐ์ ํํ๋ก ํ ์คํธ๋ฅผ ํธ์ถํ ์ ์์ต๋๋ค.
toast.defualt("ํ ์คํธ์ ๋ค์ด๊ฐ ๋ด์ฉ์
๋๋ค");
toast.info("ํ ์คํธ์ ๋ค์ด๊ฐ ๋ด์ฉ์
๋๋ค");
toast.success("ํ ์คํธ์ ๋ค์ด๊ฐ ๋ด์ฉ์
๋๋ค");
toast.warning("ํ ์คํธ์ ๋ค์ด๊ฐ ๋ด์ฉ์
๋๋ค");
toast.error("ํ ์คํธ์ ๋ค์ด๊ฐ ๋ด์ฉ์
๋๋ค");
์์ชฝ๋ถํฐ ์ฐจ๋ก๋๋ก default, info, success, warning, error ์ ๋ํ ๋์์ธ์ ๋๋ค.
ํ ์คํธ ์ถ๋ ฅ์ ๋๋ฒ์งธ ์ธ์๋ก ์ต์ ๊ฐ์ฒด๋ฅผ ๋๊ธธ ์ ์์ต๋๋ค. ์ต์ ์๋ ๋ค์๊ณผ ๊ฐ์ ์ค์ ์ด ์์ต๋๋ค.
export type ToastOption = {
autoClose?: false | number;
closeOnClick?: boolean;
pauseOnHover?: boolean;
position?: ToastPosition;
};
autoClose
: ํ ์คํธ๊ฐ ์๋์ผ๋ก ๋ซํ์ง ์ฌ๋ถ. ๊ธฐ๋ณธ๊ฐ์ 3์ด, false๋ฅผ ๋ฃ์ผ๋ฉด ํด๋ฆญํ ๋ ๊น์ง ํ ์คํธ๊ฐ ๋ซํ์ง ์์ต๋๋ค.closeOnClick
: ํด๋ฆญํด์ ํ ์คํธ๋ฅผ ๋ซ์ ์ ์๋์ง ์ฌ๋ถ์
๋๋ค.pauseOnHover
: ํ ์คํธ์ ๋ง์ฐ์ค ํธ๋ฒ์ ํ ์คํธ๊ฐ ๋ฉ์ถ์ง ์ฌ๋ถ์
๋๋ค.position
: ํ ์คํธ์ ์์น๋ฅผ ๊ฒฐ์ ํฉ๋๋ค. (topLeft, topRight, topCenter, bottomLeft, bottomRight, bottomCenter)๊ฐ ์์ต๋๋ค.ํ ์คํธ๋ฅผ console.log
์ฒ๋ผ ์ฌ์ฉํ๊ธฐ ์ํด react-toastify ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋ด๋ถ ์ฝ๋๋ฅผ ๋ถ์ํ๋ฉฐ ์ ํ ์คํธ๋ฅผ ๊ตฌํํ์ต๋๋ค. ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ด๋ถ๋ฅผ ์ฒ์ ์ด์ด๋ดค์ ๋ ๊ฐ์๊ธฐ ๋ฑ์ฅํ eventManger๋ก ์ธํด ์์ฒญ ์ด๋ ต๊ฒ๋ง ๋๊ปด์ก๋๋ฐ์. ์ eventManger๋ฅผ ์ฌ์ฉํ์ด์ผ ํ๋๊ฐ์ ๋ํด์ ์ค์ ์ ์ผ๋ก ์ฝ๋๋ฅผ ๋ถ์ํด๋ณด๋ ์๊ฐ๋ณด๋ค ์ด๋ ต์ง ์์ ๊ฒ์ ์ ์ ์์์ต๋๋ค. eventManger๋ฅผ ํตํด ํ ์คํธ์ UI๋ฅผ ๊ด๋ฆฌํ๋ ๊ฒ์ pub/sub ํจํด์ด๋ผ๋ ๊ฒ์ ๊นจ๋ซ๊ณ ๋ ์ดํ๋ถํฐ๋ ์ฝ๋๊ฐ ์ ์ ์ฝํ๊ธฐ ์์ํ์ต๋๋ค. ๋๋ถ์ ์ด๋ ๊ฒ ์ฌ์ฉํ๊ธฐ ํธ๋ฆฌํ๋ฉด์๋ ์ด์ ํ ์คํธ๋ฅผ ๊ตฌํํ ์ ์์์ต๋๋ค.
์์ง UX๋ฅผ ๋ค๋ฌ๊ธฐ ์ ์ด๋ผ toast๊ฐ ์ ๊ทน์ ์ผ๋ก ์ฌ์ฉ๋๊ณ ์์ง ์์์ ํ ์คํธ์ ์ฌ์ฉ ๊ฒฝํ์ ๋ํ ํผ๋๋ฐฑ์ ๋ฃ์ง ๋ชปํ ์ํฉ์ธ๋ฐ์. ํ ์คํธ๋ฅผ ์ ๊น ์ฌ์ฉํด๋ณด์ ์ฑ์ธ๋ ์์ฃผ ๊ทน์ฐฌ์ ํด์ฃผ์ ์ ๊ต์ฅํ ๋ฟ๋ฏํฉ๋๋ค ใ ใ
์ถํ ํ๋ก์ ํธ์์ toast๊ฐ ์ ๊ทน์ ์ผ๋ก ์ฌ์ฉ๋ ๋ ํ ์คํธ์ ๋ํ ํ๊ธฐ์ ๋ํด ๋ ๋จ๊ฒจ๋ณด๋๋ก ํ ๊ฒ์!
์ด ํ ์คํธ ์ฝ๋๋ ์๋ PR์์ ํ์ธํ ์ ์์ต๋๋ค. gomterview
React-toastify | React-Toastify fkhadra/react-toastify: React notification made easy ๐ ! requestAnimationFrame ํ์ฉ (์)