라라벨 Livewire, 다크 모드 기능 구현

포스트 썸네일 이미지

이번 포스트는 라라벨로 만든 웹사이트에 다크 모드 기능을 추가해 보겠다.

다크 모드는 어차피 구현할 계획이라면 초반에 작업해 두는 것이 좋다.

처음부터 고려하지 않고 페이지를 늘려가다 보면, 나중에 다크 모드를 적용할 때 손대야 할 곳이 너무 많아져 꽤나 귀찮아질 수 있기 때문이다.




이전 포스트




전체적인 코드 수정은 이 깃허브 링크를 통해서 확인할 수 있다.





tailwind.config.js 파일 수정


import defaultTheme from 'tailwindcss/defaultTheme';
import forms from '@tailwindcss/forms';

/** @type {import('tailwindcss').Config} */
export default {
    darkMode: 'class', // 이 코드를 추가함

    content: [
        './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
        './storage/framework/views/*.php',
        './resources/views/**/*.blade.php',
    ],

    theme: {
        extend: {
            fontFamily: {
                sans: ['Figtree', ...defaultTheme.fontFamily.sans],
            },
        },
    },

    plugins: [forms],
};

export default { }; 안에 darkMode: 'class'를 추가했다.

이 설정은 tailwindcss가 다크 모드를 어떻게 적용할지 정의하는 부분이다.


darkMode: 'class' 이 코드는 다크 모드가 클래스 기반으로 적용된다는 의미다.

즉, class="dark"라는 클래스를 HTML 요소에 추가할 때, tailwindcss에서 제공하는 다크 모드 스타일이 활성화된다.


예를 들어, class="dark" 속성이 <html> 태그에 추가되면,

다크 모드로 설정된 스타일이 활성화되어 사이트 전체에 다크 모드 테마가 적용된다.

만약 이 설정을 'media'로 변경하면, 브라우저의 시스템 설정(예: 운영체제의 다크 모드)을 기반으로 tailwind가 다크 모드를 자동으로 적용한다.

하지만 'class'를 사용하면 시스템 설정과는 무관하게 HTML에 class="dark"를 추가하거나 제거함으로써 수동으로 다크 모드를 제어할 수 있다.





뷰 파일에 추가할 코드들


welcome.blade.php와 layouts/app.blade.php와 layouts/guest.blade.php, 총 3개의 뷰 파일에 다음의 코드들을 추가한다.

정확한 위치는 위에 링크한 깃허브 링크를 참고하자.




<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" :class="{ 'dark': $store.darkMode.dark }">
    <head>
        <!-- 생략 -->

        <script>
            if (localStorage.getItem('darkMode') === 'true') {
                document.documentElement.classList.add('dark');
            } else {
                document.documentElement.classList.remove('dark');
            }
        </script>

        <!-- 생략 -->
    </head>
    <body>
        <!-- 생략 -->
    </body>
</html>

먼저 <html> 태그에 :class="{ 'dark': $store.darkMode.dark }"> 속성을 추가했다.

이 속성은 Alpine.js의 $store.darkMode.dark의 값이 true면 <html> 태그에 dark 클래스를 붙이고, false면 떼어내는 실시간 바인딩 장치다.


이 코드는 Alpine.js의 store에서 다크 모드 상태를 가져와 <html> 태그의 클래스를 동적으로 설정한다.

초기화 시점에 app.js 파일에서 Alpine.store('darkMode')에서 dark 값을 가져와 다크 모드 여부를 확인하고, 그 값에 따라 <html> 태그에 dark 클래스를 추가하거나 제거하는 역할을 한다.




<head> 태그 안에 넣은 자바스크립트 코드는 웹사이트가 켜지는 첫 0.001초 동안의 화면 깜빡임(Lighthouse 점수와 UX를 갉아먹는 주범)을 원천 차단하는 방패 역할을 한다.


  • if (localStorage.getItem('darkMode') === 'true'): 브라우저의 전역 객체인 window.localStorage를 열어서 'darkMode'의 값이 'true'인지 확인한다.
  • document.documentElement.classList.add('dark');: (참일 때) 만약 기존에 다크모드를 켰던 유저라면, 최상단 <html> 태그에 'dark'라는 클래스를 즉시 추가한다.
  • document.documentElement.classList.remove('dark');: (거짓일 때) 라이트모드 유저거나 처음 방문한 사람이라면, <html> 태그에서 'dark' 클래스를 제거한다.





