20. 서버 사이드 렌더링

히치키치·2022년 2월 2일
0

✔ 서버 사이드 렌더링

  1. 빈 페이지
  2. 자바스크립트 실행
  3. 컴포넌트 렌더링되며 사용자에게 보임

⭐ 서버 쪽에서 초기 렌더링을 하고 사용자가 HTML 전달 받을 때 그 내부에 렌더링된 결과물이 보임

✔ 장점

검색엔진의 웹 어플리케이션 수집이 수월함

  • 자바스크립트가 실행되지 않는 환경에서 SPA가 제대로 나타나지 않을 수 있음
  • 이 때 서버에서 클라이언트 대신 렌더링하면 검색 엔진이 페이지 내용을 수월하게 수집할 수 있음

초기 렌더링 성능 개선 가능
1. 서버 사이드 렌더링이 구현되지 않은 경우,
자바스크립트가 로딩되고 실행될 때까지 사용자는 비어 있는 페이지 보며 대기
2. 서버 사이드 렌더링을 구현한 경우,
자바스크립트 파일 다운로드가 완료되지 않은 상태에도
Html 상에 사용자가 볼 수 있는 콘텐츠가 존재해 대기 시간이 최소화

✔ 단점

  1. 서버 리소스가 사용
  2. 고려할 것이 많은 개발
    성능 최적화 : 캐싱, 로드 밸런싱, 데이터 미리 불러오기, 코드 스플리팅과의 호환\

✔ 서버 사이드 렌더링과 코드 스플리팅 충돌

페이지 깜박임 발생
1. 서버 사이드 렌더링된 결과물이 브라우저에 나타남
2. 자바스크립트 파일 로딩 시작
3. 자바스크립트가 실행되면서 앚딕 불러오지 않은 컴포넌트를 null로 렌더링함
4. 페이지에서 코드 스플리팅된 컴포넌트들이 사라짐
5. 코드 스플리팅된 컴포넌트들이 로딩된 이루 제대로 나타남

=> 라우트 경로마다 코드 스플리팅된 파일 중에서 필요한 모든 파일을 브라우저에서 렌더링라기 전에 미리 불러와야 힘

1. 컴포넌트 생성



2. 서버 사이드 렌더링 구현

  1. yarn eject를 통한 웹팩 커스터마이징 진행
  2. 서버 사이드 렌더링용 엔트리 생성
  • 엔트리 : 웹팩에서 가장 먼저 불러오는 파일로 여기서 시작해 내부에 필요한 다른 컴포넌트/모듈 불러옴

  • ReactDOMServer의 renderToString 함수 사용

  • 해당 함수에 JSX를 넣어서 호출 시 렌더링 결과를 문자열로 반환됨

index.server.js

import React from 'react';
import ReactDOMServer from 'react-dom/server';
 
const html = ReactDOMServer.renderToString(
    <div>Hello Server Side Rendering!</div>
);
 
console.log(html)
  1. 웹팩 환경 설정
  • 엔트리 파일을 웹팩으로 불러와 빌드 하기 위해 서버 전용 환경 설정 진행
  • srIndexJs : 불러올 파일의 경로
  • ssrBuild : 웹팩으로 처리한 뒤 결과물을 저장할 경로
  • config/path.js에 추가

config/path.js

(..)
  ssrIndexJs: resolveApp('src/index.server.js'), // 서버 사이드 렌더링 엔트리
  ssrBuild: resolveApp('dist'), // 웹팩 처리 후 저장 완료
(..)
  1. 웹팩 기본 설정
    빌드할 때 어떤 파일에서 시작해서 불러오고 어디에 그 결과 저장할지 정함
    config/webpack.config.server.js
const paths = reuqire('./paths');
 
