Toast 라이브러리 없이 구현해보기 😉

박희수·2024년 1월 25일
0
post-thumbnail

본문으로 들어가기 전 .. 🎈

🙇‍♀️ 참고한 문서 🙇‍♀️

react-tostify를 이용해 toast 기능을 구현하려던 중,
이전에 봤던 글이 떠올라 직접 만들어보게 되었다!

위 문서만 쭉쭉 따라가도 큰 어려움 없이 만들 수 있을 것이다.

📌 현재 사용하고 있는 기술 스택

  • Next.js14
  • TypeScript
  • React-Query
  • zustand
  • TailwindCSS

기존 Toast 동작 생각해보기 🤔

라이브러리 없이 구현해야하기 때문에, toast가 어떻게 동작해야 하는지 우리는 알 필요가 있다.
예를 들어 여러 게시글을 지우고 추가한다고 생각해보자.
빠르게 삭제하는 동안 toast 알림은 3-4개가 같이 띄워져 있기도 하며 약간의 시간차로 사라진다.
즉, 여러 토스트들이 한 저장소 내에서 같이 관리되고 있는 것이다.
우리는 어떤 컴포넌트에서든 배열 내에 새로운 토스트를 추가할 수 있게 zustand로 상태 관리를 해줄 것이다.

🎉 구현 순서

  1. toast를 담아줄 배열 만들기
  2. 사용자가 보게 될 Toast 컴포넌트 만들기
  3. toast를 전역으로 띄워줄 ToastProvider 만들기
  4. 이벤트 성공시, toast 추가해보기

▶ 1. toast를 담아줄 배열 만들기

toast 컴포넌트를 만들 때 필요한 요소들은 사용자가 보게될 문구(title), 성공/실패 여부(type), 배열 내의 toast들을 구분할 수 있는 고유 id 이렇게 3가지이다.

🔺 우리는 이 세가지 요소를 하나의 객체로 묶어, 이벤트 성공시 연속적으로 배열에 추가/삭제될 수 있게 만들어야 한다.

const toastStore = create<toastStore>((set) => ({
  toasts: [],
  actions: {
    addToasts: (newToast) =>
      set((prevToast) => ({
        ...prevToast,
        toasts: [...prevToast.toasts, newToast],
      })),
    removeToasts: (id) =>
      set((prevToast) => ({
        toasts: prevToast.toasts.filter((toast) => toast.id !== id),
      })),
  },
}));

toasts는 toast를 담아둘 빈 배열, 즉 초기값을 말한다.
zustand에서 actions는 여러 동작들을 담아줄 수 있고, 우리는 같은 초기값을 사용하기 때문에 toastStore라는 저장소에 여러 동작을 저장해두기로 한다.

🔺 좀 더 자세하게 코드를 살펴보자면,

    addToasts: (newToast) =>
      set((prevToast) => ({
        ...prevToast,
        toasts: [...prevToast.toasts, newToast],
      })),

zustand는 set 함수를 사용해 이전 상태와 병합해 주는데 set이 없으면 상태를 변경할 수 없다.

여기서 헷갈렸던 건 prevToast라고 했기 때문에 당연히 이전의 toasts만 가져올 줄 알았는데 toastStore 자체를 가르키고 있었다. 이건 create를 할 때 store의 타입을 toastStore로 액션이 모두 담긴 타입을 줘서 그런 것이다. 만약 타입을 다른 방식으로 줬다면 가르키는 요소가 달랐을 것이다.

어쨌든 !! 우리는 배열에 담아주는 방식을 택했기 때문에 prevToast의 toasts에 넣어준다고 명시하고 newToast(파라미터)를 추가해준다.

🔺 remove 또한 마찬가지이다.

    removeToasts: (id) =>
      set((prevToast) => ({
        toasts: prevToast.toasts.filter((toast) => toast.id !== id),
      })),

prevToasts의 toasts에 접근해 id 값으로 구분 후 삭제해준다.

▶ 2. 사용자가 보게 될 Toast 컴포넌트 만들기

