[React] Modal을 Redux로 구현해보자!

NB·2020년 9월 4일
9
post-thumbnail

웹페이지를 구현할 때, Modal이라는 개념은 정말 많이 쓰인다. 하지만 단방향으로만 데이터를 바인딩해주는 React 특성상 Modal 만들 때에는 구조적이나 z-index과 같은 뷰쪽에서 깔끔하지 않다는 문제가 발생한다..🤔

그래서, 혹시 Redux를 이용하여 Modal 구조를 이쁘게 만들 수 있지 않을까..? 라는 생각으로 이렇게 만들어보았다!

먼저 사용한 의존성 모듈은 다음과 같다.

  • styles-components
  • immutable.js

그렇다면, Modal을 만드는 과정을 따라가보자!

주의.
본 게시글에 나오는 코드는 핵심기능을 제외하고는 생략되어있습니다!

  • 2021년 5월 6일 기준으로 직렬화가 되지않는 아래 방법보다는 다른 방법을 사용하여 구현하게 되었습니다. 아래 방법도 동작은 하지만 redux-devtools 를 사용할 때, 의도치 않은 결과가 보일 수 있습니다. 자세한 내용은 본 포스트 제일 하단을 확인해주시길 바랍니다!

동작구조 🔧

이번에 만든 모달 구조는 Redux를 이용하여 만들어졌다. 즉, 모달을 여는 버튼을 클릭하면 Store에 해당 모달 요소를 set해준다. 그렇게 되면, 해당 모달 요소가 Modal-Wrapper에 뿌려지는 구조이다.

구조에 대해서 이해가 끝났다면, 본격적으로 코드 작성으로 넘어가보자.


Module 작성 💻

이번 실습에 작성된 모듈은 Ducks Pattern으로 작성되었으며 다음과 같다.

Ducks Pattern 이란?
Reducer 파일 내에 Actions 타입 및 생성자를 선언해놓는 구조를 의미한다. 그리고 이렇게 합쳐진 것을 Module이라고 칭한다.
[ What is Redux Ducks? - Matthew Holman ]

/*
	Types
*/
const SHOW_MODAL = 'modal/SHOW_MODAL';
const DROP_MODAL = 'modal/DROP_MODAL';
/*
	Actions
*/
export const showModal = (element)=> ({ type: SHOW_MODAL, payload: element });
export const dropModal = ()=> ({ type: DROP_MODAL});
/*
	InitialState
*/
const initialState = Map({
  show: false,			// 모달 표시 여부
  element: null			// 모달 Component
});
/*
	Reducer
*/
export default function snackbar(state = initialState, action) {
  switch(action.type) {
    case SHOW_MODAL:
      document.querySelector('body').style.overflow = "hidden";
      return state.set('show', true)
                  .set('element', action.payload);
    case DROP_MODAL:
      document.querySelector('body').removeAttribute('style');
      return state.set('show', false);
    default:
      return state;
  }
}

ShowModal의 인자로 들어온 Modal Component는 SHOW_MODAL Action을 통해서 store에 저장된다. 그리고 DropModal을 통해서 현재 표시된 Modal을 제거할 수 있다.


Container 작성 💻

Container에서 핵심적으로 보아야할 부분은 다음과 같다.

Event Propagation

Container에서는 모듈의 show의 여부에 따라서 모달의 표시 여부를 결정해준다. 이 때, 유심히 볼 부분은 preventModalOff 함수이다.

모달자체는 Background를 클릭하면 닫히게 동작하기 때문에, ModalWrapper element에 close Event를 넣어야 한다. 하지만, 이렇게 되면 Modal Background가 아닌 Modal 자체를 클릭해도 모달이 닫히게 될 것이다. 그래서 필요한 동작을 하는 함수가 PreventModalOff 이다. 이 함수는 모달을 클릭했을 때, Event가 Background로 전파가 되지 않도록 하는 역할을 한다. 자세한 내용은 Javascript Event Propagation에 적어두었다!

CSS