module.exports = {
    mode: 'production', // 프로덕션 모드로 설정하여 최적화 옵션들을 활성화
    entry: paths.ssrIndexJs, // 엔트리 경로
    target: 'node', // node 환경에서 실행될 것이라는 점을 명시
    output: {
        path: paths.ssrBuild, // 빌드 경로
        filename: 'server.js', // 파일 이름
        chunkFilename: 'js/[name]/chunk.js', // 청크 파일 이름
        publicPath: paths.publicUrlOrPath, // 정적 파일이 제공될 경로
    }
};
  1. 로더 설정
  • 파일을 불러올 때 확장자에 맞게 필요한 처리해줌
  • 자바스크립트 : babel 사용 : 트랜스파일링 해줌
  • css : 모든 css코드를 결합
  • 이미지 파일 : 파일을 다른 경로에 따로 저장하고 그 경로를 자바스크립트 참조 가능하게 함
  • import 구문으로 react, react-dom/server 등 라이브러리 사용 : node_modules에서 찾아 사용
  • 라이브러리를 불러오면 빌드할 때 결과물 파일 안에 해당 라이브러리 관련 코드가 함께 번들링됨

config/webpack.config.server.js

const paths = reuqire('./paths');
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent'); // CSS Module의 고유 className을 만들 때 필요한 옵션
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
 
module.exports = {
    mode: 'production', // 프로덕션 모드로 설정하여 최적화 옵션들을 활성화
    entry: paths.ssrIndexJs, // 엔트리 경로
    target: 'node', // node 환경에서 실행될 것이라는 점을 명시
    output: {
        path: paths.ssrBuild, // 빌드 경로
        filename: 'server.js', // 파일 이름
        chunkFilename: 'js/[name]/chunk.js', // 청크 파일 이름
        publicPath: paths.publicUrlOrPath, // 정적 파일이 제공될 경로
    },
    module: {
        rules: [
            {
                oneOf: [
                    // 자바스크립트를 위한 처리
                    // 기존 webpack.config.js를 참고하여 작성
                    {
                        test: /\.(js|mjs|jsx|ts|tsx)$/,
                        include: paths.appSrc,
                        loader: require.resolve('babel-loader'),
                        options: {
                            customize: require.resolve(
                                'babel-preset-react-app/webpack-overrides'
                            ),
                            plugins: [
                                [
                                    require.resolve('babel-plugin-named-asset-import'),
                                    {
                                        loaderMap: {
                                            svg: {
                                                ReactComponent: '@svgr/webpack?-svgo![path]'
                                            }
                                        }
                                    }
                                ]
                            ],
                            cacheDirectory: true,
                            cacheCompression: false,
                            compact: false
                        }
                    },
                    // CSS를 위한 처리
                    {
                        test: cssRegex,
                        exclude: cssModuleRegex,
                        // exportOnlyLocals: true 옵션을 설정해야 실제 CSS 파일을 생성하지 않습니다.
                        loader: require.resolve('css-loader'),
                        options: {
                            onlyLocals: true
                        }
                    },
                    // CSS Module을 위한 처리
                    {
                        test: cssModuleRegex,
                        loader: require.resolve('css-loader'),
                        options: {
                            modules: true,
                            onlyLocals: true,
                            getLocalIdent: getCSSModuleLocalIdent
                        }
                    },
                    // Sass를 위한 처리
                    {
                        test: sassRegex,
                        exclude: sassModuleRegex,
                        use: [
                            {
                                loader: require.resolve('css-loader'),
                                options: {
                                    onlyLocals: true
                                }
                            },
                            require.resolve('sass-loader')
                        ]
                    },
                    // Sass + CSS Module을 위한 처리
                    {
                        test: sassRegex,
                        exclude: sassModuleRegex,
                        use: [
                            {
                                loader: require.resolve('css-loader'),
                                options: {
                                    modules: true,
                                    onlyLocals: true,
                                    getLocalIdent: getCSSModuleLocalIdent
                                }
                            },
                            require.resolve('sass-loader')
                        ]
                    },
 
                    // url-loader를 위한 설정
                    {
                        test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
                        loader: require.resolve('url-loader'),
                        options: {
                            emitFile: false, // 파일을 따로 저장하지 않는 옵션
                            limit: 10000, // 원래는 9.76KB가 넘어가면 파일로 저장하는데 emitFile 값이 false일 때는 경로만 준비하고 파일은 저장하지 않는다.
                            name: 'static/media/[name].[hash:8].[ext]'
                        }
                    },
                    // 위에서 설정된 확장자를 제외한 파일들은 file-loader를 사용한다.
                    {
                        loader: require.resolve('file-loader'),
                        exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
                        options: {
                            emitFile: false, // 파일을 따로 저장하지 않는 옵션
                            name: 'static/media/[name].[hash:8].[ext]'
                        }
                    }
                ]
            }
        ]
    },
    resolve: {
        modules: ['node-modules']
    }
};
  1. node_modules 제외 번들링
    webpack-node-externals 라이브러리 사용해 서버를 번들링할 때는 node_modules에서 불러오는 것을 제외하고 번들링할 수 있게 함
