My project/Tistory

[티스토리] 자동 목차 만들기

naiLED 2024. 10. 29. 14:25

     

    자동 목차 만드는 순서

    온라인으로 학습한 공부 내용을 정리할 때 노션을 많이 이용하는 편이다. 

    그 중 단연코 가장 많이 사용하는 기능은 바로 '목차'다. 

     

    마크다운 방식으로 제목만 입력하면 목차에 알아서 정리된다. 또한, 하이퍼링크도 자동으로 생성되어 해당 항목으로 손쉽게 이동할 수 있다.

     

    이 기능을 내 블로그에도 적용하고 싶었다. 검색해보니 영어로는 TOC(Table of contents)라고 부르는 것 같았다.

    유사시 마음대로 커스터마이징 하고 싶은 욕심에 좀 고생스럽지만 직접 만들어보기로 했다. 

     

    목표를 정리해보면 다음과 같다. 

     

    1. 제목 태그(h2, h3, h4)를 인식해서 목차 자동으로 만들게 하기
    2. 목차를 클릭하면 해당 항목으로 이동하기 
    3. 디자인은 노션처럼 단순 심플하게 만들기

     

     

    1. 제목 태그를 인식해서 목차 자동으로 만들게 하기

    티스토리에 기본으로 설정되어 있는 제목1, 제목2, 제목3 태그는 각각 html에서 h2, h3, h4에 해당된 것을 알 수 있다. 따라서 해당 태그 사용 시 블로그 상단에 자동으로 목차가 형성될 수 있도록 만들고자 한다. 

     

     

    (1) HTML -> list를 담을 그릇 만들기

      <body>
        <ul id="myToc" class="toc_all"></ul>
        <h2>제목입니다</h2>
        <h3>작은제목</h3>
        <h4>더작은제목</h4>
     </body>

     

    (2) Javascript -> queryselector로 h2, h3, h4 태그 모두 불러오기 

    var tocHeaderList = document.querySelectorAll("h2, h3, h4");
    var myTocList = document.getElementById("myToc");

    또한 (1)에서 만든 list를 담을 그릇(myToc) 불러오기.

     

     

    (3) for 문과 createElement, innerText, appendChild 메소드를 활용해서 그릇(myToc)에 헤더(h2, h3, h4) 내용들 집어넣기

     

    for (let i = 0; i < tocHeaderList.length; ++i) {
      let li = document.createElement("li");
      li.innerText = tocHeaderList[i].innerText;
      myTocList.appendChild(li);
      if (tocHeaderList[i].tagName == "H2") {
        li.classList.add("toc_h2");
      } else if (tocHeaderList[i].tagName == "H3") {
        li.classList.add("toc_h3");
      } else if (tocHeaderList[i].tagName == "H4") {
        li.classList.add("toc_h4");
      }
    }

     

    여기서 queryselector로 만든 tocHeaderList는 NodeList를 만들어주는데, NodeList는 엄밀히 말하면 array는 아니지만 마치 array처럼 순서대로 꺼내서 쓸 수 있다. 따라서 createElement로 만든 요소(<li>)에 innerText 메소드로 tocHeaderList에서 순서대로 꺼낸 헤더 내용들을 그대로 집어넣고, appendChild 메소드로 이를 myTocList에 추가해준다.

     

    한편, 나중에 css로 스타일 변경하기 쉽도록 h2, h3, h4를 분류하여 각각 클래스를 덧붙여주기로 했다.

     

    2. 목차를 클릭하면 해당 항목으로 이동하기 

    이 대목에서는 많은 공부가 필요했다.

    먼저 '목차를 클릭하면' 이라는 조건은 addEventListener를 사용하면 되겠다.

    한편 '해당 항목으로 이동'은 scrollIntoView라는 메소드가 유용해보인다. 이는 스크롤을 해당 element로 이동시켜주는 기능을 가지고 있다.

     

    scrollIntoView가 특정 element로 이동하게 하려면, 다른 항목과 아예 차별점을 두기 좋게 'id'를 활용하는 편이 좋아보인다.

     

    1. 내가 목차의 3번 항목을 클릭한다
    2. 목차의 3번 항목이 내 본문의 3번째 헤더 태그(h2, h3, h4)의 id를 가져온다
    3. 해당 id로 scrollIntoView를 수행한다.

     

    이 때 '목차의 3번 항목'도 id를 활용하면 좋겠지만, 하늘 아래 두 개의 id를 둘 수는 없는 노릇이다.

    이 대목에서 chat-GPT의 도움을 받았고, 해답은 '임의의 attribute를 추가하는 것'에 있었다.

     

    즉, 'data-id'라는 임의의 attribute를 만들어서 활용하면 해결되는 문제였다.

     

    위의 알고리즘을 수행하기 위해 scrollIntoView의 목적지가 되는 헤더 태그는 실제 id인 편이 좋겠지만(getElementById를 쓰기 위함) 목차의 경우 그저 몇 번째 항목인지만 '참조'하면 되기 때문에 진짜 id일 필요는 없기 때문이다.

     

    (1) setAttribute 메소드를 활용하여 목차(myTocList)와 본문의 헤더 리스트(tocHeaderList)에 '순서'를 알려주는 정보를 집어넣는다.

    Array.from(myTocList.children).forEach((element, index) => {
      element.setAttribute("data-id", `header-${index + 1}`);
    });
    
    tocHeaderList.forEach((header, index) => {
      header.setAttribute("id", `header-${index + 1}`);
    });

     

    이때 ` (백틱) 기호로 자바스크립트의 템플릿 리터럴(Template Literal)기능을 사용할 수 있으며, 백틱 안에서 ${ } 로 변수나 표현식을 집어넣을 수 있다는 것을 새로 배웠다.

     

     

    (2) 추가된 순서 정보(data-id, id)를 활용하여 목차의 해당 항목(data-id)을 클릭하면 본문의 해당 헤더(id)로 연결시켜주는 if문을 작성한다. 이 때 addEventListener, scrollIntoView 가 사용된다.

    myTocList.addEventListener("click", function (event) {
      if (event.target.tagName === "LI") {
        let jump = event.target.getAttribute("data-id");
        let targetElement = document.getElementById(jump);
        if (targetElement) {
          targetElement.scrollIntoView({
            behavior: "smooth",
          });
        }
      }
    });

     

    여기서 tagName을 "li" 가 아니라 "LI" 라고 대문자로 쓰는 이유는, HTML DOM tree에서는 반환된 태그 이름이 항상 대문자로 표시되기 때문이다. 한편 XML DOM에서는 태그의 대소문자가 유지되는 차이점이 있다.

     

     

    * DOM(Document Object Model) 은 HTML이나 XML의 문서 구조를 트리 구조로 표현한 모델이며, 웹 페이지의 콘텐츠를 프로그램 코드(특히 자바 스크립트)로 쉽게 다룰 수 있게 만든 일종의 인터페이스. 웹 브라우저가 HTML 문서를 로드하면, 그 문서를 하나의 트리 구조로 변환해 메모리에 저장한다. 이 구조를 DOM트리라고 한다.

     

     

     

    3. 디자인은 노션처럼 단순 심플하게 만들기

    @import url('https://fonts.googleapis.com/css2?family=Nanum+Gothic+Coding:wght@400;700&display=swap');
    
    toc_all {
            background-color:#e9ecef;
            font-size: 25px;
            font-family: "Nanum Gothic Coding", monospace;
            font-weight: 700;
            font-style: normal;
            list-style-type: none !important;
            cursor: pointer;
            padding: 80px 0 !important;
    }
    
    .toc_h2 {
            padding: 5px 0 5px 10px;
    }
    
    .toc_h3 {
            text-indent: 4mm;
            padding: 5px 0 5px 10px;
    }
    .toc_h4 {
            text-indent: 8mm;
            padding: 5px 0 5px 10px;
    }

     

    list-style-type은 나중에 티스토리에 적용하는 과정에서 설정하게 되었고, 뒤에서 설명하겠다.

    한편 티스토리에서는 ul 태그의 padding이나 margin을 설정할 때 반드시 !important를 꼭 붙여주어야만 적용되는 것을 확인했다.

     

    이래저래 허접하지만 내 손으로 직접 만든 TOC가 탄생했다.

     

     

    티스토리에 적용해보기

    이걸 티스토리에 적용해보니 다음과 같은 문제점이 발생했다.

     

    F12로 웹페이지를 뜯어보니, h2 h3 h4 태그들이 본문 외에도 다수 포진해 있어 오류가 발생하는 것이 확인되었다.

    그래서 본문 영역에서만 참조할 수 있도록 코드를 수정했다.

     

    var tocArticleView = document.getElementsByClassName(
      "tt_article_useless_p_margin"
    )[0];
    var tocHeaderList = tocArticleView.querySelectorAll("H2, H3, H4");
    var myTocList = document.getElementById("myToc");

     

    본문 영역의 클래스 이름이 tt_article_useless_p_margin이었으며, getElementsByClassName 메서드는 HTML collection을 반환하는데, 이것은 배열과 유사하지만 사실 배열이 아니어서 querySelectorAll 같은 메서드를 직접적으로 사용할 수 없다. 여러 컬렉션 중에서 콕 집어서 특정 컬렉션 하나를 선택해야지만 비로소 querySelectorAll을 사용할 수 있어, 뒤에 [0]; 이라는 단서를 붙여준 것이다.

     

    그리고 위에서 설명했듯 HTML 돔트리에서 태그는 항상 대문자로 반환되기 때문에, H2 H3 H4도 모두 대문자로 써주었다.

     

     

    한편,

      <ul id="myToc" class="toc_all"></ul>

     

    이 코드가 티스토리에서 글 저장만 하고 나면

     

    <ul id="myToc" class="toc_all" style="list-style-type: disc;" data-ke-list-type="disc"></ul>

     

    이런 쓸데없는 attribute이 자동 생성되었고, 다음 사진처럼 보기 싫은 땡땡이를 만들어내고 있었다.

    이건 쉽게 해결되지 않아서 다음과 같은 해결책을 모두 동원하였다.

     

    첫번째. css에서 list-style-type에 !important 사용하기

      list-style-type: none !important;

     

    두번째. javascript로 해당 attribute값 삭제하기

    document.getElementById("myToc").removeAttribute("data-ke-list-type");
    document.getElementById("myToc").setAttribute("style", "list-style-type: none");

     

    이렇게 해두자 아직까지는 해결이 잘 되고 있는 것 같다.

    이 문서에도 내가 완성한 TOC가 적용되어 있다.

    비록 다른 사람들이 만든 것처럼 fancy하진 않지만 뿌듯하다.