Toast 메시지 구현

윤병현·2024년 7월 18일
0

FeedB

목록 보기
7/10
post-thumbnail

FeedB는 개발자들이 자신이 만든 토이 프로젝트나 사이드 프로젝트를 공유하고, 다른 사람들이 피드백을 남길 수 있는 서비스입니다.

Toast 메시지 기능은 사용자 인터페이스에서 중요한 역할을 합니다. 예를 들어 사용자에게 즉각적인 피드백을 제공하여, 작업이 성공적으로 완료되었는지, 오류가 발생했는지 등을 실시간으로 알려줄 수 있습니다.

이는 사용자의 행동에 대한 명확한 결과를 전달하여 사용자 경험을 향상시킬 수 있습니다.

그래서 이번에는 Toast UI를 어떻게 구현했는지 설명해드리곘습니다.

Step 1 - ToastContext

"use client";

import { createContext, useContext, useState, ReactNode } from "react";

interface Toast {
  id: number;
  message: string;
  type: "success" | "error";
}

interface ToastContextType {
  toasts: Toast[];
  addToast: (message: string, type: "success" | "error") => void;
  removeToast: (id: number) => void;
}

const ToastContext = createContext<ToastContextType | undefined>(undefined);

export const ToastProvider = ({ children }: { children: ReactNode }) => {
  const [toasts, setToasts] = useState<Toast[]>([]);

  const addToast = (message: string, type: "success" | "error") => {
    setToasts([...toasts, { id: Date.now(), message, type }]);
  };

  const removeToast = (id: number) => {
    setToasts(toasts.filter(toast => toast.id !== id));
  };

  return <ToastContext.Provider value={{ toasts, addToast, removeToast }}>{children}</ToastContext.Provider>;
};

export const useToast = (): ToastContextType => {
  const context = useContext(ToastContext);
  if (!context) {
    throw new Error("useToast는 ToastProvider안에서 사용해주세요");
  }
  return context;
};

왜 Toast 메시지 기능을 context로 구현했는가?

1. 상태 관리의 일관성 유지

콘텍스트를 사용하면, 애플리케이션의 어떤 부분에서도 손쉽게 Toast 메시지를 추가하거나 삭제할 수 있습니다. 전역 상태를 관리하기 때문에, 상태의 일관성을 유지하고, 다양한 컴포넌트 간에 데이터 전달을 효율적으로 할 수 있습니다.

2. 페이지 이동과 무관한 상태 유지

Next.js와 같은 프레임워크에서는 페이지 간 이동이 발생할 때 상태가 초기화되는 경우가 많습니다. 하지만 콘텍스트를 사용하면, Toast 메시지와 같은 상태를 글로벌하게 관리할 수 있어, 페이지 이동 후에도 메시지 상태가 유지됩니다. 이는 사용자 경험을 향상시키는 중요한 요소입니다.

3. 컴포넌트 간의 의존성 감소

Toast 메시지를 콘텍스트로 분리함으로써, 개별 컴포넌트는 Toast 메시지를 직접 관리할 필요가 없어집니다. 대신, useToast 훅을 사용하여 필요한 곳에서 손쉽게 Toast 기능을 사용할 수 있습니다. 이는 코드의 가독성을 높이고, 컴포넌트 간의 의존성을 줄여 유지보수를 용이하게 합니다

4. 유연한 메시지 관리

addToast 함수와 removeToast 함수를 통해, 동적으로 Toast 메시지를 관리할 수 있습니다. 메시지의 추가 및 삭제가 직관적이고, 필요에 따라 메시지의 타입(성공, 오류 등)을 쉽게 설정할 수 있어, 다양한 상황에 맞는 메시지를 제공할 수 있습니다.


Step 2 - layout 적용

export const metadata: Metadata = {
...
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="ko">
      <head>
        <Script src="https://developers.kakao.com/sdk/js/kakao.js" strategy="beforeInteractive" />
      </head>
      <body>
        <div id="modal" />
        <Providers>
          <LoginProvider>
            <ToastProvider>
              <Header />
              <LoadingWrapper>
                {children}
                <ToastContainer />
              </LoadingWrapper>
            </ToastProvider>
          </LoginProvider>
        </Providers>
      </body>
    </html>
  );
}

위에서 만든 ToastProvider를 전역으로 사용할 수 있게 최상단 layout.ts 파일에 넣어주었습니다.

다음은 ToastContainer 컴포넌트에 대해서 설명드리겠습니다.


Step 3 - ToastContainer

"use client";

import React from "react";
import { useToast } from "@/app/_context/ToastContext";
import ModalPortal from "@/app/_utils/ModalPortal";
import Toast from "./Toast";