yarn add webpack-node-externals

config/webpack.config.server.js

const nodeExternals = require('webpack-node-externals')
(...)
 
module.exports = {
    (...)
    resolve: {
        modules: ['node-modules']
    },
    externals: [nodeExternals()] //추가
};
  1. 환경변수 주입
    process.env.NODE_ENV 값을 참조하여 현재 개발 환경 여부 확인 가능

config/webpack.config.server.js

const nodeExternals = require('webpack-node-externals')
const paths = reuqire('./paths');
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent'); // CSS Module의 고유 className을 만들 때 필요한 옵션
const webpack = require('webpack');
const getClientEnvironment = require('./env');
 
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
 
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
 
module.exports = {
    (...)
    externals: [nodeExternals()],
    plugins: [
        new webpack.DefinePlugin(env.stringified) 
        // 환경변수 주입
    ]
};
  1. 빌드 스크립트 작성
    클라이언트에서 사용할 빌드 파일 생성

scripts/build.server.js

process.env.BABEL_ENV = 'production';
process.env.NODE_ENV = 'production';
 
process.on('unhandledRejection', err => {
    throw err;
});
 
require('../config/env');
const fs = require('fs-extra');
const webpack = require('webpack');
const config = require('../config/webpack.config.server');
const paths = require('../config/paths');
 
function build() {
    console.log('Creating server build');
    fs.emptyDirSync(paths.ssrBuild);
    let compiler = webpack(config);
    return new Promise((resolve, reject) => {
        compiler.run((err, status) => {
            if (err) {
                console.log(err);
                return;
            }
            console.log(status.toString());
        });
    });
}
 
build();

빌드 결과!!

테스팅으로 작성한 JSX

package.json 을 수정해 매번 빌드시 파일 경로 작성하는 것 생략!!

✔ 서버 코드 작성하기

서버사이드 랜더링 처리할 서버 작성

//express 사용 
yarn add express

StaticRouter 컴포넌트가 사용

  • 서버 사이드 렌더링 위해 사용되는 라우터
  • props로 넣어주는 location값에 따라 라우팅 (지금은 req.url 사용)
  • props로 context 추가 : 렌더링한 컴포넌트에 따라 HTTP 상태 코드 설정 가능

index.server.js

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);
 
// 5000 포트로 서버를 가동
app.listen(5000, () => {
    console.log('Running on http://localhost:5000');
});

network 탭의 response 보면 컴포넌트 렌더링 결과가 문자열로 전달됨 확인 가능

✔ 정적 파일 제공

Express 내장된 static 미들웨어 사용해 서버를 통해 build에 있는 JS, CSS 정적 파일에 접근

index.server.js

import React from 'react';
import ReactDOMServer from 'react-dom/server';
import express from 'express';
import {StaticRouter} from 'react-router-dom';
import App from './App';
import path from 'path';
 
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); // 클라이언트에게 결과물을 응답한다.
};
 
