Recoil을 사용하여 커스텀 토스트 훅 만들기

1106laura·2022년 1월 19일
20

letsgo-react

목록 보기
3/3
post-thumbnail

서론

토스트란?

토스트란 특정 이벤트에 반응하여 간단한 피드백을 짧은 텍스트로 일정 시간동안 제공하는 UI이다.

토스트 구현 방식에 대한 고민

아래와 같이 특정 이벤트가 발생하는 상황을 상태로 관리하여 토스트를 띄울 수 있지만,

const [isError, setIsError] = useState(false);
...
{ isError && <Toast/> }

화면 이동 중에도 토스트가 떠 있어야 하고,
토스트가 떠 있는 시간 이후에는 언마운트해야 하지만 토스트가 다시 떠야 할 경우 마운트시켜줘야 하는 과정이 번거롭기 때문에
전역 변수로 토스트를 관리하고 토스트가 포함된 공간도 라우터보다 상단에 두어야 한다.

이 글에서 다루는 내용

이 글에서는 위의 고민 내용을 바탕으로 Recoilstyled component를 사용하여 토스트 컨테이너 컴포넌트를 구현하고 토스트를 사용하는 훅을 제작하는 로직을 다루었다.

https://github.com/Neogasogaeseo/Naega-Web/pull/78

본론

Recoil Atom 정의하기

@stores/toast

import { atom } from 'recoil';

export interface Toast {
  id?: string;
  content: string;
  duration?: number;
  bottom?: number;
}

export const toastState = atom<Toast[]>({
  key: 'toastState',
  default: [],
});

화면에 표시될 토스트들을 모두 보관하는 state를 만들어 두었다.
각각의 역할은 다음과 같다.

  • id : 토스트를 목록에서 삭제할 때 식별할 id
  • content : 화면에 보여질 내용
  • duration : 화면에 보여질 시간 (밀리초 단위)
  • bottom : 맨 바닥으로부터의 px

useToast 훅 만들기

@hooks/useToast

import { Toast, toastState } from '@stores/toast';
import { getRandomID } from '@utils/etc';
import { useRecoilState } from 'recoil';

export function useToast() {
  const [toasts, setToasts] = useRecoilState(toastState);

  const removeToast = (toastID: Toast['id']) =>
    setToasts((prev) => prev.filter((toast) => toast.id !== toastID));

  const fireToast = (toast: Toast) => {
    setToasts((prev) => [...prev, { ...toast, id: getRandomID() }]);
    setTimeout(() => removeToast(toast.id), 600 + (toast.duration ?? 1000));
  };

  return { toasts, fireToast };
}

이 때, id를 설정해주는 함수는 아래와 같다.

@utils/etc

export const getRandomID = () => String(new Date().getTime());

위에서 만들어 둔 state를 조작하는 hook을 만든다.
toasts는 레코일에서 받아온 그대로 리턴하고, toast를 추가하고 duration 이후에 toast를 삭제하는 fireToast라는 함수를 리턴해준다.
duration에 600을 더해준 이유는 토스트가 사라지는 애니메이션이 표시될 시간을 확보하기 위함이다.

Toast Item과 Toast List 컴포넌트 구현하기

@common/Toast/Item

import { Toast } from '@stores/toast';
import { useEffect, useState } from 'react';
import { StToastItem } from './style';

function ToastItem(props: Toast) {
  const { content, bottom, duration } = props;
  const [isClosing, setIsClosing] = useState(false);

  useEffect(() => {
    const setExistTimeout = setTimeout(() => {
      setIsClosing(true);
      clearTimeout(setExistTimeout);
    }, duration ?? 1000);
  });

  return (
    <StToastItem bottom={bottom} isClosing={isClosing}>
      {content}
    </StToastItem>
  );
}

export default ToastItem;

toast가 언마운트될 때 애니메이션을 주기 위하여 isClosing이라는 상태를 관리하고 duration 이후에 true로 바꾸어준다.
styled component에서 isClosing값을 받아와 다른 애니메이션을 먹여준다.

@common/Toast/Item/style

import styled from 'styled-components';
import { ANIMATION } from '@styles/common/animation';

export const StToastItem = styled.div<{ bottom?: number; isClosing: boolean }>`
  position: absolute;
  ...
  bottom: ${({ bottom }) => bottom ?? 26}px;
  animation: 0.3s forwards
    ${({ isClosing }) => (isClosing ? ANIMATION.FADE_OUT : ANIMATION.FADE_IN)};
`;

@common/Toast/List

import { toastState } from '@stores/toast';
import { useRecoilValue } from 'recoil';
import ToastItem from '../Item';
import { StToastList } from './style';

function ToastList() {
  const toasts = useRecoilValue(toastState);
  return (
    <StToastList>
      {toasts.map((toast) => (
        <ToastItem key={toast.id} {...toast} />
      ))}
    </StToastList>
  );
}

export default ToastList;

토스트 리스트를 넣어준다.
이 때, 어느 페이지에서도 fix된 형태로 보여야 하므로 position을 fixed로 준다.

@common/Toast/List/style

import styled from 'styled-components';

export const StToastList = styled.div`
  bottom: 0;
  left: 0;
  position: fixed;
  z-index: 1000;
`;

App.tsx에 심어두기

App.tsx

import GlobalStyle from '@styles/global';
import Router from '@routes/Router';
import ToastList from '@components/common/Toast/List';

function App() {
  return (
    <>
      <GlobalStyle />
      <ToastList />
      <Router />
    </>
  );
}

export default App;

위에서 구현한 ToastList를 최상단에 넣어준다.

결론

컴포넌트에서 사용하기

const { fireToast } = useToast();

fireToast({ content:"안녕" });

토스트가 생기고 없어지고 애니메이션이 입혀지는 등등의 과정은 컴포넌트 안에 추상화되어 있으므로 외부 컴포넌트에서는 fireToast라는 함수만 호출하면 된다.

이렇게 커스텀 훅과 레코일을 사용하여 편하게 토스트를 사용할 수 있다.

profile
뭐라도 더 하자~

4개의 댓글

comment-user-thumbnail
2022년 1월 21일

진짜 멋있네

답글 달기
comment-user-thumbnail
2022년 1월 25일

알찬 글 잘 보고 갑니다 :)

답글 달기
comment-user-thumbnail
2022년 1월 26일

이번에도 토스트 예쁘게 구우셨네요! 제빵장인 서진서진!

답글 달기
comment-user-thumbnail
2022년 2월 4일

다음 글은 언제 나오가시?

답글 달기