CSR은 단순 뼈대만 있는 HTML document와 JS 파일들을 클라이언트로 모두 보낸 뒤, 클라이언트 단에서 JS 코드들을 통해 웹 화면을 동적으로 빠르게 렌더링하는 방식이다. 덕분에 사용자 경험이 좋아지고, 서버에 요청하는 횟수가 훨씬 적기 때문에 서버의 부담이 덜하다는 장점이 있다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
import React from "react";
import ReactDOM from "react-dom";
import App from './src/App';
ReactDOM.render(<App />, document.getElementById("root"));
하지만 앞에서 언급했듯이 뼈대만 있는 하나의 index.html
을 가지고 JS 파일을 이용하여 웹 화면을 구성하기 때문에 실제 HTML 코드 안에 내용은 하나도 없는 상태이다. 위에 보이는 HTML 마크업을 통해 나머지는 JS 파일을 통해 HTML DOM 요소 중 id=root
를 가진 엘리먼트를 찾아 하위로 주입하여 동작한다.
이렇게 되면 검색 봇은 데이터까지 받은 페이지를 당연히 읽지 못하고, 우리가 사용하는 React의 CSR 방식의 단점이기도 하다.
Next.js는 클라이언트에게 웹 페이지를 보내기 전에 서버단에서 미리 웹 페이지를 Pre-rendering하
고, Pre-rendering으로 인해 생성된 HTML document를 클라이언트에게 전송한다.
현재 클라이언트가 받은 웹 페이지는 단순히 웹 화면만 보여주는 HTML일 뿐이고, 자바스크립트 요소들이 하나도 없는 상태이다. 이는 웹 화면을 보여주고 있지만, 특정 JS 모듈 뿐만 아니라 단순 클릭과 같은 이벤트 리스너들이 각 웹 페이지의 DOM 요소에 하나도 적용되어 있지 않은 상태임을 말한다.
그러면 빈 껍데이인 웹 페이지가 나중에 어떻게 정상적으로 동작할까?
ReactDOM.hydrate(element, container[, callback]);
hydrate는 서버단에서 렌더링된 정적 페이지와 번들링된 JS 파일을 클라이언트에게 보낸 뒤, 클라이언트단에서 HTML 코드와 React인 JS 코드를 서로 매칭시키는 과정을 말한다. Next.js의 주요 동작 방식 중 하나로, 페이지가 브라우저에 로드되고 JS 코드가 실행되면서 페이지가 인터렉티브하게 동작하는 상태가 되는 과정이다.
위 사진을 보면 알 수 있듯이 Next.js 서버에서는 Pre-rendering된 웹 페이지를 클라이언트에게 보내고 나서, 바로 chunk 단위로 리액트가 번들링된 JS 파일들을 클라이언트에게 전송한다. 그리고 이 JS 코드들이 이전에 보내진 HTML DOM 요소 위에서 한 번 더 렌더링을 하면서, 각자 자기 자리를 찾아가며 매칭된다.
서버 단에서 빠르게 Pre-rendering하고 유저에게 빠른 웹 페이지로 응답할 수 있다는 것에 큰 이점을 가져갈 수 있다. 심지어 이 Pre-rendering한 document는 모든 자바스크립트 요소들이 빠진 굉장히 가벼운 상태이므로 클라이언트에게 빠른 로딩이 가능하다. 따라서 두 번 렌더링하는 것이 비효율적으로 보일 수 있지만 그런 단점을 보완할 수 있을 정도의 이점을 가져갈 수 있다.(+ 매칭시키기 위한 목적으로 한 번 더 렌더링하므로 다시 그리는 과정은 거치지 않는다.)
dehydrate은 수분을 없앤다는 의미이다. 다시 말해서 동적인 것을 정적으로 만드는 행위를 dehydrate
이라고 할 수 있다. 그리고 나서 JS가 실행되면서 리액트가 정적인 HTML과 store를 동적인 리액트 컴포넌트 트리와 store로 변환하는 과정이 일어나는데, 이를 (re)hydrate이라 한다. 수분기 없는 정적인 상태를 다시 수분 넘치는 동적인 상태로 변환하는 것이다.
React Query는 서버에서 데이터를 미리 가져와(prefetch)하고 캐시를 dehyrate하고 클라이언트에서 다시 수화하는 방식을 지원한다. 이는 서버가 페이지 로드 시 즉시 사용할 수 있는 마크업을 Pre-rendering할 수 있다.
export default function MyApp({ Component, pageProps }: AppProps) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
suspense: true,
useErrorBoundary: true,
},
},
}));
return (
<>
<Head>
<title>HOW ABOUT OOTD</title>
</Head>
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<Layout>
<Component {...pageProps} />
</Layout>
<ReactQueryDevtools />
</Hydrate >
</QueryClientProvider>
</Provider>
</>
);
}
서버에서 캐싱 쿼리를 지원하고 수화를 설정하려면
QueryClient
인스턴스를 만든다. 이렇게 하면 서로 다른 사용자와 요청 간에 데이터를 공유하지 않고 구성 요소 수명 주기당 한 번만 QueryClient를 생성할 수 있다.<QueryClientProvider>
로 래핑하고 client에 전달한다.<Hydrate>
를 래핑하고 state에 pageProps.dehydratedState
를 전달한다.export async function getServerSideProps() {
const queryClient = new QueryClient();
await queryClient.prefetchInfiniteQuery({
queryKey: [queryKeys.lookbooks],
queryFn: ({ pageParam = 1 }) => getLookbooks(pageParam),
});
return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
}
QueryClient
인스턴스를 만든다. 이렇게 하면 사용자와 요청 간에 데이터가 공유되지 않는다.prefetchQuery
(or prefetchInfiniteQuery
)를 사용하여 데이터를 프리패치한다.dehydrate
의 인자로 queryClient를 사용한다.dehydratedState
prop으로 해당값을 설정한다._app.js
에서 작성한 prop과 같아야 한다.dehydratedState: JSON.parse(JSON.stringify(dehydrate(queryClient))),
위와 같이 코드를 수정해주면 undefined였던 요소들이 모두 null값으로 변경되어 에러가 해결된다.
서버에서 데이터를 prefetch하여 Pre-rendering으로 생성된 HTML에 잘적용된 것을 확인할 수 있다. 근데 alt을 안줬네.. 작성해야 겠다ㅎㅎ