컴포넌트 스타일은 참조 문서랑 거의 동일하게 했다.

 return (
    <div
      className={`mb-[5px] mt-[5px] bg-[#6E6E6E] text-white w-[225px] h-[50px]  
      flex gap-5xs justify-center items-center px-3xs py-5xs bg-gray-60 rounded-[13px] 
      mb-5xs transition-all duration-350 ease-in-out ${opacity}`}
    >
      <span className="material-icons-outlined">check_circle</span>
      <div className="ml-[7px] ">{title}</div>
    </div>
  );

여기서 type을 인자로 받아오게 되는데 type에 따라 스타일을 구분할 수 있는데
우리 구조에서는 딱히 없어서 스타일에 차이를 주진 않았다.

🔺 렌더링 코드를 보자!

export const Toast = ({ id, title, type }: ToastProps) => {
const [opacity, setOpacity] = useState('opacity-[0.2]'); 
  const { removeToasts } = useToastActions();

  useEffect(() => {
    setOpacity('opacity-[0.8]');
    const timeoutForRemove = setTimeout(() => {
      removeToasts(id);
    }, DURATION_TIME);

    const timeoutForVisible = setTimeout(() => {
      removeToasts('opacity-0');
    }, DURATION_TIME - ANIMATION_DURATION);

    return () => {
      clearTimeout(timeoutForRemove);
      clearTimeout(timeoutForVisible);
    };
  }, [id, removeToasts]);
  ...
 }

opacity를 상태관리를 함으로써, 지워질 때쯤 점점 toast가 점점 불투명에 가깝게 해주고,
1번에서 만들었던 removeToasts 액션을 가져와 setTimeout 안에서 사용해줬다.

▶ 3. Toast를 전역으로 띄워줄 ToastProvider 만들기

Toast 컴포넌트를 만들었지만, 어느 곳에서든 띄워주려면 Provider가 필요하다.
우리는 토스트 하나의 요소를 가르키는 Toast 컴포넌트를 map을 돌려 배열 내의 모든 토스트를 연속적으로 어디서든 띄워줄 수 있게할 것이다.

export const ToastProvider = () => {
  const toasts = useToasts();
 
  return (
    <>
      <div className={`fixed bottom-[82px] left-1/2 transform translate-x-[-50%]`}>
        {toasts.map((toast) => (
          <Toast key={toast.id} {...toast} />
        ))}
      </div>
    </>
  );
};

우리는 Provider를 관리하는 파일이 따로 있었기 때문에 거기에 넣어줄 것이다.
따로 provider를 관리하지 않는다면, next14환경에서는 레이아웃에 넣어주면 될 것이다!

// Providers.tsx
   <QueryClientProvider client={queryClient.current}>
      <ToastProvider />
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>

▶ 이제 직접 사용해보자!!

export const usePatchInspectionMutate = () => {
	const { addToasts } = useToastActions();
    const { mutate } = useMutation ({
    	mutationFn : ...
        onMutate : ...
        onSuccess: () => addToasts({ type: 'success', title: '검수가 완료되었습니다.', id: Date.now().toString() }),
...
}

1번에서 만들었던 addToasts를 가져왔고, 넘겨줘야 했던 type, title, id들을 넘겨주었다.
이렇게 넘겨줌으로써, 데이터 요청이 성공할 때마다 toasts 배열에 추가가 되고 있다고 볼 수 있다.

이제 구현이 다됐다! 🎉🎉🎉🎉

회사에서 참여하는 프로젝트에 넣었기 때문에 ..
아주 화면을 줄이고 녹화한 거라 화질이 많이 안 좋지만 어쨌든 성공!

도은님의 벨로그를 많이 참고했다! 감사합니당 😳
라이브러리 사용에 익숙해져 toast자체가 어떻게 구성되는 지는 생각해본 적 없었는데, 그 덕에 라이브러리 의존도도 낮추고, (물론 하나지만) 도은님 벨로그에는 atom으로 상태관리를 하고 있었기에 오랜만에 atom 사용법도 다시 찾아보고 zustand로 변경하는 과정에서도 조금 애를 썼지만 더 다양한 사용법을 알게되었다.

사실 도은님 벨로그에선 스타일을 주는 option도 전역으로 관리해주고 있는데, 나는 그걸 뺐지만 더 다양한 타입에서 스타일을 만들고 싶다면 따로 만들어보는 것도 추천한다! 👍

profile
프론트엔드 개발자입니다 :)

0개의 댓글