nextJS기반으로 내 블로그 만들기 프로젝트를 하면서 꼭 추가하고 싶었던 기능은 다크모드였다. 이유는 아주 간단히, 내가 대부분의 웹페이지를 사용할때 다크모드를 이용했기 때문이다. 게다가, 요즘에는 모바일로 자기전에 보는 경우가 대다수니..! 다크모드는 꼭 있어야해 했다. 또, tailwind css를 사용하고 있었어서 다크모드를 한번 테스트해보니 아주 쉽게 적용할 수 있었던 것이다. 그래서 금방할 수 있겠지 했다. (실제로 아주 쉽지 않았다..)
진행중인 개발스택은 다음과 같았다.
다크모드를 진행하기 위해, 테일윈드가 지원해주는 방식을 그대로 사용하도록 하자. 다른 css였다면 아마 테마프로바이더 등을 만들어서 main, sub -color...
등을 다르게 해서 내려주는 방식을 사용하는 것 같지만, 테일윈드에서 다크모드는 아주 쉽게 적용할 수 있다.
// tailwind config
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
darkMode: "class",
...
};
이런 방식으로, config
폴더에 darkMode
를 사용할 것이라고 설정하자.
// _document
export default class MyDocument extends Document {
render() {
return (
<Html className='w-[450px] sm:w-full dark'>
// 여기를 보자! 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
모드도 있다는 것이고 우리는 이를 스위칭할 수 있게 해야한다. 여기서 부터 조금 까다롭다.
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을 해줄 수 있다. 참, 사실 여기서 중요한 문제가 등장했었다. 원래 했던 방식은 위와 같은 방식으로 진행하지 않았다. 원래했던 방식은 다음과 같았다. (코드를 지워버렸지만..)
localstroage
의 값이 변경됨이 방식은, 정말 큰 문제가 있었다. 테마를 다크모드로 해놓았을때도 기본적인 light모드가 먼저 실행되고 -> Dom렌더링 후 -> useEffect가 실행되면, 다크도므로 변경되는 로직이 된다.
이런 로직으로 실행되게 되면, 페이지가 전환될때마다 하얀색화면이 잠깐 먼저보이고 다크모드가 적용된다.
아니, 내가 직접 만든 블로그고 이 블로그에 사람들이 와서 보았으면 좋겠다는 마음으로 만드니까 이런 UI가 더 너무너무 거슬렸다. 눈아프고, 안이쁘고 불편했다.
물론 깜빡임의 문제도 없고, 이제 페이지 전환이 잘 되는 것 같지만 페이지가 초기렌더링될때는 로컬스토리지에 저장되어 있는 테마를 불러올 수 없다. _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
를 가져올 수 있다.
깜빡임 없는 다크모드,, 만들었다. 덕분에 _document
를 사용해보기도 했고, useEffect
의 렌더링과정을 다시한번 생각하게 됐다. 아마, react라면 useLayoutEffect
만 해도 되었으려나? 무수한 궁금증이 들긴 하는데, 이번 프로젝트는 next였으니까 여기까지다.