개발/리액트

[리액트] snackbar에 대해서 (react-toastify, react-hot-toast, notistack)

핸디(Handy) 2022. 9. 15. 10:41

들어가며

개발을 진행하다가 웹소켓을 통해 들어온 알람을 유저에게 전달해야 하는 경우가 생겼습니다.

이때 주로 사용하는 방법이 snackbar를 통해 오른쪽 하단 또는 중앙 상단에 텍스트 창을 띄우는 것입니다.

이번 글에서는 snackbar에 기본부터, 이를 구현한 라이브러리 3종(notistack, react-hot-toast, react-toastify)의 사용법과 비교해보도록 하겠습니다.

그리고 더 나아가 실제 프로젝트에 어떤 식으로 사용하면 좋을까 개인적인 사용방법에 대해 언급하며 마무리하겠습니다.

Snackbar

snackbar(이하 스낵바)의 정의는 여러가지입니다만, 구글의 디자인 철학에 의하면 아래와 같습니다.

Snackbars provide brief messages about app processes at the bottom of the screen.
-  https://material.io/components/snackbars 중

여기서 중요한 문구는 바로 "brief messages"입니다.  영문 그대로 짧은 메시지를 전달하는 것을 목적으로 합니다.

그 다음으로는 "bottom of the screen"이라는 문구가 있습니다. 하단에 있어야 한다는 것이지요.

Snackbar 예시

하단에서 보이는 구글형님들의 snackbar

실제로 제품들을 보면 모조리 하단에 있는 모습을 확인할 수 있습니다. 

이런 것을 보면 구글의 디자인철학에 맞게 거의 모든 제품이 이를 따라간다는 점에서 당연한 것이지만, 개발자가 수만 명 단위인데 이런 철학을 전사가 공유한다는 건 참 대단한 일이 아닐까 싶습니다.

번외로 "bottom of the screen"이라고 하였는데 다른 제품들의 경우 하단이 아닌 상단에서 뜨는 경우를 종종 확인 할 수 있습니다.

하단에 있어야 한다는 건 구글의 snackbar 철학이고 정답은 아니니 상황에 따라 잘 선택하도록 하면 되겠습니다.

구글의 Snackbar

구글의 snackbar에 대한 설명글입니다. 살펴보면 Snackbar의 UX/UI에 대한 감을 잡을 수 있을 거예요.

 

Material Design

Build beautiful, usable products faster. Material Design is an adaptable system—backed by open-source code—that helps teams build high quality digital experiences.

material.io

Snackbar vs Toast

UX/UI를 하다 보면 비슷한 기능을 제공하는 컴포넌트가 다른 이름을 가진채 돌아다니는 것을 종종 확인할 수 있습니다.

아무래도 다양한 플랫폼과 환경이 있다 보니 각자의 철학에 따른 명명법으로 인해 발생한 것이라고 생각합니다.

snackbar 또한 비슷한 기능을 제공하는 Toast가 있습니다.

다만 사후 이벤트 처리가 다른데 Toast의 경우 유저의 이벤트에 따라 제거할 수 없는 알아서 살아지는 팝업 메시지입니다.

snackbar의 경우 내부에 "확인", "나중에 하기" 등과 같이 별도의 이벤트를 처리할 수 있는 것과 대조적이죠.

 

Snackbars & toasts - Components - Material Design

Show only one snackbar on screen at a time. Placement Snackbars appear above most elements on screen, and they are equal in elevation to the floating action button. However, they are lower in elevation than dialogs, bottom sheets, and navigation drawers. B

material.io

p.s Toast라는 이름 자체가 안드로이드(구글)의 UX/UI 용어이기에 ios에서 구현하려면 별도의 라이브러리 사용하거나 자체 구현해야 합니다. 안드로이드는 내장된 라이브러리를 통해 간단히 제공할 수 있습니다.

라이브러리

이제 snackbar의 개념에 대해 알았으니 이를 잘 구현한 라이브러리 3종을 소개드리겠습니다.

react-toastify ( 추천 : 상 )

위에서 언급한 라이브러리 중에 가장 오래되고 유명한 라이브러리입니다.

 

React-toastify | React-Toastify

