Skip to content

Toast 메시지 상태 관리를 위한 컴포넌트와 훅 구현하기

유지수 Jisoo Yoo edited this page Dec 6, 2023 · 9 revisions

문제 상황

  • API 호출 성공/실패 등의 상황에서 유저가 수행한 액션에 대해 일관성 있는 피드백 제공 필요

해결 과정

  • Toast 메시지 상태 관리를 위한 Store, Toast/Toaster 컴포넌트 및 useToast hook을 직접 구현

Toaster Store

📌 Code

🔗 Code Link

export const useToasterAtom = atom(
  (get) => get(toasterAtom).toasts,
  (get, set, { type, payload }: ToasterAction) => {
    const prevAtom = get(toasterAtom);

    switch (type) {
      case "add":
        set(toasterAtom, {
          toasts: [...prevAtom.toasts, { ...payload, id: prevAtom.index + 1 }],
          index: prevAtom.index + 1,
        });
        break;
      case "remove":
        set(toasterAtom, {
          toasts: prevAtom.toasts.filter((toast) => toast.id !== payload.id),
          index: prevAtom.index + 1,
        });
        break;
    }
  }
);

Toaster, Toast Component

📌 Code

🔗 Code Link

export default function Toaster() {
  const toasts = useAtomValue(useToasterAtom);

  return (
    <StyledToaster>
      {toasts.map((toast: ToasterType) => (
        <Toast key={toast.id} {...toast} />
      ))}
    </StyledToaster>
  );
}

function Toast({ id, type, title, message }: ToasterType) {
  const setToaster = useSetAtom(useToasterAtom);
  const [opacity, setOpacity] = useState(0);

  useEffect(() => {
    setOpacity(1);

    const removeTimeout = setTimeout(() => {
      setToaster({ type: "remove", payload: { id } });
    }, TOAST_DURATION);

    return () => {
      clearTimeout(removeTimeout);
    };
  }, [id, setToaster]);

  useEffect(() => {
    const visibilityTimeout = setTimeout(() => {
      setOpacity(0);
    }, TOAST_DURATION - ANIMATION_DURATION);

    return () => {
      clearTimeout(visibilityTimeout);
    };
  }, []);

  return (
    <StyledToast
      $opacity={opacity}
      $animationDirection={opacity === 1 ? "enter" : "exit"}>
      <div className="toast-container">
        <ToastIcon type={type} />
        <ToastText>
          <span className="toast-title">{title}</span>
          <span className="toast-message">{message}</span>
        </ToastText>
      </div>
    </StyledToast>
  );
}

useToast Hook

📌 Code

🔗 Code Link

import { useSetAtom } from "jotai";
import { useCallback } from "react";
import { ToasterInfo, useToasterAtom } from "store/toaster";

export const useToast = () => {
  const setToaster = useSetAtom(useToasterAtom);

  const toast = useCallback(
    (toasterInfo: ToasterInfo) =>
      setToaster({ type: "add", payload: { ...toasterInfo } }),
    [setToaster]
  );

  return {
    toast,
  };
};

핵심 트러블 슈팅

Toast UI Animation 효과 구현

  • 컴포넌트 라이프사이클에 따라 FadeIn, FadeOut keyframes를 추가
📌 Code
const TOAST_DURATION = 3000;
const ANIMATION_DURATION = 1000;

const fadeIn = keyframes`
  from {
    opacity: 0;
    transform: translateY(50%);
  }
  to {
    opacity: 1;
    transform: translateY(0%);
  }
`;

const fadeOut = keyframes`
  from {
    opacity: 1;
    transform: translateY(0%);
  }
  to {
    opacity: 0;
    transform: translateY(50%);
  }
`;

const StyledToast = styled.div<{
  $opacity: number;
  $animationDirection: "enter" | "exit";
}>`
  opacity: ${({ $opacity }) => $opacity};
  transition: all ${ANIMATION_DURATION}ms ease-in-out;
  animation: ${({ $animationDirection }) =>
      $animationDirection === "enter" ? fadeIn : fadeOut}
    ${ANIMATION_DURATION}ms ease-in-out;
`;