recoil, typescript, react 환경에서 모달을 전역 상태에서 관리하는 방법을 알아보자!
문제점. 너무 많아진 모달 관련 상태관리값들
많은 모달들이 사용되어
모달을 열기 위한 상태관리값들(useState)과 그에 대한 함수들(열고 닫는 함수등)이 불필요하게 많아지고 있었다!
마치 아래와 같은 느낌이었다!
const App = () => {
const [isOpen1, setOpen1] = useState(false)
const [isOpen2, setOpen2] = useState(false)
const [isOpen3, setOpen3] = useState(false)
const [isOpen4, setOpen4] = useState(false)
const handleToggle1 = () => {
setOpen1((prev) => !prev);
}
const handleToggle2 = () => {
setOpen2((prev) => !prev);
}
const handleToggle3 = () => {
setOpen3((prev) => !prev);
}
const handleToggle4 = () => {
setOpen4((prev) => !prev);
}
return (
<div className="App">
<button onClick={handleToggle1}>모달1 열기</button>
<button onClick={handleToggle2}>모달2 열기</button>
<button onClick={handleToggle3}>모달3 열기</button>
<button onClick={handleToggle4}>모달4 열기</button>
<Modal1 isOpen={isOpen1} onClose={handleToggle1}/>
<Modal2 isOpen={isOpen2} onClose={handleToggle2}/>
<Modal3 isOpen={isOpen3} onClose={handleToggle3}/>
<Modal4 isOpen={isOpen4} onClose={handleToggle4}/>
</div>
)
}
해결방법. 모달 상태관리를 전역에서 해보자!
목표 다음과 같다.
- 상태관리를 모달이 필요한 컴포넌트가 아닌 전역에서 하기
- 모달 닫기는 모달 컴포넌트 안에서 닫기
- 각각 모달이 사용되는 곳에서 모달 컴포넌트 렌더링 제거
간단하게 미리 보자면
openModal(모달 컴포넌트, 모달에 필요한 props)
함수를 실행하면 모달이 독립적으로 열리고 닫힐 수 있도록 구현하고자 했다!
atom 생성
modalsState
를 만들어 모달들의 state값을 배열 형태로 저장한다.
import { atom } from 'recoil'
import { ModalType } from 'src/types/modal'
export const modalsState = atom<ModalType<FunctionComponent<any>>[]>({
key: 'modalsState',
default: [],
})
useModal custom hook
openModal에서는 Component
과 props
를 받아 props
에는 isOpen: true
을 추가해 주고 배열에 push 시킨다
closeModal에서는 Component
를 비교하여 필터링 해 기존의 배열에서 제거시켰다.
열릴 때 애니메이션을 위해 isOpen: true
를 전달해줘야 했다
import { ComponentProps, FunctionComponent, useCallback } from 'react'
import { useRecoilState } from 'recoil'
import { modalsState } from 'src/recoils/atoms/modalsAtom'
export default function useModal() {
const [modals, setModals] = useRecoilState(modalsState)
const openModal = useCallback(
<T extends FunctionComponent<any>>(Component: T, props: Omit<ComponentProps<T>, 'isOpen'>) => {
setModals((modals) => [...modals, { Component, props: { ...props, isOpen: true } }])
},
[setModals],
)
const closeModal = useCallback(
<T extends FunctionComponent<any>>(Component: T) => {
setModals((modals) => modals.filter((modal) => modal.Component !== Component))
},
[setModals],
)
return { modals, openModal, closeModal }
}
저장된 모달들 렌더링
modals를 전역에서 가져와 map
을 돌리며 각각의 컴포넌트를 리턴 시켰다
nextjs에서 실행하여 ssr일 경우(typeof window === 'undefined'
) return 시켰다
import React, { useEffect } from 'react'
import useModal from 'src/hooks/useModal'
import ConfirmModal from './ConfirmModal'
import ImgZoomModal from './ImgZoomModal'
export const modals = {
confirm: ConfirmModal,
imgZoom: ImgZoomModal,
}
const Modals = () => {
const { modals } = useModal()
useEffect(() => {
document.body.style.overflow = modals.length > 0 ? 'hidden' : 'initial'
}, [modals])
if (typeof window === 'undefined' || modals.length === 0) return null
return (
<>
{modals.map(({ Component, props }, idx) => {
return <Component key={idx} {...props} />
})}
</>
)
}
export default Modals
Modals 컴포넌트를 최상단에 import 해주고
import Modals from 'src/components/base/Modals'
function MyApp({ Component, pageProps: { ...pageProps }}: MyAppProps) {
...
return (
<>
...
<Modals />
...
</>
)
}
사용할 모달
useModal
에서 만들었던 closeModal
을 import
해 컴포넌트 안에서 닫아줄 수 있도록 했다.
확인 또는 취소 시 함께 처리해야 할 비즈니스 로직이 있을 수 있어 옵셔널로 값(onConfirm
, onClose
)을 받았다.
onConfirm
, onClose
이 성공 후(비즈니스 로직 성공 후) 모달이 닫혀야 하기 때문에 async/await
를 사용해 성공으로 끝난 경우 닫히도록 처리했다.
import React, { useRef } from 'react'
import ButtonComponent from './button'
import useModal from 'src/hooks/useModal'
export interface ConfirmModalProps {
isOpen: boolean
title?: string
component?: React.ReactNode // modal안에 내용(정적인 내용만 가능)
confirmText?: string // "확인"버튼에 대한 text (default: 확인)
cancelText?: string // "취소"버튼에 대한 text (default: 취소)
onConfirm?: () => void // "확인"버튼을 눌렀을 때
onClose?: () => void // "취소"버튼을 눌렀을 때
}
const ConfirmModal = ({
isOpen,
title,
component,
confirmText = '확인',
cancelText = '취소',
onConfirm,
onClose,
}: ConfirmModalProps) => {
const { closeModal } = useModal()
const backgroundRef = useRef<HTMLDivElement | null>(null)
const onClickBackground = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === backgroundRef.current) {
closeModal(ConfirmModal)
}
}
const onClickConfirm = async () => {
const result = onConfirm && (await onConfirm()) // 혹시 비동기로 api 요청해 할 경우
closeModal(ConfirmModal)
}
const onClickCancel = async () => {
const result = onClose && (await onClose()) // 혹시 비동기로 api 요청해 할 경우
closeModal(ConfirmModal)
}
return (
<BackConfirmModalWrapper ref={backgroundRef} onClick={onClickBackground} isOpen={isOpen}>
<ConfirmModalWrapper isOpen={isOpen}>
{title && (
<TitleWrapper>
<p>{title}</p>
</TitleWrapper>
)}
{component && <ComponentWrapper>{component}</ComponentWrapper>}
<ButtonListWrapper>
<ButtonComponent type="button" onClick={onClickConfirm} size="xl" color="primaryBlue">
{confirmText}
</ButtonComponent>
<ButtonComponent type="button" onClick={onClickCancel} size="xl" color="secondaryGray">
{cancelText}
</ButtonComponent>
</ButtonListWrapper>
</ConfirmModalWrapper>
</BackConfirmModalWrapper>
)
}
export default ConfirmModal
openModal 적용
마지막으로! openModal
함수 하나면 모달을 열렸다가 알아서 닫힌다!🎉
...
const { openModal } = useModal();
...
return (
<button
type="button"
onClick={() => {
openModal(modals.confirm, {
title: '안녕하세요?',
onConfirm: () => console.log('@@'),
component: <p>반가워요</p>
})
}}
></button>
);
타입 관련
import { ComponentProps, FunctionComponent } from 'react'
export interface ModalType<T extends FunctionComponent<any>> {
Component: T
props: ComponentProps<T>
}
타입스크립트의 자동완성 기능도 잘 돌아가고
잘못된 타입을 넣었을 경우 에러를 잘 뿜어내고 있다
context api를 활용한 모달관리
recoil을 활용한 모달관리
'IT > React' 카테고리의 다른 글
[Zustand] Toast 알림을 전역에서 관리 (0) | 2024.05.11 |
---|---|
useState 원리 (+closure) 그리고 useCallback 언제 사용하면 좋을까? (0) | 2024.01.12 |
필터 구현 로직 custom hook으로 만들기(+뒤로가기 시 필터값 유지) (0) | 2023.12.05 |
Pagination custom hook (0) | 2023.11.06 |
[React] Button Component 잘 만드는 방법! (0) | 2023.11.05 |