Zustand를 활용한 toast 만들기
React 프로젝트에서 Zustand로 전역 toast 알림을 직접 만든 과정. 스토어 설계와 컴포넌트 구성, 라이브러리 없이 구현하며 배운 상태 관리 패턴을 코드와 함께 정리한다.

이번 프로젝트에서 만든 toast
이번 프로젝트에서는 알림 기능을 Toast 형태로 구현해보기로 팀 내에서 결정하였습니다.
Toast 알림이란, 토스트기에서 빵이 튀어나오는 것처럼 화면에 잠깐 나타났다가 사라지는 형태의 알림입니다. 주로 간단한 성공/실패 메시지를 사용자에게 전달할 때 유용하게 사용할 수 있습니다. 모든 알림에 일일이 ‘확인’ 버튼을 눌러야 한다면, 저처럼 귀찮음을 느끼는 사용자 입장에서는 사용성을 해칠 수 있다고 생각했습니다. 😅
Toast를 사용할 수 있도록 도와주는 라이브러리는 다양합니다. 위치나 스타일 등도 자유롭게 지정할 수 있게 만들어진 라이브러리들이 많았지만, 구현 자체가 그리 어렵지 않았고, 이번 프로젝트에서는 복잡한 UI를 요구하지 않기 때문에 직접 구현해보기로 하였습니다.
🛠️ useToastStore
저희 팀이 상태 관리 라이브러리로 zustand를 선택한 데에는 여러 이유가 있지만, 가장 큰 이유는 정말 간편하다는 점이었습니다. 상태를 store 내부에서 직접 관리할 수 있어 atom 개념처럼 추가적으로 학습해야 할 내용이 없고, 아주 적은 코드만으로도 빠르게 스토어를 구성하여 사용할 수 있었습니다.
실제로 Toast의 open/close 상태를 관리하기 위해 만든 store는 아래와 같은 간단한 코드 몇 줄로 완성되었습니다. 작은 상태 하나를 관리하기 위해 복잡한 러닝커브를 감수하고 많은 보일러플레이트를 작성해야 했다면, 그냥 alert()으로 끝냈을지도 모르겠습니다.
import { Toast } from "@coworkers-types";
import { create } from "zustand";
import { devtools } from "zustand/middleware";
interface ToastState {
toast: Toast | null;
isOpen: boolean;
}
interface ToastAction {
setToast: (toast: Toast) => void;
setIsOpen: (isOpen: boolean) => void;
}
export const useToastStore = create<ToastState & ToastAction>()(
devtools((set) => ({
toast: null,
isOpen: false,
setToast: (toast: Toast | null) => {
set({ toast });
},
setIsOpen: (isOpen: boolean) => {
set({ isOpen });
},
}))
);
이 코드가 바로 toast 상태 관리를 위한 전체입니다. toast와 isOpen이라는 상태값의 초기값을 설정하고, 이를 제어할 수 있는 set 함수를 정의하였습니다.
devtools()는 zustand에서 제공하는 미들웨어로, 이를 사용하면 Redux DevTools에서 상태를 시각적으로 확인할 수 있습니다. 이 외에도 persist() 등 다양한 미들웨어가 제공되며, 로컬 스토리지를 통한 상태 저장이나 타입 추론 같은 기능도 지원하고 있어 매우 유용합니다.
🧱 Toast 컴포넌트
스토어를 만들었으니, 이제는 UI를 담당할 Toast 컴포넌트를 생성하였습니다. 이 컴포넌트는 별다른 로직 없이 UI만을 책임지도록 구성하였습니다.
import { useToastStore } from "@store/useToastStore";
import { IconToastDanger, IconToastInfo, IconToastSuccess, IconToastWarning } from "@utils/icon";
export default function Toast() {
const { toast, isOpen } = useToastStore();
return (
<>
{isOpen && (
<div className="fixed left-1/2 top-45 z-50 w-max min-w-fit -translate-x-1/2 animate-fadeIn rounded-8 border border-solid border-border-primary">
<div className="flex min-h-40 items-center rounded-8 border-solid bg-background-secondary md:min-h-50">
<span className={`min-h-40 w-5 shrink-0 rounded-l-lg md:min-h-50 ${pointColor}`} />
<div className="px-10 md:px-20">
{toast?.type === "info" ? <IconToastInfo /> : ""}
{toast?.type === "success" ? <IconToastSuccess /> : ""}
{toast?.type === "warn" ? <IconToastWarning /> : ""}
{toast?.type === "danger" ? <IconToastDanger /> : ""}
</div>
<p className="min-w-fit border border-[#aaa] pr-20 text-center text-12 font-medium leading-5 text-[#fff] md:text-16">
{toast?.content}
</p>
</div>
</div>
)}
</>
);
}
useToastStore를 통해 toast와 isOpen 상태를 가져오고, isOpen이 true일 때만 컴포넌트를 노출하도록 조건부 렌더링을 사용했습니다. toast.type에 따라 각기 다른 아이콘이 보이도록 구현했습니다. 처음에는 중첩 삼항 연산자로 작성했지만 ESLint에 의해 거부당해 😅 현재처럼 각 조건을 개별적으로 분기 처리했는데, 결과적으로 가독성 측면에서 훨씬 나은 선택이 되었습니다.
🧩 useToast 훅
사용자들이 쉽게 토스트를 호출할 수 있도록 커스텀 훅도 만들었습니다. 내부 동작을 몰라도 필요한 정보만 넘기면 사용할 수 있도록 단순한 인터페이스를 설계하려고 고민했습니다.
import { useToastStore } from "@store/useToastStore";
export const useToast = () => {
const { setToast, setIsOpen } = useToastStore();
/**
* 토스트 내용을 지정할 수 있습니다.
* @param type
* "info" - 정보를 표시하는 파란색 `i` 아이콘 사용
* "success" - 성공을 표시하는 녹색 체크 아이콘 사용
* "warn" - 경고를 표시하는 노란색 ! 아이콘 사용
* "danger" - 위험을 표시하는 빨간색 x 아이콘 사용
* @param content 알림에 표시할 메세지를 기입합니다.
*/
const toast = (type: "info" | "success" | "warn" | "danger", content: string) => {
const data = { type, content };
setToast(data);
setIsOpen(true);
setTimeout(() => {
setIsOpen(false);
}, 3000);
};
return { toast };
};
내부적으로 setToast와 setIsOpen을 호출해서 토스트를 열고, 3초 후 자동으로 닫히게 만들었습니다.
사용자는 아래와 같이 간단하게 사용할 수 있습니다
import { useToast } from "@hooks/useToast";
export default function Component() {
const { toast } = useToast();
if(success) {
toast("success", "성공했습니다");
} else {
toast("danger", "실패했습니다");
}
}
마치며
공부와 실습을 목적으로 프로젝트를 진행하다 보면 규모가 작기 때문에 zustand와 같은 상태 관리 라이브러리를 사용하는 것이 오버스펙이 아닐까 고민하게 됩니다. 실제로 이전 프로젝트에서는 그런 이유로 zustand나 tanstack-query 같은 라이브러리를 도입하지 않았던 적도 있었습니다.
하지만 이번에는 도입해서 직접 사용해보면서 느낀 점은, 도구가 주는 편리함은 경험해 봐야 안다는 것입니다. 물론 모든 프로젝트에 무조건적으로 사용하는 것은 지양해야겠지만, 작은 프로젝트라도 이러한 도구들을 적절하게 도입해보는 경험은 앞으로 더 나은 설계와 구현에 큰 도움이 될 수 있다고 생각합니다.
그래도… 오버스펙은 언제나 주의해야겠지요!
ref.
리액트 상태 관리 라이브러리, 어떤 것을 써야할까?
Zustand 핵심 정리
상태 관리 라이브러리 Zustand