다크모드로 세련된 웹 만들기

홍구
홍구
May 29 · 10 min read

맥 모하비 업그레이드 이후부터 OS단에서 적극적으로 다크모드를 지원하기 시작하였다. 사실 윈도우즈도 접근성은 좀 떨어지고 잘 알려지지 않았지만, 환경설정에서 지원을하고 있고, 안드로이드 등에서 야간모드 등 다크모드 비슷한 게 있긴 했었다.

보통의 OS 단에서의 변화가 나의 웹페이지에 별로 영향을 안 주는 것이 맞지만, 모바일 시대로 인해서 반응형이, 아이폰 레티나로 인해서 고해상도 이미지 처리를 해야 했던 것처럼.. 이 다크모드라는 환경 변화(?)로 인해서 우리도 이에 대응해야 할 필요성이 생겼다.

왜냐하면 사용자가 다크모드를 사용하고 있을 때, 새하얀 앱을 접하게 되면 눈갱이 되기도하고 앱이 최신이 아닌 느낌이 들기도 한다. SaaS 서비스라 그 접근빈도가 어플리케이션 수준으로 높은 웹서비스라면 지원하는 것이 좋을것이다.

이런식의 다크모드 지원이 그냥 매니아들만 쓰는 별난 특징정도로 생각하고 넘어갈 수도 있긴한데.. 그러기엔 너무 OS 자체적으로 잘 지원할 뿐더러 주요앱들이 너도나도 적극적으로 지원하고 있고, MacOS를 넘어 거의 모든 프로그램들이 뛰어들고 있는 느낌이다. 왠지 안하면 뒤쳐지는 느낌이 든다.

다크모드에서는 모든 것이 다크하다

CSS 변수와 미디어쿼리

가장 먼저… 같은 요소에 색상만 다른 것이므로, CSS 변수를 사용한다. 최신의 웹표준 브라우저에서는 prefers-color-scheme 미디어 쿼리를 지원한다. 현시점에서는 사파리 최신버전에서 테스트해볼 수 있다#.

:root {
--bg-color: #fff;
--box-color: rgba(0,0,0,0.05);
--color1: #ccc;
--color2: #999;
--color3: #000;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #000;
--box-color: rgba(255,255,255,0.15);
--color1: #999;
--color2: #ccc;
--color3: #fff;
}
}
OS의 다크모드에 따른 CSS 변수값 변화

사용자의 OS 테마값 설정에 따라 자동으로 화면을 띄워주게 된다.

이 예제에 대한 전체 소스는 이곳에 있다.

익스플로러 fallback

그런데 위와 같은 세련된 방식은 익스플로러와 엣지 저버전에서는 무용지물이다#. 사실 다크모드랑 무관하게 CSS 변수를 이용하는 것에 대한 리스크이다. 따라서 CSS 변수마저 지원하지 않는 브라우저를 위하여 주요 색상에 대해서는 아래와 같이 보완 코드를 작성하여야 한다.

