dark모드 하면서 있었던 일

otter·2022년 9월 8일
1

nextJS기반으로 내 블로그 만들기 프로젝트를 하면서 꼭 추가하고 싶었던 기능은 다크모드였다. 이유는 아주 간단히, 내가 대부분의 웹페이지를 사용할때 다크모드를 이용했기 때문이다. 게다가, 요즘에는 모바일로 자기전에 보는 경우가 대다수니..! 다크모드는 꼭 있어야해 했다. 또, tailwind css를 사용하고 있었어서 다크모드를 한번 테스트해보니 아주 쉽게 적용할 수 있었던 것이다. 그래서 금방할 수 있겠지 했다. (실제로 아주 쉽지 않았다..)

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

  • nextJS
  • tailwindCss
  • typeScript

Dark만 적용하기

Tailwind config

다크모드를 진행하기 위해, 테일윈드가 지원해주는 방식을 그대로 사용하도록 하자. 다른 css였다면 아마 테마프로바이더 등을 만들어서 main, sub -color... 등을 다르게 해서 내려주는 방식을 사용하는 것 같지만, 테일윈드에서 다크모드는 아주 쉽게 적용할 수 있다.

// 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='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 모드도 있다는 것이고 우리는 이를 스위칭할 수 있게 해야한다. 여기서 부터 조금 까다롭다.


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의 상태가 변경되면, 테마가 스위칭 됨

이 방식은, 정말 큰 문제가 있었다. 테마를 다크모드로 해놓았을때도 기본적인 light모드가 먼저 실행되고 -> Dom렌더링 후 -> useEffect가 실행되면, 다크도므로 변경되는 로직이 된다.
이런 로직으로 실행되게 되면, 페이지가 전환될때마다 하얀색화면이 잠깐 먼저보이고 다크모드가 적용된다.
아니, 내가 직접 만든 블로그고 이 블로그에 사람들이 와서 보았으면 좋겠다는 마음으로 만드니까 이런 UI가 더 너무너무 거슬렸다. 눈아프고, 안이쁘고 불편했다.

물론 깜빡임의 문제도 없고, 이제 페이지 전환이 잘 되는 것 같지만 페이지가 초기렌더링될때는 로컬스토리지에 저장되어 있는 테마를 불러올 수 없다. _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를 가져올 수 있다.

깜빡임 없는 다크모드,, 만들었다. 덕분에 _document를 사용해보기도 했고, useEffect의 렌더링과정을 다시한번 생각하게 됐다. 아마, react라면 useLayoutEffect만 해도 되었으려나? 무수한 궁금증이 들긴 하는데, 이번 프로젝트는 next였으니까 여기까지다.

profile
http://otter-log.world 로 이사했어요!

0개의 댓글