๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

๐Ÿ‘ฉ๐Ÿป‍๐Ÿ’ป dev

[react-hydration-error] Hydration failed because the server rendered didn't match the client

[์ด์ „๊ธ€ : ๋””์ž์ธ ์‹œ์Šคํ…œ์ด ๋ญ”์ง€ ์•Œ๊ณ  ์‹ถ๋‹ค๋ฉด ๐Ÿ‘‰ ] 2024.07.11 - [๐Ÿ‘ฉ๐Ÿป‍๐Ÿ’ป dev] - ๋””์ž์ธ ์‹œ์Šคํ…œ 101 : ๋””์ž์ธ ์‹œ์Šคํ…œ์ด๋ž€?

๐Ÿ‘ฉ๐Ÿป‍๐Ÿ”ง scss + next monorepo ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ๋„์ž…ํ•˜๋Š” ์ค‘์ด๋‹ค.
๋‹คํฌ๋ชจ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๊ธฐ ์œ„ํ•ด์„œ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด ์คฌ๋Š”๋ฐ ์ƒˆ๋กœ๊ณ ์นจ์„ ํ•  ๋•Œ๋งˆ๋‹ค "Hydration failed because the server rendered didn't match the client"์˜ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.
๋‹ค์Œ์— ๊ฐ™์€ ์‹ค์ˆ˜๋ฅผ ํ•ด๋„ ํ—ค๋งค์ง€ ์•Š๊ณ , ํ˜น์‹œ๋‚˜ ๋‚˜์ฒ˜๋Ÿผ ํ—ค๋งค๊ณ  ๊ณ„์‹  ๋ถ„๋“ค์„ ์œ„ํ•ด ์กฐ๊ธˆ์ด๋‚˜ ๋„์›€์ด ๋ ๊นŒ ๊ธฐ๋ก์„ ๋‚จ๊ฒจ๋‘๊ณ ์ž ํ•œ๋‹ค.
์—๋Ÿฌ๋Š” ์™œ ๋ฐœ์ƒํ•œ ๊ฑด์ง€, ๊ทธ๋ฆฌ๊ณ  ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ–ˆ๋Š”์ง€ ์‚ดํŽด๋ณด์ž! 

๐Ÿ•ต๏ธ ๋ฌธ์ œ ์›์ธ ์ฐพ๊ธฐ

์ด๋Ÿฐ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด next์€ ์•„์ฃผ ์นœ์ ˆํ•˜๊ฒŒ ์›์ธ์ผ ์š”์†Œ๋“ค, ๊ทธ๋ฆฌ๊ณ  ํ•ด๊ฒฐ์ฑ…๋“ค์„ ์•ˆ๋‚ดํ•ด ์ค€๋‹ค.

๋ฌผ๋ก  ๊ทธ์ค‘์— ํ•˜๋‚˜ ํ˜น์€ ์—ฌ๋Ÿฌ ๊ฐœ๊ฐ€ ์›์ธ์ด ๋˜์–ด์„œ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒƒ์ผ ์ˆ˜ ์žˆ์ง€๋งŒ,

ํ•˜๋‚˜์”ฉ ์†Œ๊ฑฐ๋ฒ•์œผ๋กœ ์ถ”๋ ค๋ณด๋ฉด ํ•ด๊ฒฐ ๊ฐ€๋Šฅํ•  ๊ฑฐ ๊ฐ™๋‹ค.

 

WHY!?

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋ Œ๋”๋ง ํ•˜๋Š” ๋„์ค‘ ์„œ๋ฒ„์—์„œ pre-render ํ–ˆ๋˜ ๋ฆฌ์•กํŠธ ํŠธ๋ฆฌ์™€ ๋ธŒ๋ผ์šฐ์ €์— ์ฒ˜์Œ ๋ Œ๋”๋˜๋Š” ๋ฆฌ์•กํŠธ ํŠธ๋ฆฌ๊ฐ€ ๋ถˆ์ผ์น˜ํ•ด์„œ ๋ฐœ์ƒํ•œ๋‹ค.

Hydration์ด๋ž€ ๋ฆฌ์•กํŠธ๊ฐ€ pre-render ๋œ ์„œ๋ฒ„ ์ชฝ์˜ html์„, ์ƒํ˜ธ์ž‘์šฉ์ด ๊ฐ€๋Šฅํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ๋˜๋„๋ก ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ถ”๊ฐ€ํ•œ ๋ฒ„์ „์œผ๋กœ ๋ณ€๊ฒฝํ•ด ์ฃผ๋Š” ์ž‘์—…์ด๋‹ค.

 