resources/js/app.js 파일에 코드 추가


// 1. Alpine.js 전역 스토어 설정
Alpine.store('darkMode', {
    dark: localStorage.getItem('darkMode') === 'true',

    toggle() {
        this.dark = ! this.dark;
        localStorage.setItem('darkMode', this.dark);
        this.apply();
    },

    apply() {
        if (this.dark) {
            document.documentElement.classList.add("dark");
        } else {
            document.documentElement.classList.remove("dark");
        }
    }
});

// 2. [핵심] Livewire가 wire:navigate로 페이지를 바꿀 때마다 실행될 이벤트
document.addEventListener('livewire:navigated', () => {
    // 페이지가 바뀔 때마다 로컬스토리지 값을 읽어서 HTML 클래스를 강제로 동기화한다.
    const isDark = localStorage.getItem('darkMode') === 'true';
    if (isDark) {
        document.documentElement.classList.add("dark");
    } else {
        document.documentElement.classList.remove("dark");
    }
});

텅 비어있던 resources/js/app.js 파일에 코드를 추가했다.


주석으로 간단한 설명을 써놓았다.

자세한 설명은 생략하겠다.




livewire/layout/navigation.blade.php 파일 수정


<div class="mx-2">
    <button x-data @click="$store.darkMode.toggle()" class="p-1.5 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300 dark:bg-gray-700 rounded-full">
        <template x-if="$store.darkMode.dark">
            <!-- 다크 모드일 때 아이콘 -->
            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
                <path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
            </svg>
        </template>
        <template x-if="!$store.darkMode.dark">
            <!-- 라이트 모드일 때 아이콘 -->
            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
                <path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />
            </svg>
        </template>
    </button>
</div>

코드를 추가하는 위치는 역시 깃허브 링크를 참고하자.


  • x-data: 버튼 요소에서 Alpine.js의 상태를 관리하는 데 사용된다. 특별한 값 없이 x-data만 사용하면 빈 데이터 스코프를 생성한다.
  • @click="$store.darkMode.toggle()": 사용자가 버튼을 클릭했을 때, Alpine.js의 darkMode 스토어의 toggle() 메서드를 호출한다. 이 메서드는 다크 모드를 활성화하거나 비활성화하는 역할을 한다.
  • <template x-if="$store.darkMode.dark">: 다크 모드 상태일 때 특정 아이콘을 렌더링하는 조건부 렌더링이다.


이 코드들에 대해서는 다음 포스트에서 다시 한번 더 자세히 설명할 예정이다.




아이콘은 heroicons에서 가져왔다.





웹사이트 상단에 다크 모드 버튼이 생겼다

이제 테스트를 해보자.

웹사이트 상단의 내비게이션 바에 다크 모드 버튼이 생겼다.




다크 모드 버튼을 클릭했더니 다크 모드가 되었다

클릭하면 이렇게 다크 모드가 된다.




로그아웃 후에도 계속 다크 모드가 유지된다

페이지를 이동하거나 로그아웃을 한 후에도 다크 모드가 계속 유지되는 것을 확인했다.




여담이지만, 지금 이 화면은 라라벨 11 버전일 때의 라라벨 웹사이트 첫 화면이다.

그런데 분명 지금의 설치된 버전은 13이다.


라라벨 브리즈를 Blade로 설치하면 첫 화면이 라라벨 13 버전으로 유지가 되지만, Livewire로 설치하면 첫 화면이 라라벨 11 버전일 때의 화면으로 바뀌는 현상이 있다.

이것도 일종의 버그인 것인지, 언젠가 해결을 해줄 것인지 모르겠다.




다음 포스트

이 글이 도움이 됐거나 유익했다면 스크롤을 조금만 더 내려서 댓글을 남겨주세요. (비로그인도 가능합니다!)
응원이나 피드백이 담긴 댓글은 제가 계속 블로그를 해나갈 수 있는 원동력이 됩니다. 😊

지인에게 보여주고 싶은 글이었다면 URL을 복사해서 메신저나 소셜 미디어에 공유해 주세요.
이전 포스트 다음 포스트

댓글 쓰기

0 Comments

문의하기 양식