Scroll이 가능한 Page에서도 정상적으로 Modal이 표시되어야 한다. 그 때, 아래 코드와 같이 CSS를 설정해두면 해당 Modal Background 컴포넌트는 사용자가 스크롤위치의 변화에 상관없이 바로 표시될 것이다.

const Container = styled.div`
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background-color: rgba(249, 249, 249, 0.85);
  z-index: 100;
`;

const ModalWrapper = ({show, element, dropModal})=> {
  const Modal = element;

  const PreventModalOff = (e)=> {
    e.stopPropagation();
  }

  return (
    <>
      {show && <Container onMouseDown={dropModal}>    
        <Modal PreventModalOff={PreventModalOff} ModalOff={dropModal}></Modal>
      </Container>}
    </>
  );
}

App에 모달을 추가 💻

이제 모달이 표시될 공간을 넣어주어야 한다. 다음과 같이 넣어주면 된다. <Switch>...</SWitch> 는 react-router 를 통해서 Page를 바꿔주는 컴포넌트이다. 이 때, ModalWrapper 컴포넌트를 제일 하단에 위치하게 해준다.

const App = ()=> {
  return (
    <>
      <Switch>
        <Route path="/" component={HomePage} exact />
        <Route path="/board" component={BoardPage} exact />
        <Route path="*" component={NotFound} status={404} />
      </Switch>
      <ModalWrapper></ModalWrapper>
    </>
  );
}

위와 같이 ModalWrapper를 하단에 위치하게 함으로써 이전에 발생되던 z-index 문제가 발생하지 않게 된다. 또한 개인적인 느낌이겠지만, 깔끔해진다..😀


실행 🛹

여기까지 왔다면, 이제 어떤 컴포넌트에서도 Modal 모듈의 showModal 액션함수를 실행시키면 등록하면 모달을 띄울 수 있다!
아래에서 나오는 AuthModal은 실제로 ModalBackgrond에서 띄어질 Modal 컴포넌트이다.

import AuthModal from '../modal/path'

const AuthBtn = ({showModal})=> {
  return (
    {/*버튼을 클릭하면 로그인 모달이 열린다.*/}
    <Button onClick={()=> {showModal(AuthModal)}}>로그인</Button>
  );
}

마치며

사실 이 글을 쓸 때에는 Portal구조가 좋을지 Redux를 이용한 구조가 좋으지 고민하다가 한 번 Redux를 이용해서 구현해봤다. 사실 Portal 방법이 더 깔끔해보여서 다음에 모달구조를 만들 때에는 Portal 방법으로 만들어봐야겠다!


2020-09-06 추가

생각해보니, 이 방식으로 Modal을 제작하게 되면, 상위 Component에서의 Event를 Modal에 전달하기가 애매해질 것 같다. 예로 들면 Root에 속한 Element의 State를 변경하는 함수가 Modal에서 작동해야할 경우, Portal을 이용하면 함수를 그대로 모달로 바인딩해주면 되지만, 내 방법을 사용하면 그 state도 store에 등록시켜 의도치않게 reducer를 추가시키는 경우가 발생할 수 있다.

뭐가 더 나은 선택일까!?!? 🤔

2020-10-10 추가

Portal 방법으로 modal을 작성하다가, 약간 난감한 상황이 발생했다. 예로 들어서 A 라는 모달 내에서 B 라는 모달을 열려고 할 때, B는 A 컴포넌트 내에서 Portal을 열어서 렌더링되게 된다. 하지만 이 때, B라는 모달을 열면서 동시에 A라는 모달을 닫는 순간, A에 속한 B 모달도 닫히게 된다. 이 과정을 수정하기 위해서, B 모달을 작동하는 State를 A 모달 상위 컴포넌트로 빼고, A 모달에게 B 모달을 작동하는 setState를 넘겨주어 문제를 해결하였다.

