1. 목차 - PC

Tistory Blog처럼 부드러운 목차를 구현하고 싶었습니다.
1. 요구 사항
- 스크롤을 이동할 때 스크롤 위치에 따라 현재 활성화된 목차가 변한다.
- 스크롤이 지나가며 지나가는 목차가 잠깐잠깐 활성화되어 자연스러움을 준다.
- 목차에서 해당하는 제목을 누르면 해당 제목으로 이동한다.
- 해당 제목이 가장 상단으로 위치한다.
- 스크롤을 이동할 때 처럼 지나가는 목차가 활성화된다.
2. Intersection-Observer
현재 화면에서 보이는 돔을 바탕으로 목차 활성화 상태가 바뀌므로 intersection-observer을 이용하면 제격이라 판단하였습니다. Heropy님의 Blog 글을 읽어 그림으로 쉽게 설명되어 있어 이해하는 데 큰 도움이 되었습니다.
우선, 제 블로그는 h1태그를 대주제의 제목, h2태그를 소주제의 제목으로 설정하였습니다. 그래서, document.querySelectorAll(’h1, h2’)를 이용하여 현재 페이지의 모든 h1와 h2태그를 찾습니다. 그리고 observer.observer에 등록을 하여 IntersectionObserver가 관찰할 수 있도록 합니다.
useEffect(()=>{
const observer = new IntersectionObserver(
// observer에 대한 설정
)
const headingElementList = Array.from(document.querySelectorAll('h1, h2'));
headingElementList.map(header => {
observer.observe(header)
}
},[])
observer에 관찰하고 싶은(목차에 들어갈) 각 요소들을 등록을 하였다면 위 주석 부분에 observer에 대한 설정을 해주어야 합니다. 이 부분에서 저희가 할 일은 다음과 같습니다.
- 스크롤을 지나갈 때, 가장 상단에 있는 요소가 활성화 된다.
- 스크롤을 내릴 때 : 아직 사라지지 않은 가장 위에 있는 heading이 활성화된다.
entry.isIntersecting === false
- 스크롤을 내릴 때 : 아직 사라지지 않은 가장 위에 있는 heading이 활성화된다.
- 스크롤을 올릴 때 : 올리면서 가장 먼저 보이는 heading이 활성화된다.
entry.isIntersecting === true
3. scroll 방향 판단하는 함수
let direction: 'up' | 'down';
let prevYPosition = 0;
const checkScrollDirection = (prevY:number) => {
if(window.scrollY === 0 && prevY === 0) return;
else if(window.scrollY>prevY) direction = 'down';
else direction = 'up';
prevYPosition = window.scrollY;
}
prevYPosition(이전 scroll의 y값)을 넘겨서, 현재 window.scrollY(scroll의 y값)이 이전 prevYPosition보다 크다면 스크롤 방향이 내리는 중, 아니라면 올리는 중이라고 판단합니다.
4. scroll 방향에 따라 목차 활성화
관찰 대상이 루트 요소와 교차 상태로 들어가거나(true) 교차 상태에서 나가는 지(false)여부를 판단하는 isIntersecting 프토퍼티를 이용하였습니다.
entries.forEach(entry => {
if (direction === 'down' && !entry.isIntersecting) setState(entry.target.innerHTML);
if (direction === 'up' && entry.isIntersecting) setState(entry.target.innerHTML)
})
이렇게 구현할 경우, 스크롤을 올릴 때는 정상 동작했지만, 내릴 때는 치명적인 단점이 있었습니다. 목차를 누르면 el.scrollIntoView({ behavior: 'smooth' });를 이용하여 스크롤을 이동하였습니다. 이 명령어는 해당 요소가 가장 위에 오도록 스크롤을 이동시켜줍니다. 그런데, Observer에서 스크롤을 내릴 때는 해당 요소가 사라질 때 활성화되도록 구현하였습니다. 그래서 스크롤은 el.scrollIntoView 명령어에 따라 잘 이동은 하였지만, 활성화된 목차는 그 이전 목차가 되어버렸습니다. 이 부분은 아직 해결하지 못한 채로 남겨두었습니다.
2. 목차 - Mobile
