OTTER-LOG

다크모드 적용하기

다크모드 적용하기
by otter2022년 9월 9일에 최종수정되었습니다.
잘못된 내용이 있으면 댓글을 달아주세요.

nextJS기반으로 내 블로그 만들기 프로젝트를 하면서 꼭 추가하고 싶었던 기능은 다크모드였다. 사용자의 편의성에도 좋을 것이라고 생각했고, 우선 내가 모든 웹사이트를 다크모드로 사용하고 있었기 때문이다.

진행중인 개발스택은 다음과 같았다.

  • nextJS (12)
  • tailwindCss
  • typeScript

DarkMode 적용하기

Tailwind config 설정하기

우선, tailw

// tailwind config module.exports = { content: [ "./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}", ], darkMode: "class", ... };

이런 방식으로, config 폴더에 darkMode를 사용할 것이라고 설정하자.

document

// _document export default class MyDocument extends Document { render() { return ( <Html className='dark w-[450px] sm:w-full'> // 여기를 보자! calssName에, dark를 적어주고 <Head> <link href='/static/favicon.ico' rel='shortcut icon' /> </Head> <body className='w-full dark:bg-black dark:text-white'> // 하위 클래스에 dark:테일윈드 스타일코드로 dark모드를 적용할 수 있다. <script dangerouslySetInnerHTML={{ __html: setThemeMode }} /> <Main /> <NextScript /> </body> </Html> ); } }

_document 는, nextjs가 렌더링되기 이전 전역에 사용할 수 있는 정적인 설정들을 커스텀으로 설정할 수 있도록 도와주는 파일이다. 여기서 html 태그에 직접적으로 접근해서, dark 를 클래스에 적어주고, dark 모드를 사용할때에 바꿀 부분을 dark:bg-black 등으로 적어주자.

이런 방식으로, 아주 간단하게 dark 모드를 적용할 수 있다. 그런데, dark모드라면, light 모드도 있다는 것이고 우리는 이를 스위칭할 수 있게 해야한다. 여기서 부터 조금 까다롭다.


darkmode switching

dark모드를 light모드와 스위칭할 수 있도록 해보자. 그런데, 생각을 해보자. 뭔가 단순히 스테이트에 dark모드의 true, false만을 등록해두면 해당 컴포넌트가 재실행되거나 페이지가 변할 때에 스테이트를 잃을 수 있다. 쉽게 말해 다른페이지로 이동할때 다크모드의 설정값을 잃게 될 수 있다. 그러면, 쉽게 로컬스토리지를 사용할 수 있지 않을까? 대부분 로컬 스토리지를 사용하는 것 같기도 하고, 로컬 스토리지를 사용해보자.

const Nav = () => { const [isOpen, toggleIsOpen] = useState(false); const [themeIsDark, setThemeIsDark] = useState(false); const themeModeHandle = (e: React.MouseEvent) => { e.preventDefault(); localStorage.theme = localStorage.theme === "dark" ? "light" : "dark"; document.documentElement.classList.toggle("dark"); setThemeIsDark(!themeIsDark); }; useEffect(() => { if (localStorage.theme === "dark") { setThemeIsDark(true); } else { setThemeIsDark(false); } }, []); return ( <nav className='flex space-x-2 md:space-x-3'> <NavLists isOpen={isOpen} /> <button className='rounded-xl bg-black px-2 py-1 text-xl font-semibold text-white dark:bg-white dark:text-black' onClick={themeModeHandle} > {themeIsDark ? "DARK" : "DARK"} </button> .... )}

이런식으로, swtiching을 해줄 수 있다. 참, 사실 여기서 중요한 문제가 등장했었다. 원래 했던 방식은 위와 같은 방식으로 진행하지 않았다. 원래했던 방식은 다음과 같았다. (코드를 지워버렸지만..)

  • useState로 상태가 변경됨
  • useState의 상태가 변경되면, localstroage의 값이 변경됨
  • useState의 상태가 변경되면, useEffect 로 테마가 스위칭 됨

이 방식은, 정말 큰 문제가 있었다. 테마를 다크모드로 해놓았을때도 기본적인 light모드가 먼저 실행되고 -> Dom렌더링 후 -> useEffect가 실행되면, 다크도므로 변경되는 로직이 된다. 이런 로직으로 실행되게 되면, 페이지가 전환될때마다 하얀색화면이 잠깐 먼저보이고 다크모드가 적용되었다.

물론 깜빡임의 문제도 없고, 이제 페이지 전환이 잘 되는 것 같지만 페이지가 초기렌더링될때는 로컬스토리지에 저장되어 있는 테마를 불러올 수 없다. _document를 조금 수정해주자.

document에서 초기설정 불러오기

// _document import Document, { Head, Html, Main, NextScript } from "next/document"; export default class MyDocument extends Document { render() { const setThemeMode = ` if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { document.documentElement.classList.add('dark') } else { document.documentElement.classList.remove('dark') } `; return ( <Html className='w-[450px] sm:w-full'> <Head> <link href='/static/favicon.ico' rel='shortcut icon' /> </Head> <body className='w-full dark:bg-black dark:text-white'> <script dangerouslySetInnerHTML={{ __html: setThemeMode }} /> // 이 스크립트를 실행시킨다. 렌더링되기전에 이부분을 먼저 불러온다. <Main /> <NextScript /> </body> </Html> ); } }

이렇게, _document파일을 이용해서 렌더링되기전에 먼저 localstroage를 가져올 수 있다.