Skip to content

베이스 토스트 만들기

김서진 edited this page Dec 2, 2021 · 3 revisions

개요

커스텀 토스트를 개발한 동기

토스트 개발 이전에는 사용자의 동작에 대한 성공/실패 여부를 유저가 알도록 하는 방법으로
성공/실패 상태를 컴포넌트에 저장하고 해당 상태에 따라 에러메시지 혹은 성공메시지가 출력되는 방식을 사용하였습니다.

이 방법에서 두 가지 문제점을 찾을 수 있었습니다.

  1. 사용자의 행동에 대한 반응이 눈에 띄게 보이지 않아 에러가 발생한 경우 알아차리기 어렵다.
  2. API 호출이 있는 모든 모달 컴포넌트에게 요청 성공/실패, 실패 원인 등이 저장되는 상태를 갖고 있도록 해야 한다.

이러한 문제점을 해결하기 위하여, 사용자의 행동에 대한 반응이 눈에 띄게 보이면서
상태 저장 없이 바로 메시지를 보여줄 수 있는 토스트를 사용하기로 결정내렸습니다.

어떤 식으로 토스트를 만들면 조원들 모두가 편하게 호출하여 사용할 수 있을지 고민하다가,
이전부터 관심있던 모달/토스트 라이브러리 sweetalert가 생각났습니다.

Toast.fire({
  icon: 'success',
  title: 'Signed in successfully'
})

위의 코드는 sweetalert의 토스트 호출 방식인데요,
이처럼 .fire({옵션들})을 호출하면 손쉽게 이용할 수 있는 토스트를 구현하기로 마음먹었습니다.

커스텀 토스트 미리보기

2021-11-30 23 01 30

주의 : 이 토스트는 Redux에 의존성을 가지고 있는 토스트입니다. 다른 상태관리 툴을 사용하시는 분들, 상태관리 툴을 사용하지 않으시는 분들께서는 해당 글을 참고하셔서 코드를 상황에 맞게 변형하셔야 합니다.

개발 과정

Related Pull Request

https://github.com/boostcampwm-2021/web09-Duxcord/pull/308

Toast Interface 정의

토스트가 기본적으로 가지고 있는 값은

  1. 표시할 메시지
  2. 종류 (성공/실패)
  3. 지속 기간

이며, 추가적으로 토스트를 삭제할 때 토스트를 식별할 수 있도록 id 값을 부여하여 인터페이스를 구성했습니다.

interface ToastData {
  message: string;
  type: 'success' | 'warning';
  duration?: number;
  id?: string;
}

Redux Toast Slice 정의

리덕스 스토어에서 전역 상태로 가져야 할 토스트의 목록을 관리하는 slice를 생성했습니다.

  • 초기값으로는 빈 ToastData Array를 갖고
  • (1) 토스트 추가 (2) 토스트 삭제 두 가지의 동작을 수행할 수 있는

슬라이스를 정의해두었습니다.

import { ToastData } from '@customTypes/toast';
import { createSlice } from '@reduxjs/toolkit';

const initState = Array<ToastData>();

const { reducer: toastReducer, actions } = createSlice({
  name: 'toast',
  initialState: initState,
  reducers: {
    addToast: (state, { payload }) => [...state, payload],
    popToast: (state, { payload }) => state.filter((toast) => toast.id !== payload.id),
  },
});

export const { addToast, popToast } = actions;

export default toastReducer;

useToast Hook 정의

호출 시 토스트를 추가하고 duration 이후 토스트를 삭제하는 함수를 반환하는 훅을 구현했습니다.

export const useToast = () => {
  const dispatch = useDispatch();
  const encodeBase64 = (str: string): string => Buffer.from(str, 'binary').toString('base64');

  const fireToast = ({ message, type, duration = 2000 }: ToastData) => {
    const id = encodeBase64(String(new Date().getTime()).slice(-6));
    dispatch(addToast({ message, type, duration, id }));
    setTimeout(() => dispatch(popToast({ id })), duration + 600);
  };

  return { fireToast };
};

600을 더해준 이유는 토스트가 사라지는 애니메이션이 보일 시간을 확보하기 위함입니다.

Toast, ToastItem 컴포넌트 정의

전역으로 선언된 Toast 컴포넌트의 경우 리덕스 스토어의 toast 목록을 참조하여 토스트들을 화면에 보여줍니다.

const useToasts = () => useSelector((state: RootState) => state.toast);

export default function Toast() {
  const toasts = useToasts();

  return (
    <Wrapper>
      {toasts.map((toast) => (
        <ToastItem ... />
      ))}
    </Wrapper>
  );
}

ToastItem의 경우 상태에 따라 다른 애니메이션이 보이도록 구현했습니다.

export default function ToastItem({ message, type, duration }: ToastData) {
  const [isClosing, setIsClosing] = useState(false);

  useEffect(() => {
    const setExitTimeout = setTimeout(() => {
      setIsClosing(true);
      clearTimeout(setExitTimeout);
    }, duration!);

    return () => clearTimeout(setExitTimeout);
  }, []);

  return (
    <Wrapper isClosing={isClosing}>
      <InnerWrapper color={type}>
        <div>{message}</div>
      </InnerWrapper>
      <Line type={type} duration={duration! / 1000} />
    </Wrapper>
  );
}

const Wrapper = styled.div`
  ...
  animation: 0.3s forwards ${(props) => (props.isClosing ? 'slideToTop' : 'slideFromRight')};
  ...
`
const Line = styled.div` 
  ...
  animation: ${(props) => props.duration}s linear timer;
  ...

Component에서의 사용

다음과 같이 useToast 훅과 fireToast 함수를 사용할 수 있습니다.

// import 
import { useToast } from '@hooks/index';

// 초기화
const { fireToast } = useToast();

// 사용하기
fireToast({ message: TOAST_MESSAGE.___, type: 'success' });
Clone this wiki locally