생각보다 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 디자인은 되어있는 상태이니 빠르게 시도해보자.
매우 성공적! 속이 다 시원하다! 거의 그대로 했는데 바로 성공했다. 적용 코드는 아래와 같다.
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 추가했다.
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 };
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"));
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);
// };
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를 적극 사용할 수 있도록 해야겠다.
도움되었어요, 감사합니다~