SSR 기초(A beginner’s guide to React Server-Side Rendering 번역)

Cramming An·2022년 3월 29일
0

FrontEnd

목록 보기
2/9
post-thumbnail

번역 동기

View를 React로 작성하면서, 느낀 점 중에 하나는 브라우저 없이 virtual DOM을 구성할 수 있는 능력은 필연적으로 SSR을 이끌어낸다는 점 입니다. 따라서 React를 십분 활용하려면 SSR에 대한 알아야한다고 생각했습니다. (다수의 의역과 필요한 경우 생략이 있을 수 있습니다.)

기본 파일 구성

이번 샘플 프로젝트는 다음과 같은 파일 구성(https://github.com/course-one/react-ssr)을 가지고 있고, 웹팩과 Babel의 도움으로 React와 ES6 를 자바스크립트로 트랜스파일 할 예정입니다.
다음은 src.index.jssrc/components/app입니다.

#src/index
import React from 'react';
import ReactDOM from 'react-dom';

// import App components
import { App } from './components/app';

// compile App component in `#app` HTML element
ReactDOM.render( <App/>, document.getElementById( 'app' ) );
#src/components/app
import React from 'react';

// import child components
import { Counter } from '../counter';

// export entry application component
export class App extends React.Component {
    constructor() {
        console.log( 'App.constructor()' );
        super();
    }

    // render view
    render() {
        console.log( 'App.render()' );

        return (
            <div className='ui-app'>
                <Counter name='Monica Geller'/>
            </div>
        );
    }
}

파일 구성에서, src/index.js는 컴파일 시작점이고 src/components/app은 어플리케이션의 엔트리 컴포넌트 입니다. $ npm run start 라는 커멘드로 서버를 시작하게 되면, 브라우저에서 다음과 같은 화면을 보게 됩니다.

브라우저의 개발자 도구을 열게 되면 HTML이 만들어진 모습을 볼 수 있습니다. 모두들 잘 아시다시피, index.html<div id="app"></div> 라고 쓰여진 빈칸만 가지고 있습니다. 따라서 우리가 개발자 도구에서 보고 있는 이 HTML 파일은 App 컴포넌트에 의해 클라이언트 사이드에서 만들어진 것임을 알 수 있습니다.
서버의 응답을 찾아보면(개발자 도구에서), 다음 사진과 같이 여전히<div id="app"></div> 라고 쓰여진 빈칸만 가지고 있습니다.

이러한 현상은 검색 엔진 크롤러에게는 용납될 수 없기 때문에, 서버측에서 <div id="app"></div> 를 적절한 HTML파일로 채워줄 필요가 있습니다. 클라이언트 사이드의 경우, ReactDOM.render() 함수를 이용해 App 컴포넌트로 채워줄 수 가 있었습니다. 따라서 서버측에서도 비슷한 방법을 이용해야 할 것이지만, 이 방법이 말처럼 쉬운 것은 아닙니다. 시작해 봅시다.
Setting up an SSR server
어떤 종류의 프레임워크를 사용해도 상관이 없지만 저는 Express.js을 사용해 보겠습니다. server폴더 아래에 express.js와 index.js 다음과 같이 만들어 줍니다.

server/express.js

const express = require( 'express' );
const fs = require( 'fs' );
const path = require( 'path' );

// create express application
const app = express();

// serve static assets
app.get( /\.(js|css|map|ico)$/, express.static( path.resolve( __dirname, '../dist' ) ) );

// for any other requests, send `index.html` as a response
app.use( '*', ( req, res ) => {

    // read `index.html` file
    let indexHTML = fs.readFileSync( path.resolve( __dirname, '../dist/index.html' ), {
        encoding: 'utf8',
    } );

    // set header and status
    res.contentType( 'text/html' );
    res.status( 200 );

    return res.send( indexHTML );
} );

// run express server on port 9000
app.listen( '9000', () => {
    console.log( 'Express server started at http://localhost:9000' );
} );

server/index.js

require( './express.js' );

앞으로 server/index.js를 node를 이용해 실행을 시킬 것 입니다. 여기서 index.js을 사용하는 이유는 express.js의 동작을 SSR이 일어날 수 있도록 커스터마이징을 할 필요가 있기 때문입니다.
$ node server/index.js를 이용하여, dist 디렉토리로 부터 파일을 받은 HTTP 서버를 실행해 봅시다.

우리 HTTP 서버는 9000번 포트에서 돌아가고 있습니다. 위의 사진을 보면, SSR을 하지 않는 것 만 제외하고 모든게 잘 작동하는 것 처럼 보입니다. 이제 우리는 서버 사이드 렌더링에 대해 좀 더 생각을 해봐야할 시점에 왔습니다.
브라우저의 경우, src/index.js 파일은 어플리케이션의 시작점 입니다. index 파일은 App 컴포넌드를 import 하고 아래처럼 <div id="app"></div> 위치에 렌더링 합니다.

// import App components
import { App } from './components/app';
// compile App component in `#app` HTML element
ReactDOM.render( <App/>, document.getElementById( 'app' ) );

따라서 서버에서도 같은 방법을 사용해야 합니다. 서버에서 index.html을 App 컴포넌트를 이용해 <div id="app"></div> 부분을 HTML로 채워줄 필요가 있습니다. 이 때 의문이 하나가 생깁니다. "어떻게 App 컴포넌트로 부터 HTML을 가져올 수 있을까요?"
react-dom/server패키지는 ReactDOM.render()와 비슷한 역할을 하는 renderToString()이라는 함수를 가지고 있습니다. 하지만 이 함수는 DOM 엘리먼트을 채우는 대신, HTML string을 리턴하는 특징이 있습니다. 그럼 우리가 작성했던 server/express.js파일을 수정해보고, 브라우저로 응답이 도착하기전에, 리턴된 HTML string으로 <div id="app"></div>을 채워봅시다.

server/express.js

const express = require( 'express' );
const fs = require( 'fs' );
const path = require( 'path' );
const React = require( 'react' );
const ReactDOMServer = require( 'react-dom/server' );

// create express application
const app = express();

// import App component
const { App } = require( '../src/components/app' ); 

// serve static assets
app.get( /\.(js|css|map|ico)$/, express.static( path.resolve( __dirname, '../dist' ) ) );

// for any other requests, send `index.html` as a response
app.use( '*', ( req, res ) => {

    // read `index.html` file
    let indexHTML = fs.readFileSync( path.resolve( __dirname, '../dist/index.html' ), {
        encoding: 'utf8',
    } );

    // get HTML string from the `App` component
    let appHTML = ReactDOMServer.renderToString( <App /> );

    // populate `#app` element with `appHTML`
    indexHTML = indexHTML.replace( '<div id="app"></div>', `<div id="app">${ appHTML }</div>` );

    // set header and status
    res.contentType( 'text/html' );
    res.status( 200 );

    return res.send( indexHTML );
} );

// run express server on port 9000
app.listen( '9000', () => {
    console.log( 'Express server started at http://localhost:9000' );
} );

server/index.js

const path = require( 'path' );

// ignore `.scss` imports
require( 'ignore-styles' );

// transpile imports on the fly
require( '@babel/register')( {
    configFile: path.resolve( __dirname, '../babel.config.js' ),
} );

// import express server
require( './express.js' );

위의 express.js처럼, 우리는 App 컴포넌트를 import 했고, App 컴포넌트로 부터 HTML string을 얻었습니다. 그리고나서 <div id="app"></div>에 이 string을 채워넣었습니다. 하지만 우리는 React 컴포넌트를 import했고 당연히 JSX 문법도 사용하게 됐습니다. 따라서 우리는 express.js를 트랜스파일을 할 필요가 있고, Babel을 이용해 이를 진행해 봅시다.
우선 다음과 같은 패키지들이 필요합니다.
$ npm i -D @babel/register ignore-styles
우선 @babel/register.jsx 파일들은 자바스크립트로 트랜스파일 해주고, ignore-styles는 babel이 처리할 수 없는 .scss 파일을 무시할 수 있게 도와줍니다.
$ npm run startserver/index.js를 실행시켜주면 express.js 파일은 패키지에 의해 트랜스파일이 이루어지고, 아래 사진처럼 서버로부터 HTML이 미리 채워지는 모습을 볼 수 있습니다.

야호! 우리가 해냈습니다. 서버로 부터 받은 HTML 응답을 보면, 더이상 <div id="app"></div>는 비어있지 않습니다. 이게 바로 우리가 원하던 것입니다. 이제 검색엔진이 우리 웹사이트를 방문할 때, 비어있는 페이지로 더이상 보지 않을 것입니다.
지금까지 백엔드 시점에서 바라보았습니다. 하지만, 이제 프론트엔드의 몇가지 부분도 수정될 필요가 있습니다. 현재, 서버가 보내주고 있는 것은 프론드엔드에게는 별로 도움이 되지 않습니다. 우리 React 어플리케이션은 <div id="app"></div>가 비어있던지 채워져있던지 별로 신경쓰지 않습니다. React 어플리케이션은 예전과 같이(CSR) App 컴포넌트를 렌더링 할 것입니다.
바로 이런 상황을 우리는 피해야 합니다. 우리는 (서버측에서 이미 렌더링 한 부분을) 한 번 더 렌더링 할 필요가 없습니다. 서버측에서 렌더링 된 HTML을 재사용 할 수 있고, React로 하여금 이미 존재하는 HTML을 그냥 사용하도록 하게 할 수 있습니다. 그러면 React는 이미 존재하는 DOM 엘리먼트에 이벤트 리스너 만 붙이면 됩니다.
이렇게 서버측에서 렌더링 된 HTML을 그대로 이용하는 과정을 hydration 이라고 부릅니다. ReactDOM.render() 함수는 hydration을 하지 못 함으로, ReactDOM.hydrate()을 사용합니다.

// import App components
import { App } from './components/app';
// compile App component in `#app` HTML element
ReactDOM.hydrate( <App/>, document.getElementById( 'app' ) );

이 함수는 render 함수와 같은 역할을 하지만, 서버에서 렌더링 된 HTML을 사용합니다. 그러나 이 함수는 서버에서 렌더링 된 HTML과 클라이언트에서 렌더링 될 HTML이 같을 때 만 사용 가능합니다.

원문

https://medium.com/jspoint/a-beginners-guide-to-react-server-side-rendering-ssr-bf3853841d55

profile
La Dolce Vita🥂

0개의 댓글