왜 사람들을 Next.js를 사용하는 걸까?
google에 입력해보면 대부분 SSR하려고 쓴다라는 포스팅 뿐이다. (물론 이 외 이점들이 있다.)
애초에 Next.js 는 React 프레임워크인데 그냥 Next.js 없이 React만으로 SSR을 하면 되는게 아닐까?
오히려 Next.js를 쓰는게 추후 기술적 부채가 될수도 있지 않을까? 라는 생각에 React만으로 SSR을 하는 법을 파헤쳐본다.
SSR
SSR(Server Side Rendering)은 서버에서 사용자에게 보여줄 페이지를 모두 구성하여 사용자에게 페이지를 보여주는 방식이다. 서버를 이용해서 페이지를 구성하기 때문에 클라이언트에서 구성하는 CSR(Client Side Rendering)보다 페이지를 구성하는 속도는 늦어지지만 전체적으로 사용자에게 보여주는 콘텐츠 구성이 완료되는 시점은 빨라진다. 더불어 SEO(search engine optimization) 또한 쉽게 구성할 수 있다.
SSR + SPA = Universal SSR
React에서 말하는 SSR은 SPA(Single Page Application)까지 접목된 Universal SSR이다. 첫 요청에만 SSR을 수행하고 그 이후에 SPA로 동작한다.
Universal SSR의 장점
- SSR로 완성된 HTML을 내려줌으로써 SEO와 빠른 초기 렌더링
- SPA의 장점인 페이지 이동 시 빠른 렌더 속도
React에서 SSR하기
SSR 순서
- 서버에서 전체 앱 데이터를 가져온다.
- 서버에서 앱 전체를 HTML로 렌더링하고 응답으로 보낸다.
- 클라이언트에서 전체 앱에 대한 JS코드를 로드한다.
- 클라이언트에서 JS로직을 전체 앱에 연결한다. (hydration)
요약하자면, 서버에서 React Component를 렌더링하고, 서버렌더링 결과물을 클라이언트 결과물에 Hydrate하면 된다.
v 18 Suspense SSR Architecture
New Suspense SSR Architecture in React 18 · Discussion #37 · reactwg/react-18
- v 18 이전
- 위의 SSR순서를 지켜야만 했다. 이전 단계가 완료되지 않으면 다음 단계로 진행하지 못했다.
- 모든 곳에 hydration를 끝내야 인터렉션이 가능했다.
- 따라서, 기존 waterful 방식의 SSR을 개선해야만 했었다.
- v 18 이후
- 앱을 작은 유닛으로 분할하여 독립적으로 단계를 거치게 된다.
- Suspense를 사용하여 각 Component의 로딩상태가 분리되며, 로딩이 끝난 컴포넌트들은 그 다음 단계 (html streaming, hydrating)를 진행된다.
SSR 메서드
- renderToPipeableStream
- renderToReadableStream
- renderToString
- renderToStaticMarkup
총 이 4가지 메서드들 중 하나를 이용하여 SSR을 할 수 있다. 이 중 renderToString과 renderToStaticMarkup은 v 0.14.3부터 추가가 되어졌으며, renderToPipeableStream과 renderToReadableStream은 v 18.0.0부터 추가가 되어졌다. renderToNodeStream 메서드도 있었으나 v 18.0.0 부터 deprecated되었다. 리액트 문서상 renderToStaticNodeStream은 deprecated 표시가 없지만 위와 같은 이유로 사용하지 않는 것이 좋다. (링크)
renderToPipeableStream
Node.js 환경에서 구동되는 메서드이다. React Element를 초기 HTML로 렌더링된다. 출력 처리할 pipe(res) 메서드와 이를 중단하는 abort() 메서드가 있는 스트림을 반환한다. 나중에 <script> 태그를 통해 HTML의 Suspense와 스트리밍을 지원한다. (링크)
let didError = false;
const stream = renderToPipeableStream(
<App />,
{
onShellReady() {
// The content above all Suspense boundaries is ready.
// If something errored before we started streaming, we set the error code appropriately.
res.statusCode = didError ? 500 : 200;
res.setHeader('Content-type', 'text/html');
stream.pipe(res);
},
onShellError(error) {
// Something errored before we could complete the shell so we emit an alternative shell.
res.statusCode = 500;
res.send(
'<!doctype html><p>Loading...</p><script src="clientrender.js"></script>'
);
},
onAllReady() {
// If you don't want streaming, use this instead of onShellReady.
// This will fire after the entire page content is ready.
// You can use this for crawlers or static generation.
// res.statusCode = didError ? 500 : 200;
// res.setHeader('Content-type', 'text/html');
// stream.pipe(res);
},
onError(err) {
didError = true;
console.error(err);
},
}
);
예제
renderToReadableStream
Web Stream(즉, 브라우저) 환경에서 구동되는 메서드이다. React 요소를 초기 HTML로 스트리밍한다. readable stream으로 해결되는 Promise를 반환한다. Suspense와 HTML Streaming을 완전히 지원한다. (링크)
let controller = new AbortController();
let didError = false;
try {
let stream = await renderToReadableStream(
<html>
<body>Success</body>
</html>,
{
signal: controller.signal,
onError(error) {
didError = true;
console.error(error);
}
}
);
// This is to wait for all Suspense boundaries to be ready. You can uncomment
// this line if you want to buffer the entire HTML instead of streaming it.
// You can use this for crawlers or static generation:
// await stream.allReady;
return new Response(stream, {
status: didError ? 500 : 200,
headers: {'Content-Type': 'text/html'},
});
} catch (error) {
return new Response(
'<!doctype html><p>Loading...</p><script src="clientrender.js"></script>',
{
status: 500,
headers: {'Content-Type': 'text/html'},
}
);
}
renderToString
스트림을 지원하지 않는 환경에서도 쓸 수 있다. React Element를 초기 HTML로 렌더링한다. HTML 문자열을 반환한다. (링크)
v 18기준으로 Suspense 지원이 제한적이며, 서버에서 렌더링이 일시 중단될 때 더이상 오류가 발생되지 않는다. 대신 가장 가까운 Suspense에 대한 대체 HTML을 내보낸 다음 클라이언트에서 동일한 컨텐츠 렌더링 시도한다. (링크)
renderToStaticMarkup
스트림을 지원하지 않는 환경에서도 쓸 수 있다. renderToString과 유사하지만, data-reactroot와 같이 React가 내부적으로 사용하는 추가 DOM 특성이 생성되지 않는다. 이 기능은 React를 단순한 정적 페이지 생성기로 사용할 때 유용하다. 추가 속성을 제거하면 바이트를 절약할 수 있기 때문이다. 클라이언트에서 리액트를 사용하여 마크업을 인터랙티브하게 할 계획이라면 이 방법을 사용하면 안된다. (링크)
v 18기준으로 Suspense 지원이 제한적이며, 서버에서 렌더링이 일시 중단될 때 더이상 오류가 발생되지 않는다. 대신 가장 가까운 Suspense에 대한 대체 HTML을 내보낸 다음 클라이언트에서 동일한 컨텐츠 렌더링 시도한다. (링크)
* data-reactroot 리액트 컴포넌트의 루트 엘리먼트를 식별해주는 속성이다. |
renderToString ⇒ renderToPipeableStream
앞서 말했듯이 React에서는 renderToString에서 renderToPipeableStream으로 교체할 것을 권장하고 있다. 아래 링크에 그에 관련된 내용이 담겨져있다.
Upgrading to React 18 on the server · Discussion #22 · reactwg/react-18
왜 교체해야하나요?
- renderToString 에서는 서버 렌더링 오류가 발생하면 렌더링을 클라이언트에게 넘기기 때문에, 초기 컨텐츠가 비어있을 수 있다.
- Suspense 기능이 완전히 지원된다.
- lazy HTML Streaming이 적용된다.
'📝 꾸준함이 무기 > React' 카테고리의 다른 글
tailwind 폰트적용 (0) | 2022.11.09 |
---|---|
React Hook Form 간단정리 (0) | 2022.06.03 |
styled-component의 css를 이용하여 코드가독성 높이기! (0) | 2021.11.05 |
이벤트의 순서 (onKeyPress, onKeyDown, onKeyUp, onChange) (1) | 2021.11.04 |
Next.js에서 페이지 이동을 하려면? (2) | 2021.10.28 |