세 번째 고민 (무한 스크롤)

김민찬·2023년 4월 29일
0

프로젝트 회고

목록 보기
4/4
post-thumbnail

발단

사용자들이 급식에 관해 얘기하고 본인의 식습관에 따라 얻은 캐릭터를 자랑할 수 있는 SNS기능을 구현해야 했다.

모바일 환경을 가정하고 화면을 구성하는 만큼 무한스크롤을 통해 UX를 높이고 싶었다.

당시엔 비교적 여유가 있어 다음 두 가지 방법으로 구현해보고 더 나은 것을 취사선택 하기로 했다. 일단 이 글에선 두 번째 방법만 소개할까 한다.

  1. 사용자의 scroll이벤트를 인식해 일정 높이까지 height가 내려가면 새로운 정보를 받아온다.
  2. Intersection Observer API를 활용해 사용자의 브라우저 화면이 마지막 글에 접근하면 새로운 정보를 받아오기


구현

  • 통신함수
const getMoreList = async () => {
  		// 마지막 글을 봤다면 더 이상 요청을 보내지 않는다.
      if (boardInfo.lastBoardId === 1) {
        return;
      }
  		// 처음 방문하거나 사용자가 글 목록의 끝에 도달한다면 발동될 함수
      await getBoardList().then((data) => {
        if (articles[0]?.id === -1) {
          setArticles(data);
        } else {
          setArticles([...articles, ...data]);
        }
      });
    };

통신을 위한 함수를 선언한다. 그리고 통신에 성공하면 2가지 경우로 나뉜다.혹시나 첫 통신이라면 에러방지용 static 배열을 교체하고 아니라면 가져온 정보들을 render할 배열의 뒤에 붙여넣는다.

그리고 받아온 데이터가 만약 너무 작아 뒤에서 설정할 조건에 계속해서 걸리거나 마지막 글을 체크했다면 더 이상 통신을 시도하지 않는다.


  • Obsever
const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach(async (entry) => {
      if (entry.isIntersecting) {
        await getMoreList();
      }
    });
  },
  [observerOptions, articles]
);

observer.observe(articleListRef.current.lastChild);

Intersection Observer API를 활용하여 마지막 글에 Observer를 붙인다. 이 옵저버에 사용자의 브라우저가 접근하면 미리 준비해뒀던 통신 함수를 사용할 수 있다.


  • ObseverOption
const observerOptions = {
  root: null,
  rootMargin: "100px",
  threshold: 0.5,
};

또한 위와 같은 option을 조절하여 원하는 타이밍에 함수가 작동하게도 할 수 있다. 각각의 효능(?)은 다음과 같다.

  • root
    • 어떤 컴포넌트를 통해 observer가 발동할지 결정하는 요소다. null로 둔다면 최상단 컴포넌트가 되어 브라우저의 화면이 되겠지만 특정 컴포넌트를 지정할 수도 있다.
      다만 이때 그 컴포넌트는 observer가 부착될 컴포넌트보다 상위 컴포넌트여야만 한다.
  • rootMargin
    • root에서 정한 컴포넌트에 margin을 줘 더 빠르게 트리거할 수 있게 해주는 요소이다.
      예를 들어 100px을 준다면 root에서 선언한 컴포넌트 반경 100px이 observer가 부착된 컴포넌트에 닿는다면 트리거된다.
  • threshold
    • observer가 부착된 컴포넌트에 root에서 설정한 컴포넌트가 얼마나 침범(?)해야지 트리거되는지 정하는 요소이다.
      0.5로 해둘 경우 반만 보여도 트리거되며, 1일 경우엔 전부 다 보여야지 트리거가 발동한다.

좀 더 자세한 걸 알고싶다면 언제나 그렇듯 MDN 공식문서를 추천한다.


  useEffect(() => {
	// 통신함수
    
    // Observer에 부착될 함수

    observer.observe(articleListRef.current.lastChild);

    return () => observer.disconnect();
  }, [articles, boardInfo]);

마지막으로 위에서 선언한 함수들을 모두 useEffect안에 넣어둔다.
게시글을 가져온다면 마지막 글에 Observer를 부착해두고 다시 발동한다면 떼어내 중복으로 요청되지 않도록 한다.



한계점

마지막 글을 봤을 때 사용자는 왜 더 이상 글이 나오지 않는가 의문이 들거라 생각했다. 페이스북이나 트위터 같이 마지막 글에서 아래로 스크롤 하면 로딩 아이콘을 보여주고 싶었고, 마찬가지로 위로 스크롤하면 새로고침을 하게도 하고 싶었다. 하지만 아쉽게도 시간이 부족해 미처 구현하지 못해 UX적인 측면에서 아쉬움이 있다.

또한 dependencyArray에 useEffect 내부에서 set하는 state가 들어있어 불필요하게 2번 발동할 거라 생각한다. 이 점 또한 개선해야할 부분이라 생각한다.



마무리

6주 동안 눈 코 뜰 새 없이 바빴다고 생각했는데 돌아보니 그냥 시간을 허비했다는 느낌이 많이 드는건 왤까. 이미 시행착오를 다 알고 있어서 과거의 내가 좀 더 빠르게 일하길 바라는 꼰대 마인드가 아닐까?

암튼 다음 글은 모바일과 데스크탑 환경에서의 사진 저장이 될 것 같다.

profile
열심히 공부 중인 프론트엔드 개발자

0개의 댓글