자바스크립트 Intersection Observer: 무한 스크롤 구현

콘텐츠 대표 이미지 - 자바스크립트 Intersection Observer: 무한 스크롤 구현

 

 

스크롤 이벤트의 혁명! 무한 스크롤의 비밀을 파헤쳐보자 🚀

Intersection Observer 감지 영역 Intersection Observer로 무한 스크롤 구현하기

안녕하세요, 개발자 여러분! 🙌

오늘은 정말 꿀잼 주제로 찾아왔어요! 바로 자바스크립트의 Intersection Observer API를 활용한 무한 스크롤 구현에 대해 알아볼 거예요. 인스타그램, 페이스북, 트위터처럼 스크롤 내리면 계속해서 새로운 콘텐츠가 로딩되는 그 마법 같은 기능 말이죠! ✨

혹시 여러분, 예전에 스크롤 이벤트로 무한 스크롤 구현해보신 적 있나요? 그 끔찍한 성능 이슈와 복잡한 계산... 생각만 해도 머리가 지끈지끈 아프죠? ㅋㅋㅋ 근데 걱정 마세요! Intersection Observer API를 사용하면 이런 고통에서 벗어날 수 있어요! 🎉

이 글에서는 재능넷에서 활용할 수 있는 무한 스크롤 기능을 어떻게 구현하는지 쉽고 재미있게 알아볼 거예요. 개발자든 비개발자든 누구나 이해할 수 있게 설명해드릴게요! 그럼 시작해볼까요? 🚀

Intersection Observer란 뭐길래? 🤔

일단 Intersection Observer가 뭔지부터 알아볼게요! 이름부터 좀 어려워 보이죠? ㅋㅋㅋ 근데 생각보다 개념은 간단해요!

Intersection Observer는 특정 요소가 뷰포트(사용자 화면)에 보이는지 관찰하는 API예요. 쉽게 말하면, "야! 이 요소가 화면에 보이면 나한테 알려줘!"라고 브라우저에게 부탁하는 거죠. 그럼 브라우저는 성실하게 그 요소가 화면에 들어오거나 나갈 때 우리에게 알려줍니다. 👀

기존에는 scroll 이벤트 + getBoundingClientRect() 메서드 조합으로 이런 기능을 구현했는데요, 이 방식은 스크롤할 때마다 계속 함수가 호출되어서 성능에 안 좋았어요. 특히 모바일에서는 더더욱요! 😱

하지만 Intersection Observer는 다르답니다! 이 API는 비동기적으로 작동하기 때문에 메인 스레드를 차단하지 않고, 필요할 때만 콜백 함수를 실행해요. 즉, 성능이 훨씬 좋다는 거죠! 👍

Intersection Observer의 기본 사용법 📝

자, 이제 Intersection Observer를 어떻게 사용하는지 알아볼게요! 코드를 보면서 설명할게요.

// 1. 옵저버 콜백 함수 정의
const callback = (entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      console.log('요소가 화면에 보여요! 🎉');
      // 여기서 원하는 작업을 수행해요
    }
  });
};

// 2. 옵저버 옵션 설정
const options = {
  root: null, // null이면 뷰포트가 root가 됨
  rootMargin: '0px', // root 요소의 마진
  threshold: 0.5 // 50% 이상 보일 때 콜백 실행
};

// 3. 옵저버 인스턴스 생성
const observer = new IntersectionObserver(callback, options);

// 4. 관찰할 요소 지정
const target = document.querySelector('#my-element');
observer.observe(target);
    

어때요? 생각보다 간단하죠? ㅎㅎ 이제 각 부분을 자세히 살펴볼게요! 👇

  1. callback 함수: 관찰 중인 요소가 뷰포트에 들어오거나 나갈 때 실행되는 함수예요. entries 매개변수는 IntersectionObserverEntry 객체의 배열이에요.
  2. options 객체: 옵저버의 동작을 설정하는 객체예요.
    • - root: 관찰 대상의 가시성을 확인하는 뷰포트 요소예요. null이면 브라우저 뷰포트가 됩니다.
    • - rootMargin: root 요소의 마진이에요. CSS 마진 형식으로 작성해요 (예: '10px 20px').
    • - threshold: 콜백이 실행되는 타이밍을 설정해요. 0~1 사이의 값으로, 0은 1픽셀이라도 보이면, 1은 100% 다 보일 때 콜백이 실행돼요.
  3. observer 인스턴스: IntersectionObserver 생성자로 만든 인스턴스예요.
  4. observe 메서드: 관찰할 요소를 지정하는 메서드예요.