CSS 변수처럼, 필요한 모든 곳에 언급을 해주어야 하기 때문에 SCSS 변수를 활용하였다(SCSS를 안 쓴다면 #fff와같이 값을 바로 입력하여도 무방)

html,body {
width: 100%;
height: 100%;
padding:0;
margin:0;
font-family:sans-serif;
line-height: 1.7rem;
background-color: $bg-color; /* CSS 변수가 안 될 때 */
background-color: var(--bg-color);
color:$color3;
color:var(--color3);
}
.box {
background-color: $box-color; /* CSS 변수가 안 될 때 */
background-color: var(--box-color);
}

예제코드는 이곳에 있다.

자바스크립트와 연동, 최상위 클래스 부여

개인적인 취향이지만 CSS에 관련된 환경들은 항상 자바스크립트와 함께 다루는 것을 좋아한다. 실제로 웹앱을 만들다 보면 현 상황에 따라 스타일 외에 자바스크립트에서 여러 가지 작업들을 분기해야 할 일이 많다. 예를들면 modernizr 를 사용했을 때 CSS에서는 html.touch .. 를 사용하지만 자바스크립트에서는 if (Modernizer.touch) { ... } 를 사용하는 것 등이 있다.

이 밖에도 다른 CSS 셀렉터와 결합하려 할 때 미디어쿼리보다는 class 선택자를 결합하는 것이 편할 때도 많다.

따라서 OS의 다크모드에 따라 최상의 DOM인 html.darkmode 클래스를 토글하는 이벤트를 걸어줄 것이다. OS 테마 상태를 저장할 전역변수를 만들고 그에 따라 클래스를 토글한다.

function userTheme() {
window.__THEME_MODE = 'light';
if (!!window.matchMedia) {
window.__THEME_MODE = window.matchMedia("(prefers-color-scheme: dark)").matches ? 'dark' : 'auto';
}
document.getElementsByTagName('html')[0].classList[window.__THEME_MODE === 'dark' ? 'add' : 'remove']('darkmode');
}
if (!!window.matchMedia) {
['light', 'dark'].forEach(mode => {
window.matchMedia(`(prefers-color-scheme: ${mode})`).addListener(e => {
if(!!e.matches) {
userTheme();
}
});
});
}
userTheme();

위와 같이 해줌으로써 이제 미디어쿼리랑 결합하지 않아도 다른셀렉터들과 함께 html.darkmode 를 이용하여 커스텀이 가능해진다(최상위 DOM에 클래스로 부여하면 바로 아래 설명할 사용자 임의 설정 지원에서도 이점이 생긴다)

/* 미디어 쿼리 없이 일반적인 형식으로 대체 */
html.darkmode:root {
...
}
.btn-primary {
// 라이트모드에서의 속성들
html.darkmode & { /* 다른 셀렉터와 손쉽게 결합 */
// 다크모드에서의 속성 오버라이드
}
}

그리고 이 값을 자바스크립트에서도 편리하게 상황에 따라 사용할 수 있게 되었다.

if (window.__THEME_MODE) {
// 다크모드에서만 실행하는 코드...(뭐가 있지? 여튼..)
}

이에 대한 소스는 이곳에서 확인할 수 있다.

사용자 임의 설정 지원

OS의 테마에 따라 자동으로 인지하는 것은 세련되어 보이지만, 실제 상황을 생각해보면 은근히 불편할 거란 생각이 든다. 왜냐하면 웹페이지의 디자인을 변경하기 위해서 OS의 환경설정을 해야 하는 상황은 웹페이지의 흐름을 벗어나는 일이기도 하며, 환경설정의 접근성이 생각보다 떨어진다.

또한 다크 모드가 최초 디자인 시점부터 고려된 것이 아니라, 이미 만들어진 웹페이지에 억지로 적용했다면, 사용자는 그냥 원래 쓰던 라이트 모드를 사용하고 싶을 수도 있다.

따라서 우리의 웹페이지는 기본적으로 OS의 환경설정값을 따르되, 사용자가 임의로 선택을 할 수 있도록 하는 것이 좋을 것이다. 로컬에 환경을 저장하기 위해서 여러 가지 대안이 있겠지만, 데이터의 중요성을 고려했을 때 localStorage를 사용하는 것이 적절하다고 판단된다(별로 안 중요하니까)

function userTheme(toggle = false) {
let userMode = localStorage.userThemeMode || 'auto';
const osMode = !!window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? 'dark' : 'light';
if(toggle) {
switch(userMode) {
case 'auto':
userMode = 'dark'; break;
case 'dark':
userMode = 'light'; break;
default:
userMode = 'auto';
}
localStorage.userThemeMode = userMode;
}
console.log(`current mode : ${userMode}`);
window.__THEME_MODE = userMode === 'auto' ? osMode : userMode;
document.getElementsByTagName('html')[0].classList[window.__THEME_MODE === 'dark' ? 'add' : 'remove']('darkmode');
}
if (!!window.matchMedia) {
['light', 'dark'].forEach(mode => {
window.matchMedia(`(prefers-color-scheme: ${mode})`).addListener(e => {
if(!!e.matches) {
userTheme();
}
});
});
}
userTheme();

이렇게 구현함으로써, 다크모드를 쓰는 사용자는 웹페이지 진입 시 자연스럽게 다크모드로 볼 수 있고, 필요할 때에는 임의로 다른 모드로 손쉽게 전환도 가능해졌다.

더불어 바로 위의 최상위 클래스와 결합하여 CSS가 아래와 같이 수정된다면, prefers-color-scheme 기능을 지원하지 않는 일반적인 웹 브라우저에서도 다크모드를 지원할 수 있게 된다.

/* prefers-color-scheme 를 지원하는 곳에서만 가능한 코드 */
@media (prefers-color-scheme: dark) {
:root {
...
}
}
/* 유저설정에 의해 클래스가 부여되므로 모든 CSS 변수 지원브라우저에서 가능한 코드 */
html.darkmode:root {
...
}
사용자가 임의로 다크모드를 설정할 수 있다

최종 동작하는 코드는 이곳에서 확인할 수 있다.

fallback과 사용자 임의설정 지원 두 가지를 결합하면, 익스플로러 11버전에서까지도 지원이 가능해진다.


이제 프론트엔드를 디자인할 때, N스크린 대응, 고해상도 이미지(레티나) 대응, 각 브라우저별 대응, 터치/논터치 대응에 이어서 라이트/다크모드 대응 속성이 추가되었다!! 와아~! 신난다!

사실 다크모드에 관련된 코드는 짧고 원리도 간단하다. 문제는 최초 설계시 다크모드를 고려하여 설계하여야하며, 이에 따라 어느정도 디자인에 제약점도 생긴다는 것이다. 또한 React 등에서 Theme Provider를 사용한다면 각 컴포넌트 별로 가상의 계층이 하나 더 생기는 개념이기 때문에 귀찮음이 증가할 수 있겠다.


위와 같은 방식으로 다크모드를 실제 적용한 웹사이트는 https://new.whooing.com 에서 볼 수 있다.

React에서 Hook을 이용하여 다크모드를 제공할 수 있는 방법 : https://github.com/donavon/use-dark-mode

홍구

Written by

홍구

tyle.io(Co-CEO), whooing.com(CEO)

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade