[POB#02] Assignment 1 : 하얀 마인드 기업 과제 기술 정리. Infinity Scroll과 고려해야 할 것들

dongwon·2021년 7월 27일
3

원티드-프리온보딩

목록 보기
2/25
post-thumbnail

원티드 프리온보딩 첫 번째 과제의 주요 기능인 Infinity Scroll에 관한 글입니다.

관련 자료

🔗 테스트 링크

🔗 깃허브 저장소

알아야 할 것

redux와 redux-saga를 이용해 전역 상태 관리, 비동기 네트워크 통신을 했습니다.
앞으로 나오는 이벤트들에 의해 page가 변하게 되고, 해당 page에 맞는 데이터들을 가져옵니다.

useEffect(() => {
  dispatch(fetchCommentsRequest(page));
}, [dispatch, page]);

♾️ Infinity Scroll 구현 방법들

⚙️ Scroll Event

스크롤 이벤트height를 이용한 방법입니다.

useEffect(() => {
  function onScroll() {
    // window.scrollY : 내가 보고 있는 화면의 최상단 지점의 높이. 즉 스크롤을 통해 내린 height.
    // document.documentElement.clientHeight : 내 모니터 화면의 height.
    // document.documentElement.scrollHeight : 페이지의 총 높이 height.
    if (
      window.scrollY + document.documentElement.clientHeight >
      document.documentElement.scrollHeight - 300
    ) {
      // 조건 만족 시 다음 페이지 Set
      const nextPage = page + 1;
      setPage(nextPage);
    }
  }

  window.addEventListener("scroll", onScroll);
  return () => {
    // 뒷정리 함수를 이용해 스크롤이 끝나면 스크롤 이벤트 제거
    window.removeEventListener("scroll", onScroll);
  };
}, [page]);

장점

  1. 직관적입니다. height를 이용하기 때문에 무슨 의미인지 한눈에 알아볼 수 있었습니다.
  2. 부드러운 UI. 페이지의 총 높이에서 -300 정도 값을 빼 맨 끝에 닿기 전에 다음 페이지 로드할 수 있어 부드러운 UI를 구현할 수 있었습니다.

단점

  1. 스크롤 할 때마다 수많은 이벤트가 발생합니다. 렌더링 문제가 있을 수 있고 이를 조절하기 위해 throttle을 구현해야 하는 불편함이 있습니다.
  2. dispatch 역시 짧은 시간에 다수 발생합니다. 자원 낭비가 될 수 있고 이를 해결하기 위해 redux-saga의 effect인 throttle을 이용해 해결해야 하는 불편함이 있습니다.

⚙️ Intersection Observer API

스크롤 이벤트가 height를 이용한다면 Intersection Observer은 구역을 이용하는 느낌입니다. 브라우저의 뷰포트와 설정한 요소에 포착된다면 발생합니다.

const [target, setTarget] = useState(null);

useEffect(() => {
  let observer;
  const _onIntersect = ([entry]) => {
    // 포착 된 경우
    if (entry.isIntersecting) {
      const nextpage = page + 1;
      setPage(nextpage);
    }
  };
  if (target) {
    // observer 생성, 가시성 범위 설정
    observer = new IntersectionObserver(_onIntersect, { threshold: 1 });

    // 설정한 요소 관찰
    observer.observe(target);
  }
  // observer 삭제
  return () => observer && observer.disconnect();
}, [page, target, fetchLoading]);

return (
  <div>
    <CardList />
    <div ref={setTarget}>loading</div>
  </div>
);

장점

  1. scorll 이벤트 연속 호출을 제어할 수 있습니다. 이로 인해 dispatch 발생 또한 제어할 수 있습니다.

단점

  1. 저 같은 경우 Infinity Scroll과 같이 맨 밑에 만을 관찰하는 경우에는 쉽게 사용했지만 Scroll SPY와 같이 세밀한 height를 계산하는 경우 가시성의 범위를 구하는데 애를 먹고 포기한 경험이 있습니다. (저만 어려울 수도..)

⚙️ react-intersection-observer 패키지

react-intersection-observer 패키지를 이용했고 최종적으로 선택한 방법입니다. useInView hook을 이용한 간단한 사용법과 그로 인한 짧은 코드 길이가 특징입니다.

import { useInView } from "react-intersection-observer";

...

// ref 설정과 관찰 여부 변수
const [ref, inView] = useInView();

useEffect(() => {
  // 관찰
  if (inView) {
    dispatch(fetchCommentsRequest(page));

    const nextPage = page + 1;
    setPage(nextPage);
  }
}, [inView, page, dispatch]);

return (
  <>
    <CommentList />
    // 관찰 요소
    <div ref={ref} />
  </>
);

✔️ 고려할 점

🤔 화면에 DOM이 그려지기 전에 관찰할 요소가 관찰 당해버린다면

무슨 소리냐면.. 요소를 관찰하기 위해 만든 dom이 아직 데이터를 못 받아와 dom을 못 그리고 있을 때 뷰포트에 관찰 당해버리면 어떻게 되냐는 의미 입니다. (?)

당연히 감지되어 dispatch가 발생합니다. 하지만 왜인지 dispatch가 무한히 발생해 터져버렸습니다.

🎯 해결 방법

fetchLoading 변수를 이용했습니다. fetchLoadingfalse 일 때 즉, REQUEST가 끝난 SUCCESS, FAILURE 상태일 때만 호출되도록 구현했습니다.

// comment reducer
const comment = (state = initialStete, action) => {
  switch (action.type) {
      // 네트워크 요청 때 fetchLoading을 true로 설정합니다.
      // 요청이 SUCCESS, FAILURE 됐을 때 false로 변합니다.
    case FETCH_COMMENTS_REQUEST:
      return {
        ...state,
        fetchDone: false,
        fetchError: null,
        fetchLoading: true,
      };
    ...
  }
};
// 컴포넌트
useEffect(() => {
  // fetchLoading가 flase일 때, 즉 request 상태가 아닐때만 발생합니다.
  if (inView && !fetchLoading) {
    dispatch(fetchCommentsRequest(page));

    const nextPage = page + 1;
    setPage(nextPage);
  }
}, [inView, page, fetchLoading, hasMoreComments, dispatch]);

🤔 페이지의 끝까지 스크롤 해 더 이상 불러올 데이터가 없다면 ?

만약 끝까지 스크롤 해 더 이상 불러올 데이터가 없다면 어떻게 될지 고려해봤습니다. postman으로 확인해본 결과 총 500개의 댓글이 있는 것을 알 수 있었고 50번 스크롤 해서 마지막까지 스크롤 해봤습니다. ( limit을 바꾸면 한 번에 확인 가능합니다.. )

더 이상 로드할 데이터가 없어 같은 뷰포트에 머물러 있기 때문에 수많은 이벤트가 발생하고 있었습니다.

🎯 해결 방법

지금이 마지막 페이지라는 것을 알려줄 hasMoreComments 변수로 해결했습니다. fetch 요청이 성공했을 때, 받아온 데이터들의 길이를 이용해, 받아온 데이터의 개수가 0개라면 더 이상 받아올 데이터가 없음을 이용했습니다.
hasMoreComments를 이용해 dispatch 발생 여부를 결정했습니다.

// comment reducer
const comment = (state = initialStete, action) => {
  switch (action.type) {
    case FETCH_COMMENTS_SUCCESS:
      return {
        ...state,
        // 받아올 데이터가 남아 있나요?
        hasMoreComments: action.payload.length !== 0,
        comments: state.comments.concat(action.payload),
        fetchDone: true,
        fetchError: null,
        fetchLoading: false,
      };
  }
};
useEffect(() => {
  // 관찰 되어야 하고, 로딩도 끝나야하고, 더 받아올 데이터도 있어야지 다음 페이지 로딩
  if (inView && !fetchLoading && hasMoreComments) {
    dispatch(fetchCommentsRequest(page));

    const nextPage = page + 1;
    setPage(nextPage);
  }
}, [inView, page, fetchLoading, hasMoreComments, dispatch]);

return (
  <>
    <CommentList />
    <div ref={hasMoreComments && !fetchLoading ? ref : undefined} />
  </>
);
profile
데이원컴퍼니 프론트엔드 개발자입니다.

0개의 댓글