- 결국은 사용성 fake loading , lazy loading?
- 궁금하지만 아직 해결하지 못한 부분
무한 스크롤은 스크롤이 끝나지 않고 쭉 내려가는 UI입니다. 사용자가 봤을 때는 데이터가 계속 보여지는 것 같지만 데이터를 어떻게 로드할까를 생각해본다면 페이지의 개념이라고 생각 할 수 있습니다.
하지만 다음페이지 버튼을 눌렀을 때 다음 페이지에 해당하는 데이터를 로드해 오는 것처럼 생각한다면 페이지네이션과 같다고 생각할 수 있습니다.
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);
}
...
}
});
...
};
하지만 문제가 있습니다 내가 다음글을 보기 전에 새로운 글이 등록되거나, 삭제되었다면 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 ...
}
});
그럼 데이터는 언제 불러서 언제 로드해야할까요? 무한 스크롤이므로 스크롤이 스크린의 바닥에 닿았을 때 새로운 데이터들을 로드해와야 합니다.
현재 저희는 react 와 graphql neo4j를 이용해서 개발을 진행하고 있습니다.
리엑트에서의 저희가 구현한 방법으로 예시를 들겠습니다.
document.documentElement.scrollTop + document.documentElement.clientHeight === document.documentElement.scrollHeight
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;
};
그런데 스크롤 위치를 저렇게 감지하는 것은 동기적으로 작동됩니다. 하지만 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.
=> 비동기적으로 타겟 엘리먼트와, 해당 엘리먼트의 부모나 뷰포트상에서의 교차 지점을 감지하는 방법
intersectionObserver web api 에서는 전체 화면을 viewport로 하고 그 안에 구독으로 등록 해 놓은 element 가 들어왔는지를 감지하고 이벤트를 발생시킵니다. 옵져버니까요.
그래서 페이지의 제일 끝에 감지용 element를 두고 해당 element가 viewport에 보여지면 새로운 데이터를 불러오는 방식을 적용하였습니다.
*viewport?
컴퓨터 그래픽상에서 현재보여지는 화면 영역
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)
const [ref, setRef] = useState(null);
const checkIntersect = (
entry: IntersectionObserverEntry[],
observer: IntersectionObserver
) => {
if (entry[0].isIntersecting) {
interSectAction();
}
};
useEffect(() => {
let observer: any;
if (ref) {
observer = new IntersectionObserver(checkIntersect, baseOption);
observer.observe(ref);
}
return () => observer && observer.disconnect();
}, [ref, option.root, option.threshold, option.rootMargin, checkIntersect]);
return [ref, setRef];
ref : intersectionObserver mdn
https://tech.lezhin.com/2017/07/13/intersectionobserver-overview
이제 스크롤에 따라 계속 데이터가 계속 이어져서 보이는 것처럼 보입니다.하지만 만약 다른 페이지로 갔다가 뒤로가기를 누른다면 어떻게 될까요? 다른 게시글에 갔다가 돌아왔을 때 다시 맨 위부터 다시 보이는 것 보다 보고 있던 페이지 근처의 데이터가 보이는 것이 더 좋을 것 같습니다.
그래서 하나의 목록을 쭉 불러온 다음 페이지의 끝에 페이지 정보를 저장하면 되지 않을까..??.
restoration
intersection observer 를 사용해서 해당 이미지가 뷰포트에 들어올 때 로드하도록 설정
목록이 길어진 경우 뷰포트에서 사라질 경우 돔에서 랜더링하지 않도록 하는 기술로
https://ko.reactjs.org/docs/optimizing-performance.html#virtualize-long-lists 기법을 사용할 수 있다.
캐싱 문제
Scroll Optimzation
이벤트를 일정한 주기마다 발생하도록 하는 기술
연이어 호출되는 함수들 중 마지막 함수(또는 제일 처음)만 호출하도록 하는 것
3. fetch 해올 때 state가 변경된다면 컴포넌트가 전부 리렌더링 되지 않는지