리액트는 컴포넌트 기반의 UI 라이브러리 입니다. 이러한 특성 덕분에 화면에 보여주고 싶은 UI는 컴포넌트를 만들어서 조합해 사용합니다. 리액트에서의 모달도 이와 다르지 않은데요. 모달을 렌더링 하기 위해서 모달 컴포넌트를 만들어서 렌더링 해줘야 합니다. 이 때 발생하는 몇 가지 불편한 점과 어떻게 개선하면 좋을지 살펴보도록 하겠습니다.
리액트에서 모달을 직접 구현하기 귀찮으니 구글에서 react modal을 검색해 봅니다. 현재 날짜 기준으로는 react-modal 이라는 라이브러리가 바로 보이는데 이걸 이용해보겠습니다.
저는 귀찮아서 CodeSandbox 라는 멋진 서비스를 사용하도록 하겠습니다. 여기서는 CodeSandbox에 대해서는 자세히 설명하지 않겠습니다. 그냥 온라인상에서 빠르게 코딩을 할 수 있는 서비스라고 생각하면 이해가 쉬울것 같습니다.
CodeSandbox로 React 템플릿을 이용해서 만든 후에 좌측을 보면 dependency를 추가할 수 있습니다. 여기에 react-modal을 검색해서 추가해주도록 합니다.
이렇게 하면 빠르게 테스트 해볼 수 있는 환경이 생겼습니다.
이제 모달을 이용해 보도록 하겠습니다.
react-modal GitHub에 README를 읽어보면 처음에 모달을 렌더링할때 기준이 되는 엘리먼트를 지정해줘야 한다고 합니다.
리액트를 앱을 렌더링 해주는 root 엘리먼트를 똑같이 지정하면 될것 같습니다.
ReactModal의 setAppElement로 #root
를 지정해줍니다.
그리고 react-modal을 이용해서 모달 컴포넌트를 만들어 보도록 하겠습니다.
ReactModal은 isOpen prop을 받아서 모달의 열고 닫고를 컨트롤 해주기 때문에 MyModal 컴포넌트에도 isOpen을 받아서 ReactModal에 전달할 수 있도록 해줍니다.
이제 모달을 사용할 준비가 다된것 같습니다. 이렇게 만들어진 모달 컴포넌트를 가지고 일반적으로 사용하는 법에 대해서 먼저 살펴보도록 하겠습니다.
보통은 이렇게 만들어진 모달 컴포넌트는 이 모달을 띄워 줄 수 있는 컴포넌트에 같이 렌더링 해서 사용합니다.
App 컴포넌트에서 특정 버튼을 눌렀을때 MyModal을 열어주고 싶다고 생각해봅시다.
App 컴포넌트에서 MyModal을 열어줘야 하기 때문에 App 컴포넌트의 렌더링 부분에 MyModal을 같이 렌더링 해서 사용합니다.
그리고 모달을 열어주기 위한 isOpen 값을 사용하기 위해서 useState
도 같이 사용해줍니다.
이제 버튼을 눌렀을때 모달을 열어줘야 하기 때문에 onClick 이벤트를 처리하는 함수에서 state 값만 변경해주면 완성입니다.
모달 열기 버튼을 누르면 이렇게 모달이 열리게 됩니다!
위에 살펴본 방식은 가장 기본적인 방법으로 아주 간단하지만 몇가지 문제가 있습니다.
App 컴포넌트에 여러개의 모달이 필요하면 어떻게 될까요?
App 컴포넌트에서 모달을 열고 싶을 뿐인데 부수적인 코드가 너무 많아지는 현상이 나타나게 됩니다. App 컴포넌트 자체적인 로직이 있다면 저런 부수적인 코드도 같이 섞여있어 가독성도 같이 떨어지게 되겠죠.
그럼 다시 비대해진 App 컴포넌트를 다시 살펴볼까요? 이제 Modal 컴포넌트는 onSubmit과 onCancel이 필요합니다.
각 모달에 onSubmit, onCancel을 추가해볼까요? 우선 onSubmit과 onCancel을 처리해줄 함수를 정의해줍니다.
이렇게 정의한 함수를 각 모달에 prop으로 넘겨주도록 합니다.
App 컴포넌트가 엄청나게 뚱뚱해 집니다.
간단 합니다. 그냥 ChildComponent에 모달을 열어줄 수 있는 함수를 넘겨주고 ChildComponent에서는 이 함수를 실행하기만 하면 되죠.
이런 상황이 뭐가 문제가 되느냐하고 하실 수 있겠지만 개인적으로는 좋은 패턴으로 보이지는 않습니다.
부모 컴포넌트에서 모달을 열기위한 state 관리를 하기 때문에 생기는 현상입니다.
ChildComponent가 지금은 하나지만 만약 아까와 같이 ChildComponent에서도 열 수 있는 상황이 생겨야 한다면 또다시 각 ChildComponent에 openModal 이라는 함수를 넘겨줘야 하기 때문이죠.
useState를 직접 쓰지 않아도 되기 때문에 뚱뚱한 코드가 조금 더 줄어듭니다.
지금까지 일반적으로 모달을 사용할 때 발생하는 문제점들에 대해서 살펴봤습니다. 이런 문제점들로 인해서 컴포넌트의 코드가 불필요하게 복잡해지고 커진다는 점을 발견했는데요. 이런 것들을 좀더 좋은 방법으로 변경할 수 있다는 가정하에 코드가 많이 줄어드는것을 확인했습니다.
위에서 살펴봤던 개선항목들을 어떻게하면 구현할 수 있을지 같이 살펴보도록 하겠습니다.
위에서 발견한 문제는 사실 모달 관리 방법이 파편화돼있기 때문에 발생하는 문제입니다.
모달이 필요한 컴포넌트별로 각 모달 컴포넌트를 렌더링 하고, 모달을 열어줄 state를 스스로 관리하기 때문에 발생하는 문제입니다.
모달을 사용할 때 가장 핵심적인 내용은 모달을 열고 모달안의 액션에 따라 어떤 비지니스 로직을 처리해줄것이냐 입니다. 하지만 모달을 관리하는 코드때문에 코드가 너무 복잡해진다는데 있죠.
그럼 어떻게 위 개선 방법을 구현할 것이냐? 바로 파편화된 모달 관리를 효율적으로 한군데서 하자는 아이디어로 시작합니다.
바로 중앙화된 모달 관리라는것이죠.
App 컴포넌트에서 직접 모달을 열고 닫을 수 있도록 관리하던 state를 외부에서 관리하도록 해주겠습니다.
state 관리 도구는 여러가지가 있습니다만 이 포스트에서는 React에서 제공하는 ContextAPI를 사용하는것으로 설명하겠습니다.
ModalsContext 파일을 만들어서 구현을 시작합니다.
ModalsDispatchContext에는 모달을 열고 닫을 수 있는 함수를 구현해줍니다.
Context를 Ditpatch 와 State로 나눠서 한 이유는 ContextAPI의 성능 관련 이슈가 있어서 인데 이부분은 따로 언급하지 않겠습니다.
기회가 된다면 관련해서 글을 써보도록 하겠습니다.
이제 ContextAPI를 사용하기 위해서 Provider를 만들어 줄 차례입니다. 위에 만들었던 Context를 import해서 렌더링해줍니다. 거기에 Provider의 children으로 들어올 내용을 갖이 렌더링해주면 Provider의 하위 컴포넌트로 들어오는 컴포넌트는 모두 ModalsContext를 사용할 수 있게 됩니다.
이제 ModalsProvider에서 모달에 대한 state를 관리할 수 있도록 useState를 사용합니다.
다음으로 모달을 열고 닫아줄 수 있는 함수를 구현해줍니다. ModalsDispatchContext는 open, close 함수가 필요하기 때문에 이 값을 구현해주면 되겠습니다.
그럼 이제 모달을 관리해줄 state에 어떤 값을 저장할것인가 하는게 문제가 됩니다. 여러가지 고민해본 결과 컴포넌트와 거기에 넘겨줄 수 있는 props를 같이 저장해주면 좋겠다는 결론을 내렸습니다.
open 함수는 파라미터를 두 개 받습니다. 첫 번째는 열고 싶은 모달 컴포넌트, 두 번째는 모달 컴포넌트로 넘겨주고 싶은 props 입니다.
이 두 값을 받아서 { Component, props }
형태의 오브젝트를 만들어 저장을 해주도록 합니다.
리스트의 마지막에 모달을 추가하기 위해서 setter 함수의 파라미터로 함수를 이용합니다.
close 함수는 파라미터로 모달 컴포넌트를 받습니다. 그리고 state가 리스트 형태기 때문에 filter를 이용해서 파라미터 받은 모달 컴포넌트를 제외하고 필터링 해줍니다. 이러면 파라미터로 받은 모달 컴포넌트를 제외하고 나머지만 다시 저장해주게 됩니다.
그리고 마지막으로 ModalsDispatchContext에 넘겨주는 dispatch가 항상 재 생성돼서 리렌더링 되는것을 막아주기 위해 useMemo를 사용해줍니다.
ContextAPI를 사용하기 위한 Provider도 준비가 끝났습니다.
App 컴포넌트를 감싸주도록 index.js 에 ModalsProvider를 추가해줍니다. 이렇게 하면 ModalsProvider의 children은 모두 ModalsContext에 접근할 수 있게 되겠죠.
ModalsProvider를 통해서 ModalsContext를 제공해줬으니 이를 이용해서 사용하는 쪽을 구현해야 합니다.
useModals
라는 커스텀 훅을 만들어서 사용하도록 하겠습니다.
useModal.js로 파일을 만들고 커스텀 훅을 만들어 봅시다.
모달을 열 때는 openModal을 이용하도록 하고, 모달을 닫을때는 closeModal을 사용하도록 만들어 줍시다.
openModal과 closeModal 의 구현은 ModalsDispatchContext에 구현한 open, close를 이용합니다.
open
함수는 모달 컴포넌트와 props 를 넘겨줘야 하고, close
함수는 모달 컴포넌트만 넘겨주면 됩니다.
이제 훅을 사용해서 App 컴포넌트의 button 핸들러를 바꿔줍니다.
useModals 훅을 import 해주고, useModals가 반환하는 openModal을 사용합니다. 이 때 openModal의 파라미터로 모달 컴포넌트를 넘겨줘야 합니다. 다시 한 번 MyModal을 import 해서 넘겨줍니다.
이제 모달 열기 버튼을 누르고 난 후 Context의 State에는 아래와 같이 열고자 하는 모달의 컴포넌트와 props가 추가된걸 확인할 수 있습니다.
여기까지 하게 되면 모달을 열기위한 state 관리는 ContextAPI를 이용해서 작업이 끝나게 됩니다.
외부에서 관리하는 모달 State를 가지고 모달을 렌더링 해주는 부분은 다음 글에서 이어서 하도록 하겠습니다.