과제 1 - Infinite Scrolling

늘보·2021년 7월 27일
0

Overview

배포한 과제 링크

과제 GitHub 링크

원티드 프리온보딩 프론트엔드 코스의 첫 번째 기술 과제를 마쳤다. 이전에 개인 프로젝트에서 무한 스크롤 기능을 구현했어서 큰 어려움을 겪지는 않았다. 강사님 말씀으로는? 간단하게 실력을 평가하는 척도로 Infinite-scolling을 구현하는 과제라고 하셨다. 작동은 아래와 같다.

스타일 코드를 제외한 코드는 아래와 같다.(React, JS 사용)

import React, { useState, useEffect, useRef } from 'react';
import {
  CommentContainer,
  CommentBox,
  InfiniteScrollContainer,
  CommentDetail,
} from './InfiniteScrollListStyle';

function InfiniteScrollList() {
  const [comments, setComments] = useState([]);
  const [pageNumber, setPageNumber] = useState(1);
  const [loading, setLoading] = useState(false);

  const fetchComments = async (pageNumber) => {
    const res = await fetch(
      `https://jsonplaceholder.typicode.com/comments?_page=${pageNumber}&_limit=10`,
    );
    const data = await res.json();
    setComments((p) => [...p, ...data]);
    setLoading(true);
  };

  useEffect(() => {
    fetchComments(pageNumber);
  }, [pageNumber]);

  const pageEnd = useRef();

  let num = 1;

  useEffect(() => {
    if (loading) {
      const observer = new IntersectionObserver(
        (entries) => {
          if (entries[0].isIntersecting) {
            num++;
            loadMore();
            // num이 50이 되는 순간은 데이터 전체 개수인 500개가 되는 순간이기 때문에
            // observer를 off 시켜준다.
            if (num >= 50) observer.unobserve(pageEnd.current);
          }
        },
        { threshold: 1 },
      );
      observer.observe(pageEnd.current);
    }
  }, [loading, num]);

  const loadMore = () => {
    setPageNumber((prevPageNumber) => prevPageNumber + 1);
  };

  return (
    <InfiniteScrollContainer>
      {comments.map((comment, index) => (
        <CommentContainer key={index}>
          <CommentBox>
            <div>Comment Id</div>
            <span>{comment.id}</span>
          </CommentBox>
          <CommentBox>
            <div>Email</div>
            <span>{comment.email}</span>
          </CommentBox>
          <CommentDetail>
            <div>Comment</div>
            <p>{comment.body}</p>
          </CommentDetail>
        </CommentContainer>
      ))}
      <div ref={pageEnd}></div>
    </InfiniteScrollContainer>
  );
}

export default InfiniteScrollList;

간단하게 정리하자면, Intersection Observer API를 사용하였다. Infinite Scrolling을 구현하는 방법이 기존의 스크롤 이벤트, 내가 사용한 방법 등 몇 가지 있는 걸로 아는데, 주워들은 얘기지만 현업에서는 Intersection Observer API를 사용한다고 들었다.

기존 scroll 이벤트의 문제

웹사이트를 개발할 때 특정 위치에 도달했을 때 어떤 액션을 취해야 한다면 어떻게 구현할 수 있을까? 보통 addEventListener()의 scroll 이벤트를 먼저 생각할 것이다. document에 스크롤 이벤트를 등록하고, 엘리먼트가 도달했을 때 실행할 콜백 함수를 등록하는 것이다.

하지만 scroll 이벤트는 단시간에 수백번, 수천번 호출될 수 있고 동기적으로 실행되기 때문에 메인 스레드(Main Thread) 영향을 준다고 한다. 또한 한 페이지 내에 여러 scroll 이벤트(무한 스크롤, 광고 배너, 애니메이션 등)가 등록되어 있을 경우, 각 엘리먼트마다 이벤트가 등록되어 있기 때문에 사용자가 스크롤할 때마다 이를 감지하는 이벤트가 끊임없이 호출된다.

디바운싱(Debouncing)과 쓰로틀링(Throttling)을 통해 이러한 문제를 개선시킬 수도 있기도 하고, 특정 지점을 관찰하기 위해서는 getBoundingClientRect() 함수를 사용해야 하는데, 이 함수는 리플로우(reflow) 현상이 발생한다는 단점이 있다.

  • 리플로우(reflow): 리플로우는 브라우저가 웹 페이지의 일부 또는 전체를 다시 그려야하는 경우 발생한다.

해당 블로그에 따르면, Intersection Observer API(교차 관찰자 API)를 사용하면 위와 같은 문제를 해결할 수 있다고 한다. 결과적으로, 비동기적으로 실행되기 때문에 메인 스레드에 영향을 주지 않으면서 변경 사항을 관찰할 수 있다.


위의 코드 중 아래의 설명은 내가 개인 프로젝트를 진행했을 때 바로 이해하기 힘들었던 부분들이다.

threshold

  • default: 0

observer의 콜백이 실행될 대상 요소의 가시성 퍼센티지를 나타내는 단일 숫자 혹은 숫자 배열입니다. 만일 50%만큼 요소가 보여졌을 때를 탐지하고 싶다면, 값을 0.5로 설정하면 됩니다. 혹은 25% 단위로 요소의 가시성이 변경될 때마다 콜백이 실행되게 하고 싶다면 [0, 0.25, 0.5, 0.75, 1] 과 같은 배열을 설정하세요.
기본값은 0이며(이는 요소가 1픽셀이라도 보이자 마자 콜백이 실행됨을 의미합니다). 1.0은 요소의 모든 픽셀이 화면에 노출되기 전에는 콜백을 실행시키지 않음을 의미합니다.

요약해서, 내가 작성한 threshold: 1pageEndref가 있는 요소(div태그) 전체가 보여야만 콜백함수(loadMore())을 실행시킬 수 있다는 말이다.

isIntersecting

// MDN에서 제공되는 코드다.
let callback = (entries, observer) => {
  entries.forEach(entry => {
    // Each entry describes an intersection change for one observed
    // target element:
    //   entry.boundingClientRect
    //   entry.intersectionRatio
    //   entry.intersectionRect
    //   entry.isIntersecting -> 내가 사용한 부분
    //   entry.rootBounds
    //   entry.target
    //   entry.time
  });
};

isIntersecting 프로퍼티는 target element가 교차 영역에 있다면 true를 반환하고, 아니라면 false를 반환한다.


후기

Intersection Observer API에 대해 더 알 수 있던 좋은 기회였다.

2개의 댓글

comment-user-thumbnail
2022년 2월 9일

안녕하세요! 이번 원티드 프리온보딩 프론트에 지원하였습니다! 궁금한것이 있는데 질문드려도 될까요?

1개의 답글