코린이의 개발 일지

React로 Server Side Rendering 구현하기 - 2. 서버 구축 본문

웹 (web)/프론트엔드

React로 Server Side Rendering 구현하기 - 2. 서버 구축

폴라민 2023. 3. 9. 18:32
반응형

서버 구축

이제 서버 사이드 렌더링을 처리할 서버를 작성할 차례이다.
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가 삭제되었다고 한다…

https://stackoverflow.com/questions/70487238/react-router-dom-v6-staticrouter-context-is-not-working

 

react-router-dom v6 StaticRouter context is not working

I'm trying to make SSR React web application. Everything works fine except staticContext. My server code is // IMPORTS const renderer = (req, store, context) => { const content = renderToSt...

stackoverflow.com

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

 

Release v6.0.0-alpha.4 · remix-run/react-router

Lots of great stuff in this release, especially if you like Intellisense :) Major Features Migrated the core codebase to TypeScript and added TypeScript declarations Added a migration guide for fo...

github.com

 

 

⇒ 추후에 ContextProvider 사용해서 HTTP 상태코드 체크할 예정
일단 context 제외하고 다음과 같이 적어보자.

// ./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 정적 파일에 접근 가능해야 스타일이 적용된다.

https://emotion.sh/docs/ssr
위의 공식 문서를 보면 추가 설정 없이 emotion/react나 emotion/styled만 사용하는 경우 Emotion 10이상에서 서버 측 렌더링이 즉시 작동함을 알 수 있다.
그 아래 내용은 css에서 first-child 와 같은 n-child를 사용할 때, 서버사이드 렌더링이 작동하면 unsafe하므로 따로 설정하는 내용이다.
https://www.notion.so/polarlsm/emotion-SSR-n-child-955a93a703f348eea741cdbdac1e5905
아무튼 현재는 CSS를 연결할 필요는 없지만 결국 JS를 로드 해오기 위해서 서버에서 정적파일을 불러올 수 있도록 설정을 해주어야한다.

 

emotion: SSR에서 n-child 사용하면 안되는 이유

Next에서 css에 first-child 대신 first-of-type으로 쓰는것이 안전하다.

www.notion.so

 

Emotion – Server Side Rendering

Server side rendering in Emotion 10 has two approaches, each with their own trade-offs. The default approach works with streaming and requires no additional configuration, but does not work with nth child or similar selectors. It's strongly recommended tha

emotion.sh

 

정적파일 접근 설정

  • Express의 static 미들웨어를 사용하여 서버를 통해 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="/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="/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 파일을 만들어주어야한다.
  • 서버에서 렌더링해서 root에 넣어줄 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을 해준다.

 

클라이언트 엔트리 파일

이제 서버에서 이미 초기 렌더링된 HTML 문서를 보내주므로 클라이언트에서 다시 root부터 render할 필요 없이 HTML골격에 이벤트를 붙여주기만 하면된다.

리액트는 구성되어 넘어오는 HTML 요소에는 자바스크립트를 붙여주고 그 안에 추가로 렌더링할 내부의 DOM을 관리한다. 그래서 일반적으로 root 태그와 함께 hydrate 메소드를 한번만 호출해주면 된다.

// ./src/index.tsx

import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';

function Root() {
  return (
    <BrowserRouter>
      <App />
    </BrowserRouter>
  );
}

const root = document.getElementById('root') as HTMLElement;
hydrateRoot(root, <Root />);

React 18부터 hydrate 메소드는 Deprecate 되었다고 하니 hydrateRoot를 쓰도록하자

https://beta.reactjs.org/reference/react-dom/hydrate

 

hydrate

A JavaScript library for building user interfaces

beta.reactjs.org

 

실행

  • 자바스크립트가 로드 된것을 볼 수 있다.

자 여기까지만 하면 SSG는 끝난것이다.
정적페이지만 있는 웹 사이트는 이제 만들 수 있다.

 

 

지금까지 Server에서 Components 컴파일해서 HTML문서 생성하고

클라이언트에서 응답으로 받은 HTML에 다운 받은 JavaScript 파일을 Hydration해서 이벤트 동작까지 하는 과정을 구현했다.

 

 

다음 글에서는 서버에서 api 요청을 통해 fetching한 데이터를 html에 넣어주는 데이터 로딩을 다룰 예정이다.


 
 
전체 코드는 아래 깃헙 주소에서 확인할 수 있다.
https://github.com/leesunmin1231/React_SSR/tree/main/client

 

GitHub - leesunmin1231/React_SSR: React로 server side rendering 구현하기

React로 server side rendering 구현하기. Contribute to leesunmin1231/React_SSR development by creating an account on GitHub.

github.com

 

반응형
Comments