이렇게 설정하면 #my-element가 화면에 50% 이상 보일 때 콜백 함수가 실행되는 거예요! 😎

무한 스크롤의 원리 이해하기 🧠

무한 스크롤의 기본 원리는 정말 간단해요! 사용자가 페이지 하단에 도달하면 새로운 콘텐츠를 로드하는 거죠. 이걸 Intersection Observer로 구현하려면 어떻게 해야 할까요? 🤔

감지 요소 (footer) 이 요소가 화면에 보이면 새 콘텐츠 로드! 무한 스크롤의 원리

무한 스크롤을 구현하는 핵심 아이디어는 이렇습니다:

  1. 페이지 하단에 '감지 요소'(sentinel 또는 footer)를 배치해요.
  2. 이 요소를 Intersection Observer로 관찰해요.
  3. 이 요소가 화면에 보이면(사용자가 페이지 하단에 도달했다는 의미), 새로운 콘텐츠를 로드해요.
  4. 새 콘텐츠가 추가되면 감지 요소는 자동으로 아래로 밀려나고, 과정이 반복돼요.

이 방식의 장점은 스크롤 이벤트를 사용하지 않기 때문에 성능이 훨씬 좋다는 거예요! 스크롤 이벤트는 매우 자주 발생하지만, Intersection Observer는 요소가 화면에 들어오거나 나갈 때만 콜백을 실행하니까요. 👌

무한 스크롤 구현하기: 단계별 가이드 🛠️

자, 이제 실제로 무한 스크롤을 구현해볼게요! 단계별로 따라오세요~ 🚶‍♂️

1단계: HTML 구조 만들기

먼저 기본 HTML 구조를 만들어볼게요:

<div id="content-container">
  <!-- 여기에 콘텐츠가 추가될 거예요 -->
</div>

<div id="loading-spinner" style="display: none;">
  로딩 중...
</div>

<div id="sentinel">
  <!-- 이 요소가 화면에 보이면 새 콘텐츠를 로드할 거예요 -->
</div>
    

여기서 sentinel 요소가 바로 우리가 관찰할 감지 요소예요! 이 요소가 화면에 보이면 새 콘텐츠를 로드할 거예요. 😉

2단계: CSS 스타일링 (선택사항)

간단한 스타일을 추가해볼게요:

#content-container {
  max-width: 800px;
  margin: 0 auto;
}

.content-item {
  padding: 20px;
  margin-bottom: 20px;
  background-color: #f9f9f9;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

#loading-spinner {
  text-align: center;
  padding: 20px;
}

#sentinel {
  height: 10px;
  /* 감지 요소는 보이지 않게 할 수도 있어요 */
}
    

3단계: Intersection Observer 설정하기

이제 자바스크립트로 Intersection Observer를 설정해볼게요:

// 페이지 번호 변수 (API 요청에 사용)
let page = 1;

// 데이터를 가져오는 중인지 확인하는 플래그
let isLoading = false;

// Intersection Observer 콜백 함수
const handleIntersect = (entries, observer) => {
  // entries 배열의 첫 번째 요소 가져오기
  const entry = entries[0];
  
  // 요소가 화면에 보이고, 현재 로딩 중이 아니라면
  if (entry.isIntersecting && !isLoading) {
    // 새 콘텐츠 로드 함수 호출
    loadMoreContent();
  }
};

// Observer 옵션
const options = {
  root: null, // 뷰포트를 root로 사용
  rootMargin: '0px', // 마진 없음
  threshold: 0.1 // 10% 이상 보이면 콜백 실행
};

