여러개의 모달을 전역 상태로 관리하기 (feat. context)

younoah·2022년 3월 15일
2
post-custom-banner

와플카드 서비스가 궁금하다면!
와플카드 서비스 둘러보기 : https://waffle-card.com/
와플카드 깃허브 둘러보기 : https://github.com/waffle-card

🏃🏻‍♂️ intro

와플카드 서비스에서는 하나의 페이지에서 여러개의 모달을 관리해야 한다.

  • 홈페이지
    • 와플카드 생성 모달
    • 와플카드 수정 모달
    • 채팅카드 모달
  • 마이페이지
    • 이름(닉네임) 수정 모달
    • 비밀번호 수정 모달

하나의 페이지에서 여러개의 모달을 관리하다보니 코드가 상당히 지전분해지는 현상을 발견하게 되었다.

이번 글은 여러개의 모달을 효율적으로 관리하기 위해 ModalsProvider를 도입하는 여정을 작성해보고자 한다.

우선 와플카드에서 사용되는 모달 컴포넌트 코드를 보자.

const Modal = ({
  children,
  visible,
  width,
  height,
  backgroundColor = Common.colors.background_modal,
  onClose,
  ...props
}: ModalProps) => {
  const ref = useClickAway(() => {
   onClose && onClose();
  });

  return (
    <Portal>
      <BackgroundDim style={{ display: visible ? 'block' : 'none' }}>
        <ModalContainer
          ref={ref}
          width={width}
          height={height}
          backgroundColor={backgroundColor}
          {...props}
        >
          {children}
        </ModalContainer>
      </BackgroundDim>
    </Portal>
  );
};

모달 컴포넌트는 핵심 prop은 아래와 같다.

visible : 모달이 보여지는지 여부

onClose : 모달을 종료하는 콜백함수

이 2개의 prop은 모달을 사용하는 컴포넌트에서 관리해주어야 한다.


Modal 컴포넌트 사용예시

const App = () => {
  const [isOpenModal, setIsOpenModal] = useState(false);
  
  const handleCloseModal = () => {
    setIsOpenModal(false);
  }
  
  return (
  	<Modal visible={isOpenModal} onClose={handleCloseModal} />
  )
}

하나의 모달을 관리하기 위해 모달을 visible상태와 handleClose 함수를 관리해야한다.


그렇다면 만약 모달을 3개 관리해야 한다면 어떨까?

const App = () => {
  const [isOpenModal1, setIsOpenModal1] = useState(false);
  const [isOpenModal2, setIsOpenModal2] = useState(false);
  const [isOpenModal3, setIsOpenModal3] = useState(false);
  
  const handleCloseModal1 = () => {
    setIsOpenModal1(false);
  }
  
  const handleCloseModal2 = () => {
    setIsOpenModal2(false);
  }
  
  const handleCloseModal3 = () => {
    setIsOpenModal3(false);
  }
  
  return (
    <>
      <Modal visible={isOpenModal1} onClose={handleCloseModal1} />
      <Modal visible={isOpenModal2} onClose={handleCloseModal2} />
      <Modal visible={isOpenModal3} onClose={handleCloseModal3} />
    </>
  )
}

모달의 갯수만큼 관리해줘야 하기 때문에 visible상태와 handleClose 함수의 갯수가 그 만큼 증가하게 된다.

App 컴포넌트에서 모달을 열고 닫고 싶을 뿐인데 부수적인 코드가 너무 많아지는 현상이 나타나게 된다. App 컴포넌트 자체적인 로직이 있다면 저런 부수적인 코드도 같이 섞여있다면 가독성이 현저히 떨어지게 된다.

만약 모달에 전달해줘야하는 prop이 증가한다면 더욱 끔찍한 상황을 직면하게 될 것이다.


ModalsProvider

이를 해결하기 위해서 모달의 상태관리를 부모가 하지않고 전역상태로 한번에 관리하는 방식을 고안하게 되었다.

import React, { useState } from 'react';
import { createContext } from 'react';
import { ModalsStateType } from '@/types';

interface ModalsProviderProps {
  children: React.ReactElement | React.ReactElement[];
}

