Search
Duplicate
👨🏻‍💻

[팔만코딩경 개발기] oopy서비스를 활용해 목차를 velog처럼 만들어보자

간단소개
css와 intersection observer api를 활용한 floating content table 만들기
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
Javascript
태그
Scrap
8 more properties
내용이 길어질수록 아래처럼 목차가 너무 길어지는 문제(?)가 발생했다. 처음으로 페이지가 로딩 될 때, 조금 더 유용한 내용을 유저에게 전달하고 싶어서 pc 버전에서 목차를 오른쪽 여백에 띄워주는 기능을 추가하기로 하였다.
첫 화면에서는 목차와 제목만 보이는 것을 확인할 수 있다
velog등 다른 블로그 서비스들은 목차를 오른쪽에 항상 띄워준다. 출처 : velopert velog

oopy 서비스에 추가할 코드 사전 작업하기

본격적으로 개발을 진행하기 전에, 사전 작업을 해보려고 한다. 내가 직접 개발한 코드가 아니라 oopy서비스 코드에 injection을 해야하는 상황이다. oopy에 직접 들어가 코드를 작성하면서 변경사항을 참고해도 된다.
하지만 자동완성, 반영속도 등이 만족스럽지 않아서 다른 방법을 사용하기로 했다. oopy에서 우클릭을 한 후 페이지 소스보기를 누른다. 그럼 해당 페이지에 대한 소스가 나오게 되는데 이걸 vscode, sublime text 등 본인이 개발하기 편한 곳에 복사 붙여넣기 후 작업을 진행하면 된다. 수정된 화면은 경로에 저장된 html을 직접 열어서 확인할 수 있다.

CSS 속성 생각해보기

적용하려고 하는 화면은 아래와 같다. 우선 속성을 없애고 작성자만 따로 빼오려고 한다. oopy에서 속성을 보이지 않게하는 기능이 있지만, 작성자까지 사라지는 문제가 있어서 해당 기능은 직접 제작하려고 한다.
추가로 목차를 content 옆으로 옮기고 스크롤을 하더라도 viewport에 고정될 수 있도록 작업하겠다.
목차는 감싸고 있는 element에 absolute 설정, height 100%를 주고 자식에게 sticky, top offset을 주면 될 것 같다.
작성자를 추가하는 것은 css 속성으로 할 수 있을지 조금 고민되긴 하지만 나머지 element는 display none 처리 후, 컨트리뷰터 이름만 남기면 될 것 같았다.

코드로 구현하기

css 속성 적용이 필요한 class이름을 찾아서 적절히 적용해 주었다. 목차의 max-width를 250px로 고정해주고 목차의 위치를 translate로 계산해 주었다. 그리고 media query를 활용해서 --page-max-width + 목차 witdh * 2 만큼의 너비를 기준으로 display 속성을 정의했다.
@media (min-width: 1400px) { .notion-scroller { position: static !important; } .notion-scroller-options { display: none; } .notion-frame { position: relative !important; } .notion-table_of_contents-block { position: absolute !important; max-width: 250px; height: calc(100% - 660px); top: 660px !important; transform: translateX(calc((var(--page-max-width) + 250px - 96px) / 2)); } .notion-table_of_contents-block div { background-image: none !important; white-space: normal !important; } .notion-table_of_contents-block > div { position: sticky !important; padding-left: 10px; top: 60px; border-left: 3px solid rgba(256, 0, 0, 0.1); } } @media (max-width: 1400px) { .notion-table_of_contents-block { display: none; } }
CSS
아래처럼 정상적으로 동작하는 것을 확인할 수 있다!

현재 위치의 목차에 스타일을 적용해보자

어떤 식으로 구현할지 고민하다가 목차에 들어가는 내용들이 h2, h3, h4 태그로 되어있는 것을 확인했다. querySellectorAll()을 활용해서 해당 태그에 해당하는 내용을 모두 가져오기로 하였다.
하지만 문제가 발생했다. 이렇게 하니 목차의 순서가 유지되지 않았다. 처음에 h2를 모두 가져오고 h3를 가져오다보니 h2의 자식 h3를 바로 뒤에 가져오지 않았다. querySelectorAll()에서 or condition을 사용하면 해결 할 수 있을 것 같아 사용해보았다. 이제 원하는 결과를 출력할 수 있었다.
이제 여기에 intersection observer를 추가하면 될 것 같다. callback 함수를 사용해 observer를 만들어주고 해당 observer가 감지할 element들을 observe()메서드로 추가해준다. isIntersecting 속성을 사용해서 해당 element가 viewport 안에 들어오는 경우만 판단한다. 나가는 경우는 판단하지 않는다.
let ele = document.querySelector(".notion-page-content") let headers = ele.querySelectorAll("h2, h3, h4") let content = document.querySelector(".notion-table_of_contents-block > div"); let callback = (entries, observer) => { let indexContext = 0; if (!headers && !content) return; headers.forEach((header, index) => { if (header === entries[0].target && entries[0].isIntersecting) { // 범위에 들어왔으면 목차의 색을 바꾸는 기능을 활용해 테스트를 해보자 indexContext = index } else { content.childNodes[index].style.color = ""; } }); content.childNodes[indexContext].style.color = "red"; } let observer = new IntersectionObserver(callback, { threshold: [1] // If 50% of the element is in the screen, can count it }); headers.forEach(d => { observer.observe(d); })
JavaScript

적용하기

이제 css와 상태관리를 합쳐서 실제로 서비스에 어떻게 적용되는지 확인해보자!
function addScrollObserver() { let ele = document.querySelector(".notion-page-content") let headers = ele.querySelectorAll("h2, h3, h4") let content = document.querySelector(".notion-table_of_contents-block > div"); function useIndex() { let index = 0; return function (newIndex) { if (newIndex !== undefined) index = newIndex; return index; }; } let indexContext = useIndex(); let callback = (entries, observer) => { if (!headers && !content) return; headers.forEach((header, index) => { if (header === entries[0].target && entries[0].isIntersecting) { indexContext(index); } else { content.childNodes[index].style.color = ""; } }); content.childNodes[indexContext()].style.color = "red"; } let observer = new IntersectionObserver(callback, { threshold: [1] // If 50% of the element is in the screen, can count it }); headers.forEach(d => { observer.observe(d); }) }
JavaScript

브라우저 이슈..?

사파리에서는 sticky속성이 원하는데로 작동하지 않는다.. 이유는 아직 찾지 못했다..

Reference