[javascript] 다크모드 토글 기능 구현과 다크모드 토글 디자인 구현

웹페이지, 또는 사이트에 적용하는 다크 모드는 구현하는 방법이 여러 가지 있습니다.

CSS 속성을 기준으로 구분하면 사용된 배경색과 전경(글자) 색들을 바꾸기만 하는 간단한 구현이기 때문에 어떤 방법을 사용해도 무방합니다.

CSS 클래스로 구현

다크모드 구현은 다크 모드용 컬러 CSS 클래스를 정의한 후 자바스크립트로 <html>, 또는 <body> 태그에 다크 모드용 클래스를 추가하거나 삭제하는 방식으로 구현합니다.

다음 자바스크립트 코드는 'toggle" ID를 가진 요소를 클릭하면 '.dark-mode" 클래스를 <body> 태그에 추가하거나 삭제하는 간단한 이벤트 핸들러 입다.

구현하기에 따라서는 ".light-mode", ".dark-mode" 2개의 클래스로 적용하는 테마 컬러를 전환하는 클래스를 각각 구현할 수도 있습니다.

document.getElementById('toggle').addEventListener("click", function() {
    if(document.querySelector('body').classList.contains('dark-mode')){
        document.body.classList.remove("dark-mode");
    }else{
        document.body.classList.add("dark-mode");
    }
},false);

dataset으로 구현

기본적으로는 <html> 태그, 또는 <body> 태그에 다크 모드를 표시하는 클래스를 추가해서 표시를 하는 방법과 같습니다. 다만 그 구분 표시를 클래스로 하는 것이 아니라 "data-" 속성으로 해서 클래스와는 별도로 구분되도록 함으로써 관리를 조금 더 쉽게 하는 정도의 차이가 있습니다.

<body data-theme="dark">
 ...
</body>

"data-" 속성 값을 사용한 다크 모드 제어는 CSS로도 할 수 있고 자바스크립트로도 할 수 있습니다. 결과적으로는 "data-" 속성 값으로 다크 모드를 구분할 뿐 코딩이 더 간결해지거나 하는 것은 딱히 없습니다.

CSS로 구현할 때는 다음처럼 "data=" 속성으로 모드를 구분해서 컬러 속성을 정의합니다.

body {
    color: #000;
    background-color: #fff;
}

body[data-theme='dark'] {
    color: #fff ;
    background-color: #000;
}

자바스크립트로 다크모드를 구현할 때는 "data-" 속성 값을 읽어서 추가로 정의한 다크모드를 위한 CSS 클래스를 추가해야 합니다.

if(document.querySelector('body').dataset.theme == 'dark'){
    document.body.classList.add("dark-mode");
}

이 방식은 대표적으로 네이버에서 사용하는 방식입니다.

네이버 사이트 오른쪽 하단의 다크모드 보기 버튼을 눌러 다크모드를 켜면 <html> 태그에 data-dark="true" 속성이 추가되면서 다크모드 CSS가 적용됩니다.

구조적으로 명확하게 다크 모드가 명시되고 호환성 문제도 없기 때문에 다크 모드를 웹사이트에 적용한다면 이 방법을 가장 추천합니다.

미디어 쿼리로 구현

최신의 다크 모드 구현 방법이고, 가장 세련된 구현 방법이지만 거의 사용하지 않습니다.

웹 브라우저의 다크 모드, 또는 운영체제의 다크 모드와 연동되어 다크모드가 실행되기 때문에 대상 기기의 사용 환경과 일체화 되어 동작하는 장점이 있지만, 내 웹사이트를 방문한 이용자가 선택에 따라 다크모드를 껏다 켰다 할 수 있도록 하려면 앞서의 다크모드 적용 방법들을 추가로 구현해야 하기 때문에 결국 앞서의 다크모드 구현 방법을 주로 사용하는 추세입니다.

컬러 스키마 미디어 쿼리는 다음과 같이 작성합니다.

스키마 키워드는 "dark", "light" 2가지가 사용 가능하고, 2가지 미디어 쿼리를 모두 정의한 경우, 둘 중 한 가지는 반드시 실행됩니다. 기본 적용으로 "light" 컬러 스키마 미디어 쿼리가 적용된다는 뜻입니다.

body 태그에 기본 적용된 배경색은 빨간색이지만 "light" 미디어 쿼리가 기본 적용되기 때문에 최종적으로는 흰색 배경으로 적용이 됩니다.

주의해야 합니다.

body {
    background-color: red;
}          

@media (prefers-color-scheme: dark) {
    body{
        background-color: black;
    }
}
@media (prefers-color-scheme: light) {
    body{
        background-color: white;
    }
}

