React 마크다운 렌더러에 스마트 목차(TOC)와 스크롤 스파이(Scroll-Spy) 적용기
React 마크다운 렌더러에 스마트 목차(TOC)와 스크롤 스파이(Scroll-Spy) 적용기
기술 블로그의 특성상 하나의 아티클에 코드 블록, 아키텍처 다이어그램, 트러블슈팅 단계 등이 들어가다 보면 글의 길이가 3,000자~5,000자를 훌쩍 넘는 경우가 많습니다.
독자가 긴 기술 글을 읽을 때 가장 답답함을 느끼는 순간은 **"내가 지금 전체 내용 중 어디를 읽고 있는지, 다음 단계는 무엇인지 파악하기 어려울 때"**입니다.
이 문제를 해결하기 위해, 벨로그(Velog)나 깃허브 위키처럼 마크다운 본문의 헤딩 태그를 자동으로 분석하여 **스티키 목차(Table of Contents)**를 구성하고, 화면 스크롤에 맞춰 현재 섹션을 하이라이트해 주는 **스크롤 스파이(Scroll-Spy)**를 구현했습니다.
1. 목차(TOC) 데이터 자동 추출 알고리즘
수동으로 목차를 작성하는 것은 글을 수정할 때마다 싱크가 깨질 위험이 큽니다. 따라서 본문 마크다운 문자열에서 정규표현식을 이용해 헤딩을 실시간으로 파싱하도록 설계했습니다.
export interface TocItem {
id: string;
text: string;
level: 2 | 3;
}
export function extractToc(markdown: string): TocItem[] {
const headingRegex = /^(#{2,3})\s+(.+)$/gm;
const items: TocItem[] = [];
let match;
while ((match = headingRegex.exec(markdown)) !== null) {
const hashes = match[1];
const text = match[2].trim().replace(/[#*`_\[\]()]/g, ""); // 마크다운 서식 제거
// 한국어 및 특수문자를 안전한 URL 해시 id로 변환
const id = text
.toLowerCase()
.replace(/[^a-z0-9가-힣\s-]/g, "")
.replace(/\s+/g, "-");
items.push({
id,
text,
level: hashes.length === 2 ? 2 : 3,
});
}
return items;
}
- 정규식 매칭: 줄 시작 부분의
##(H2)와###(H3)만 목차 항목으로 추출합니다. H1은 글 제목이므로 제외합니다. - 인라인 마크다운 제거: 볼드(
**), 백틱(```) 등의 서식 기호를 정제하여 순수 텍스트만 추출합니다. - 한국어 슬러그 지원: 한글 헤딩도 브라우저 앵커 링크(
#아이디)로 자연스럽게 이동할 수 있도록 유니코드 안전 ID를 생성합니다.
2. 본문 렌더러에 앵커(Anchor) 링크 연결
추출된 ID와 실제 본문 HTML의 제목 태그가 일치해야 목차 클릭 시 해당 위치로 스크롤됩니다.
커스텀 마크다운 렌더러에서 각 헤딩 컴포넌트를 오버라이드합니다:
h2: ({ children }) => {
const text = String(children);
const id = text.toLowerCase().replace(/[^a-z0-9가-힣\s-]/g, "").replace(/\s+/g, "-");
return (
<h2 id={id} className="group relative scroll-mt-24 text-2xl font-bold mt-10 mb-4">
<a href={`#${id}`} className="absolute -left-6 opacity-0 group-hover:opacity-100 text-muted-foreground transition-opacity">
#
</a>
{children}
</h2>
);
}
scroll-mt-24: 상단 고정 헤더에 제목이 가려지지 않도록 스크롤 오프셋을 여유 있게 지정합니다.group-hover: 마우스 호버 시#앵커 아이콘이 나타나 URL 링크를 쉽게 공유할 수 있도록 배려했습니다.
3. IntersectionObserver 기반 고성능 스크롤 스파이
스크롤 위치를 추적하기 위해 전통적인 window.addEventListener('scroll')을 사용하면 렌더링 프레임이 떨어질 수 있습니다.
대신 브라우저 내장 최적화 API인 **IntersectionObserver**를 활용하여, 현재 화면 상단(20% ~ 40% 영역)에 진입한 헤딩의 ID를 상태값(activeId)으로 유지했습니다.
useEffect(() => {
if (toc.length === 0) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveId(entry.target.id);
}
});
},
{
rootMargin: "0px 0px -60% 0px", // 뷰포트 상단 40% 지점에 도달했을 때 감지
threshold: 0.1,
}
);
toc.forEach((item) => {
const el = document.getElementById(item.id);
if (el) observer.observe(el);
});
return () => observer.disconnect();
}, [toc]);
4. 반응형 레이아웃 설계 (Desktop vs Mobile)
목차 컴포넌트는 화면 너비에 따라 최적의 레이아웃을 취해야 합니다.
- 데스크톱 (xl 이상):
- 본문 우측에 260px 너비의 Sticky 사이드바로 항상 고정 노출됩니다.
- 독자가 글을 읽어 내려감에 따라 현재 읽는 목차 항목에 에메랄드 인디케이터 바와 폰트 강조가 매끄럽게 이동합니다.
- 모바일 및 태블릿:
- 화면 공간이 협소하므로 상단 본문 시작 직전에 접이식(Accordion) 목차 박스로 축약 배치합니다.
- 독자가 원할 때 터치하여 원하는 챕터로 바로 점프할 수 있습니다.
5. 적용 후 체감 효과
스마트 목차를 적용한 후 다음과 같은 실질적인 변화를 체감할 수 있었습니다:
- 글의 구조화: 글을 쓸 때도 독자의 목차 흐름을 의식하게 되어, 보다 체계적이고 기승전결이 뚜렷한 글을 작성하게 되었습니다.
- 체류 시간 및 가독성 개선: 5,000자 이상의 긴 아티클에서도 독자가 길을 잃지 않고 원하는 핵심 트러블슈팅 단락을 즉시 찾아볼 수 있게 되었습니다.
- 전문성 향상: 상용 기술 블로그 플랫폼과 견주어도 손색없는 완성도 높은 독서 환경을 제공하게 되었습니다.
클릭할 때마다 작가에게 따뜻한 응원이 전달됩니다 ❤️
함께 읽으면 좋은 다른 개발일지
'개발' 및 추천 아카이브 글 모음