일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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코테
- 몽고DB
- 리액트
- 프로그래머스코테
- 코딩테스트
- 백준js
- 자바스크립트
- 백준골드
- 백준
- 프로그래머스
- JS프로그래머스
- 코테
- 포이마웹
- HTML
- CSS
- 다이나믹프로그래밍
- 리액트커뮤니티
- 안드로이드 스튜디오
- 백준알고리즘
- 백준nodejs
- dp알고리즘
- 백준구현
- HTML5
- 프로그래머스JS
- 백준구현문제
- 알고리즘
- css기초
- JS
- Today
- Total
개발새발 로그
팝오버를 구현하면서 본문
📖들어가며..
오늘은 팝오버 기능을 다양한 방식으로 구현하고, 정리해보려고 한다.
사실 이전에 Modal과 비슷한 부분이 있어서 간단하게 정리했다.
방법으로는
해당 컨텐츠의 자식 요소로 팝오버를 출력하는 방법(조건부 렌더링)
CreatePortal을 이용한 방법
HTML <dialog>를 이용한 방법
popover API를 이용한 방법
이 있다.
🛠️1. 자식요소로 팝오버를 출력하는 방법(조건부 렌더링)
가장 간단하게 생각할 수 있는 방법이다.
popover가 필요한 각 요소에 하위 요소로 popover를 조건부 렌더링으로 나타내는 방식이다.
간단하게 보면 이렇게 구성될 것이다.
MenuPopover컴포넌트가 실제 popover요소이고, close 함수를 Props로 받는다.
그리고 아래와 같이 구성한다.
마치 모달과 비슷하게 Overlay를 갖고있다.
Overlay를 준 이유는
"하나의 popover만 띄우게 하기 위해서"이다.
이렇게 Overlay를 주면 하나의 popover가 열리는 것을 강제할 수 있다.
마치 Modal처럼 말이다.
그리고 실제 popover요소의 스타일은 absoulte로 줘서 부모 위치에 상대적으로 위치하도록 하면 된다.
그러면 자연스럽게 popover가 나타날 것이다.
이때 상위 부모요소는 당연히 relative 스타일을 가져야 한다.
그래야 어떤 부모를 기준으로 위치할 건지 absoulte가 알 수 있다.
여기까지가 첫 번째 방식의 popover다.
장단점을 나열해보면
장점
- 구현이 간단하다.
- overlay를 통해 하나의 popver만 열릴 수 있게 할 수 있다.
-> -레이어가 존재하지 않는다면 Popover를 여러개 클릭해서 열 수 있게 된다.(같은 컴포넌트가 많아짐)
단점
- 만약 각 리스트 아이템에 overflow: hidden이 적용되어있다면 Popover가 해당 스타일로 인해 잘려나가는 현상이 생긴다.(내부 자식요소로 존재하게됨으로써 생기는 문제)
-
여기서 추가적으로 화면 Viewport에 따라 위치가 바뀌는 기능도 있다.
이전에 useStyleInView 훅을 만들어서 화면에서 요소가 나가게 되는 경우 top,left,bottom,right 값 등을 변경해서
화면에 올바르게 나오도록 하는 기능을 만들었었다.
해당 기능은 아래에서 다시 설명하려고 한다.
🛠️2. CreatePortal을 이용한 방법
위에서 말했던 단점 중 "부모 요소에 overflow:hidden이 적용되어 있는 경우 popover 요소도 같이 감춰진다."를 해결하기 위해 CreatePortal을 사용한다.
간단하게 설명하면
popover가 띄워질 위치를 따로 Portal할 공간을 만들어 해당 요소에 띄우고,
위치를 절대 위치로 바꿔주면 된다.
이전에 만든 로직에서 크게 달라지는 것이 없다.
추가 된 것은 buttonRef가 존재한다는 것이다.
이는 useStyleInView에 필요하기 때문에 Props로 내려줘야 한다.
MenuPopover는 아래와 같이 구성하면된다.
아까와 달라진점은 CreatePortal을 사용한다는 점과
useStyleInView를 사용하는 점, "absolute"라는 인수를 넘겨준다는 것이다.
이제 useStyleInView를 살펴보려고한다.
🚨useStyleInView의 개선사항
1. 현재 top,bottom,left,right 중 어떤 위치가 필요한지만 계산하는 상황
먼저 나는 이전에 useStyleInView가 그저 top,left,bottom,right 중 어느 위치에 위치해야하는지만 반환하는 훅으로 만들었다.
즉 뷰포트 계산 이후 top, left, bottom, right 중 하나의 값을 반환하는 것이다.
하지만 만약 요소의 기존 스타일에 top, left, bottom, right 의 값이 정해져있다면 부정확한 위치가 나타나게 된다.
이를 해결하기 위해 실제 style값을 계산해 반환하도록 했다.
간단하게 설명하면
현재 style이란 useState는 아래와 같은 객체의 형태를 가진다.
현재 top이나 right의 값은 커스텀하게 설정된 값으로 원래는 기본값으로 0을 가진다.
즉 현재 위치와 상관없는 값들도 강제로 설정해주는 것이다.
verticalKey는 화면 아래 공간이 충분한지 확인하여 top 또는 bottom을 결정하고,
horizontalKey는 화면 오른쪽 공간이 충분한지 확인하여 left 또는 right를 결정한다.
이후 남은 값은 auto로 해주는 것이다.
그리고 위에서 반환받은 스타일을 인라인 스타일로 적용하는 것이다.
CSS 스타일 우선순위
1. !important
2. 인라인 스타일
3. ID 선택자
4. 클래스, 속성, 가상 클래스 선택자
5. 요소, 가상 요소 선택자
6. 전역 선택자 및 상속
이를 통해 좀 더 정확한 스타일을 적용할 수 있다.
2. CreatePortal을 사용하면 target요소는 절대 위치 값이 필요한 상황
이제 위에서 말했던 방식에 필요한 기능을 넣어줘야한다.
CreatePortal로 모달 처럼 어떤 요소로 보내게 되면 상대위치로는 Popover가 불가능해진다.
그러므로 절대 위치가 필요할 때 이를 계산할 수 있도록 해야한다.
먼저 useStyleInView를 사용할 때 현재 절대 위치로 계산할건지 상대 위치로 계산할건지를 정해주는 인수를 전달해준다.
이를 이용해서 아래와 같이 분기처리해서 계산해줘야 한다.
간단하게 설명하면
절대위치가 필요한 경우
1. 먼저 부모요소의 정보인 wrapperRect를 이용해 현재 wrapperRef가 어디에 위치해있는지 계산한다.(top값)
2. 만약 verticalKey가 top인 경우(wrapper의 아래에 위치하는 것)
- absoluteTop(위에서부터 wrapperRef의 위치 값)
- wrapperRect.height(wrapperRef의 높이)
- 위 둘을 더한 값을 top에 적용하면 wrapperRef 아래에 위치한다.
3. 만약 verticalKey가 bottom인 경우(wrapper의 위에 위치하는 것)
- viewportRect.height(현재 뷰포트의 전체 높이)
- absoluteTop(위에서부터 wrapperRef의 위치 값)
- 위 둘을 뺀 값을 bottom에 적용하면 wrapperRef 위에 위치한다.
이렇게 left와 right도 계산해주면 된다.(생략하겠습니다)
상대 위치일 경우
상대 위치일 경우에는 부모 요소의 위치에 상대적으로 위치하므로 따로 계산할 필요가 없다.
아까 로직과 동일하다.
이를 통해서 CreatePortal 같이 부모 요소의 상대 위치를 사용할 수 없는 경우에도 위치할 수 있도록 해주었다,
🛠️3. Context API를 사용하지않는 방법
위 useStyleInView에서 Context를 사용하지않고 할 수 있지 않을까?라는 생각이 들어 구현해봤다.
사실 기존에 useSyncExternalStore을 사용하면 Context API를 사용하지 않아도 된다.
기존에 subscribe함수와 브라우저 API값을 가져오는 로직을 구현해놓았기 때문이다.
이때 위 처럼 브라우저 API값을 외부에 두어야 한다.
아니면 해당 값이 바뀔 때 무한 반복이 생긴다.
그러므로 아래와 같이 직접 로직을 만들어 줘야한다.
원래는 useState로 viewportRect값을 관리해서 실시간으로 사이즈가 변할 때마다 위치를 변하게 하고 싶었지만
클릭으로 popover 요소가 잠깐 나타날 때 viewportRect가 변하게 되면서 5번의 불필요한 렌더링이 발생하게 된다.
또한 각각 커스텀 훅으로 viewportRect에 접근하므로 값이 계속해서 0으로 초기화되는 것도 문제였다.
즉 각각의 요소들이 ViewPortRect에 접근하니까 생기는 문제다.
그래서 ref로 하는 것이 괜찮다고 생각했고, 3번의 렌더링으로 최소화했다.
하지만 아까 말했듯이 실시간으로 위치가 변하지는 않는다.
이를 통해 어쩌면 ViewportRect는 사실 Context API나 상태관리 라이브러리로 관리하는 것이 좋겠다고 생각이 들었다.
다시 돌아와서 단편적으로 보면 2번의 렌더링만 하는 것이 맞는데
왜 3번의 렌더링을 할까 봤더니 아래와 같은 순서로 3번의 렌더링이 되고있었다.
- 상태 변경: 버튼 클릭 시 toggleMenu(true)를 호출하여 menuOpened 상태를 true로 변경합니다. 이 상태 변경은 React가 해당 컴포넌트를 다시 렌더링하도록 트리거합니다.
- 조건부 렌더링: menuOpened가 true일 때 MenuPopover 컴포넌트가 렌더링됩니다. 이 과정에서 MenuPopover가 새로 렌더링되며, 이로 인해 또 한 번의 렌더링이 발생합니다.
- 자식 컴포넌트의 렌더링: MenuPopover 내부에서 createPortal을 사용하여 DOM에 추가되는데, 이 과정에서도 렌더링이 발생합니다. createPortal은 React가 새로운 DOM 노드를 생성하고 이를 렌더링하는 과정에서 추가적인 렌더링을 유발할 수 있습니다.
그래서 이를 5번째 방법에서 해결해보려고 한다.
🛠️4. Dialog를 이용한 방식
사실 Dialog로 구현하는 방식은 가능하긴 하지만 여러모로 좋지않은 접근법이다.
먼저 show()라는 메서드를 사용하면 showModal()과 다르게 overlay없이 요소를 나타나게 할 수 있다.
근데 overlay가 없다면 popover가 중복으로 열 수가 있게 된다.
결과적으로 show메서드는 overlay가 없는 문제와 좌표수정이 어렵고, overflow:hidden에 대응하기 어려워 적합하지 않다고 판단했습니다.
그래서 기존 Modal을 구현할 때 사용했던 방식처럼 showModal()을 사용해서 구현할 수 있었다.
dialog를 참조하는 ref를 하위 컴포넌트에 전달해주고, 해당 ref로 동작을 제어한다.
이때 이전에 말했던 useStyleInView의 "absolute"방식이 필수이다.
dialog의 showModal()방식도 뒤에 overlay가 깔리기때문에 상대위치가 부모요소에 종속되지 않는다.
그래서 "absolute" 옵션이 필요하다.
또한 현재 만든 useStyleInView에서 추가된 부분이 있는데 needUpdate라는 매개변수다.
이건 dialog를 위한 매개변수인데
dialog가 처음에 열리게 되면 style이 계산되지 않은채 그대로 화면에 나타나게 된다.
그래서 올바르지 않은 위치에 나타나게 되는 문제가 있는데
이를 위해 dialog의 open 상태를 props로 받아 이게 변할 때 정확히 style을 계산할 수 있도록 해야한다.
위 처럼 스크롤이나 resize이벤트가 일어나지 않는다면 제대로된 계산이 안되는 것을 볼 수 있다.
왜이런지 생각해보니 dialog는 화면에 보이지 않더라도 떠있는 상태이다.
그래서 이 상태에서의 위치로 계산을 하다보니 위처럼 옳지않은 결과가 나온 것 같았다.
🛠️4. Popover API를 이용한 방식
https://ykss.netlify.app/translation/introducing_the_popover_api/
이번에 비교적 최신 기능으로 나온 popover API가 있다.
아래 코드처럼 작성하면 바로 popover기능을 만들 수 있다는 것이다.
그래서 리액트에서 구현해보려고 하니 아직 react 18 버전에서는 안되는 것 같았다.
5. 단점을 개선한 Popover
먼저 이제까지 만든 popover의 단점을 보자
1. popover 내부요소가 고정되어있어 수정할 수가 없다.
2. createPortal방식에서 항상 개발자가 id가 "popoverRoot"로 된 div요소를 넣어줘야한다.
먼저 1번 단점은 간단하게 children을 사용하면 된다.
2번 단점도 간단하게 createPortal할 요소를 내부에서 알아서 만들거나 하는 방법으로 하면되는데
첫 번째로 간단한 방법은 portal을 body로 하는 것이다.
그러면 사실 "portalRoot"를 만들 필요가 없다.
대신 조건부 렌더링을 popover 사용자가 표현해줘야한다.
그래서 나는 이 부분도 popover 사용자가 신경쓰지않도록 내부에서 처리하도록 했다.
위 코드에서 보이듯이 popover 상태를 props로 내려주고 createportal을 조건에 따라 나타나게 했다.
여기서 문제는 useStyleInView에서 미리 스타일이 계산이 되어버려서 빈 값을 계산하게 된다.
그래서 이 스타일을 다시 계산해줘야하는데 opened 상태가 변해도 계산을 하지 않게 된다.
그래서 위에 dialog를 위해 추가했던 useStyleInView의 needUpdate를 사용해줘야한다.
정리하자면
조건부 렌더링을 사용하지 않고, 내부에서 알아서 처리하게 하려면 위 로직처럼 opened를 사용해줘야한다.
조건부 렌더링을 사용하면 위 코드와 같은 처리가 필요없다.
여기서 만약 document.body에 portal하는 것이 문제가 될 것 같다. 혹은 portal할 공간에 스타일이 필요할 때는 아래와 같이 하면된다.
이렇게 하는 방법도 있다는 것만 알아두자.
이 방법은 스낵바를 구현할 때 사용했던 방법인데
스낵바는 portal할 공간에 css스타일링이 필요해서 사용하기 좋았던 방법이었다.
정리해보자면
1. 조건부 렌더링 사용의 불편함
- 내부에 popover 의 opened 상태를 props로 내려주고 내부에서 처리하는 방법으로 해결하려고 함
- 그리고 opened && createPortal을 사용
- 이로인해 style 값이 빈값으로 계산되는 문제 발생
- useStyleInView의 needUpdate 매개변수 사용 필수
-> 만약 조건부렌더링을 한다면 needUpdate 불필요,
2. Portal할 공간을 document.body로 하는 방법과 document.body.appendChild로 하는 방법
- 두 가지 방법은 별 차이 없음
- 단지 portal할 공간에 스타일링이 필요하다면 appendChild하는 방법이 적합함
3. popover 내부 수정을 할 수 있도록
- children 사용하면 됨
4. 2번의 렌더링으로 최적화
1. 상태 변경: 버튼 클릭 시 toggleMenu(true)를 호출하여 menuOpened 상태를 true로 변경합니다. 이때 React는 해당 컴포넌트를 다시 렌더링해야 한다고 판단합니다. 이 과정에서 첫 번째 렌더링이 발생합니다.
2.조건부 렌더링: MenuPopover 컴포넌트는 opened prop을 사용하여 렌더링됩니다. opened가 true일 때만 createPortal을 통해 MenuPopover가 DOM에 추가됩니다. 이 과정에서 두 번째 렌더링이 발생합니다.
으로 기존에 3번 렌더링하는 부분을 변경했습니다.
📘마치며..
이렇게 Popover를 구현해봤다.
약간 Modal을 구현할 때와 비슷하긴 했지만
이 방식을 구현하면서 useStyleInView의 기능을 좀 더 좋은 방향으로 개선한 것 같아서 좋았다.
특히 이 훅은 두루두루 쓰일 것 같았다.
훅으로하면 좋긴하지만 ref로 값을 관리해서 실시간으로 변하지 않는다는 단점이 있긴했다.
그래서 요구사항마다 적절하게 바꿔 사용해야 할 것 같았다.
popover를 사용할 때는 CreatePortal을 이용한 방식이 가장 최적의 방법이라 생각했다.
Popover API를 사용하면 좋겠지만 아직 리액트 18에서는 되지않으니까..
가장 마지막에 만든 단점을 개선한 5번째 방법이 최선의 방법이라 생각된다.
'React' 카테고리의 다른 글
Carousel을 구현하면서 (1) | 2024.12.27 |
---|---|
이미지 슬라이드를 구현하면서 (0) | 2024.12.15 |
Modal을 구현하는 방법 - Context API, CreatePortal, Dialog (1) | 2024.11.28 |
SnackBar를 구현하면서...- Context API, CreatePortal (0) | 2024.11.22 |
ScrollSpy를 구현하면서 - 합성 컴포넌트 패턴, Render Props 패턴 (0) | 2024.11.01 |