react-infinite-scroller + Graphql / JS Infinite Scroll

이병관·2021년 6월 2일
0
post-custom-banner

무한으로 긁긁긁


페이지네이션의 기법중 하나인 무한스크롤은
일반적으로 사용자에게 편리한 글목록을 보여주기 위한 방법중 하나이다.


다음 글목록을 보거나 다른 글로 넘어가기 위해 페이지 버튼을 누르는거 대신,
특정 위치에 사용자의 스크롤이 도달하면 자동으로 쿼리를 날려서
페이지 아래단에 다시 글을 붙여주는 형식이다.

예를 들자면 페이스북, 트위터, 인스타그램이 있다.

하지만 무한 스크롤의 단점도 존재한다.

  • 사용자가 현재의 위치를 알기 힘들고 표류하는 느낌이 난다.
  • 원하는 위치에 있는 자료를 찾기 힘들다.
  • 웹페이지의 푸터 부분을 볼 수 없다.
  • 글을 읽고난 후 뒤로가기를 했을 때 원래 위치로 돌아가기 힘들다.

그러니 UX를 고려하여 알맞은 페이지에 구현하는것이 중요하다.

그래서 나는 댓글 부분은 무한스크롤로 구현해보기로 했다.

그래서 어떻게 하는데


나도 처음엔 몰라서 3시간 정도 삽질을 했다.
하지만 다음에는 이런 실수가 없기를 바라며 조금 적어본다.

아직 나는 병아리 수준이기 때문에 Intersection Observer과 같은 무시무시한 친구 보다는 전통적인 방법과 React Infinite Scroll Component를 이용해 두번 구현해보기로 했다.

Intersection Observer(교차 관찰자)?

IntersectionObserver(교차 관찰자 API)는 타겟 엘레멘트와 타겟의 부모 혹은 상위 엘레멘트의 뷰포트가 교차되는 부분을 비동기적으로 관찰하는 API이다.

뷰포트(viewport)는 현재 화면에 보여지고 있는 다각형(보통 직사각형)의 영역입니다. 웹 브라우저에서는 현재 창에서 문서를 볼 수 있는 부분(전체화면이라면 화면 전체)을 말합니다. 뷰포트 바깥의 콘텐츠는 스크롤 하기 전엔 보이지 않습니다.
MDN

즉, Intersection Observer 란 화면(뷰포트) 상에 내가 지정한 타겟 엘레멘트가 보이고 있는지를 관찰하는 API이다.

물론 Intersection Observer를 사용한다면 디바운싱 같은 작업이 필요하지 않지만... 좀만 더 공부해보고 나중에 구현하기로하고, 일단 전통적인 방법에 따르면 다음과 같다.

  1. 페이지의 끝부분, 즉 컨텐츠의 끝 부분에 도달했음을 감지 했을때,
  2. 다음글이나 목록에 해당되는 쿼리문을 날려 가져온 후, 그것을 페이지 끝에 다시 넣어준다.

컨텐츠의 크기


그렇다면 페이지의 끝부분에 해당되는 영역에 도달했음을 알기 위해선, 현재 페이지의 크기와 스크롤의 위치. 그리고 끝부분의 위치가 필요하다.

간략화 한다면 다음과 같은 페이지를 생각해보자

<div class="infinite_Wrapper">
<div class="contents">
헤더
</div>
<div class="contents">
바디
</div>
</div>
<div class="end">
</div>

https://javascript.info/size-and-scroll

자바스크립트 화면의 모든 부분의 크기(scrollHeight)
스크롤을 올리면 화면에 보이지 않는 부분 (scrollTop)
스크롤바의 공간을 제외한 부분 (clientHeight)을 정의한다.

let scrollHeight = document.documentElement.scrollHeight
let scrollTop = document.documentElement.scrollTop
let clientHeight = document.documentElement.clientHeight

그래서 이들을 비교하면 되는데, 문제는 브라우저별로 도출하는 크기의 기준이 틀리다는것이다...

Document sizes are a browser compatibility nightmare because, although all browsers expose clientHeight and scrollHeight properties, they don't all agree how the values are calculated.

There used to be a complex best-practice formula around for how you tested for correct height/width. This involved using document.documentElement properties if available or falling back on document properties and so on.

The simplest way to get correct height is to get all height values found on document, or documentElement, and use the highest one.
https://stackoverflow.com/questions/1145850/how-to-get-height-of-entire-document-with-javascript

