hexo+icarus 다크모드 깜빡임 해결

예전에 메모해놓고 이제 게시글로 적는다!

기본 배경 색 없애기

먼저 css레벨에서 적용되는 css를 초기화해야한다.
브라우저는 기본적으로 css가 없을 시 페이지 이동할 때 이전 페이지의 배경색을 그대로 유지한다.

\theme\icarus\css\include\style\base.styl
7줄 기본 배경색 none으로 수정하기

1
$body-background-color ?= none;

기존 js파일을 두 개로 나누기

로컬스토리지를 읽고 다크모드를 적용하는 함수와 html을 전부 로드한 후 버튼의 innerText를 수정하는 함수로 나누었다.

다크모드를 적용하는 함수는 render blocking이 이루어져야하고 버튼의 innerText의 수정은 HTML이 모두 로드 후 이루어져야하기 때문이다.

따라서 버튼을 수정하는 함수는 defer로 불러오고 다크모드를 확인하고 적용하는 함수는 속성 없는 script태그로 불러온다.

\themes\icarus\source\js\darkmode.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function init() {
const darkScheme = window.matchMedia("(prefers-color-scheme:dark)");
const theme = localStorage.getItem("theme");
if (theme === "dark") {
document.body.classList.add("dark");
return;
}
if (theme === "light") {
return;
}

if (darkScheme) document.body.classList.add("dark");

localStorage.setItem("theme", darkScheme ? "dark" : "light");
}

hexo\themes\icarus\source\js\darkmodebutton.js

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
const darkModeButton = document.querySelector("#changeModeButton");
const darkScheme = window.matchMedia("(prefers-color-scheme:dark)");

darkScheme.addEventListener("change", (e) => {
console.log("hidark");
const newColorScheme = e.matches ? "dark" : "light";
localStorage.setItem("theme", newColorScheme);

if (newColorScheme === "dark") {
document.body.classList.add("dark");
btn.innerText = "🌙";
return;
}
document.body.classList.remove("dark");
btn.innerText = "🌞";
});

function toggleTheme() {
console.log("hibutton");
document.body.classList.toggle("dark");
darkModeButton.innerText = document.body.classList.contains("dark")
? "🌙"
: "🌞";
localStorage.setItem(
"theme",
document.body.classList.contains("dark") ? "dark" : "light"
);
}

function initButton() {
console.log("initButton");
const data = localStorage.getItem("theme");
if (data == "dark") {
darkModeButton.innerText = "🌙";
return;
}
darkModeButton.innerText = "🌞";
}

initButton();
darkModeButton.addEventListener("click", toggleTheme);

\themes\icarus\layout\common\scripts.jsx
56번쨰 줄에 darkmodebutton.js추가

1
2
<script src={url_for("/js/main.js")} defer></script>
<script src={url_for("/js/darkmodebutton.js")} defer></script>

hexo\themes\icarus\layout\common\head.jsx

js를 로드하고 실행하는 동안 렌더링을 막아야하기 때문에 script에 속성을 지정하지 않아야한다.
적다보니 preload속성을 추가해도 좋을 것 같다.

1
<script src={url_for("/js/darkmode.js")}></script>

고의로 render blocking 만들기

새로고침이 되어서 다크모드가 적용되기 전 까지 렌더링이 이루어지면 안된다.

body의 최상단에 darkmode.js에서 정의한 init함수를 호출해준다.
hexo\themes\icarus\layout\layout.jsx

1
2
3
4
<html lang={language ? language.substr(0, 2) : ""}>
<Head site={site} config={config} helper={helper} page={page} />
<body class={`is-${columnCount}-column`}>
<script>init();</script>

ref

https://www.youtube.com/watch?v=ElsZ-v4Ow08

hexo + github 블로그 다크모드 깜빡임 해결

렌더링이 시작된 후에 defer속성으로 받아온 스크립트가 실행되다보니 화면 전환중에 흰화면으로 잠시 전환되는 현상이 발생하였다.

다음과 같은 방법으로 수정하였다.

body의 기본 배경 색 없애기

먼저 7번째 줄에 있는 body의 background-color를 none으로 수정하였다.

\theme\icarus\css\include\style\base.styl

1
$body-background-color ?= none;

기존 js파일을 두 개로 나누기

기존 js파일의 경우 init 함수에서 버튼 설정과 다크모드 로드를 동시에 해주었는데,
다크모드 로드는 렌더링 전에 실행되어야 하고 버튼 설정은 렌더링 후에(최소 DOM트리 생성 이후) 실행되어야 한다.
때문에 파일을 두 개로 나누었다.

