-
Notifications
You must be signed in to change notification settings - Fork 1
스크롤 거꾸로 올릴 때 IntersectionObserver가 뷰포트 감지 못하는 현상
특정 컴포넌트 위치에서 헤더 스타일 다르게 적용하는 방법
위와 같이 구현했을 때 문제가 발생했다. Main 페이지 기준으로 봤을 때, LearnMore 페이지 컴포넌트에서는 헤더가 dark 모드이고 그 바로 스크롤 위에 있는 이전 컴포넌트인 Rush 컴포넌트에서는 헤더가 light 모드인데, LearnMore에서 Rush로 스크롤이 올라갔을 때 dark 모드에서 light 모드로 변경되지 않았다.
이 문제는 찾아보니 IntersectionObserver의 작동 방식 때문에 발생할 수 있는데, 실제로 IntersectionObserver는 요소가 뷰포트에 진입할 때만 콜백을 실행하고, 요소가 뷰포트를 벗어날 때는 콜백을 실행하지 않는다고 한다. 추측컨대, 스크롤을 내렸다가 다시 거꾸로 올라가면서 위에서 이미 거쳐온 뷰포트가 아직 벗어나는 중이거나 옵저버가 이미 지나온 뷰포트에 대해 인식을 제대로 하지 못하는 것 같았다.
따라서 본 문제를 해결하기 위해 다음과 같이 코드를 수정했다.
const { setActiveSection, setHeaderType } = useScrollHeaderStyleContext();
const containerRef = useRef<HTMLDivElement>(null);
// 현재 보이는 섹션들을 추적하기 위한 ref 추가
const visibleSectionsRef = useRef<Set<SectionKey>>(new Set());
// 모든 섹션을 추적하여 보이는 경우 visibleSections Set을 업데이트하는 함수
const updateVisibleSections = (
entries: IntersectionObserverEntry[],
newVisibleSections: Set<SectionKey>
) => {
// 각 엔트리를 순회하면서 화면에 보이는 경우 visibleSections Set 업데이트
entries.forEach((entry) => {
const sectionId = entry.target.id as SectionKey;
if (entry.isIntersecting) newVisibleSections.add(sectionId);
else newVisibleSections.delete(sectionId);
});
// 참조 값을 새로운 visibleSections Set으로 변경
visibleSectionsRef.current = newVisibleSections;
};
// 가장 위에 있는 섹션을 기반으로 헤더 스타일을 업데이트하는 함수
const updateHeaderStyle = (newVisibleSections: Set<SectionKey>) => {
// 보이는 섹션 중 가장 첫 번째 섹션을 가져옴
const mostTopSection = Array.from(newVisibleSections)[0];
if (mostTopSection) {
setActiveSection(mostTopSection); // activeSection 업데이트
const newHeaderType: HeaderType = config.darkSections.includes(mostTopSection)
? "dark"
: "light";
setHeaderType(newHeaderType); // headerType 업데이트
}
};
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
const newVisibleSections = new Set(visibleSectionsRef.current);
updateVisibleSections(entries, newVisibleSections);
updateHeaderStyle(newVisibleSections);
},
{
root: containerRef.current,
threshold: 0.8,
}
);
...
}, [setActiveSection, setHeaderType, config.darkSections]);
- 현재 뷰포트에 보이는 모든 섹션을 추적하는 visibleSections 상태 추가
- IntersectionObserver 콜백에서 섹션이 뷰포트에 들어오거나 나갈 때 visibleSections 업데이트
- 가장 위에 있는 보이는 섹션을 찾아 activeSection 설정, 이 섹션이 darkSections에 존재하는지에 따라 headerType 설정
이렇게 수정한 후 LearnMore 컴포넌트에서 Rush 컴포넌트로 스크롤을 거꾸로 올릴 때도 헤더 스타일이 올바르게 변경이 되는 것을 확인할 수 있다.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Set
우선 Set을 사용하면 중복된 값을 저장할 수 없기 때문에 각 섹션 별로 고유한 키 값을 저장해둘 수 있다. 그리고 자료구조 Set 자체가 추가, 삭제, 요소 확인하는 시간복잡도가 O(1)이기 때문에 배열을 사용해서 요소를 찾고 추가하고 삭제하는 과정보다 훨씬 빠르고 효율적이다. 그리고 무엇보다 ES6 이후부터는 Set는 삽입 순서를 보장해준다고 하여 가장 최근에 뷰포트에 들어온 섹션을 쉽게 찾을 수 있다.
예를 들어서 사용자가 빠르게 스크롤할 때 여러 섹션이 동시에 뷰포트에 들어오거나 나갈 수 있는데, 이때 Set을 사용하면 이러한 상황에서도 효율적으로 현재 보이는 섹션들의 목록을 유지할 수 있다.
따라서 useHeaderStyleObserver 훅의 전체 코드 구조는 다음과 같다.
import { useEffect, useRef } from "react";
import useScrollHeaderStyleContext from "@/hooks/useScrollHeaderStyleContext";
import { HeaderType } from "@/types/scrollHeaderStyle";
import { SectionKey } from "@/types/sections.ts";
interface HeaderStyleConfig {
darkSections: SectionKey[];
}
export default function useHeaderStyleObserver(config: HeaderStyleConfig) {
const { setActiveSection, setHeaderType } = useScrollHeaderStyleContext();
const containerRef = useRef<HTMLDivElement>(null);
const visibleSectionsRef = useRef<Set<SectionKey>>(new Set());
const updateVisibleSections = (
entries: IntersectionObserverEntry[],
newVisibleSections: Set<SectionKey>
) => {
entries.forEach((entry) => {
const sectionId = entry.target.id as SectionKey;
if (entry.isIntersecting) newVisibleSections.add(sectionId);
else newVisibleSections.delete(sectionId);
});
visibleSectionsRef.current = newVisibleSections;
};
const updateHeaderStyle = (newVisibleSections: Set<SectionKey>) => {
const mostTopSection = Array.from(newVisibleSections)[0];
if (mostTopSection) {
setActiveSection(mostTopSection);
const newHeaderType: HeaderType = config.darkSections.includes(mostTopSection)
? "dark"
: "light";
setHeaderType(newHeaderType);
}
};
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
const newVisibleSections = new Set(visibleSectionsRef.current);
updateVisibleSections(entries, newVisibleSections);
updateHeaderStyle(newVisibleSections);
},
{
root: containerRef.current,
threshold: 0.8,
}
);
const sections = document.querySelectorAll("section");
sections.forEach((section) => observer.observe(section));
return () => observer.disconnect();
}, [setActiveSection, setHeaderType, config.darkSections]);
return containerRef;
}
- 페이지가 렌더링 된 후 스크롤을 최상단으로 올리기
- DOM을 이미지로 저장하기
- Context API에서 불필요한 리렌더링을 줄이는 방법
- 무한 Transition 애니메이션
- Github Action 워크플로우 정의하기
- 무한 Transition 애니메이션 최적화하기
- 무한 Transition 애니메이션 최적화하기 2
- 무한 Transition 애니메이션 최적화하기 3
- fetch timeout 구현하기
- ErrorBoundary 구현하기
- 뒤로가기 confirm 로직 구현하기
- 선착순 밸런스 게임 상태 관리
- Modal 내부 컴포넌트에서 무한 스크롤이 제대로 동작 안되는 문제
- useToggleContents 훅 기본값 설정 및 조건부 사용법
- 폰트 굵기 적용 이슈
- SVG 내부 stroke 속성 값 제어를 위한 SVGIcon Util 함수 및 SVGR 사용 과정
- tailwindCSS의 @apply를 cva로 바꾸기
- 스크롤 내려갈 때 해당 섹션의 요소들 인터렉션 동작
- 공통 컴포넌트 내부에 애니메이션을 넣는 것에 대한 고민
- 특정 컴포넌트 위치에서 헤더 스타일 다르게 적용하는 방법
- 스크롤 거꾸로 올릴 때 IntersectionObserver가 뷰포트 감지 못하는 현상
- 선착순 밸런스 게임 최종 결과 계산에 대한 고민 (08.14)
- 프로그래스바 공통 컴포넌트로 분리
- (08.23 기준) 선착순 서버 시간 연동 문제
- 게임 종료된 상태에서 사용자 게임 참여 여부에 따른 FinalResult 분기 처리
- FinalResult 컴포넌트의 “당신의 선택” 카테고리 설정 이슈
- 게임 접속 시 게임 현재 진행 상태 초기화 및 카운트 다운 설정
- 선착순 밸런스 게임 UX 개선