[React 3] Modal과 Toast Message 만들기

김헤일리·2023년 1월 5일
1

React

목록 보기
7/10
post-custom-banner

파이널 프로젝트를 진행하면서 기존처럼 모든 메세지를 alert()로 처리 하지 않고 더 나은 UI/UX를 위해 모달과 토스트 메세지를 활용하기로 했다.

기본적으로 토스트 메세지는 toastify 라이브러리를 사용했지만, 특정 시간 이후 사라지는 모달이 필요할 땐 토스트 모달을 따로 만들어서 사용했다.

이제 alert 메세지보다 더 좋은 메세지를 만들 수 있다!


1. Modal Portal

  • React 공식문서에 따르면, Portal은 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식을 렌더링하는 최고의 방법이라고 소개하고 있다.
  • Modal Portal을 사용할 경우, index.html의 #root div 아래 modal이 표시되는 또 다른 div를 두게된다.

Portal의 장점:

  • portal을 사용하게되면, root div와도 분리된 공간에서 모달이 생기기 때문에 DOM Tree 상에서 부모-자식 관계를 갖지 않는다고 한다.
  • 그렇기 때문에 root div에서 생겨나는 리렌더링에 영향을 받지않는다.

// index.html에 새로운 div를 생성한다.
<body>
    <div id="root"></div>
    <div id="modal"></div>
</body>

// modal div에 자식 컴포넌트를 출력하는 modal portal 컴포넌트를 만든다.
import reactDom from 'react-dom';

const ModalPortal = ({ children }) => {
  const element = document.getElementById('modal');
  // 1. element의 id가 'modal'인 html element를 element라는 변수에 담는다.
  return reactDom.createPortal(children, element);
  // 2. 해당 element에 modal portal과 children 요소가 함께 생성되도록 한다.
  // 2-1. createPortal() 메서드로 인자로 넘긴 컴포넌트를 렌더링할 수 있다.
  // 2-2. 이때 인자는 렌더링할 컴포넌트와 타겟노드 2개이다.
};

export default ModalPortal;

✨ Modal Component

  • modal portal의 자식 컴포넌트로 Modal의 뼈대를 생성한다.
  • 해당 레이아웃 컴포넌트가 modal div에 생성될 모달이고, 이 컴포넌트의 자식 컴포넌트가 모달의 내용이 된다.
function Modal({ onClose, content }) {
  // 1. 모달이 닫히도록 onClose와 모달의 내용을 될 content를 props로 가져온다.
  return (
    <ModalPortal>
    // 2. modal portal 아래 위치하도록 한다.
      <StBackground onClick={onClose}>
      // 3. 외부 영역 클릭 시, 모달이 닫히도록 설정
        <StModalBorder
          role="presentation"
          onClick={(event) => {
            event.stopPropagation();
          }}
          // 4. 해당 부분도 모달의 내용이 아니다보니, 레이아웃 부분을 클릳해도 닫힐 수 있다.
          // 4-1. 그런 문제를 방지하기 위해 모달 레이아웃 컴포넌트 클릭 시 event.stopPropagation을 심어서 이벤트 버블링을 방지한다.
        >
          <StCloseBtn onClick={onClose}>
          // 5. 모달에 [X] 버튼을 하나둬서 클릭할 경우 닫히도록 한다.
            <img src={closeBtn} alt="방 닫기" />
          </StCloseBtn>
          <div>{content}</div>
		  // 6. 여러가지 모달을 만들 때, 매번 다른 내용을 보여주기 위해 content를 따로 설정한다.
        </StModalBorder>
      </StBackground>
    </ModalPortal>
  );
}
  • 모달이 표시되게 하기 위해 사용할 컴포넌트에 boolean값을 가진 useState를 선언해서 해당 state가 true로 변경되었을 때 모달 컴포넌트가 출력되도록 변경했다.

✨ Modal Content Component

  • modal 컴포넌트에서 {content} 부분에 추가될 새로운 컴포넌트를 만들었다.
    • 해당 컴포넌트는 방을 만들 시 생성되는 내용을 담고 있다.
