-
반응형
React에서 모달을 띄우는 방식을 선언적으로 처리하는법을 소개합니다.
모달을 띄우는 방식
기존의 모달을 띄우던 방식은 명령형 방식에 따르고 있습니다. 내부 함수나 로직들은 선언적으로 처리하고 모달 처리는 명령형으로 되어있는게 무언가 미적(?)으로 안 좋아 보이며 모달이 켜져있는지 꺼져있는지에 대한 여부는 hook로 숨기고 싶어 선언형으로 바꿔보았습니다.
기존 명령형 방식
...const [isOpen, setIsOpen] = useState();return (<div><button onClick={()=>setIsOpen(true)}>모달 오픈</button>{isOpen ? <Modal /> : null}</div>);...새로운 선언형 방식
...const { open: openComfirmDialog } = useComfirmDialog();return (<div><button onClick={openComfirmDialog}>모달 오픈</button></div>);...간단하게 hook에서 open 함수만 가져와서 사용하면 됩니다. 열려진 상태인지 아닌지는 hook에 숨기고 열기 버튼만 가져와서 사용하는 모습입니다. 이런식으로 처리한다면 좀 더 선언적으로 깔끔하게 처리가 가능합니다.
구현하기
React Portal과 함께 사용
React에는 다음과 같이 컴포넌트를 랜더링합니다.
render() {return (<div><Modal /></div>);}하지만 Modal 컴포넌트의 위치를 더 상위 트리에 위치 시키고 싶을 때가 있는데요. 그럴때는 말 그대로 포탈을 열어주면 된니다. React에는 이러한 Portal을 제공합니다.
https://ko.reactjs.org/docs/portals.html
Portal을 이용할 수 있는 컴포넌트 만들기
import { ReactNode, useEffect, useRef, useState } from "react";import { createPortal } from "react-dom";export function Portal({ children }: { children: ReactNode }) {const [mounted, setMounted] = useState(false);const portalEle = useRef<HTMLDivElement | null>(null);useEffect(() => {setMounted(true);portalEle.current = document.createElement("div");portalEle.current.id = "portal";document.body.appendChild(portalEle.current);return () => {if (portalEle.current != null) {document.body.removeChild(portalEle.current);}};}, []);if (!mounted || portalEle.current == null) {return null;}return createPortal(children, portalEle.current);}children으로 컴포넌트를 받고 body 태그 밑에 포탈을 연다. 그 다음 그 포탈안으로 children으로 들어온 컴포넌트를 넣어준다. 그렇다면 아래와 같이 사용하면 된다.
<Portal><Modal /></Portal>;모달 전용 Context Provider 만들기
interface DialogContextProps {children: ReactNode;}interface DialogContextValue {open: boolean;openDialog: ({ node }: { node: ReactNode }) => void;closeDialog: () => void;}export const DialogContext = createContext<DialogContextValue>({open: false,openDialog: () => {},closeDialog: () => {},});export function DialogProvider({ children }: DialogContextProps) {const [isOpen, setIsOpen] = useState(false);const [node, setNode] = useState<ReactNode>(null);useLockBodyScroll(isOpen);const openDialog = useCallback(({ node }: { node: ReactNode }) => {setNode(node);setIsOpen(true);}, []);const closeDialog = useCallback(() => {setNode(null);setIsOpen(false);}, []);const value = useMemo((): DialogContextValue => {return {open: isOpen,openDialog,closeDialog,};}, [isOpen]);return (<DialogContext.Provider value={value}>{children}{isOpen ? (<Portal><DialogBackground>{node}</DialogBackground></Portal>) : null}</DialogContext.Provider>);}const DialogBackground = styled.div`position: fixed;top: 0;left: 0;right: 0;bottom: 0;background: rgba(0, 0, 0, 0.7);z-index: 1000;`;export function useDialog() {const context = useContext(DialogContext);return context;}이 Provider 컴포넌트를 자식에서 useContext로 접근하여 사용하기 위해서 해당 provider을 최상단에서 묶어줍니다.
context에 넣어준 value는 열려있는지에 대한 상태, 닫기, 열기 함수가 있습니다. 이 값들로 Modal을 컨트롤하게 됩니다. 이제 모달별 커스텀 훅스 예시를 보면 됩니다.
모달별로 custom hook 만들기
const useComfirmDialog = () => {const { openDialog, closeDialog } = useDialog();const openTestModal = useCallback(() => {openDialog({node: (<DialogWrapper role="dialog"><h2>모달</h2><p>테스트 모달입니다.</p><button onClick={closeDialog}>닫기</button></DialogWrapper>),});}, [openDialog, closeDialog]);return useMemo(() => ({open: openTestModal,}),[openTestModal]);};const DialogWrapper = styled.div`position: fixed;z-index: 1000;top: 50%;left: 50%;margin: 0;transform: translate(-50%, -50%);background: #fff;box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);color: var(--main-bg-color);`;export default useComfirmDialog;Context에 저장된 openDialog 함수를 가져오고 파라메타 node에 어느 모달을 띄울지 전달 합니다. 이 모달은 openDialog 함수 실행시 Portal에 전달되어 body 밑에 랜더링 될 것이고 closeDialog 함수 실행시에 조건부랜더링에 걸려 Portal이 사라지며 모달이 사라지게 됩니다.
반응형'코딩' 카테고리의 다른 글
blogger, blogspot에서 내가 원하는 폰트로 바꾸기 (0) 2023.03.16 1년차 프론트엔드 개발자가 들은 면접 질문 모음 공유 (네카라쿠배, 시리즈 b~c 기업) (0) 2022.03.08 웹 접근성 개선하기 - 코로나 백신 통계 사이트 (lighthouse 점수 올리기) (2) 2022.02.07 WAI-ARIA - 접근성 (0) 2022.02.06 프로그래머스 - 고양이 사진첩 애플리케이션 (0) 2022.01.12