✏️ Next.js

[에러 해결] Warning: Prop className did not match.

category
✏️ Next.js
date
May 21, 2023
thumbnail
https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fd51dd65-0f37-4c5b-ac16-9e6f676190f0/Untitled.png
slug
next-js-warning-prop-className-did-not-match
author
status
Public
tags
⚠️ error
summary
type
Post
 

문제 상황

Next13 버전에서 다크모드와 라이트모드에 따라 해당 모드를 변경하는 버튼을 포함한 코드에서 오류가 발생했다.
notion image
 
 

문제 발생 코드

import { useState } from 'react';

export default function Layout() {
	const [theme, setTheme] = useState(() =>
	    typeof window !== 'undefined'
	      ? localStorage.getItem('theme') === 'dark'
	        ? 'dark'
	        : 'light'
	      : 'light'
	  );
	
	const handleClick = () => {
	  const theme = localStorage.getItem('theme');
	  if (theme === 'dark') {
	    localStorage.setItem('theme', 'light');
	    setTheme('light');
	  } else {
	    localStorage.setItem('theme', 'dark');
	    setTheme('dark');
	  }
	};
	
	return(
		<button className="px-2" onClick={handleClick}>
		  {theme === 'dark' ? (
		    <Image
		      width={20}
		      height={20}
		      src={'/light-mode.svg'}
		      alt="light"
		    />
		  ) : (
		    <Image width={20} height={20} src={'/dark-mode.svg'} alt="dark" />
		  )}
		</button>
	);
}
 
 

원인 분석

문제는 localStorage를 사용하여 클라이언트 측에서 테마 모드를 가져오는 과정에서 발생했다. 서버 사이드 렌더링 환경에서는 localStorage가 작동하지 않으므로 초기 상태가 서버와 클라이언트에서 다를 수 있었다.
 

해결 방법

localStorage는 서버 측에서는 작동하지 않으며 클라이언트 측에서만 작동한다. 따라서 서버 사이드 렌더링 (SSR) 환경에서는 localStorage.getItem('theme')에서 null을 반환하게 되고, 이로 인해 클라이언트와 서버의 초기 상태가 다르게 설정되어 경고가 발생하게 된다.
이 문제를 해결하기 위해 클라이언트에서 초기 상태를 서버와 일치시키는 방법이 필요해서 useEffect 훅을 사용하여 클라이언트 측에서 초기 테마 모드를 설정해주었다.
 
import { useState, useEffect } from 'react';

export default function Layout() {
	const [theme, setTheme] = useState('light');

  useEffect(() => {
    const storedTheme = localStorage.getItem('theme');
    if (storedTheme) {
      setTheme(storedTheme);
    }
  }, []);

  const handleClick = () => {
    const newTheme = theme === 'dark' ? 'light' : 'dark';
    localStorage.setItem('theme', newTheme);
    setTheme(newTheme);
  };
	
	return(
		<button className="px-2" onClick={handleClick}>
		  {theme === 'dark' ? (
		    <Image
		      width={20}
		      height={20}
		      src={'/light-mode.svg'}
		      alt="light"
		    />
		  ) : (
		    <Image width={20} height={20} src={'/dark-mode.svg'} alt="dark" />
		  )}
		</button>
	);
}
 
notion image
 
function MyComponent() {
       // This condition depends on `window` so in the first render of the browser the `color` variable will be different
  	const color = typeof window !== 'undefined' ? 'red' : 'blue
      // As color is passed as a prop there is a mismatch between what was rendered server-side vs what was rendered in the first render
  	return <h1 className={`title ${color}`}>Hello World!</h1>
  }
  
  
  // In order to prevent the first render from being different you can use `useEffect` which is only executed in the browser and is executed during hydration:
  import {useEffect, useState} from 'react'
  function MyComponent() {
       // The default value is 'blue', it will be used during pre-rendering and the first render in the browser (hydration)
       const [color, setColor] = useState('blue')
       // During hydration `useEffect` is called. `window` is available in `useEffect`. In this case because we know we're in the browser checking for window is not needed. If you need to read something from window that is fine though.
       // By calling `setColor` in `useEffect` a render is triggered after hydrating, this causes the "browser specific" value to be available. In this case 'red'.
       useEffect(() => setColor('red'), [])
      // As color is a state passed as a prop there is no mismatch between what was rendered server-side vs what was rendered in the first render. After useEffect runs the color is set to 'red'
  	return <h1 className={`title ${color}`}>Hello World!</h1>
  }
 