따라서 math.max()함수를 사용해 body에 담겨있는 geometry 값과 비교하여 둘 중에서 큰 값을 따르도록 바꾸자.

 let scrollHeight = Math.max(
      document.documentElement.scrollHeight,
      document.body.scrollHeight
    );
    let scrollTop = Math.max(
      document.documentElement.scrollTop,
      document.body.scrollTop
    );
    let clientHeight = document.documentElement.clientHeight;

scrollTop + clientHeight의 값이 스크롤 높이보다 크거나 같을 때가 바로
컨텐츠의 끝에 도달했다는 뜻이니

if (scrollTop + clientHeight >= scrollHeight)

이때 원하는 쿼리문을 날려버리자. 물론 나는 지금 Graphql을 사용중이기 때문에 해당 쿼리를 이용해서 날려야한다.

const infiniteScrolling = () => {
    let scrollHeight = Math.max(
      document.documentElement.scrollHeight,
      document.body.scrollHeight
    );
    let scrollTop = Math.max(
      document.documentElement.scrollTop,
      document.body.scrollTop
    );
    let clientHeight = document.documentElement.clientHeight;

    if (scrollTop + clientHeight >= scrollHeight) {
      if(data.fetchBoardComments.length % 10 !== 0)
      return;
      fetchMore({
        variables: {
          boardId : String(router.query.id),
          page: (data.fetchBoardComments.length / 10) + 1
        },
        updateQuery: (prev, {fetchMoreResult}) => {
          if (!fetchMoreResult) 
          return prev;
  
          return Object.assign({}, prev, {
            fetchBoardComments: [
              ...prev.fetchBoardComments,
              ...fetchMoreResult.fetchBoardComments
            ]
          });
        }
      })
    }
  };

FetchMore이 이상하게 동작하는건지, 아니면 useState를 저기 안에서 인식을 못하는것인지, useState를 사용해서 page의 값을 바꾸면 무한으로 쿼리를 날려버린다.
따라서 가져온 값을 Backend에서 정의한 갯수에 도달 했을때 그 다음 페이지로 넘겨버리기 위해 저렇게 입력했다.
하나의 페이지엔 10개의 댓글을 불러오기때문에 10으로 나눈 나머지가 있으면 다음페이지로, 없으면 일단 한번 다음페이지로 넘어간 후, 진짜 글이 없을 경우 return을 통해 함수를 빠져나오게 한다.

boardComment.presenter


그리고 무한으로 스크롤 할 div를 그려주고, 그 안에 반복적인 스타일을 적용할 컴포넌트들을 넣어주고 onScroll 이벤트로 해당 함수를 적용 하면 된다.

약간의 스타일적 문제가 있어서 overflowY:scroll로 보이지 않는 부분에 스크롤을 적용해야 하는게 좀 아쉽긴 햇지만 그래도 잘 작동했다.

물론 잘 작동은 했지만, 끝부분에서 대기했을때 불러오는 부분을 따로 주지 않아서 특정 지역이 도달하자 마자 계속해서 쿼리가 동작해서 계속 불러오는것이 아쉽긴 하다. 그부분은 따로 또 수정이 필요하다

react-infinite-scroller


react-infinite-scroller를 설치 한 후,
그냥 presenter에서 해당 부분을 불러와서 사용하면 끝이다.

//함수부분
 const onLoadMore = () => {
    if(data.fetchBoardComments.length % 10 !== 0)
    return;

    fetchMore({
      variables: {
        boardId : String(router.query.id),
        page: (data.fetchBoardComments.length / 10) + 1
      },
      updateQuery: (prev, {fetchMoreResult}) => {
        if (!fetchMoreResult) 
        return prev;

        return Object.assign({}, prev, {
          fetchBoardComments: [
            ...prev.fetchBoardComments,
            ...fetchMoreResult.fetchBoardComments,
          ]
        });
      }
    })

  }

container에서 함수를 정의 한 후,

<InfiniteScroll
     pageStart={0}
     loadMore={onLoadMore}
     hasMore={true || false}
     height={1000}
          > }
     
     ...<반복되는 스타일>

을 정의 하면 된다.

라이브러리가 편하긴 햇지만 한번 직접 구현도 해보고 싶었다.

profile
뜨겁고 매콤하고 화끈하게
post-custom-banner

0개의 댓글