๐ฉ๐ป๐ง scss + next monorepo ๋์์ธ ์์คํ ์ ๋์ ํ๋ ์ค์ด๋ค.
๋คํฌ๋ชจ๋๋ฅผ ์ถ๊ฐํด์ฃผ๊ธฐ ์ํด์ ์ฝ๋๋ฅผ ์ถ๊ฐํด ์คฌ๋๋ฐ ์๋ก๊ณ ์นจ์ ํ ๋๋ง๋ค "Hydration failed because the server rendered didn't match the client"์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค.
๋ค์์ ๊ฐ์ ์ค์๋ฅผ ํด๋ ํค๋งค์ง ์๊ณ , ํน์๋ ๋์ฒ๋ผ ํค๋งค๊ณ ๊ณ์ ๋ถ๋ค์ ์ํด ์กฐ๊ธ์ด๋ ๋์์ด ๋ ๊น ๊ธฐ๋ก์ ๋จ๊ฒจ๋๊ณ ์ ํ๋ค.
์๋ฌ๋ ์ ๋ฐ์ํ ๊ฑด์ง, ๊ทธ๋ฆฌ๊ณ ์ด๋ป๊ฒ ํด๊ฒฐํ๋์ง ์ดํด๋ณด์!
๐ต๏ธ ๋ฌธ์ ์์ธ ์ฐพ๊ธฐ
์ด๋ฐ ์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด next์ ์์ฃผ ์น์ ํ๊ฒ ์์ธ์ผ ์์๋ค, ๊ทธ๋ฆฌ๊ณ ํด๊ฒฐ์ฑ ๋ค์ ์๋ดํด ์ค๋ค.
๋ฌผ๋ก ๊ทธ์ค์ ํ๋ ํน์ ์ฌ๋ฌ ๊ฐ๊ฐ ์์ธ์ด ๋์ด์ ์๋ฌ๊ฐ ๋ฐ์ํ ๊ฒ์ผ ์ ์์ง๋ง,
ํ๋์ฉ ์๊ฑฐ๋ฒ์ผ๋ก ์ถ๋ ค๋ณด๋ฉด ํด๊ฒฐ ๊ฐ๋ฅํ ๊ฑฐ ๊ฐ๋ค.
WHY!?
์ ํ๋ฆฌ์ผ์ด์ ์ ๋ ๋๋ง ํ๋ ๋์ค ์๋ฒ์์ pre-render ํ๋ ๋ฆฌ์กํธ ํธ๋ฆฌ์ ๋ธ๋ผ์ฐ์ ์ ์ฒ์ ๋ ๋๋๋ ๋ฆฌ์กํธ ํธ๋ฆฌ๊ฐ ๋ถ์ผ์นํด์ ๋ฐ์ํ๋ค.
Hydration์ด๋ ๋ฆฌ์กํธ๊ฐ pre-render ๋ ์๋ฒ ์ชฝ์ html์, ์ํธ์์ฉ์ด ๊ฐ๋ฅํ ์ ํ๋ฆฌ์ผ์ด์ ์ด ๋๋๋ก ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ์ถ๊ฐํ ๋ฒ์ ์ผ๋ก ๋ณ๊ฒฝํด ์ฃผ๋ ์์ ์ด๋ค.
๊ฐ๋ฅ์ฑ ๋์ ์์ธ๋ค
- ์๋ชป๋ HTML ํ๊ทธ ์ค์ฒฉ
- <p> ํ๊ทธ๋ฅผ ๋ค๋ฅธ <p> ํ๊ทธ ์์ ์ค์ฒฉํ ๊ฒฝ์ฐ
- <div>์ <p> ํ๊ทธ ์์ ์ค์ฒฉํ ๊ฒฝ์ฐ
- <ul>์ด๋ <ol>์ <p> ํ๊ทธ ์์ ์ค์ฒฉํ ๊ฒฝ์ฐ
- ์ํธ์์ฉ์ด ๊ฐ๋ฅํ ์ฝํ ์ธ ๋ฅผ ์ค์ฒฉํ ๊ฒฝ์ฐ (์ : <a> ํ๊ทธ ์์ <a> ํ๊ทธ ์ค์ฒฉ, <button> ํ๊ทธ ์์ <button> ํ๊ทธ)
- ๋ ๋ ๋ก์ง์ผ๋ก typeof window!= 'undefined'๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ
- ๋ธ๋ผ์ฐ์ ์๋ง ๋ด์ฅ๋ API (window, localStorage)๋ฅผ ๋ ๋ ๋ก์ง์ผ๋ก ์ฌ์ฉํ ๊ฒฝ์ฐ
- Date() ์์ฑ์์ ๊ฐ์ ์๊ฐ์ ์์กดํ๋ API๋ฅผ ๋ ๋ ๋ก์ง์ ์ฌ์ฉํ ๊ฒฝ์ฐ
- HTML์ ๋ณ๊ฒฝํ๋ ๋ธ๋ผ์ฐ์ ํ์ฅ์๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ
- CSS-in-JS ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์๋ชป ์ค์ ํ ๊ฒฝ์ฐ
- 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