전역 관리의 이점
- 안드로이드 백버튼 제어가 가능하다.
- 사용할 때마다 생기는 open state 및 컴포넌트를 선언하지 않아도 된다.
- 현재 페이지와 상관없이 열고 끄기가 자유롭다.
구현 방법
useContext
/* eslint-disable @typescript-eslint/no-empty-function */
import React, { PropsWithChildren, createContext, useState } from "react";
import Modal from "..";
import { IModalProps } from "../interface/ModalProps";
type ModalProps = PropsWithChildren<Omit<IModalProps, "open">>;
type TModal = {
isOpen: boolean;
open: (props: ModalProps) => void;
close: () => void;
};
const defaultValue = {
isOpen: false,
open: () => {},
close: () => {},
};
const ModalContext = createContext<TModal>(defaultValue);
const ModalProvider = ({ children }: { children: React.ReactNode }) => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [modal, setModal] = useState<ModalProps>({ okText: "" });
const open = (props: ModalProps) => {
setIsOpen(true);
setModal(props);
};
const close = () => {
setIsOpen(false);
};
return (
<ModalContext.Provider value={{ isOpen, open, close }}>
<Modal
{...modal}
open={isOpen}
onOk={() => {
close();
modal.onOk?.();
}}
onClose={() => {
close();
modal.onClose?.();
}}
/>
{children}
</ModalContext.Provider>
);
};
export { ModalProps, ModalContext, ModalProvider };
import { useContext } from "react";
import { ModalContext, ModalProps } from "./modal";
import { DURATION } from "../constant/duration";
export default function useModal() {
const { isOpen, open, close } = useContext(ModalContext);
const openModal = (props: ModalProps) => {
open(props);
};
const closeModal = (callback?: () => void) => {
close();
if (callback) {
setTimeout(() => {
callback();
}, DURATION);
}
};
return { isOpen, openModal, closeModal };
}
redux / recoil
상태 관리 라이브러리로도 동일하게 구현 가능. 이 글에서는 useContext 기준으로 정리한다.
사용 방법
전역으로 관리되기 때문에 최상단에 선언해준다.
createBybRouter에서 선언했다. 선언 순서에 다음 제약이 있었다.
BottomSheet내에서 route 기능이 쓰이기 때문에RouterProvider보다는 하위에- 안드로이드 백버튼 선언(
window.nativeBack)이V2App하위에 있기 때문에V2App보다는 상위에 ModalProvider가BottomSheetProvider보다 상위에
이후에는 useModal, useBottomSheet의 isOpen, open, close 함수를 이용한다.
import { useModal } from "@htbeyond/uikit";
import { openLoginModal } from "@module/shared/util/modal/openLoginModal";
export function useHomeState() {
const { openModal } = useModal();
// ...
/* 입주민 신청 */
const onApplyResident = () => {
if (!AuthManager.isLogin()) {
openLoginModal(openModal);
return;
}
// ...
};
// ...
}
import { ModalProps, Typo } from "@htbeyond/uikit";
import { NativeInterfaceUtil } from "@module/shared/util/NativeInterfaceUtil";
export function openLoginModal(openModal: (props: ModalProps) => void) {
openModal({
children: (
<Typo.Body size="14" align="center">
로그인이 필요한 기능입니다.{"\n"}로그인 하시겠어요?
</Typo.Body>
),
okText: "로그인 하기",
cancelText: "취소",
onOk: () => NativeInterfaceUtil.signOut(false),
});
}
안드로이드 백버튼 제어
import { useBottomSheet, useModal } from "@htbeyond/uikit";
export function useNativeBack(callback?: () => void): () => {
type: NativeBackResult;
} {
const navigate = useNavigate();
const { isOpen: isBottomSheetOpen, closeBottomSheet } = useBottomSheet();
const { isOpen: isModalOpen, closeModal } = useModal();
// 안드로이드 백버튼 이벤트 처리용
return () => {
// ...
if (isBottomSheetOpen) {
closeBottomSheet();
} else if (isModalOpen) {
closeModal();
callback?.();
} else navigate(-1);
return { type: "NOT_EXIT" };
};
}
고려해야 할 점 (어려웠던 점)
-
ReactPortal을 통해 body 바로 아래에 선언해주기 때문에
- 기존에 페이지를 벗어나면 모달이나 바텀시트가 (꺼지진 않았으나) 언마운트되기 때문에 사라지게 되었으나
- 전역 관리를 하게 되면 페이지를 벗어나도 모달이나 바텀시트가 살아있기 때문에 제대로 꺼 주어야 한다.
- (ReactRouterPrompt 구현했을 때 겪은 문제)
-
ModalProvider가BottomSheetProvider보다 상위에 존재해야 하는 이유는 소통공간의 피드와 댓글 기능을 살펴보면 쉽게 알 수 있다.- 바텀시트가 꺼진 후 모달이 켜져야 하기 때문에 반대로 되게 되면 모달이 안 켜진다.
- 바텀시트가 없으면 그 하위의 것도 없기 때문!
- (위 기능이 구현된 파일:
FeedMoreActionController.tsx,CommentMoreActionController.tsx)
-
모달과 바텀시트가 꺼질 때 애니메이션이 적용되기 때문에 그 다음 스텝이 필요할 때 딜레이가 필요하다.
- (이건 대부분..?)
적용 결과
- 전역 관리로 코드가 더 깔끔해졌다.
- 상태 관리가 용이해졌다. 신경을 덜 써도 된다.
- 원하는 곳에서 호출이 가능하다.
- 할 수 없었던 기능을 구현할 수 있었다. (백버튼…)