모달 버그 해결기(feat. 무한 스크롤)

윤다은·2022년 12월 5일
0

회사에서 모달에 관한 버그를 수정해보기로 했습니다. 지금은 이미 수정되어서 프로덕션 레벨까지 잘 반영되어 있습니다.

총 3가지 버그가 존재했었는데요. 차례차례 그 해결 방안을 공유하고자 합니다.

뷰 소개

문제의 레이아웃은 아래와 같습니다

모달이 있으며 모달 내부에는 탭이 있습니다. 탭 안에는 리스트가 존재합니다. 리스트는 무한 스크롤로 동작합니다.

문제 상황

발견된 버그는 총 3가지 였습니다.
1. 무한 스크롤이 동작하지 않았습니다.
2. 모달이 떠 있는 동안 오버레이 너머에 있는 화면에서도 스크롤이 발생합니다.
3. 모바일 웹에서 스크롤을 반복하면 탭과 리스트 컴포넌트가 겹치는 현상이 발생합니다.

해결 방법

문제를 해결하기 위해선 그 원인을 알아야 합니다. 지금부터 그 원인을 차례로 파악해보겠습니다.

1. 브라우저의 기본 동작을 파악하자

먼저 문제 1번의 경우엔 기본적인 브라우저 동작입니다. 하지만 지금은 이 동작을 원하지 않습니다. 모달이 떠 있는 동안엔 뒷 배경의 스크롤이 동작하지 않았으면 하죠.

간단하게 모달이 떠 있는 동안에 뒷 배경에 overflow:hidden을 적용해서 이 문제를 해결할 수 있습니다. 이 속성은 많은 브라우저에서 사용할 수 있기 때문에 가장 간단한 해결 방법이 될 수 있을 것 같습니다(can i use 참고). 다만 이 방법은 iOS 사파리에서 사용하기 위해 추가 속성이 필요합니다.

다른 방법으론 CSS의 overscroll-behavior을 이용하는 방법이 있습니다. 이 속성은 스크롤 체이닝 처리를 위해 나온 속성입니다. overscroll-behaviorcontain으로 해두면 아까와 마찬가지로 이 문제를 해결할 수 있습니다.

2. 무한 스크롤 라이브러리를 파헤쳐보자

당시 사용하고 있던 무한 스크롤 라이브러리에 혹시 문제가 있을까 싶어 라이브러리 코드를 직접 파헤쳤습니다. 당시 쓰고 있던 라이브러리는
react-infinite-scroll-component였습니다.

관련 코드 중 다음 데이터를 가지고 오는 로직을 중점적으로 봤습니다. 분석 결과 아래와 같은 원리로 동작합니다.

  1. 스크롤 발생 시 스크롤이 발생한 노드를 가져옵니다.
  2. 만약 이 이벤트가 이미 트리거 되었다면 바로 함수를 종료합니다.
  3. 타겟이 충분히 아래쪽에 왔는 지 확인합니다.
    3-1. targetclientHeight를 가져옵니다.
    3-2. 미리 설정한 threshold 값을 참조합니다.
    3-3. 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

여기서 조금 이상한 점이 있습니다. clientHeigthscrollHeight이 같다는 점입니다. 이렇게 되는 경우 스크롤이 애초에 발생하지 않습니다.

여기서 target의 cliengtHeightscrollHeight보다 작게 하면 오버플로우가 발생하기 때문에 스크롤 이벤트가 발생합니다.

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번째 문제 상황만 남았습니다.

3. 사파리 개발자 도구를 통한 UI 디버깅

마지막 문제 상황을 그림으로 나타내면 아래와 같습니다.

스크롤로 리스트의 마지막 요소까지 불러오고 반복적으로 스크롤을 시도할 시 탭 영역 위로 리스트 컴포넌트가 겹쳐 올라가는 문제가 발생합니다.

이 부분은 스타일이 어떻게 그려지는 지 확인이 필요했고, 이는 사파리 개발자 도구의 레이어를 통해 볼 수 있습니다. 사파리의 개발자 도구 탭에서 레이어 항목에 들어가면 쌓여있는 레이어를 볼 수 있습니다. 아래는 예시 화면입니다.

다시 문제 상황으로 돌아가서 제가 궁금했던 지점은 두 가지 입니다.
1. 저 스크롤은 어디서 일어나는 걸까?
2. 왜 리스트 컴포넌트가 탭 영역의 위로 올라가는 걸까?

1번의 경우 모달 내에서 스크롤이 일어나므로 모달의 스크롤을 방지함으로써 해결할 수 있습니다.
2번의 경우 레이어 탭으로 확인한 결과 탭 영역보다 리스트 영역이 상위 레이어로 그려지고 있었습니다.

이렇게 그려진 이유를 알기 위해 MDN에 나와있는 쌓임 맥락 문서를 참고 했습니다.

그러던 중 새로운 쌓임 맥락이 생길 수 있는 스타일(-webkit-overflow-scrolling이 touch인 요소)이 존재하는 것을 알았고 모든 의문점을 풀 수 있었습니다.

정리

이 버그를 해결하기 위해 총 3가지 방법을 써보면서 많은 공부가 되었습니다. 문제 해결을 위한 코드는 많이 작성하지 않았지만, 모달 버그를 수정하기 위한 근본적인 문제들을 파헤치면서 좋은 경험을 한 것 같습니다.

profile
코끼리가 코로 걸어다니는 코드를 지양합니다.

0개의 댓글