목표
- 재사용성이 높은 모달을 구현한다
- '모달 띄우기' 버튼을 클릭하면 화면에 모달이 뜬다
- 모달 외부는 어둡게 변한다
- 모달이 뜬 상태에서는 스크롤 기능을 막는다
- 모달 외부를 클릭하면 모달이 사라진다
- 모달에 다양한 기능을 넣는다
목차
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;
'Next.js' 카테고리의 다른 글
[Next.js] 페이지네이션 구현하기, 무한스크롤 (react-query) (0) | 2024.07.04 |
---|---|
[Next.js] 페이지 기반 페이지네이션 구현하기 (react-query) (0) | 2024.07.04 |
[Next.js] 이미지 업로드하기 (Nest.js연결 및 S3에 업로드) (1) | 2024.06.14 |