// Observer 생성
const observer = new IntersectionObserver(handleIntersect, options);

// 감지 요소 관찰 시작
const sentinel = document.getElementById('sentinel');
observer.observe(sentinel);
    

4단계: 콘텐츠 로드 함수 구현하기

이제 새 콘텐츠를 로드하는 함수를 구현해볼게요:

// 콘텐츠 로드 함수
const loadMoreContent = () => {
  // 로딩 상태로 변경
  isLoading = true;
  
  // 로딩 스피너 표시
  const loadingSpinner = document.getElementById('loading-spinner');
  loadingSpinner.style.display = 'block';
  
  // API에서 데이터 가져오기 (예시로 setTimeout 사용)
  setTimeout(() => {
    // 실제로는 fetch API 등을 사용해 데이터를 가져올 거예요
    // fetch(`https://api.example.com/posts?page=${page}`)
    //   .then(response => response.json())
    //   .then(data => {
    //     renderContent(data);
    //     page++;
    //     isLoading = false;
    //     loadingSpinner.style.display = 'none';
    //   });
    
    // 예시 데이터로 테스트
    const mockData = Array.from({ length: 5 }, (_, i) => ({
      id: page * 5 + i,
      title: `게시물 ${page * 5 + i}`,
      body: `이것은 페이지 ${page}의 게시물 ${i + 1}입니다. 무한 스크롤 테스트 중!`
    }));
    
    renderContent(mockData);
    
    // 페이지 증가 및 상태 업데이트
    page++;
    isLoading = false;
    loadingSpinner.style.display = 'none';
  }, 1000); // 1초 지연 (API 호출 시뮬레이션)
};
    

5단계: 콘텐츠 렌더링 함수 구현하기

마지막으로, 가져온 데이터를 화면에 렌더링하는 함수를 구현해볼게요:

// 콘텐츠 렌더링 함수
const renderContent = (items) => {
  const container = document.getElementById('content-container');
  
  // 각 아이템을 HTML로 변환하여 컨테이너에 추가
  items.forEach(item => {
    const itemElement = document.createElement('div');
    itemElement.className = 'content-item';
    itemElement.innerHTML = `
      

${item.title}

${item.body}

`; container.appendChild(itemElement); }); };

6단계: 초기 콘텐츠 로드하기

페이지가 처음 로드될 때 초기 콘텐츠를 로드해볼게요:

// 페이지 로드 시 초기 콘텐츠 로드
document.addEventListener('DOMContentLoaded', () => {
  loadMoreContent();
});
    

짜잔! 🎉 이제 기본적인 무한 스크롤 구현이 완료됐어요! 사용자가 페이지 하단에 도달하면 자동으로 새 콘텐츠가 로드될 거예요. 어때요? 생각보다 간단하죠? ㅎㅎ

전체 코드 살펴보기 📜

이제 지금까지 작성한 코드를 모두 합쳐볼게요! 이 코드를 복사해서 바로 사용해보세요:

// HTML
/*
<div id="content-container">
  <!-- 여기에 콘텐츠가 추가될 거예요 -->
</div>

<div id="loading-spinner" style="display: none;">
  로딩 중...
</div>

<div id="sentinel">
  <!-- 이 요소가 화면에 보이면 새 콘텐츠를 로드할 거예요 -->
</div>
*/

// JavaScript
// 페이지 번호 변수 (API 요청에 사용)
let page = 1;

// 데이터를 가져오는 중인지 확인하는 플래그
let isLoading = false;

