회사에서 모달에 관한 버그를 수정해보기로 했습니다. 지금은 이미 수정되어서 프로덕션 레벨까지 잘 반영되어 있습니다.
총 3가지 버그가 존재했었는데요. 차례차례 그 해결 방안을 공유하고자 합니다.
문제의 레이아웃은 아래와 같습니다
모달이 있으며 모달 내부에는 탭이 있습니다. 탭 안에는 리스트가 존재합니다. 리스트는 무한 스크롤로 동작합니다.
발견된 버그는 총 3가지 였습니다.
1. 무한 스크롤이 동작하지 않았습니다.
2. 모달이 떠 있는 동안 오버레이 너머에 있는 화면에서도 스크롤이 발생합니다.
3. 모바일 웹에서 스크롤을 반복하면 탭과 리스트 컴포넌트가 겹치는 현상이 발생합니다.
문제를 해결하기 위해선 그 원인을 알아야 합니다. 지금부터 그 원인을 차례로 파악해보겠습니다.
먼저 문제 1번의 경우엔 기본적인 브라우저 동작입니다. 하지만 지금은 이 동작을 원하지 않습니다. 모달이 떠 있는 동안엔 뒷 배경의 스크롤이 동작하지 않았으면 하죠.
간단하게 모달이 떠 있는 동안에 뒷 배경에 overflow:hidden
을 적용해서 이 문제를 해결할 수 있습니다. 이 속성은 많은 브라우저에서 사용할 수 있기 때문에 가장 간단한 해결 방법이 될 수 있을 것 같습니다(can i use 참고). 다만 이 방법은 iOS 사파리에서 사용하기 위해 추가 속성이 필요합니다.
다른 방법으론 CSS의 overscroll-behavior
을 이용하는 방법이 있습니다. 이 속성은 스크롤 체이닝 처리를 위해 나온 속성입니다. overscroll-behavior
을 contain
으로 해두면 아까와 마찬가지로 이 문제를 해결할 수 있습니다.
당시 사용하고 있던 무한 스크롤 라이브러리에 혹시 문제가 있을까 싶어 라이브러리 코드를 직접 파헤쳤습니다. 당시 쓰고 있던 라이브러리는
react-infinite-scroll-component였습니다.
관련 코드 중 다음 데이터를 가지고 오는 로직을 중점적으로 봤습니다. 분석 결과 아래와 같은 원리로 동작합니다.
target
의 clientHeight
를 가져옵니다.threshold
값을 참조합니다.target.scrollTop + clientHeight >= (threshod.value/100)*target.scrollHeight
를 계산하여 참이면 충분히 아래쪽에 위치한다는 의미이므로 다음 페이지를 로딩합니다. 논리 상으론 틀린 것이 없어보입니다. 그럼 이제 해당하는 컴포넌트의 실제 속성 값을 대입해 봅니다.
잠깐
scrollTop
,clientHeight
,scrollHeight
란?
1. scrollTop : 수직으로 스크롤된 값.
2. clientHeight: content의 픽셀 값 (CSS Height + CSS padding)
3. scrollHeight: overflow로 인해 보이지 않는 content까지 다 합친 값
개발자 모드로 뷰를 확인해서 각각의 수치를 구했습니다
1. 타겟의 scrollTop
값 : 0
2. target의 clientHeight
: 880
3. target의 scrollHeight
: 880
여기서 조금 이상한 점이 있습니다. clientHeigth
와 scrollHeight
이 같다는 점입니다. 이렇게 되는 경우 스크롤이 애초에 발생하지 않습니다.
여기서 target의 cliengtHeight
를 scrollHeight
보다 작게 하면 오버플로우가 발생하기 때문에 스크롤 이벤트가 발생합니다.
onScrollListener = (event: MouseEvent) => {
if (typeof this.props.onScroll === 'function') {
// Execute this callback in next tick so that it does not affect the
// functionality of the library.
setTimeout(() => this.props.onScroll && this.props.onScroll(event), 0);
}
const target =
this.props.height || this._scrollableNode
? (event.target as HTMLElement)
: document.documentElement.scrollTop
? document.documentElement
: document.body;
// return immediately if the action has already been triggered,
// prevents multiple triggers.
if (this.actionTriggered) return;
const atBottom = this.props.inverse
? this.isElementAtTop(target, this.props.scrollThreshold)
: this.isElementAtBottom(target, this.props.scrollThreshold);
// call the `next` function in the props to trigger the next data fetch
if (atBottom && this.props.hasMore) {
this.actionTriggered = true;
this.setState({ showLoader: true });
this.props.next && this.props.next();
}
this.lastScrollTop = target.scrollTop;
};
스타일 수정으로 문제를 해결할 수 있었습니다. 이제 3번째 문제 상황만 남았습니다.
마지막 문제 상황을 그림으로 나타내면 아래와 같습니다.
스크롤로 리스트의 마지막 요소까지 불러오고 반복적으로 스크롤을 시도할 시 탭 영역 위로 리스트 컴포넌트가 겹쳐 올라가는 문제가 발생합니다.
이 부분은 스타일이 어떻게 그려지는 지 확인이 필요했고, 이는 사파리 개발자 도구의 레이어를 통해 볼 수 있습니다. 사파리의 개발자 도구 탭에서 레이어
항목에 들어가면 쌓여있는 레이어를 볼 수 있습니다. 아래는 예시 화면입니다.
다시 문제 상황으로 돌아가서 제가 궁금했던 지점은 두 가지 입니다.
1. 저 스크롤은 어디서 일어나는 걸까?
2. 왜 리스트 컴포넌트가 탭 영역의 위로 올라가는 걸까?
1번의 경우 모달 내에서 스크롤이 일어나므로 모달의 스크롤을 방지함으로써 해결할 수 있습니다.
2번의 경우 레이어 탭으로 확인한 결과 탭 영역보다 리스트 영역이 상위 레이어로 그려지고 있었습니다.
이렇게 그려진 이유를 알기 위해 MDN에 나와있는 쌓임 맥락 문서를 참고 했습니다.
그러던 중 새로운 쌓임 맥락이 생길 수 있는 스타일(-webkit-overflow-scrolling이 touch인 요소)이 존재하는 것을 알았고 모든 의문점을 풀 수 있었습니다.
이 버그를 해결하기 위해 총 3가지 방법을 써보면서 많은 공부가 되었습니다. 문제 해결을 위한 코드는 많이 작성하지 않았지만, 모달 버그를 수정하기 위한 근본적인 문제들을 파헤치면서 좋은 경험을 한 것 같습니다.