darkmode.js엔 chinsun9님의 게시글을 읽고 matchMedia를 읽어서 실행하는 부분을 추가하였다.
(너무 친절하게 답변해주셨다..!)

localstorage와 matchMedia를 이용하여 다크모드를 로드하는 함수이다.
\themes\icarus\source\js\darkmode.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function init() {
const darkScheme = window.matchMedia("(prefers-color-scheme:dark)");
const theme = localStorage.getItem("theme");
if (theme === "dark") {
document.body.classList.add("dark");
return;
}
if (theme === "light") {
return;
}

if (darkScheme) document.body.classList.add("dark");

localStorage.setItem("theme", darkScheme ? "dark" : "light");
}

버튼 관련 설정을 하는 js 파일이다.
hexo\themes\icarus\source\js\darkmodebutton.js

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
const darkModeButton = document.querySelector("#changeModeButton");
const darkScheme = window.matchMedia("(prefers-color-scheme:dark)");

darkScheme.addEventListener("change", (e) => {
console.log("hidark");
const newColorScheme = e.matches ? "dark" : "light";
localStorage.setItem("theme", newColorScheme);

if (newColorScheme === "dark") {
document.body.classList.add("dark");
btn.innerText = "🌙";
return;
}
document.body.classList.remove("dark");
btn.innerText = "🌞";
});

function toggleTheme() {
console.log("hibutton");
document.body.classList.toggle("dark");
darkModeButton.innerText = document.body.classList.contains("dark")
? "🌙"
: "🌞";
localStorage.setItem(
"theme",
document.body.classList.contains("dark") ? "dark" : "light"
);
}

function initButton() {
console.log("initButton");
const data = localStorage.getItem("theme");
if (data == "dark") {
darkModeButton.innerText = "🌙";
return;
}
darkModeButton.innerText = "🌞";
}

initButton();
darkModeButton.addEventListener("click", toggleTheme);

작성한 js파일의 위치 지정해주기

  1. darkmodebutton.js
    버튼은 렌더링후에(최소 DOM트리 생성 이후) 실행되어야 하므로 아래부분에 추가해주었다.
    \themes\icarus\layout\common\scripts.jsx
    56번쨰 줄에 darkmodebutton.js추가

    1
    2
    <script src={url_for("/js/main.js")} defer></script>
    <script src={url_for("/js/darkmodebutton.js")} defer></script>
  2. darkmode.js
    body 최상단에서 실행하기위해 head태그에서 js파일을 로드했다.
    적다보니 preload속성을 추가해도 좋을 것 같다.

    hexo\themes\icarus\layout\common\head.jsx

    1
    <script src={url_for("/js/darkmode.js")}></script>
  3. darkmode.js의 init 함수
    다크모드를 로드하는 건 렌더링 이전에 실행되어야한다.
    때문에 body태그 최상단에 rendering block 요소로 defer나 async 속성을 지정하지 않고 넣어주었다.
    hexo\themes\icarus\layout\layout.jsx

    1
    2
    3
    4
    <html lang={language ? language.substr(0, 2) : ""}>
    <Head site={site} config={config} helper={helper} page={page} />
    <body class={`is-${columnCount}-column`}>
    <script>init();</script>

ref

[A1] 웹뷰에서 다크모드 상속받기: 일관성있는 사용자 경험을 위하여
blog fix 다크 모드 화면 깜박임 제거

hexo+ icarus 다크모드 구현하기

나는 다크모드를 아주 애용한다.

다크모드를 적용하며 유익한 경험이여서 글로 작성해두려고 한다.

hexo + icarus 작동원리

우선 hexo + icarus의 원리부터 알아야한다.

아래에 작성할 모든 디렉터리는 hexo루트 폴더 기준이다.

루드 디렉토리의 내부 파일을 수정한다.

그 후 정적파일인 public 폴더를 생성하고 배포하면 사이트에 반영되는 구조이다.

1
hexo generate --deploy

커스텀 디자인을 하기 위해선 이 ./themes 폴더를 수정해야한다.

버튼 만들기

./themes/icarus/layout/common 폴더엔 icarus의 여러 요소들의 jsx파일이 작성되어있다.

hexo\themes\icarus\layout\common\navbar.jsx파일에서 검색버튼 옆에 버튼을 만들었다.

