Skip to content

3주차기술공유

yeon_ edited this page Dec 23, 2019 · 2 revisions

infinite scroll

주제 : 무한스크롤을 구현 할 때 고려해본 것들

목차

  1. 무한스크롤 == pagination
  2. offset, limit 을 이용한 pagination
  3. cursor based pagination
  4. data fetch 의 타이밍
  5. scroll과 history
  1. 결국은 사용성 fake loading , lazy loading?
  1. 궁금하지만 아직 해결하지 못한 부분

추가로 궁금 스크롤에서 캐싱은 왜하지? list에 대해 state관리를 어떻게 하는지

  • fetch 해올 때 state가 변경된다면 컴포넌트가 전부 리렌더링 되지 않는지! ㄴ 오! 저도 궁금해요

일단 목차에 해당하는 내용에서 생각해본 내용이나 궁금한부분들을 적어주시면 좋을 것 같아요 . 해결책도 적어주시면 좋습니다. 적은 부분에 대해서 피드백도 좋고 추가로 작성해 주시는 것도 좋습니다.🙌🏼

  1. 무한스크롤에서도 캐싱을 해야할까? 왜?? - 이렇게!

그럼 취합해서 글을 만들겠습니다


1. 무한스크롤과 pagination

무한 스크롤은 스크롤이 끝나지 않고 쭉 내려가는 UI입니다. 사용자가 봤을 때는 데이터가 계속 보여지는 것 같지만 데이터를 어떻게 로드할까를 생각해본다면 페이지의 개념이라고 생각 할 수 있습니다.

하지만 다음페이지 버튼을 눌렀을 때 다음 페이지에 해당하는 데이터를 로드해 오는 것처럼 생각한다면 페이지네이션과 같다고 생각할 수 있습니다.

2. offset, limit을 이용한 pagination 구현

pagination 을 구현하기 위해서는 database의 offset, limit의 개념을 사용할 수 있습니다. offset은 어느 index부터 데이터를 조회할 것인지 limit은 offset으로부터 어디까지의 데이터를 조회할 것인지를 의미합니다.

const OFFSET = 10;
const fetchMoreFeed = async () => {
    const { data: value } = await fetchMore({
      variables: {
        first: OFFSET,
        after: cursorIdx,
      },
      updateQuery: (prev, { fetchMoreResult }) => {
        if (!fetchMoreResult) {
          return prev;
        } else {
          const { feeds: feedItems } = fetchMoreResult;
          setCursorIdx(OFFSET + cursorIdx);
        }
        ...
      }
    });
    ...
  };
  • apollo useQuery Hook을 이용해서 데이터 요청

3. cursor based pagination

하지만 문제가 있습니다 내가 다음글을 보기 전에 새로운 글이 등록되거나, 삭제되었다면 DB에서는 이미 조회한 글이나 다다음 글을 조회해 올 수 있습니다.

저희가 진행하고 있는 facebook과 같이 사용자들의 글이 실시간으로 등록되고 보여져야 되는 경우도 이런 경우라고 생각했습니다. 그래서!

cusrsor based pagination을 적용하기로 했습니다. 지금 보고있는 피드의 마지막 item id 이후의 데이터만 조회하는 것 입니다.

여기서 feed item의 id는 고유한 값이여야 합니다. 그래야 데이터베이스쪽에서 잘못된 값을 조회하지 않으니까요.

 const [cursor, setCursor] = useState<string>("9999-12-31T09:29:26.050Z");