// 콘텐츠 로드 함수
const loadMoreContent = () => {
  // 로딩 상태로 변경
  isLoading = true;
  
  // 로딩 스피너 표시
  const loadingSpinner = document.getElementById('loading-spinner');
  loadingSpinner.style.display = 'block';
  
  // API에서 데이터 가져오기 (예시로 setTimeout 사용)
  setTimeout(() => {
    // 실제로는 fetch API 등을 사용해 데이터를 가져올 거예요
    // fetch(`https://api.example.com/posts?page=${page}`)
    //   .then(response => response.json())
    //   .then(data => {
    //     renderContent(data);
    //     page++;
    //     isLoading = false;
    //     loadingSpinner.style.display = 'none';
    //   });
    
    // 예시 데이터로 테스트
    const mockData = Array.from({ length: 5 }, (_, i) => ({
      id: page * 5 + i,
      title: `게시물 ${page * 5 + i}`,
      body: `이것은 페이지 ${page}의 게시물 ${i + 1}입니다. 무한 스크롤 테스트 중!`
    }));
    
    renderContent(mockData);
    
    // 페이지 증가 및 상태 업데이트
    page++;
    isLoading = false;
    loadingSpinner.style.display = 'none';
  }, 1000); // 1초 지연 (API 호출 시뮬레이션)
};

// 콘텐츠 렌더링 함수
const renderContent = (items) => {
  const container = document.getElementById('content-container');
  
  // 각 아이템을 HTML로 변환하여 컨테이너에 추가
  items.forEach(item => {
    const itemElement = document.createElement('div');
    itemElement.className = 'content-item';
    itemElement.innerHTML = `
      

${item.title}

${item.body}

`; container.appendChild(itemElement); }); }; // Intersection Observer 콜백 함수 const handleIntersect = (entries, observer) => { // entries 배열의 첫 번째 요소 가져오기 const entry = entries[0]; // 요소가 화면에 보이고, 현재 로딩 중이 아니라면 if (entry.isIntersecting && !isLoading) { // 새 콘텐츠 로드 함수 호출 loadMoreContent(); } }; // Observer 옵션 const options = { root: null, // 뷰포트를 root로 사용 rootMargin: '0px', // 마진 없음 threshold: 0.1 // 10% 이상 보이면 콜백 실행 }; // Observer 생성 const observer = new IntersectionObserver(handleIntersect, options); // 페이지 로드 시 초기 설정 document.addEventListener('DOMContentLoaded', () => { // 초기 콘텐츠 로드 loadMoreContent(); // 감지 요소 관찰 시작 const sentinel = document.getElementById('sentinel'); observer.observe(sentinel); });

이 코드를 HTML 파일에 넣고 실행해보면, 스크롤을 내릴 때마다 새로운 콘텐츠가 로드되는 무한 스크롤이 구현될 거예요! 🚀

실제 API와 연동하기 🔄

지금까지는 가짜 데이터로 테스트했지만, 실제 프로젝트에서는 API와 연동해야겠죠? 예를 들어, 재능넷에서 사용자의 재능 목록을 무한 스크롤로 보여주고 싶다면 이렇게 구현할 수 있어요:

const loadMoreContent = () => {
  isLoading = true;
  
  const loadingSpinner = document.getElementById('loading-spinner');
  loadingSpinner.style.display = 'block';
  
  // 실제 API 호출
  fetch(`https://api.jaenung.net/talents?page=${page}&limit=10`)
    .then(response => {
      if (!response.ok) {
        throw new Error('네트워크 응답이 올바르지 않습니다');
      }
      return response.json();
    })
    .then(data => {
      // 더 이상 불러올 데이터가 없으면 observer 중지
      if (data.length === 0) {
        observer.unobserve(sentinel);
        loadingSpinner.style.display = 'none';
        
        // 더 이상 데이터가 없다는 메시지 표시
        const container = document.getElementById('content-container');
        const endMessage = document.createElement('div');
        endMessage.textContent = '더 이상 표시할 재능이 없습니다';
        endMessage.style.textAlign = 'center';
        endMessage.style.padding = '20px';
        container.appendChild(endMessage);
        
        return;
      }
      
      // 데이터 렌더링
      renderContent(data);
      
      // 페이지 증가 및 상태 업데이트
      page++;
      isLoading = false;
      loadingSpinner.style.display = 'none';
    })
    .catch(error => {
      console.error('데이터를 가져오는 중 오류가 발생했습니다:', error);
      isLoading = false;
      loadingSpinner.style.display = 'none';
    });
};
    

그리고 렌더링 함수도 API 응답 형식에 맞게 수정해야 해요:

