Blog
gatsby블로그에 다크모드 구현 시 화면 깜빡임 해결하기
darkMode
2024.01.11.
목차보기

    개요

    이번 블로그에 다크 모드를 처음 도입해보게 되었다. 쉽게 생각했던 것과 다르게 과정 중 SSR환경으로 화면 깜빡임 해결에 애를 먹엇다. 그래서 과정 정리하면서 화면 깜빡임은 어떻게 해결했는지 작성하고자한다.
    현재 블로그 기술 스택이다. 혹시 참고하실 분들은 아래 스택 확인해보세요 :)
    구현은 Gatsby.js
    스타일은 Emotion

    깜빡임 원인

    develop vs build의 차이점을 먼저 알아보자.

    gatsby docs 요약하자면,

    runtime은 클릭을 통해 상호작용하는 웹브라우저에서 발생하는 프로세스라고 할 수 있다. 자바스크립트 코드는 브라우저와 상호작용할 수 있고, 브라우저가 제공하는 API를 이용할 수 있다. gatsby는 초기 HTML을 로드하고, 브라우저에서 기본적으로 자바스크립트가 운영되는 런타임을 만든다. 반면에 build time은 서버 프로세스를 사용하여 후에 웹브라우저에 전달되는 파일로 사이트를 컴파일하는 과정을 말한다. 그래서 window와 같은 브라우저 API를 이용할 수 없다.

    develop 모드에서 다크모드를 개발할 때는 브라우저 API에 접근할 수 있으니, 깜빡임 없이 구현이 된 것 같다가
    배포 시 첫 페이지에서 테마 초기값으로 렌더링이 되었다가 클릭하여 상호작용(다음페이지 이동 등)이 일어나면서 localStorage에 저장된 값으로 테마를 적용시키게 된다.

    DOM이 구성 되기 전 테마에 대한 값을 읽어와 세팅해주는 코드가 필요하다.

    깜빡임 해결

    gatsby에서는 gatsby-ssr파일에서 gatsby에서 제공하는 onRenderBody API를 이용하여 body 바로 다음 script를 붙여줘서 body 내부 엘리먼트들이 페인팅 되기 전에 실행하도록 한다. 이렇게 하면 스크립트는 DOM 파싱을 막아 스크립트가 실행된 후 페인팅 되는 것이 보장된다.

    onRenderBody APIsetPreBodyComponents함수를 제공하는데 이 함수를 통해 body 바로 밑에 스크립트를 넣어줄 수 있다.

    const scriptElement = [
      createElement('script', {
        dangerouslySetInnerHTML: {
          __html: `
          (() => {
            try {
              preferredTheme = localStorage.getItem('theme');
            } catch (err) {}
    
            // 사용자가 dark모드인지
            let darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
        
            var settingTheme = preferredTheme || (darkQuery.matches ? 'dark' : 'light');
            
            document.documentElement.dataset.theme = settingTheme;
    
            localStorage.setItem('theme', settingTheme);
          })();`,
        },
      }),
    ]
    
    export const onRenderBody = ({ setPreBodyComponents }) => {
      setPreBodyComponents(scriptElement)
    }

    댓글 다크모드 적용 방법

    giscus 기준 적용 방법이다. useEffect 안에서 데이터 초기화할 때 테마 값은 localStorage값을 이용해준다.
    로컬스토리지 변경 됐을 때 giscus 테마도 변경해야하는 부분이 포인트다. 포인트라고 생각한 이유는 내 생각보다 이벤트 등록이 생각대로 적용되지 않았다. event를 강제로 실행시켜봤더니 이벤트 핸들러가 동작했고 giscus iframe에 postMessage 를 전송해 테마 설정을 변경하는 걸로 적용했다.

    const handleChangeStorage = (event: StorageEvent) => {
      if (event.key) {
        const theme = event.key
    
        const iframe = document.querySelector<HTMLIFrameElement>(
          'iframe.giscus-frame',
        )
        iframe?.contentWindow?.postMessage(
          {
            giscus: {
              setConfig: {
                theme: `${theme}_tritanopia`,
              },
            },
          },
          'https://giscus.app',
        )
      }
    }
    //댓글 컴포넌트 파일에서
    useEffect(() => {
      window.addEventListener('storage', handleChangeStorage)
    
      return () => {
        window.removeEventListener('storage', () => {})
      }
    }, [])
    
    //테마 토글 메소드 안에서
    const toggleTheme = () => {
      const newTheme = localStorage.getItem('theme') === 'light' ? 'dark' : 'light'
      localStorage.setItem('theme', newTheme)
      document.documentElement.dataset.theme = newTheme
      window.dispatchEvent(new StorageEvent('storage', { key: newTheme }))
    }

    개발 다 하고 보니 좋다. :3

    Front Developer 김정희 😊

    글 목록보기