const OFFSET = 10;
const fetchMoreFeed = async () => {
    const { data: value } = await fetchMore({
      variables: {
        first: OFFSET,
        currentCursor: cursor
      },
      updateQuery: (prev, { fetchMoreResult }) => {
        if (!fetchMoreResult) {
          return prev;
        } else {
          const { feeds: feedItems } = fetchMoreResult;
          const lastFeedItem = feedItems[feedItems.length - 1];
 setCursor(lastFeedItem.createdAt);
        }

        return ...
      }
    });
  • 마지막으로 feed item의 시간을 cursor로 결정하고 해당 피드 이후의 피드들에서 offset으로 가지고오기로 했습니다
  • feed item의 등록 시간은 밀리초까지 저장되는 datetime type입니다.

data fetch 의 타이밍

그럼 데이터는 언제 불러서 언제 로드해야할까요? 무한 스크롤이므로 스크롤이 스크린의 바닥에 닿았을 때 새로운 데이터들을 로드해와야 합니다.

현재 저희는 react 와 graphql neo4j를 이용해서 개발을 진행하고 있습니다. 리엑트에서의 저희가 구현한 방법으로 예시를 들겠습니다.

  1. scroll 이벤트의 감지
  • document height : 문서 전체의 높이
  • window height : 화면의 높이.
  • scroll top :스크롤의 top이 위치하고 있는 높이.
document.documentElement.scrollTop + document.documentElement.clientHeight === document.documentElement.scrollHeight
  1. scroll의 끝을 감지하는 hook
const useScrollEnd = () => {
  const [state, setState] = useState(false);
  const onScroll = () => {
    if (
      document.documentElement.scrollTop +
        document.documentElement.clientHeight ===
      document.documentElement.scrollHeight
    ) {
      setState(true);
    } else {
      setState(false);
    }
  };
  useEffect(() => {
    window.addEventListener("scroll", onScroll);
    // 스크롤 이벤트는 꼭 삭제해줍니다!
    return () => window.removeEventListener("scroll", onScroll);
  }, []);
  return state;
};
  1. 해당 상황이 감지가되어서 상태값을 바꿀 때 마다 앞에서 구현한 fetchMoreFeed를 수행합니다.

scroll과 history

이제 스크롤에 따라 계속 데이터가 계속 이어져서 보이는 것처럼 보입니다.하지만 만약 다른 페이지로 갔다가 뒤로가기를 누른다면 어떻게 될까요? 다른 게시글에 갔다가 돌아왔을 때 다시 맨 위부터 다시 보이는 것 보다 보고 있던 페이지 근처의 데이터가 보이는 것이 더 좋을 것 같습니다. 페이지를 패칭할 때

intersectionObserver

정의 : provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's *viewport. => 비동기적으로 타겟 엘리먼트와, 해당 엘리먼트의 부모나 뷰포트상에서의 교차 지점을 감지하는 방법

*viewport? 컴퓨터 그래픽상에서 현재보여지는 화면 영역

뭐가 장점인데?

비동기로 작업하기 때문에 스크롤이 더 자연스러움

how to

let options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 1.0
}
// 1. IntersectionObserver를 만든다
let observer = new IntersectionObserver(callback, options);
// 2. tar 만든다
let target = document.querySelector('#listItem');
observer.observe(target)

ref : intersectionObserver mdn https://tech.lezhin.com/2017/07/13/intersectionobserver-overview

Fake Loading / Lazy Loading

Fake Loading이란.. Eager Loading

  • 한번에 불러오는 것
  • 서버에 query를 한번만 보내면 된다.

Lazy Loading

  • 하나당 여러개의 Collection이 존재할 때 사용한다.
  • 관련된 개체를 즉시 사용하지 않을 경우에 사용

ref

8.궁금하지만 아직 해결하지 못한 부분

  1. 캐싱 문제 모든 사용자들에게 똑같은 페이지를 로딩하는 경우, 캐시는 큰 도움이 될 수 있다. 하지만 페이스북, 인스타그램과 같이 쿠키에다가 사용자 정보를 담아서 사용자에 따라 다른 정보를 제공하는 페이지라면, 캐싱이 의미가 있을까? 오히려 쿼리로 캐싱을 구분하는 apollo-client가 모든 사용자에게 캐싱된 하나의 같은 페이지를 보여주게 되는 것 아닐까?
    => 먼저, 사용자 정보를 쿠키에 담지 않고, 쿼리의 인자에 담는다면 사용자마다 쿼리가 구분될 것이고 캐싱이 가능해 질 것 같다. 또한 서로 다른 사용자의 페이지 로딩에는 캐싱이 큰 도움이 되지 않지만, 사용자가 새로 고침을 할 경우 페이지 로딩에 도움을 줄 것 같다.

Scroll Optimzation

  • 매 스크롤마다 스크롤 이벤트에 대한 콜백이 발생합니다.
  • 이러한 콜백에는 많은 리소스가 필요할 수도 있습니다.
  • 이벤트 핸들러가 많은 연산(예 : 무거운 계산 및 기타 DOM 조작)을 수행(이벤트 핸들러의 과도한 횟수가 발생하는 것)하는 경우 에 대해 제약을 걸어 제어할 수 있는 수준으로 이벤트를 발생(그 핸들러를 더 적게 실행하면 빠져 나갈 수 있음)시켜야 합니다.

Fake Loading / Lazy Loading

intersection observer 를 사용해서 해당 이미지가 뷰포트에 들어올 때 로드하도록 설정

windowing

목록이 길어진 경우 뷰포트에서 사라질 경우 돔에서 랜더링하지 않도록 하는 기술로 https://ko.reactjs.org/docs/optimizing-performance.html#virtualize-long-lists 기법을 사용할 수 있다.

throttling

  • 이벤트를 일정한 주기마다 발생하도록 하는 기술

debouncing

  • 연이어 호출되는 함수들 중 마지막 함수(또는 제일 처음)만 호출하도록 하는 것

ref

requestAnimationFrame

ref

Clone this wiki locally