Nextjs에서 TOC 구현하기

힘센캥거루
2025년 1월 23일(수정됨)
4
29
Nextjs에서 TOC 구현하기-1

오늘은 남들 다 달려있는 TOC를 만들어보기로 했다.

나는 지금 마크다운으로 쓴 글을 next-mdx-remote로 컴파일해서 보여주고 있다.

rehype-slug로 이미 제목 태그마다 모두 아이디가 부여되어 있으니 이미 반은 준비되어 있다.

일단은 useEffect와 querySelectorAll로 간단하게 만들어보자.

1. 간단하게 TOC 만들어보기

Nextjs에서 TOC 구현하기-2

뭔가 처음 시도해보는 것이라 길을 찾지 못할 때는 질문하는 법을 아는 것도 능력이다.

조언할 사람이 없는 나같은 독학러들은 뤼튼, 클로드, 지피티 같은 큰 형님들에게 조언을 구하는게 가장 빠른 길.

큰 형님들에게 받은 코드를 조금 고쳐서 써보았다.

"use client";

import { useEffect, useState } from "react";

interface StringKeyValue {
  [key: string]: string;
}

export default function TOC() {
  const [headings, setHeadings] = useState<StringKeyValue[]>([]);
  const [toc, setToc] = useState(false);

  // 먼저 컴포넌트가 로드되면 1번 실행한다.
  useEffect(() => {
    // querySelectorAll로 h2, h3, h4 수준의 태그들을 찾는다.
    const hTags = document.querySelectorAll<HTMLHeadingElement>("h2, h3, h4");
    // 요소들을 배열로 만들어준다.
    const headings = [...hTags];
    if (headings.length == 0) {
      return;
    }
    setToc(true);

    // 요소에서 TOC를 만들기 위해 필요한 내용만 추출
    const allHeadings = headings.map((heading) => ({
      text: heading.innerText,
      id: heading.id || heading.innerText.replace(/\s+/g, "-").toLowerCase(),
      tagName: heading.tagName,
    }));

    setHeadings(allHeadings);
  }, []);

  return (
    toc && (
      // TOC는 게시물의 바로 옆에 오도록 calc로 위치를 계산해 주었다.
      <div className="fixed hidden xl:block top-40 w-64 p-4 left-[calc(50%+390px)]">
        <div className="text-2xl mb-4">Contents</div>
        <div className="pl-2">
          <ul className="prose">
            {headings.map((heading) => (
              // 어차피 h4 수준 까지 들어갈 일은 없어서 h3 태그 까지만 클래스를 설정했다.
              <li
                key={heading.id}
                className={`${
                  heading.tagName == "H3" && "pl-6"
                } border-l-2 border-gray-200`}
              >
                <a
                  href={`#${heading.id}`}
                  className={`block no-underline pl-2`}
                >
                  // 제목에 1. 2. 과 같은 숫자를 항상 붙여주기에 replace와
                  정규형으로 제거해 줌.
                  {heading.text.replace(/^\d+\.\s*/, "")}
                </a>
              </li>
            ))}
          </ul>
        </div>
      </div>
    )
  );
}

그리고 globals.css에서 스크롤에 smooth 옵션을 주면 이동시 스크롤이 부드러워진다.

어차피 딱딱한 스크롤은 별로 쓸데가 없으니 상남자처럼 글로벌로 집어넣었다.

@layer base {
    html {
        scroll-behavior: smooth;
    }
}

클릭 테스트를 해보았다.

잘된다.

Nextjs에서 TOC 구현하기-3

이제 intersection observer로 뷰포인트 안에 들어온 태그들을 강조해주자.

2. Intersection Observer

함수에 들어가는 파라미터들은 아래와 같다.

const options = {
  root: null, // 기본값은 뷰포트. 기준이 될 htmlelement 등록 가능.
  rootMargin: '0px', // 뷰포트 경계 설정.
  threshold: 0.5 // 50%가 뷰포트로 들어오면 콜백 호출
};

const observer = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      console.log(`${entry.target.id}가 보임.`);
    } else {
      console.log('${entry.target.id}가 보임.');
    }
  });
}, options);

// 관찰할 요소 선택
const target = document.querySelector('#target');
observer.observe(target);

useRef로 옵저버 변수를 하나 만들고 여기에 인터렉션 옵저버를 할당하자.

코드는 ssoon-m님의 velog를 참고했다.

