DropDown hooks 만들기
💡 문제 인식
- DropDown hooks 사용시 발생하는 문제
- DropDown 사용 중, 화면을 움직이면 DropDown 위치가 변경된다.
- DropDown 사용 중, 화면을 확대 축소하게 되면 DropDown 위치가 변경된다.
- scroll에 따른 DropDown 위치 변경
- Modal 창에서 DropDown, 사용 시, DropDown이 보이지 않는다.
- DropDown이 브라우저 범위를 벗어나게 된다면 list가 보이지 않는다.
- DropDown position값은 어떻게 설정할것인가? </aside>
🚫 문제 분석
- 브라우저의 변경에 따라 DropDown이 왜 변경되는지 확인
- DropDown은 position을 absolute 로 사용중이기 때문에 브라우저가 변경될 때 마다 position 정보를 update 해줘야됨.
- Modal 창에서 DropDown 안뜨는 이유 확인
- Modal 생성 방식 분석
- 현재 Modal은 position 을 fixed 로 사용하고 있고 z-index를 1500을 주고 있는 상태
- DropDown은 position 을 absolute 를 쓰고 있지만, z-index 값이 없기 때문에 현재는 Modal 창 뒤쪽으로 나타나고 있는 상황
- DropDown이 브라우저 범위를 벗어나는 문제
- DropDown에게 position 정보를 넘겨 줄때, 브라우저의 높이값을 고려하지 않고 주었기 때문에 브라우저 범위를 벗어나게 됨
- DropDown이 브라우저 범위를 벗어나게 될 상황에 대한 예외처리 필요
- DropDown position 문제
- useRef를 사용해 내가 DropDown으로 사용할 Dom 요소에 접근해서 해당 요소의 position 값을 불러온다.
- 값은 2가지를 가져올 것이고, input창을 감싸고있는 div 태그와 li 태그를 감싸고 있는 ul태그를 가져온다. </aside>
⚙ 시도한 것
- 브라우저의 상대좌표 / 절대좌표 구하는 방법 알아보기
- 상대좌표 / 절대좌표screen.width : 화면(모니터 해상도)의 너비screen.height : 화면(모니터 해상도)의 높이브라우저 크기 구하기//실제 사용하는 브라우저 안쪽 너비document.body.scrollWidth : 스크롤바를 포함한 콘텐츠의 실제 가로 크기. 즉, 화면에 보이지 않는 부분까지 모두 포함한 전체 너비HTML5 표준window.innerWidth : 브라우저 두께를 제외한 너비window.innerHeight : **브라우저 두께를 제외한 높이
- window.outerHeight : 브라우저 창의 높이
- window.outerWidth : 브라우저 창의 너비
- document.body.clientWidth : 내부 콘텐츠의 영역의 너비를 나타내는 속성입니다. 이 속성은, 요소의 내부 콘텐츠 영역에서 스크롤바와 패딩을 제외한 실제 가로 길이를 나타내며, 즉, 스크롤바를 제외한 너비
- document.body.offsetWidth: 이속성은 요소의 가장 바깥쪽 경계를 포함한 크기를 나타내며, 즉, 요소의 테두리(border), 패딩(padding), 스크롤바 등의 크기를 모두 포함.
- 브라우저 크기를 구하고 싶은 경우
- screen.availHeight : 모니터 화면의 작업 표시줄을 제외한 높이
- screen.availWidth : 모니터 화면의 작업 표시줄을 제외한 너비
- screent 객체 화면 크기 구하기
- 코드 분석 & 문제 접근
useEffect(() => {
//input 태그를 감싸는 div
const { current } = divRef;
//팀목록을 li를 감싸고 있는 UI
const ulCurrent = ulRef.current;
if (current !== null && ulCurrent !== null) {
const { top, left, height, width } = current.getBoundingClientRect();
const absoluteTop = window.pageYOffset + current.getBoundingClientRect().top;
const absoluteLeft = window.pageXOffset + current.getBoundingClientRect().left;
//브라우저 전체 높이값보다 input을 감싸고 있는 div 태그 + UI 높이값보다 클때, UL 태그를 위로 올리기
if (top + height + ulCurrent.getBoundingClientRect().height > window.innerHeight) {
const ulHeight = ulCurrent.getBoundingClientRect().height;
const newTop = absoluteTop - height - ulHeight;
setInputPosition({ top: newTop, left: absoluteLeft, height, width });
} else {
setInputPosition({ top: absoluteTop, left: absoluteLeft, height, width });
}
}
}, [isOpen, width]);
- 화면에 넘어갔냐 안넘어갔냐를 판별할 조건 필요.
- 화면이 넘어갔다면, DropList를 위로 펼쳐야됨.(가로로 펼치는경우는 못봤음)
- 넘어가지 않는다면, 그대로 아래로 내려오게 해야된다.
- 화면에서 좌측 스크롤이 있을 경우도 있으니, 좌측 스크롤을 고려해서 left를 설정한다. </aside>
🛠 해결
1️⃣ 브라우저 크기 변경에 따른 DropDown position 값 reset
해결 코드
const [width, setWidth] = useState(window.innerWidth);
const resizeHandler = () => {
setWidth(window.innerWidth);
};
useEffect(() => {
window.addEventListener('resize', resizeHandler);
return () => {
//cleanUp
window.removeEventListener('resize', resizeHandler);
};
}, []);
useEffect(() => {
...
}, [isOpen, width]);
- addEventListener()를 통해서 ‘resize’ 크기 변화를 감지한다.
- 변화값을 useState에 넣는다.
- useState값을 position 값을 세팅해주는 useEffect dependency array에 넣어서 값 변경에 따라 position 값을 reset 해주도록 한다.
2️⃣ scroll을 고려한 DropDown position 값 설정하기
const { top, left, height, width } = current.getBoundingClientRect();
const absoluteTop = window.pageYOffset + current.getBoundingClientRect().top;
const absoluteLeft = window.pageXOffset + current.getBoundingClientRect().left;
- 스크롤 x, y 로 2가지 경우가 생길 수 있다는 것을 인지한다.
- 스크롤 변화값에 따른 좌표값을 window.pageYOffset 로 불러온다.
- 내가 기준이 잡은 좌표 top 값과 left 값에 scroll에 따른 offset값을 더해준다.
3️⃣ Modal 창에서 DropDown, 사용 시, DropDown이 보이지 않는다. DropDown에서 Ul태그의 z-index 값을 2000 으로 설정함으로써, Modal 보다 높게 설정한다.
4️⃣ DropDown이 브라우저 범위를 벗어나게 된다면 list가 보이지 않는다.
- 브라우저의 범위를 벗어났을 때 예외처리를 생각한다.
- 브라우저의 범위를 벗어나게 되면 DropDown 된 List를 기준 div의 아래가 아닌 위로 나타나게 한다.
- 계산 방식은, 현재 브라우저의 높이값을 구한 뒤, 기준 div의 top + height 값에 list의 height 값을 더 했을 때, 브라우저의 높이값보다 큰지를 확인하는 조건을 형성한다.
if (top + height + ulCurrent.getBoundingClientRect().height > window.innerHeight
- 조건이 true 일 경우
const ulHeight = ulCurrent.getBoundingClientRect().height;
const newTop = absoluteTop - height - ulHeight;
setInputPosition({ top: newTop, left: absoluteLeft, height, width });
조건이 true가 된다면, list가 기준 div의 아래가 아닌 위로 나타나게 해야되기 때문에 기준 top 값에서 높이값과 list의 높이값을 빼줌으로써 위로 나타나게 한다.
- 조건이 false 일 경우
setInputPosition({ top: absoluteTop, left: absoluteLeft, height, width });
5️⃣ DropDown position값은 어떻게 설정할것인가
const [divRef, ulRef, setIsOpen, isOpen, inputPosition] = useDropDown();
...
<styles.StInputBlock ref={divRef}>
{tagList?.map(item => {
return (
<styles.StTagBlock key={item}>
<styles.StProfileBlock>
<styles.StImageBlock />
{item}
</styles.StProfileBlock>
{props.disable === false && (
<styles.StDeleteBlock id={item} onClick={deleteClickHandler}>
x
</styles.StDeleteBlock>
)}
</styles.StTagBlock>
);
})}
<styles.StInput
onMouseDown={mouseDownHandler}
onKeyPress={onInputKeyDownHandler}
value={inputValue}
onChange={e => setInputValue(e.target.value)}
disabled={props.disable}
/>
</styles.StInputBlock>
기준이 되는 div태그의 DOM 요소에 접근할 수 있게 Ref값을 설정해 주고,
list가 되는 ul태그도 ref값을 설정해준다.
//item list 값이 바뀔때마다, 위치를 재확인한다.
//포탈은 position을 이용하기 때문에 위치 정보값이 필요하다.
// getBoundingClientRect은 해당 요소의 상태좌표값을 가져오고,
// 절대 좌표를 얻기 위해서는 window.pageYOffset을 더해주어야 한다.
useEffect(() => {
//input 태그를 감싸는 div
const { current } = divRef;
//팀목록을 li를 감싸고 있는 UI
const ulCurrent = ulRef.current;
if (current !== null && ulCurrent !== null) {
const { top, left, height, width } = current.getBoundingClientRect();
const absoluteTop = window.pageYOffset + current.getBoundingClientRect().top;
const absoluteLeft = window.pageXOffset + current.getBoundingClientRect().left;
//브라우저 전체 높이값보다 input을 감싸고 있는 div 태그 + UI 높이값보다 클때, UL 태그를 위로 올리기
if (top + height + ulCurrent.getBoundingClientRect().height > window.innerHeight) {
const ulHeight = ulCurrent.getBoundingClientRect().height;
const newTop = absoluteTop - height - ulHeight;
setInputPosition({ top: newTop, left: absoluteLeft, height, width });
} else {
setInputPosition({ top: absoluteTop, left: absoluteLeft, height, width });
}
}
}, [isOpen, width]);
그다음 Ref로 설정한 요소들의 top,left,width,height 값을 가져와 position 값을 setting 해준 뒤 그 값을 return 해준다.
❓ 궁금했던 부분
현재 문제가 되고 있는 것이, ul태그의 dom 요소에 접근해서 getBoundingClientRect() 메서드를 통해 ul 태그의 높이값을 불러오고 있는데,초기 태그의 높이값과 그 이후의 높이값이 다른 문제가 생겼다. 초기값의 높이값이 ul태그의 높이값만을 포함하고있는 것이 아닌, div태그와의 거리값도 포함이 되어 있어서,
list가 브라우저를 벗어났을때, 즉 위로 열리게 되면 문제가 생긴다.
문제는 해결방안을 찾고 있고, 해결하는 즉시 내용을 공유할 예정이다.
😆 궁금했던 내용 해결방안
문제가 됬던 부분은 , ul태그 안에 li태그 값들을 동적으로 받는 HashTag Component 였다.
다음 이미지와 같이, 처음 값을 받을 때를 보면 position 값과 크기값이 다른 것을 확인할 수 있었다.
{props.disable === false &&
isOpen &&
createPortal(
<styles.StUlBlock ref={ulRef} className="tags" pos={inputPosition}>
<styles.StTeamMark className="tags">Team A :</styles.StTeamMark>
{data?.map(item => {
return (
<styles.StLiBlock
className="tags"
key={nanoid()}
onClick={onLiClickHandler}
>
{item.userName}-{item.userId}
</styles.StLiBlock>
);
})}
</styles.StUlBlock>,
document.getElementById('root') as HTMLElement
)}
useEffect(() => {
//input 태그를 감싸는 div
const { current } = divRef;
//팀목록을 li를 감싸고 있는 UI
const ulCurrent = ulRef.current;
if (current !== null && ulCurrent !== null) {
const { top, left, height, width } = current.getBoundingClientRect();
const absoluteTop = window.pageYOffset + current.getBoundingClientRect().top;
const absoluteLeft = window.pageXOffset + current.getBoundingClientRect().left;
//브라우저 전체 높이값보다 input을 감싸고 있는 div 태그 + UI 높이값보다 클때, UL 태그를 위로 올리기
if (top + height + ulCurrent.getBoundingClientRect().height > window.innerHeight) {
const ulHeight = ulCurrent.getBoundingClientRect().height;
const newTop = absoluteTop - height - ulHeight;
setInputPosition({ top: newTop, left: absoluteLeft, height, width });
} else {
setInputPosition({ top: absoluteTop, left: absoluteLeft, height, width });
}
}
}, [isOpen, width]);
isOpen이 true가 되었을 때 문제가 생겼을까??? 나는 이것에 접근해보기 위해 여러 console.log를 찍어봤다.
그것들중 가장 결정적인 log는 위의 그림과 같았다. isOpen이 true가 되었을 때, useDropDown안에 useEffect가 호출되고,
ulRef의 DOM정보를 가져온다. 그때 ulRef의 값을 log로 찍어봤더니, 왠걸 li태그값이 한개도 없고 높이값도 더 높게 측정이 됬다.
이유가 뭔지 찾던 중, 나는 ul태그의 높이값을 지정해두지 않고 li태그 크기만큼 설정되게 두었었다.
그리고 서버에서 data를 받아서 data.map함수를 돌려서 ul태그의 크기를 결정하고 있었다.
근데 서버에서 데이터를 불러오는것은 비동기 처리로 작업하기 때문에 useEffect 호출 되었을 때도 아직 data를 가져오지 못해
li태그 없이 높이값을 측정했고 결국 이상한 값으로 설정이 되서 문제가 발생했었던 것이다.
이를 해결하기위해서 ul태그의 높이값을 동적으로 설정하는 것이 아닌, 고정값으로 주었고 overflow에 대한 값은 scroll 처리를 해줌으로 써 해결을 했다.
해결방안
정리를 하자면, 렌더링 과정을 다시 생각해 보면된다.
1. 초기 렌더링이 일어난다.
- useGetTeamInfo, useDropDown 등의 커스텀 훅이 실행된다.
- <HashTag /> 컴포넌트의 HTML 구조와 스타일을 적용하여 DOM에 렌더링을 한다.
- isOpen의 초기값이 false 이기 때문에 createPortal 내용은 렌더링 되지 않는다.
2. useEffect 동작한다.
- useEffect가 첫 렌더링 후에 실행된다.
- props.mention의 값에 따라 tagList 값을 설정한다.
- useDropDown 커스텀 훅의 useEffect가 초기 렌더링 후 실행되서, 이때 화면 크기 변경 리스너를 등록하고,
divRef와 ulRef에 따른 위치 재확인 로직이 작동한다.
- ulRef 값은 null 이기 때문에, 초기 리턴값들은 초기 값으로 리턴된다.
3. isOpen이 true가 되었을 때, 렌더링
- 서버에서 데이터를 비동기 적으로 불러온다.
- 서버에서 데이터가 늦게 불러와져 useDropDown useEffect가 먼저 호출이 되었고,
- li태그가 반영되지 않은 높이값을 가져와 문제가 발생
- 이후 렌더링에서는 isOpen이 변경될 때마다 useEffect가 호출되고, 이미 존재하는 li태그(이전 정보)와 함께 위치 정보 업데이트
정리
여기서 발생한 문제는 서버에서 받아온 데이터와 dropdown의 위치좌표를 결정하는 hook간의 연관성이 없었다? 가 맞는 표현이 될거같다. 서버에서 받아온 데이터는 react-query로 인해 비동기 적으로 작업을 하고, 이후 결과값에 대해 리렌더링을 일으킨다.
여기서 발생한 문제는 리렌더링이 일어나도 useDropDown안에 useEffect는 작동하지 않는다. 왜냐하면,
useEffect의 dependency Array의 값에는 서버에서 받아온 데이터 하고는 상관없는 정보를 담고 있기 때문이다.
그렇기에 서버에서 데이터를 비동기적으로 받아와 리렌더링을 일으켜도 useDropDown은 작동하지 않아 position 정보는 업데이트가 되지 않았고 브라우저에서 봤을 때, 정확한 position 정보가 입력되지 않아 실제 원하는 위치보다 높게 띄워지는 것 같은 형상을 보여지는 것이었다. 이후에는 이전 data를 기준으로 position 값을 계산했기 때문에 문제가 없어 보였던 거지, 사실 서버 데이터가 변경되었다면 문제가 발생했었을 것이다.
그래서 Ul태그를 data와 상관없이 동적으로 받아오는 것은 무리였고, 이를 해결하기 위해 정적으로 높이값을 설정해 주었고 overflow 로에 대해 서는 scroll 처리를 해서 해결을 했다.
이러한 문제 때문에 브라우저 렌더링과정과 useEffect의 렌더링처리과정에 대해 많이 공부 할 수 있었던 시간이었던거같다.
오늘도 작은 문제로 큰성장을 이룬거같다.
'실전 프로젝트' 카테고리의 다른 글
onWeekCalendar (0) | 2023.06.13 |
---|---|
Event bubbling Trouble Shooting (0) | 2023.06.13 |