일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- JS
- 코테
- js코테
- 리액트
- JS프로그래머스
- 백준
- HTML
- HTML5
- 백준js
- 다이나믹프로그래밍
- css기초
- 백준알고리즘
- 백준골드
- 코딩테스트
- 리액트커뮤니티
- 프로그래머스JS
- 포이마웹
- 백준구현
- 안드로이드 스튜디오
- 프로그래머스
- 리액트댓글기능
- dp알고리즘
- 몽고DB
- 익스프레스
- 백준구현문제
- 알고리즘
- 백준nodejs
- 프로그래머스코테
- CSS
- 자바스크립트
- Today
- Total
개발새발 로그
Modal을 구현하는 방법 - Context API, CreatePortal, Dialog 본문
📖들어가며..
이전에 Modal을 구현하는 것을 기록한 적이 있다.
useModal이라는 커스텀 훅을 만들면서 문제점을 개선해나갔었다.
이번에는 좀 더 다양한 방법을 만들어보면서 장단점을 알아보고,
기존에 만들었던 useModal도 개선해보려고 한다.
🛠️1. Context API를 이용한 방법
먼저 Modal 데이터들을 Context API에서 관리하고 뿌려줄 것이다.
Context API 설계는 이전에 스낵바 방식과 비슷하다.
여기서 특이한 점은 Map으로 관리한다는 것이다.
Map으로 관리하면 모달을 추가하거나 업데이트할 때 아래와 같이 간단하게 가능하다.
Map을 사용하면 기존 데이터를 간단하게 업데이트할 수 있다.
또한 간단히 데이터를 순서가 유지되면서 추가할 수 있다.
만약 배열을 사용한다면 아래와 같이 복잡하게 구성해야할 것이다.
대신 각 모달을 만들 때 고유한 ID 값을 주어야한다.
그래야 열거나 닫는 메서드를 실행할 때 어떤 Modal이 닫혔는지 알 수 있을 것이다.
간단한 사용법은 아래와 같다.
openModal 메서드로 먼저 띄울 Modal을 정의해야한다.
그리고 해당 메서드를 button에 등록하면 Modal이 사용가능하다.
해당 기능의 장단점을 보면
✨장점
- 모달이 리스트로 관리돼서 Modal 갯수에 따른 설정을 할 때 편리함
✨단점
- 상태 공유가 되지 않음
- Provider에 의존됨
실제로 Modal이 열렸을 때 body의 스크롤이 되지않도록 아래와 같은 코드를 추가했다.
코드 한줄로 간단하게 가능했다.
하지만 단점 중 상태 공유가 되지않는 점이 있었다.
확인을 누르면 확인 유무 값이 true가 된다.
상태 공유가 되지않는 것을 확인하기 위해 임시로 확인을 눌러도 Modal이 닫히지 않도록 했다.
위 사진을 보면 현재 Modal에서는 X가 나타나있지만 뒤에 확인 유무를 보면 V가 되어있다.
🛠️2. useModal + createPortal을 이용한 방법
useModal + createPortal을 이용한 방법은 예전에 블로그에서 썻던 방식과 비슷하다.
하지만 다른 개발자 분들이 사용한 방법을 보여주도록 하겠다.
Modal의 상태를 관리하는 useModal은 아래와 같이 간단하게 만든다.
그리고 Modal 컴포넌트는 아래와 같이 Modal의 현재 상태에 따라 createPortal을 수행하고,
닫는 메서드도 Props로 받는다.
사용법도 아래와 같이 간단하다
중요한 점은 ModalRoot를 따로 선언해 줘야 한다는 것이다.
장단점을 확인해보면
✨장점
- 사용하기가 간단합니다.
- 상태 공유가 됩니다.
✨단점
- ModalRoot를 꼭 선언해줘야 한다는 점
- 사라지는 애니메이션을 적용하기가 까다롭다.
-> Modal이 useState 상태로 관리되기 때문에 사라질 때 상태변화로 애니메이션이 작동하지않는다.
위 단점을 모두 개선한 것이 내가 만든 useModal이다.
먼저 ModalRoot 문제는 아래와 같이 해결했다.
그리고 애니메이션 관련 문제는 애니메이션 상태를 하나 더 만들어 관리함으로써 해결했다.
내가 만든 useModal은 Modal 컴포넌트에서도 따로 useState가 존재한다.
먼저 useModal은 위에서 말했던 useModal가 똑같다.
그 상태 값을 isOpen으로 받고있다.
간단하게 설명하면
1. isOpen값은 isAnimaiting의 초기값이된다.
2. Modal이 createPortal되면 그 즉시 useEffect로 setIsAnimation()을 수행해 애니메이션이 나타나게 한다.
3. 이후 Modal을 닫으면 isAnitmation의 값을 false로 바꾸고 사라지는 애니메이션이 수행되도록한다.
4. 사라지는 애니메이션이 끝나면 onAnimationEnd으로 인해 실제 isOpen값이 false가 되는 close 메서드를 수행한다.
5. 결과적으로 Modal은 DOM트리에서도 보이지 않게 된다.
내가 만든 useModal의 렌더링 횟수를 보면 4번의 렌더링을 하게 된다.
기존 useModal 방식은 2번의 렌더링을 하게된다.
애니메이션을 적용하면 렌더링 횟수가 증가하게 된다.
당연하게도 Modal의 상태가 두개로 관리되기 때문이다.
🛠️3. Dialog를 이용한 방법
Diaglog를 사용하면 간단하게 Modal을 구현할 수가 있다.
아래 코드를 보자
Dialog는 특이하게 BackDrop 영역을 수행할 Div 요소가 필요없다.
그저 위처럼 dialog만 사용하면 된다.
그리고 CSS를 이용해 아래처럼 활용할 수 있다.
dialog의 CSS는 그대로 Modal inner스타일로 넣어주면 되고,
backdrop에 Modal의 BackDrop 스타일을 넣어주면된다.
그리고 open 속성이 없을 때 display:none을 넣어서 DOM에 생성되지않도록 한다.
dialog는 open이라는 attribute를 넣어 Modal을 열고 닫는다.
Dialog의 open 속성을 넣을 때 showModal() 메서드를 사용하면 된다.
그리고 기본적으로 ESC키로 닫힌다고 한다.
🚨주의점
1. Modal이 열릴 때 body의 스크롤을 없애는 기능을 만들 때
Context API를 사용했을 때는 Modal 갯수를 파악할 수 있기 때문에 쉽게 구현이 가능했고,
useModal 커스텀 훅을 사용했을 때는 각 Modal에서 아래와 같이 body에 접근해 Modal의 갯수를 파악해서 스크롤이 사라지는 기능을 하도록 했습니다.
<dialog> 또한 이러한 기능을 하기 위해서 Modal을 open하거나 close할 때마다 아래 함수를 실행해서 Modal의 갯수를 파악하도록 했습니다.
하지만 매번 querySelectorAll로 요소를 탐색하는 것이 성능에 좋지 않기 때문에 force라는 값을 인수로 받을 수 있게 하고,
해당 인자를 사용해 만약 boolean타입이 들어온다면 계산하지 않고 toggle하도록 설정했습니다.
이는 아래와 같이 Modal을 Open할 때 사용합니다.
Modal을 오픈할 때는 Modal이 0개 이상인 것이 확실하므로 이렇게 구현했습니다.
2. BackDrop 영역을 클릭했을 때 Modal이 닫히는 기능을 구현할 때
여타 다른 Modal들과 다르게 BackDrop 영역이 div요소처럼 존재하지않고, CSS로만 구성이 되어있다.
그래서 BackDrop 영역에 따로 Click 이벤트를 등록할 수가 없다.
그래서 아래와 같이 dialog에 Click이벤트를 걸고 클릭한 영역이 dialog 영역인 경우에 닫게하면 된다.
왜 dialog영역인 경우(modalRef.current === e.target)일 때 닫는 걸까?
e.target을 찍어보면 BackDrop 영역은 dialog가 찍히지만
실제 Modal 영역을 클릭하면 children에 넣었던 요소들이 찍히게 된다.
그래서 아래와 같이 children을 감싸는 inner div요소를 만들고 stopPropagation을 사용해도 된다,
3. <dialog> 사용시 애니메이션 적용 방법
자료를 찾아보던 중에 좋은 예시가 있었다. 예시 링크
위 코드를 기반으로 간단하게 애니메이션도 적용이 가능했다.
간단하게 설명하면
dialog는 기본적으로 사라지는 애니메이션(popOut)을 가지고 있고,
open 속성이 있을 때는 나타나는 애니메이션(popIn)을 가져야 한다.
open 속성이 없을 때는 화면에 보이지 않도록 opacity를 준 것이다.
만약 opacity 속성을 주지 않는다면 새로고침할 때마다 dialog의 popOut애니메이션이 실행될 것이다.
이때 문제는 DOM트리내에서 Modal을 사용하지 않더라도 내부 요소가 나타난다는 점이다.
애니메이션을 적용하기 전에는 아래와 같이 DOM트리에는 나타나지만 화면에서는 보이지 않고 있다.
이유는 기존에 dialog는 open속성이 없을 때 아래와 같이 해주었다.
그런데 우리는 애니메이션을 위해서 현재 dialog에 기본적으로 사라지는 애니메이션(popOut)을 넣었다.
그래서 초기 렌더링 때 애니메이션이 실행되어 화면에 노출될 것이다.
이를 막은 것이 opacity다.
그래서 아래와 같이 설정하지 않고, dialog에 바로 애니메이션을 적용한 것이다.
onAnimationEnd를 활용해서 없애보려고 했지만 그러면 기본 애니메이션이 동작하지 않게 된다.
그래서 일단은 현재 상태를 유지하기로 했다.
🤔{visibility:hidden}와 { pointer-events: none; opacity: 0; }의 차이
- pointer-events: none; + opacity: 0;:
- 요소를 완전히 숨기면서, 상호작용도 차단하고 싶을 때 사용합니다.
- 예: 비활성화된 버튼, 잠시 사용 불가능한 UI.
- visibility: hidden;:
- 요소가 보이지는 않지만, 배치된 공간과 이벤트 처리가 유지되어야 할 때 사용합니다.
- 예: 드롭다운 메뉴를 숨길 때, 요소의 위치는 유지하면서 애니메이션을 적용하고 싶을 때.
dialog의 애니메이션 방법은 useModal처럼 복잡하게 onAnimationEnd도 사용하지않아도 되는 점이 좋았다.
📘마치며..
이렇게 Modal을 구현하는 방법을 모두 알아봤다.
저번에 useModal을 만들면서 부족했던 스크롤 방지나 ESC 기능도 추가했다.
이번에 Modal의 구현 방식을 만들면서 또 배우게 된 것 같았다.
그리고 Dialog 방식은 이제 거의 모든 브라우저가 지원하는 것 같았다.
그렇게 되면 Dialog 방식이 가장 좋다고 판단된다..!
아무튼 이렇게 Modal 구현 방법을 다 배우게 되면서 Modal 기능에 좀 더 익숙해진 것 같다.
'React' 카테고리의 다른 글
이미지 슬라이드를 구현하면서 (0) | 2024.12.15 |
---|---|
팝오버를 구현하면서 (1) | 2024.12.06 |
SnackBar를 구현하면서...- Context API, CreatePortal (0) | 2024.11.22 |
ScrollSpy를 구현하면서 - 합성 컴포넌트 패턴, Render Props 패턴 (0) | 2024.11.01 |
React - Modal 기능을 구현하면서 (3) | 2024.09.04 |