intro 팀 프로젝트에서 무한 스크롤 기능을 구현하게 되었다. 처음 사용하다 보니 걱정도 많았고, 실수도 많았지만 차근차근 사용 경험을 정리해 보고자 한다. 서론에서는 많은 도움을 준 블로깅들을 간략하게 정리한 후에 본론으로 들어가 보도록 하겠다.
무한 스크롤(Infinite Scroll)이란?
사용자가 특정 페이지 하단에 도달했을 때, API가 데이터를 호출하여 콘텐츠가 끊기지 않고 계속 로드되는 사용자 경험 방식이다.
한 페이지에서 스크롤만으로 새로운 콘텐츠를 보여주게 되므로, 많은 양의 콘텐츠를 스크롤하여 볼 수 있는 장점이 있다.
스크롤 이벤트로 무한 스크롤을 구현하면 각 Element마다 이벤트가 등록되어 있어 감지될 때마다 이벤트가 끊임없이 호출된다는 문제점이 있었고 이는 곧 리플로우에 좋지 않다는 글들을 많이 접할 수 있었다. 많은 블로깅 글과 멘토님의 코멘트 덕분에 해당 API로 기능을 구현하기로 결정하였다.
내가 사용한 건 Intersection Observer API이다.
Intersection Observer API는 기본적으로 브라우저 Viewport와 Target으로 설정한 요소의 교차점을 관찰하여 그 Target이 Viewport에 포함되는지 구별하는 기능을 제공한다. 이를 통해 성능적으로 더 나은 무한 스크롤을 구현할 수 있게 만들어 준다.
아래는 MDN에서 정의한 내용을 가져와봤다.
Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법입니다.
MDN
# root
: 이 옵션에 정의된 Element를 기준으로 Target Element가 노출 여부를 판단한다. null 또는 지정되지 않았을 때 기본값으로 Browser Viewport가 설정된다.
# rootMargin
: root에 정의된 Element가 가진 마진 값을 의미한다. 사용법은 CSS의 margin 속성과 매우 유사하다. threshold를 계산할 때 rootMargin 만큼 더 계산한다. 기본값은 0px 0px 0px 0px이다. (반드시 단위 입력 필요)
# threshold
: Target Element가 root에 정의된 Element에 노출되었을 때 Callback 함수를 실행시킬지 정의하는 옵션이다. 콜백이 실행되기 위해 target의 가시성이 얼마나 필요한지 백분율로 표시한다. 기본값은 배열 [0]이며, Number 타입의 단일 값으로 작성 가능하다. number로 정의할 경우, Target Element의 노출 비율에 따라 Callback 함수를 한번 호출할 수 있지만, number[ ]로 정의할 경우, 각각의 비율로 노출될 때마다 Callback 함수를 호출한다.
let options = {
root: document.querySelector('#scrollArea'),
rootMargin: '0px',
threshold: 1.0
}
let observer = new IntersectionObserver(callback, options);
➡ 즉, Intersection Observer API는 Target 요소가 root 요소와 교차가 일어나는지를 판단하여 콜백 함수를 실행한다!
빠른 적용을 위해 Intersection Observer를 더 쉽게 사용할 수 있는 라이브러리를 통해 기능을 구현하였다.
설치하기
npm install react-intersection-observer
1. target 생성하기
해당 라이브러리에서는 useInView 훅을 제공하는 데 이를 사용하면 구성 요소의 상태를 쉽게 모니터링할 수 있다. 필요한 옵션을 넣어 useInView를 호출한다. 모니터링하려는 DOM 요소에 ref를 할당하면 inView가 상태를 보고한다.
2. 콜백 함수 생성하기
데이터 요청 함수이다. page를 받아서 해당 page를 요청하게 되고, 받아온 데이터를 상태에 저장해 주고 있다.
getLetters는 data가 바뀔 때마다 재생성되는 함수이다.
💥 테스트 중 만난 오류 사항
이곳에서 렌더링이 될 때마다 같은 데이터가 중복되는 현상이 발생하였다. 초깃값을 주어도 이를 해결할 수 없었다.
이럴 때는 내가 햇병아리구나... 싶긴 하다. 결국 멘토님의 도움으로 문제를 해결하였다.
아직 설명하진 못했지만, 아래 이미지도 함께 보면서 이해를 하면 좋을 것 같다!
초창기 나의 코드에는 콜백 함수 매개변수로 아무 값도 받지 않았다. 하지만 호출 문에서 page 전달받는 것으로 변경하였고, 데이터를 상태에 저장할 때 분기 처리를 해주었다.
만약 1페이지라면 1페이지 데이터만 가져오고, 그게 아닌 그다음 페이지부터는 데이터가 쌓이도록 작성하였다.
그랬더니 간단히 문제가 해결되었다.
보고 나면 너무 당연한 코드가 내 머릿속엔 왜 없는 걸까... 흑흑 더 분발하자!!!!!
+ useCallback을 내가 잘 알지 못하고 사용하고 있다는 생각도 들어, useCallback에 대한 블로깅도 진행해 보도록 하겠다. 현재 코드가 조금 어색해 보여도 잠시 눈감아주자...
3. 실행하기
두 번째 useEffect부터 보자. 해당 useEffect은 inView의 상태가 변경될 때마다 실행하게 된다.
만약 사용자가 마지막 요소를 보고 있고(inView가 true이고), 로딩이 false 이면,
로딩이 true가 돼서 로딩 문구가 나타나고 1.5초 뒤에 페이지가 1 증가하여 페이지의 상태가 바뀌게 된다.
로딩은 다시 false로 지정해 준다.
페이지의 상태가 바뀌면, 첫 번째 useEffect 문이 실행되게 된다. 즉, 1이 증가된 페이지 숫자를 전달하여 데이터 요청이 보내진다. 그럼 다시 2번에서 데이터를 상태에 저장하게 되고, 화면에 map으로 데이터가 뿌려지게 된다.
요약하면 맨 하단으로 내렸을 때 page 값이 증가되면서 다음 page에 대한 데이터를 서버에서 가져온다.
우여곡절 끝에 무한스크롤링 구현을 성공하였다. 하지만 데이터를 저장하는 방식이나 내가 작성한 로직에 대해서 더 고민해 보고 싶다. 무한스크롤에서 필터링을 하는 방법에 대해 그리고 실제 API로 구현하는 방법도 추후 블로깅에 추가 작성하고 싶다.
2023.03.21. 15:02
📜 출처