ougi FE

무한 스크롤과 가상 스크롤에 대해서 알아보자 본문

카테고리 없음

무한 스크롤과 가상 스크롤에 대해서 알아보자

ougi 2025. 11. 25. 02:26

오늘은 저희 팀 친구가 정말 가볍고 좋은 무한 스크롤 그리고 가상 스크롤 라이브러리를 내서 

저희 프로젝트에 적용 하는 겸 이렇게 무한 스크롤과 가상 스크롤에 대해서 글을 쓰게 되었습니다


무한 스크롤이란?

무한스크롤이란 말 그대로 연속적인 스크롤을 제공하는 UI/UX 요소입니다

화면에 표시된 아이템 목록의 끝까지 내려갔을 때 자동으로 다음 데이터를 요청하여 추가 아이템을 불러오는 기술입니다

페이지네이션과의 차이는 페이지네이션은 다음 페이지로 넘어갈 때 api가 호출되어 데이터를 불러오지만

무한 스크롤은 스크롤할 때마다 api가 데이터를 불러옵니다

한 번에 모든 데이터를 불러오는 것이 아니라서 큰 장점이 있습니다

적용 사례는 유튜브, 인스타그램 등 많은 소셜 미디어에 적용되어 있습니다

하지만 페이지에 과도한 요소가 추가되면 브라우저 성능이 저하되어 페이지 로딩에 실패하거나 브라우저가 다운될 수 있습니다

-> 1500개 이하 권장

장점

  • 무한스크롤은 사용자가 콘텐츠를 끊김 없이 탐색할 수 있습니다 자연스러운 사용자 경험 제공
  • 스크롤만으로 추가 콘텐츠가 로드되기 때문에 사용자가 더 많은 콘텐츠를 소비하게 유도합니다
  • 스크롤만으로 콘텐츠 탐색이 가능해 사용자에게 편리함을 제공합니다

단점

  • 지나치게 많은 스크롤을 요구할 수 있습니다
  • 사용자의 기기 성능과 네트워크 속도에 따라 느려질 수 있습니다
  • 특정 콘텐츠에 바로 접근하기 어려워지고 원하는 정보를 찾는 것에 어려움이 생깁니다

무한 스크롤 적용하는 방법

Scroll Event

사용자가 스크롤을 할 때 특정 위치에 도달하면 새로운 데이터를 자동으로 불러오는 방식입니다

스크롤 이벤트를 통해 현재 스크롤 위치를 감지하고 페이지의 끝에 가까워지면 데이터를 추가로 로드합니다

비교적 간단하게 구현 가능하지만 성능상 좋지 않을 수 있습니다

Intersection Observer API

특정 요소가 뷰포트에 나타나는 시점을 감지하고 해당 시점에 새로운 데이터를 로드하는 방식입니다

요소와 뷰포트 간의 교차를 비동기적으로 관찰할 수 있고 스크롤 성능에 미치는 영향을 줄일 수 있습니다

이 방법은 최신 브라우저에서 지원됩니다


가상 스크롤이란?

유저 입장에서 직접 보이는 요소들만 렌더링하고 보이지 않는 요소들은 렌더링하지 않는 기술입니다

아이템이 너무 많을 때 모든 요소를 렌더링하고 있으면 성능이 분명히 안좋아질 것입니다

이럴 때 필요한 것이 바로 가상 스크롤입니다

대용량 데이터를 한번에 보여주거나 무한 스크롤에서 계속 요소들이 쌓일 때, 로그 화면처럼 엄청난 스크롤이 생길 때 유용합니다

원리

  1. 높이 계산
  2. 스크롤 위치 계산 및 보여야 할 요소계산

높이 계산

기본적으로 아이템의 높이와 폭을 알고 있어야 하고 존재하는 모든 아이템의 높이와 폭을 알아야 합니다

const totalHeight = itemHeight * itemCount;

 

 

스크롤 위치를 감지해 현재 필요한 인덱스만 계산