앞의 예에서 "prefers-color-scheme: dark"에 해당하는 미디어 쿼리가 컬러 스키마 미디어 쿼리입니다.

다크 모드, 또는 라이트 모드가 적용될 때 웹페이지에 적용하는 컬러 속성들을 모아서 정의하게 됩니다.

사용방법은 앞의 예에서 보듯이 기존에 알던 미디어 쿼리 사용방법 그대로입니다.

그리고 Caniuse.com에서 호환성을 확인해보면 인터넷 익스플로러를 제외한 나머지 브라우저들은 컬러 스키마 미디어 쿼리를 잘 지원합니다.

컬러 스키마 미디어 쿼리 안에 변수를 사용할 수 있기 때문에 다음처럼 변수로 필요한 컬러 값을 변수로 설정하면 조금 더 유연하게 다크 모드를 위한 컬러 값 관리를 할 수도 있습니다.

:root{
    --bg-color: red;
}
body {
    background-color: var(--bg-color);
}          

@media (prefers-color-scheme: dark) {
    :root{
        --bg-color: black;
    }
}
@media (prefers-color-scheme: light) {
    :root{
        --bg-color: white;
    }
}

윈도우10/11, 맥OS, 안드로이드, iOS 그리고 모바일용 크롬 웹 브라우저에는 설정 항목에 다크 모드를 켜는 기능이 있고, 다크 모드 옵션 항목을 켜면 컬러 스키마 미디어 쿼리가 잘 동작합니다.

다만 데스크탑용 크롬 브라우저에는 아직 다크 모드를 켜서 컬러 스키마 미디어 쿼리 CSS를 적용하는 기능이 없습니다.

크롬 확장으로 제공되는 다크 테마, 또는 실험실 기능의 강제 다크 모드 기능은 컬러 스키마 미디어 쿼리가 적용되는 기능이 아니며, 크롬 자체적으로 구현된 기능입니다.

주의해야 합니다.

크롬 실험실 기능의 강제 다크모드는 컬러 스키마 미디어쿼리 CSS를 동작시키는 기능이 아니고, 웹페이지를 강제 반전시켜주는 기능입니다.

크롬 개발자 도구로 컬러 스키마 미디어 쿼리를 강제 적용해보기

컬러 스키마 미디어 쿼리를 작성했으면 테스트를 해볼 수 있어야 합니다.

크롬에는 개발자 도구에서 컬러 스키마 디디어 쿼리를 강제 적용해볼 수 있는 기능을 제공합니다.

개발자 도구(F12)를 연후 Ctrl + Shift + P 키를 눌러서 명령어 검색기를 엽니다. (맥 OS는 Cmd + Shift + P)

"렌더링 표시"를 검색해서 선택합니다. 영문 개발자 모드로 사용할 경우 "Show Rendering"입니다.

개발자 도구에 "렌더링" 탭이 추가됩니다.

렌더링 탭의 항목들을 스크롤해서 내려가다 보면 중간쯤에 "CSS 미디어 기능 prefers-color-scheme 에뮬레이션" 항목이 있습니다.

선택 목록에서 라이트 모드(light), 또는 다크 모드(dark)를 선택해서 라이트 모드와 다크 모드를 강제로 적용할 수 있습니다.

예를 들어 CSS에 다음과 같이 다크 모드 컬러 스키마 미디어 쿼리를 적용했으면, 기본 상태에서는 배경색이 흰색으로 보이지만 개발자 도구에서 "perefers-color-scheme: dark"를 선택하면 배경색이 검정색으로 변합니다.

body {
    background-color: #fff;
}          

@media (prefers-color-scheme: dark) {
    body{
        background-color: #000;
    }
}

개발자도구에서 확인하면 <body> 태그에 컬러 스키마 다크 모드 CSS 가 작용된 것을 확인할 수 있습니다.

prefers-color-scheme를 선택하면 &amp;amp;amp;lt;body&amp;amp;amp;gt; 태그에 컬러스키마 다크모드가 적용되는 것(오른쪽 상단)을 확인할 수 있습니다.

윈도우10/11 다크 모드 설정 변경으로 컬러 스키마 미디어 쿼리 CSS 적용하기

윈도우10/11 설정에서 다크 모드로 변경하면 즉시 컬러 스키마 미디어 쿼리가 적용됩니다.

윈도우10/11 시작 > 설정 > 개인설정(Personalization) > 색으로 들어가서 색 선택을 "어둡게"로 선택합니다.

기본 앱 모드가 따로 표시될 경우 기본 앱 모드를 "어둡게"로 변경해야 합니다.

