Toast 기능 추가

바닐라 자바스크립트 -> react에서 Toast 적용하기

바닐라 자바스크립트에서도 Toast 메시지를 구현할 때, insertAdjacentElement, removeChild등과 같은 메서드로 돔을 직접적으로 조작하여 구현하였다.

하지만 react에서는 직접적인 돔조작을 하지말라고 들었기 때문에 다른방식으로 구현해야 했다.

구현방식

토스트 메시지 목록을 전역 상태로 관리하여 추가 삭제를 하였고 div.root 요소를 건드리지 않고 body에 토스트 컨테이너를 생성한뒤 그 컨테이너에 createPortal을 사용하여 토스트 메시지들을 붙이는 방식을 이용했다.

keyword
  • Portals
  • useContext
body 하위에 toast-container 요소를 생성 후 하위에 토스트 메시지들을 관리
function CreateToastPortal({ children }: PortalProps) {
  const container = document.getElementById('toast-container');
  let newContainer;

  if (!container) {
    const toast = document.createElement('div');
    toast.setAttribute('id', 'toast-container');

    newContainer = toast;
    document.body.appendChild(newContainer);
  } else {
    newContainer = container;
  }

  return ReactDOM.createPortal(children, newContainer);
}
useContext로 메시지 상태 관리
const [toasts, setToasts] = useState<ToastState[]>([]);

const createToast = useCallback((toast: Toast) => {
  setToasts((prevToasts) => [...prevToasts, { id: nanoid(), ...toast }]);
}, []);

const hideToast = (toastId: string) => {
  setToasts((prevToasts) => prevToasts.filter(({ id }) => id !== toastId));
};

<toastContext.Provider value={createToast}>
  <CreateToastPortal>
      {toasts.map((toast) => (
        <ToastComponent key={toast.id} hideToast={hideToast} {...toast} />
      ))}
  </CreateToastPortal>
  {children}
</toastContext.Provider>

이제 사용하고 싶은 곳에서 적절하게 메시지 상태를 추가해주면 된다.

이전에 바닐라로 구현했던 방식

import './toast.scss';

class Toast {
  constructor(props) {
    this.timeout = props?.timeout || 3000;
    this.type = props?.type || 'success';
    this.content = props?.content || '성공';

    this.render();
  }

  createDom(tagName, attrs) {
    const $dom = document.createElement(tagName);
    for (const [key, value] of Object.entries(attrs)) {
      $dom[key] = value;
    }
    return $dom;
  }

  render() {
    const container = document.querySelector('.toast-container');
    let newContainer;

    if (!container) {
      newContainer = this.createDom('div', {
        className: 'toast-container',
      });
      document.body.appendChild(newContainer);
    } else {
      newContainer = container;
    }

    this.toast = this.createDom('div', {
      className: 'toast',
    });

    newContainer.insertAdjacentElement('beforeend', this.toast);
    this.toast.classList.add(this.type);

    // setting content
    this.toast.appendChild(
      this.createDom('h4', {
        innerText: this.content,
        className: 'toast-content',
      }),
    );

    // setting timer
    this.toastTimeout.call(this, this.timeout);

    this.progress = this.createDom('div', {
      className: 'toast-progress',
    });
    this.toast.insertAdjacentElement('beforeend', this.progress);
    this.progress.style.animation = `toast_progress ${this.timeout}ms linear forwards`;
  }

  toastTimeout(time) {
    setTimeout(() => {
      this.toast.classList.add('is-hiding');
      setTimeout(() => {
        this.hide();
      }, 1500);
    }, time);
  }

  hide() {
    this.toast.parentNode?.removeChild(this.toast);
  }
}

export default Toast;

// 사용법

// 1. 사용할 컴포넌트에서 import
// import Toast from '../../components/Toast/Toast';

// 2. 알림을 사용하고 싶은 곳에서
// new Toast({ content: '입력하고 싶은 메시지'})
// new Toast({ timeout: 3000, content: 'test', type: 'success' })
// new Toast({ timeout: 3000, content: 'test', type: 'fail' })
// new Toast()

// default timeout:3000, content: 성공, type: success

좋은 웹페이지 즐겨찾기