[Tistory] [2024-02-09] 팀프로젝트 개인 회고 – SSG에서 코드블록 무시

원글 페이지 : 바로가기

1. 서버에서 렌더링한 것과 클라이언트에서 렌더링한 결과가 다르다고? 다크모드를 위해 다크모드 버튼을 만들면서 생긴 상황이다. 기존에 리액트에서 구현해서 그대로 사용하면 되지않을까? 했다. “use client”;

import { ComponentProps, useEffect, useState } from “react”;

import Icon from “../Icon”;
import { cn } from “@/utils/cn”;

interface ThemeButton extends ComponentProps<"button"> {}

const ThemeButton = ({ className }: ThemeButton) => {

const isLocalStorageAvailable = typeof localStorage !== “undefined”;
const isBrowser = typeof window !== “undefined”;
const savedDarkMode =
isLocalStorageAvailable && localStorage.getItem(“darkMode”);
const windowDarkMode =
isBrowser && window.matchMedia(“(prefers-color-scheme: dark)”).matches;

const initialDarkMode =
savedDarkMode === null
? windowDarkMode
: savedDarkMode && JSON.parse(savedDarkMode) === “dark”
? true
: false;

const [dark, setDark] = useState(initialDarkMode);

const darkSetButton = () => {
setDark(!dark);
};

useEffect(() => {
if (dark) {
document.documentElement.classList.add(“dark”);
localStorage.setItem(“darkMode”, JSON.stringify(“dark”));
} else {
document.documentElement.classList.remove(“dark”);
localStorage.setItem(“darkMode”, JSON.stringify(“light”));
}
}, [dark]);

return (

);
};

export default ThemeButton; 정말 간단한 코드다 간단하게 설명하면 localStorage에서 darkMode라는 키에 값을 가져와서 이전에 dark였는지 light였는지 확인하고, 값이 없다면 현재 윈도우의 테마가 다크테마인지 라이트테마인지 확인해서 dark라는 state값을 초기화 시켜주는 것이다. 그래서 다크모드버튼을 누를 때마다 setState로 바꿔주고, 해당되는 값을 다시 로컬스토리지에 저장하고, 요소에 “dark”라는 클래스를 추가하거나 제거하는 것이다. dark라는 클래스를 통해 테일윈드로 다크모드일 때의 스타일링을 지정해줄 수 있다. 그런데 문제가 있다! 위 처럼 코드를 작성하고 실행하면 아래와 같은 오류가 뜬다 NextJS를 사용하면서 자주 보게될 것 같은 에러였다. 이 에러를 간단하게 설명하면 현재 서버에서 렌더링하는 Icon컴포넌트의 svg 경로와 클라이언트에서 렌더링하는 Icon 컴포넌트의 svg 경로가 다르다는 것이다. 처음에는 이해가 안갔는데 내가 작성한 코드에서 오류가 있었다. 아무리 상단에 “use client”를 작성했더라 하더라도 서버에서 먼저 한번 요소를 그리고, 그다음에 다시 클라이언트에서 그리는 것이다. 아래 코드를 자세히 보자 const isLocalStorageAvailable = typeof localStorage !== “undefined”;
const isBrowser = typeof window !== “undefined”;

const savedDarkMode =
isLocalStorageAvailable && localStorage.getItem(“darkMode”);
const initialDarkMode =
(savedDarkMode && JSON.parse(savedDarkMode) === “dark”) ||
(isBrowser && window.matchMedia(“(prefers-color-scheme: dark)”).matches)
? true
: false;

