[React] React Modal 어디까지 만들어봤니?

2024. 10. 24. 17:03·개발/React
728x90
반응형

React Modal 어디까지 만들어봤니?

모든 코드는 github에 있습니다.

서론

나는 개인적으로 모달창을 좋아한다.

페이지 이동없이 내가 의도한대로 데이터를 전달받을수 있고 다른 화면을 클릭하면 모달이 꺼지는등 키고 끄는 동작을 간편하게 설정할 수 있기 때문이다.

image

backdrop-filter에 blur를 설정하여 위와 같이 모달에 집중하게 할 수 도있고 아닐수도 있다.

지금까지 간단히 모달을 띄우는 작업을 하다가 복잡한 모달의 요청을 받아서 하나의 리액트 훅으로 처리했던 코드를 공유하려고 한다.

요구 사항

  • 모달이 아닌 부분 클릭하면 '작성 중이던 글을 취소하시겠습니까?' 라는 새로운 모달 띄우고 닫기 or 유지하기
  • 모달 step 만들기 - 1단계 -> 2단계 -> 3단계 (완료)

단일 모달만 만들어본 나에게는 모달지옥처럼 보였고 이를 깔끔하게 정리하고 싶어서 훅을 만들게 되었다.

개발

1. 필요한 라이브러리 설치

npm i styled-components
npm i --save -dev @types/styled-components
  • typescript를 사용한다.

1. BaseModal

먼저 Modal의 root가 되는 BaseModal 을 만든다.

BaseModal은 React Node를 받으며 받은 컴포넌트를 모달로 띄우는 컴포넌트이다.

필요에 따라 모달을 닫을때 콜백 함수도 받을수 있고 backdrop-filter 등 커스텀해서 받으면 된다.

interface IBaseModalProps {
  closeCallBack: () => void;
  children: ReactNode;
  isBackgroundBlack: boolean;
}

export const BaseModal = ({
  closeCallBack,
  children,
  isBackgroundBlack,
}: IBaseModalProps) => {
  const closeHandler = (event: React.MouseEvent) => {
    if (event.currentTarget === event.target) {
      closeCallBack();
    }
  };

  return (
    <Wrapper
      $isBackgroundBlack={isBackgroundBlack}
      onClick={(event) => {
        event.preventDefault();
        closeHandler(event);
      }}
    >
      {children}
    </Wrapper>
  );
};
export const Wrapper = styled.div<{
  $isBackgroundBlack: boolean;
}>`
  position: fixed;
  display: flex;
  justify-content: "center";
  align-items: "center";
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: ${(props) =>
    props.$isBackgroundBlack ? "rgba(0,0,0,0.3)" : "transparent"};
  z-index: 999;
`;

2. useModal 훅 만들기

훅을 만드는 이유는 모달을 개발해보면서 모달을 열고 닫는 타이밍을 내가 지정해주고 싶기 때문이 크다고 생각한다.

모달의 단계가 있기 때문에 모달을 띄울 컴포넌트들을 배열로 받고 창을 닫을 때 띄어주는 모달도 따로 받을거다.

생각해보니 창을 닫을때도 1단계, 2단계 가 있을수도 있을거라 생각해서 이것도 배열로 받아주었다.

