잡쇼퍼 프로젝트를 진행하면서 Next.js와 styled components를 함께 사용하면서 발생했던 렌더링 문제에 관해서 이야기해보려 한다.
기존의 EJS로 구성되어 있던 잡쇼퍼 서비스를 리액트 기반으로 리뉴얼하면서, EJS의 Server Side Rendering의 장점을 살려 구현 하기 위해 Next.js를 사용하였다.
Next.js는 리액트 기반이기 때문에 리액트를 사용했을 때 스타일링 하기 위한 여러 가지 방법중 하나로 styled components를 선택하였다.
styled components는 CSS 모델을 문서 레벨이 아닌 컴포넌트 레벨로 관리할 수 있고, JS와 CSS 간에 상수 및 기능을 쉽게 공유할 수 있는 등 여러 가지 장점이 있다.
SSR과 CSS-in-JS의 장점들을 취하여 개발을 하다 보니 하나의 문제점이 발생했다. 보이는 이미지와 같이, 바로 스타일이 적용되지 않은 채 HTML이 렌더링되는 현상을 볼 수 있었다.
SSR은 서버에서 페이지마다 HTML을 미리 만들어놓고 자바스크립트 로딩이 끝나기 전에 바로 사용자들에게 화면을 보내줘서 사이트의 속도를 빠르게 한다.
그런데 CSS-in-JS를 사용한 모든 스타일은 자바스크립트에 들어가 있기 때문에 CSS가 static하게 추가되는 것이 아니라 자바스크립트에 의해 dynamic하게 추가되므로 자바스크립트가 로딩을 끝내기 전에 스타일이 적용되지 않은 HTML이 렌더링 되는 것이 문제였다.
Next.js는 렌더링이 될 때 pages 디렉토리 안에 기본적으로 같이 렌더링 되는 컴포넌트가 있다. 이 컴포넌트를 커스터마이징하여 위에서 언급한 문제를 해결할 수 있었다.
Next.js에서 styled-components 같은 CSS-in-JS 라이브러리에 대한 SSR을 지원하기 위해서는 , 태그를 보강할 수 있는 커스텀 를 사용하여 HTML 구조를 확장해야 한다.
커스텀 는 Next.js의 /pages에 _document.js 파일을 만들어 정의할 수 있는데, _document.js는 HTML 문서로 커스텀 Document를 만들 때만 작성이 필요하며 생략된 경우 Next.js가 기본값을 사용한다.
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
static async getInitialProps(ctx) {
//...
}
render() {
return (
<Html>
<head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
왜 HTML 구조를 확장해야 할까?
React는 컴포넌트가 마운트 되는 최초 요소로써
Next.js에 styled components를 적용하기 위해서는 HTML 구조에서 에
과 를 넣고 의 은 각 라우트에 해당하는 페이지가 렌더링되는 부분이며, 는 Next.js 관련한 자바스크립트 파일이다._document.js는
Next.js에서 Global한 설정 값을 줄 경우
import { ServerStyleSheet, injectGlobal } from "styled-components";
injectGlobal`
html {
font-size: 10px;
}
body {
font-family: "Merriweather", serif;
font-size: 1.6em;
line-height: 1.6;
}
`;
class MyCustomDocument extends Document {
//....
전역 스타일을 적용해야 하는 경우 _document.js에 injectGlobal을 가져와서 커스텀 Document 컴포넌트를 정의하기 전에 스타일을 제공할 수 있다.
import Document, { Head, Main, NextScript } from 'next/document'
import { ServerStyleSheet } from 'styled-components'
class MyDocument extends Document {
static async getInitialProps(ctx: NextDocumentContext) {
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>
<style />
</head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
ServerStyleSheet를 사용해 정의된 모든 스타일을 수집하여 페이지가 렌더링되기 전에 props로 채워지도록 반환한다. 그 다음 렌더함수 내부에서 {sheet.getStyleElement()}가 엘리먼트의
ServerStyleSheet는 HTML이 렌더됨과 동시에 스타일도 정적으로 렌더할 수있게 해주는 객체로 context API를 통해 받은 style을 React tree에 추가하는 방식으로 스타일 적용을 가능하게 해준다.
renderPage는 실제 React 렌더링 로직을 동기적으로 실행하는 콜백함수이다. 서버 렌더링 래퍼를 지원하기 위해 이 함수를 사용하는 것이 유용하다.
yarn add -D babel-plugin-styled-components
하여 .babelrc를 설정한다. {
"presets": [
"next/babel",
"@zeit/next-typescript/babel"
],
"plugins": [["styled-components", {"ssr":true}]]
}
https://nextjs.org/docs#custom-document
https://www.merixstudio.com/blog/nextjs-features-overview/