히치키치

모달 현명하게 관리하기 본문

Cognisle

모달 현명하게 관리하기

히치키치 2024. 7. 1. 18:07

다음은 Cognisle에서 사용되는 다양한 모달의 형태이다.

 

 

[1] 서비스 및 요구 사항 분석

 

모달 컴포넌트의 관리와 관련되어 필요한 기능을 다음과 같다.

1. 5초가 지나면, 자동으로 꺼짐

2. 닫힘 버튼이 있을 때, 닫힘 버튼을 누르면 꺼짐

3. 모달 밖의 뒷 영역을 누르면, 꺼짐

→ 모달 생성과 열고 닫음 제어

 

모달 컴포넌트의 디자인과 관련되어 필요한 기능을 다음과 같다.

1. 뒷 배경색 직접 지정 또는 투명 가능해야 함

2. 화면의 가장 가운데 위치해야 함

3. 내용 및 컴포넌트에 따라 높이와 너비가 다르다

→ 모달 공통 컴포넌트의 기본 레이아웃 및 스타일 

 

[2] 제어 관련 로직 및 구조 설계

 

모달의 생성과 열고 닫음을 어떻게 제어할 것인가?

1. React Portal을 사용해 생성함

2. Zustand의 상태관리로 단일 origin에서 열고 닫음 상태를 책임짐

3. 모달 영역 밖의 클릭 이벤트 발생 시 모달 닫음 실행

4. 5초 지난 경우 닫음 실행

 

[3] React Portal을 사용해 모달 생성

 

React Portal이란?

부모 컴포넌트 DOM 계층 구조 바깥에 있는 DOM 노드에 자식을 렌더링 하는 기술

createPortal을 사용하면 일부 자식을 DOM의 다른 부분으로 렌더링 가능함

 

React Portal을 사용한 이유는 무엇인가?

1. DOM 구조의 독립성 유지

부모 컴포넌트가 랜더링되면서 자식 컴포넌트가 랜더링되는 Tree 구조로 인해 부모-자식 관계에서 불편함이 발생함.

리액트 모달을 사용해 모달을 생성한 경우, 부모 컴포넌트의 일부분이 아닌 최상위 루트의 독립적인 위치에서 추가되어 랜더링됨. 즉, 기존 DOM 구조 내 다른 요소들에 의해 시각적으로 기능적으로 영향을 받지 않게 함.

 

2. CSS 스타일링 문제 발생 방지

CSS 스타일 과정에서 기존 DOM 구조 내에서 랜더링 될 경우, CSS 상속/스타일규칙/Z-index 등으로 예기치 않은 스타일 문제가 발생할 수 있는데 React Portal을 활용해 이러한 문제점을 사전에 방지할 수 있음.

 

3. 접근성 개선

모달이 최상위 DOM 노드에 존재하면 스크린 리더와 같은 접근성 도구가 보다 원활하게 사이트를 이해하고 전달 가능하다고 함.

 

4. DOM 노드의 물리적 배치만 변경함

물리적 배치 변경 : DOM 계층 구조 밖에 있는 DOM 노드의 자식으로 랜더링됨

변경되지 않는 것 : (논리적 구조는 변경되지 않는다고 생각하면 편하다!!!)

portal에 렌더링하는 JSX는 여전히 이를 렌더링하는 부모 컴포넌트의 자식 노드 역할함에 따라 부모가 제공하는 컨텍스트에 접근가능함. 예를 들어, 자식은 부모 트리가 제공하는 context에 액세스할 수 있으며, 이벤트는 React 트리에 따라 자식에서 부모로 버블 업됨

 

공식 문서

https://react.dev/reference/react-dom/createPortal

 

createPortal – React

The library for web and native user interfaces

react.dev

 

 

Modal이 생성될 root 지정

 

/pages/_document.tsx

 

앞으로 모든 모달은 div로 id가 modal-root인 node의 자식으로 생성될 것임

 

 

React Portal을 사용해 모달 생성하는 컴포넌트

ReactDOM.createPortal(child, container)

child : 랜더링 할 수 있는 리액트 컴포넌트
container : Dom Element로 child에 넣은 컴포넌트가 부모 컴포넌트가 아닌 이 container의 컴포넌트로 랜더링됨

 

 