const serve = express.static(path.resolve('./build'), {
    index: false // "/" 경로에서 index.html을 보여 주지 않도록 설정
});
 
app.use(serve); // 순서가 중요하다. serverRender 전에 위치해야 한다.
app.use(serverRender);
 
// 5000 포트로 서버를 가동
app.listen(5000, () => {
    console.log('Running on http://localhost:5000');
});
  1. JS와 CSS 불러오도록 html에 코드 삽입
    build 디렉터리의 asset-manifest.json을 통해 빌드하고 바뀌는 파일 이름 참고해 작성

index.server.js

(...)
// asset-manifest.json에서 파일 경로들을 조회한다.
const manifest = JSON.parse(
    fs.readFileSync(path.resolve('./build/asset-manifest.json'), 'utf8')
);
 
const chunks = Object.keys(manifest.files)
    .filter(key => /chunk\.js$/.exec(key)) // chunk.js로 끝나는 키를 찾아서
    .map(key => `<script src="${manifest.files[key]}"></script>`) // 스크립트 태그로 변환하고
    .join(''); // 합침
 
function createPage(root) {
    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>
       <link href="${manifest.files['main.css']}" rel="stylesheet" />
    </head>
    <body>
       <noscript>You need to enable JavaScript to run this app.</noscript>
       <div id="root">
          ${root} 
       </div>
       <script src="${manifest.files['runtime-main.js']}"></script>
       ${chunks}
       <script src="${manifest.files['main.js']}"></script>
    </body>
    `;
}
(...)


링크 눌러 이동 : 클라이언트 랜더링 : 네트워크 요청 추가로 발생 X
첫번째 랜더링 : 서버 통해 : 그 이후부터는 브라우저가 처리

✔ 데이터 로딩

서버의 경우 문자열 형태로 렌더링하기 때문에 state나 리덕스 스토어의 상태가 바뀐다고 해서 자동으로 리렌더링되지 않음
-> renderToString함수 다시 호출 하는 것이 필요함

  1. redux-thunk 준비
//설치
yarn add redux react-redux redux-thunk axios

Ducks 패턴 사용한 리덕스 모듈 작성

modules/users.js

thunk 함수 : getUsers
액션 GET_USERS_PENDING, GET_USERS_SUCCESS, GET_USERS_FAILURE 사용한 상태 관리
모듈 상태 : 객체 loading과 error

import axios from 'axios';
 
const GET_USERS_PENDING = 'users/GET_USERS_PENDING';
const GET_USERS_SUCCESS = 'users/GET_USERS_SUCCESS';
const GET_USERS_FAILURE = 'users/GET_USERS_FAILURE';
 
const getUsersPending = () => ({type: GET_USERS_PENDING});
const getUsersSuccess = payload => ({type: GET_USERS_SUCCESS, payload});
const getUsersFailure = payload => ({
    type: GET_USERS_FAILURE,
    error: true,
    payload
});
 
export const getUsers = () => async dispatch => {
    try {
        dispatch(getUsersPending());
        const response = await axios.get(
            'https://jsonplaceholder.typicode.com/users'
        );
        dispatch(getUsersSuccess(response));
    } catch (e) {
        dispatch(getUsersFailure(e));
        throw e;
    }
};
 
const initialState = {
    users: null,
    user: null,
    loading: {
        users: false,
        user: false
    },
    error: {
        users: null,
        user: null
    }
};
 
function users(state=initialState, action) {
    switch (action.type) {
        case GET_USERS_PENDING:
            return {...state, loading: {...state.loading, users: true}};
        case GET_USERS_SUCCESS:
            return {
                ...state,
                loading: {...state.loading, users: false},
                users: action.payload.data
            };
        case GET_USERS_FAILURE:
            return {
                ...state,
                loading: {...state.loading, users: false},
                error: {...state.error, users: action.payload}
            };
        default:
            return state;
    }
}
 
export default users;

루트 리듀서 생성
modules/index.js

import {combineReducers} from 'redux';
import users from './users';
 
const rootReducer = combineReducers({users});
export default rootReducer;

Provider 컴포넌트 사용해 리덕스를 적용

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {BrowserRouter} from 'react-router-dom';
import {createStore, applyMiddleware} from 'redux';
import {Provider} from 'react-redux';
import thunk from 'redux-thunk';
import rootReducer from './modules';
 
const store = createStore(rootReducer, applyMiddleware(thunk));
 
ReactDOM.render(
  <Provider store={store}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>
  ,
  document.getElementById('root')
);
 
reportWebVitals();
  1. Users, UsersContainer 컴포넌트 생성
    서버사이드 랜더링 시 이미 있는 정보는 재요청하지 않도록 처리
    components/Users.js
import React from 'react';
import {Link} from 'react-router-dom';
 
const Users = ({users}) => {
    if(!users) return null; // users가 유효하지 않다면 아무것도 보여주지 않음
    return (
        <div>
            <ul>
                {users.map(user => (
                    <li key={user.id}>
                        <Link to={`/users/${user.id}`}>{user.username}</Link>
                    </li>
                ))}
            </ul>
        </div>
    );
};
export default Users;

containers/UserContainer.js

(...)
const UsersContainer = ({users, getUsers}) => {
    // 컴포넌트가 마운트되고 나서 호출
    useEffect(() => {
        if (users) return; // users가 이미 유효하다면 요청하지 않음
        getUsers();
    }, [getUsers, users]);
    return <Users users={users} />;
};
(...)

컴포넌트 보여줄 페이지 컴포넌트 작성 후 라우팅 설정!!

✔ PreloadContext 만들기

  • 랜더링 전 API 요청으로 받은 데이터를 스토어에 담아야함
  • 서버에서는 클래스형 컴포넌트가 지니고 있는 constructor 메소드 사용 또는 render 함수 자체에서 처리
  • 요청이 끝날 때까지 대기했다가 재렌더링

lib/PreloadContext.js
서버 사이드 랜더링에서 처리할 일 실행 & 기다려야 하는 Promise 수집 (끝나면 재랜더링)
resolve라는 함수를 props를 받아서 컴포넌트가 렌더링될 때 서버환경에서 다시 resolve() 함수를 호출

import { createContext, useContext } from 'react'

const PreloadContext = createContext(null)
export default PreloadContext

export const Preloader = ({ resolve }) => {
  const preloadContext = useContext(PreloadContext)
  if (!preloadContext) return null
  if (preloadContext.done) return null

  preloadContext.promises.push(Promise.resolve(resolve()))
  return null
}

이를 UserContainer에서 사용하면..

containers/UserContainer.js

(...)
import {Preloader} from '../lib/PreloadContext'
 
const {useEffect} = React;
 
const UsersContainer = ({users, getUsers}) => {
    // 컴포넌트가 마운트되고 나서 호출
    useEffect(() => {
        if (users) return; // users가 이미 유효하다면 요청하지 않음
        getUsers();
    }, [getUsers, users]);
    return(
        <>
            <Users users={users} />;
            <Preloader resolve={getUsers} />
        </>
    );
};
(...)

서버에서 리덕스 설정 & PreloadContext 사용

서버가 실행될 때 스토어 한 번 생성 X, 요청이 들어올 때마다 새로운 스토어 생성

index.server.js

첫번째 랜더링 : renderToStaticMarkup 함수 사용 : 정적인 페이지를 만들 때 사용
-> 클라이언트 쪽에서 HTML DOM 인터랙션 지원 어려움
-> Preloader로 넣어 주었던 함수 호출 & renderToString보다 빠름

import React from 'react';
import ReactDOMServer from 'react-dom/server';
import express from 'express';
import {StaticRouter} from 'react-router-dom';
import App from './App';
import path from 'path';
import fs from 'fs';
import {createStore, applyMiddleware} from 'redux';
import {Provider} from 'react-redux';
import thunk from 'redux-thunk';
import rootReducer from './modules';
import PreloadContext, { Preloader } from './lib/PreloadContext';
 
// asset-manifest.json에서 파일 경로들을 조회한다.
const manifest = JSON.parse(
    fs.readFileSync(path.resolve('./build/asset-manifest.json'), 'utf8')
);
 
const chunks = Object.keys(manifest.files)
    .filter(key => /chunk\.js$/.exec(key)) // chunk.js로 끝나는 키를 찾아서
    .map(key => `<script src="${manifest.files[key]}"></script>`) // 스크립트 태그로 변환하고
    .join(''); // 합침
 
function createPage(root) {
    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>
       <link href="${manifest.files['main.css']}" rel="stylesheet" />
    </head>
    <body>
       <noscript>You need to enable JavaScript to run this app.</noscript>
       <div id="root">
          ${root} 
       </div>
       <script src="${manifest.files['runtime-main.js']}"></script>
       ${chunks}
       <script src="${manifest.files['main.js']}"></script>
    </body>
    `;
}
 