[![Financial Contributors on Open Collective](https://opencollective.com/react-toastify/all/badge.svg?label=financial+contributors)](https://opencollective.com/react-toastify) ![Travis (.org)](https://img.shields.io/travis/fkhadra/react-toastify.svg?label=

fkhadra.github.io

기본 사용법

최상단 컴포넌트에 ToastContainer를 선언해주고 이후 toast함수로 값을 넣어주는 방식입니다.

  import React from 'react';
  import { ToastContainer, toast } from 'react-toastify';
  import 'react-toastify/dist/ReactToastify.css';

  function App(){
    const notify = () => toast("Wow so easy !");

    return (
      <div>
        <button onClick={notify}>Notify !</button>
        <ToastContainer />
      </div>
    );
  }

그리고 이 ToastConainter를 통해 전체 snackbar를 조절하거나 제어할 수 있습니다.

특징

첫 번째로 잘 정리된 공식문서와 사용하기 쉽게 만들어진 예시들입니다.

공식문서에서 playground를 제공해 간단한 조작을 통해 원하는 snackbar를 바로 확인할 수 있고 다양한 옵션과 직관적인 사용법이 인상적입니다.

두 번째로는 다양한 옵션에 대한 상세한 설명, 압도적인 지원으로 인한 빠른 업데이트, 호환성을 자랑합니다. (이건 아무래도 사용자가 많은 라이브러리의 강력한 힘?)

라이브러리가 자랑하는 feature는 다음과 같습니다.

그 중에 제가 생각하는 장점, 특징은 아래 5개 입니다.

  • Super easy to customize | 커스텀하기 쉬움
  • RTL support | 왼-오 선택만 하세요
  • Super easy to use an animation of your choice | 다양한 애니메이션 지원함
  • Promise support | 비동기 지원함
  • Pause toast when the window loses focus 👁 | 포커싱 되면 자동으로 멈춰줌

예시와 실제로 사용해보면 snackbar가 가지고 있어야 할 거의 모든 것을 가지고 있는 완성체라고 보시면 됩니다.

react-hot-toast ( 추천 : 중 )

그다음으로는 딱 심플한 snackbar에 적합한 react-hot-toast입니다. 실제 프로젝트에선 이 친구를 사용하고 있습니다.

이 친구를 react-toastify 대신에 사용하는 이유는 단 한 가지, 제가 그전에 사용해오던 라이브러리였기 때문입니다.( 구관이 명관이다)

 

react-hot-toast - The Best React Notifications in Town

Add beautiful notifications to your React app with react-hot-toast. Lightweight. Smoking hot by default.

react-hot-toast.com

기본 사용법

최상단 컴포넌트에 Toaster를 선언해주고 이후 toast함수로 값을 넣어주는 방식입니다. ( 사용법은 react-toastify랑 같습니다 )

import toast, { Toaster } from 'react-hot-toast';

const notify = () => toast('Here is your toast.');

const App = () => {
  return (
    <div>
      <button onClick={notify}>Make me a toast</button>
      <Toaster />
    </div>
  );
};

특징

뭔가 기시감이 느껴지는 특징입니다. react-toastify랑 유사합니다.

이 라이브러리의 공식문서도 충실히 기능을 제공하고 있습니다. 

스타일 변경 또한 style 객체를 받아서 처리하기 떄문에 제가 주로 사용하는 라이브러리기도 합니다.

toast('I have a border.', {
  style: {
    border: '1px solid black',
  },
});

저에게 필요한 snackbar는 기본 기능에 디자인만 약간 바뀐 snackbar였으니 적절한 선택이었던 셈이지요.

notistack ( 추천 : 하 )

mui를 즐겨 사용하는 저에게 mui에서 추천하는 notistack은 가장 처음 접한 snackbar라이브러리입니다.

공식문서 gif

 

React Snackbar component - Material UI

Snackbars provide brief notifications. The component is also known as a toast.

mui.com

기본 사용법

최상단 root에 SnackbarProvider를 선언하고 하위 컴포넌트에서 useSnackbar라는 훅을 통해 사용합니다.

// App.tsx
import { SnackbarProvider } from 'notistack';

<SnackbarProvider maxSnack={3}>
    <App />
</SnackbarProvider>


// 사용하는 어디선가.tsx
import { useSnackbar } from 'notistack';

const MyButton = () => {
    const { enqueueSnackbar, closeSnackbar } = useSnackbar();

    const handleClick = () => {
        enqueueSnackbar('I love hooks');
    };

    return (
        <Button onClick={handleClick}>Show snackbar</Button>
    );
}

특징

notistack의 특징은 별다른게 없습니다. 다른 라이브러리가 제공하는 기본 기능을 충실히 제공합니다.

다만 사용하면서 2가지 불편함이 있었습니다.

첫 번째로 공식문서가 잘못되어있습니다.

enqueneSnackbar 공식 문서 사용 예시, 그러나 안된다.

enqueneSnackbar는 위의 사용법처럼 useSnackbar 훅을 통해 꺼내와야 합니다. 초반에 이거 때문에 헤맸죠.

두 번째로 커스텀 디자인이 번거롭습니다.

기본 스타일이 보시다시피 배경에 강한 단색입니다. 그리고 이 색상을 컴포넌트 내부 단위에서 수정할 수가 없습니다.

다른 라이브러리는 내부 옵션으로 color값을 통해 바꿀 수 있었다는 점에서 상대적인 단점입니다.

그래서 나중에 변경했습니다. ( 변경했던 경험은 아래에 후술하였습니다. )

라이브러리 3종 정리

react-hot-hoast는 snackbar가 가진 기능을 충실히 제공하는 정석 같은 라이브러리라면

react-toastify는 모든 것을 지원하는 풀세트 같은 라이브러리입니다.

notistack는 애매한 라이브러리입니다.

그러니 react-hot-hoast, react-toastify 중에 공식문서 보고 맘에 드는 거 쓰면 됩니다.

Snackbar를 사용하는 방법

이제 이러한 라이브러리를 실제 프로젝트에 녹이는 방법에 대해 살펴보겠습니다.

이 단락은 공식문서가 아닌 제 머릿속 문서이기 때문에 참고 수준에서 읽으시길 추천드립니다.

외부 라이브러리를 사용 -> 분리

일단 저는 외부 라이브러리를 사용하게 되면 추상화, 모듈화를 해둡니다.

이게 어떤 말인지 장황하게 설명하기보단 코드로 살펴보시죠.

import Button from "@component/atom/button/button/Button";
import FlexBox from "@component/atom/flexbox/Flexbox";
import ContentText from "@component/atom/text/contentText/ContentText";
import styled from "@emotion/styled";
import CloseIcon from "@mui/icons-material/Close";
import toast from "react-hot-toast"; // 저는 위에서 말쓰드렸다시피 이걸 썼습니다.

// 모듈화, 추상화를 위해 별도의 커스텀 훅으로 만들었습니다.
export default function useSnackBar() {
  const enqueueDefaultBar = (message: string) => {
    toast(
      (t) => <DefaultSnackBar status={"Default"} message={message} id={t.id} />,
      {
        duration: 5000,
        style: {
          minWidth: "400px",
          maxWidth: "400px",
          borderRadius: "6px",
          background: "#333",
          color: "#fff",
        },
      }
    );
  };

  const enqueueSuccessBar = (message: string) => {
    toast(
      (t) => <DefaultSnackBar status={"Success"} message={message} id={t.id} />,
      {
        duration: 5000,
        style: {
          minWidth: "400px",
          maxWidth: "400px",
          borderRadius: "6px",
          background: "#dedede",
          color: "black",
        },
      }
    );
  };
  
  /* 중략 */

  const enqueueBarWithType = (
    message: string,
    type: "default" | "success" | "error" | "warning" | "info"
  ) => {
    switch (type) {
      case "default": {
        enqueueDefaultBar(message);
        return;
      }
      case "success": {
        enqueueSuccessBar(message);
        return;
      }
      case "error": {
        enqueueErrorBar(message);
        return;
      }
      case "warning": {
        enqueueWarningBar(message);
        return;
      }
      case "info": {
        enqueueInfoBar(message);
        return;
      }
      default: {
        enqueueDefaultBar(message);
        return;
      }
    }
  };

  return {
    enqueueDefaultBar,
    enqueueSuccessBar,
    enqueueErrorBar,
    enqueueWarningBar,
    enqueueInfoBar,
    enqueueBarWithType,
  };
}

const StatusText = styled(ContentText)`
  min-width: 50px;
`;
const MessageText = styled(ContentText)`
  max-width: 230px;
`;

// 기본 snackbar의 UI를 위해 별도로 만들어 사용중입니다.
const DefaultSnackBar = ({
  id,
  status,
  message,
}: {
  id: string;
  status: string;
  message: string;
}) => {
  return (
    <FlexBox justify={"space-between"} align={"center"} gap={"0.5rem"}>
      {status !== "Default" && <StatusText bold="bold">{status}</StatusText>}
      <FlexBox justify={"flex-start"}>
        <MessageText>{message}</MessageText>
      </FlexBox>
      <Button variant="icon" onClick={() => toast.dismiss(id)}>
        <CloseIcon />
      </Button>
    </FlexBox>
  );
};

그리고 실제 사용하는 컴포넌트 내부에서 훅으로 가져와 사용합니다.

 import useSnackBar from "@util/hooks/useSnackBar";
  
const TestComponent = () => {
  const { enqueueErrorBar } = useSnackBar();

  return (
    <div>
      ...
      <Button
        onClick={() => {
          enqueueErrorBar("ErrorSnackbar!!!");
        }}
      >
        snackbar
      </Button>
    </div>
  );
};

이렇게 분리해서 사용하면 2가지 장점이 있습니다.

장점 1 | 사용하는 입장에서 라이브러리를 알 필요가 없다.

대부분의 라이브러리가 만들어진 목적은 다시 개발하지 않기 위함입니다. 그래서 대부분의 라이브러리들은 웬만해선 풍부한 옵션을 제공하는 편인데요.

이러한 기조로 인해 간단한 기능을 사용하기 위해 라이브러리를 찾았지만 친절하고 방대한 공식문서로 인해 "헉"하게 되는 경우가 있습니다.

물론 라이브러리를 사용했다면 공식문서를 읽는 게 당연하지만 이걸 가져다 쓰는 동료는 라이브러리의 모든 옵션을 알 필요는 없습니다.

기획과 기능에 맞게 적절히 제가 구현하면, 동료는 간단하게 메서드만 보고 가져다 쓰는 게 올바른 협업이지요.

useSnackbar 훅도 내부적으로는 react-hot-toast를 사용했지만 동료는 그걸 알 필요 없이 useSnackbar훅만 가져가서 적절히 메시지를 넣어주시면 하면 되는 것입니다.

장점 2 | 만드는 입장에서 관리하기 쉽다.

맨 처음 react-hot-toast를 사용하기 전에는 notistack를 이용했다고 위에서 언급했었습니다.

초기 코드를 보면 다음과 같습니다.

import { useSnackbar as useNotiStackBar } from "notistack";

export default function useSnackBar() {
  const { enqueueSnackbar } = useNotiStackBar();
  const enqueueDefaultBar = (message: string) => {
    enqueueSnackbar(message, {
      variant: "default",
    } as any);
  };
  
  /* 중략 */

  return {
    enqueueDefaultBar,
    enqueueSuccessBar,
    enqueueErrorBar,
    enqueueWarningBar,
    enqueueInfoBar,
    enqueueBarWithType,
  };
}

useSnackBar의 이름과 제공하는 메서드들은 달라진 게 없습니다. ( 다만 최상단 root에서 제공하는 container 컴포넌트는 그냥 사용했습니다.)

저는 간단하게 이 파일 내부에서만 notistack -> react-hot-toast로 변경하는 것으로 프로젝트 내에 모든 snackbar를 변경할 수 있었습니다.

마치며

이번 글에서는 snackbar의 개념부터 이를 구현한 라이브러리를 3종, 그리고 실제 프로젝트에서 사용하는 저만의 팁으로 글을 구성하였습니다.

snackbar를 사용을 고민하는 여러분께 짧은 인사이트를 줄 수 있기를 기대하며 이만 마치겠습니다.

끝.