function CreateRoomModal() {
 //////// 코드 생략 ////////

  return (
    <StModalContainer onKeyUp={onKeyUpEnter}>
      <StTitle>방 만들기</StTitle>
      <Input
        placeholder="방 제목은 7글자 이하로 입력 가능하닭"
        value={gameRoomName}
        onChange={(e) => {
          setGameRoomName(e.target.value);
          setInputCount(e.target.value.length);
        }}
        maxLength={7}
      />
      <StLimit>{inputCount}/7</StLimit>
      <button onClick={onClickRoomCreate}>
        <img src={modalCreateBtn} alt="방 만들기" />
      </button>
    </StModalContainer>
  );
}
  • 모달 하나를 만들기 위해 3개의 컴포넌트를 만들었다.
  • 약간 비효율적인가? 싶다가도 app.js에 하위 컴포넌트를 두는것 보다 나은것 같아서 portal을 사용했다.
  • 그래도 modal portal과 modal 컴포넌트는 프로젝트를 진행하면서 여러번 쓰이고 내용물 전용 컴포넌트만 생성하면 되기 때문에 크게 비효율적이라는 생각은 들지 않는다!


2. 토스트 모달

  • 모달을 만들고 직접 끄거나 외부 영역 클릭 시 닫히는 모달 말고, 특정 시간 이후에 저절로 사라지는 모달을 구현하고자 했다.
  • 이번 실전 프로젝트 때 게임이 시작되거나 종료될 경우 약 3초 정도 생성됐다 사라지는, 외부 영역을 클릭해도 사라지지 않는 모달이 필요했기 때문에 컴포넌트를 따로 빼서 생성하게 되었다.

✨ Toast Modal 사용처

  • 아까 모달과 마찬가지로, 해당 toast modal도 여전히 modal이기 때문에 해당 모달의 상태를 boolean으로 인식하는 useState를 사용했다.
<div>
  {isStartModal && (
   // 1. isStartModal의 값이 true일 때 모달이 나타나고 false일 땐 나타나지 않는다.
    <ToastMessage setToastState={setIsStartModal} type="start" />
    // 1-1. setToastState라는 이름으로 setIsStartModal 함수를 보내서 자식 컴포넌트에서도 state를 변경할 수 있도록 한다.
    // 1-2. 이때 type이라는 특정값도 함께 보내서 어떤 형태의 토스트 모달을 띄워야할지 알려준다.
  )}
  {isEndGameModal && (
  // 2. isEndGameModal의 값이 true일 때 모달이 나타나고 false일 땐 나타나지 않는다.
    <ToastMessage setToastState={setIsEndGameModal} 
type="end" />
 	// 2-1.  setToastState라는 이름으로 setIsEndModal 함수를 보내서 자식 컴포넌트에서도 state를 변경할 수 있도록 한다.
	// 2-2. 해당 모달의 자식 컴포넌트 (content)가 받을 type값을 end로 변경해서 보낸다.
  )}
</div>

✨ Toast Modal 내용

  • 시간이 지나면 저절로 사라지게 하기 위해 간단하게 setTimeout 함수를 사용했다.
function Toast({ setToastState, type }) {
  useEffect(() => {
  // 1. useEffect를 사용해서 컴포넌트가 처음 렌더링 됐을 때 아래 내용이 실행된다.
    const timer = setTimeout(() => {
      setToastState(false);
      // 2. setTimeout 함수를 이용해서 3초 후 setToastState의 값이 false가 되도록 만든다.
    }, 3000);
    return () => {
      clearTimeout(timer);
      // 3. 그리고 실행됐던 setTimeout 함수를 없애는 clearTimeout 함수를 이용한다.
    };
  }, []);

  return (
    <StBackground>
      <StToastBorder>
        {type === 'start' ?
         // 4. props로 받아온 type의 값이 'start' 일 때,
          <StToastMessage>
            <img src={gameStart} alt="게임 시작" />
    		// 4-1. 게임 시작 이미지가 보이게 만들고,
          </StToastMessage>
        ) : (
    	// 4-2. type이 'start'가 아닐 경우,
          <StToastAnswer>
            <img src={gameAnswer} alt="게임 끝" />
    		// 4-3. 게임 종료 이미지가 보이게 만든다.
          </StToastAnswer>
        )}
      </StToastBorder>
    </StBackground>
  );
}
  • 모달을 한번 만들어보니, 모달을 만드는 방식을 응용해서 다양한 종류의 모달을 만들 수 있을 것이라고 생각했고, 결과적으로 토스트 메세지처럼 특정 시간 후 사라지는 모달을 만들 수 있었다.


