-
React Floating Button 만들기 (with Intersection Observer)React 2024. 11. 9. 21:13728x90반응형
플로팅 버튼(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이 넘어가면 더 이상 데이터를 가지고 오지 않게 멈춥니다.
반응형'React' 카테고리의 다른 글
React Portal에 대해 알아보기 (Feat. Next.js 예시) (1) 2024.11.16 Compound Component 패턴 (feat. React, Next.js) (0) 2024.09.21 react-virtuoso를 사용한 렌더링 최적화 (0) 2024.03.30 Cypress vs Playwright E2E 테스트 프레임워크 비교 (2) 2024.02.13 고급 React Hooks 사용을 위한 팁과 요령 (0) 2024.02.05