Explanation

다크 모드 (Dark Mode) 후기

Gatsby로 만든 블로그에 다크 모드를 구현했다. 정적 웹사이트에 다크 모드를 붙이는 건 생각보다 더 흥미로운 일이었다.

다크 모드 (Dark Mode) 후기

Light/Dark 모드 스크린 캡쳐

들어가는 글

블로그에 다크 모드를 구현했다. 유행하는 기능을 넣어보자는 가벼운 마음으로 시작했는데 정적 사이트라는 특성 떄문에 예상 외로 더 고민이 필요했다. 특히 크리티컬(critical) 렌더링에 대해 실용적으로 접근해서 생각해볼 수 있는 드문 기회였다.

다크 모드란 간단히 말하자면 어두운 배경에 밝은 활자를 사용하도록 페이지의 색상을 반전시키는 기능을 말한다. MacOS Mojave에 다크 모드 설정이 추가되면서 이 기능을 도입한 앱들이 많아졌고, YouTube 등 웹사이트에서도 종종 보이고 있다. 특히 개발자들 블로그에서 자주 보이는데, 유행에 편승한 일인으로서, 다른 중한 일을 제쳐두고 다크 모드를 구현하는 기분을 충분히 이해한다.

다크 모드의 요건

내가 다크 모드에서 기대한 것들은 다음과 같았다:

  • 시스템 설정을 반영해서 컬러 모드가 선택된다.
  • 사용자가 컬러 모드를 직접 선택할 수 있다.
  • 사용자 설정이 시스템 설정에 우선한다.
  • 사용자 설정이 다음 방문시에도 유지된다.

스타일 변경하기

컬러 모드가 변경될 때 스타일을 조정하기 위한 방법으로 body 요소의 클래스와 CSS 변수를 이용하기로 했다.

  • 컬러 모드에 따라 <body> 태그에 light 혹은 dark 클래스가 붙는다.
  • 각각의 경우에 맞게 CSS 변수로 색상들을 정의하고, 사이트 전체에서 그 변수들을 사용하도록 했다.
<body class="dark">
body.light {
  --bg: white;
  --text: black;
  /* ... */
}

body.dark {
  --bg: black;
  --text: white;
  /* ... */
}

p {
  color: var(--text);
}

시스템 설정 읽기

시스템 설정은 WindowmatchMedia 메서드를 이용해서 확인할 수 있다.

const mqList = window.matchMedia('(prefers-color-scheme: dark)')
// => { matches: true, media: '(prefers-color-scheme: dark)', onchange: null }

시스템 설정 변경을 감시하는 것도 가능하다.

mqList.addListener(({ matches }) => {
	console.log(matches)  // true or false
})

사용자 설정 — 정적 사이트라서 생기는 문제

사용자 설정을 처리하는 부분에서 Gatsby로 만들었다는 점, 즉 정적 웹사이트라는 특징 때문에 흥미로운 고민을 하게 된다. 일반적인 동적 웹사이트라면 서버 앱에서 요청에 담긴 사용자의 설정을 읽어서 그에 맞게 페이지를 구성한 후 응답을 보내줄 수 있을 거다. 하지만 정적 사이트의 특성상 모든 페이지가 빌드 시점에 이미 완전히 결정되고 그걸로 끝이다. 결국 사용자의 컬러 모드 설정을 판단할 수 있는 곳은 클라이언트 밖에 없다. 사용자의 설정을 다음 방문시까지 기억해야 하므로 저장 위치는 자연스럽게 LocalStorage로 좁혀진다.

이제 남은 것은 읽어온 사용자의 설정을 바탕으로 컬러 모드를 결정하고 스타일을 적용하는 것이다. 문제는 이 작업을 어느 시점에 하느냐 하는 것이다. 로컬 스토리지에 담긴 사용자의 설정과 시스템 설정을 확인하는 스크립트를 언제 실행하는 것이 좋을까?

Flash of Undetermined Color Scheme