๊ฐ€๋Šฅ์„ฑ ๋†’์€ ์›์ธ๋“ค

  1. ์ž˜๋ชป๋œ HTML ํƒœ๊ทธ ์ค‘์ฒฉ
    1. <p> ํƒœ๊ทธ๋ฅผ ๋‹ค๋ฅธ <p> ํƒœ๊ทธ ์•ˆ์— ์ค‘์ฒฉํ•  ๊ฒฝ์šฐ
    2. <div>์„ <p> ํƒœ๊ทธ ์•ˆ์— ์ค‘์ฒฉํ•  ๊ฒฝ์šฐ
    3. <ul>์ด๋‚˜ <ol>์„ <p> ํƒœ๊ทธ ์•ˆ์— ์ค‘์ฒฉํ•  ๊ฒฝ์šฐ
    4. ์ƒํ˜ธ์ž‘์šฉ์ด ๊ฐ€๋Šฅํ•œ ์ฝ˜ํ…์ธ ๋ฅผ ์ค‘์ฒฉํ•  ๊ฒฝ์šฐ (์˜ˆ : <a> ํƒœ๊ทธ ์•ˆ์— <a> ํƒœ๊ทธ ์ค‘์ฒฉ, <button> ํƒœ๊ทธ ์•ˆ์— <button> ํƒœ๊ทธ)
  2. ๋ Œ๋” ๋กœ์ง์œผ๋กœ typeof window!= 'undefined'๋ฅผ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ
  3. ๋ธŒ๋ผ์šฐ์ €์—๋งŒ ๋‚ด์žฅ๋œ API (window, localStorage)๋ฅผ ๋ Œ๋” ๋กœ์ง์œผ๋กœ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ
  4. Date() ์ƒ์„ฑ์ž์™€ ๊ฐ™์€ ์‹œ๊ฐ„์— ์˜์กดํ•˜๋Š” API๋ฅผ ๋ Œ๋” ๋กœ์ง์— ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ
  5. HTML์„ ๋ณ€๊ฒฝํ•˜๋Š” ๋ธŒ๋ผ์šฐ์ € ํ™•์žฅ์ž๋ฅผ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ
  6. CSS-in-JS ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ž˜๋ชป ์„ค์ •ํ•œ ๊ฒฝ์šฐ
  7. html ์š”์ฒญ์„ ๋ณ€๊ฒฝํ•˜๋ ค๋Š” Edge/CDN ์„ค์ • (cloudflare auto minify)์ด ์ž˜๋ชป๋œ ๊ฒฝ์šฐ

1๋ฒˆ ํ™•์ธ ๊ฒฐ๊ณผ :  p ํƒœ๊ทธ๋ฅผ ์‚ฌ์šฉ x, ๋ฒ„ํŠผ ์•ˆ์— ๋ฒ„ํŠผ x

2๋ฒˆ ๋กœ์ง ์‚ฌ์šฉ x

3๋ฒˆ ๐Ÿ‘‰ ๋‹คํฌ/ ๋ผ์ดํŠธ ๋ชจ๋“œ๋ฅผ localstorage์— ํ™•์ธ/์ €์žฅํ•ด์„œ ์„ค์ • ์ค‘, ์œ ์ผํ•˜๊ฒŒ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Œ.

4๋ฒˆ ๋กœ์ง ์‚ฌ์šฉ x

5๋ฒˆ ๋ธŒ๋ผ์šฐ์ € ํ™•์žฅ์ž x

6๋ฒˆ scss ์‚ฌ์šฉ ์ค‘์ด๋ฏ€๋กœ x

7๋ฒˆ Edge/CDN ์„ค์ • x 

โ˜บ๏ธ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ๋“ค

1. useEffect๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํด๋ผ์ด์–ธํŠธ์—์„œ๋งŒ ๋™์ž‘ํ•˜๋„๋ก ํ•˜๊ธฐ

 

์ฒซ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”์™€ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋” ์‹œ ์ปดํฌ๋„ŒํŠธ ์ฝ˜ํ…์ธ ๊ฐ€ ๋™์ผํ•˜๋„๋ก ํ•ด์„œ hydration ๋ถˆ์ผ์น˜๋ฅผ ๋ฐฉ์ง€ํ•ด ๋ณด์ž.