interface UseModalProps {
  children: ReactNode[];
  closeCallBack: () => void;
  isBackgroundBlack: boolean;
  confirmationSteps?: ReactNode[];
}
export const useModal = ({
  children,
  closeCallBack,
  isBackgroundBlack,
  confirmationSteps = [],
}: UseModalProps) => {
  const [isOpen, setIsOpen] = useState<boolean>(false);
  const [currentStep, setCurrentStep] = useState<number>(0);

  const [isConfirmOpen, setIsConfirmOpen] = useState<boolean>(false);
  const [currentConfirmStep, setCurrentConfirmStep] = useState<number>(0);

  const closeAllModals = () => {
    setIsOpen(false);
    setIsConfirmOpen(false);
    setCurrentConfirmStep(0);
    setCurrentStep(0);
    closeCallBack();
  };

  const closeModal = () => {
    if (confirmationSteps.length > 0) {
      setIsConfirmOpen(true);
    } else {
      setIsOpen(false);
      setCurrentStep(0);
      closeCallBack();
    }
  };

  const moveNextStep = () => {
    if (currentStep < children.length - 1) {
      setCurrentStep((prev) => prev + 1);
    } else {
      setCurrentStep(children.length - 1);
    }
  };

  const movePrevStep = () => {
    if (currentStep > 0) {
      setCurrentStep((prev) => prev - 1);
    } else {
      setCurrentStep(0);
    }
  };

  const handleConfirmation = (confirmed: boolean) => {
    if (confirmed) {
      if (currentConfirmStep < confirmationSteps.length - 1) {
        setCurrentConfirmStep((prev) => prev + 1);
      } else {
        setIsOpen(false);
        setIsConfirmOpen(false);
        setCurrentConfirmStep(0);
        setCurrentStep(0);
        closeCallBack();
      }
    } else {
      setIsConfirmOpen(false);
      setCurrentConfirmStep(0);
    }
  };

  const modal =
    isOpen && children[currentStep] ? (
      <BaseModal
        children={children[currentStep]}
        closeCallBack={closeModal}
        isBackgroundBlack={isBackgroundBlack}
      />
    ) : null;

  const confirmModal =
    isConfirmOpen && confirmationSteps[currentConfirmStep] ? (
      <BaseModal
        children={confirmationSteps[currentConfirmStep]}
        closeCallBack={() => handleConfirmation(false)}
        isBackgroundBlack={true}
      />
    ) : null;

  return {
    modal,
    confirmModal,
    setIsOpen,
    handleConfirmation,
    moveNextStep,
    movePrevStep,
    closeAllModals,
  };
};

함수 분석

앞으로 모달과 컨펌모달로 분리하겠다. 모달은 내가 띄우려는 모달 그 자체고 창을 닫을때 나타나는 모달을 컨펌모달로 칭하겠다.

1. closeAllModals

열려있는 모달, 컨펌모달들을 모두 닫는 함수다.

2. closeModal

컨펌모달이 있다면 컨펌모달을 열고 없다면 모달을 닫는 함수다.

3. moveNextStep

모달의 다음 단계가 있다면 다음단계를 보여주고 없다면 마지막 스텝을 유지한다.

4. movePrevStep

모달의 이전페이지로 이동한다.

5. handleConfirmation

컨펌이 true 라면 컨펌모달의 단계를 높이거나 모달을 닫고 false 면 컨펌 모달만 닫는다.

6. 컴포넌트

const modal =
  isOpen && children[currentStep] ? (
    <BaseModal
      children={children[currentStep]}
      closeCallBack={closeModal}
      isBackgroundBlack={isBackgroundBlack}
    />
  ) : null;

const confirmModal =
  isConfirmOpen && confirmationSteps[currentConfirmStep] ? (
    <BaseModal
      children={confirmationSteps[currentConfirmStep]}
      closeCallBack={() => handleConfirmation(false)}
      isBackgroundBlack={true}
    />
  ) : null;

현재 step 과 컨펌 step 에 따라서 모달과 컨펌모달이 나오게끔 했다.

3. 모달 사용

모달 사용은 모달 3개 (Step1, Step2, Step3)와 컨펌 모달 2개(Confirm1, Confirm2)를 사용하여 총 5개의 컴포넌트를 사용한다.

훅 사용은 다음과 같이 App.tsx에 정의했다.

const {
  closeAllModals,
  confirmModal,
  handleConfirmation,
  modal,
  moveNextStep,
  movePrevStep,
  setIsOpen,
} = useModal({
  children: [
    <Step1 moveNextStep={() => moveNextStep()} />,
    <Step2
      moveNextStep={() => moveNextStep()}
      movePrevStep={() => movePrevStep()}
    />,
    <Step3 movePrevStep={() => movePrevStep()} />,
  ],
  closeCallBack: () => {},
  isBackgroundBlack: true,
  confirmationSteps: [
    <Confirm1
      handleConfirmation={(confirm: boolean) => handleConfirmation(confirm)}
    />,
    <Confirm2
      handleConfirmation={(confirm: boolean) => handleConfirmation(confirm)}
    />,
  ],
});

각 컴포넌트들의 코드는 다음과 같다.

export const Step1 = ({ moveNextStep }: { moveNextStep: () => void }) => {
  return (
    <StepWrapper>
      Step1
      <button onClick={() => moveNextStep()}>다음</button>
    </StepWrapper>
  );
};

export const Step2 = ({
  movePrevStep,
  moveNextStep,
}: {
  movePrevStep: () => void;
  moveNextStep: () => void;
}) => {
  return (
    <StepWrapper>
      Step2
      <button onClick={() => movePrevStep()}>이전</button>
      <button onClick={() => moveNextStep()}>다음</button>
    </StepWrapper>
  );
};

