PJH's Community Site - Infinite Scroll

박정호·2022년 12월 4일
0

Community Project

목록 보기
11/14
post-thumbnail

🚀 Start

메인 페이지에는 수많은 게시글이 출력될 것이다. 따라서, 화면을 내릴 수록 게시글이 계속 보일 수 있도록 무한 스크롤 기능을 적용해보자.



⚙️ useSWR Infinite

SWR은 페이지 매김 및 무한 로딩과 같은 일반적인 UI 패턴을 지원하기 위해서 전용 API인 useSWRInfinite을 제공한다.

useSWRInfinite는 하나의 Hook으로 여러 요청을 트리거(어느 특정한 동작에 반응해 자동으로 필요한 동작을 실행하는 것을 뜻)할 수 있는 기능을 제공합니다.

import useSWRInfinite from 'swr/infinite'

// ...
const { data, error, isValidating, mutate, size, setSize } = useSWRInfinite(
  getKey, fetcher?, options?
)

위와 같이 useSWR이 새로운 Hook은 요청 키, 가져오기 기능 및 옵션을 반환하는 함수를 허용한다.

무한 로딩에서 한 페이지에 하나의 요청이며, 여러 페이지를 가져와서 렌더링하는 것이다.

참고하자! 👉 useSWRInfinite



✔️ client

✅ 게시글들이 무한스크롤되어 출력될 곳인 메인페이지(index.tsx)useSWRInfinite을 사용하여 api 요청을 해주자.

1️⃣ getKey: 각 페이지의 SWR 키를 얻기 위한 함수로, fetcher에 의해 혀용된 값을 반환한다. null이 반환되면 페이지의 요청은 시작되지 않는다.

2️⃣ 화면에 즉시 보이는 게시글 데이터들은 previousPageData에 저장된다.

3️⃣ 만약 previousPageDatalength가 존재하지 않는다면 이는 더 이상의 데에터가 저장될 것이 없다는 것으로 화면의 끝이 도달했다는 뜻이다.

4️⃣ 만약 그렇지 않고 스크롤할 게시글이 남았다면 api 요청을 한다.

5️⃣

  • data: 각 페이지의 가져오기 응답 값의 배열
  • erroruseSWR: useSWR의 error와 동일
  • isValidatinguseSWR: useSWR의 isValidating와 동일
  • mutate: useSWR의 바인딩된 mutate 함수와 동일하지만 데이터 배열을 조작
  • size: 가져와서 반환 할 페이지 수
  • setSize: 가져와야 하는 페이지 수 설정
  // index.tsx
  // 1️⃣ 번
  const getKey = (pageIndex: number, previousPageData: Post[]) => { // 2️⃣ 번
    if (previousPageData && !previousPageData.length) return null; // 3️⃣ 번
    return `/posts?page=${pageIndex}`; // 4️⃣  번
  };

  const { // 5️⃣ 번
    data,
    error,
    size: page,
    setSize: setPage,
    isValidating,
    mutate,
  } = useSWRInfinite<Post[]>(getKey);

게시글 나열하기(UI 작성)



✔️ server

1️⃣ 현재 페이지를 요청해온 페이지 값 또는 그렇지 않다면 첫번째 페이지인 0으로 저장.

2️⃣ 페이지당 요청해온 값만큼 또는 그렇지 않다면 8개씩 보이게 저장.

3️⃣ 게시글 데이터를 가져올건데 다음과 같은 조건으로 가져온다.

  • order: 작성일자를 기준으로 내림차순으로 가져오기
  • relations: sub(커뮤니티), votes(투표), comments(댓글) 정보도 join하여 가져오기
  • skip: 8개의 데이터를 제외한 다음의 데이터를 제공
  • take: 8개의 데이터를 가져온다.

4️⃣ 투표에 대해 항상 반영되어야하는 유저정보 저장.

const getPosts = async (req: Request, res: Response) => {
  const currentPage: number = (req.query.page || 0) as number; // 1️⃣ 번
  const perPage: number = (req.query.page || 8) as number; // 2️⃣ 번

  try {
    const posts = await Post.find({ // 3️⃣ 번
      order: { createdAt: 'DESC' },
      relations: ['sub', 'votes', 'comments'],
      skip: currentPage * perPage,
      take: perPage,
    });
    if (res.locals.user) { // 4️⃣ 번
      posts.forEach(p => p.setUserVote(res.locals.user));
    }

    return res.json(posts);
  } catch (error) {
    console.log(error);
    return res.status(500).json({ error: '문제가 발생했습니다.' });
  }
};

router.get('/', userMiddleware, getPosts);


⚙️ Intersection observer

이제는 앞서 설정한 8개의 게시글이 존재하는 화면의 끝에 도달하면, 다음 게시글이 보여지는 액션을 구현해야 한다. 이때 생각할 수 있는 것은 addEvenListner(), scroll 이벤트이다.

둘의 이벤트를 사용한다는 것은 document 스크롤 이벤트를 등록하고, 특정지점을 관찰하여 앨리먼트가 위치에 도달했을 때 실행할 콜백함수를 등록하는 것이다.

