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

- 스크롤 양의 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

@@ -296,11 +296,11 @@ footer {
align-items: center; align-items: center;
z-index: 1000; z-index: 1000;
border-top: 0.1px solid var(--color-E5E5E5); border-top: 0.1px solid var(--color-E5E5E5);
transition: transform 0.3s ease-in-out; will-change: transform;
} }
.bottom-tabbar.hidden { .bottom-tabbar.snapping {
transform: translateY(100%); transition: transform 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
} }
.tab-button { .tab-button {
@@ -313,14 +313,9 @@ footer {
background: none; background: none;
border: none; border: none;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease, opacity 0.3s ease-in-out; transition: all 0.2s ease;
padding: 8px 4px; padding: 8px 4px;
box-sizing: border-box; box-sizing: border-box;
opacity: 1;
}
.bottom-tabbar.hidden .tab-button {
opacity: 0.1;
} }
.tab-button:hover { .tab-button:hover {

View File

@@ -15,7 +15,6 @@ export const FooterNavigation = ({
}: FooterProps) => { }: FooterProps) => {
const { navigate } = useNavigate(); const { navigate } = useNavigate();
const [isFooterOn, setIsFooterOn] = useState<boolean>(true); const [isFooterOn, setIsFooterOn] = useState<boolean>(true);
const [isFooterVisible, setIsFooterVisible] = useState<boolean>(true);
const onClickToNavigate = (path?: string) => { const onClickToNavigate = (path?: string) => {
if(!!path){ if(!!path){
@@ -98,21 +97,59 @@ export const FooterNavigation = ({
}; };
useEffect(() => { 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 lastScrollY = 0;
let currentTranslateY = 0; // 별도로 추적
let isScrolling = false;
let scrollTimeout: NodeJS.Timeout;
let ticking = false; let ticking = false;
const handleScroll = () => { const handleScroll = () => {
const currentScrollY = window.scrollY; const currentScrollY = window.scrollY;
const scrollDelta = currentScrollY - lastScrollY;
if (!ticking) { if (!ticking) {
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
// 스크롤 다운 & 일정 거리 이상 스크롤된 경우 -> 숨김 // 스크롤 시작
if (currentScrollY > lastScrollY && currentScrollY > 50) { if (!isScrolling) {
setIsFooterVisible(false); isScrolling = true;
tabbar.classList.remove('snapping');
} }
// 스크롤 업 -> 표시
else if (currentScrollY < lastScrollY) { // 스크롤 방향에 따라 translateY 조정
setIsFooterVisible(true); 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; lastScrollY = currentScrollY;
@@ -121,19 +158,49 @@ export const FooterNavigation = ({
ticking = true; 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 }); window.addEventListener('scroll', handleScroll, { passive: true });
return () => { return () => {
window.removeEventListener('scroll', handleScroll); window.removeEventListener('scroll', handleScroll);
clearTimeout(scrollTimeout);
}; };
}, []); }, []);
return ( return (
<> <>
{ isFooterOn && { isFooterOn &&
<nav className={`bottom-tabbar ${!isFooterVisible ? 'hidden' : ''}`}> <nav className="bottom-tabbar">
{ getFooterButtonItems() } { getFooterButtonItems() }
</nav> </nav>
} }