이 글은 Prop `props이름` did not match. 로 시작하는 Warning에 관한 내용입니다.
또한 Next.js에서 styled-components를 사용하면서 겪은 여러 가지 문제를 다룹니다.
_document.tsx(jsx)
Next.js에서 styled-components를 사용할 때 _document를 따로 설정해서 SSR될 때 CSS가 head에 주입되도록 해야 한다. 만약 따로 설정하지 않는다면, styled-components가 적용되지 않은 상태로 렌더링될 수 있다.
import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext
} from 'next/document';
import { ServerStyleSheet } from 'styled-components';
class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />)
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
)
};
} finally {
sheet.seal();
}
}
render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
babel-plugin-styled-components
Next.js에서 styled-components를 사용하면 위와 같은 경고가 뜨곤 한다.
경고 문구에서도 알 수 있듯이, 서버와 클라이언트의 클래스명이 다른 것이 원인이다.
Next.js는 첫 페이지 로드가 SSR로 동작하기 때문에, 서버에서 생성된 컴포넌트와 CSR로 클라이언트에서 생성된 컴포넌트의 클래스명이 서로 달라지게 된다.
이렇게 환경에 따라 달라지는 className을 일관되게 해주는 것이 바로 babel-plugin-styled-components이다.
$ yarn add -D babel-plugin-styled-components
.babelrc 파일에 아래와 같이 작성하면 된다.
{
"plugins": ["babel-plugin-styled-components"]
}
만약 CNA(create-next-app)을 사용하고 있다면, 루트 디렉토리에 .babelrc 파일을 만들고 아래와 같이 작성하면 된다.
{
"presets": ["next/babel"],
"plugins": ["babel-plugin-styled-components"]
}
아래와 같이 babel-plugin-styled-components에 추가적인 설정을 할 수 있다.
{
"presets": ["next/babel"],
"plugins": [
[
"babel-plugin-styled-components",
{
"ssr": true, // SSR을 위한 설정
"displayName": true, // 클래스명에 컴포넌트 이름을 붙임
"pure": true // dead code elimination (사용되지 않는 속성 제거)
}
]
]
}
+ SWC를 사용하는 경우 (2022/11/13 추가)
Next.js 12버전부터 babel 대신 swc를 사용하여 컴파일하도록 변경되었다. 그러나 .babelrc가 있다면 babel을 사용하게 되므로 위 babel-plugin-styled-components를 사용하지 않고 아예 babelrc를 제거한 다음, next.config.js에서 nextConfig에 styledComponents 설정만 해주면 swc를 사용하면서 동일하게 문제를 해결할 수 있다.
/** @type {import('next').NextConfig} */
const nextConfig = {
compiler: {
styledComponents: true,
},
};
module.exports = nextConfig;
여기까지가 이 오류와 관련하여 검색을 하면 대부분 나오는 내용들이다.
하지만 이렇게 해도 해결이 되지 않을 수 있다.
react-responsive의 useMediaQuery를 사용할 때
function Component() {
const isMobile = useMediaQuery({ query: '(max-width: 767px)'});
return <Tag isMobile={isMobile} />;
}
interface tagProps {
isMobile: boolean;
}
const Tag = styled.div<tagProps>`
max-width: ${({ isMobile }) => isMobile && '200px'};
`;
위와 같이 react-responsive의 useMediaQuery를 사용해서 뷰포트 크기에 따른 styled-components의 분기 처리를 해야 할 경우에도 동일한 경고문이 뜰 수 있다.
위 상황에서 모바일 뷰포트인 767px 이하로 뷰포트를 만들어놓은 상태에서 페이지를 로드하면 아래와 같이 Prop `className` did not match로 시작하는 경고문이 뜬다.
그러나 자세히 보면 맨 처음에 봤던 오류와는 조금 다른 점이 있다.
맨 처음 오류는 서버와 클라이언트의 클래스명이 아예 달랐다면, 이번에는 클래스명은 같으나, 서버에는 1개의 클래스명만 있고 클라이언트에는 2개의 클래스명이 있다는 점이다.
즉, 서버에서는 별도의 스타일이 추가되지 않았고, 클라이언트에서는 erGBsS라는 클래스명에 해당하는 스타일이 추가된 것이다.
useMediaQuery는 내부적으로 window.matchMedia에 의존하고 있다.
그런데, 서버 환경에서는 window가 존재하지 않으므로 정상적으로 isMobile을 구할 수 없다.
이처럼 window가 없는 환경인 서버에서 useMediaQuery는 항상 false를 반환한다.
useEffect로 마운트되었을 때 useMediaQuery로 isMobile을 구하도록 변경하여 해결할 수 있다.
interface hookProps {
query: string;
}
function useBreakpoint(settings: hookProps) {
const [mounted, setMounted] = useState(false);
const value = useMediaQuery(settings);
useEffect(() => {
setMounted(true);
}, []);
return mounted ? value : false;
}
function Component() {
const isMobile = useBreakpoint({ query: '(max-width: 767px)'});
return <Tag isMobile={isMobile} />;
}
useBreakpoint라는 커스텀 훅을 만들고 useEffect로 마운트 될 때 useMediaQuery의 반환값인 value를 반환하고, 마운트되지 않았을 때는 false를 반환하도록 하였다.
console.log(isMobile)을 해보면 서버에서는 항상 false가 출력되고, 클라이언트에서는 false와 true가 각각 한 번씩 출력될 것이다. 클라이언트에서 첫 번째 출력 역시 항상 false인데, 아마 mount 되기 직전에 서버와 클라이언트가 둘 다 스타일이 적용되지 않은 상태로 같아서 경고가 뜨지 않는 것 같다.
Theme과 Typescript 관련 문제
Theme과 관련하여서도 Prop `className` did not match 경고가 뜰 수 있다.
이 부분은 겪어보지는 않았지만, 아래 링크 뒷부분에 자세하게 설명되어 있으니 참고하면 좋을 것 같다.
https://blog.shift.moe/2021/01/02/prop-classname-did-not-match/
참고자료
https://github.com/vercel/next.js/blob/main/examples/with-styled-components/pages/_document.js
https://blog.shift.moe/2021/01/02/prop-classname-did-not-match/
https://seungjoon-lee.oopy.io/d45a150b-9598-4c27-9fea-6ee658a0ac57
https://nextjs.org/docs/advanced-features/customizing-babel-config
https://styled-components.com/docs/tooling#usage
https://github.com/chakra-ui/chakra-ui/issues/4319
'IT > Next.js' 카테고리의 다른 글
[Next.js] React Query로 SSR 구현하기 (1) | 2022.11.18 |
---|---|
[Next.js] Next.js에서 mongoose 연동해서 API 만들기 (0) | 2022.04.03 |