React에서 무한 스크롤 구현하기

Jun Young Kim·2024년 9월 19일
0

TIL

목록 보기
63/65
post-thumbnail

현대 웹 애플리케이션은 사용자 경험(UX)을 향상시키기 위해 다양한 기술과 패턴을 활용합니다. 그 중 하나가 바로 무한 스크롤(Infinite Scroll)입니다. 무한 스크롤은 사용자가 페이지를 아래로 스크롤할 때 자동으로 새로운 콘텐츠를 로드하여 페이지 전환 없이도 끊김 없는 탐색을 가능하게 합니다. Facebook, Twitter, Instagram과 같은 소셜 미디어 플랫폼에서 흔히 볼 수 있는 기능입니다.

이 블로그에서는 React를 사용하여 무한 스크롤을 구현하는 방법에 대해 상세히 알아보겠습니다. 기본적인 개념부터 실제 코드 구현, 그리고 성능 최적화와 사용자 경험 개선을 위한 팁까지 모두 다룹니다.


무한 스크롤의 필요성

장점 :

  • 사용자 경험 개선 : 추가 콘텐츠를 보기 위해 페이지를 전환할 필요가 없습니다.
  • 높은 참여도 : 끊김 없는 콘텐츠 제공으로 사용자 참여도를 높일 수 있습니다.
  • 로딩 시간 단축 : 초기 로딩 시 모든 데이터를 가져오지 않으므로 초기 로딩 시간이 단축됩니다.

단점 :

  • 페이지 하단 접근 : 페이지 하단의 푸터나 중요한 정보를 보기가 어려울 수 있습니다.
  • 브라우저 성능 문제 : 많은 데이터를 한 페이지에서 렌더링하면 메모리 사용량이 증가할 수 있습니다.

구현 방법

무한 스크롤을 구현하는 방법은 크게 두 가지로 나뉩니다.

  1. 수동 구현 : 스크롤 이벤트를 직접 처리하여 필요한 시점에 데이터를 로드합니다.
  2. 라이브러리 사용 : 기존에 검증된 라이브러리를 사용하여 빠르게 구현합니다.

각 방법을 단게별로 살펴보겠습니다.


방법 1 : 수동 구현

  1. 필요한 패키지 설치 : HTTP 요청을 위해 axios를 설치합니다
  2. API 설정 : 여기서는 JSONPlaceholder의 게시물 API를 사용하겠습니다.
  3. 기본 컴포넌트 구조 작성 :
import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [posts, setPosts] = useState([]);

  return (
    <div className="App">
      {posts.map(post => (
        <div key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.body}</p>
        </div>
      ))}
    </div>
  );
}

export default App;
  1. 데이터 로드 함수 작성 :
useEffect(() => {
  fetchPosts();
}, []);

const fetchPosts = async () => {
  const res = await axios.get('https://jsonplaceholder.typicode.com/posts?_limit=10');
  setPosts(res.data);
};
  1. 스크롤 이벤트 핸들러 추가 :
useEffect(() => {
  window.addEventListener('scroll', handleScroll);
  return () => window.removeEventListener('scroll', handleScroll);
}, []);

const handleScroll = () => {
  if (window.innerHeight + document.documentElement.scrollTop !== document.documentElement.offsetHeight) return;
  fetchMorePosts();
};
  1. 추가 데이터 로드 함수 작성 :
const [page, setPage] = useState(1);

const fetchMorePosts = async () => {
  setPage(prevPage => prevPage + 1);
  const res = await axios.get(`https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=10`);
  setPosts(prevPosts => [...prevPosts, ...res.data]);
};
  1. 로딩 상태 및 에러 처리 :
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);

const fetchMorePosts = async () => {
  setLoading(true);
  try {
    setPage(prevPage => prevPage + 1);
    const res = await axios.get(`https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=10`);
    setPosts(prevPosts => [...prevPosts, ...res.data]);
  } catch (err) {
    setError(true);
  }
  setLoading(false);
};

전체 코드

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [posts, setPosts] = useState([]);
  const [page, setPage] = useState(2);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(false);

  useEffect(() => {
    fetchPosts();
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);

  const fetchPosts = async () => {
    const res = await axios.get('https://jsonplaceholder.typicode.com/posts?_page=1&_limit=10');
    setPosts(res.data);
  };

  const fetchMorePosts = async () => {
    setLoading(true);
    try {
      const res = await axios.get(`https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=10`);
      setPosts(prevPosts => [...prevPosts, ...res.data]);
      setPage(prevPage => prevPage + 1);
    } catch (err) {
      setError(true);
    }
    setLoading(false);
  };

  const handleScroll = () => {
    if (window.innerHeight + document.documentElement.scrollTop + 1 >= document.documentElement.scrollHeight) {
      fetchMorePosts();
    }
  };

  return (
    <div className="App">
      {posts.map(post => (
        <div key={post.id} style={{ margin: '20px', borderBottom: '1px solid #ccc' }}>
          <h3>{post.title}</h3>
          <p>{post.body}</p>
        </div>
      ))}
      {loading && <h4>Loading...</h4>}
      {error && <h4>Error occurred</h4>}
    </div>
  );
}