const renderContent = (talents) => {
  const container = document.getElementById('content-container');
  
  talents.forEach(talent => {
    const talentCard = document.createElement('div');
    talentCard.className = 'talent-card';
    talentCard.innerHTML = `
      
${talent.title}

${talent.title}

${talent.description}

${talent.price}원
${talent.sellerName}
`; container.appendChild(talentCard); }); };

이런 식으로 실제 API와 연동하면, 재능넷에서 사용자들이 스크롤을 내릴 때마다 새로운 재능들이 로드되는 멋진 무한 스크롤 기능을 구현할 수 있어요! 😄

무한 스크롤 고급 기능 추가하기 🔥

기본적인 무한 스크롤은 구현했지만, 실제 서비스에서는 더 많은 기능이 필요할 수 있어요. 몇 가지 고급 기능을 추가해볼게요!

1. 로딩 애니메이션 개선하기

단순한 "로딩 중..." 텍스트 대신 멋진 스피너 애니메이션을 추가해볼게요:

/* HTML */
<div id="loading-spinner" style="display: none;">
  <div class="spinner"></div>
</div>

/* CSS */
.spinner {
  width: 40px;
  height: 40px;
  margin: 20px auto;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #3498db;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
    

2. 에러 처리 개선하기

네트워크 오류나 API 오류가 발생했을 때 사용자에게 친절하게 알려주는 기능을 추가해볼게요:

/* HTML에 에러 메시지 컨테이너 추가 */
<div id="error-message" style="display: none; color: red; text-align: center; padding: 20px;"></div>

/* JavaScript에서 에러 처리 */
fetch(`https://api.example.com/posts?page=${page}`)
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP 오류! 상태: ${response.status}`);
    }
    return response.json();
  })
  .then(data => {
    // 데이터 처리
  })
  .catch(error => {
    console.error('데이터를 가져오는 중 오류가 발생했습니다:', error);
    
    // 에러 메시지 표시
    const errorMessage = document.getElementById('error-message');
    errorMessage.textContent = '콘텐츠를 불러오는 중 오류가 발생했습니다. 다시 시도해주세요.';
    errorMessage.style.display = 'block';
    
    // 재시도 버튼 추가
    const retryButton = document.createElement('button');
    retryButton.textContent = '다시 시도';
    retryButton.style.marginTop = '10px';
    retryButton.style.padding = '8px 16px';
    retryButton.addEventListener('click', () => {
      errorMessage.style.display = 'none';
      loadMoreContent();
    });
    
    errorMessage.appendChild(retryButton);
    
    isLoading = false;
    loadingSpinner.style.display = 'none';
  });
    

3. 스크롤 위치 기억하기

사용자가 페이지를 새로고침하거나 뒤로 가기를 했을 때 이전 스크롤 위치를 기억하는 기능을 추가해볼게요:

// 스크롤 위치 저장
window.addEventListener('beforeunload', () => {
  // 현재 스크롤 위치와 로드된 페이지 수를 localStorage에 저장
  localStorage.setItem('scrollPosition', window.scrollY);
  localStorage.setItem('currentPage', page);
  localStorage.setItem('loadedContent', document.getElementById('content-container').innerHTML);
});

// 페이지 로드 시 이전 상태 복원
document.addEventListener('DOMContentLoaded', () => {
  const savedScrollPosition = localStorage.getItem('scrollPosition');
  const savedPage = localStorage.getItem('currentPage');
  const savedContent = localStorage.getItem('loadedContent');
  
  if (savedContent && savedPage) {
    // 저장된 콘텐츠와 페이지 번호 복원
    document.getElementById('content-container').innerHTML = savedContent;
    page = parseInt(savedPage);
    
    // 감지 요소 관찰 시작
    const sentinel = document.getElementById('sentinel');
    observer.observe(sentinel);
    
    // 스크롤 위치 복원 (약간의 지연 필요)
    if (savedScrollPosition) {
      setTimeout(() => {
        window.scrollTo(0, parseInt(savedScrollPosition));
      }, 100);
    }
  } else {
    // 저장된 상태가 없으면 처음부터 시작
    loadMoreContent();
    
    // 감지 요소 관찰 시작
    const sentinel = document.getElementById('sentinel');
    observer.observe(sentinel);
  }
});
    

4. 필터링 및 정렬 기능 추가하기

사용자가 콘텐츠를 필터링하거나 정렬할 수 있는 기능을 추가해볼게요:

/* HTML에 필터 및 정렬 옵션 추가 */
<div id="filter-options">
  <select id="sort-by">
    <option value="latest">최신순</option>
    <option value="popular">인기순</option>
    <option value="price-low">가격 낮은순</option>
    <option value="price-high">가격 높은순</option>
  </select>
  
  <select id="category-filter">
    <option value="all">모든 카테고리</option>
    <option value="design">디자인</option>
    <option value="programming">프로그래밍</option>
    <option value="marketing">마케팅</option>
  </select>
</div>

/* JavaScript에서 필터 및 정렬 처리 */
// 필터 및 정렬 상태 변수
let sortBy = 'latest';
let categoryFilter = 'all';

// 필터 변경 이벤트 리스너
document.getElementById('sort-by').addEventListener('change', (e) => {
  sortBy = e.target.value;
  resetAndReload();
});

document.getElementById('category-filter').addEventListener('change', (e) => {
  categoryFilter = e.target.value;
  resetAndReload();
});

// 필터 변경 시 콘텐츠 리셋 및 재로드
const resetAndReload = () => {
  // 기존 콘텐츠 및 상태 초기화
  document.getElementById('content-container').innerHTML = '';
  page = 1;
  
  // 새 필터로 콘텐츠 로드
  loadMoreContent();
};

// API 호출 함수 수정
const loadMoreContent = () => {
  isLoading = true;
  
  const loadingSpinner = document.getElementById('loading-spinner');
  loadingSpinner.style.display = 'block';
  
  // 필터 및 정렬 옵션을 쿼리 파라미터에 추가
  const categoryParam = categoryFilter !== 'all' ? `&category=${categoryFilter}` : '';
  
  fetch(`https://api.jaenung.net/talents?page=${page}&limit=10&sort=${sortBy}${categoryParam}`)
    .then(response => response.json())
    .then(data => {
      renderContent(data);
      page++;
      isLoading = false;
      loadingSpinner.style.display = 'none';
    })
    .catch(error => {
      console.error('데이터를 가져오는 중 오류가 발생했습니다:', error);
      isLoading = false;
      loadingSpinner.style.display = 'none';
    });
};
    

