블로그 서비스에서 페이지 오른쪽에 목차가 있는 것을 보신 적이 있나요?
클릭하면 해당 항목으로 이동하고, 스크롤하면 현재 위치에 맞춰 목차가 강조되어 편리합니다.
JavaScript를 사용해 HTML 페이지에 이런 목차를 추가하는 방법을 알아보겠습니다.
1. 동적으로 목차를 생성하고 링크 추가하기
우선, 목차를 담을 공간을 만들어 보겠습니다.
<aside>
<div class="toc"></div>
</aside>
body {
background-color: #F2F0EE;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.toc {
position: fixed;
top: 20px;
right: 20px;
width: 250px;
background-color: #f0f0f0;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
.toc ul {
list-style-type: none;
padding: 0;
}
.toc ul li {
margin-bottom: 5px;
}
.toc ul li a {
text-decoration: none;
color: #333;
display: block;
padding: 3px;
transition: background-color 0.3s;
}
.toc ul li a:hover {
background-color: #ddd;
border-radius: 3px;
}
목차는 사이드에 띄울 예정이므로, 간단히 `<aside>` 내부에 `.toc` 클래스를 가지는 태그 하나를 추가했습니다. 간단한 css도 작성해서 목차의 위치를 지정해 주었습니다.
이제 Javascript를 이용해서 목차에 사용할 헤더를 불러오겠습니다.
document.addEventListener('DOMContentLoaded', function() {
const tocContainer = document.querySelector(".toc");
const headers = document.querySelectorAll('h2, h3, h4');
const tocList = document.createElement('ul');
let headerCnt = 0; // 고유 ID를 위한 카운터
headers.forEach(header => {
// 헤더 정보를 담을 li
const tocItem = document.createElement('li');
// 헤더에 고유한 ID를 만들어 추가
const id = `header-${headerCnt++}`;
header.id = id;
// 링크를 만들어 li에 추가
const anchor = document.createElement('a');
anchor.href = `#${id}`;
anchor.textContent = header.textContent;
tocItem.appendChild(anchor);
tocList.appendChild(tocItem);
});
tocContainer.appendChild(tocList);
});
Javascript 코드는 다음과 같이 동작합니다.
1. 헤더 태그 불러오기
- `document.querySelectorAll('h2, h3, h4');` 로 헤더로 사용되는 h2, h3, h4 태그를 불러옵니다.
- (h1, h5를 사용해도 상관없습니다.)
2. 목차 리스트 생성하기
- `document.createElement('ul');` 로 목차 내용을 담을 `<ul>` 요소를 만들어 줍니다.
3. 헤더에 ID 추가하고 링크 만들기
- 불러온 헤더 (h2, h3, h4 태그)를 살펴보면서, `<li>` 요소를 만들어준 후
- 헤더의 개수를 카운트해서 `header-3` 과 같은 고유 id를 헤더에 추가해 줍니다.
- id를 이용해서 해당 헤더로 이동하는 `<a>`를 만들어 `<li>` 에 넣어줍니다.
여기까지 만들어 주면 위와 같이 간단한 목차가 만들어집니다.
2. 헤더 레벨에 맞춰 스타일링
아직 h2, h3, h4가 구분되지 않아서 불편하기 때문에, 구분이 가능하도록 수정해 보겠습니다.
headers.forEach(header => {
...
// 헤더 레벨에 맞게 클래스 추가
tocItem.classList.add(`toc-${header.tagName.toLowerCase()}`);
tocList.appendChild(tocItem);
});
앞선 코드에서 헤더 레벨에 맞게 클래스를 추가하는 코드 한 줄을 추가했습니다.
이제 h2 태그는 .toc-h2 , h3는 .toc-h3 과 같이 레벨에 맞는 클래스가 추가됩니다.
/* 헤더 레벨에 따라 들여쓰기 */
.toc-h2 {
padding-left: 0;
}
.toc-h3 {
padding-left: 20px;
}
.toc-h4 {
padding-left: 40px;
}
이제 css를 이용해서 헤더 레벨에 맞춰서 들여 쓰기 효과를 넣어주면 목차가 완성됩니다.
See the Pen TOC 1 by AA (@vchgekmq-the-flexboxer) on CodePen.
3. 스크롤 위치에 맞춰서 목차 강조하기
이제 사용자가 스크롤 한 위치에 맞춰서, 현재 읽고 있는 목차를 강조해 보도록 하겠습니다.
const updateCurrentToc = () => {
let fromTop = window.scrollY; // 현재 스크롤 위치
// headers 배열에 포함된 각 헤더 요소를 처리
headers.forEach((header) => {
// 해당 헤더 상단이 페이지 맨 위에서 얼마나 떨어져 있는지 확인
const headerOffsetTop = document.getElementById(header.id).offsetTop;
// 헤더 상단이 화면에 보이는 범위 안에 들어오면
if (headerOffsetTop <= fromTop + 100) {
// 현재 강조된 항목 제거
const currentActive = tocList.querySelector(".current-header");
if (currentActive) {
currentActive.classList.remove("current-header");
}
// 현재 스크롤 위치에 해당하는 항목 강조
const currentLink = tocList.querySelector(`[href="#${header.id}"]`);
if (currentLink) {
currentLink.classList.add("current-header");
}
}
});
};
window.addEventListener("scroll", updateCurrentToc);
`scroll` 이벤트가 발생할 때마다 호출되는 `updateCurrentToc` 함수를 만들었습니다.
함수는 다음과 같이 동작합니다.
1. 우선, 현재 스크롤 위치 `window.scrollY`를 `fromTop` 에 저장합니다.
- `window.scrollY` 는 페이지가 맨 위에서부터 얼마나 아래로 스크롤되었는지를 픽셀 단위로 측정한 값입니다.
2. 앞서 만들었던 `headers` 를 이용해서 전체 헤더 요소의 `offsetTop` 을 파악합니다.
- `offsetTop` 은 해당 요소가 페이지 맨 위에서부터 몇 px 떨어져 있는지를 나타내는 값입니다.
- `window.scrollY` 와 `offsetTop` 을 시각적으로 나타내면 아래와 같습니다.
3. 그런 다음, 현재 확인하고 있는 헤더가 화면에 보이는 범위 안에 들어오면
- (= 해당 헤더의 `offsetTop` 이 `fromTop` + 100 범위 내에 안에 들어오면)
- 해당 헤더 `<a>` 태그를 강조하기 위해 `.current-header` 를 추가해 줍니다.
- (만약 이미 강조된 요소가 있으면 제거하기 위해 `.current-header` 를 먼저 제거합니다.)
/* 현재 스크롤 위치의 헤더 링크 강조*/
.current-header {
font-weight: bold;
background-color: white;
box-shadow: 0px 10px 10px -5px rgba(0, 0, 0, 0.3);
}
`.current-header` 의 css도 작성해 주면 아래와 같이 동작합니다.
See the Pen TOC 2 by AA (@vchgekmq-the-flexboxer) on CodePen.
'WEB > 💡 Javascript' 카테고리의 다른 글
JavaScript에도 String Pool이 있을까? - V8의 String Table 알아보기 (1) | 2024.11.09 |
---|---|
[JavaScript] 비교적 정확한 타이머 만들기 (0) | 2024.08.15 |
자바스크립트가 숫자를 저장하는 방법 (IEEE 754) (0) | 2024.04.28 |
[Javascript] 마이크 변경과 볼륨 조절하기 (0) | 2024.03.23 |
[Javascript] 역따옴표(백틱)을 이용한 템플릿 리터럴(template literal) (0) | 2022.07.27 |