[react] Modal Modal Modal!

ARA JO·2021년 2월 26일
10

Simtime 개발일지

목록 보기
5/7

목표

생각보다 Modal을 많이 사용하게 된다. 하지만 그 때마다 Modal open/close 로직을 심어야 하는 반복되는 과정이 너무너무너무 지겨워서 Modal의 상태를 전 화면에서 공유할 수 있도록 만들고 싶어졌다.

react context와 custom hook으로 반복되는 modal 코드를 줄여보자.

 function Friends(props) {
  const [isModalOpen, setIsOpenModal] = useState(false);
  const [targetModal, setTargetModal] = useState("friend"); //friend, group


  const handleOpenModal = (target) => {
    setTargetModal(target);
    setIsOpenModal(true);
  };

  const handleCloseModal = () => {
    setIsOpenModal(false);
  };

 return (
  <Wrap>
      <Section bottom="30px">
        <SectionTitle>
          <Header type="h3" color="MAIN_COLOR">Friends</Header>
          <StyledSearch width="125px" desc="Find a friend" height="25px" />
        </SectionTitle>
        .
        .
        생략
        .
        .
 		{isModalOpen && (
          <ModalPortal
            children={
              <Modal onClose={handleCloseModal}>
                {targetModal == "group" ? <div>group</div> : <div>member</div>}
              </Modal>
            }
          ></ModalPortal>)}
  </Wrap>
 );

시도

Redux로 관리할까 생각도 해봤지만, Redux는 서버와 주고 받는 데이터를 위한 용도로만 남겨두고 싶다.

(추가 읽을거리 - https://blueshw.github.io/2017/06/26/presentaional-component-container-component/)

Modal open/close 기능은 오직 UI문제이니 react에서 제공하는 Context를 사용해보기로 했다.

리덕스의 state가 복잡해지는 것을 방지하고, 기능을 구분할 수 있으며 Context 연습용도(!)로 제격이다.

구글링 하자마자 결과 상단에서 좋은 글 발견

content로 component를 넘겨줄 수 있을지만 확인이 되면, 완벽하게 원하는 방식이다. 이미 ModalPortal과 Modal 디자인은 되어있는 상태이니 빠르게 시도해보자.

결과

매우 성공적! 속이 다 시원하다! 거의 그대로 했는데 바로 성공했다. 적용 코드는 아래와 같다.

1)useModal.js

import React from "react";

export default () => {
  let [modal, setModal] = React.useState(false);
  let [modalContent, setModalContent] = React.useState("I'm the Modal Content");

  let handleModal = (content = false) => {
    setModal(!modal);
    if (content) {
      setModalContent(content);
    }
  };

  let closeModal = (content = false) => {
    setModal(false);
    setModalContent(content);
  };

  let openModal = (content = false) => {
    setModal(true);
    if (content) {
      setModalContent(content);
    }
  };

  return { modal, handleModal, openModal, closeModal, modalContent };
};

//openModal, closeModal 추가했다.

2) modalContext.js

import React from "react";
import useModal from "../hooks/useModal";
import ModalPortal from "../AtomicComponents/A-Atomics/Modal/ModalPortal";

let ModalContext;
let { Provider } = ( ModalContext = React.createContext());

let ModalProvider = ({ children }) => {
  let { modal, handleModal, openModal, closeModal, modalContent } = useModal();
  return (
    <Provider value={{ modal, handleModal, openModal, closeModal, modalContent }}>
      <ModalPortal />
      {children}
    </Provider>
  );
};

export { ModalContext, ModalProvider };

3) App.js

ModalProvider 추가

import { ModalProvider } from "../contexts/modalContext";

//..생략..//

class App extends Component {
//..생략..//
  render() {
    return (
      <Provider store={store}>
        <ModalProvider>
        <AlertProvider template={AlertTemplate} {...alertOptions}>
          <HashRouter>
            <Fragment>
              <GlobalStyle />
              <AppContents>
                <Header />
                <Alerts />
                <Switch>
                  <PrivateRoute exact path="/" component={CalendarPage} />
                  <Route exact path="/register" component={Register} />
                  <Route exact path="/login" component={Login} />
                  <Route exact path="/friends" component={Friends} />
                  <Route exact path="/mysimtime" component={MySimtime} />
                </Switch>
              </AppContents>
            </Fragment>
          </HashRouter>
        </AlertProvider>
        </ModalProvider>
      </Provider>
    );
  }
}

ReactDom.render(<App />, document.getElementById("app"));