하지만, 이 동작은 단점을 갖는다.

  • scroll 이벤드는 수백번 호출될 수 있고, 동기적으로 실행하여 메인 스레드에 영향을 준다.

  • 각 엘리먼트마다 이벤트가 등록되어 있기 때문에 이벤트가 끊임없이 호출되어 리플로우 현상이 발생한다.

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

Intersection Observer API를 사용하면 위와 같은 문제를 해결할 수 있다.

Intersection Observer는 비동기적으로 실행되기 때문에 메인 스레드에 영향을 주지 않으면서 변경 사항을 관찰할 수 있다.

Intersection observer는 기본적으로 브라우저 뷰포트(Viewport)와 설정한 요소(Element)의 교차점을 관찰하며, 요소가 뷰포트에 포함되는지 포함되지 않는지, 더 쉽게는 사용자 화면에 지금 보이는 요소인지 아닌지를 구별하는 기능을 제공한다.

이 기능은 비동기적으로 실행되기 때문에, scroll 같은 이벤트 기반의 요소 관찰에서 발생하는 렌더링 성능이나 이벤트 연속 호출 같은 문제 없이 사용할 수 있다.

  • 페이지 스크롤 시 이미지를 Lazy-loading(지연 로딩)할 때
  • Infinite scrolling(무한 스크롤)을 통해 스크롤할 때 새로운 콘텐츠를 불러올 때
  • 광고의 수익을 계산하기 위해 광고의 가시성을 참고할 때
  • 사용자가 결과를 볼 것인지에 따라 애니메이션 동작 여부를 결정할 때

element가 교차(intersection)될 때 이를 관찰하여 알려주는 관찰자(observer)가 필요!

참고하자!
👉 Intersection Observer API
👉 Intersection Observer - 요소의 가시성 관찰
👉 The Intersection Observer API



기능 생성

1️⃣ 스크롤을 내려서 observedPost에 닿으면 다음 페이지 게시글들을 가져오기 위한 id state

2️⃣ Infinite Scroll 기능 구현

3️⃣ 게시글이 없으면 return하여 종료

4️⃣ 게시글이 담긴 배열안의 마지막 게시글의 id를 저장.

5️⃣ posts(게시글 배열)안에 새로운 게시글이 추가되어 마지막 게시글이 변경된다면 observedPost에 마지막 게시글 id를 저장.

6️⃣ 인자로 document.getElementById(id)를 담고 observeElement 호출

  • getElementById(): 태그에 있는 id 속성을 사용하여 해당 태그에 접근하여 하고 싶은 작업을 할 때 쓰는 함수. 해당 id가 없는 경우 null 에러가 발생

7️⃣ 브라우저 viewport와 설정한 element의 교차점을 관찰

  • new IntersectionObserver: 생성자를 호출하고 임계값이 한 방향 또는 다른 방향으로 교차할 때마다 실행되는 콜백 함수를 전달하여 교차 관찰자를 만든다.
  • entries: IntersectionObserverEntry 인스턴스의 배열

8️⃣ 교차상태 true

  • isIntersecting: 관찰 대상의 교차 상태(Boolean)
  • observer.unobserve: 대상 요소의 관찰을 중지한다.

9️⃣ observer가 실행되기 위해 대상의 가시성 비율이 얼마나 필요한지 백분율로 표시한 단일 숫자 또는 숫자 배열

  • 가시성이 50% 표시를 통과할 때만 감지하려는 경우 값 0.5를 사용할 수 있다.
  • 기본값은 0.(단 하나의 픽셀이라도 표시되는 즉시 콜백이 실행됨을 의미).
  • 1.0 값은 모든 픽셀이 표시될 때까지 임계값이 통과된 것으로 간주되지 않음을 의미.

🔟 observer.observe: 대상 요소의 관찰을 시작


// index.tsx
const [observedPost, setObserverPost] = useState(''); // 1️⃣ 번

  // 2️⃣ 번
  useEffect(() => {
    if (!posts || posts.length === 0) return; // 3️⃣ 번

    const id = posts[posts.length - 1].identifier; // 4️⃣ 번

    if (id !== observedPost) { // 5️⃣ 번
      setObserverPost(id);
      observeElement(document.getElementById(id)); // 6️⃣ 번
    }
  }, [posts]);

  const observeElement = (element: HTMLElement | null) => {
    if (!element) return;

    const observer = new IntersectionObserver( // 7️⃣ 번
      entries => {
        if (entries[0].isIntersecting === true) { // 8️⃣ 번
          console.log('Reached bottom of post');
          setPage(page + 1);
          observer.unobserve(element);
        }
      },
      { threshold: 1 } // 9️⃣ 번
    );

    observer.observe(element); // 🔟 번
  };


다음 페이지의 게시글이 잘 들어온다!

profile
기록하여 기억하고, 계획하여 실천하자. will be a FE developer (HOME버튼을 클릭하여 Notion으로 놀러오세요!)

0개의 댓글