[React] 전역상태를 이용해 모달을 띄우는 게시물을 만들어보자

fgStudy·2022년 7월 14일
9

프론트엔드 공부

목록 보기
3/6
post-thumbnail
post-custom-banner

최근 토스 SLASH 22 - Effective Component 지속 가능한 성장과 컴포넌트 컨퍼런스를 통해 전역상태를 이용해 모달이 띄워지는 Select 컴포넌트 인터페이스를 설계하는 법에 대해 배우게 되었다.

영상의 댓글에서 해당 컴포넌트에 대한 자세한 설명이 담긴 블로그 포스팅을 공유받게 되었다.

원티드 프리온보딩 프론트엔드 부트캠프에서 모달이 띄워지는 게시물을 만들어야 했는데, 위의 포스팅을 열심히 참고해 나름대로 구현에 성공했다!

아래 코드 샌드박스는 내가 만든 모달을 띄운 게시물이다. 게시물(숫자 1,2,3) 클릭하면 게시물의 content가 모달에 띄워지는 것을 확인할 수 있다!

해당 포스팅은 모달을 띄우는 게시물을 만드는데 생각했던 고민들과 결과물에 대한 자세한 설명을 다루어보고자 한다.


🤔 What. (기능 정의)

Card가 가져야할 기능을 중심으로 정리해본 요구사항들은 다음과 같다.

  • 게시물을 클릭했을 때 모달이 띄워져야 한다.
  • 모달에는 모달을 닫는 close 버튼과 이후 작업을 수행할 submit 버튼이 있어야 한다.
  • 모달이 열렸는지, 닫혔는지, 그리고 모달 내부 내용은 어떤 것이 들어가는지에 대해 숨긴다.

➰ How. (구현)

앞서 정의한 요구사항은 어떤 방법으로, 어떤 상태관리를 통해 구현할 수 있을까?
위에서 정의한 요구사항을 토대로 구현방향을 자세하게 설명해보자.

  • 게시물을 클릭했을 때 모달이 띄워져야 한다.
    • 모달의 렌더링 여부를 결정할 isOpen 상태 필요하다.
  • 모달에는 모달을 닫는 close 버튼과 이후 작업을 수행할 submit 버튼이 있어야 한다.
    • 모달의 close 버튼, submit 버튼을 클릭 시 수행할 작업을 정의해야 한다.
    • 예컨대 close 버튼일 경우 사용자가 모달을 닫으려고 할 때 진짜로 닫을지에 대한 문구를 띄울 수 있다.
  • 모달이 열렸는지, 닫혔는지, 그리고 모달 내부 내용은 어떤 것이 들어가는지에 대해 숨긴다.
    • state를 숨기기 위해 Context API를 이용한다.
    • 게시물과 모달을 응집시킴으로써 불필요한 인터페이스와 모달 state를 숨긴다.

🎨 구조

앞서 구현 설명 때 Modal에서 필요한 상태를 Context API로 관리하기로 말했다.
Modal의 상태는 크게 4가지이다.

  • isOpen: 모달의 렌더링 여부 결정
  • openModal: 모달을 여는 콜백 함수
  • closeModal: 모달을 닫는 콜백 함수
  • modalData: 모달 content, 모달에서 수행할 처리 로직들 등등...

우리는 Card를 클릭 시 Modal에 렌더링할 content와, 열고 닫을 때 처리 로직을 Modal에 주입을 시킬 것이다.

  • children: 모달 content
  • onCancel: 모달을 닫을 때 처리할 로직
  • onSubmit: 모달을 submit할 때 처리할 로직

그럼 아래의 그림 형태로 구조가 잡히게 된다.


🎂 코드 설명

이제 앞서 설명한 구조를 코드로 하나하나 설명하고자 한다.
불필요한 인터페이스를 제거하고 Modal의 state를 숨김으로써 추상화 레벨을 높이는데 집중해보자.

1. Modal Context api

상태를 숨기기 위해 Context API를 이용하자.

type ModalData = {
  children?: ReactNode;
  onCancel?: () => unknown;
  onSubmit?: () => unknown;
};

const ModalContext = createContext<{
  isOpen: boolean;
  openModal: (modalData: ModalData) => unknown;
  closeModal: () => unknown;
  modalData: ModalData;
}>({} as any);

export const ModalContextProvider: FC<{ children: ReactNode }> = ({
  children,
}) => {
  const [isOpen, setIsOpen] = useState(false);
  const [modalData, setModalData] = useState<ModalData>({});

  const openModal = ({ children, onCancel, onSubmit }: ModalData) => {
    setIsOpen(true);
    setModalData({
      children,
      onCancel,
      onSubmit,
    });
  };

  const closeModal = () => {
    setIsOpen(false);
    setModalData({});
  };

  return (
    <ModalContext.Provider value={{ isOpen, openModal, closeModal, modalData }}>
      {children}
    </ModalContext.Provider>
  );
};