React를 이용하는 Gatsby 사이트이다 보니 가장 먼저 든 생각은 componentDidMount 나 hook을 이용해서 페이지가 마운팅된 후 해당 코드를 실행하는 것이었다. 일단 디폴트인 라이트 모드로 콘텐츠를 보여주고, 시스템과 사용자의 설정을 읽어와 컬러 모드를 결정하면 해당 스타일을 적용할 수 있겠다. 혹은 컬러 모드가 결정되기 전까지 아예 콘텐츠를 가려버리는 수도 있겠다.

하지만 사용자가 보는 앞에서 흰 화면이 검은 화면으로 바뀌는 것과 내용이 늦게 표시되는 것 어느 쪽이든 좋은 사용자 경험은 아니다.

컬러 모드를 결정하는 것은 중대한(critical) 작업이다

대부분의 시간을 React나 Hooks 같은 것들만 고민하며 지내다 보니 더 기본적이고 본질적인 것들에는 생각이 잘 미치지 않는 경향이 생기는 것 같다. 이쯤 되니 드는 생각은 컬러 모드를 결정하는 일은 사용자 경험에 있어 중대한(critical) 작업이라는 것이다. 이 작업은 가능한 한 빨리 수행되어야 한다. React 컴포넌트가 클라이언트에서 재차 하이드레이트되는 때보다 더 빨리.

렌더 블로킹 인라인 스크립트

다크 모드가 브라우저 리소스 로딩의 특성까지 고민하게 만드는 기능일 거라고는 예상하지 않았었다. React 등으로 앱을 만들다보면 그 맥락 안에서 모든 JS 코드를 쓰게 되고 외부 사정을 생각할 일이 잘 없긴 하다. 웹의 모범적인 렌더링 기법을 얘기하자면, 초기 렌더링에 불필요한 스크립트는 비동기로 실행하거나 렌더링 후에 실행되도록 미루어야 한다. 하지만 크리티컬한 코드가 필요한 경우에는 스크립트의 블로킹 속성을 활용할 수 있다.

생각해보면 이 상황에서 필요한 것은 간단하다. 페이지의 내용이 렌더링될 때 <body> 태그에 컬러 모드를 구별하는 클래스가 붙어있으면 된다. 그 클래스를 바탕으로 컬러 모드에 맞는 스타일이 적용될 것이다. 그러기 위해선, 첫째, 페이지 렌더링을 막고, 둘째, 컬러 모드를 결정하고, 셋째, <body> 태그에 결정된 컬러 모드 클래스를 추가하고, 넷째, 렌더링을 이어가면 된다.

페이지 렌더링에 크리티컬한 코드를 실행하기 위해서 인라인 스크립트를 사용할 수 있다. 그리고 렌더링은 막으면서 body 태그에 접근할 수 있어야 하므로 위치는 여는 body 태그 바로 아래가 적절하다. 스크립트 내에서 필요한 작업은 다음과 같다:

  • 로컬 스토리지에서 읽어온 사용자의 설정을 확인한다.
  • 시스템 설정을 확인한다.
  • 사용자 설정이 있으면 시스템 설정에 우선한다.
  • 시스템 설정에 반응할 수 있도록 이벤트 리스너를 추가한다.
  • <body> 태그에 컬러 모드를 나타내는 클래스를 추가한다.

나가는 글

사실 Gatsby 사이트에 다크 모드를 추가하는 일은 아주 간단하다. 이전에 Dan Abramov가 블로그 Overreacted에 다크 모드 기능을 넣을 때 많은 사람들이 관심을 가졌었다. 그 구현을 그대로 따라서 만든 gatsby-plugin-dark-mode 플러그인이 있는데, 그걸 사용하면 바로 토글 스위치를 붙일 수도 있다. 하지만 정적 사이트에 다크 모드를 구현하면서 의외의 흥미로운 고민을 할 수 있었던 것이 반가워 글로 남긴다. 정적 사이트와 다크 모드의 조합은 웹페이지 로딩에 대한 보기 드문, 실용적이면서도 효과적인 교육자료다. 정적 사이트를 갖고 있는 개발자라면 다크 모드를 달아 보는 것을 추천한다.

Join the Newsletter

I will send you only good things.

Your thoughts? Please leave a reply.