React 모달 라이브러리를 만들기까지의 변천사


리액트로 개발할 때 모달 컴포넌트를 구현하는 경우가 상당히 많은데, 단순한 alert, confirm 수준의 모달은 컴포넌트조차 jsx 내에 포함시키는 것이 예전부터 코드가 너무 길어지는 느낌이 들었다. 그래서 차라리 함수 로직 내에서 실행하는 것이 더 편하고 깔끔하지 않을까?라는 생각이 꾸준히 들었다.
그래서 리액트 모달 라이브러리를 만들게 됐다.
https://react-handle-alert.vercel.appreact-handle-alert
라는 이름의 라이브러리를 만들었다.
- 가장 중요한 건 React Dom과 통합된다.
- window alert, confirm api와 사용법이 유사하다.
- 스타일도 자유롭게 커스텀 가능하다
어떻게 라이브러리가 탄생하게 됐느냐?
1. 아이디어의 시작

처음에는 라이브러리로 접근하지 않고 함수로 모달 컴포넌트를 띄울 수 있는 방법을 생각하고자 했다.
전역 state로 모달 상태를 관리하고 컴포넌트는 최상위에 하나만 두면 되지 않을까? 그래서 당시 프로젝트에서 사용하던 zustand를 이용해서 전역 모달 상태 훅을 만들었다.
jsximport { create } from 'zustand';import { devtools } from 'zustand/middleware'; const defaultStates = { text: '', open: false, onAction: () => {},} const confirmStore = (set) => ({ ...defaultStates, setOpen: (open) => set({ open }), onClose: () => set({ open: false }), handleConfirm: (e) => { set({ text: e.text, open: e.open, onClose: () => { e?.onClose(); set({ open: false }); }, onAction: () => e?.onAction(), }); },}); export const useConfirmStore = create(devtools(confirmStore));
jsximport { Modal } from '../components/common/Modal/Modal';import { useConfirmStore } from '../store/useConfirmStore'; const ConfirmWrapper = ({ children }) => { const { open, text, onClose, onAction } = useConfirmStore(); return ( <> {children} <Modal isOpen={open} button={{ text: '확인', onClick: onAction }} closeButton={{ text: '취소', onClick: onClose }} > <div style={{ marginTop: 'var(--30px)' }}> <p style={{ color: 'var(--black)' }}>{text}</p> </div> </Modal> </> );};export default ConfirmWrapper;
이렇게 만든 ConfirmWrapper
를 App.jsx 또는 최상단 컴포넌트에 감싸고
jshandleConfirm({ text: '콘솔에 로그좀 찍을게요', open: true, onAction: () => { console.log('로그가 찍혔어요'); onClose(); }, });
이렇게 실행하면 함수로만 모달 컴포넌트를 띄워지게 된다.
2. 이슈 발생
인턴십을 하던 회사는 zustand가 아니라 redux를 사용하고 있어서 팀장님께 말씀드리고 redux 버전으로 위처럼 만들었는데.. 문제 상황이 발생했다.
모달을 중첩해서 띄우는 상황이 있었는데 안 됐다 . . 당연하죠. 컴포넌트는 하나니깐.. 그래서 그런 경우에는 다시 모달 컴포넌트를 사용해서 뭔가 찝찝함이 남았다.
3. 그냥 라이브러리로 만들어버리자
라이브러리로 만드려면 고려해야 할 부분이
- React Dom과 통합돼야 함.
- 컴포넌트 스타일 커스텀이 자유로워야 함.
- 함수로만 실행할 수 있어야 함.
위 세 가지 조건을 충족하면서 만들어야 했기에 하루종일 저의 뇌를 지배하게 됐다.
우선 가장 고민됐던 게 컴포넌트 선언 없이 함수에서 어떻게 컴포넌트를 띄우지?였는데 생각해보니 우리는 이 코드를 안다.
jsx// CRA -> index.tsx, vite -> main.tsxconst root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement);root.render(<App />);
ReactDom에 그냥 생성하면 되는구나! 유레카였다.
바로 개발을 시작했다.
4. 라이브러리 개발 시작
jsxexport const handleAlert = ( text: React.ReactNode, options?: AlertOptions): void => { const container = document.createElement("div"); container.id = alertContainerId; document.body.appendChild(container); const root = createRoot(container); function cleanup() { root.unmount(); container.remove(); document.removeEventListener("keyup", handleKeyUp); } function onClose() { addFadeOutEvent(cleanup); } const handleKeyUp = (e: KeyboardEvent) => { if (e.key === "Enter" || e.key === "Escape") { onClose(); } }; removeFocus(); document.addEventListener("keyup", handleKeyUp); root.render( <AlertModal text={text} onClose={() => { onClose(); }} {...options} /> );};
위는 handleAlert 함수밖에 없지만 handleConfirm도 있다
과연 이 코드가 동작을 하는가?
아주 잘 된다.
이 원리로 바로 npm에 배포를 했다.
5. 후기
처음 라이브러리를 만들면 좋지 않을까라는 생각을 한지 8개월만에 결국 만들었더니 속이 참 후련했다..
Related Articles
- 대용량 엑셀 데이터 가상화 적용하기가상화 라이브러리를 사용할지 직접 구현할지 고민 끝에 직접 구현했다.2025.02.11ryxxn
- 모바일 키보드를 고려한 버튼 위치 맞추기입력창에 맞춰 키보드가 올라오면 버튼이 가려지는 경우 버튼을 키보드에 맞춰 올린 경험이에요2024.08.08ryxxn
- React Quill에서 이미지를 업로드하기 위한 올바른 방법검색 결과에 나온 코드를 참고했다가 버그가 생겨 한참을 헤맸던 경험이에요. React에서 DOM API를 사용했을 때 발생한 버그를 겨우 찾은 이야기2024.04.30ryxxn
- Firebase와 Algolia 동기화 지연 문제 해결하기firebase database와 algolia 간의 동기화 사이에 data fetching했을 때 싱크가 맞지 않는 문제를 찾고 해결한 경험이에요2024.03.24ryxxn