children 인자로 랜더링할 컴포넌트를 전달 받음. createPortal을 이용해 리액트 컴포넌트인 children를 document 하위 div로 id가 modal-root인 node에 자식으로 랜더링함. nextjs 사용 중으로 첫번째 랜더링에서는 SSR 방식을 취하기 때문에 서버에서 document를 찾을 수 없음. useEffect를 사용해 컴포넌트가 갱신되거나 마운트 된 이후로 작동하도록 하기 위해 useEffect를 사용함.

 

사용할 모달에 적용

부모로 PortalModal을 감싸고 보여줄 모달을 자식으로 넣으면 됨

 

 

실제 nextjs의 pages 하위 컴포넌트가 랜더링되는 div(id=_next)가 아닌 div(id=modal-root)에서 모달이 생성되었음을 볼 수 있음

 

[4] Zustand의 상태관리로 단일 origin에서 열고 닫음 상태를 책임짐

 

Zustand의 상태관리를 활용해 제어하고자 한 이유는?

1. 모달 열고 닫는 함수를 매번 작성하는 것은 코드의 중복 작성임

2. 중앙 집중식 상태 관리

모달이 사용되는 컴포넌트에서 모달을 관리하는 로직까지 담당하게 하는 것은 단일 책임 원칙을 위반함

3. 오직 한개의 모달만 띄우는 상황임. 중첩적으로 여러 모달을 띄운다거나 여러번 모달을 계속 띄워야 하는 경우가 없음

→ 전역으로 관리해 한 번에 한 개만 띄움. 필요한 곳에서 호출해 사용함

 

zustand에 모달 제어 관련 store 및 액션 설계

중앙에서 관리할 상태 : 현재 모달 열림 여부 
중앙에서 공유될 액션 : 모달 열기, 모달 닫기, 모달 토글 (현재 모달 상태의 반대로)

 

 

중앙에서 관리할 상태 : 현재 모달 열림 여부 (isOpen)

중앙에서 공유될 액션 : 모달 열기(openModal), 모달 닫기(closeModal), 모달 토글 (toggleModal : 현재 모달 상태의 반대로)

 

처음에 사이트를 방문했을 때는 당연히 아무 모달도 열린 상태가 아님으로 isOpen는 false로 지정함. 이후 각 액션에 맞게 true와 false 값을 지정하도록 함. 상태와 액션을 useModalStore와 useModalActions로 export하여 앞으로 모든 사이트 내 모달의 열고 닫음은 이 zustand의 모달 관련 store와 action으로만 제어함.

 

 

위는 로그인 폼 컴포넌트로 에러가 발생한 경우 이를 보여주는 모달 (StateModal)이 열리고 닫힘. StateModal에 열고 닫음 상태(isOpen)와 제어하는 함수(handleClose)를 useModalStore와 useModalAction으로 불러와서 State 모달의 props로 전달함.

 

[5] 모달 영역 밖의 클릭 이벤트 발생 시 모달 닫음 실행

 

모달 뿐만 아니라 드롭 다운이 열린 상태, 멀티 탭 바 중 하나가 선택된 상태 등 해당 UI 영역이 아닌 다른 곳을 클릭 했을 때, 해당 UI에 어떤 작동이 되어야 하는 상황이 종종 있다. chakra-UI에 useOutsideClick으로 제공되는 것만 봐도 빈번하게 사용되는 것을 알 수 있다. 이걸 참고해서 커스텀 훅 형태로 만들고자 했다..

 

https://v2.chakra-ui.com/docs/hooks/use-outside-click

 

Chakra UI - A simple, modular and accessible component library that gives you the building blocks you need to build your React a

Simple, Modular and Accessible UI Components for your React Applications. Built with Styled System

v2.chakra-ui.com

 

useOutsideClick

외부 클릭 이벤트를 감지하는 커스텀 훅으로 특정 요소 외부에서 클릭이 발생했을 때 지정된 핸들러 함수 호출

 

 

useOutsideClick(ref, handler)

ref : 대상이 되는 영역
handler : ref에서 지정한 영역 외에서 이벤트 발생한 경우 호출되는 함수

 

 

useEffect 

의존성 배열 (ref, handler) :
대상이 되는 영역(ref) 또는 핸들러 함수(handler)가 바뀐 경우 useEffect 다시 실행됨

