지난편 실패담🤦🏻 요약.

  • 리액트와 개츠비의 Hydration 특징 때문에 오류가 나는데...
  • 리렌더링으로 억지로 해결은 가능했지만...
  • 깜박임은 신경쓰이고, 리렌더링도 하고싶지 않은데...

이번 포스팅은 위의 문제를 해결한 성공담🙆🏻이다!

BBAK

어우우우우 해결해서 속시원하니 열심히 작성해볼게여

배포한 결과물에 문제가 생기는 것이므로, gatsby가 bulild하는 과정을 자세히 살펴봐야 할 필요가 있을 것 같다.

gatsby의 build

gatsby공식페이지의 Docs의 Overview of the Gatsby Build Process에 자세하게 설명 되어 있다.
해당 문서에서 테마를 만들면서 직접적으로 문제가 발생한 부분을 가져와 기록하려 한다.

develop vs build

우선 Docs에선 build timeruntime에 대해 이렇게 설명한다.

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

develop시엔 runtime이 작동하므로 바로바로 브라우저 API에 접근이 가능하다. 그러나 build time이 작동되는 build시점에는 브라우저 API에 접근하지 못하는 것이다.

그러므로, 배포된 페이지 처음 빌드되는 시점의 첫 페이지는 기본으로 설정된 light 모드였다가, 클릭하여 다음 페이지로 넘어가면서(브라우저와 상호작용하면서) localStorage에 저장된 값을 읽어와서 테마를 적용시키게 되는 것이다.

결국 이 특징은 정적페이지의 특징 때문이기도 하다. 대부분 비슷하지만 develop할 때랑은 다르게, (server-side rendering (SSR)을 진행하려면) build 시점에 미리 모든 정보들을 포함시켜놓으려 하고 이 시점에는 localStorage에 접근하지 못하고 후에 hydration하게 되는 것이다.

렌더링하고 hydrate되는 것이 아닌, 렌더링이 완료되기 전에 테마 클래스가 결정이 되어야 한다.

해결책은?

즉 HTML이 만들어지기 전에(DOM트리가 완성되기 전에) 테마를 결정짓는 클래스를 미리 붙여줘야 한다.
gatsby는 Customizing HTML기능을 통해 이를 해결할 수 있다. src 디렉토리안에 html.js 파일을 만들어 스크립트를 추가하면 HTML을 필요한대로 설정할 수 있다. (혹은 gatsby-ssr.js에 설정할 수 있다.)

🎯 테마의 주요 기능

  • 어디서든지 접근 가능한 Header에 있는 스위치로 변경
  • 댓글 위젯의 테마도 변경
  • light 모드, dark 모드 두가지
  • 재방문시 테마가 유지
  • 새로고침시에도 유지(깜박임도 없어야함)
  • 사용자 시스템의 테마 반영

Customizing HTML

gatsby 공식문서 Customizing HTML를 참고하였다.

아래 명령어를 실행하여 src에 html.js 파일을 만든다.

cp .cache/default-html.js src/html.js

테마 설정을 위해선, <body>태그 속 <script>를 사용하여 미리 실행되어야 할 커스텀 스크립트를 추가한다. 자바스크립트는 파싱을 중단시키므로 보통(렌더링이 끝난 후에 작동시키기 위해) </body>태그가 닫히기 직전에 붙여주는 것이 일반적이다. 그러나 여기선 테마를 결정하고, <body>에 클래스를 붙이고, 다시 렌더링을 해야한다. 미리 <body>태그의 클래스로 테마를 정하고 파싱할 것이므로 <body>태그 시작 초기에 <script>를 삽입할 것이다.

src/html.js
...
// 기본 class는 light로 설정
<body {...props.bodyAttributes} className="light">
  <script
    dangerouslySetInnerHTML={{
      __html: `
              // 처음 테마 결정하는 코드 삽입!
          `,
    }}
  />
...

처음 테마를 결정하는 커스텀 스크립트를 작성해야한다. 조건 중 재방문시 테마가 유지, 운영체제의 테마를 따르기 등의 조건을 여기서 결정한다. overreacted.io의 코드를 참고했다.

  1. 즉시 실행 함수(IIFE)로 작성하여 바로 실행되도록 한다.
  2. 나중에 사용할 window객체들은 __를 붙여 작성한다.

    • window.__theme: 전역 변수에 테마 저장
    • window.__setPreferredTheme: 전역 변수에 테마를 설정하는 함수 저장
  3. localStorage로부터 테마를 가져온다.
  4. <body>태그에 class로 테마를 등록한다.
  5. matchMedia로 사용자의 시스템 설정을 가져온다.

    • window.matchMedia('(prefers-color-scheme: dark)')는 다크모드인지 아닌지가 boolean 값 리턴
  6. 테마를 설정하는 함수 setTheme를 콜백함수를 포함하여 작성한다.