이런 고급 기능들을 추가하면 사용자 경험이 훨씬 좋아질 거예요! 재능넷 같은 플랫폼에서는 이런 기능들이 특히 유용하겠죠? 😊

성능 최적화 팁 💪

무한 스크롤은 편리하지만, 페이지에 계속해서 요소가 추가되면 성능 문제가 발생할 수 있어요. 몇 가지 최적화 팁을 알려드릴게요!

로드된 항목 수 메모리 사용량 최적화 전 최적화 후 무한 스크롤 성능 최적화 효과

1. 가상 스크롤(Virtual Scrolling) 구현하기

가상 스크롤은 화면에 보이는 요소만 렌더링하고, 보이지 않는 요소는 DOM에서 제거하는 기법이에요. 이렇게 하면 메모리 사용량을 크게 줄일 수 있어요:

// 가상 스크롤 구현 예시
const VISIBLE_ITEMS_COUNT = 20; // 한 번에 화면에 표시할 아이템 수
let allItems = []; // 모든 아이템 데이터
let visibleStartIndex = 0; // 현재 화면에 보이는 첫 번째 아이템의 인덱스

// 스크롤 이벤트 핸들러
window.addEventListener('scroll', () => {
  const scrollTop = window.scrollY;
  const containerTop = document.getElementById('content-container').offsetTop;
  
  // 현재 스크롤 위치에 따라 보여줄 아이템의 시작 인덱스 계산
  const newStartIndex = Math.max(
    0,
    Math.floor((scrollTop - containerTop) / ITEM_HEIGHT) - 5 // 5개 아이템 미리 로드
  );
  
  // 시작 인덱스가 변경됐을 때만 다시 렌더링
  if (newStartIndex !== visibleStartIndex) {
    visibleStartIndex = newStartIndex;
    renderVisibleItems();
  }
});