현재 스크롤을 할 때 보여야 할 아이템을 구해야합니다

const startIndex = floor(스크롤이 내려간 거리 / itemHeight);
const endIndex = startIndex + 화면에 필요한 아이템 수;

빈 공간 주기

아이템이 전부 렌더링 되지 않기 때문에 중간에 위치해야 할 실제 자리로 배치하려면 위 아래에 빈 공간을 주어서 실제로는 다른 요소들이 

있는 것처럼 속인다

const offsetTop = 100 * 50 = 5000;
const offsetBottom = 100 * 50 = 5000;

무한 스크롤 vs 가상 스크롤

둘은 이름은 비슷하지만 완전히 다른 기능입니다 하지만 두 기능 같이 유용하게 쓰입니다 두 기능의 차이를 정리 해보았습니다

구분 무한 스크롤 가상 스크롤
목적 데이터 계속 불러오기 렌더링 최적화하기
핵심 동작 리스트 끝 도달 시 api 요청 전송 스크롤 위치 변화에 따라 렌더 구간 변경
데이터 크기 계속 증가 동일, 증가하지 않음 (단지 렌더링만 최적화)
렌더링 수 무제한 증가 항상 일정한 개수
메모리 영향 시간이 지날수록 증가 일정하게 유지

 


내가 적용한 것

저는 scrolloop라는 라이브러리를 사용하여서 가상 스크롤을 적용하였습니다

https://www.npmjs.com/package/scrolloop

이 링크를 타고 가시면 라이브러리에 대해서 더 자세히 알 수 있습니다

저는 이 라이브러리를 이용하여 간단한 구현에 성공 하였습니다 제가 구현한 코드를 보여드리겠습니다

처음에 뚝뚝 끊기거나 마지막에 부자연스럽게 아이템이 사라지는 경우가 있었는데 overscan을 올리니 아무 문제 없이 잘 작동하였습니다

import { RefreshControl } from 'react-native';
import Post from '~/shared/ui/Post';
import { useLocalSearchParams } from 'expo-router';
import { ProductType } from '~/shared/types/type';
import { ModeType } from '~/shared/types/mode';
import { useCallback, useState } from 'react';
import { useGetPosts } from '~/shared/model/useGetPosts';
import { returnValue } from '~/view/post/model/handleCategory';
import { Category } from '~/view/post/model/category';
import { VirtualList } from 'scrolloop/native';

export default function PostList({ category }: { category: Category }) {
  const { type } = useLocalSearchParams<{ type: ProductType; mode?: ModeType }>();

  const [refreshing, setRefreshing] = useState(false);
  const currentMode = category ? returnValue(category) : undefined;

  const { data = [], refetch } = useGetPosts(
    currentMode as ModeType | undefined,
    type as ProductType | undefined
  );

  const onRefresh = useCallback(async () => {
    setRefreshing(true);
    try {
      await refetch();
    } finally {
      setRefreshing(false);
    }
  }, [refetch]);

  return (
    <VirtualList
      decelerationRate={0}
      refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
      itemSize={120} // 아이템 사이즈
      overscan={12} // 보이는 아이템 갯수 말고 얼마나 더 렌더링 할 것인지
      count={data.length} // 아이템 갯수
      renderItem={(index) => {
        const item = data[index];
        if (!item) return null;

        return <Post {...item} />;
      }}
    />
  );
}

 


글을 마치며

오늘은 제가 제 프로젝트 refactor 성능 개선을 하면서 어떤 걸 도입해야 좋을지 고민하며 공부하고 제가 적용한 것을 보여드렸습니다

이걸 하면서 한 번에 두마리 토끼를 잡은 것 같아서 좋았던거 같습니다 여러분들도 더 좋은 방법을 잘 골라서 사용하시면 좋을거 같습니다

다음에는 꼭 제가 직접 한 번 구현해보는 것도 괜찮을거 같습니다 글을 읽어주셔서 감사합니다

728x90