운영체제에서 다크 모드가 적용되면 구글 크롬도 다크 모드로 동작하며, 웹사이트의 CSS에 작성한 컬러 스키마 미디어쿼리중 "dark" 로 표시한 미디어쿼리가 적용됩니다.

자바스크립트로 다크모드 CSS 클래스를 적용하는 위치 알기

웹페이지 자체에 다크모드 토글 기능을 구현하려면 HTML 요소 어딘가에는 다크모드 컬러를 적용하는 클래스를 추가해야 합니다.

전역으로 클래스를 적용해야 전체 웹 페이지에 다크 모드를 적용할 수 있으므로 <html>, 또는 <body> 태그에 CSS 클래스를 추가하는 것이 일반적입니다.

<html> 태그, <body> 태그 어느 쪽에 사용해도 무방합니다. 취향의 차이 정도라고 생각하면 됩니다.

DOM안의 요소에 적용할 때는 쿼리 셀렉터로 태그를 선택해서 작성하면 되지만 <html>, <body> 태그는 document 객체에서 직접 접근하게 됩니다.

자바스크립트로 CSS 클래스를 추가할 때 <html> 태그에 클래스를 추가하려면 다음과 같이 작성합니다.

documentElement는 <html> 태그를 가리킵니다.

document.documentElement.classList.add("dark-mode");

<body> 태그는 다음 방식으로 접근합니다.

document.body.classList.add("dark-mode");

다크 모드 토글 버튼 만들기

체크박스를 이용해 만드는 기초적인 토글 버튼에 대한 구현 방법은 다음 글을 읽어보면 도움이 됩니다.

> 체크박스와 라디오버튼 디자인 기초

여기서는 조금 더 보기 좋게 아이콘을 사용해 다크 모드 토글 버튼을 만들어봅니다.

라디오 버튼 2개를 이용해 클릭하는 데 따라 나이트 모드와 라이트 모드가 선택되는 방식으로 만들었으며, 아이콘은 폰트어썸 프리 버전을 이용했습니다.

완성된 아이콘 HTML과 CSS 코드는 다음과 같습니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>DarkMode</title>
	<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.0/css/all.css">
    <style>

        #toggle{
            display: inline-flex;
            padding: 5px;
            border-radius: 1.5em;
            background-color: #82d8ff;
        }
        #toggle i{
            font-size: 1.5em;
            padding: 5px;
            background-color: #fff;
            color: #666;
        }
        #light-mode{
            border-radius: 50% 0 0 50%;
        }
        #dark-mode{
            border-radius: 0 50% 50% 0;
        }
        #toggle input{
            display: none;
        }
        #toggle input[type=radio]:checked + label > i {
            background-color: #82d8ff;
            color: #fff;
            transition: all 0.3s ease-in-out;
        }
    </style>
</head>
<body>
    <div id="toggle">
        <input type="radio" name="toggle" id="toggle-radio-light" checked><label for="toggle-radio-light"><i id="light-mode" class="fas fa-sun"></i></label>
        <input type="radio" name="toggle" id="toggle-radio-dark"><label for="toggle-radio-dark"><i id="dark-mode" class="fas fa-moon"></i></label>
    </div>
</body>
</html>

이제 완성된 다크 모드 토글에 자바스크립트로 클릭 이벤트 핸들러를 붙이고, 자바스크립트로 <body> 태그에 "dark-mode" 클래스를 추가하는 기능을 부여합니다.

당연히 "dark-mode" 클래스는 배경색과 글자색을 다크 모드에 맞춰 변경하는 CSS가 들어가게 됩니다.

document.addEventListener('DOMContentLoaded', ()=>{
    document.getElementById('toggle').addEventListener("click", e=>{
        if(e.target.id == 'dark-mode'){
            document.body.classList.add("dark-mode");
        }else if(e.target.id == 'light-mode'){
            document.body.classList.remove("dark-mode");
        }
    },false);        
})
다크모드 토글 클릭에 따라 &lt;body&gt; 태그에 dark-mode 클래스가 추가됩니다.

다크 모드 사용자 선택을 로컬 정보로 저장하기

사용자가 선택한 다크모드 값은 쿠키로 저장할 수도 있고 로컬 스토리지에 저장할 수도 있습니다.

여기서는 쿠키보다 조금 더 구현이 간편한 로컬 스토리지에 저장하는 방법을 소개합니다.

로컬 스토리지는 전역으로 접근할 수 있는 "localStorage" 객체가 제공되므로 이 객체의 메서드(getItem(), setItem())로 로컬 스토리지에 다크 모드 값을 저장하고 가져올 수 있습니다.