const app = express();
 
// 서버 사이드 렌더링을 처리할 핸들러 함수
const serverRender = async (req, res, next) => {
    // 이 함수는 404가 떠야 하는 상황에 404를 띄우지 않고 서버 사이드 렌더링을 해준다.
 
    const context = {};
    const store = createStore(rootReducer, applyMiddleware(thunk));
    
    const preloadContext = {
        done: false,
        promises: []
    };
    const jsx = (
        <PreloadContext.Provider value={preloadContext}>
            <Provider store={store}>
                <StaticRouter location={req.url} context={context}>
                    <App />
                </StaticRouter>
            </Provider>
        </PreloadContext.Provider>
    );
    ReactDOMServer.renderToStaticMarkup(jsx); // renderToStaticMarkup으로 한번 렌더링한다.
    try {
        await Promise.all(preloadContext.promises); // 모든 프로미스를 기다린다.
    } catch (e) {
        return res.status(500);
    }
    preloadContext.done = true;
    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);
 
// 5000 포트로 서버를 가동
app.listen(5000, () => {
    console.log('Running on http://localhost:5000');
});

✔ 스크립트로 스토어 초기 상태 주입

서버에서 만들어 준 상태를 브라우저에서 재사용하려면, 현재 스토어 상태를 문자열로 변환한 뒤 스크립트로 주입

