블로그 삽질기 - 1
방학동안 학교에서 진행된 백엔드 특강
에 참여했다. 2주동안 하루에 7시간씩 아침부터 저녁까지 필요한 내용들을 직접 찾아가며 공부하고 간단한 프로젝트에 적용시켜보는 과정을 반복하며 정말 힘들었지만, 짧은 기간동안 { HTTP 요청/응답 구조
, FastAPI 프레임워크
사용법, DB 관리를 도와주는 ORM(SQLAlchemy)
, API 설계 시 고려할 사항, 주의사항
등 } 많은 개념들을 배우고 직접 사용해볼 수 있었다.
FastAPI
프레임워크에서는 기본적으로 Swagger UI
를 통한 API 테스팅
을 지원한다. 이 Swagger UI
를 통한 API 테스팅이 너무나도 간편하고 직관적이어서 사실 API 설계 및 개발 단계에서는 프론트엔드 지식에 대한 필요성
을 체감하지 못했었다.
API 구현과 테스트를 마친 뒤엔 시연을 위해 간단한 프론트엔드 페이지도 만들어야 했는데, HTML
, CSS
, JavaScript
가 뭔지, 각각 브라우저에서 어떤 일을 하는지 정도만 겨우 아는 정도였기 때문에 이걸 어디서부터 어떻게 건드려야 할 지 정말 막막했다. 생성형 AI의 도움을 받아 어찌저찌 누더기같은 페이지를 만들어내긴 했지만, 프론트엔드에 대해서도 조금 더 배워둬야겠다는 생각이 들었다.
프론트에 대해 어떻게 배울 수 있을까? 이번에 참여했던 특강을 통해 배운 내용들을 곧바로 프로젝트에 적용
시켜보는 방식의 학습이 굉장히 효과적이라는것을 느낄 수 있었기에, 프론트엔드 공부도 뭔가 프로젝트를 진행하면서 직접 부딛혀가며
배우고 싶다는 생각이 들었다. 그렇다고 처음부터 시작해야 하는 프로젝트는 너무 부담스럽고, 어느 정도 뼈대가 잡혀 있는걸 내 마음대로 이리저리 바꿔보고 싶었다.
그래서 내 블로그를 만들어보기로 했다.
플랫폼을 골라보자
블로그를 시작해봐야겠다고 마음먹기는 했는데, 블로그 플랫폼은 선택지가 너무 많다보니 어떤 플랫폼이 나랑 제일 잘 맞을지 알아보기 위해 간단하게나마 여러 플랫폼들을 사용해 봤다. 처음에 생각해봤던 플랫폼은 Velog
, Naver Blog
, Tistory
, Jekyll + GitHub Pages
네가지였는데, 결국에는 후보에 없던 Ghost + GitHub Pages
를 선택하게 되었다.
개발 및 기술 관련 블로그에서는 Velog 를 많이들 사용하던데, 개인적으로는 살짝 마음에 안 드는 부분이 있었다. 먼저 글 맨 아래에 관심 있을 만한 포스트
라며 비슷한 주제의 글을 더 보여주는 부분이 있는데, 기본적으로 다른 블로그의 글까지 표시되는데다가 이걸 아예 꺼버릴 수도 없었다. 또 페이지 레이아웃이나 HTML/CSS를 전혀 건드릴 수 없어서 누구나 똑같이 생긴 블로그에 글을 쓴다는 점도 조금 싫었다. Naver Blog는 컴포넌트 레이아웃을 수정할 수는 있지만 HTML/CSS를 전혀 건드릴 수 없고, HTML/CSS를 어느정도 수정할 수 있게 해 주는 Tistory에서도 결국 건드릴 수 없는 부분이 있었다. 나는 나한테 필요한 부분, 내가 사용하는 부분만 남겨두고 싶었다.