// 화면에 보이는 아이템만 렌더링
const renderVisibleItems = () => {
  const container = document.getElementById('content-container');
  const visibleItems = allItems.slice(
    visibleStartIndex,
    visibleStartIndex + VISIBLE_ITEMS_COUNT
  );
  
  // 컨테이너 높이 설정 (전체 아이템이 있는 것처럼 보이게)
  container.style.height = `${allItems.length * ITEM_HEIGHT}px`;
  
  // 실제 아이템 요소 생성 및 배치
  container.innerHTML = '';
  visibleItems.forEach((item, index) => {
    const itemElement = document.createElement('div');
    itemElement.className = 'content-item';
    itemElement.innerHTML = `
      

${item.title}

${item.body}

`; // 아이템 위치 설정 itemElement.style.position = 'absolute'; itemElement.style.top = `${(visibleStartIndex + index) * ITEM_HEIGHT}px`; itemElement.style.width = '100%'; container.appendChild(itemElement); }); };

2. 이미지 지연 로딩(Lazy Loading) 구현하기

이미지가 많은 콘텐츠라면, 이미지도 지연 로딩을 구현하는 것이 좋아요:

// 이미지 지연 로딩 구현
const lazyLoadImages = () => {
  // 모든 이미지 요소 선택
  const lazyImages = document.querySelectorAll('img[data-src]');
  
  // 이미지 관찰을 위한 Intersection Observer 설정
  const imageObserver = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        
        // data-src 속성의 URL을 src 속성으로 이동
        img.src = img.dataset.src;
        
        // 이미지 로드 완료 후 data-src 속성 제거
        img.onload = () => {
          img.removeAttribute('data-src');
        };
        
        // 이미지 관찰 중지
        observer.unobserve(img);
      }
    });
  });
  
  // 모든 지연 로딩 이미지 관찰 시작
  lazyImages.forEach(img => {
    imageObserver.observe(img);
  });
};

