DEVELOP
article thumbnail

구현 결과

Recoil으로 전역으로 상태를 관리하며 위처럼 토스트 메시지 띄우는 작업을 커스텀 훅을 만들어 구현해보고자 한다.

타임아웃을 기본값으로 3초를 설정하여, 생성 3초 후 자동으로 없어지도록 하고, 이 타임아웃 값은 메시지 별로 설정 가능하다.

Recoil + 커스텀 훅으로 토스트 메시지를 구현한 이유는

  1. 토스트 컴포넌트를 사용할 때마다 직접 추가하지 않아도 된다. (App 파일 에서만 추가 )
  2. 모든 페이지에서 사용가능성이 있다. (중복 최소화)
  3. 페이지가 이동되어도 메시지들이 유지되어야 한다. (Recoil 로 전역관리 )

구현할 것들은

  1. 토스트 메시지 타입 선언 (ToastMessage.ts)
  2. 토스트 메시지 전역 상태 관리 변수 (ToastMessageState.ts)
  3. 토스트 메시지 아이템 컴포넌트 (ToastMessageItem.tsx)
  4. 토스트 메시지 컨테이너 컴포넌트 (항상 상위에 유지, ToastMessageContainer.tsx)
  5. 토스트 메시지를 추가하고, 삭제하는 함수를 포함하고 있는 커스텀 훅 (useToast.tsx)

토스트 메시지 타입

▼ ToastMessage.ts

export default interface ToastMessage {
  id?: number;
  content: string;
  type?: "default" | "success" | "error";
  timeout?: number;
}
  1. id
    1. 3초 후 메시지를 삭제할 때 해당 메시지를 판별하기 위한 고유값으로서, 생성 시에 자동으로 Date.now()값(타임스탬프)을 대입하여 고유하도록 지정한다.
  2. content
    1. 메시지에 포함할 내용을 의미한다.
  3. type
    1. default , success, error 중 하나로, 각 type에 대한 스타일을 구분하기 위한 속성이다.
  4. timeout
    1. 메시지가 사라질 시간(ms)를 의미한다.

토스트 메시지 전역 상태 관리 변수 (Recoil)

먼저, recoil이 설치되어있지 않다면, 설치해야 한다.

$ yarn add recoil 

루트 파일인 main.tsx에서 App을 RecoilRoot로 감싸준다.

▼ main.tsx

<React.StrictMode>
  <QueryClientProvider client={queryClient}>
    <BrowserRouter>
      <RecoilRoot>
        <App /> /* RecoilRoot로 App 컴포넌트 감싸주기 */
      </RecoilRoot>
    </BrowserRouter>
  </QueryClientProvider>
</React.StrictMode>

토스트 메시지 state를 저장하는 atom 을 정의한다.

▼ src/recoil/atom/ToastMessageState.ts

import { atom } from "recoil";
import ToastMessage from "../../types/ToastMessage";

export const toastMessageState = atom<ToastMessage[]>({
  key: "toastMessagesState",
  default: [],
});

해당 atom을 조회하는 key값을 toastMessageState로 지정하고,

기본 값으로는 메시지가 하나도 없을 경우인 빈 배열을 지정한다.

이제 아래처럼, 키 값으로 지정한 toastMessageState으로 전역 변수의 상태값을 사용하고, 업데이트할 수 있다.

const toasts = useRecoilValue(toastMessageState); // 상태 
const setToasts = useSetRecoilState(toastMessageState); // 상태 업데이트 

토스트 메시지 아이템 컴포넌트

▼ src/components/Toast/ToastMessageItem.tsx

import ToastMessage from "../../../types/ToastMessage";

const ToastMessageItem = ({ type, content }: ToastMessage) => {
  return (
    <div
      className={`my-1 w-[20vw] rounded-md flex items-center text-center px-4 py-2 justify-center text-sm ${
        type == "success"
          ? "bg-primary-green text-white "
          : type == "error"
          ? "bg-error-red text-white "
          : "bg-black text-white"
      }`}
    >
      {content}
    </div>
  );
};

export default ToastMessageItem;

type에 따라 원하는 대로 스타일을 다르게 지정해주고,