export default function TOC() {

...
  const observer = useRef<IntersectionObserver>();
  const [activeToc, setActiveToc] = useState("");

  useEffect(() => {

    ...
    // observer.current로 인터렉션 옵저버 할당
    observer.current = new IntersectionObserver(
      (entries) => {
        // 감시하는 요소들 중 인터렉션 중인 요소의 아이디를 activeToc로 등록
        entries.forEach((entry) => {
          if (!entry.isIntersecting) return;
          setActiveToc(entry.target.id);
        });
      },
      {
        // 최상단부터 최하단에서 -70%인 지점 사이를 감시
        rootMargin: "0px 0px -70% 0px",
        threshold: 0.8,
      }
    );

    ...
    const allHeadings = headings.map((heading) => {
        //인터렉션옵저버에서 요소를 감시함.
      observer.current?.observe(heading);
    ...
    });

    ...
    //return에 익명함수를 호출하면서 cleanup 함수를 부여함.
    return () => observer.current?.disconnect();
  }, []);

  return (
    toc && (
        ...
        // 대충 현재 제목 강조하는 클래스 부여
            {headings.map((heading) => (
              <li
                key={heading.id}
                className={`${
                  heading.tagName === "H3" && "pl-6"
                } ${activeToc == heading.id ? "border-l-4 border-gray-500":"border-l-2 border-gray-200"} `}
              >
                <a
                  href={`#${heading.id}`}
                  className={`block no-underline pl-2 ${
                    activeToc == heading.id ? "text-black font-black" : "text-gray-500"
                  }`}
                >
        ...
  );
}

이렇게 한 뒤 슬라이드를 하며 TOC를 확인해보았다.

클릭이나 스크롤에 따라 잘 반응한다.

Nextjs에서 TOC 구현하기-4

3. 후기

SSR로 TOC를 만들어보고 싶기도 했으나, 반응형이기 때문에 클라이언트 측에서 생성하는게 정신 건강에 좋다는 것을 깨달았다.

그리고 다른 블로그에서 TOC가 달려 있는 것을 보고, 나도 언젠가 한번 구현해봐야지 하고 생각만 하고 있었는데 이렇게 만들게 되어 기쁘다.

다음에는 포스트들을 모두 데이터베이스에 집어넣는 작업을 해봐야겠다.

관련 글

Next.js 풀스택 블로그 개발기
Next.js 풀스택 블로그 개발기
웹개발을 처음 접한지 1년정도 되었을 때, 나만의 블로그를 갖고싶다는 생각을 하게 되었다.그래서 6개월 정도 여기에만 매달려서 만들어보게 되었다.프론트 앤드에서의 기능은 아래 김도형님의 블로그를 참고하는 것으로 충분할 듯하다.나도 mdx를 이용해 블로그를 만드는데는 채...
Caddy를 이용한 Nextjs 무중단 배포(로컬서버)
Caddy를 이용한 Nextjs 무중단 배포(로컬서버)
홈페이지에 뭔가 자꾸 얹고 싶은 욕심이 들 때 마다 빌드를 했더니, 그 사이에 가끔 접속하는 사람이 종종 있긴 한것 같다.그러다 보니 서치콘솔에서 점수가 점점 하락하는 현상이 발생했다.이대로는 안될 것 같아 무중단 배포를 하는 방법을 생각해 보게 되었다.1. 대표적인 ...
구글 검색 색인 자동화 - Web Search Indexing API
구글 검색 색인 자동화 - Web Search Indexing API
지난번 IndexNow에 이어, 구글도 자동화를 해보기로 했다.찾아보니 구글은 API로 Web Search Indexing이라는 걸 지원하고 있었다.1. 허용범위공식적으로 해당 API가 지원하는 범위는 채용공고와 스트리밍 영상 서비스이다.실시간이 중요한 내용에 대해 색...
검색 색인 생성 자동화 - IndexNow
검색 색인 생성 자동화 - IndexNow
Bing에 검색등록을 하다가 알게 되었는데, Bing에서는 IndexNow라는 기능을 제공한다.핵심은 API 키를 이용해서 글을 쓰자마자 바로 색인 요청을 날릴 수 있다는 것.아래와 같은 요청을 fetch로 만들고, 글쓰기에 연동해 놓으면 글을 DB에 저장함과 동시에 ...
Nextjs, React 서버 해킹당한 경험 - React2Shell
Nextjs, React 서버 해킹당한 경험 - React2Shell
맨 처음 보안 이슈를 접했던건 12월 5일 새벽이었다.리액트에서 인증 없이 원격 코드 실행이 가능하다는 것.해당 뉴스를 접하고 다른 사람에게 알렸지만, 나는 괜찮으려니 싶어 아무 생각없이 넘겼다.1. 해킹 흔적 발견그런데 블로그 코드를 업데이트 하려고 접속했더니 터미널...
블로그에 다국어 기능 추가하기(NextJS, next-intl, Vercel AI SDK)
블로그에 다국어 기능 추가하기(NextJS, next-intl, Vercel AI SDK)
최근 블로그에 다국어 기능이 필요하다는 생각이 들었다.그래서 next-intl을 이용해 다국어 서비스를 구현해보기로 했다.1.i18n먼저 다국어 서비스를 할 때는 지켜야 할 원칙들이 있다.이걸 internationalization이라고 하는데, 무척 길기에 첫글자 i와...

댓글을 불러오는 중...