1
<button type="button" id="changeModeButton" class="navbar-item" />

버튼을 커스텀하기

./themes/icarus/include/style 폴더엔 여러 css들이 작성되어있다.

확장자가 styl인데 기존에 작성된 파일을 보니 들여쓰기로 depth를 관리하는 듯 했다.

그리고 이 파일들은 ./themes/source/css/style.styl에서 모두 import되고 이를 빌드하여 ./public/css에 하나의 css파일을 생성한다.

./themes/icarus/include/style/navbar.styl 파일에 다음과 같은 내용을 추가하였다.

1
2
3
4
5
6
#changeModeButton {
background: none;
border: none;
cursor: pointer;
font-size: 2rem;
}

버튼에 로직 추가하기

./themes/source/js 폴더엔 여러 js파일들이 있다.

이는 ./themes/icarus/layout/common/scripts.js에서 한 번에 모아서 ./public/index.js에 작성된다.

여기에 다크모드 버튼 관련 파일을 작성하였다.

페이지에 접속 시 로컬스토리지를 확인하고 darkMode가 true일경우 html의 body에 darkMode 태그를 추가하고 버튼의 모양을 달 모양으로 해주었다.

값이 존재하지 않을경우 버튼의 모양을 해 모양으로 해주었다. 기본 값은 다크모드의 미적용이기 때문에 따로 처리를 해주진 않았다.

그리고 버튼에 이벤트를 등록해주었다.

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
const darkModeButton = document.querySelector("#changeModeButton");

function init() {
loadDarkModeData();
darkModeButton.addEventListener("click", clickEvent);
}

const loadDarkModeData = () => {
const data = localStorage.getItem("darkMode");
const body = document.querySelector("body");
if (data === "true") {
darkModeButton.innerText = "🌙";
body.classList.add("darkMode");
} else {
darkModeButton.innerText = "🌞";
}
};

const setDarkMode = () => {
const body = document.querySelector("body");
if (body.classList.contains("darkMode")) {
darkModeButton.innerText = "🌞";
body.classList.remove("darkMode");
localStorage.setItem("darkMode", false);
} else {
darkModeButton.innerText = "🌙";
body.classList.add("darkMode");
localStorage.setItem("darkMode", true);
}
};

const clickEvent = () => {
setDarkMode();
console.log(localStorage.getItem("darkMode"));
};

init();

그리고 ./themes/icarus/layout/common/scripts.js에 아래 내용을 추가해주었다.

정확히 어떤 구조인지는 모르지만 main.js가 이런 식으로 작성되어 있길래 똑같이 적었다.

defer는 script로딩시 버튼이 없으면 안되기 때문에 추가해주었다.

1
<script src={url_for("/js/darkmode.js")} defer></script>

다크모드 css 추가하기

다음으로 darkmode.styl 파일을 작성해주었다.

처음 보는 확장자명이기도 했고 ./themes/include/style에 작성할지 ./themes/source/css에 작성할지 고민되었다.

고민끝에 include폴더는 컴포넌트마다의 css를 작성하는 느낌이 강해서 ./themes/source/css/darkmode.styl에 작성하였다.

css는 개발자 도구를 여러 하나하나 클래스명을 확인하며 적어주었다.

빌드과정에서 압축될 것이여서 하나씩 풀어 적었다.

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
body.darkMode
background-color: #171717;
color:#F9F9F9;
.navbar ,& .navbar-menu
background-color: #444444;
section,article,.card{
background-color: inherit;
color:inherit;
}
& .navbar-item
color:#F9F9F9;
&:hover{
color:#3273dc;
background-color:#393E46;
}
& .menu-list>li *{
color:rgba(255,255,255,0.9);
}

& .menu-list a:hover{
color:#3273dc;
background-color:#393E46;
}

& .menu-list a.is-active{
background:#141010;
}
& .is-active
color:#3273dc;
& article
& > * {
color:rgba(255,255,255,0.7);
};
& h1,h2,h3,h4,h5,h6{
color:rgba(255,255,255,0.9);
}
& .article-meta{
color:rgba(255,255,255,0.38);
}
& .card-content
& > * {
color:rgba(255,255,255,0.7);
};

& .title, .location{
color:rgba(255,255,255,0.9);
}

& .menu{
color:rgba(255,255,255,0.7);
}

& .level-start{
color:rgba(255,255,255,0.7);
}

& .menu-label{
color:rgba(255,255,255,0.9);
}