content 내용을 띄운다 .

토스트 메시지 컨테이너 컴포넌트

▼ src/component/Toast/ToastMessageContainer.tsx

import { useRecoilValue } from "recoil";
import { toastMessageState } from "../../../recoil/atom/ToastMessageState";
import ToastMessageItem from "./ToastMessageItem";

const ToastMessageContainer = () => {
  const toasts = useRecoilValue(toastMessageState); // 전역 토스트 상태 가져오기

  return (
    <div className="fixed top-0 left-1/2 -translate-x-1/2 z-10">
      {toasts &&
        toasts.map(({ content, type, id }) => (
          <ToastMessageItem content={content} type={type} key={"toast" + id} />
        ))}
    </div>
  );
};

export default ToastMessageContainer;

여기서 Recoil을 이용해 저장한 메시지의 전역 상태값을 사용한다.

const toasts = useRecoilValue(toastMessageState);

해당 toasts가 빈 배열이 아니면, map으로 ToastMessageItem 컴포넌트를 호출한다.

가장 상단 가운데에 고정되도록 스타일을 지정해준다. (fixed top-0 left-1/2 -translate-x-1/2)

그리고 이 토스트 컨테이너를 모든 페이지에 적용하기 위해 App에서 호출한다. (다른 코드는 생략 )

▼ App.tsx

/* ... */
import ToastMessageContainer from "./components/common/Toast/ToastMessageContainer";

function App() {
  return (
  /* ... */
   <ToastMessageContainer />
    /* ... */
  );
}

export default App;

토스트 커스텀 훅

▼ src/hooks/useToast.tsx

import { useSetRecoilState } from "recoil";
import { toastMessageState } from "../recoil/atom/ToastMessageState";
import ToastMessage from "../types/ToastMessage";

export function useToast() {
  const setToasts = useSetRecoilState(toastMessageState);

  const addToast = ({
    content,
    type = "default",
    timeout = 3000,
  }: ToastMessage) => {
    const newToast = { id: Date.now(), content, type };
    setToasts((prev) => [...prev, newToast]);

    setTimeout(() => {
      removeToast(newToast.id);
    }, timeout);
  };

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

  return { addToast, removeToast };
}

setToast

토스트 상태를 업데이트하는 setToasts 를 가져온다.

const setToasts = useSetRecoilState(toastMessageState);

addToast

  • 토스트 메시지를 추가하는 함수로서, 필요한 곳에서 이 함수를 호출함으로써 메시지를 띄울 수 있다.
  • type은 “default”가 기본값이 되도록, timeout은 3000(3초)가 기본 값이 되도록 지정한다.

type = "default", timeout = 3000

newToast

  • id 값은 Date.now() ( 타임스탬프) 값을 지정해주어 고유한 값이 되도록 하고, 메시지의 내용이 되는 넘겨받은 content와 메시지의 스타일을 결정하는 type을 지정해준다.

const newToast = { id: Date.now(), content, type };

setToasts((prev) => [...prev, newToast]);

: newToast를 toast state에 추가하도록 setToasts를 호출하고, newToast를 추가한 배열을 넘겨주어 상태를 업데이트한다.

setTimeout(() => {
  removeToast(newToast.id);
}, timeout);

: timeout값이 되면 해당 메시지가 삭제되도록 하기 위해 timeout이 종료되면 removeToast를 호출하고, 해당 토스트의 아이디를 넘겨준다.

removeToast

  • 넘겨받은 id 값에 해당하는 메시지를 삭제한 (filter) 메시지 배열로 state를 업데이트한다.

토스트 메시지 추가 함수 호출

이제 토스트 메시지를 생성하는 함수 addToast로 토스트를 생성할 수 있다.

임의로, 어떤 버튼을 눌렀을 때, "Toast Message! " + new Date().toLocaleTimeString() 을 content로 하도록 addToast를 호출해본다.

addToast({
  content: "Toast Message! " + new Date().toLocaleTimeString(),
  type: "success",
});

아래처럼, 버튼을 클릭할 때마다 메시지가 생기고, 3초 후에 해당 메시지가 사라지는 것을 확인할 수 있다!

profile

DEVELOP

@JUNGY00N