logo

Blog

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

#react#library
ryxxn profileryxxn
2024.05.04
thumbnail

리액트로 개발할 때 모달 컴포넌트를 구현하는 경우가 상당히 많은데, 단순한 alert, confirm 수준의 모달은 컴포넌트조차 jsx 내에 포함시키는 것이 예전부터 코드가 너무 길어지는 느낌이 들었다. 그래서 차라리 함수 로직 내에서 실행하는 것이 더 편하고 깔끔하지 않을까?라는 생각이 꾸준히 들었다.

그래서 리액트 모달 라이브러리를 만들게 됐다.

https://react-handle-alert.vercel.app

react-handle-alert라는 이름의 라이브러리를 만들었다.

  • 가장 중요한 건 React Dom과 통합된다.
  • window alert, confirm api와 사용법이 유사하다.
  • 스타일도 자유롭게 커스텀 가능하다

어떻게 라이브러리가 탄생하게 됐느냐?

1. 아이디어의 시작

처음에는 라이브러리로 접근하지 않고 함수로 모달 컴포넌트를 띄울 수 있는 방법을 생각하고자 했다.

전역 state로 모달 상태를 관리하고 컴포넌트는 최상위에 하나만 두면 되지 않을까? 그래서 당시 프로젝트에서 사용하던 zustand를 이용해서 전역 모달 상태 훅을 만들었다.

jsx
import { 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));
jsx
import { 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 또는 최상단 컴포넌트에 감싸고

js
 handleConfirm({        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. 라이브러리 개발 시작

jsx
export 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