React
React Floating Button 만들기 (with Intersection Observer)
Kir93
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이 넘어가면 더 이상 데이터를 가지고 오지 않게 멈춥니다.
반응형