์˜๋„์ ์œผ๋กœ useEffect ํ›…์„ ํ†ตํ•ด ํด๋ผ์ด์–ธํŠธ ์ชฝ์—์„œ ๋‹ค๋ฅธ ์ฝ˜ํ…์ธ ๊ฐ€ ๋ณด์ด๋„๋ก ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

import { useState, useEffect } from 'react'
 
export default function App() {
  const [isClient, setIsClient] = useState(false)
 
  useEffect(() => {
    setIsClient(true)
  }, [])
 
  return <h1>{isClient ? 'This is never prerendered' : 'Prerendered'}</h1>
}

 

๋ฆฌ์•กํŠธ hydration ๋™์•ˆ useEffect๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค.

๋”ฐ๋ผ์„œ window์™€ ๊ฐ™์€ ๋ธŒ๋ผ์šฐ์ € api๋ฅผ hydration mismatch ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

 

2. ํŠน์ • ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” SSR ๋™์ž‘ ๋ฐฉ์ง€ โญ๏ธ 

nextjs๋Š” ํŠน์ • ์ปดํฌ๋„ŒํŠธ๊ฐ€ prerender ๋˜๋Š” ๊ฑธ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค. 

import dynamic from 'next/dynamic'
 
const NoSSR = dynamic(() => import('../components/no-ssr'), { ssr: false })
 
export default function Page() {
  return (
    <div>
      <NoSSR />
    </div>
  )
}

 

ํ…Œ๋งˆ ํ† ๊ธ€ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ํ…Œ๋งˆ ๋ฐ”๋€๋‹ค

 

3. suppressHydrationWarning ์‚ฌ์šฉ

๊ฐ€๋” ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด์˜ ์ฝ˜ํ…์ธ  ๋ถˆ์ผ์น˜๋ฅผ ํ”ผํ•  ์ˆ˜ ์—†์„ ์ˆ˜๋„ ์žˆ๋‹ค.  (ex. timestamp)

suppressHydrationWarning={true}๋ฅผ ์—˜๋ฆฌ๋จผํŠธ์— ์ถ”๊ฐ€ํ•˜์—ฌ hydration ๋ถˆ์ผ์น˜ ๊ฒฝ๊ณ ๊ฐ€ ๋œจ์ง€ ์•Š๋„๋ก ํ•  ์ˆ˜ ์žˆ๋‹ค.

<time datetime="2016-10-25" suppressHydrationWarning />

4. iOS ์ด์Šˆ

ios๋Š” ์ „ํ™”๋ฒˆํ˜ธ, ์ด๋ฉ”์ผ ์ฃผ์†Œ ํ˜น์€ ์ฝ˜ํ…์ธ ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋งํฌ๋กœ ๋ณ€๊ฒฝํ•˜๋ ค๊ณ  ์‹œ๋„ํ•œ๋‹ค.

์ด๋Ÿฐ ์‹œ๋„ ๋•Œ๋ฌธ์— hydration ๋ถˆ์ผ์น˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์ด meta ํƒœ๊ทธ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ๋ณ€๊ฒฝ ์‹œ๋„๋ฅผ ์ฐจ๋‹จํ•  ์ˆ˜ ์žˆ๋‹ค.

<meta
  name="format-detection"
  content="telephone=no, date=no, email=no, address=no"
/>

 

์ด ์ค‘์— ๋‚ด๊ฐ€ ์‚ดํŽด๋ณธ ํ•ด๊ฒฐ์ฑ…์€ 2๋ฒˆ!

 

1๋ฒˆ ์ œ์™ธ ์ด์œ  :  ์ฒซ ๋ Œ๋”๋ง ์‹œ ๋‚ด๊ฐ€ ์„ ํ˜ธํ•˜๋Š” ๋ชจ๋“œ๋ฅผ ์ ์šฉํ•œ ํ…Œ๋งˆ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ๊ธฐ ๋•Œ๋ฌธ์— ๋ณด์ผ ํŽ˜์ด์ง€๊ฐ€ ๊ฐ™์•„์•ผ ํ•œ๋‹ค๋Š” ์ ์—์„œ ์กฐ๊ฑด๋ถ€๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์˜๋ฏธ๊ฐ€ ์—†์–ด์ง

 

2๋ฒˆ ์„ ํƒ ์ด์œ  :  ํ›…์€ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ๋งŒ ํ˜ธ์ถœ๊ฐ€๋Šฅํ•˜๋‹ค. ํ›…์„ ๋ฐ˜๋“œ์‹œ ์จ์•ผ ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ์˜ prerender๋ฅผ ๋ฐฉ์ง€ํ•˜์ž.

 