브라우저에서 상태를 재사용할 때는 다음과 같이 스토어 생성 과정에서 window.PRELOADED_STATE를 초깃값으로 사용

index.server.js

(...)
 
function createPage(root, stateScript) {
    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>
       <link href="${manifest.files['main.css']}" rel="stylesheet" />
    </head>
    <body>
       <noscript>You need to enable JavaScript to run this app.</noscript>
       <div id="root">
          ${root}
       </div>
       ${stateScript}
       <script src="${manifest.files['runtime-main.js']}"></script>
       ${chunks}
       <script src="${manifest.files['main.js']}"></script>
    </body>
    `;
}
 
const app = express();
 
// 서버 사이드 렌더링을 처리할 핸들러 함수
const serverRender = async (req, res, next) => {
    (...)
    const root = ReactDOMServer.renderToString(jsx); // 렌더링을 한다.
    // JSON을 문자열로 변환하고 악성 스크립트가 실행되는 것을 방지하기 위해 <를 치환처리
    // https://redux.js.org/recipes/server-rendering#security-considerations
    const stateString = JSON.stringify(store.getState()).replace(/>/g, '\\u003c');
    const stateScript = `<script>__PRELOADED_STATE__ = ${stateString}</script>`; // 리덕스 초기 상태를 스크립트로 주입한다.
    res.send(createPage(root, stateScript)); // 클라이언트에게 결과물을 응답한다.
};
 
(...)

0개의 댓글