// 콘텐츠 렌더링 함수 수정
const renderContent = (items) => {
  const container = document.getElementById('content-container');
  
  items.forEach(item => {
    const itemElement = document.createElement('div');
    itemElement.className = 'content-item';
    itemElement.innerHTML = `
      

${item.title}

${item.body}

${item.title} `; container.appendChild(itemElement); }); // 이미지 지연 로딩 시작 lazyLoadImages(); };

3. 디바운싱(Debouncing) 적용하기

스크롤 이벤트는 매우 자주 발생하므로, 디바운싱을 적용하면 성능을 향상시킬 수 있어요:

// 디바운스 함수
const debounce = (func, delay) => {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
};

// 디바운스된 스크롤 핸들러
const handleScroll = debounce(() => {
  // 스크롤 처리 로직
  console.log('스크롤 이벤트 처리 중...');
}, 100); // 100ms 디바운스

// 스크롤 이벤트에 디바운스된 핸들러 연결
window.addEventListener('scroll', handleScroll);
    

4. 오래된 콘텐츠 제거하기

무한 스크롤에서 너무 많은 콘텐츠가 쌓이면 브라우저 성능이 저하될 수 있어요. 화면에서 멀리 벗어난 오래된 콘텐츠를 제거하는 것이 좋아요:

// 오래된 콘텐츠 제거 함수
const removeOldContent = () => {
  const container = document.getElementById('content-container');
  const items = container.querySelectorAll('.content-item');
  
  // 아이템이 특정 개수(예: 100개) 이상이면 가장 오래된 아이템 일부 제거
  if (items.length > 100) {
    // 가장 오래된 20개 아이템 제거
    for (let i = 0; i < 20; i++) {
      if (items[i]) {
        items[i].remove();
      }
    }
    
    console.log('오래된 콘텐츠 20개를 제거했습니다.');
  }
};

// 새 콘텐츠를 로드한 후 오래된 콘텐츠 제거
const loadMoreContent = () => {
  // ... 기존 코드 ...
  
  fetch(`https://api.example.com/posts?page=${page}`)
    .then(response => response.json())
    .then(data => {
      renderContent(data);
      page++;
      isLoading = false;
      loadingSpinner.style.display = 'none';
      
      // 오래된 콘텐츠 제거
      removeOldContent();
    })
    .catch(error => {
      console.error('데이터를 가져오는 중 오류가 발생했습니다:', error);
      isLoading = false;
      loadingSpinner.style.display = 'none';
    });
};
    

이런 최적화 기법들을 적용하면 무한 스크롤이 있는 웹 페이지의 성능을 크게 향상시킬 수 있어요! 특히 모바일 기기에서는 이런 최적화가 더욱 중요하답니다. 🚀

브라우저 호환성 고려하기 🌐

Intersection Observer API는 비교적 최신 기술이기 때문에, 모든 브라우저에서 지원되지 않을 수 있어요. 브라우저 호환성을 고려한 코드를 작성해볼게요:

// Intersection Observer 폴리필 로드
if (!('IntersectionObserver' in window)) {
  // 폴리필 스크립트 로드
  const script = document.createElement('script');
  script.src = 'https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver';
  document.head.appendChild(script);
  
  console.log('Intersection Observer 폴리필을 로드했습니다.');
}

// 폴리필 로드 후 또는 네이티브 지원 시 초기화
const initInfiniteScroll = () => {
  // Intersection Observer 설정 및 무한 스크롤 초기화
  const observer = new IntersectionObserver(handleIntersect, options);
  const sentinel = document.getElementById('sentinel');
  observer.observe(sentinel);
  
  // 초기 콘텐츠 로드
  loadMoreContent();
};

// 문서 로드 완료 시 초기화
document.addEventListener('DOMContentLoaded', () => {
  if ('IntersectionObserver' in window) {
    // 네이티브 지원 시 바로 초기화
    initInfiniteScroll();
  } else {
    // 폴리필 로드 후 초기화
    const checkIntersectionObserver = setInterval(() => {
      if ('IntersectionObserver' in window) {
        clearInterval(checkIntersectionObserver);
        initInfiniteScroll();
      }
    }, 100);
    
    // 10초 후에도 로드되지 않으면 폴백 방식 사용
    setTimeout(() => {
      if (!('IntersectionObserver' in window)) {
        clearInterval(checkIntersectionObserver);
        initFallbackInfiniteScroll();
      }
    }, 10000);
  }
});

// 폴백 방식: 스크롤 이벤트 사용
const initFallbackInfiniteScroll = () => {
  console.log('폴백 방식으로 무한 스크롤을 초기화합니다.');
  
  // 초기 콘텐츠 로드
  loadMoreContent();
  
  // 스크롤 이벤트 리스너 추가
  window.addEventListener('scroll', debounce(() => {
    const sentinel = document.getElementById('sentinel');
    const rect = sentinel.getBoundingClientRect();
    
    // 감지 요소가 화면에 보이는지 확인
    if (rect.top < window.innerHeight && !isLoading) {
      loadMoreContent();
    }
  }, 200));
};
    

이렇게 하면 Intersection Observer를 지원하지 않는 브라우저에서도 무한 스크롤이 작동해요! 물론 성능은 조금 떨어질 수 있지만, 기능은 정상적으로 작동할 거예요. 😊

실제 사례: 재능넷에 무한 스크롤 적용하기 🎯

지금까지 배운 내용을 바탕으로, 재능넷 같은 플랫폼에 무한 스크롤을 어떻게 적용할 수 있는지 구체적인 예시를 살펴볼게요!

재능넷은 다양한 재능을 거래하는 플랫폼이니, 재능 목록 페이지에 무한 스크롤을 적용하면 사용자 경험이 크게 향상될 거예요. 아래는 재능넷의 재능 목록 페이지에 무한 스크롤을 적용하는 예시 코드예요: