history.pushState
와 history.replaceState
로 이뤄진다.<body/>
내부에 아무런 내용이 없고, 렌더링에 필요한 모든 내용을 JS 코드로 삽입한 이후 렌더링한다.기존 웹 개발은 LAMP 스택으로 구성되어 있었다.
Linux(운영체제), Apache(서버), MySQL(데이터베이스), PHP/Python(웹 프레임워크)
새로운 프레임워크의 등장으로 JAM 스택이 등장했다.
Javascript, API, Markup
리액트는 리액트 애플리케이션을 서버에서 렌더링할 수 있는 API도 제공한다.
const result = ReactDOMServer.renderToString(
React.createElement("div", { id: "root" }, <SampleComponent />)
);
const result = ReactDOMServer.renderToStaticMarkup(
React.createElement("div", { id: "root" }, <SampleComponent />)
);
renderToString과 결과물이 완전히 동일하지만 차이점이 있다.
ReactDOMServer.renderToNodeStream() : The streaming API is not available in the browser. Use ReactDOMServer.renderToString() instead.
그렇다면 어디서 필요할까?
renderToString과 renderToNodeStream으로 생성된 HTML 콘텐츠에 JS 핸들러나 이벤트를 붙이는 역할
render
import * as ReactDOM from "react-dom";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
hydrate
import * as ReactDOM from "react-dom";
import App from "./App";
// containerId를 가리키는 element는 서버에서 렌더링된 HTML의 특정 위치를 의미한다.
const element = document.getElementById(containerId);
// 해당 element를 기준으로 리액트 이벤트 핸들러를 붙인다.
ReactDOM.hydrate(<App />, element);
이미 렌더링된 HTML이 있다는 가정하에 작업이 수행되고, 렌더링된 HTML을 기준으로 이벤트를 붙이는 작업만 실행한다.
리액트 관련 정보가 없는 순수한 HTML 정보를 넘겨주면 에러가 발생한다.
불가피하게 불일치가 발생할 수 있는 경우
<div suppressHydrationWarning>{new Date().getTime()}</div>
import { hydrate } from "react-dom";
import App from "./components/App";
import { fetchTodo } from "./fetch";
async function main() {
const result = await fetchTodo();
const app = <App todos={result} />;
const el = document.getElementById("root");
hydrate(app, el);
}
main();
__placeholder__
unpkg
browser.js
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SSR Example</title>
</head>
<body>
__placeholder__
<script src="https://unpkg.com/react@17.0.2/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js"></script>
<script src="/browser.js"></script>
</body>
</html>
server.ts
사용자의 요청 주소에 따라 어떠한 리소스를 내려줄 지 결정하는 역할을 한다.
서버 사이드 렌더링을 위해 리액트 트리를 만드는 역할도 담당한다.
createServer
function main() {
createServer(serverHandler).listen(PORT, () => {
console.log(`Server has been started ${PORT}...`);
});
}
serverHandler
async function serverHandler(req: IncomingMessage, res: ServerResponse) {
const { url } = req;
switch (url) {
//...
default: {
res.statusCode = 404;
res.end("404 Not Founc");
}
}
}
server.ts의 루트 라우터 /
const { result } = await fetchTodo();
const rootElement = createElement(
"div",
{ id: "root" },
createElement(App, { todos: result })
);
const renderResult = renderToString(rootElement); // 리액트 컴포넌트 -> HTML
const htmlResult = html.replace("__placeholder__", renderResult); // replace
res.setHeader("Content-Type", "text/html");
res.write(htmlResult);
res.end();
return;
server.ts의 /stream 라우터
switch (url) {
case "/stream": {
res.setHeader("Content-Type", "text/html");
res.write(indexFront);
const result = await fetchTodo();
const rootElement = createElement(
"div",
{ id: "root" },
createElement(App, { todos: result })
);
const stream = renderToNodeStream(rootElement);
stream.pipe(res, { end: false });
stream.on("end", () => {
res.write(indexEnd);
res.end();
});
return;
}
}
__placeholder__
부분을 반으로 나눈 코드그 밖의 라우터
switch (url) {
// 브라우저에 제공되는 리액트 코드
case "/broswer.js": {
res.setHeader("Content-Type", "application/javascript");
createReadStream("./dist/browser.js").pipe(res);
return;
}
// 위 파일의 소스맵
case "/browser.js.map": {
res.setHeader("Content-Type", "application/javascript");
createReadStream("./dist/browser.js.map").pipe(res);
return;
}
default: {
res.statusCode = 404;
res.end("404 Not Found");
}
}
broswer.js : 애플리케이션에서 작성한 리액트 및 관련 코드를 제공하는 파일. 웹팩이 생성한다.
broswer.js.map : browser.js와 관련된 소스맵 파일. 디버깅 용도로 쓰인다.
webpack.config.js
<html>
이나 <body>
에 DOM 속성을 추가할 때<head>
는 오직 _document에서만 실행할 수 있고, <title>
을 사용할 수 없다.<Link>
를 사용하고, window.location.push 대신 router.push를 사용해야 한다.사용자와 관계없이 정적으로 결정된 페이지를 보여주고자 할 때 사용되는 함수
반드시 함께 있어야 사용할 수 있다.
빌드 시점에 미리 데이터를 불러온 뒤 정적인 HTML 페이지를 만들 수 있다.
// /pages/post/[id]가 접근 가능한 주소를 정의하는 함수
export const getStaticPaths: GetStaticPaths = async () => {
return {
paths: [{ params: { id: "1" } }, { params: { id: "2" } }], // /post/1과 /post/2만 접근 가능하다.(그 외에는 404)
fallback: false,
};
};
// 위에서 정의한 페이지를 기준으로 해당 페이지로 요청이 왔을 때 제공할 props를 반환하는 함수
export const getStaticProps: GetStaticProps = async ({ params }) => {
const { id } = params;
const post = await fetchPost(id);
return {
props: { post },
};
};
// getStaticProps가 반환한 post를 렌더링하는 역할
export default function Post({ post }: { post: Post }) {
// post로 페이지 렌더링
}
fallback : 미리 빌드해야 할 페이지가 너무 많은 경우에 사용 가능한 옵션
서버에서 실행되는 함수. 무조건 페이지 진입 전에 이 함수를 실행한다.
빌드 시에도 서버용 JS 파일을 별도로 만든다.
Next.js의 SSR은 getServerSideProps의 실행과 함께 이뤄지며, 이 정보를 기반으로 페이지가 렌더링된다.
export default function({post} : {post: Post}){
// 렌더링
}
export const getServerSideProps : GetServerSideProps = async(context) => {
const {
query: {id: ''},
} = context
const post = await fetchPost(id.toString())
return {
props: {post}
}
}
<body>
<div id="__next" data-reactroot="">
<h1>안녕하세요</h1>
<h1>반갑습니다</h1>
</div>
<!-- 생략 -->
<script id="__NEXT_DATA__" type="application/json">
{
"props": {
"pageProps": {
"post": { "title": "안녕하세요", "contents": "반갑습니다." }
},
"__N_SSP": true,
"page": "/post/[id]",
"query": { "id": "1" },
"buildId": "development",
"isFallback": false,
"gssp": true,
"scriptLoading": []
}
}
</script>
</body>
__NEXT_DATA__
스트립트
window.__NEXT_DATA__
)일반적인 리액트의 JSX와 다르게 getServerSideProps의 props로 내려줄 수 있는 값은 JSON으로 제공할 수 있는 값으로 제한된다.
무조건 서버에서만 실행된다.