const [dark, setDark] = useState(initialDarkMode); 컴포넌트가 호출되었을 때 수행하는 부분이다. 이 문제는 다시 정확한 원리를 알고 수정할 예정 일단 현재 윈도우 환경이 다크모드라고 가정한다.(window.matchMedia의 값이 true임) 이 코드의 흐름을 보면 처음에 서버사이드에서는 dark라는 state가 false다. 왜냐하면 isLocalStorageAvailable과 isBrowser로 인해 서버사이드에서 렌더링 될때 false가 되므로 dark는 initialDarkMode로 false값을 갖는다. 이후 클라이언트사이드에서는 dark라는 state가 true가된다. isLocalStorageAvailable과 isBrowser가 클라이언트 사이드에서는 true값을 갖게된다. 그래서 dark의 initialDarkMode는 true 값을 갖게된다. 오류를 다시 보자 오류를 보면 서버와 클라이언트의 href가 같지않다는 것이다. 그래서 내가 추측한 문제로 인해 생긴 오류라고 판단되었다. 이 문제를 해결하기 위해서는 dark의 initialDarkMode를 서버 사이드와 클라이언트 사이드에서 렌더링 할 때 동일하게 해줘야 한다. 아래 코드로 해결했다. “use client”;

import { ComponentProps, useEffect, useState } from “react”;

import Icon from “../Icon”;
import { cn } from “@/utils/cn”;

interface ThemeButton extends ComponentProps<"button"> {}

const ThemeButton = ({ className }: ThemeButton) => {
const [dark, setDark] = useState(false);

useEffect(() => {
const savedDarkMode = localStorage.getItem(“darkMode”);
const prefersDarkMode = window.matchMedia(
“(prefers-color-scheme: dark)”
).matches;

const initialDarkMode =
savedDarkMode === null
? prefersDarkMode
: JSON.parse(savedDarkMode) === “dark”;

setDark(initialDarkMode);
}, []);

const toggleDarkMode = () => {
setDark((state) => {
const update = !state;
localStorage.setItem(
“darkMode”,
JSON.stringify(update ? “dark” : “light”)
);
return update;
});
};

useEffect(() => {
if (dark) {
document.documentElement.classList.add(“dark”);
} else {
document.documentElement.classList.remove(“dark”);
}
}, [dark]);

return (

);
};

export default ThemeButton; 처음에 dark의 상태를 동적으로 지정하는 게 아닌 initialState를 false로 고정하고, 이후 클라이언트 사이드 렌더링으로 갔을 때 useEffect로 값을 바꿔주는 것이다. 이때 문제는 처음에 화면에 뷰가 떴을 때 false로 고정되어있기 때문에 false 상태의 Icon 컴포넌트가 노출된다. 이 부분은 뒤에서 추가로 설명해보겠다. 2. 새로고침했을 때 클라이언트사이드에서 다크모드를 적용하게되니까 깜빡거려.. 아까 위에서 말했듯이 클라이언트 사이드에서 다크모드 값을 적용하다보니 위와 같이 깜빡임이 생겼다. false값이 서버사이드와 클라이언트 사이드에서 고정되었기 때문에 light 모드에서 -> dark모드로 가는 현상이다. 이 문제를 해결하기 위해서 즉시 실행 함수를 사용해서, 페이지가 로드 될때 즉시 실행함수를 실행시켜주어서 이 깜빡임 현상을 방지시켜줄 것이다. 이를 RootLayout에서 적용한다. import type { Metadata, Viewport } from “next”;
import { Inter } from “next/font/google”;
import “./globals.css”;
import ThemeButton from “./_component/common/ThemeButton”;

const inter = Inter({ subsets: [“latin”] });

export const metadata: Metadata = {
title: “Hands Up”,
description: “중고물품을 실제 경매장처럼 사고 팔 수 있다?!”,
};
export const viewport: Viewport = {
themeColor: “yellow”,
};

export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode; }>) {
const themeInitializerScript = `(function() {
${setInitialColorMode.toString()}
setInitialColorMode();
})()
`;

function setInitialColorMode() {
function getInitialColorMode() {
const savedDarkMode = localStorage.getItem(“darkMode”);
const prefersDarkMode = window.matchMedia(
“(prefers-color-scheme: dark)”
).matches;

const initialDarkMode =
savedDarkMode === null
? prefersDarkMode
: JSON.parse(savedDarkMode) === “dark”;
return initialDarkMode;
}

const currentColorMode = getInitialColorMode();

if (currentColorMode) {
document.documentElement.classList.add(“dark”);
} else {
document.documentElement.classList.remove(“dark”);
}
}

return (