export const ModalsDispatchContext = createContext<{
  open: (Component: React.ReactElement, props: { [key: string]: any }) => void;
  close: (Component: React.ReactElement) => void;
}>({
  open: () => {
    return;
  },
  close: () => {
    return;
  },
});

export const ModalsStateContext = createContext<ModalsStateType[]>([]);

const ModalsProvider = ({ children }: ModalsProviderProps) => {
  const [openedModals, setOpenedModals] = useState<ModalsStateType[]>([]);

  const open = (
    Component: React.ReactElement,
    props: { [key: string]: any },
  ) => {
    setOpenedModals(modals => {
      return [...modals, { Component, props }];
    });
  };

  const close = (Component: React.ReactElement) => {
    setOpenedModals(modals => {
      return modals.filter(modal => modal.Component !== Component);
    });
  };

  const dispatch = { open, close };

  return (
    <ModalsStateContext.Provider value={openedModals}>
      <ModalsDispatchContext.Provider value={dispatch}>
        {children}
      </ModalsDispatchContext.Provider>
    </ModalsStateContext.Provider>
  );
};

export default ModalsProvider;

ModalsProvider는 상태 타입을 모달 컴포넌트를 인자로 하는 배열로 지정해주었다. 즉, ModalsProvider는 렌더링할 모달들을 배열로써 관리한다.

그리고 아래 2가지 dispatch 메서드를 제공한다.

  • open 함수
    • 렌더링하고 싶은 모달 컴포넌트와 해당 모달 컴포넌트에 전달하고 싶은 prop객체를 인자로 받는다.
    • openedModals 상태에 {Component, props} 의 원소 형태로 배열에 저장한다.
  • close 함수
    • 렌더링을 해제하고 싶은 모달 컴포넌트를 인자로 전달한다.
    • openedModals 상태에서 해당 모달 컴포넌트를 삭제한다.

그렇다면 어떻게 렌더링을 시킬까?


Modals 컴포넌트

ModalsProvider의 배열의 state에 존재하는 모달 컴포넌트들은 Modals 컴포넌트에서 렌더링을 처리한다.

import React, { useContext } from 'react';
import { ModalsDispatchContext, ModalsStateContext } from '@/contexts';
import { CardEditModal, ChattingCardModal } from '@/components';

export const modals = {
  cardEditModal: CardEditModal,
  chattingCardModal: ChattingCardModal,
};

const Modals = () => {
  const openedModals = useContext(ModalsStateContext);
  const { close } = useContext(ModalsDispatchContext);

  return openedModals.map((modal, index) => {
    const { Component, props } = modal;
    const { onSubmit, ...restProps } = props;
    const onClose = () => {
      close(Component);
    };

    const handleSubmit = async () => {
      if (typeof onSubmit === 'function') {
        await onSubmit();
      }
    };

    return (
      <Component
        {...restProps}
        key={index}
        onClose={onClose}
        onSubmit={handleSubmit}
      />
    );
  });
};

export default Modals;

App에서는 아래와 같이 modalsProvider와 Modals 컴포넌트를 사용하면 된다.

import { Modal1, Modal2, Modal3 } from '@/components'

const App = () => {  
  const { open, close } = useContext(ModalsDispatchContext);
  
  const handleClick1 = () => {
   	open(Modal1, {onClose: close});
  }
  
  const handleClick2 = () => {
		open(Moda2, {onClose: close});
  }
  
  const handleClick3 = () => {
		open(Moda3, {onClose: close});
  }
  
  return (
    <>
    	<button onClick={handleClick1}>모달1</button>
		<button onClick={handleClick2}>모달2</button>
    	<button onClick={handleClick3}>모달3</button>
    	<Modals />
    </>
  )
}

부모컴포넌트인 App에서 모든 모달의 상태를 관리하지 않게되어 코드가 훨씬 간결해진 것을 확인할 수 있다.

profile
console.log(noah(🍕 , 🍺)); // true
post-custom-banner

1개의 댓글

comment-user-thumbnail
2023년 6월 11일

잘 보고 갑니다

답글 달기