& .tag{
background:#444444;
color:rgba(255,255,255,0.7);
}
.searchbox-container
background:#444444;
.searchbox-input, .searchbox-close{
background:#444444;
color:rgb(255,255,255);
}
.footer{
background:#444444;
}
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: #abb2bf;
background: #282c34;
}

.hljs-comment,
.hljs-quote {
color: #5c6370;
font-style: italic;
}

.hljs-doctag,
.hljs-keyword,
.hljs-formula {
color: #c678dd;
}

.hljs-section,
.hljs-name,
.hljs-selector-tag,
.hljs-deletion,
.hljs-subst {
color: #e06c75;
}

.hljs-literal {
color: #56b6c2;
}

.hljs-string,
.hljs-regexp,
.hljs-addition,
.hljs-attribute,
.hljs-meta-string {
color: #98c379;
}

.hljs-built_in,
.hljs-class .hljs-title {
color: #e6c07b;
}

.hljs-attr,
.hljs-variable,
.hljs-template-variable,
.hljs-type,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-number {
color: #d19a66;
}

.hljs-symbol,
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-title {
color: #61aeee;
}

.hljs-emphasis {
font-style: italic;
}

.hljs-strong {
font-weight: bold;
}

.hljs-link {
text-decoration: underline;
}


그리고 ./themes/source/css/style.styl에 import 해주었다.

1
@import './darkmode.styl'

정리

전체적인 로직은 다음과 같다.

  1. body.darkMode의 하위 태그들에 적용이 되도록 여러 css를 작성해둔다.
  2. 첫 접속시에 로컬스토리지에 darkMode 관련 값이 저장되어있는지 확인하고 알맞은 처리를 해준다. 새로고침시에 상태가 유지되게 하기 위함이다.
  3. 버튼 클릭시 다크모드가 적용되도록 이벤트를 추가해준다.(body태그에 darkMode 클래스를 toggle하도록)

완성본

다크모드가 적용될 경우 적용되지 않을경우의 버튼이 다르고 글자 색과 배경 색도 다르다.

다크모드 미적용 다크모드 적용

느낀 점

  1. styl 확장자 파일을 작성해보았다.
    • Stylus CSS파일이라고 하는데, 작성해보니 의외로 굉장히 편하였다.
  2. REACT가 아닌 jsx로 작성되어 이러한 파일들을 중앙에서 제어하여 빌드시 html을 만드는 과정에 대한 이해가 늘었다.
    • jsx문법을 이용하여 컴포넌트 단위로 파일을 작성하고 관리하는 것이 얼마나 가독성이 좋은지 다시 한 번 느꼈다.
  3. 역시 다크모드가 눈이 편하다
    • 다크모드가 최고다 ㅠ

아쉬운 점

  1. 구현을 하고 보니 코드블록이나 위젯에는 다크모드가 적용이 안 되었다.
    • 이것도 개발자 도구를 이용하여 css를 까보고 수정해야겠다.
  2. 모바일에서 레이아웃이 아쉽다.
    • 버튼을 추가하다보니 상단바에 스크롤바가 생겼다.
    • 이것도 수정해보아야겠다. 레이아웃에 손을 대거나 전체적으로 크기를 줄이는 방향으로 갈 것 같다.
    • 모바일
  3. 정신없이 작업하고 복기하며 메모하다보니 빠진 부분이 있을까봐 걱정된다.
    • 다음부턴 간략한 개요든지 사진을 틈틈히 찍어두어야겠다 ㅠ

+++ 수정

다크모드를 끄고 게시글에 접근시 다크모드가 적용되는 버그가 있었다.
로컬스토리지에 데이터가 string으로 저장되는데 if(data)의 형태로 확인해서 발생한 버그였다.

1
2
3
4
5
6
7
8
9
10
const loadDarkModeData = () => {
const data = localStorage.getItem("darkMode");
const body = document.querySelector("body");
if (data) {
darkModeButton.innerText = "🌙";
body.classList.add("darkMode");
} else {
darkModeButton.innerText = "🌞";
}
};

위 부분을 다음과 같이 변경하였다.

1
2
3
4
5
6
7
8
9
10
const loadDarkModeData = () => {
const data = localStorage.getItem("darkMode");
const body = document.querySelector("body");
if (data === "true") {
darkModeButton.innerText = "🌙";
body.classList.add("darkMode");
} else {
darkModeButton.innerText = "🌞";
}
};