export default App;

방법 2 : 라이브러리 사용

  1. 라이브러리 설치 : react-infinite-scroll-component 라이브러리를 설치합니다.
npm install react-infinite-scroll-component
  1. 컴포넌트 수정 : InfiniteScroll 컴포넌트를 사용하여 코드를 간소화합니다.
import InfiniteScroll from 'react-infinite-scroll-component';

function App() {
  // 이전 코드와 동일한 상태 및 함수들

  return (
    <InfiniteScroll
      dataLength={posts.length}
      next={fetchMorePosts}
      hasMore={true}
      loader={<h4>Loading...</h4>}
    >
      {posts.map(post => (
        <div key={post.id} style={{ margin: '20px', borderBottom: '1px solid #ccc' }}>
          <h3>{post.title}</h3>
          <p>{post.body}</p>
        </div>
      ))}
    </InfiniteScroll>
  );
}
  1. 주요 속성 설명 :
    • dataLength : 현재까지 로드된 아이템의 수
    • next : 다음 데이터를 로드하기 위한 함수
    • hasMore : 추가 데이터가 더 있는지 여부
    • loader : 로딩 중일 때 표시할 컴포넌트

전체 코드

import React, { useState, useEffect } from 'react';
import axios from 'axios';
import InfiniteScroll from 'react-infinite-scroll-component';

function App() {
  const [posts, setPosts] = useState([]);
  const [page, setPage] = useState(2);

  useEffect(() => {
    fetchPosts();
  }, []);

  const fetchPosts = async () => {
    const res = await axios.get('https://jsonplaceholder.typicode.com/posts?_page=1&_limit=10');
    setPosts(res.data);
  };

  const fetchMorePosts = async () => {
    const res = await axios.get(`https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=10`);
    setPosts(prevPosts => [...prevPosts, ...res.data]);
    setPage(prevPage => prevPage + 1);
  };

  return (
    <InfiniteScroll
      dataLength={posts.length}
      next={fetchMorePosts}
      hasMore={page <= 10}
      loader={<h4>Loading...</h4>}
      endMessage={<p style={{ textAlign: 'center' }}><b>모든 데이터를 로드했습니다.</b></p>}
    >
      {posts.map(post => (
        <div key={post.id} style={{ margin: '20px', borderBottom: '1px solid #ccc' }}>
          <h3>{post.title}</h3>
          <p>{post.body}</p>
        </div>
      ))}
    </InfiniteScroll>
  );
}

export default App;

성능 최적화

1. 쓰로틀링 및 디바운싱

스크롤 이벤트는 매우 빈번하게 발생하므로, 성능 저하를 방지하기 위해 쓰로틀링이나 디바운싱을 적용할 수 있습니다. Lodash의 throttle 함수를 사용하여 구현할 수 있습니다.

import _ from 'lodash';

useEffect(() => {
  const throttledHandleScroll = _.throttle(handleScroll, 1000);
  window.addEventListener('scroll', throttledHandleScroll);
  return () => window.removeEventListener('scroll', throttledHandleScroll);
}, []);

2. 가상화(Virtualization)

많은 양의 데이터를 렌더링하면 DOM에 부담이 될 수 있습니다. react-windowreact-virtualized 라이브러리를 사용하여 가상 스크롤을 구현하면 성능 향상을 기대할 수 있습니다.


사용자 경험 개선

1. 로딩 스피너 디자인 개선

단순한 텍스트보다 시각적인 로딩 스피너를 사용하여 사용자 경험을 향상시킬 수 있습니다.

2. 에러 처리 메세지

데이터 로드에 실패했을 때 사용자에게 명확한 에러 메세지를 제공하고, 재시도 옵션을 제공합니다.

3. 맨 위로 가기 버튼

페이지가 길어질 경우 맨 위로 쉽게 이동할 수 있는 버튼을 제공하는 것이 좋습니다.


결론

무한 스크롤은 현대 웹 애플리케이션에서 사용자 경험을 향상시키는 중요한 기능입니다. React에서 무한 스크롤을 구현하는 방법은 수동 구현과 라이브러리 사용으로 나뉘며, 각 방법에는 장단점이 있습니다.

  • 수동 구현: 커스터마이징이 용이하지만 구현에 시간이 걸릴 수 있습니다.
  • 라이브러리 사용: 빠르게 구현할 수 있지만 커스터마이징에 제약이 있을 수 있습니다.

프로젝트의 요구 사항과 팀의 역량에 따라 적절한 방법을 선택하시기 바랍니다. 또한 성능 최적화와 사용자 경험 개선을 위한 추가적인 고려 사항들을 염두에 두고 개발하면 더욱 완성도 높은 애플리케이션을 만들 수 있습니다.

0개의 댓글