다크모드 적용하기
잘못된 내용이 있으면 댓글을 달아주세요.
nextJS기반으로 내 블로그 만들기 프로젝트를 하면서 꼭 추가하고 싶었던 기능은 다크모드였다. 이유는 아주 간단했다. 내가 대부분의 웹페이지를 사용할때 다크모드를 이용했기 때문이다.
진행중인 개발스택은 다음과 같았다.
- nextJS (12)
- tailwindCss
- typeScript
DarkMode 적용하기
Tailwind config
다크모드를 진행하기 위해, 테일윈드가 지원해주는 방식을 사용할 수 있다. 다른, css-in-js
라이브러리보다 상대적으로 간단히 적용할 수 있다는 생각이 들었다.
// 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
를 가져올 수 있다.