src/html.js
...
// 1. 즉시 실행 함수
(() => {
  // localStorage 확인가능하면 할당
  let preferredTheme;
  try {
    preferredTheme = localStorage.getItem('theme');
  } catch (err) {}

  // 테마를 설정하는 함수
  // window.__theme에 할당
  // body의 class로 등록
  const setTheme = (newTheme) => {
    window.__theme = newTheme;
    preferredTheme = newTheme;
    document.body.className = newTheme;
  }

  // 테마를 설정하는 window객체
  window.__setPreferredTheme = (newTheme) => {
    // 테마를 설정하는 setTheme함수 실행
    setTheme(newTheme);
    // 여기서 localStorage에 등록
    try {
      localStorage.setItem('theme', newTheme);
    } catch (err) {}
  }

  // 미디어쿼리가 dark모드로 설정되어있는지 확인
  let darkQuery = window.matchMedia('(prefers-color-scheme: dark)');

  // 미디어쿼리 변경시 작동할 콜백함수 설정
  darkQuery.addListener(function (e) {
    window.__setPreferredTheme(e.matches ? 'dark' : 'light');
  });

  // localStorage 테마값이 없으면 미디어쿼리 설정 따르기
  window.__setPreferredTheme(preferredTheme || (darkQuery.matches ? 'dark' : 'light'));
})();
...

만약 gatsby-ssr.js에 등록을 하고 싶다면, Gatsby Server Rendering APIs를 참고하여 작성할 수 있다.

gatsby-ssr.js
export const onRenderBody = ({ setPreBodyComponents }) => {
  setPreBodyComponents([
    React.createElement('script', {
      dangerouslySetInnerHTML: {
        __html: `
          // 커스텀 스크립트
        `,
      },
    }),
  ])
}

Context 만들기

기존에 작성해둔 context와 크게 차이가 없다. 다만, 그 전에는 state를 isDarkMode라는 이름으로 true/false 값으로 관리했는데, 이번에는 테마값과 적용되는 클래스 값이 같으므로 theme이라는 state에 light/dark 값으로 수정하였다.

src/store/ThemeContext.js
import React, { createContext } from 'react';
import GlobalState from './GlobalState';

const defaultState = {
  theme: 'light',
};

const ThemeContext = createContext(defaultState);

const ThemeProvider = ({ children }) => {
  return (
    <ThemeContext.Provider value={GlobalState()}>
      {children}
    </ThemeContext.Provider>
  );
};

export default ThemeContext;

export { ThemeProvider };

GlobalState()

마찬가지로 기존 작성해둔 코드를 수정한다.

  1. TOGGLE_MODE는 테마를 바꾸는 스위치에 적용될 액션이다.

    • action일때, state값을 반대로 변경
    • window.__setPreferredTheme()을 이용하여 <body>클래스와 localStorage 값을 변경
  2. 빌드시 발생하는 window is not defined 오류를 방지
src/store/ThemeContext.js
import { useReducer } from 'react';

const reducer = (state, action) => {
  switch (action.type) {
    case 'TOGGLE_MODE':
      const toggled = window.__theme === 'dark' ? 'light' : 'dark';
      window.__setPreferredTheme(toggled);
      return {
        theme: toggled,
      };

    default: {
      return state;
    }
  }
};

const GlobalState = () => {
  let getThemefromWeb = () => {
    if (typeof window !== 'undefined') {
      return {
        theme: window.__theme,
      };
    } else {
      return { theme: 'light' };
    }
  };
  const [state, dispatch] = useReducer(reducer, getThemefromWeb());

  return { state, dispatch };
};

export default GlobalState;

기타 컴포넌트 수정

기존에는 레이아웃 전체를 감싸는 최상단 <div>에 클래스를 붙였지만, 이젠 더 상단인 <body>에서 클래스를 관리한다. 그래서 layout내에서 클래스를 관리하던 useContext부분은 삭제하였다. 또한 기존에 Context를 사용하던 Header와 Comment 컴포넌트에서는 state.isDarkMode에서 state.theme으로만 변경해주었다.

마무리

사실 플러그인을 사용하면 스위치부터 테마변경까지 모두 간단하게 붙일 수 있다. 시간은 비교적 오래걸렸으나 이렇게 직접 만들어보면서 많은 고민을 할 수 있었다. 리액트의 hydrate와 정적사이트의 빌드, 웹의 렌더링 등 좀 더 웹의 기본적인 것들에 대해 고민해 볼 수 있었다. 테마 하나였지만 많은 것을 공부하고, 파고들었던 기회였다.

CSS custom properties를 이용한 테마 디자인은 블로그 디자인🎨에서...

Reference & Learn More

Gatsby docs
Gatsby docs
Gatsby docs
Implementing dark mode in React
Toggle dark/light theme based on your user's preferred scheme
🌙 How I set Dark Mode for Gatsby website

Comments