애드블럭 종료 후 보실 수 있습니다.

ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • React Floating Button 만들기 (with Intersection Observer)
    React 2024. 11. 9. 21:13
    728x90
    반응형

    플로팅 버튼(Floation Button)은 사용자 경험을 높이기 위해 자주 사용되는 요소 중 하나입니다.

    Intersection Observer를 활용해 Footer 영역에서 멈추는 플로팅 버튼을 구현해 보겠습니다.

    1. 인피니티 스크롤 구현

    인피니티 스크롤은 사용자가 스크롤을 내릴 때마다 새로운 콘텐츠를 불러오는 기능입니다.

    간단한 예제로 구현해 보겠습니다.

    // App.js
    import React, { useState, useEffect } from 'react';
    
    function App() {
      const [items, setItems] = useState(Array.from({ length: 20 }, (_, i) => i));
    
      const loadMore = () => {
        setItems((prevItems) => [
          ...prevItems,
          ...Array.from({ length: 20 }, (_, i) => prevItems.length + i),
        ]);
      };
    
      useEffect(() => {
        const handleScroll = () => {
          if (
            window.innerHeight + window.scrollY >= document.body.offsetHeight - 500
          ) {
            loadMore();
          }
        };
    
        window.addEventListener('scroll', handleScroll);
        return () => window.removeEventListener('scroll', handleScroll);
      }, []);
    
      return (
        <div>
          {items.map((item) => (
            <div key={item} style={{ height: '100px', border: '1px solid #ccc' }}>
              Item {item + 1}
            </div>
          ))}
          {/* 나중에 Footer와 FloatingButton을 추가할 예정입니다 */}
        </div>
      );
    }
    
    export default App;
    • items 상태를 관리하여 리스트를 렌더링 합니다.
    • 스크롤 이벤트를 감지하여 사용자가 페이지 하단에 가까워지면 loadMore 함수를 호출합니다.

    2. 플로팅 버튼 만들기

    화면 오른쪽 하단에 위치하는 플로팅 버튼을 생성합니다.

    // FloatingButton.js
    import React from 'react';
    import './FloatingButton.css';
    
    function FloatingButton() {
      return <button className="floating-button" onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}>Top</button>;
    }
    
    export default FloatingButton;
    /* FloatingButton.css */
    .floating-button {
      position: fixed;
      right: 20px;
      /* 추가적인 스타일을 여기에 추가하세요 */
    }

    2.1 App.js에 플로팅 버튼을 추가합니다.

    // App.js
    import FloatingButton from './FloatingButton';
    // 기존 코드 생략
    return (
      <div>
        {/* 기존 코드 */}
        <FloatingButton />
      </div>
    );

    3. Intersection Observer로 버튼 제어하기

    이제 버튼이 Footer 영역에 도달하면 멈추도록 구현하겠습니다.

    3.1 Footer 컴포넌트 추가

    // Footer.js
    import React from 'react';
    
    function Footer(props, ref) {
      return (
        <div ref={ref} style={{ height: '200px', backgroundColor: '#f1f1f1' }}>
          Footer
        </div>
      );
    }
    
    export default React.forwardRef(Footer);

    3.2 App.js에서 Footer를 추가하고 ref를 전달합니다.

    // App.js
    import Footer from './Footer';
    // 기존 코드 생략
    import { useRef } from 'react';
    
    function App() {
      // 기존 코드
      const footerRef = useRef(null);
    
      return (
        <div>
          {/* 기존 코드 */}
          <Footer ref={footerRef} />
          <FloatingButton footerRef={footerRef} />
        </div>
      );
    }

    3.3 FloatingButton 수정

    Intersection Observer를 사용하여 Footer가 보이는지 감지하고, 버튼의 위치를 변경합니다.

    // FloatingButton.js
    import React, { useEffect, useState } from 'react';
    import './FloatingButton.css';
    
    function FloatingButton({ footerRef }) {
      const [isFooterVisible, setIsFooterVisible] = useState(false);
    
      useEffect(() => {
        const observer = new IntersectionObserver(
          ([entry]) => {
            setIsFooterVisible(entry.isIntersecting);
          },
          {
            root: null,
            threshold: 0,
          }
        );
    
        if (footerRef.current) {
          observer.observe(footerRef.current);
        }
    
        return () => {
          if (footerRef.current) {
            observer.unobserve(footerRef.current);
          }
        };
      }, [footerRef]);
    
      return (
        <button
          className="floating-button"
          style={{
            position: isFooterVisible ? 'absolute' : 'fixed',
            bottom: isFooterVisible ? '200px' : '20px',
          }}
        >
          Top
        </button>
      );
    }
    
    export default FloatingButton;
    • isFooterVisible 상태로 Footer의 가시성을 관리합니다.
    • Footer가 보이면 버튼의 position을 absolute로 변경하고, 그렇지 않으면 fixed로 설정합니다.

    마지막으로 조금 더 최적화하는 과정을 끝으로 글을 마무리하겠습니다.

    처음 인피니티 스크롤을 구현할 때 스크롤 이벤트를 사용하여 글을 더 불러오는 방식으로 구현했지만, 플로팅 버튼을 구현할 때와 동일하게 Intersection Observer를 활용해 최적화를 진행해 보겠습니다.

    // App.js
    import React, { useState, useEffect, useRef } from 'react';
    import Footer from './Footer';
    import FloatingButton from './FloatingButton';
    
    function App() {
      const [items, setItems] = useState(
        Array.from({ length: 20 }, (_, i) => i)
      );
    
      const loadMore = () => {
        setItems((prevItems) => [
          ...prevItems,
          ...Array.from({ length: 20 }, (_, i) => prevItems.length + i),
        ]);
      };
    
      const observerRef = useRef(null);
      const sentinelRef = useRef(null);
      const footerRef = useRef(null);
    
      useEffect(() => {
        observerRef.current = new IntersectionObserver(
          (entries) => {
            if (entries[0].isIntersecting) {
              loadMore();
            }
          },
          {
            root: null,
            threshold: 0.1,
          }
        );
    
        if (sentinelRef.current) {
          observerRef.current.observe(sentinelRef.current);
        }
        
        if (items.length > 100) {
          observerRef.current.disconnect();
        }
    
        return () => {
          if (observerRef.current && sentinelRef.current) {
            observerRef.current.unobserve(sentinelRef.current);
          }
        };
      }, [sentinelRef.current]);
    
      return (
        <div>
          {items.map((item) => (
            <div key={item} style={{ height: '100px', border: '1px solid #ccc' }}>
              Item {item + 1}
            </div>
          ))}
          {/* 감시자 요소 */}
          <div ref={sentinelRef} style={{ height: '1px' }} />
          <Footer ref={footerRef} />
          <FloatingButton footerRef={footerRef} />
        </div>
      );
    }
    
    export default App;

     

    • IntersectionObserver를 생성하여 observerRef에 저장합니다.
    • sentinelRef가 뷰포트에 들어오면 entries[0].isIntersecting이 true가 되고, loadMore 함수를 호출합니다.
    • 리스트의 마지막에 위치한 <div>로, 높이를 최소화하여 스크롤에 영향이 없도록 합니다.
    • 이 요소가 뷰포트에 나타날 때마다 새로운 아이템을 로드합니다.
    • items의 길이가 100이 넘어가면 더 이상 데이터를 가지고 오지 않게 멈춥니다.
    반응형

    댓글

Designed by Tistory.