Modal context에서는 위의 그림처럼 children, onCancle, onSubmit을 인자로 전달받는다.

  • openModal

    • 모달을 여는 콜백함수
    • 게시물을 클릭하면 모달 open 상태를 true로 변경
    • ModalData를 인자로 전달한 children, onCancel, onSubmit으로 변경한다.
    • 이 ModalData는 Modal 컴포넌트에서 사용해 컴포넌트를 만들 예정이다. children은 Modal의 ui를, onCancel, onSubmit 콜백은 비즈니스 로직을 담당하게 된다.
  • closeModal

    • 모달을 닫는 콜백함수
    • 모달을 닫을 경우 모달 open 상태를 false로 변경
    • 모달의 데이터를 초기화

2. Modal 컴포넌트

export const Modal = () => {
  const { isOpen, modalData, closeModal } = useModalContext();

  if (!isOpen) {
    return <></>;
  }

  const { children, onCancel, onSubmit } = modalData;

  const onCancelInternal = () => {
    onCancel?.();
    closeModal();
  };

  const onSubmitInternal = () => {
    onSubmit?.();
    closeModal();
  };

  return (
    <Portal>
      <div className="modal">
        <div className="modal__dropdown" onClick={closeModal} />

        <div className="modal__contents">
          <div>{children}</div>

          <div className="modal__actions">
            <button onClick={onCancelInternal}>cancel</button>
            <button onClick={onSubmitInternal}>submit</button>
          </div>
        </div>
      </div>
    </Portal>
  );
};

Modal Context api에서 정의한 비즈니스 로직을 이용해 모달 UI를 만들어보자.

  • 모달은 isOpen 상태에 따라 렌더링 여부를 결정한다.

  • onCancelInternal

    • 모달을 닫는 이벤트 핸들러로 background 또는 cancel 버튼에 걸어준다.
    • onCancel은 모달을 닫을 때 문구 띄워주기 등 추가로 처리하는 콜백함수이다. 모달을 사용하는 컴포넌트에서 이를 전달받는다.
    • Modal context api에서 정의한 closeModal 콜백함수(isOpen를 false로 만들어줌)를 이용해 모달을 닫는다.
  • onSubmitInternal

    • 모달을 submit하는 이벤트 핸들러로, submit 버튼에 걸어준다.
    • 대부분의 모달의 경우 Submit시 자동으로 닫히게 되므로 closeModal 콜백함수를 이용해 submit후 닫히게끔 구현하였다.

3. CardList

import { memo } from "react";
import { Modal, useModalContext } from "./components/Modal";

const CardList = memo(() => {
  const { openModal } = useModalContext();

  const openModalWithData = (data: number) =>
    openModal({
      children: <p>{data}</p>,
      onSubmit: () => console.log("submit")
    });

  return (
    <>
      {[1, 2, 3].map((data) => (
        <p key={data} onClick={() => openModalWithData(data)}>
          {data}
        </p>
      ))}
    </>
  );
});

export default function App() {
  return (
    <div className="App">
      <CardList />
      <Modal />
    </div>
  );
}
  • CardList는 게시물 list를 렌더링한다.

  • 각 게시물을 클릭 시 모달을 렌더링해야 하므로 context api에서 정의한 openModal 콜백함수를 이용한다.

  • openModalWithData

    • children은 모달에서 렌더링하고자 하는 컴포넌트를 전달한다. 앞서 모달의 content는 게시물의 content와 동일하게 구현하겠다고 했으므로 게시물의 data를 전달하였다.
    • onSubmit은 api 호출함수를 걸어준다. 데모이므로 간단히 콘솔로그 함수를 걸어주었다.
    • 게시물을 닫을 때 수행하고자 하는 로직이 딱히 없어 onCancel 함수는 정의하지 않았다. 하지만 게시물을 닫을 때 처리하고자 하는 로직이 있을 시 정의해주면 된다.
  • 각 게시물에 openModalWithData 콜백함수를 이벤트 핸들러로 걸어준다.



🌻 맺으며

토스 컨퍼런스를 보던 당시 도저히 이해를 못하겠던 코드를 1달 사이에 조금이라도 이해해 적용을 하게되어서 정말 기쁘다.

하루하루 공부하면서 내가 잘하고 있는 것이 맞을까 하고 두려울 때가 많았으나, 이번 구현을 통해 "아! 그래도 성장하고 있긴하구나!" 라는 생각이 들었다.

늘 조금씩 차근차근 착실히 성장해야겠다!

토스 컨퍼런스를 보고 구현하려다가 어려움을 겪으신 분들, 또는 모달을 구현하시는 분들에게 이 글이 조금이라도 도움이 되길 바란다.



(ref)

profile
지식은 누가 origin인지 중요하지 않다.
post-custom-banner

0개의 댓글