React에서 Server Side Rendering 구현 – 2

  • by

서버 구축

이제 서버측 렌더링을 처리하는 서버를 만듭니다.

Express를 설치합니다.

npm install express

이전에 만든 index.server.tsx 파일의 내용을 초기 렌더링하여 정적 파일을 클라이언트에 전달하는 형식으로 바꿉니다.

서버에서 렌더링하여 클라이언트로 전달

// ./src/index.server.tsx

import React from 'react';
import ReactDOMServer from 'react-dom/server';
import express from 'express';
import { StaticRouter } from 'react-router-dom';
import App from './App';

const app = express();

// 서버 사이드 렌더링을 처리할 핸들러 함수
const serverRender = (req, res, next) => {
  // 이 함수는 404가 떠야하는 상황에 404를 띄우지 않고 서버 사이드 렌더링을 해준다.

const context = {}; const jsx = ( <StaticRouter location={req.url} context={context}> <App /> </StaticRouter> ); const root = ReactDOMServer.renderToString(jsx); // 렌더링 res.send(root); // 클라이언트에 결과물 응답 }; app.use(serverRender); app.listen(5000, () => { console.log(`Now listening on port 5000`); });

문제 발생: StaticRouter context api 지원 중단

  • 원래는 StaticRouter 에 context 라는 props 를 넣어주고, 이 값을 사용해 나중에 렌더링 한 컴퍼넌트에 따라 HTTP 스테이터스 코드를 설정하려고 했습니다만…
  • StaticRouter context API가 삭제되었다는…

react-router-dom v6 StaticRouter context is not working

https://github.com/remix-run/react-router/releases/tag/v6.0.0-alpha.4

⇒ 향후 ContextProvider를 사용하여 HTTP 상태 코드를 확인할 예정

TypeScript 환경에서 React Context API를 올바르게 활용

일단 컨텍스트를 제외하고 다음과 같이 쓰자.

// ./src/index.server.tsx

import React from 'react';
import ReactDOMServer from 'react-dom/server';
import express from 'express';
import { StaticRouter } from 'react-router-dom/server';
import { Location } from 'react-router-dom';
import App from './App';

type Request = { url: string | Partial<Location> };
type Response = { send: (arg0: string) => void };

const app = express();

// 서버 사이드 렌더링을 처리할 핸들러 함수
const serverRender = (req: Request, res: Response) => {
  // 이 함수는 404가 떠야하는 상황에 404를 띄우지 않고 서버 사이드 렌더링을 해준다.

// 따라서 Not Found 처리가 반드시 이뤄져야함 const jsx = ( <StaticRouter location={req.url}> <App /> </StaticRouter> ); const root = ReactDOMServer.renderToString(jsx); // 렌더링 res.send(root); // 클라이언트에 결과물 응답 }; app.use(serverRender); app.listen(5000, () => { console.log(`Now listening on port 5000`); });

그럼 실행해 보겠습니다.

실행

npm run build:server
npm run start:server

연결해 보면 렌더링도 잘 되고 라우팅도 잘 되는 것을 확인할 수 있다.

💡 서버에서 라우팅할 때 useNavigate 후크 대신 Link 태그를 사용해야 합니다.

useNavigate 후크는 client side rendering에서만 작동합니다.


(네트워크) 탭을 열면 서버에서 렌더링된 html 파일이 첫 번째 응답에서 왔는지 확인할 수 있습니다.


정적 파일에 액세스(JS, CSS)

왜 스타일이 이미 입고 있나요?

  • 현재까지의 설정에서는 정적 파일을 로드하지 않는다.

    (JavaScript 또는 CSS)
  • 현재 스타일이 베풀어지고 있는 이유는 emotion을 사용했기 때문이다.

  • emotion 대신 CSS 또는 styled-component를 사용하는 경우 서버는 CSS 정적 파일에 액세스할 수 없으면 스타일이 적용되지 않습니다.

Emotion – Server Side Rendering

위의 공식 문서를 보면 추가 설정 없이 emotion/react 또는 emotion/styled만을 사용하는 경우 Emotion 10 이상에서 서버측 렌더링이 즉시 작동한다는 것을 알 수 있습니다.

그 아래의 내용은 css에서 first-child와 같은 n-child를 사용할 때 서버 사이드 렌더링이 작동하면 unsafe이므로 따로 설정하는 내용이다.

emotion: SSR에서 n-child를 사용하지 마십시오.

어쨌든 현재 CSS를 연결할 필요는 없지만 결국 JS를로드하기 위해 서버에서 정적 파일을 호출 할 수 있도록 구성해야합니다.

정적 파일 액세스 설정

  • Express의 정적 미들웨어를 사용하여 서버를 통해 build 폴더의 정적 파일에 액세스할 수 있습니다.

  • build 폴더는 client side build를 하면 생성되는 폴더입니다.

  • npm run build를 사용하여 build 폴더에 asset-manifest.json 파일을 열어 보면 정적 파일 경로가 들어 있습니다.

  • {“files”: {“main.js”: “/static/js/main.581b925d.js”, “index.html”: “/index.html”, “main.581b925d.js.map”: “/ static/js/main.581b925d.js.map” }, “entrypoints”: ( “static/js/main.581b925d.js” ) }
  • emotion 이외의 css 등을 사용하면 main.css도 있을 것이다.

  • “main.js”: “/static/js/main.581b925d.js”, “main.css”: “~~”
  • 이 두 파일을 html 내부에 삽입해야합니다.

  • index.server.tsx에 설정을 넣습니다.

// ./src/index.server.tsx

import React from 'react';
import ReactDOMServer from 'react-dom/server';
import express from 'express';
import { StaticRouter } from 'react-router-dom/server';
import { Location } from 'react-router-dom';
import path from 'path';
import fs from 'fs';
import App from './App';

type Request = { url: string | Partial<Location> };
type Response = { send: (arg0: string) => void };

const manifest = JSON.parse(fs.readFileSync(path.resolve('./build/asset-manifest.json'), 'utf8'));

function createPage(root: string) {
  return `<!
DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="shortcut icon" href="http://polarmin./favicon.ico" /> <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no" /> <meta name="theme-color" content="#000000" /> <title>React App</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"> ${root} </div> <script src="${manifest.files('main.js')}"></script> </body> </html> `; } const app = express(); // 서버 사이드 렌더링을 처리할 핸들러 함수 const serverRender = (req: Request, res: Response) => { // 이 함수는 404가 떠야하는 상황에 404를 띄우지 않고 서버 사이드 렌더링을 해준다.

// 따라서 Not Found 처리가 반드시 이뤄져야함 const jsx = ( <StaticRouter location={req.url}> <App /> </StaticRouter> ); const root = ReactDOMServer.renderToString(jsx); // 렌더링 res.send(createPage(root)); // 클라이언트에 결과물 응답 }; const serve = express.static(path.resolve('./build'), { index: false, // "/"경로에서 index.html을 보여주지 않도록 설정 }); app.use(serve); // 순서 중요. serverRender 전에 위치해야한다.

app.use(serverRender); app.listen(5050, () => { console.log(`Now listening on port 5050`); });

코드를 차분히 살펴보면

const manifest = JSON.parse(
	fs.readFileSync(path.resolve('./build/asset-manifest.json'), 'utf8')
);
  • 먼저, 앞서 본 asset-mainfest.json에서 정적 파일 경로를 가져와야합니다.

  • mainifest 파일을 읽고 객체의 형태로 저장합니다.

const createPage = (root: string) => `
  <!
DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="shortcut icon" href="http://polarmin./favicon.ico" /> <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no" /> <meta name="theme-color" content="#000000" /> <title>React App</title> </head> <body> <div id="root"> ${root} </div> <script src="${manifest.files('main.js')}"></script> </body> </html> `;
  • 서버에서 클라이언트로 보내는 첫 번째 html 파일을 만들어야 합니다.

  • 서버에서 렌더링하고 루트에 넣기 HTML을 매개 변수로 받고 div root에 넣고 JavaScript 파일을 script 태그에 넣습니다.

  • css 파일도 있으면 link 태그에 넣어 주면 된다.

// 서버 사이드 렌더링을 처리할 핸들러 함수
const serverRender = (req: Request, res: Response) => {
  // 이 함수는 404가 떠야하는 상황에 404를 띄우지 않고 서버 사이드 렌더링을 해준다.

// 따라서 Not Found 처리가 반드시 이뤄져야함 const jsx = ( <StaticRouter location={req.url}> <App /> </StaticRouter> ); const root = ReactDOMServer.renderToString(jsx); // 렌더링 res.send(createPage(root)); // 클라이언트에 결과물 응답 }; const serve = express.static(path.resolve('./build'), { index: false, // "/"경로에서 index.html을 보여주지 않도록 설정 }); app.use(serve); // 순서 중요. serverRender 전에 위치해야한다.

app.use(serverRender);
  • 다른 부분은 같고, 원래는 res.send(root)로 root내에 내용만을 보내준 것을 위에서 작성한 html 내용 전체를 보내준다.

  • 그리고 “/” 루트 경로에서 index.html 대신 서버에서 보낸 html을 표시해야하므로 index : false로 설정합니다.

  • serverRender 전에 express static 미들웨어를 사용하여 서버를 통해 build에 액세스할 수 있도록 하고 server rendering을 수행합니다.

실행



  • 자바스크립트가 로드되었음을 알 수 있습니다.

그런데 여기까지만 하면, SSG는 끝난 것이다.

단지 정적 페이지가 있는 웹사이트는 지금 만들 수 있다.

다음 기사에서는 서버에서 api 요청을 통해 받은 데이터를 html에 넣는 데이터 로드에 대해 설명합니다.

마지막으로, Loadable Component를 사용한 코드 스플릿을 취급할 예정이다.

전체 코드는 아래 깃털 주소에서 확인할 수 있습니다.

https://github.com/leesunmin1231/React_SSR/tree/main/client