계획

차례창에 다음과 같은 기능을 구현하고자 합니다.

  1. 포스트의 제목과 소주제들이 나열된다.
  2. 소주제를 누르면 소주제의 위치로 이동한다.
  3. 유저가 읽고 있는 부분을 차례창에서 굵게 표시한다.

구현

기능 구현은 javascript 를 이용하였습니다. 우선 PostContentsList 라는 클래스를 만들고 저번에 만든 contents-list div 를 querySelector 로 불러왔습니다. 저는 이 DOM 을 wrapper 라고 부르겠습니다. 이제 wrapper 에다가 차례창 기능을 구현하도록 하겠습니다.
1
2
3
4
5
6
class PostContentsList {
  constructor(){
    this.wrapper =
    document.querySelector('.contents-list');
  }
}

1. 포스트의 제목과 소주제들 나열

저의 포스트의 제목은 h3 태그를 사용하고 소주제들은 크기에 따라 h3, h4, h5 태그를 이용하여 나타냅니다. 제목과 소주제들을 나열하기 위하여 querySelectorAll 로 post 안의 모든 h3, h4, h5 DOM 을 불러온 후 wrapper.innerHTML 에 적절히 추가하는 방식으로 구현하였습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
class PostContentsList {
  this.wrapper =
  document.querySelector('.contents-list');
  this.contents =
  document.querySelectorAll('h3, h4, h5');
  for (let i = 0 ; i < this.contents.length ; i++){
    this.wrapper.innerHTML += `
    <a>
    ${this.contents[i].innerText}
    </a>
    <br>`
  }
}

2. 소주제 클릭 시 그 위치로 이동

소주제를 누르면 소주제의 위치로 이동하기 위하여 위의 for loop 에서 각각의 소주제에 id 를 주고 a 태그의 href 속성에 해당 id 위치로 이동하게 설정하였습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class PostContentsList {
  this.wrapper =
  document.querySelector('.contents-list');
  this.contents =
  document.querySelectorAll('h3, h4, h5');
  for (let i = 0 ; i < this.contents.length ; i++){
    this.contents[i].id = String(i); // id 추가
    // a 태그에 href 속성 추가
    this.wrapper.innerHTML += `
    <a href='#${i}'>
    ${this.contents[i].innerText}
    </a>
    <br>`
  }
}

3. 유저가 읽고 있는 부분을 차례창에서 굵게 표시

우선 유저가 읽고 있는 부분이 뭔지 정의하겠습니다. 저는 유저가 어떤 소주제 A 를 읽는다고 했을 때 A 소주제 제목이 viewport 최상단에 나올 때부터 다음 소주제의 제목이 viewport 최상단에 나올 때까지를 유저가 읽고 있는 부분이라고 정의하겠습니다.
이제 구현하겠습니다. 저는 window 에 scroll event 를 추가하는 방식으로 구현했습니다. scroll event 는 소주제들의 제목의 위치와 현제 viewport 의 위치를 비교하여 유저가 어떤 글을 읽고있는지 알아내고, 유저가 읽고있는 소주제 링크에 bold 클래스를 추가하는 방식으로 동작합니다. 코드는 아래와 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 새로운 변수 설정

// 소주제 링크들을 저장하는 변수
this.contentsList =
document.querySelectorAll('.contents-list a');

// 유저가 지금 보고 있는 링크를 저장하는 변수
// + 초깃값 설정
this.curContent = this.contentsList[0];
this.curContent.classList.add("bold");

// 스크롤 이벤트 리스너 추가
window.addEventListener('scroll', (event)=>{
  // 이전에 유저가 보고 있던 링크 해제
  this.curContent.classList.remove("bold");

  // 현재 viewport 위치
  let curY = window.pageYOffset;
  for (var i = 1 ; i < this.contents.length ; i++){
    // 각각의 소주제 위치와 viewport 의 위치 비교
    if (curY < this.contents[i].offsetTop){
      // 결과 반영
      this.curContent = this.contentsList[i-1];
      this.curContent.classList.add("bold");
      return
    }
  }
  // viewport 가 마지막 소주제보다 아래에 있을 때 결과 반영
  this.curContent = this.contentsList[i-1];
  this.curContent.classList.add("bold");
})

코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class PostContentsList {
  addSpace(element, times){
    for (let i = 0 ; i < times ; i++){
      element.innerHTML += "&nbsp"
    }
  }
  constructor(){
    this.wrapper =
    document.querySelector('.contents-list');

    this.contents =
    document.querySelectorAll('h3, h4, h5');

    for (let i = 0 ; i < this.contents.length ; i++){
      this.contents[i].id = String(i);
      this.wrapper.innerHTML += `
      <a href='#${i}'>
      ${this.contents[i].innerText}
      </a>
      <br>`;
    }

    this.contentsList =
    document.querySelectorAll('.contents-list a');
    this.curContent = this.contentsList[0];
    this.curContent.classList.add("bold");
    window.addEventListener('scroll', (event)=>{
      this.curContent.classList.remove("bold");
      let curY = window.pageYOffset;
      for (var i = 1 ; i < this.contents.length ; i++){
        if (curY < this.contents[i].offsetTop){
          this.curContent = this.contentsList[i-1];
          this.curContent.classList.add("bold");
          return
        }
      }
      this.curContent = this.contentsList[i-1];
      this.curContent.classList.add("bold");
    })
  }
}