이러한 점에서 볼 때, 아직 어떤 방법이 더 편하다기 보다는 그냥 이렇게도 쓰고, 저렇게도 쓰는 것 같다. 물론 Redux 방법에서는 Props를 넘겨주기 위해서는, Props까지도 상태 관리를 해주어야 하는 더러운 코드가 발생할 수도 있다. 사실 나도 그 부분까지는 직접 구현은 안 해봤다. ^^;;

한달 전에 쓴 글이지만, 개발을 진행하는 와중에 틈틈히 고민해보고 생각해보니 이와 같은 결론이 나게 되었던 것 같다. +_+

2021-05-06 추가

기존 Redux 방법으로 구현하게 되면, 가장 문제점이 발생하는 것이 직렬화 데이터를 저장한다는 점이다. 이런 방식으로 저장하게 되면 redux devtools에서 타임머신 기능을 사용할 때, 의도치 않은 결과가 발생할 수가 있다. 그렇기에 수정한점이 React Component 자체를 넘기는 것이 아닌, Component명을 보내서 Modal Container에서 import된 모달을 선택적으로 따로 불러오는 것이다. 본인은 이 방법을 사용하여 직렬화 문제점을 해결하게 되었다. 자세한 코드는 여기에서 확인할 수 있다.

profile
𝙄 𝙖𝙢 𝙖 𝙛𝙧𝙤𝙣𝙩𝙚𝙣𝙙 𝙙𝙚𝙫𝙚𝙡𝙤𝙥𝙚𝙧 𝙬𝙝𝙤 𝙚𝙣𝙟𝙤𝙮𝙨 𝙙𝙚𝙫𝙚𝙡𝙤𝙥𝙢𝙚𝙣𝙩. 👋 💻

7개의 댓글

comment-user-thumbnail
2020년 9월 23일

모달 구현하신 방법 잘 보았습니다 :D

1개의 답글
comment-user-thumbnail
2020년 10월 1일

몇가지 보면서 의문? 지적할 수 있는 내용? 을 적자면..

결국 여기서 리덕스의 역할은... 모달을 띄울려는 컴포넌트에서 동적으로 어떤 컴포넌트를 렌더링할지 결정하기 위해 모달이 띄워질 지정된 컴포넌트 위치로 컴포넌트를 전달하고자 하는 통신수단으로 쓰이는거네요? 단순히 통신을 위한거면 다른 좀더 직접적인 방법도 있지 않나요? 리덕스를 사용해야하는 이유가 있을가요?

또 보통 포탈을 사용해 모달을 구현하는 방식을 많이 사용하는데요, 띄울 컴포넌트에 대한 선언성을 유지하면서 실제 컴포넌트가 띄워질 위치를 안으로 감추기 때문에 props사용이 자유롭고 복수의 모달을 띄울 수 있는데.. 지금 이 방식은 단일 인터페이스로 강제해 props제약이 심하고 모달도 하나밖에 띄울수 없게 제약되기도 하고... 특히 리덕스의 장점인 단일스토어로 serialize 용이하다는것이 serialize하기 어려운 내용(모달컴포넌트)이 침투해서.. 결론적으로 꽤 많은 단점들이 생기는 걸루 보이는데요. 잃는것 대비해서 다른 얻는 장점이 있을까요?

추가로 리듀서안에 document.body.style 바꾸는 사이드이펙트가 있는게 별루 바람직해보이지 않네요. 예컨데 다른쪽에서 이같이 body 스타일을 바꾸는 리듀서가 또 하나 추가된다면 둘이 충돌하지 않을까요

1개의 답글
comment-user-thumbnail
2020년 11월 20일

안녕하세요? 혹시 전체 코드 예시를 볼 수 있을까요? 참고해서 프로젝트에 적용중인데 ModalWrapper에 어떻게 element등의 props를 넘겨주는지 등의 문제에서 막혀있습니다 ㅠㅠ 그래서 괜찮으시면 코드를 더 참고할 수 있을지 궁금합니다! 그리고 예시로 보여주신 AuthModal은 뷰만 보여주게끔 구현하셨나요?

1개의 답글