4) ModalPortal.js

import React, { Fragment } from "react";
import ReactDOM from "react-dom";
import styled from "styled-components"
import { ModalContext } from "../../../contexts/modalContext";
import GlobalStyle from "../../GlobalStyle";

const MyModal = styled.div`
  background: rgba(0, 0, 0, 0.25);
  position: fixed;
  left: 0;
  top: 0;
  height: 100%;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 999;
`;

const ContentWrap = styled.div`
  border: solid 1px red;
  background: rgba(0, 0, 0, 0);
  width: auto;
  height: auto;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-items: flex-end;
  @media only screen and (max-width: 320px) {
    width: 98%;
    position: relative;
  }
`;

const ModalPotal = (props) => {
  const el = document.getElementById("app-modal");
  let { modalContent, handleModal, modal } = React.useContext(ModalContext);
  if (modal) {
    return ReactDOM.createPortal(
      <Fragment>
             <MyModal>
               <ContentWrap>
                  {modalContent}
               </ContentWrap>
             </MyModal>
         </Fragment>,
      el
    );
  } else return null;
};

export default ModalPotal;


// 이전코드
// const ModalPotal = (props) => {
//   const el = document.getElementById("app-modal");
//   return ReactDOM.createPortal(
//   <Fragment>
//       <MyModal>
//         <ContentWrap>
//             {props.children}
//         </ContentWrap>
//       </MyModal>
//   </Fragment>, el);
// };

5) Component 사용 예시 - CalendarPage

Friends 보다 조금 더 정돈된 CalendarPage를 예시로 기록한다.

useContext를 통해 ModalContext의 handleModal과 closeModal을 가져왔다.

//..생략..//
import EventMaker from "../D-Templates/Event/EventMaker"; //모달 Content
import { ModalContext } from "../../contexts/modalContext";

//..생략..//

function CalendarPage() {
  const { handleModal, closeModal } = React.useContext(ModalContext);

  useEffect(() => {});

  return (
    <Wrap>
      <LeftWrap>
        <StyledFilter />
        <StyledCalendar height="98%" />
      </LeftWrap>
      <RightWrap>
        <StyledDashedButton
          hasIcon={true}
          src="https://bucket-simtime.s3.ap-northeast-2.amazonaws.com/static/assets/img/icons/edit2.png"
          onClick={() => handleModal(<EventMaker onClose={closeModal}/>)}
        >
          Add a new event
        </StyledDashedButton>
        <StyledDetail height="98%" />
      </RightWrap>
    </Wrap>
  );
}

export default CalendarPage;

'새로운 이벤트 만들기' 버튼 onClick 이벤트 발생 시, handleModal이 EventMaker를 Content로 갖는 Modal을 오픈해준다.

toggle 형식(!modal)의 handleModal만 사용하면 추가적인 코드 작성없이 깔끔하지만, CloseModal이 따로 있는게 직관적이고 확실한 동작을 나타낸다고 생각해서 closeModal 추가했다.

EventMaker(modalContent)가 이벤트 생성 완료 후 자체적으로 Modal을 닫을 수 있도록 CloseModal 함수를 props로 전달했다. EventMaker에서 다시 ModalContext를 import할 필요가 없어 간결해진다.

<StyledDashedButton
          hasIcon={true}
          src="https://bucket-simtime.s3.ap-northeast-2.amazonaws.com/static/assets/img/icons/edit2.png"
          onClick={() => handleModal(<EventMaker onClose={closeModal}/>)}
        >
          Add a new event
</StyledDashedButton>

이 글의 첫번째 코드블럭으로 돌아가 modal부분을 비교해보면 훨~~~씬 사용성이 좋아졌다.

더 훈련(!)해서 이런 공통적이고 반복적인 UI작업에 custom hook과 context를 적극 사용할 수 있도록 해야겠다.

profile
Sin prisa pero sin pausa (서두르지 말되, 멈추지도 말라)

3개의 댓글

comment-user-thumbnail
2021년 10월 1일

도움되었어요, 감사합니다~

답글 달기
comment-user-thumbnail
2022년 2월 4일

궁금한 점이 있습니다.

왜 useState를 let 키워드로 할당하시는지요?
두번째 매개변수인 setState dispatch 함수로 변경하는 것 말고 직접적인 변경이 가능함을 알려주는 것이 아닌가 생각이 듭니다.
보통은 const로 할당하는 것을 더 많이 본지라.... 다른 이유가 있으신지 궁금합니다.

1개의 답글