본문 바로가기
Next.js

[Next.js] Modal 구현하기

by 상똥프 2024. 6. 25.

 

목표

- 재사용성이 높은 모달을 구현한다

- '모달 띄우기' 버튼을 클릭하면 화면에 모달이 뜬다

- 모달 외부는 어둡게 변한다

- 모달이 뜬 상태에서는 스크롤 기능을 막는다

- 모달 외부를 클릭하면 모달이 사라진다

- 모달에 다양한 기능을 넣는다

 

목차

1. 기본 환경 구성

2. 모달 컴포넌트 생성하기

3. 화면에 모달 띄우기

4. 모달에 기능 넣기

5. 전체 코드

 


[1. 기본 환경 구성]

1. 버튼 생성

- 아래와 같이 모달을 띄울 버튼을 생성한다

- 코드

// src/app/page.tsx

"use client";
import React, { useState } from "react";

function Home() {
  return (
    <>
      <button
        title="modal-button"
        type="button"
        onClick={}
        className="m-10 p-5 bg-slate-300 rounded-md hover:bg-slate-500"
      >
        모달 띄우기
      </button>
    </>
  );
}

export default Home;

 

2. 모달 상태 설정

- useState를 사용해 모달의 상태를 관리한다.

- 초기값은 false로, 버튼을 눌러야 모달이 등장하도록 한다.

// src/app/page.tsx

"use client";
import React, { useState } from "react";

function Home() {
  // useState로 관리 => modalStatus가 true로 바뀌면 모달 등장
  const [modalStatus, setModalStatus] = useState(false);

  // 모달의 상태 변경
  const onChangeModalStatus = () => {
    setModalStatus(!modalStatus);
  };

  return (
    <>
      <button
        title="modal-button"
        type="button"
        onClick={onHandleChangeModalStatus}  //버튼을 눌러 모달 띄우기
        className="m-10 p-5 bg-slate-300 rounded-md hover:bg-slate-500"
      >
        모달 띄우기
      </button>
    </>
  );
}

export default Home;

[2. 모달 컴포넌트 생성하기]

※ React.FC 사용 시 타입 체킹이 명확하지 않다는 지적이 있어 그냥 인터페이스를 생성하는 방식으로 글을 쓰고자 한다.

 

1. 모달에 쓰일 props 인터페이스 구성하기

- title : 모달 이름

- clickModal : 화면을 클릭하여 모달 상태 결정하는 역할 (on / off)

// src/components/Modal.tsx

interface ModalProps {
  title?: string;	//Modal 호출 시 생략 가능
  setModal: () => void;
}

 

2. 모달 구성

- preventOffModal : 모달 외부를 눌렀을 때에만 모달창이 꺼지도록 하고, 모달창을 눌렀을 때는 모달창이 꺼지지 않도록 한다

- css(tailwind)를 통해 모달을 배경과 구분한다 

- tailwind 설명 (접은 글)

더보기

1. id=모달 외부

  • fixed inset-0 : 화면 전체를 덮도록 한다. '모달 띄우기'버튼 위를 덮도록 한다
  • flex justify-center w-full h-full : 화면의 정가운데에 모달창이 띄워지도록 한다
  • bg-gray-500/50 : 모달이 띄워지면 배경을 흐릿하게 한다

 

2. id=모달

  • bg-white : 모달창을 하얀색으로 설정한다
  • w-1/2 : 모달의 가로 사이즈를 설정한다
  • rounded-md : 모달창의 모서리를 둥글게 한다
  • p-5 : 모달 안에 여백을 둔다
  • text-gray-400 : 모달 제목 색을 회색으로 설정한다
  • text-black : 모달 내용 색을 검은색으로 설정한다
const Modal = ({ title, setModal }: ModalProps) => {
  // 모달 내부를 눌렀을 때 모달이 꺼지는 것을 방지
  const preventOffModal = (event: React.MouseEvent) => {
    event.stopPropagation();
  };
  
  // 모달이 뜬 상태에서는 뒷 화면 스크롤 방지
  useEffect(() => {
    // 모달이 뜨면 body의 overflow를 hidden으로 설정
    document.body.style.overflow = 'hidden';
    // 모달이 사라지면 body의 overflow를 다시 auto로 설정
    return () => {
      document.body.style.overflow = 'auto';
    };
  }, []);

  return (
    <div
      id="모달 외부"
      onClick={setModal}
      className="fixed inset-0 flex justify-center items-center w-full h-full bg-gray-500/50"
    >
      <div
        id="모달"
        onClick={preventOffModal}
        className="bg-white w-1/2 rounded-md p-5"
      >
        <div className="text-gray-400">{title}</div>
        <div className="text-black">모달 등장</div>
      </div>
    </div>
  );
};

export default Modal;

[3. 화면에 모달 띄우기]

1. 모달 호출

- 모달을 호출하려는 페이지의 UI 아래에 모달을 불러오는 코드를 입력한다

- modalStatus가 true일 때 Modal을 호출한다

- Modal을 호출할 때 title과 모달의 상태를 관리하는 함수를 설정한다

// src/app/page.tsx


"use client";
import Modal from "@/components/Modal";
import React, { useState } from "react";

