반응형
문제 발견: 기존 모달 컴포넌트의 중복 코드 양산
리액트 프로젝트에서 모달 컴포넌트를 만들어 두고 사용할 때마다 매번 아래처럼 모달 상태를 위한 코드들이 양산되었다.
// 모달을 보여줄 것인지 아닌지의 boolean 상태
const [show, setShow] = useState(false);
// 필요시 모달 상태 핸들러까지...
const modalHandler = () => {
setShow((prev: boolean) => !prev);
};
return (
<Modal isOpen={show} onClose={modalHandler}>
모달 내용(children)
</Modal>
)
위처럼, Modal 컴포넌트로 props를 내려주기 위해서 모달을 사용하는 상위 컴포넌트마다 모달 상태 관련 코드를 중복 작성해야 하는 것이다. 이런 컴포넌트 작성 패턴을 '렌더 프롭스 패턴(Render Props pattern)'라고 하는데, 모달처럼 하위 컴포넌트를 다양하게 사용해야하는 경우에는 비효율적이다. 이런 경우에 필요한 패턴이 '컴파운드 컴포넌트 패턴(Compound component pattern)'이다.
컴파운드 컴포넌트 패턴의 키 포인트
1. React Context API 사용한 상태 관리
2. (Context API가 감싸고 있는) 부모 컴포넌트
3. (외부 진입 버튼 + 모달 내용) 자식 컴포넌트를 부모 컴포넌트의 property로 할당
4. 자식 컴포넌트에서 리액트 최상위 API인 cloneElement()를 활용하여 외부로 onClick 이벤트 전달
모달 컴포넌트 작성
"use client";
import {
cloneElement,
createContext,
Dispatch, MouseEvent,
ReactElement,
ReactNode,
SetStateAction,
useContext, useRef,
useState
} from "react";
import { createPortal } from "react-dom";
interface IModalContext {
showModalName: string;
setShowModalName: Dispatch<SetStateAction<string>>;
close: () => void;
}
const ModalContext = createContext<IModalContext | undefined>(undefined);
// 모달 부모 컴포넌트
function Modal({ children }: { children: ReactNode }) {
const [modalName, setModalName] = useState<string>("");
const close = () => setModalName("");
return (
<ModalContext.Provider value={{ showModalName: modalName, setShowModalName: setModalName, close }}>
{children}
</ModalContext.Provider>
);
}
// 모달 자식 컴포넌트 Open, Window
function Open({ children, modalName }: { children: ReactNode, modalName: string }) {
const { setShowModalName } = useContext(ModalContext)!;
return cloneElement(children as ReactElement, { onClick: () => setShowModalName(modalName) });
}
function Window({ children, modalName }: { children: ReactElement; modalName: string; }) {
const { showModalName, close } = useContext(ModalContext)!;
const ref = useRef<HTMLDialogElement>(null);
const onClose = (e: MouseEvent) => {
if (e?.target === ref?.current) {
close();
}
};
if (modalName === showModalName) {
ref?.current?.showModal();
} else {
ref?.current?.close();
}
return createPortal(
<dialog
ref={ref}
onClick={onClose}
className={"w-full max-w-[300px] backdrop-opacity-75 bg-grey-200 rounded-[20px]"}>
<div>
<button onClick={close}>닫기</button>
<div>{cloneElement(children as ReactElement, { onCloseModal: close })}</div>
</div>
</dialog>,
document.body
);
}
Modal.Open = Open;
Modal.Window = Window;
export default Modal;
상위 컴포넌트에서 모달 컴포넌트 사용하기
export default function App() {
return (
<div className="App">
<Modal>
<Modal.Open modalName="modal1">
<button>모달 열기 버튼</button>
</Modal.Open>
<Modal.Window modalName="modal1">
<>모달 내용 작성</>
</Modal.Window>
</Modal>
</div>
);
}
반응형
'웹_프론트엔드 > React' 카테고리의 다른 글
[리액트] gh-pages 배포 후 새로고침시 404 오류 (1) | 2024.03.17 |
---|---|
[리액트] react-daum-postcode 우편번호 검색 컴포넌트 예시 (0) | 2023.11.23 |
[JSX] 조건에 따른 JSX 작성 패턴 - if, switch, object 구조 (0) | 2023.07.19 |
리액트 JSX/TSX에서 switch 문법 사용하기 (0) | 2023.02.03 |
[React.ts] props로 setState 넘길 때 타입 지정하는 방법 (0) | 2022.12.19 |