SSR과 useEffect를 함께 사용하는 이유

SSR과 useEffect를 함께 사용하는 이유는 초기 상태를 동기화하기 위해서이다.
서버 사이드 렌더링(SSR)은 웹 페이지의 초기 렌더링을 서버에서 수행하고 클라이언트에게 전달한다. 이 과정에서 서버는 데이터를 불러와 초기 상태를 설정하여 HTML로 렌더링한다. 그리고 클라이언트는 해당 HTML을 받아 렌더링한 후 JavaScript가 실행되면서 클라이언트 컴포넌트가 마운트된다.
하지만 SSR은 초기 상태만 전달할 뿐, 이후 클라이언트에서 발생하는 상태 변화는 서버에 반영되지 않는다. 따라서 클라이언트 측에서 추가적인 상태 변경이 필요한 경우, useEffect 훅을 사용하여 초기화 작업을 수행한다. useEffect는 클라이언트 컴포넌트가 마운트된 후 한 번 실행되며, 클라이언트 측에서만 작동한다.
이렇게 useEffect를 사용하면 서버에서 전달받은 초기 상태와 클라이언트 측에서 업데이트되는 상태가 동기화된다. useEffect 내부에서는 클라이언트 측에서 로컬 스토리지(LocalStorage)를 통해 상태를 저장하고 업데이트할 수 있다. 따라서 초기 상태를 SSR로 설정하고, useEffect를 통해 클라이언트에서 상태를 업데이트함으로써 서버와 클라이언트의 초기 상태가 일치하게 된다.
 
💡
  1. 클라이언트가 웹 애플리케이션의 특정 페이지에 접근한다.
  1. 서버는 해당 페이지를 렌더링하기 위해 getServerSideProps 또는 getStaticProps 함수를 실행한다.
  1. 서버에서 데이터를 불러오고 초기 상태를 설정하여 최종 HTML을 생성한다.
  1. 서버는 생성된 HTML과 초기 상태를 클라이언트에게 전송한다.
  1. 클라이언트는 전송받은 HTML을 렌더링하고 초기 렌더링된 페이지를 보여준다.
  1. 클라이언트 측 JavaScript가 실행되면서 클라이언트 컴포넌트들이 마운트된다.
  1. 마운트된 클라이언트 컴포넌트들은 필요한 데이터를 요청하고 필요한 초기화 작업을 수행한다.
  1. useEffect 훅을 사용하여 클라이언트 측에서 초기 테마 모드를 가져와 상태를 업데이트한다.
  1. 클라이언트에서 테마 모드를 변경할 때는 localStorage를 사용하여 값을 저장하고, 상태를 업데이트한다.
 
위 과정에서 SSR과 useEffect는 함께 사용된다. SSR은 초기 렌더링을 위해 서버에서 데이터를 가져와 HTML을 생성하고, 클라이언트에서는 useEffect를 사용하여 초기화 작업을 수행한다. SSR 단계에서 설정된 초기 상태는 클라이언트에게 전달되고, useEffect는 클라이언트 측에서 초기 상태를 업데이트하고 추가적인 작업을 수행한다.
 
 

마무리

구글링과 chatGPT를 통해 에러를 해결할 수 있었다.
이번 에러를 해결해보면서 서버 사이드 렌더링과 클라이언트 사이드 렌더링에 대한 이해도를 높일 수 있었다.
서버 사이드 렌더링에서 클라이언트의 초기화가 필요한 경우 useEffect를 사용하자.
 
 

참고