탭바 스크롤 연동 개선: 실시간 위치 추적 및 스냅 동작

- 스크롤 양의 50%만큼 탭바가 실시간으로 이동
- currentTranslateY 변수로 위치 추적 (DOM 읽기 제거)
- 스크롤 중지 시 50% 기준으로 자동 스냅
- 점진적 opacity 변화 적용 (0.1~1)
- will-change와 cubic-bezier로 성능 최적화

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jay Sheen
2025-10-16 18:23:13 +09:00
parent f4e2fe4769
commit b48c936f12
2 changed files with 79 additions and 17 deletions

View File

@@ -15,7 +15,6 @@ export const FooterNavigation = ({
}: FooterProps) => {
const { navigate } = useNavigate();
const [isFooterOn, setIsFooterOn] = useState<boolean>(true);
const [isFooterVisible, setIsFooterVisible] = useState<boolean>(true);
const onClickToNavigate = (path?: string) => {
if(!!path){
@@ -98,21 +97,59 @@ export const FooterNavigation = ({
};
useEffect(() => {
const tabbar = document.querySelector('.bottom-tabbar') as HTMLElement;
if (!tabbar) return;
const TABBAR_HEIGHT = 70;
const SCROLL_THRESHOLD = 50;
const SNAP_THRESHOLD = 0.5; // 50% 보이면 표시, 아니면 숨김
const SCROLL_MULTIPLIER = 0.5; // 스크롤의 50%만큼 이동
let lastScrollY = 0;
let currentTranslateY = 0; // 별도로 추적
let isScrolling = false;
let scrollTimeout: NodeJS.Timeout;
let ticking = false;
const handleScroll = () => {
const currentScrollY = window.scrollY;
const scrollDelta = currentScrollY - lastScrollY;
if (!ticking) {
window.requestAnimationFrame(() => {
// 스크롤 다운 & 일정 거리 이상 스크롤된 경우 -> 숨김
if (currentScrollY > lastScrollY && currentScrollY > 50) {
setIsFooterVisible(false);
// 스크롤 시작
if (!isScrolling) {
isScrolling = true;
tabbar.classList.remove('snapping');
}
// 스크롤 업 -> 표시
else if (currentScrollY < lastScrollY) {
setIsFooterVisible(true);
// 스크롤 방향에 따라 translateY 조정
if (currentScrollY > SCROLL_THRESHOLD) {
// 스크롤 다운
if (scrollDelta > 0) {
currentTranslateY = Math.min(currentTranslateY + scrollDelta * SCROLL_MULTIPLIER, TABBAR_HEIGHT);
}
// 스크롤 업
else {
currentTranslateY = Math.max(currentTranslateY + scrollDelta * SCROLL_MULTIPLIER, 0);
}
tabbar.style.transform = `translateY(${currentTranslateY}px)`;
// opacity 계산 (0 = 완전히 보임, TABBAR_HEIGHT = 완전히 숨김)
const opacityRatio = 1 - (currentTranslateY / TABBAR_HEIGHT);
const buttons = tabbar.querySelectorAll('.tab-button') as NodeListOf<HTMLElement>;
buttons.forEach(button => {
button.style.opacity = `${Math.max(0.1, opacityRatio)}`;
});
} else {
// 페이지 상단 근처에서는 완전히 표시
currentTranslateY = 0;
tabbar.style.transform = 'translateY(0)';
const buttons = tabbar.querySelectorAll('.tab-button') as NodeListOf<HTMLElement>;
buttons.forEach(button => {
button.style.opacity = '1';
});
}
lastScrollY = currentScrollY;
@@ -121,19 +158,49 @@ export const FooterNavigation = ({
ticking = true;
}
// 스크롤 중지 감지
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
isScrolling = false;
// 현재 위치 확인 후 snap
const visibleRatio = 1 - (currentTranslateY / TABBAR_HEIGHT);
tabbar.classList.add('snapping');
if (visibleRatio >= SNAP_THRESHOLD) {
// 50% 이상 보이면 완전히 표시
currentTranslateY = 0;
tabbar.style.transform = 'translateY(0)';
const buttons = tabbar.querySelectorAll('.tab-button') as NodeListOf<HTMLElement>;
buttons.forEach(button => {
button.style.opacity = '1';
});
} else {
// 50% 미만이면 완전히 숨김
currentTranslateY = TABBAR_HEIGHT;
tabbar.style.transform = `translateY(${TABBAR_HEIGHT}px)`;
const buttons = tabbar.querySelectorAll('.tab-button') as NodeListOf<HTMLElement>;
buttons.forEach(button => {
button.style.opacity = '0.1';
});
}
}, 150);
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => {
window.removeEventListener('scroll', handleScroll);
clearTimeout(scrollTimeout);
};
}, []);
return (
<>
{ isFooterOn &&
<nav className={`bottom-tabbar ${!isFooterVisible ? 'hidden' : ''}`}>
<nav className="bottom-tabbar">
{ getFooterButtonItems() }
</nav>
}