TOC는 Table Of Contents의 약자로 한국어로는 목차를 뜻한다.
대부분의 블로그에는 이 TOC를 이용해 긴 포스트에서 현재 스크롤 중인 위치의 (소)제목을 표시해주고, 제목을 누르면 해당 위치로 스크롤이 이동하는 기능을 제공한다.
블로그를 만들며 TOC를 구현해 보았는데, heading에 따라 들여쓰기가 적용되는 TOC를 제작한 경험을 설명하고자 한다.
일반적으로 TOC에서 heading에 따라 들여쓰기를 적용할 때 h1은 margin: 0rem, h2는 margin: 1.25rem... 같이 heading 태그에 따라 margin을 나누어주지만 나는 내 위에 상위 heading이 있는지에 따라 margin을 주도록 구현했다.
게시글에 따라 heading을 높은 것만 주고싶을때도 있고 하니...
우선 현재 페이지 내의 Heading들을 추출해주어야 한다.
document.quertSelectorAll
을 이용해 페이지 내의 h1, h2, h3, h4 태그들을 추출하고 엘리먼트에 포함된 내용들을 불러와준다.
불러온 엘리먼트들은 HTMLHeadingElement 타입으로 id, innerText, tagName 등의 속성을 가지고 있다.
불러온 Heading 태그들의 리스트를 순회하며 본인과 같거나 큰 태그가 스택의 맨 위에 있을 시 pop하고 본인보다 작다면 스택의 맨 위에 있는 아이템의 들여쓰기 레벨 + 1과 함께 스택에 넣도록 구현했다.
useEffect(() => {
if (typeof document === "undefined") return;
const headingStack: { id: string, text: string, tag: string, level: number }[] = [];
const headings = Array.from(document.querySelectorAll<HTMLHeadingElement>("h1, h2, h3, h4")).map((heading) => {
const id = heading.id;
const text = heading.innerText;
const tag = heading.tagName;
let level: number;
if (headingStack.length === 0) {
headingStack.push({ id, text, tag, level: 0 });
level = 0;
} else {
let last = headingStack[headingStack.length - 1];
while (headingStack.length > 0) {
last = headingStack[headingStack.length - 1];
if (parseInt(tag[1]) <= parseInt(last.tag[1])) headingStack.pop();
else break;
}
if (headingStack.length == 0) {
headingStack.push({ id, text, tag, level: 0 });
level = 0;
}
else {
headingStack.push({ id, text, tag, level: last.level + 1 });
level = last.level + 1;
}
}
return { id, text, tag, level };
});
setHeadings(headings);
}, []);
최종적으로 아래와 같이 내 위에 상위 태그의 Heading이 있다면 margin을 주고, 없다면 주지 않는 TOC를 완성했다.
계산하는데 걸리는 시간은 Heading이 무작위로 10000개가 있더라도 3ms가량으로 매우 짧은 시간 내에 본인이 얼마만큼의 margin을 가져야 하는지 구할 수 있다.