export const Step3 = ({ movePrevStep }: { movePrevStep: () => void }) => {
  return <StepWrapper>Step3</StepWrapper>;
};

export const Confirm1 = ({
  handleConfirmation,
}: {
  handleConfirmation: (confirm: boolean) => void;
}) => {
  return (
    <ConfirmWrapper>
      Confirm1
      <button onClick={() => handleConfirmation(false)}>컨펌 취소</button>
      <button onClick={() => handleConfirmation(true)}>다음 스텝</button>
    </ConfirmWrapper>
  );
};

export const Confirm2 = ({
  handleConfirmation,
}: {
  handleConfirmation: (confirm: boolean) => void;
}) => {
  return (
    <ConfirmWrapper>
      Confirm2
      <button onClick={() => handleConfirmation(false)}>컨펌 취소</button>
      <button onClick={() => handleConfirmation(true)}>모달 닫기</button>
    </ConfirmWrapper>
  );
};

마지막으로 App.tsx

※주의※ modal이 confirmModal 보다 위에 있어야 한다.

return (
  <div className="App">
    {modal}
    {confirmModal}
    <button onClick={() => setIsOpen(true)}>모달 띄우기</button>
  </div>
);

실제 화면을 보면 다음과 같다.

(1) 모달 띄우기 클릭하면 아래 화면이 나온다

image

(2) 다음을 클릭하면 Step2로 이동

image

(3) 다음을 클릭하면 Step3로 이동

image

(4) 모달이 아닌 바깥쪽을 클릭하면 컨펌모달 등장

image

(5) 컨펌모달 다음스텝 클릭하면 Confirm2로 이동

image

(6) 컨펌 취소를 누르면 컨펌모달만 꺼지고 모달은 유지된다.

image

(7) 다시 들어가서 모달닫기를 누르면 모든 모달이 꺼진다.

image

기호에 맞게 기능을 추가해도되고 모달에 모달에 모달을 띄어도 된다.

하다가 또 갈아엎을 일이 생긴다면 갈아 엎겠지만 지금은 제일 편해보인다.

끝

728x90
반응형

'개발 > React' 카테고리의 다른 글

[Nextjs] 모노레포 (turborepo)  (0) 2025.02.27
[Nextjs15] 에러핸들링  (0) 2025.02.27
[React] react template 만들어놓기  (0) 2024.02.12
[React] styled-components에서 Pseudo selector &을 생략하면 오류  (0) 2023.10.04
[React] React.memo 최적화  (1) 2023.09.26
'개발/React' 카테고리의 다른 글
  • [Nextjs] 모노레포 (turborepo)
  • [Nextjs15] 에러핸들링
  • [React] react template 만들어놓기
  • [React] styled-components에서 Pseudo selector &을 생략하면 오류
TeTedo.
TeTedo.
  • TeTedo.
    TeTedo 개발 일기
    TeTedo.
  • 전체
    오늘
    어제
    • 분류 전체보기 (319)
      • 개발 (274)
        • Article (4)
        • 정리 (21)
        • Spring Boot (17)
        • JPA (2)
        • JAVA (6)
        • Database (4)
        • 자료구조 (11)
        • 알고리즘 (32)
        • React (20)
        • Docker (10)
        • node.js (18)
        • Devops (11)
        • Linux (4)
        • TypeScript (3)
        • Go (10)
        • HyperLedger (4)
        • BlockChain (43)
        • html, css, js (48)
        • CS (3)
        • AWS (3)
      • 모아두고 나중에 쓰기 (3)
      • 팀프로젝트 (18)
        • SNS(키보드워리어) (9)
        • close_sea (9)
      • 개인프로젝트 (1)
        • Around Flavor (1)
        • CHAM (13)
        • ethFruitShop (5)
      • 독서 (0)
        • 스프링부트와 AWS로 혼자 구현하는 웹 서비스 (0)
  • 블로그 메뉴

    • 홈
    • 개발일기
    • CS
    • 실습
    • 코딩테스트
    • 웹
    • Go
    • node.js
    • 팀플
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    go언어
    js
    블록체인
    React
    프로그래머스
    nodejs
    30일 챌린지
    명령어
    도커
    html
    컨테이너
    하이퍼레저
    30일챌린지
    node
    erc20
    go
    CSS
    mysql
    ERC721
    node.js
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
TeTedo.
[React] React Modal 어디까지 만들어봤니?
상단으로

티스토리툴바