3๋ฒˆ ์ œ์™ธ ์ด์œ : ์‹œ๊ฐ„์— ์˜์กดํ•˜๋Š” ๋ Œ๋”๋ง์ด ์•„๋‹Œ ๋‚ด๊ฐ€ ์„ ํƒํ•œ ๋ชจ๋“œ์— ์˜์กดํ•˜๋ฏ€๋กœ ๋ถˆ๊ฐ€ํ”ผํ•œ hydration ๋ถˆ์ผ์น˜๊ฐ€ ์•„๋‹ ๊ฒƒ ๊ฐ™์•„์„œ.

 

4๋ฒˆ ์ œ์™ธ ์ด์œ : ๋งํฌ, ์ด๋ฉ”์ผ, ์ „ํ™”๋ฒˆํ˜ธ ๋“ฑ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ๊ณ  ์žˆ์ง€ ์•Š์•„์„œ.

๐Ÿ•ต๏ธ  ํ•ด๊ฒฐ ์ฝ”๋“œ

์•ฑ์˜ ํ…Œ๋งˆ์— ๋”ฐ๋ผ ๋‹คํฌ/๋ผ์ดํŠธ ๋ชจ๋“œ ์Šคํƒ€์ผ์„ ์ ์šฉํ•˜๊ณ ์ž ๋งŒ๋“  body ์ปดํฌ๋„ŒํŠธ ํ•จ์ˆ˜๋ฅผ ์‚ดํŽด๋ณด์ž.

"use client";

import { useAppTheme } from "@tripie/hooks";
import classNames from "classnames";
import { HTMLProps, ReactNode } from "react";
import "./_body.scss";

export interface BodyProps extends HTMLProps<BodyProps> {
  children: ReactNode;
}

const Body = ({ children, className, ...props }: BodyProps) => {
  const { mode } = useAppTheme();
  // mode์„ className์— ์ถ”๊ฐ€ํ•˜๊ณ  ํ† ๊ธ€๋˜๋Š” ํ…Œ๋งˆ์— ๋”ฐ๋ผ ์Šคํƒ€์ผ ์ ์šฉ
  return <div className={classNames(className, "body", mode)}>{children}</div>;
};
export default Body;

 

์ด๋•Œ useAppTheme์€ ์ปค์Šคํ…€ ํ›…์ด๋‹ค. ํ›…์€ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ๋งŒ ํ˜ธ์ถœ๊ฐ€๋Šฅํ•˜๋‹ค. 

์—ฌ๊ธฐ์„œ client๊ณผ server ๊ฐ„์˜ ๋‚ด์šฉ ๋ถˆ์ผ์น˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๋˜ ๊ฒƒ์ด๋‹ค.

 

nextjs๊ฐ€ ์ œ์‹œํ•œ ํ•ด๊ฒฐ์ฑ…์„ ๋”ฐ๋ผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •ํ•ด ์ฃผ์ž.

<ThemeWrapper>๋Š” <Body> ํƒœ๊ทธ๋ฅผ prerender ํ•˜์ง€ ๋ชปํ•˜๋„๋ก import๋ฅผ ํ•ด์˜จ ์ปดํฌ๋„ŒํŠธ๋‹ค.

์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

import dynamic from "next/dynamic";
import { ReactNode } from "react";

const ThemeWrapper = dynamic(
  () => import("@tripie/design-system/components/body/body"),
  { ssr: false }
);

export default function ThemeProvider({
  children,
}: Readonly<{
  children: ReactNode;
}>) {
  return <ThemeWrapper>{children}</ThemeWrapper>;
}

 

๐Ÿ€ ๋งˆ์น˜๋ฉด์„œ

๐Ÿค” ๋ชจ๋…ธ๋ ˆํฌ ํŒจํ‚ค์ง€ ์•ˆ์—์„œ .scss ํŒŒ์ผ์„ import/export ํ•˜๊ณ  ์‹ถ์€๋ฐ ํ•œ์ฐธ ํ—ค๋งค๋‹ค๊ฐ€ ์•„์ง ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋‚ด์ง€ ๋ชปํ–ˆ๋‹ค..
๐Ÿ’ชํ•˜๋‚˜์”ฉ ํ•ด๋ด์•ผ์ง€

๐Ÿ“š References

https://nextjs.org/docs/messages/react-hydration-error

 

Text content does not match server-rendered HTML

Using App Router Features available in /app

nextjs.org