3. Toastify를 이용한 안내 토스트 메세지

  • 지금까지 alert()를 사용해서 사용자에게 경고 문구를 날렸었지만, react-toastify라는 라이브러리를 알게되어서 처음으로 토스트 메세지를 만들어봤다.

✨ Custom Hook으로 만든 toast message

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

const useToast = (message, type) => {
// 1. 커스텀 훅을 import 했을 때 실행될 함수를 만든다.
// 1-1. 해당 함수의 매개변수로 message와 type을 지정한다.
  const config = {
  // 2. config 값을 설정해서 기본 커스터마이징을 한다.
    position: 'top-center',
    // 2-1. 위치: 위쪽 중간
    autoClose: 2000,
    // 2-2. 2초 후 사라짐
    hideProgressBar: true,
    // 2-3. 사라지기까지 progressBar 보이지 않게 설정
    closeOnClick: true,
    // 2-4. 클릭할 경우 토스트 메세지 사라짐
    rtl: false,
    // 2-5. 알림 좌우 반전 안 함
    pauseOnFocusLoss: false,
    // 2-6. 화면 벗어나도 알람 정지 안함
    draggable: false,
    // 2-7. 드래그 불가능
    pauseOnHover: false,
    // 2-8. 마우스 올리면 알람 정지하지 않음
  };

  switch (type) {
  // 3. type 설정 시, 해당 type에 맞춰 switch case가 걸리고, 해당하는 case의 토스트 메세지가 생성된다.
    case 'success':
      return toast.success(message, config);
    case 'error':
      return toast.error(message, config);
    case 'warning':
      return toast.warning(message, config);
    default:
      return toast(message, config);
  }
  // 3-1. 성공, 실패, 경고, default 케이스마다 토스트 메세지의 마크가 다르게 표시된다.
};

export default useToast;

  • 커스텀 훅을 만들어서 굉장히 편하게 사용할 수 있었고, 라이브러리 config 값을 설정해서 나름 커스터마이징도 가능했다!
이름기능세부 옵션기본 값
position알람 위치 지정"top-left", "top-right", "top-center", "bottom-left", "bottom-right", "bottom-center""top-right"
autoClose자동 off 시간1 이상의 숫자 ( 1000 = 1초 ){3000}
hideProgressBar진행 시간 바 숨김{true} (숨김), {false} (표시){false}
newestOnTop새로운 알람 위치 설정{true} (위쪽), {false} (아래쪽){false}
closeOnClick클릭으로 알람 off{true} (끄기), {false} (끄지 않기){false}
rtl알람 좌우 반전{true} (반전), {false} (반전 안함){false}
pauseOnFocusLoss화면 벗어나면 알람 정지{true} (정지), {false} (정지 안함){true}
draggable드래그 가능{true} (가능), {false} (불가능){false}
pauseOnHover마우스 올리면 알람 정지{true} (정지), {false} (정지 안함){true}
transition알람 애니메이션 지정Slide, Bounce, Zoom, Flip ( 모듈 import 필요 ){Bounce}
limit알람 개수 제한1 이상의 숫자 ( 1 = 1개 )제한 없음

✨ 사용 예시

  • 이제 alert 대신 토스트 메세지가 생성된다!
if (comment === '') {
// 1. 만약 코멘트의 내용이 비어있을 경우,
  useToast('댓글 내용이 없닭!', 'warning');
  // 1-1. 해당 문구가 써져있는 토스트 메세지가 표시된다.
  // 1-2. type은 warning이기 떄문에 경고 마크가 표시된다.
  return;
}


사소하다면 사소한 내용이지만, 조금 더 다양한 구성의 웹사이트를 만들 수 있게 되었다.
실전 프로젝트인만큼 조금 더 신경쓰고 싶고, 실제 있는 서비스처럼 보였으면 한다.

이제 약 한달의 시간이 남았는데, 남은 시간도 무사히 지나갔으면 좋겠다!


출처:

profile
공부하느라 녹는 중... 밖에 안 나가서 버섯 피는 중... 🍄
post-custom-banner

0개의 댓글