function Home() {
  const [modalStatus, setModalStatus] = useState(false);

  const onHandleModalStatus = () => {
    setModalStatus(!modalStatus);
  };

  return (
    <>
      <div className="relative rounded-md">
       ...
      </div>
      {modalStatus && (
        <Modal title="홈페이지 모달" setModal={onHandleModalStatus} />
      )}
    </>
  );
}

export default Home;

 

2. 결과

- 모달 띄우기 버튼을 누르면 모달이 나타난다

- 모달이 나타남과 동시에 모달 외부는 어둡게 변한다

- 모달을 누를 경우 아무 변화가 없지만 모달 외부를 누르면 모달이 꺼진다


[4. 모달에 기능 넣기]

1. interface에 children 추가

- 특정 기능을 수행하기 위해 children을 추가한다

- 다양한 기능을 하도록 children의 타입은 React.ReactNode로 가장 범위가 넓은 것을 선택한다

// src/components/Modal.tsx

interface ModalProps {
  title?: string;
  setModal: () => void;
  children?: React.ReactNode;  //Modal 호출 시 생략 가능
}

 

2. 추가할 기능 만들기

- (1) 다른 페이지로 이동하는 기능 / (2) 파일 선택하는 기능 / (3) 모달을 닫는 기능

 

(1) 다른 페이지로 이동

- src/app 안에 other-page 경로를 만든다

- next/navigation에서 {Router}를 호출해 이동하는 로직을 구현한다

// src/app/page.tsx

  const router = useRouter();
  const redirectToOtherPage = () => {
    router.push("/other-page");
  };

 

(2) 파일 선택

- useRef()를 사용해 파일을 선택하는 로직을 구현한다

- 파일을 선택하면 모달이 자동으로 닫히게 한다

// src/app/page.tsx

  const file = useRef<HTMLInputElement | null>(null);
  const openFileSelector = () => {
    file.current?.click();
    setModalStatus(!modalStatus);
  };

 

(3) 모달 닫기

- onHandleModalStatus를 그대로 활용한다

 

3. 모달에 children으로 기능 버튼 추가하기

// src/app/page.tsx

  return (
    <>
      <div className="relative rounded-md">
      ...
      </div>
      {modalStatus && (
        <Modal title="홈페이지 모달" setModal={onHandleModalStatus}>
          <button onClick={redirectToOtherPage}>다른 페이지로 이동</button>
          <br />
          <button onClick={openFileSelector}>파일 선택</button>{" "}
          <input type="file" title="select file" ref={file} className="hidden" />
          <br />
          <button onClick={onHandleModalStatus}>닫기</button>
        </Modal>
      )}
    </>
  );

- 결과

다른 페이지로 이동하기 파일 선택하기 모달 닫기

[5. 전체 코드]

// src/app/page.tsx

"use client";
import Modal from "@/components/Modal";
import { useRouter } from "next/navigation";
import React, { useRef, useState } from "react";

function Home() {
  const [modalStatus, setModalStatus] = useState(false);

  const onHandleModalStatus = () => {
    setModalStatus(!modalStatus);
  };

  const router = useRouter();
  const redirectToOtherPage = () => {
    router.push("/other-page");
  };

  const file = useRef<HTMLInputElement | null>(null);
  const openFileSelector = () => {
    file.current?.click();
    setModalStatus(!modalStatus);
  };

  return (
    <>
      <div className="relative rounded-md">
        <button
          title="modal-button"
          type="button"
          onClick={onHandleModalStatus}
          className="m-10 p-5 bg-slate-300 rounded-md hover:bg-slate-500"
        >
          모달 띄우기
        </button>
      </div>
      {modalStatus && (
        <Modal title="홈페이지 모달" setModal={onHandleModalStatus}>
          <button onClick={redirectToOtherPage}>다른 페이지로 이동</button>
          <br />
          <button onClick={openFileSelector}>파일 선택</button>{" "}
          <input
            type="file"
            title="select file"
            ref={file}
            className="hidden"
          />
          <br />
          <button onClick={onHandleModalStatus}>닫기</button>
        </Modal>
      )}
    </>
  );
}

export default Home;
// src/components/Modal.tsx

'use client'
import React from "react";

interface ModalProps {
  title?: string;
  setModal: () => void;
  children?: React.ReactNode;
}

const Modal = ({ title, setModal, children }: ModalProps) => {
  const preventOffModal = (event: React.MouseEvent) => {
    event.stopPropagation();
  };

  useEffect(() => {
    document.body.style.overflow = 'hidden';
    return () => {
      document.body.style.overflow = 'auto';
    };
  }, []);
  
  return (
    <div
      id="모달 외부"
      onClick={setModal}
      className="fixed inset-0 flex justify-center items-center w-full h-full bg-gray-500/50"
    >
      <div
        id="모달"
        onClick={preventOffModal}
        className="bg-white w-1/2 rounded-md p-5"
      >
        <div className="text-gray-400">{title}</div>
        {children}
      </div>
    </div>
  );
};

export default Modal;
// src/app/other-page/page.tsx

import React from "react";

function OtherPage() {
  return <div>OtherPage</div>;
}

export default OtherPage;