document.getElementById('toggle').addEventListener("click", ()=>{ //다크모드 토글 클릭 발생
    let colorMode = localStorage.getItem("colorMode");

    if(colorMode == 'dark'){ // 현재 값이 다크모드이면
        localStorage.setItem("colorMode", "light");
        document.body.classList.remove("dark-mode");
    }else{
        localStorage.setItem("colorMode", "dark");
        document.body.classList.add("dark-mode");
    }
},false);

주/야간 시간대에 따라 다크 모드 자동 전환하기

예상했듯이 자바스크립트로 시간 체크를 해서 주간 시간대면 light-mode 컬러 스키마를 야간 시간대면 dark-mode 컬러 스키마를 적용하면 됩니다.

시간 값만 얻을 수 있으면 나머지는 앞서 구현한 자바스크립트 코드로 CSS 클래스를 적용하는 방법을 그대로 사용하면 됩니다.

const hour = +new Date().toLocaleTimeString([],{ hour: '2-digit', hour12: false });//24시간 시간 값만 얻음
if (7 <= hour && hour < 18) { // 오전 7이 이상, 저녁 6시 미만
    document.body.classList.add("light-mode");
} else {
    document.body.classList.add("dark-mode");
}

다크모드 구현 완성

배운 내용들을 기초로 다크모드 동작 웹페이지를 제작하면 다음과 같이 동작하는 기능을 구현할 수 있습니다.

구현한 샘플 HTML 코드는 다음과 같습니다.

HTML 파일로 저장해서 동작하는 모습을 확인할 수 있습니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>다크모드</title>
	<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.0/css/all.css">

    <style>
        html, body{
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0;
        }
        .wrap{
            width: 100%;
            height: 100%;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        body[data-darkmode=on] {
            background-color: #1e1f21;
            color: #e8e8e8 !important;
        }
        /* Darkmode Toggle */
        body[data-darkmode=on] .darkmode > .inner{
            background-color: rgba(255,255,255,0.25);
        }
        .darkmode > .inner{
            position: relative;
            display: inline-flex;
            padding: 5px;
            border-radius: 1.5em;
            background-color: rgba(0,0,0,0.1);
        }
        .darkmode label {
            cursor: pointer;
        }
        .darkmode label:first-of-type{
            padding: 5px 5px 5px 10px;
            border-radius: 50% 0 0 50%;
        }
        .darkmode label:last-of-type{
            padding: 5px 10px 5px 5px;
            border-radius: 0 50% 50% 0;
        }
        .darkmode i{
            font-size: 1.5em;
            color: #aaa;
        }
        .darkmode input[type=radio]{
            display: none;
        }
        .darkmode input[type=radio]:checked + label > i {
            color: #fff;
            transition: all 0.35s ease-in-out;
        }
        .darkmode .darkmode-bg{
            width: 39px;
            height: 34px;
            position: absolute;
            left: 5px;
            border-radius: 50px 15px 15px 50px;
            z-index: -1;
            transition: all 0.35s ease-in-out;
            background-color: #03a9f4;
        }
        #toggle-radio-dark:checked ~ .darkmode-bg{
            border-radius: 15px 50px 50px 15px;
            top: 5px;
            left: 44px;
        }
    </style>
    <script>
        document.addEventListener('DOMContentLoaded', function(){
            //다크모드 토글
            if(document.querySelector('.darkmode')){
                if(localStorage.getItem("darkmode") == 'on'){
                    //다크모드 켜기
                    document.body.dataset.darkmode='on';
                    document.querySelector('#toggle-radio-dark').checked = true;
                }
                //다크모드 이벤트 핸들러
                document.querySelector('.darkmode').addEventListener("click", e=>{
                    if(e.target.classList.contains('todark')){
                        document.body.dataset.darkmode='on';
                        localStorage.setItem("darkmode", "on");
                    }else if(e.target.classList.contains('tolight')){
                        document.body.dataset.darkmode='off';
                        localStorage.setItem("darkmode", "off");
                    }
                },false);
            }else{
                localStorage.removeItem("darkmode");
            }

        })
    </script>
</head>
<body>
    <div class="wrap">
        <div class="darkmode">
            <div class="inner">
                <input type="radio" name="toggle" id="toggle-radio-light" checked><label for="toggle-radio-light" class="tolight"><i class="fas fa-sun tolight"></i></label>
                <input type="radio" name="toggle" id="toggle-radio-dark"><label for="toggle-radio-dark" class="todark"><i class="fas fa-moon todark"></i></label>
                <div class="darkmode-bg"></div>
            </div>
        </div>
    </div>
</body>
</html>