@@ -23,6 +23,7 @@ export default function TableOfContents({ className, headings: propHeadings, onI
2323 const [ isExpanded , setIsExpanded ] = useState ( true ) ;
2424 const [ isUserScrolling , setIsUserScrolling ] = useState ( false ) ;
2525 const [ clickedId , setClickedId ] = useState < string | null > ( null ) ;
26+ const [ isLoading , setIsLoading ] = useState ( ! propHeadings ) ;
2627
2728 // 모바일에서는 항상 펼쳐진 상태 유지
2829 const isMobile = ! ! onItemClick ;
@@ -44,8 +45,13 @@ export default function TableOfContents({ className, headings: propHeadings, onI
4445 // propHeadings가 제공된 경우 DOM 추출 스킵
4546 if ( propHeadings ) {
4647 setHeadings ( propHeadings ) ;
48+ setIsLoading ( false ) ;
4749 return ;
4850 }
51+
52+ // 로딩 상태 시작
53+ setIsLoading ( true ) ;
54+
4955 // DOM이 완전히 로드되었는지 확인하는 함수
5056 const extractHeadings = ( ) => {
5157 const headingElements = document . querySelectorAll ( "h1, h2, h3, h4" ) ;
@@ -58,6 +64,9 @@ export default function TableOfContents({ className, headings: propHeadings, onI
5864 } ) ) ;
5965
6066 setHeadings ( items ) ;
67+ if ( items . length > 0 ) {
68+ setIsLoading ( false ) ;
69+ }
6170 } ;
6271
6372 // MutationObserver로 DOM 변경 감지
@@ -90,7 +99,11 @@ export default function TableOfContents({ className, headings: propHeadings, onI
9099
91100 // 추가적으로 여러 번 확인 (fallback)
92101 const timer1 = setTimeout ( extractHeadings , 200 ) ;
93- const timer2 = setTimeout ( extractHeadings , 500 ) ;
102+ const timer2 = setTimeout ( ( ) => {
103+ extractHeadings ( ) ;
104+ // 500ms 후에도 헤딩이 없으면 로딩 해제
105+ setIsLoading ( false ) ;
106+ } , 500 ) ;
94107
95108 return ( ) => {
96109 observer . disconnect ( ) ;
@@ -263,6 +276,27 @@ export default function TableOfContents({ className, headings: propHeadings, onI
263276 }
264277 } , [ handleClick ] ) ;
265278
279+ if ( isLoading ) {
280+ return (
281+ < nav className = { cn ( "toc w-full" , className ) } aria-label = "목차" >
282+ < div className = "mb-6 sm:mb-8 md:mb-10" >
283+ < h2 className = "text-base 2xl:text-lg font-semibold mb-3 sm:mb-4 md:mb-5 px-2 sm:px-3 pb-2 border-b border-border/50 flex items-center" >
284+ < span className = "w-1 h-4 bg-primary rounded-full mr-2 opacity-60" />
285+ 목차
286+ </ h2 >
287+ < div className = "px-1 sm:px-2 space-y-2" >
288+ { /* 스켈레톤 UI */ }
289+ { [ 1 , 2 , 3 ] . map ( ( i ) => (
290+ < div key = { i } className = "animate-pulse" >
291+ < div className = "h-4 bg-muted rounded w-3/4 mb-2" />
292+ </ div >
293+ ) ) }
294+ </ div >
295+ </ div >
296+ </ nav >
297+ ) ;
298+ }
299+
266300 if ( headings . length === 0 ) {
267301 return null ;
268302 }
0 commit comments