왼쪽에서부터 Velog, Naver Blog, Tistory
그러려면 페이지를 구성하는 코드들을 전부 내가 관리할 수 있는 방법을 찾아야겠다는 생각이 들었고, 나는 블로그로 수익을 만들 생각이 없기 때문에 무료 웹 호스팅 서비스인 GitHub Pages를 활용한 블로그에 관심이 생겼다.
GitHub Pages
가 기본적으로 Jekyll
을 사용하고 있을 뿐 아니라 한글로 된 레퍼런스 자료도 굉장히 많아서 처음에는 Jekyll을 활용한 블로그를 생각했었는데, 이미 완성되어 있는 스킨을 골라 적용하는 선에서 그치지 않고 세세한 커스터마이징을 하려면 결국 Ruby 언어를 배워야 한다는 점과 별도의 에디터를 지원하지 않는다는 점이 마음에 걸렸다.
뭔가 마음에 쏙 드는건 없는데, 그렇다고 블로그 프론트엔드와 백엔드를 처음부터 구축해보자니 이건 배보다 배꼽이 더 커지는 꼴이고.. 고민하던 찰나에 설치형 블로그 플랫폼
인 Ghost
에 대해 알게 되었다.
먼저 활발하게 개발 및 유지보수되고있는 Node.js
기반 오픈소스 프로젝트인데다 활발하게 운영되고 있는 개발자 포럼이 있어서 뭔가 불편한 부분이 생겼을 때 혼자 헤메는 시간이 적을 것 같았고, 대형 블로그 플랫폼들과 맞먹을정도로 편리한 에디터(심지어 마크다운처럼 -
, ##
, ~~
등을 사용할 수도 있다!)를 지원하며, 무엇보다도 공식 레퍼런스 문서가 이미지와 함께 정말 자세하게 작성되어 있다는 점이 마음에 들어서 이걸 한번 써보기로 했다.

공짜로는 안 될까요
Ghost
가 제공하는 두 가지 옵션인 1. Ghost Pro 구독을 통한 Cloud Hosting
과 2. Self Hosting
중 Self Hosting 방식을 사용하려면 AWS, Azure, GCP 같은 클라우드 서비스의 서버를 활용하거나 직접 홈 서버를 구성해서 Ghost 백엔드 서버를 계속 켜둬야 하는데, 세상에 공짜 컴퓨팅 자원은 없기 때문에 어떤 것을 선택하든 비용이 발생한다. 여러 명의 Author가 접속할 일이 없는 개인 블로그인데다, 유료 회원 기능과 회원 공개 기능도 사용하지 않을 것이기 때문에 백엔드 서버를 항상 실행시켜 둘 필요가 없지 않나? 하는 생각이 들었다.
작성이 끝난 포스트를 띄워주기만 하면 되는데, 어떻게 무료로 할 수 있는 방법이 없을까 싶어 이것저것 검색해보다 ghost static site generator라는걸 찾았다. 동작 방식을 간단히 설명하자면 wget을 사용해 Brute Force 방식으로 모든 페이지의 html
스냅샷을 /static
폴더에 저장해주는 것인데, 덕분에 이제 서버와의 연결 없이도 블로그 포스트들을 보여줄 수 있게 되었다.

Handlebars?

오픈 소스(+ 자유로운 MIT 라이센스!)니까 어떤 기능과 컴포넌트를 사용할 것인지 내 마음대로 설정할 수 있을 거라는 부푼 기대를 안고 테마를 다운받았는데 이게 웬걸, .hbs
라는 처음 보는 확장자의 파일들이 잔뜩 들어있었다.
앞서 언급했다시피 Jekyll
대신 Ghost
를 사용해야겠다고 마음먹게 된 이유는 Jekyll
이 Ruby
라는 언어로 작성된 프레임워크였기 때문에 블로그를 위해 새로운 언어를 배우고 싶지 않아서
였는데, 분명 Ghost
는 JavaScript
기반이라 그랬는었는데 어째서..

