-
반응형
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