티스토리 뷰

JavaScript

Tistory(#1) theme toggle 구현기

sukvvon 2022. 2. 22. 23:20

나만 없는 다크모드,,,

본 포스팅은 티스토리 #1을 기준으로 합니다.

velog나 notion 그리고 github.io를 이용하는 블로거분들의 페이지를 방문할 때가 있습니다. 그리고 이 분들과 저의 차이점, 그리고 부러운 점이 하나 있었습니다. 바로 다크모드 테마가 있다는 것이었습니다...

평소 tistory에서 제공하는 #1 스킨을 사용하는 저는 다크모드 그리고 다크모드와 라이트모드를 사용자 기호에 맞게 사용할 수 있는 토글도 없었습니다.

그래서 저의 티스토리 블로그 테마에 다크모드와 라이트모드 토글을 구현하였습니다.

FOCU 현상

문제

<body>
  <div>
    <!-- 블로그 컨텐츠 내용 -->
    ...
  </div>
  <!-- 테마 및 토글 관련 코드 -->
  <script src="themeToggle.js"></script>
</body>

처음에 구현할 때 theme.jsthemeToggle.js의 내용을 모두 합하여 코드를 작성하고 <body>태그 제일 하단에 <srcipt>태그를 추가하고 테스트를 하였습니다.

그랬더니 lihgt 테마와 dark 테마에서 페이지를 로딩할 때 화면이 번쩍이는 현상을 발견했습니다. 알고보니 이 현상은 FOCU(Flash Of Unstyled Content)였습니다.

테마 및 토글 관련 제어 코드를 담은 <srcipt>태그가 마지막에 실행되어서 화면이 반짝이는 것이었습니다.

해결

<body>
  <!-- body 태그 생성 이후 블로그 컨텐츠 내용을 받기 전에 삽입 -->
  <script src="theme.js"></script>
  <div>
    <!-- 블로그 컨텐츠 내용 -->
    ...
  </div>
  <script src="themeToggle.js"></script>
</body>

문제를 해결하기 위해 통합된 코드를 테마 관련 코드인 theme.js, 테마 토글 관련 코드인 themeToggle.js로 나누어 리펙토링하였습니다. 그리고 위와 같이 script 태그들을 삽입하였습니다.

브라우저 랜더링 과정은 간단하게 설명하면 다음과 같습니다.

<body> 태그가 생성되자마자 theme.js가 포함된 스크립트를 삽입하여 html 파싱을 중지하고 제어권을 js엔진에 넘긴 후 js엔진이 파싱합니다. js엔진이 파싱을 마친 후 html 파싱은 다시 시작되고 모두 끝난 후 마지막에 themeToggle.js가 포함된 스크립트 코드를 만나 js엔진이 파싱을 처리하게 됩니다.

위와 같은 과정으로 테마 관련 스크립트를 중간에 미리 처리하였기 때문에 화면 깜빡임 현상(FOCU)은 발생하지 않게 되었습니다.

구성

theme 파트

  • themes.css
  • theme.js

toggle 파트

  • html
  • theme-toggle.css
  • themeToggle.js

크게 theme에 대한 코드와 toggle에 대한 코드로 구성하였습니다.

light, dark 테마에 대한 css 설정을 다룬 themes.css, 사용자나 브라우저가 설정한 것에 의하여 light, dark 테마 중 하나를 적용시키는 theme.js.

toggle을 구현하는 html 코드, toggle을 구현하는 html 코드를 꾸민 css인 theme-toggle.css, 마지막으로 light, dark 테마에 따른 toggle의 반응을 나타낸 themeToggle.js입니다.

themes.css

html[theme="dark"] {
  --background-color-body: #0d1117;
  --background-color-head-foot: #161b22;
  --border-bottom-h2: 1px solid #21262d;
  --color: #c9d1d9;
  --color-accent: #58a6ff;
  --blockquote-border-left: 5px solid #30363d !important;
  --tag-background-color: #222;
  --color-a: #8b949e;
  --background-color-inline-code: rgba(110, 118, 129, 0.4);
}

html[theme="light"] {
  --background-color-body: #fff;
  --background-color-head-foot: #f6f8fa;
  --border-bottom-h2: 1px solid #d8dee4;
  --color: #24292f;
  --color-accent: #0969da;
  --blockquote-border-left: 5px solid #d0d7de !important;
  --tag-background-color: #f0f0f0;
  --color-a: #57606a;
  --background-color-inline-code: rgba(175, 184, 193, 0.2);
}

/* utterance */
html[theme="dark"] .utterance-light {
  display: none;
}

html[theme="light"] .utterance-dark {
  display: none;
}

/* scrollbar */
html,
.skin_view .area_view pre code.hljs {
  scrollbar-width: thin;
  scrollbar-color: var(--color-a) var(--background-color-body);
}

.skin_view .area_view pre code.hljs::-webkit-scrollbar {
  background-color: var(--background-color-body);
  height: 6px;
}

body::-webkit-scrollbar {
  background-color: var(--background-color-body);
  width: 6px;
}

body::-webkit-scrollbar-thumb,
.skin_view .area_view pre code.hljs::-webkit-scrollbar-thumb {
  background-color: var(--color-a);
  border-radius: 6px;
}

html[theme="COLOR"]

html[theme="dark"] {
  --background-color-body: #0d1117;
  --background-color-head-foot: #161b22;
  --border-bottom-h2: 1px solid #21262d;
  --color: #c9d1d9;
  --color-accent: #58a6ff;
  --blockquote-border-left: 5px solid #30363d !important;
  --tag-background-color: #222;
  --color-a: #8b949e;
  --background-color-inline-code: rgba(110, 118, 129, 0.4);
}

html[theme="light"] {
  --background-color-body: #fff;
  --background-color-head-foot: #f6f8fa;
  --border-bottom-h2: 1px solid #d8dee4;
  --color: #24292f;
  --color-accent: #0969da;
  --blockquote-border-left: 5px solid #d0d7de !important;
  --tag-background-color: #f0f0f0;
  --color-a: #57606a;
  --background-color-inline-code: rgba(175, 184, 193, 0.2);
}

dark 테마와 light 테마에서 적용하기 원하는 색상을 예를 들어 --color: #c9d1d9, --color: #24292f와 같이 정의 한 후 var(--color)로 css에서 사용합니다.

자세한 사항은 Custom properties (--*): CSS variables에서 확인할 수 있습니다.

댓글(utterances) 테마 설정

선택사항입니다. 만약 utterances를 사용중이라면 참고하시면 되겠습니다.

utterances html and css code

html
<div class="utterance-light">
  <script
    src="https://utteranc.es/client.js"
    repo="sukvvon/blog-comments"
    issue-term="pathname"
    theme="github-light"
    crossorigin="anonymous"
    async
  ></script>
</div>

<div class="utterance-dark">
  <script
    src="https://utteranc.es/client.js"
    repo="sukvvon/blog-comments"
    issue-term="pathname"
    theme="dark-blue"
    crossorigin="anonymous"
    async
  ></script>
</div>

만약 utterances를 사용중이라면 간단하게 light, dark 테마별로 적용시킬 수 있습니다.

<div class="area_reply "> 태그 밑에 위와 같은 html 코드를 추가합니다.

theme="github-light"가 포함된 항목은 utterance-light 클래스가 포함된 div 태그로 감싸주고, "theme="dark-blue"가 포함된 항목은 utterance-dark 클래스가 포함된 div 태그로 감싸주면 테마 별로 댓글 창을 테마별로 구현할 수 있습니다.

css
/* utterance */
html[theme="dark"] .utterance-light {
  display: none;
}

html[theme="light"] .utterance-dark {
  display: none;
}

display: none 속성을 부여하여 dark 테마일 경우엔 light utterances 테마가 보이지 않게 하고 light 테마일 경우 dark utterances 테마가 보이지 않게 합니다.

scrollbar

/* scrollbar */
html,
.skin_view .area_view pre code.hljs {
  scrollbar-width: thin;
  scrollbar-color: var(--color-a) var(--background-color-body);
}

.skin_view .area_view pre code.hljs::-webkit-scrollbar {
  background-color: var(--background-color-body);
  height: 6px;
}

body::-webkit-scrollbar {
  background-color: var(--background-color-body);
  width: 6px;
}

body::-webkit-scrollbar-thumb,
.skin_view .area_view pre code.hljs::-webkit-scrollbar-thumb {
  background-color: var(--color-a);
  border-radius: 6px;
}

dark 테마와 light 테마에 맞게 scrollbar 또한 테마를 적용하였습니다.

firefox(gaeko)

html,
.skin_view .area_view pre code.hljs {
  scrollbar-width: thin;
  scrollbar-color: var(--color-a) var(--background-color-body);
}

.skin_view .area_view pre code.hljs::-webkit-scrollbar {
  background-color: var(--background-color-body);
  height: 6px;
}

영향을 받는 대상인 전체 화면을 의미하는 html과 블로그 내 코드블럭코드 .skin_view .area_view pre code.hljs를 기준으로

scrollbar-width: thin으로 설정하여 스크롤바가 두껍지 않게 하여 가독성을 높여주도록 하고

scrollbar-color: var(--color-a) var(--background-color-body)으로 설정하여 light, dark 테마에 따라서 firefox(gaeko) 브라우저에서 동작할 수 있도록 합니다.

chrome, safari, edge, etc...(webkit)

body::-webkit-scrollbar {
  background-color: var(--background-color-body);
  width: 6px;
}

body::-webkit-scrollbar-thumb,
.skin_view .area_view pre code.hljs::-webkit-scrollbar-thumb {
  background-color: var(--color-a);
  border-radius: 6px;
}

firefox(gaeko)와는 다르게 chrome, safari, edge 등 webkit 기반의 엔진을 기반으로 삼는 브라우저는 html이 아닌 body 태그를 중심으로 ::-webkit-scrollbar 선택자를 활용하여 width: 6px, background-color: var(--color-a)

전체 스크롤바의 너비는 6px, 배경색상은 var(--background-color-body)로 설정합니다.

body뿐만이 아닌 .skin_view .area_view pre code.hljs도 포함해 ::-webkit-scrollbar-thumb 선택자를 활용하여 background-color: var(--color-a), border-radius: 6px

스크롤 핸들의 배경 색상을 var(--color-a)로 설정하고, border-radius를 6px로 설정해 꼭짓점을 6px 정도 둥글게 합니다.

theme.js

const DARK = "dark";
const LIGHT = "light";
const THEME_KEY = "theme";

const userTheme = sessionStorage.getItem(THEME_KEY);
const osTheme = window.matchMedia("(prefers-color-scheme: light)").matches
  ? LIGHT
  : DARK;

const getTheme = () => {
  const Theme = userTheme ? userTheme : osTheme;
  return Theme;
};

const setTheme = (color) => {
  sessionStorage.setItem(THEME_KEY, color);
  document.documentElement.setAttribute(THEME_KEY, color);
};

if (getTheme() === DARK) {
  setTheme(DARK);
} else {
  setTheme(LIGHT);
}

페이지에 접속했을 때 light 테마와 dark 테마 중 컴퓨터나 사용자가 선호하는 색상 테마를 적용하는 코드입니다.

자주 쓰이는 변수 선언

const DARK = "dark";
const LIGHT = "light";
const THEME_KEY = "theme";

dark, light, theme와 같은 자주 사용하는 단어들을 DARK, LIGHT, THEME_KEY로 선언하여 코드를 적을 때 오타를 방지합니다.

현재 설정된 theme 확인

const userTheme = sessionStorage.getItem(THEME_KEY);
const osTheme = window.matchMedia("(prefers-color-scheme: light)").matches
  ? LIGHT
  : DARK;

userTheme

const userTheme = sessionStorage.getItem(THEME_KEY);

sessionStorageTHEME_KEY"theme"에 저장된 값이 있는지 확인합니다. 그리고 그 값을 userTheme 변수로 선언합니다.

osTheme

const osTheme = window.matchMedia("(prefers-color-scheme: light)").matches
  ? LIGHT
  : DARK;
prefer-colors-scheme

"(prefers-color-scheme: light)" css 요소를 통하여 현재 사용자가 시스템에 light 테마를 사용하는 것을 선호하는지를 window.matchMedia().matches를 통해 확인합니다.

dark 테마를 페이지의 기본 설정으로 할 것이므로 prefers-color-scheme: light을 기준으로 삼항연산자를 활용하여 true라면 LIGHT"light"를,

false라면 DARK"dark"와 선호하는 테마가 없는 경우인 no-preferenceosTheme에 넣으며 변수로 선언합니다.

getTheme()

const getTheme = () => {
  const theme = userTheme ? userTheme : osTheme;
  return theme;
};

삼항연산자를 활용하여 theme을 기준으로 userTheme이 참(true), 존재한다면 userTheme

반대로 거짓(false), 존재하지 않는다면 osTheme를 택하게 하여 theme 변수를 선언합니다.

getTheme()의 순환 과정을 자세히 정리하자면 다음과 같습니다.

처음 사이트에 들어오게 되어 theme.js가 실행이 되어 getTheme() 함수를 호출하면 theme 값을 반환(return)하게 되는데 sessionStorage의 키 값은 없어 userTheme 값은 존재하지 않으므로 삼항연산자에 의하여 theme = osTheme이 됩니다.

블로그 내에 페이지를 이동하는 등 새로고침이 되었을 때 sessionStorage 값은 존재하므로 getTheme() 함수 내의 theme = userTheme이 되어 getTheme() 함수는 userTheme을 반환하게 됩니다.

페이지를 빠져 나갈 경우 session은 초기화가 됩니다.

setTheme()

const setTheme = (color) => {
  sessionStorage.setItem(THEME_KEY, color);
  document.documentElement.setAttribute(THEME_KEY, color);
};

sessionStorage.setItem을 통해 키 이름과 키 값을 THEME_KEY"theme"와 매개변수인 color로 초기화합니다.

document.documentElement가 가리키는 <html>태그에 setAttribute을 통해 THEME_KEYtheme 속성 이름과 color라는 매개 변수로 값을 초기화합니다.

함수를 사용할 때 color라는 매개변수를 통해 DARKLIGHT로 값을 입력합니다.

if else

if (getTheme() === DARK) {
  setTheme(DARK);
} else {
  setTheme(LIGHT);
}

getTheme()DARK"dark"일 경우와 선호하는 테마가 없는 경우인 no-preferece인 경우 setTheme(DARK) 함수를 실행하고,

반대로 LIGHT"lignt"일 경우 setTheme(LIGNT) 함수를 실행합니다.

theme toggle에 해당하는 html

<!--toggle button-->
<label class="switch-button">
  <input class="check" type="checkbox" />
  <span class="onoff-switch"></span>
</label>
<!--toggle button-->

label 태그를 통해 토글 버튼 관련 항목의 공간을 만듭니다. 그 안에 checkbox type의 <input>, 그리고 토글 버튼의 역활을 하는 <span> 태그를 추가합니다.

<label> 태그의 특성에 의해서 <label> 태그 안에 있는 부분에 이벤트가 발생하였을 때 <input> 태그에 이벤트가 발생한 것과 같은 효과를 볼 수 있습니다.

theme-toggle.css

.switch-button {
  position: relative;
  display: inline-block;
  width: 56px;
  height: 38px;
  bottom: 5.22px;
  margin: 0 2px;
}

.switch-button input {
  opacity: 0;
}

.onoff-switch {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  border-radius: 20px;
  background-color: #ddd;
  -webkit-transition: 0.4s;
  transition: 0.4s;
}

.onoff-switch::before {
  position: absolute;
  content: "";
  height: 24px;
  width: 22px;
  top: 7px;
  left: 4px;
  border-radius: 20px;
  background-color: #fff;
  -webkit-transition: 0.5s;
  transition: 0.5s;
}

.switch-button input:checked + .onoff-switch {
  background-color: #238636;
}

.switch-button input:checked + .onoff-switch::before {
  -webkit-transform: translateX(26px);
  -ms-transform: translateX(26px);
  transform: translateX(26px);
}

@media only screen and (max-width: 820px) {
  .switch-button {
    margin: 21px 0 -6px 15px;
  }
}

theme에 대한 toggle 버튼에 관한 css입니다.

.swich-button input

.switch-button input {
  opacity: 0;
}

opacity: 0 을 통해서 checkbox가 표시되지 않게 합니다.

.onoff-switch

.onoff-switch {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  border-radius: 20px;
  background-color: #ddd;
  -webkit-transition: 0.4s;
  transition: 0.4s;
}

.switch-button input:checked + .onoff-switch {
  background-color: #238636;
}

-webkit-transition: 0.4s, transition: 0.4s을 통해 inputchecked 즉 체크박스가 체크가 된 경우 토글 버튼의 바탕화면 색상이 #ddd에서 #238636으로 바뀔 때 0.4초가 걸리는 효과를 줍니다.

.onoff-switch::before

.onoff-switch::before {
  position: absolute;
  content: "";
  height: 24px;
  width: 22px;
  top: 7px;
  left: 4px;
  border-radius: 20px;
  background-color: #fff;
  -webkit-transition: 0.5s;
  transition: 0.5s;
}

.switch-button input:checked + .onoff-switch::before {
  -webkit-transform: translateX(26px);
  -ms-transform: translateX(26px);
  transform: translateX(26px);
}

마찬가지로 -webkit-transition: 0.5s, transition: 0.5s을 통해 switch-button class의 inputchecked 즉 체크박스가 체크가 된 경우 토글 버튼의 위치가 -webkit-transform: translateX(26px), -ms-transform: translateX(26px), transform: translateX(26px)에 의해서 26px만큼 우로 이동하고 이동 할 때 0.5초가 걸리는 효과를 줍니다.

@media

@media only screen and (max-width: 820px) {
  .switch-button {
    margin: 21px 0 -6px 15px;
  }
}

max-width: 820px으로 820px로 최대 너비를 설정하여 모바일 환경에서의 토글 버튼의 위치를 설정합니다.

themeToggle.js

const checkbox = document.querySelector(".check");

const themeToggle = (event) => {
  if (event.target.checked) {
    setTheme(DARK);
  } else {
    setTheme(LIGHT);
  }
};

checkbox.addEventListener("click", themeToggle);

if (getTheme() === DARK) {
  checkbox.setAttribute("checked", "");
}

theme에 관한 toggle 버튼에 대한 js 코드 설명입니다.

checkbox

const checkbox = document.querySelector(".check");

checkbox.addEventListener("click", themeToggle);

check 클래스가 포함된 태그를 선택하여 checkbox로 선언합니다. 그리고 addEventListener() 메서드를 통해서 click 이벤트 발생 시 themeToggle 함수를 실행하도록 합니다.

themeToggle()

const themeToggle = (event) => {
  if (event.target.checked) {
    setTheme(DARK);
  } else {
    setTheme(LIGHT);
  }
};

toggle 버튼에 관한 함수입니다. event 매개변수를 통하여 event.target.checked 즉 html 코드가 <input type="checkbox" checked>라면 setTheme(DARK)

반대로 <input type="checkbox">라면 setTheme(LIGHT)를 호출하도록 합니다.

if

if (getTheme() === DARK) {
  checkbox.setAttribute("checked", "");
}

getTheme()DARK"dark"일 경우와 선호하는 테마가 없는 경우인 no-preferece인 경우 토글은 checked가 되게 합니다.

참고

'JavaScript' 카테고리의 다른 글

Tistory(#1) 스크롤시 상단바 숨기고 보이게 하기  (0) 2022.03.12
댓글