function ToastContainer() {
  const { toasts, removeToast } = useToast();

  return (
    <ModalPortal>
      <div className="fixed bottom-5 left-1/2 z-50 w-[384px] -translate-x-1/2 transform">
        {toasts.map(toast => (
          <Toast key={toast.id} message={toast.message} type={toast.type} onClose={() => removeToast(toast.id)} />
        ))}
      </div>
    </ModalPortal>
  );
}

export default ToastContainer;

ToastContainer는 이름으로 예상할 수 있듯이 Toast 메시지를 감싸주는 컴포넌트입니다.

사용자가 여러 작업을 빠르게 하다 보면 자연스럽게 Toast 메시지는 여러개가 쌓이게 될 것입니다. 그걸 원하는 위치에 띄워줄 수 있도록 ModalPortal로 감싸고 그 안에서 맵핑을 돌려 여러개에 Toast 메시지가 랜더링되게 해주었습니다.

ModalPortal은 React에서 제공해주는 createPortal를 사용해서 따로 구현해둔 컴포넌트입니다. 이걸 사용해 감싸준 이유는 Toast 메시지가 부모 컴포넌트에 스타일 영향을 받지 않도록 하기 위해서 넣어주었습니다.

Step 4 - Toast

"use client";

interface ToastProps {
  message: string;
  type: "success" | "error";
  onClose: () => void;
}

const Toast = ({ message, type, onClose }: ToastProps) => {
  const [show, setShow] = useState(false);
  const [isExiting, setIsExiting] = useState(false);

  useEffect(() => {
    // 컴포넌트가 마운트되면 표시 애니메이션을 시작합니다.
    setShow(true);

    // 3초 후에 토스트를 사라지기 시작하게 설정합니다.
    const hideTimeout = setTimeout(() => {
      setIsExiting(true); // 애니메이션 시작
    }, 3000);

    // 애니메이션이 끝난 후에 onClose 호출
    const onCloseTimeout = setTimeout(() => {
      if (isExiting) {
        onClose();
      }
    }, 3500); // 애니메이션이 끝난 후 추가로 0.5초 후 호출

    return () => {
      clearTimeout(hideTimeout);
      clearTimeout(onCloseTimeout);
    };
  }, [isExiting, onClose]);

  const backgroundColor = type === "success" ? "border-blue-500 bg-blue-100" : "border-red-500 bg-red-100";

  return (
    <div
      className={`transform transition-all duration-500 ease-in-out ${show ? (isExiting ? "-translate-y-10 scale-95 opacity-0" : "translate-y-0 scale-100 opacity-100") : "translate-y-5 scale-95 opacity-0"}  ${backgroundColor} mb-2 flex items-center gap-2 rounded-xl border border-solid p-4`}>
      {type === "success" ? (
        <Image src={checkCircle} alt="성공" width={24} priority />
      ) : (
        <Image src={errorCircle} alt="실패" width={24} priority />
      )}
      <p className="text-gray-900">{message}</p>
      <Image
        src={closeIcon}
        alt="닫기 아이콘"
        width={24}
        height={24}
        priority
        className="absolute right-4 cursor-pointer"
        onClick={onClose}
      />
    </div>
  );
};

export default Toast;

마지막으로 Toast 컴포넌트 설명해드리겠습니다.

다른 곳에 비해 상태를 많이 관리하고 있는데
const [show, setShow] = useState(false) 상태는 Toast 메시지가 화면에 보이는 여부를 관리하는 상태값 입니다.

useEffect를 이용해서 컴포넌트가 랜더링 되면서 setShow(true) 훅이 실행되고 그 다음에 Toast 메시지가 화면에 보여지게 됩니다.

show값을 처음부터 true로 주어 화면에 바로 보여지게 할 수도 있지만 그렇게 한다면 처음 등장하는 애니메이션이 제대로 작동하지 않아 위와 같은 방법으로 구현을 한겁니다.

그리고 3초뒤에는 사라지는 애니매이션이 실행되어 화면에서 사라지고, 3.5초 뒤에는 쌓여있던 Toast 메시지를 삭제하게 되면서 완전히 사라지게 됩니다.

Step 5 - 사용방법

const { addToast } = useToast();

const putReflyCommentmutation = useMutation({
    mutationFn: (comment: string) => {
      return commentApi.putReflyComment(commentId, comment);
    },
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: ["comment", "reflyList", "reflyCommentList"],
      });
      setTextValue("");
      addToast("댓글이 수정되었습니다", "success");// 작업 성공적으로 수행됐을때
    },
    onError: error => {
      console.error("Error:", error);
      addToast("댓글이 수정 오류가 발생했습니다", "error");// 작업 에러 발생했을때
    },
  });

아까 전역으로 설정해둔 context를 임포트 한 후 원하시는 곳에서 함수 호출하여 매개변수로 원하는 Toast 메시지 내용과 타입을 정해서 사용할 수 있습니다.

완성


깃허브

📌 https://github.com/Feed-B/frontend

profile
프론드엔드 개발자

0개의 댓글

관련 채용 정보