결국 새로운 언어를 배우는걸 피할 수는 없구나 싶어 handlebars
에 대해 찾아봤는데, Handlebars 공식 레퍼런스 페이지와 ghost 공식 유튜브 채널의 영상을 통해 핵심 개념들을 빠르게 이해할 수 있었다:
Handlebars
는HTML
을 만들기 위한 템플릿 언어 (Originally designed to generate HTML){{
로 시작해}}
로 끝나는Handlebars expression
은 execution 시점에실제 값
으로 대체됨- 주석 문장은
{{!
로 시작함 ({{! comments }}
) #if
,#each
같은 내장 helper들과#foreach
,#has
같은 Ghost 제공 helper를 사용할 수 있음{{>
문법을 통해 partials를 불러와 사용할 수 있음 - 이걸 많이 사용하게 될 것!
테마를 처음부터 설계하려는게 아니라 기존 테마를 조금 수정해서 사용할 생각이었기 때문에 배워야 할 내용이 생각보다 많지 않았다. 기본적으로 HTML
, CSS
, JavaScript
코드인데, {{
}}
형식의 Handlebars
템플릿을 통해 여러가지 편의 기능들을 제공한다. 끗.
다크모드 적용하기
나처럼 늦은 시간대에 웹서핑을 하던 중 갑자기 새하얀 페이지가 나와서 화들짝 놀랐던 경험이 있는 사람이라면 왜 아직도 다크모드
를 지원하지 않는 페이지가 이렇게 많을까? 하는 생각을 한번쯤 해 봤을 것 같다. 진짜 왜??
먼저, 놀랍게도 다크모드라는 개념 자체가 등장한지 그렇게 오래되지 않았다. 이 블로그에서 뿐만 아니라 다크모드를 지원하는 많은 웹페이지들에서 사용하고 있는 prefers-color-scheme:dark
라는 미디어 쿼리 feature는 2019년부터 도입되기 시작했다. { 모바일 환경에서는 Android 10(2019.09.03 릴리즈), iOS 13(2019.09.20 릴리즈)부터 시스템 테마 옵션을 지원하기 시작함 }
다음으로는, 프론트엔드를 설계하는 단계에서부터 다크모드를 고려하지 않았다면 추후에 다크모드를 구현하는 과정이 굉장히 번거롭다. 컴포넌트에 사용할 색상들을 모두 CSS 변수에 저장해둔 다음 color: var(—-accent-color);
처럼 var()
함수를 통해 적용시키지 않고, color: #E6E6E6;
처럼 Hexadecimal 색상값을 하드코딩해뒀다면.. 특정 컴포넌트에 의도한 색상이 적용되지 않는다거나 하는 자잘한 오류 없이 다크모드를 완벽하게 지원하는게 정말 불가능에 가까울 것 같다. 2019년 이전에 만들어져 지금까지 운영되고 있는 웹 서비스라면 다크모드따위보다 우선순위가 높은 개발 요구사항들이 많을 것이고, 다크모드를 적용한다 하더라도 기존 프론트엔드를 리팩토링하는것보다 처음부터 다시 설계하는 편이 차라리 낫기 때문에, 추후에 프론트엔드 UI를 개편할 때 다크모드도 같이 구현하는 쪽으로 이야기가 된 것 아닐까.
이 글을 작성하는 시점 에 Velog와 네이버 블로그에서는 다크모드를 지원하지 않고, 티스토리의 경우 모든 스킨을 확인해볼 순 없었지만 사람들이 많이 사용하는 스킨의 다크모드는 어딘가 엉성하다고 느껴지는 부분이 있었다:




전부 다른 블로그의 스크린샷이며, 개인정보 대체 외의 수정은 없음. 요청 시 삭제 예정
나는 어색해 보이는 부분이 없도록 내 블로그에 다크모드를 잘
적용시키고 싶었는데, 내가 베이스로 사용하려는 Edition 테마는 직접적으로 다크모드를 지원하지는 않지만 직관적인 이름의 CSS 변수들을 사용하고 있어서 다크모드를 금방 테스트해볼 수 있었다.


OS나 브라우저의 테마 설정값과 다른 값을 사용하고 싶은 사용자가 있을 수 있으니 설정을 오버라이딩(Overriding)할 수 있도록 토글 버튼을 만들어주고 싶었다. 그런데 이 버튼이 다크모드 토글 버튼이라는걸 알려주기 위한 이미지를 넣자니 다크모드에 적절한(검은 배경에 전체적인 페이지 디자인과 어울리는) 이미지를 찾을 수가 없었고, 유니코드 문자(☀️, 🌙, ...) 중에서는 마음에 드는게 없었다. 자연스러운 다크모드 토글 버튼이 있는 페이지들에서는 아이콘을 어떻게 구현했는지 궁금해 브라우저의 개발자 도구로 아이콘을 집어 보니 svg
를 사용하고 있었다.

svg는 scalable vector graphics
의 약자로, 웹 환경에서 벡터 그래픽을 지원하기 위한 포맷이다. xml
형식으로 원 그리기, 선 그리기 등의 작업들을 나열해두면 이를 통해 브라우저에서 동적으로 이미지를 생성해주는데, 결국 텍스트이기 때문에 이미지 파일보다 훨씬 작은 용량을 차지해서 로딩 시간이 단축될 뿐만 아니라 벡터 그래픽이라 아무리 확대해도 해상도 저하가 일어나지 않는다는 장점이 있다.
<svg xmlns="http://www.w3.org/2000/svg">
<circle r="45" cx="350" cy="100" stroke="currentColor">
</svg>
간단한 svg 예시
이번에도 여러 시행착오를 거쳐 해 모양과 달 모양 두가지 svg
를 다크모드 토글 버튼에 적용시켰고, 토글 여부에 따라 아이콘이 변경되는것까지 확인한 다음 아래 기능을 구현하기 위한 Javascript 코드를 작성했다.
- 사용자의 시스템 설정에 맞춰 다크모드 또는 라이트모드 페이지를 표시
- 다크모드 토글 버튼 클릭 시, 테마를 변경하고 변경된 preference를 저장
- 저장된 preference에 유효기간을 설정해, 일정 시간이 지나면 다시 시스템 설정을 확인
Ghost 개발자 포럼에서 다크모드 적용과 관련된 괜찮은 글을 찾았는데, 여기서 제공하는 다크모드 예시 코드에서는 preference를 저장하는 기능을 구현하기 위해 브라우저 쿠키
를 사용하고 있었다.
쿠키란 브라우저의 메모리 공간에 저장되는 문자열인데, 일반적으로는 세션 정보를 관리하기 위해 사용된다. javascript
에서는 document.cookie를 통해 쿠키를 다룰 수 있는데, 토글 버튼으로 작동하는 checkbox에 Event Listener를 등록해 checkbox
의 상태가 변경될 경우 쿠키
에 다크모드 preference를 저장해줌으로써 다크모드 토글 버튼을 구현했다.
블로그에 적용한 코드는 아래와 같다:
<div>
<input type="checkbox" class="dm-checkbox" id="dm-checkbox">
<label for="dm-checkbox" class="dm-checkbox-label">
<span id="sun-icon">{{> "icons/sun"}}</span>
<span id="moon-icon">{{> "icons/moon"}}</span>
</label>
<script>
const checkbox = document.getElementById("dm-checkbox");
checkbox.addEventListener("change", () => {
document.body.classList.toggle("dark");
const expiryDate = new Date();
expiryDate.setTime(expiryDate.getTime() + (30 * 60 * 1000));
document.cookie = `darkmode-prefered=${checkbox.checked};path=/;expires=${expiryDate.toUTCString()}`;
});
function getCookie(name) {
let ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i].trim();
if (c.indexOf(name) == 0) {
return c.substring(name.length + 1, c.length);
}
}
return "none";
}
function checkCookie() {
let preference = getCookie("darkmode-prefered");
if (preference === "true") {
checkbox.checked = true;
document.body.classList.add("dark");
} else if (preference === "false") {
checkbox.checked = false;
document.body.classList.remove("dark");
} else if (window.matchMedia("(prefers-color-scheme:dark)").matches) {
checkbox.checked = true;
document.body.classList.add("dark");
}
}
checkCookie();
</script>
</div>
Spectral Web Services에서 제공하는 예시 코드에 expires를 추가함
이미지 업로드하기
서비스형 블로그 플랫폼에서는 에디터를 통해 사용자가 이미지를 업로드하면 https://postfiles.pstatic.net
, https://img1.daumcdn.net
, https://velog.velcdn.com
같은 자체 CDN을 통해 이미지를 무료로 호스팅해주는 대신, 사용자 경험을 크게 훼손하지 않는 선에서 광고 배너를 다는 방식으로 그 비용을 어느정도 회수하고 있는 것 같다.
(각각 글 작성 시점의 네이버 블로그, 티스토리, Velog 이미지 CDN 도메인)
문제는 GitHub Pages
의 경우 자체적인 이미지 CDN을 제공하지 않기 때문에 이미지를 관리할 방법을 찾아야 한다는 것인데, 리포지토리 안에 이미지를 저장해둔 다음 불러오는 방식은 GitHub Pages의 사용량 제한 정책을 고려할 때 장기적으로
유지할 수 없다고 판단했다:
- 리포지토리의 크기는
1GB 이하를 권장
하며, 호스팅 되는전체 페이지 크기의 합은 1GB를 초과할 수 없다
(리포지토리 크기와 Published Pages 크기가 다를 수 있음) 100GB
의대역폭 제한
이 존재한다 (이미지 파일 요청 또한 대역폭 산정에 포함된다!!!)

간단한 데모 페이지를 보여주는 정도라면 이미지를 직접 업로드해서 관리한다고 하더라도 100GB 대역폭
을 넘지 않을테니 상관 없겠지만, 내가 만드려는건 블로그이기 때문에 당장은 괜찮을 지 몰라도 언젠가 문제가 될 수도 있겠다는 생각이 들었다.
이것 때문에 여기서 접고 서비스형 블로그 플랫폼으로 넘어갈까?
를 정말 진지하게 고민해봤는데, 이것저것 만지다 보니 내 블로그
라는 애착이 생겨서 어떻게든 방법을 찾고 싶다는 생각이 들었고, 레퍼런스를 찾고자 github.io
도메인의 블로그들을 여럿 살펴봤다. 아까 언급했던 대로 리포지토리에 이미지를 저장해둔 다음 
처럼 불러와 사용하는 블로그도 분명 있었지만, 많은 블로그들에서 이미지 호스팅에 https://user-images.githubusercontent.com
이라는 CDN?을 사용한다는 뭔가 수상한 공통점을 찾을 수 있었다.
어떻게?
처음에는 "GitHub 이미지 호스팅 서비스를 제공하는데, 내가 몰랐나보다"라 생각했었는데, 사실 이건 편법
이었다.
README.md
파일을 수정할 때, 에디터에 이미지를drag & drop
Issue
,Pull Request
,Discussion
등의 탭에서 이미지를drag & drop
하게 되면 해당 이미지에 대한 링크가 생성되는데, 이를 사용하는 것이었다.
즉, 리포지토리의 프로젝트와 관련된 활동들을 지원하기 위해 GitHub에서 선의로 제공하는 이미지 서비스를 악용하는 것인데, 심지어는 이걸 GitHub 블로그 이미지 업로드 팁이라며 소개해둔 글도 찾을 수 있었다..
잘 되는데 뭐가 문제야? 라고 생각할 수도 있지만, 도의적인 문제는 차치하더라도 이건 언제 막혀도 이상하지 않다
는 점 때문에 다른 방법을 찾아보기로 했다.

다른 방법?
Google Drive
나 OneDrive
를 CDN처럼 사용할 수 있는 방법이 있기는 하지만 이것도 GitHub
를 통한 방식과 마찬가지로 편법
이었기 때문에 결국 이미지 CDN
을 사용해야겠다는 생각이 들었지만, AWS의 S3 버킷
이나 Cloudflare CDN
같은 유료 서비스의 경우 그렇게 큰 금액이 아니라고 해도 꾸준히 비용이 발생한다는 점이 마음에 걸렸다. 꾸준하게, 오랜 시간동안 블로그를 유지해나가려면 비용이 들지 않아야 할 것 같았다.
그러다 freeimage.host 라는 서비스를 찾았는데, 솔직히 어떻게 유지가 가능한건지 모르겠지만
꽤 오랜 기간동안(since 2018) 운영되고 있는 무료
이미지 호스팅 서비스가 있었다. 중요한 사진이나 민감한 개인정보가 담긴 사진을 보관하고 공유하기에는 적절하지 않을 지 몰라도, 나는 블로그에 누구나 볼 수 있도록
게시하기 위한 이미지들을 호스팅하는 용도로만 사용할 것이기 때문에 지금으로써는 문제가 될 소지가 없다고 판단했다.
운영 방침이 변경돼서 요금을 지불해야 한다던지, 서비스가 종료된다던지 하는 경우에 대비해서 원본 이미지들을 따로 백업해 두되, 앞으로 뭔가 문제가 생기기 전까지는 이 서비스를 활용해 이미지를 업로드할 생각이다.
디자인 수정하기
Ghost에서 몇가지 기본 테마들을 MIT 라이센스(저작권만 표시하면 자유롭게 수정, 배포, 사용하도록 허락해주는 관대한 라이센스)로 공개해준 덕분에, 기본 테마들 중 하나인 Edition 테마를 바탕으로 내가 원하는 디자인(진행중...)의 블로그를 만들어볼 수 있었다. 테마의 깔끔한 디자인이 전체적으로 마음에 들어서 자잘한 CSS 수정만 몇가지 적용시켜 사용중인데, 그마저도 대부분이 다크모드와 관련된 것들이다.
먼저 다크모드에서 CSS 변수의 색상값들을 전체적으로 변경했으며, 이미지/코드/마크다운 블럭의 각진 모서리를 살짝 라운딩 처리 해주고, opacity
속성을 사용해 다크모드에서의 이미지 밝기를 조금 낮춰줬으며, 다크모드에서의 하이라이트 색상을 어두운 테마와 어울리도록 변경해줬다. JS 라이브러리를 통해 구현한 코드 블럭, 목차와 관련된 디자인은 아래에 자세히 적어 두었다.
JS 라이브러리 적용하기
아무런 간섭 없이 컴포넌트들을 배치하고 디자인도 내 마음대로 바꿀 수 있다는 점은 정말 좋은데, 카테고리 모아 보기, 목차 표시, 코드 블럭 하이라이팅처럼 블로그 서비스 플랫폼들에서 기본적으로 제공하는 기능들이 없는 경우가 종종 있었다.
특히 Ghost 에디터에서 자체적으로 코드 블럭
을 지원하는걸 보고 Syntax Highlighting도 당연히 적용될거라 생각했었는데, 테스트용 페이지를 만들어 코드 블럭을 로딩해보니 일반적인 문단과 구분되는 블럭이 표시되기는 하지만 함수 및 변수 선언, 문자열 등등 어느 요소에도 색이 들어가 있지 않았다.
다행히도 이를 구현해놓은 PrismJS
라는 오픈소스 Javascript 라이브러리
가 npm을 통해 배포되고 있었다. npm install
을 통해 직접 설치하여 사용할 수도 있고 CDN을 통해 배포되는 JS
, CSS
파일을 가져와서 사용할 수도 있는데, Ghost 공식 튜토리얼 문서에서 Code Injection
을 활용한 방법을 소개하고 있어 이를 참고하여 구현했다. https://ghost.org/tutorials/code-snippets-in-ghost

PrismJS
PrismJS 공식 페이지를 보면 사용 가능한 여러 테마 옵션들이 있는데, 내가 보기에 라이트모드와 다크모드에 모두 어울리는 테마는 없었다. 기능을 위한 JS
가 있고 스타일링을 위한 CSS
가 있으니, 라이트모드일 때와 다크모드일 때 각각 다른 CSS를 적용시켜주면 되는 거 아닌가?
처음에는 단순하게 if
문을 통해 어떤 CSS
를 적용시킬 지 처리하도록 했는데, 페이지 로드 시점의 테마는 잘 감지하지만 다크모드 토글 버튼을 통한 테마(라이트/다크) 변경에 따라 스타일이 (당연히....) 동적으로 변경되지 않는 문제가 있었다.
테마의 스타일 변경을 감지하려면 HTML classList의 변화를 감지
하는 방법과 다크모드 토글 버튼의 상태 변화를 감지
하는 방법이 있는데, 토글 버튼의 상태를 감지하는것보다 classList의 변화를 감지하는게 더 직관적일 뿐만 아니라 추후 유지보수에도 더 좋은 방식일 것 같아(다크모드 토글 버튼의 구조나 클래스명 등이 바뀔 수 있으니!) classList 변화를 감지하는 방법을 선택하기로 했고, 이번에도 다크모드 토글 버튼처럼 addEventListener
를 활용하면 될 것 같았다.
하지만 버튼의 상태에 따라 테마를 적용해야 하는게 아니라 document.body
의 classList
에 dark
가 있는지 없는지를 감지해 테마를 적용하도록 해야 하는데, DOM에는 요소의 클래스가 변경될 때 발생하는 표준 이벤트가 없으며 이를 감지하기 위해서는 MutationObserver
사용을 권장한다는 것을 알게 되었다.
그래서 기존에 적용되어있던 PrismJS
스타일을 모두 제거한 다음 현재 테마에 맞는 스타일시트를 HTML head
에 추가해주는 updatePrismTheme()
함수를 최초 DOM 로드 시점에 한번 실행하고, MutationObserver
를 통해 document.body
의 classList가 변할때마다 updatePrismTheme()
함수를 실행해주도록 했다.

왜인지 테마를 변경할 때 코드블럭 안에서 +
, =
같은 연산자의 배경색이 이상하게(이전 테마의 색도 아님) 변하는 문제가 있었는데, updatePrismTheme()
함수에서 명시적으로 연산자의 배경색을 제거한 다음 스타일시트를 적용시켜주니 해결됐다.
블로그에 적용한 코드는 아래와 같다:
<script src="https://cdn.jsdelivr.net/npm/prismjs/prism.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs/plugins/autoloader/prism-autoloader.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
function updatePrismTheme() {
const style = document.createElement('style');
style.textContent = `.token.operator {
background: none !important;
}`;
document.head.appendChild(style);
const existingThemes = document.querySelectorAll('link[href*="prism-"]');
existingThemes.forEach(theme => theme.remove());
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = document.body.classList.contains('dark')
? 'https://cdn.jsdelivr.net/npm/prismjs/themes/prism-okaidia.min.css' // 다크모드일 때는 okaidia 테마 적용
: 'https://cdn.jsdelivr.net/npm/prismjs/themes/prism.min.css'; // 라이트모드일 때는 prism 기본 테마 적용
document.head.appendChild(link);
Prism.highlightAll();
}
updatePrismTheme();
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'class') {
updatePrismTheme();
}
});
});
observer.observe(document.body, {
attributes: true
});
});
</script>
Tocbot
목차를 사용하지 않아도 될 만큼 짧고 간단한 글이거나 브라우저 Viewport
의 가로폭이 충분하지 않을 경우에는(ex. 모바일 브라우저) 표시되지 않으며, 처음에는 본문 옆 고정된 위치에 있다가 스크롤을 충분히 내리면 졸졸 따라오기 시작하는 목차를 만들고 싶었다.
사용자가 많은 플랫폼을 사용하는데서 오는 가장 큰 장점은 이미 나와 비슷한 생각이나 고민을 해 본 사람을 쉽게 찾을 수 있다
는 것 아닐까? ghost 개발자 포럼에서 목차 구현과 관련된 글을 찾을 수 있었고, 샘플 코드의 CSS를 약간만 수정하여 내가 원하던 목차를 손쉽게 구현했다.
가로폭이 충분할 경우에만 목차를 표시해주기 위해 CSS 미디어 쿼리 @media(min-width: 1300px)
를 사용했고, position: absolute;
옵션을 준 sidebar
안에 position: sticky;
옵션을 준 container
를 담아 위치를 잡아줬으며, text-overflow: ellipsis;
옵션을 통해 Header
가 목차를 위한 sticky 영역(블럭)을 넘어갈 경우 레이아웃을 벗어나거나 잘리는 대신 ...
처럼 표시하도록 했고, font-weight
, line-height
, font-size
, margin
, padding
등의 자잘한 옵션들은 여러 값을 테스트해보며 내 눈에 예뻐 보이는 값을 골랐다.
tocbot.init()
의 파라미터 목록은 Tocbot 공식 페이지의 옵션 목록을 참고했으며, ghost의 에디터에서 제공하는 toggle
이 목차에 표시되는 문제를 해결하기 위해 tocbot.init()에 ignoreSelector
를 추가해줬다.
블로그에 적용한 코드는 아래와 같다:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tocbot@4.32.2/dist/tocbot.min.css">
<script src="https://cdn.jsdelivr.net/npm/tocbot@4.32.2/dist/tocbot.min.js"></script>
<script>
const parent = document.querySelector(".gh-content.gh-canvas");
const asideElement = document.createElement("aside");
asideElement.setAttribute("class", "gh-sidebar");
const containerElement = document.createElement("div");
containerElement.setAttribute("class", "gh-toc-container");
const divElement = document.createElement("div");
divElement.setAttribute("class", "gh-toc");
containerElement.appendChild(divElement);
asideElement.appendChild(containerElement);
parent.insertBefore(asideElement, parent.firstChild);
tocbot.init({
tocSelector: '.gh-toc',
contentSelector: '.gh-content',
headingSelector: 'h1, h2, h3, h4',
// Ghost 에디터에서 자체적으로 제공하는 toggle은 목차에서 제외
ignoreSelector: 'h4.kg-toggle-heading-text',
hasInnerContainers: true,
orderedList: false,
collapseDepth: 6,
});
const toc = document.querySelector(".gh-toc");
const sidebar = document.querySelector(".gh-sidebar");
// 목차가 필요 없는 게시물에서는 목차를 표시하지 않기 위한 if문
if (toc.querySelectorAll('li').length < 2) {
sidebar.style.display = 'none';
}
</script>
To be continued...
- {
Site Analytics
,댓글
,웹폰트
} 적용기 - 불필요하거나 사용하지 않는 요소들 제거하기
- 그 외 여러 코드 개선점들과 수정사항
그리고 전체적인 소감은 다음 글
에서 계속..