Next.js
+tailwind css
๋ฅผ ์ด์ฉํTOC
(Table Of Contents) ๊ตฌํ ๋ฐฉ๋ฒ์ ๋ํ ์ ๋ฆฌ ํฌ์คํธ์ ๋๋ค.
Next.js
๋ฅผ ์ด์ฉํ๊ธด ํ์ง๋ง, ๊ตณ์ด Next.js
๊ฐ ์๋๋๋ผ๋ ์๊ด์์ต๋๋ค.
id
๊ฐ ์กด์ฌํ๋ค๋ฉด <a href="#id">
ํํ๋ก ๋ง๋ ์ต์ปค๋ฅผ ํด๋ฆญํ๋ฉด ํด๋น ์์ด๋๋ฅผ ๊ฐ์ง ํ๊ทธ๊ฐ ํ๋ฉด์ ๋ณด์ด๋๋ก ์คํฌ๋กค์ด ์ด๋๋ฉ๋๋ค.IntersectionObserver
๋ ์ํ๋ ํ๊ทธ๊ฐ ํ๋ฉด์ ๋ ๋๋ง๋ ๋ ํน์ ์ด๋ฒคํธ๋ฅผ ์คํํ๋๋ก ๋์์ฃผ๋ API์
๋๋ค. ( ์ฐธ๊ณ )textContent
๋ผ๊ณ ๋ช
์นญ ํ๊ฒ ์ต๋๋ค.TOC
๋ฅผ ์ํ๋ ์์ญ์ ๋ชจ๋ ํฌํจํ๋ ์ต์์ ํ๊ทธ๋ฅผ ์ฐพ๋๋ค.<h*>
ํ๊ทธ๋ค์ ๋ชจ๋ ์์๋๋ก ์ฐพ๋๋ค.<h*>
ํ๊ทธ๋ค์ ๊ฐ๊ฐ id
๋ก textContent
๋ฅผ ๋ฃ๋๋ค.<a href="#textContent">
ํํ๋ก ๋ง๋ค์ด์ ์ฐ์ธก ์๋จ์ ๋ ๋๋งํ๋ค.![](https://velog.velcdn.com/images/1-blue/post/ab3cecbd-1094-40f7-898f-02f3b43229d6/image.gif)
๋ฅผ ์ด์ฉํด์ ํน์ ๋ชฉ์ฐจ๊ฐ ํ๋ฉด์ ๋ ๋๋ง๋๋ฉด ๊ฐ์กฐ ํ์ํด์ค๋ค.import { useEffect, useState } from "react";
// ํด๋์ค๋ค์ ํ๋์ ๋ฌธ์์ด๋ก ํฉ์ณ์ฃผ๋ ํฌํผ ํจ์
import { combineClassNames } from "@src/libs/utils";
const TOC = () => {
// ๋ชฉ์ฐจ ๋ฆฌ์คํธ ( index: ๋ชฉ์ฐจ, size: ๋ชฉ์ฐจ์ ํฌ๊ธฐ ( h1~h6๋ ํฌ๊ธฐ๋ฅผ ๋ค๋ฅด๊ฒ ๋ ๋๋งํด์ฃผ๊ธฐ ์ํจ ) )
const [indexList, setIndexList] = useState<{ index: string; size: number }[]>([]);
// ํ์ฌ ๋ณด์ด๋ ๋ชฉ์ฐจ ( ๊ฐ์กฐ ํ์ ํด์ฃผ๊ธฐ ์ํจ )
const [currentIndex, setCurrentIndex] = useState<string>("");
useEffect(() => {
// 1. <main> ๋ด๋ถ์์๋ง ๋ชฉ์ฐจ๋ฅผ ๋ง๋ค๊ฑฐ๋ผ์ <main> ์ ํ
// 2. <h1>, <h2>, <h3> ์ฐพ๊ธฐ ( h4~h6๋ ์๊ธฐ๋ ํ๊ณ ์์ธ๊ฑฐ๋ผ์ ์๋ต )
const hNodeList = document
.querySelector("main")
?.querySelectorAll("h1, h2, h3") as NodeListOf<Element>;
// IntersectionObserver๋ค์ด ๋ค์ด๊ฐ ๋ฐฐ์ด ( ์ด๋ฒคํธ ํด์ ๋ฅผ ์ํด )
const IOList: IntersectionObserver[] = [];
let IO: IntersectionObserver;
// ๋ง์ฝ ์ฌ๊ธฐ์ ์ค๋ฅ๊ฐ ๋๋ค๋ฉด "spread opeartor"๋ es6๋ถํฐ ์ง์๋๋ ๋ฌธ๋ฒ์ด๋ผ์ ๊ทธ ์ด์ ์ ์ฌ์ฉํ๊ธฐ ์ํด์๋ "downlevelIteration"์ ๋ํด์ ์ฐพ์๋ณด๋ฉด ๋๋ค.
[...hNodeList].forEach((node) => {
// ๋ชฉ์ฐจ ๋ด์ฉ์ด๋ ์ฌ์ด์ฆ ๊ตฌํด์ ์ ์ฅ
const index = node.textContent as string;
const size = (+node.nodeName[1] - 1) * 20;
setIndexList((prev) => {
if (prev.map((v) => v.index).includes(index)) return prev;
return [...prev, { index, size }];
});
// 3. ๊ฐ <h*>์ id๋ก ํ์ฌ ์ปจํ
์ธ ๋ด์ฉ ์ถ๊ฐ
node.id = index;
// 5. ํ๋ฉด์ ๋ณด์ด๋ฉด ๊ฐ์กฐ๋๋๋ก "IntersectionObserver" ๋ฑ๋ก
IO = new IntersectionObserver(
([
{
isIntersecting,
target: { textContent },
},
]) => {
if (!isIntersecting) return;
setCurrentIndex(textContent!);
},
{ threshold: 0.5 }
);
IO.observe(node);
// ์ด๋ฒคํธ ํด์ ๋ฅผ ์ํด ๋ฑ๋ก
IOList.push(IO);
});
// ์ด๋ฒคํธ ํด์
return () => IOList.forEach((IO) => IO.disconnect());
}, []);
return (
<aside className="fixed top-10 right-10 border-l-4 border-indigo-400 px-4 py-2 bg-white z-10">
// 4.
<ul>
{indexList.map(({ index, size }) => (
<li
key={index}
style={{
paddingLeft: size + "px",
fontSize: 17 - size / 12 + "px",
}}
className={combineClassNames(
"transition-all hover:text-blue-600",
currentIndex === index ? "text-indigo-400 scale-105" : ""
)}
>
<a href={`/#${index}`}>{index}</a>
</li>
))}
</ul>
</aside>
);
};
export default TOC;
html {
scroll-behavior: smooth;
}