ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Toast 만들기
    Language/React 2024. 7. 9. 11:07


    iOS에서 포그라운드 푸시메시지를 수신하는 화면을 React로 보여주는 기능을 넣어야 한다.

    작업방향은 Toast 형식으로 먼저 만들고 푸시수신되는 애니메이션 효과를 주면 어떨까 해서 만드는 도중에 토스트 메시지는 어느정도 완성 되었는데 푸시 수신 애니메이션을 넣으려고 하니 토스트 형태 소스에서 많이 달라질듯 하여

    토스트까지만 작업한 부분을 정리해 두려고 한다.

    테스트용 소스가 많이 들어가 있으니 지저분 하지만 여기까이 정리하자!

     

     

     

    작업 소스

    _app.jsx

    import NFToastProvider from '../components/common/NFToastProvider';
    
    function App({ Component, pageProps }) {
      return (
        <>
          ...
          
          <NFToastProvider />
          
          ...
        </>
      );
    }
    
    export default App;

     

    common.module.css

    .fixed-top {
      position: fixed;
      top: 0px;
      width: 100%;
      z-index: 99999;
    }
    
    .toast-items {
      display: flex;
      flex-direction: column;
      gap: 10px;
    }
    
    
    .item {
      /* animation: down-001 0.7s ease-in-out infinite alternate; */
    }
    
    @keyframes down-001 { 0% { transform: translate3d(0, 0, 0); } 100% { transform: translate3d(0, 24px, 0); } }
    /* @-webkit-keyframes down-001 { 0% { transform: translate3d(0, 0, 0); } 100% { transform: translate3d(0, 17px, 0); } }
    @-moz-keyframes down-001 { 0% { transform: translate3d(0, 0, 0); } 100% { transform: translate3d(0, 17px, 0); } } */
    
    .toast-items .item {
      display: flex;
      align-items: flex-start;
      /* width: 377px; */
      width: 720px;
      padding: 14px 14px 12px 14px;
      border-radius: 24px;
      background: var(--Materials-Thin, linear-gradient(0deg, #333 0%, #333 100%), rgba(166, 166, 166, 0.70));
      /* background: #CCC; */
      
      gap: 10px;
      background-blend-mode: color-dodge, normal;
      backdrop-filter: blur(75px);
    }
    
    .toast-items .item .icon {
      width: 38px;
      height: 38px;
      flex-shrink: 0;
      fill: var(--color-black-and-white-white, #FFF);
    }
    
    .toast-items .item .text-group {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      flex: 1 0 0;
      align-self: stretch;
    }
    
    .toast-items .item .text-group > .title-group {
      display: flex;
      align-items: flex-start;
      gap: 16px;
      align-self: stretch;
    }
    
    .toast-items .item .text-group > .title-group > .title {
      color: var(--Labels-Primary, #000);
      font-weight: 600;
      font-size: 15px;
      line-height: 20px; /* 133.333% */
      font-family: "SF Pro Text";
      letter-spacing: -0.4px;
      flex: 1 0 0;
      font-feature-settings: 'clig' off, 'liga' off;
      font-style: normal;
    }
    
    .toast-items .item .text-group > .title-group > .time {
      color: var(--Labels-Vibrant-Secondary, rgba(127, 127, 127, 0.50));
      font-weight: 400;
      font-size: 13px;
      line-height: 20px; /* 153.846% */
      font-family: "SF Pro Text";
      text-align: right;
      font-feature-settings: 'clig' off, 'liga' off;
      font-style: normal;
    }
    
    .toast-items .item .text-group > .message {
      color: var(--Labels-Primary, #000);
      font-weight: 400;
      font-size: 15px;
      line-height: 20px; /* 133.333% */
      font-family: "SF Pro Text";
      letter-spacing: -0.4px;
      align-self: stretch;
      font-feature-settings: 'clig' off, 'liga' off;
      font-style: normal;
    }

     

    NFToastProvider.tsx

    import React from 'react';
    
    import NFToast from './NFToast';
    import { useToastStore } from 'store/ToastStore';
    
    import classNames from 'classnames/bind';
    import commonStyles from '../../styles/common.module.css';
    
    const cxCommon = classNames.bind(commonStyles);
    
    const Toaster = () => {
      const { toasts } = useToastStore();
    
      return (
        <div className={cxCommon('fixed-top')}>
          <div className={cxCommon('toast-items')}>
            {toasts?.map((toast: any, index: number) => (
              <NFToast key={toast.id} {...toast} />
            ))}
          </div>
        </div>
      );
    };
    
    export default Toaster;

     

    NFToast.tsx

    import React, { useEffect, useState } from 'react';
    import { useToastStore } from 'store/ToastStore';
    
    import { ReactComponent as GobangIcon } from '/public/images/icon_notification_app_icon_24.svg';
    
    import classNames from 'classnames/bind';
    import commonStyles from '../../styles/common.module.css';
    
    const cxCommon = classNames.bind(commonStyles);
    
    const TOAST_DURATION = 0 + 3000;
    const ANIMATION_DURATION = 350;
    
    interface NFToastProps {
      id: string;
      title: string;
      message: string;
      // type: ToastType;
    }
    
    const NFToast = ({ title, id, message /* , type */ }: NFToastProps) => {
      const { removeToast } = useToastStore();
      const [opacity, setOpacity] = useState(0.2);
    
      useEffect(() => {
        setOpacity(1);
        const timeoutForRemove = setTimeout(() => {
          removeToast(id);
        }, TOAST_DURATION);
    
        const timeoutForVisible = setTimeout(() => {
          setOpacity(0);
        }, TOAST_DURATION - ANIMATION_DURATION);
    
        return () => {
          clearTimeout(timeoutForRemove);
          clearTimeout(timeoutForVisible);
        };
      }, [id, removeToast]);
    
      return (
        <div
          style={{
            opacity: opacity,
            transition: 'all 0.35s ease-in-out',
            transform: `translateY(${opacity === 0 ? '-10px' : '10px'})`,
          }}
          className={cxCommon('item')}
        >
          <span className={cxCommon('icon')}>
            <GobangIcon />
          </span>
    
          <div className={cxCommon('text-group')}>
            <span className={cxCommon('title-group')}>
              <span className={cxCommon('title')}>{title}</span>
              <span className={cxCommon('time')}>9:41 AM</span>
            </span>
            <span className={cxCommon('message')}>{message}</span>
          </div>
        </div>
      );
    };
    
    export default React.memo(NFToast);

     

    ToastStore.tsx

    import { create } from 'zustand';
    
    type ToastType = {
      id: string;
      title: string;
      message: string;
    };
    
    interface ToastStore {
      toasts: ToastType[] | null;
      setToasts: (toasts: ToastType[]) => void;
      addToast: (toast: ToastType) => void;
      removeToast: (id: string) => void;
    }
    
    export const useToastStore = create<ToastStore>((set) => ({
      toasts: null,
      // toasts: [
      //   { id: '111', title: 'success', message: 'success message' },
      //   { id: '222', title: 'error', message: 'error message' },
      //   { id: '333', title: 'info', message: 'info message' },
      // ],
      // setToasts: (toasts) => set((state) => ({ ...state, toasts: toasts })),
      setToasts: (toasts) => set({ toasts }), // 직접 toasts를 설정하도록 변경
    
      addToast: (toast) =>
        set((state) => ({
          // toasts: state.toasts ? [...state.toasts, toast] : [toast],
          toasts: state.toasts ? [toast, ...state.toasts] : [toast],
          // toasts: state.toasts ? [toast].concat(...state.toasts) : [toast],
        })),
      removeToast: (id) =>
        set((state) => ({
          toasts: state.toasts ? state.toasts.filter((toast) => toast.id !== id) : [],
        })),
    }));

     

    commonHook.tsx

    ...
    
    export const useToast = () => {
      const { addToast } = useToastStore();
    
      return {
        toast: {
          success: () => {
            addToast({ id: uuidv4(), title: 'success', message: 'success message' });
          },
          info: () => {
            addToast({ id: uuidv4(), title: 'info', message: 'info message' });
          },
          error: () => {
            addToast({ id: uuidv4(), title: 'error', message: 'error message' });
          },
        },
      };
    };
    
    ...

     

    Test.tsx

    import { useToast } from '../../hook/commonHook';
    
    
    const Test = () => {
    
      const { toast } = useToast();
    
      useEffect(() => {
        ...
    
        setTimeout(() => {
          toast.error({ title: 'error', message: 'error message' });
        }, 300);
      }, []);
    
      return (
        <>
            ...
            <div>
              <button type="button" onClick={() => toast.success({ title: 'success', message: 'success message' })}>
                success message
              </button>
              {/* <button type="button" onClick={() => toast.warning('messageText warning')}>
              warning message
            </button> */}
              <button type="button" onClick={() => toast.info({ title: 'info', message: 'info message' })}>
                info message
              </button>
              <button type="button" onClick={() => toast.error({ title: 'error', message: 'error message' })}>
                error message
              </button>
            </div>
            ...
            
        </>
      );
    };
    
    export default Test;

     

     

    정리안된 소스라도 나중에 필요할 수 있으니!!

     

    참고)

    https://www.halang.tech/making-toast

    댓글

Designed by Tistory.