Text Me! 이벤트 페이지 개발 중, 1인 당 최대 3개의 편지를 열람할 수 있도록 제한하는 요구사항이 생겼다. 서비스 안내 페이지가 존재하긴 하지만 3개의 편지를 무심코 눌렀다가 더 이상 편지를 열 수 없다는 것을 알게 되면 사용자가 당황할 것이라 생각해서 편지를 열기 전 확인 절차를 거치는 모달이 필요하다고 생각했다.
브라우저에서 제공하는 confirm 메서드를 사용해도 되지만, 기존에 사용하던 알림 모달 디자인이 있기 때문에, UI/UX를 고려하여 confirm 모달을 만드는 게 좋겠다고 생각했다.
모달은 전체 서비스에서 한번에 하나만 노출된다고 가정하고 전역 상태로 관리했다. 라이브러리는 Zustand
를 사용했지만, 어떤 상태 관리 라이브러리를 사용해도 무방하다.
import { create } from "zustand";
interface OpenModalProps{
content: string;
yesButtonText?: string;
noButtonText?: string;
}
interface ConfirmModal {
content: string;
yesButtonText: string;
noButtonText: string;
clickYesButton: (value: unknown) => void;
clickNoButton: (value: unknown) => void;
openConfirmModal: ({ content, yesButtonText, noButtonText }: OpenModalProps) => Promise<boolean>;
}
const useConfirmModal = create<ConfirmModal>((set, get) => ({
content: null,
yesButtonText: null,
noButtonText: null,
clickYesButton: null,
clickNoButton: null,
openConfirmModal: () => { ... }
}));
export { useConfirmModal };
이렇게 전역 상태를 만들어주고 ConfirmModal
컴포넌트를 작성했다. 루트 컴포넌트에 배치하여 전역적으로 노출 혹은 비노출되는 컴포넌트이다.
const ConfirmModal = () => {
const { content, yesButtonText, noButtonText, clickYesButton, clickNoButton } = useConfirmModal();
if (!content){
return <></>
}
return (
<Container>
<TextParser text={content} />
<RowLayout>
<Button type="button" onClick={clickNoButton}>
{noButtonText}
</Button>
<Button type="button" onClick={clickYesButton}>
{yesButtonText}
</Button>
</RowLayout>
</Container>
);
};
export default ConfirmModal;
컴포넌트 내에서 이벤트 핸들러 안에 openConfirmModal을 await로 호출하여 사용할 수 있다.
const { openConfirmModal } = useConfirmModal();
const confirmOpen = async () => {
const confirm = await openConfirmModal({
content: "편지를 열면 열 수 있는 편지 개수가 차감돼요. 열람하시겠어요?\n(현재 열람 가능 편지 수: 3)",
yesButtonText: "열기"
});
if (!confirm) {
return;
}
open(letter.id);
};
이제 핵심 기능인 openConfirmModal
의 구현을 함께 보겠다. confirm 창이 열려있는 동안 다음의 메서드들이 실행되지 않고 대기하고 있어야 하므로 Promise와 async/await을 이용해 구현했다.
const useConfirmModal = create<ConfirmModal>((set, get) => ({
// ...
openConfirmModal: ({ content, yesButtonText = "확인", noButtonText = "취소" }) => {
set({ content, yesButtonText, noButtonText });
const promise = new Promise((resolve, reject) => {
set({
clickYesButton: resolve,
clickNoButton: reject
});
});
return promise.then(
() => {
set({ content: null, yesButtonText: null, noButtonText: null });
return true;
},
() => {
set({ content: null, yesButtonText: null, noButtonText: null });
return false;
}
);
},
}));
openConfirmModal
을 선언하면 Promise가 Pending 상태에 진입한다. 인자로 넘어오는 resolve와 reject를 각각 clickYesButton
, clickNoButton
에 할당하면, 컴포넌트 상에서 Yes 버튼을 눌렀을 때 Promise가 resolve되고, No 버튼을 눌렀을 때 reject 된다.
resolve가 됐을 때는 promise.then의 첫번째 인자인 onfulfilled가, reject이 됐을 때는 두 번째 인자인 onrejected가 수행되고 이들은 각각 true와 false를 리턴한다. 이것이 위 컴포넌트 코드에서 받아온 confirm
이 된다.
열기 버튼을 누르면 편지가 열리지만 취소를 누르면 아무 일도 일어나지 않는 confirm 모달이 완성됐다.
서버와 통신할 때 Promise를 자주 쓰지만, Promise를 이용해서 프론트엔드 단의 플로우를 만들어 본 경험은 드문 것 같다.
간단한 모듈을 구현하면서 Promise의 개념도 다시 한번 복습할 수 있는 기회가 되었고, pending, fulfilled, rejected라는 상태가 어떻게 정의되고 진입하는지 모호했던 부분을 확실히 이해하게 됐다.