이벤트 리스너 함수 (listener) :
ref.current 존재 && 이벤트의 타겟이 ref.current의 자식 요소가 아님 = 지정 영역 외에서 이벤트 발생 → handler 함수 호출
ref가 null || ref.current가 없음 → 아무 일도 발생하지 않음

이벤트 리스너 등록 :
document.addEventListener로 click 이벤트에 대한 이벤트 리스너 함수 등록

이벤트 리스너 제거
useEffect 내 return 문과 document.removeEventListener로 click 이벤트에 대한 이벤트 리스너 함수 제거

 

 

모달 컴포넌트에 useOutsideClick 적용

 

contentRef :
특정 DOM 요소에 대한 참조를 만들기 위해 useRef 사용해 { current: null } 형식의 ref 객체 생성함. 
ref 속성을 통해 ref 객체와 참조할 대상 연결함.

isOpen? contentRef:null 
모달이 열려 있는 경우에만 해당 DOM 요소 전달

 

 

[6] 5초 지난 경우 닫음 실행

 

 

setTimeout과  Zustand의 상태관리 useModalActions의 닫기 액션을 사용함. 다만, 시간에 따른 모달 닫기는 추후 더 다른 모달이 추가되면서 적용이 안되는 것도 있을 수 있고... 내용에 따라 시간이 5초보다 더 길거나 적은 것도 있을 수 있으니 모달 컴포넌트 공통 제어가 아닌.. 각 모달 컴포넌트에 useEffect와 함께 개별적으로 작성함.

 

 

요렇게 작성해서 쏙 넣어주면 됨!!

 

[7] 모달 공통 레이아웃 및 스타일

 

모달 외부 뒷배경, 모달 내부 콘텐츠 컴포넌트 등 레이아웃 처리를 위해 JSX 구조를 다음과 같이 잡았다.

Portal Modal:
컴포넌트로 해당 모달이 열리는 컴포넌트의 하위 자식이 아닌 html Id값이 modal-root인 곳에 하위 children 요소가 랜더링됨

ModalWrapper와 Content:
modal-root가 nextjs pages와 같은 위계에 위치해 있기 때문에 ModalWrapper가 브라우저 전체 영역을 담도록 해 이 전체 영역에 대해 실제 모달 UI와 내용이 담긴 Content를 위치시키고 배경색 등을 스타일하고자 함

 

 

[8] 페이지 별 모달의 고유 배경색 지정

 

모달 뒷배경은 디자이너분이 주신 시안따라 페이지별로 고유한 색을 가졌다.

 

따라서 key값으로 페이지 url을, value 값으로 해당 페이지의 배경색값으로 상수 객체를 생성해 관리했다!!!

useRouter로 pathname을 받아오고 이를 해당 상수의 키값으로 호출하면 컴포넌트의 props로 해당 페이지에 맞는 배경색을 전달 가능하다!!

 

[9] Full Screen에 대한 모달 중앙 정렬과 배경색 스타일링

 

브라우저 전체 화면 기준으로 부모 요소나 스크롤 등과 무관하게 뷰포트에 상대적으로 고정되도록 position fixed를 사용했다. 부모 요소의 너비와 높이를 꽉 채우기 위해 top: 0; left: 0; bottom: 0; right: 0;가 필요했는데 inset:0으로 한번에 설정했다!!! 꽉 채워진 너비와 높이에 대해 props로 받은 페이지 배경색을 styled-component의 동적으로 스타일 입히는 방식을 적용해 배경을 스타일링했다. 또, 너비와 높이가 전체 화면이 되니 하위 요소인 모달 내용이 담긴 Content를 flex와 center로 손쉽게 중앙 정렬 가능함. 

 

 

 

이게 다 ReactPortal을 사용해 모달을 생성하는 방식을 사용해서 손쉽고 직관적이게 스타일링 작업을 한 것 같다!! 불필요하게 relative, absolute 부모 자식 관게에 대한 위치 생각하면서 작업할 필요가 없어졌다!!!

 

https://stackoverflow.com/questions/28080910/what-does-top-0-left-0-bottom-0-right-0-mean

 

What does "top: 0; left: 0; bottom: 0; right: 0;" mean?

I am reading a guide from this site about a technique on centering elements. I read the CSS code, .Absolute-Center { margin: auto; position: absolute; top: 0; left: 0; bottom: 0; right: 0;...

stackoverflow.com

 

Comments