티스토리 뷰


 작년과 올해 들어 가장 각광받는 javascript library를 뽑아보면 React도 그 안에 들어갈 것이다. React의 장점을 생각해보면 Virtual DOM, JSX, Component 그리고 Flux 등 여러가지가 있을 것이다. 그 중에서 이번엔 서버 랜더링에 대해 알아보고 정리해 보도록 하자. React는 설계 단계부터 Server-Side Rendering(SSR)을 고려하여 설계되었다.  그렇다면 Server-Side Rendering이란 무엇이고, 어떤 장점이 있는 것일까? 이에 대해서는 다음 아티클을 읽어보도록 하자. '왜 React와 서버 사이드 렌더링인가?' 


 정리해 보자면 기존 웹어플리케이션은 javscript의 로딩이 모두 완료된 후 View를 그리기 시작한다. 그러나 서버 랜더링을 통해 미리 View를 만들어 내려주면 js가 로드되기 이전에도 사용자는 View를 볼 수 있다. 실제 사용자가 처음 UI를 보게 되는 시간이 js 로딩 후 렌더링되는 시간만큼 줄어드는 것이다. 또 js가 로딩되기 전엔 빈 화면만 보여주기 때문에, 크롤러(웹 사이트의 문서를 읽어와서 검색엔진에 저장해주는 bot)들이 접속했을 때 정보를 긁어갈 수 없으므로 검색 엔진에 노출되는데 불리한 점이 있다.(물론 구글같이 js를 실행해서 완성된 문서를 크롤링하는 곳도 있다.)


 그렇다면 한가지 의문이 떠오른다. 서버에서 랜더링을 해서 내려준 후에 클라이언트에서 다시 한번 랜더링하면 비효율적인게 아닌가? 결론부터 말하면 그렇지 않다. React의 경우는 하나의 소스로 Server-Side Rendering과 Client-Side Rendering 둘다 가능하다. React로 만들어진 코드는 서버에서 랜더링 된 후 클라이언트에서 React 소스가 실행되면 Checksum을 통해 로딩됬을때 바뀌어야 할 부분만 다시 랜더링 해준다. 


 자 그럼 실제로 예제 소스를 만들어 보며 이해해 보도록 하자. 이번 예제에서 서버는 nodejs+express를 사용할 것이다.  또 Webpack과 ES6, JSX등 기술 스택도 적당히 알고 있어야 이해하는데 문제가 없을 것이다. Webpack에 대해서 잘 모른다면 'Webpack2 시작하기' 를 읽고 아래 내용을 진행하도록 하자. 



Project Setting

 먼저 React를 빌드할 수 있는 환경을 만들어 보도록 하자. 여기서는 Webpack과 Babel을 이용해서 빌드할 것이다. 먼저 npm을 이용해서 Webpack을 설치하자.

$ npm install webpack --save-dev

다음으로 Babel을 설치하도록 하자. Babel은 ES2015 문법과 React를 함께 빌드해주는 역할을 한다.

$ npm install --save-dev babel-cli babel-core babel-loader babel-preset-env babel-preset-react

이제 React를 설치하자.

$ npm install --save react react-dom


 이제 Webpack build 설정을 해보도록 하자. 프로젝트 루트 디렉토리에 webpack.config.js 파일을 만든 후 다음과 같이 작성한다. 중요한 부분은 module 부분이다. babel-loader를 통해서 webpack에서 build할때 babel을 사용할 수 있다. option으로 앞에서 설치한 ES2015(env)과 react를 주면 빌드될 때 자동으로 ES5코드로 트랜스파일링 된다.


webpack.config.js

const { resolve } = require('path');
const webpack = require('webpack');

module.exports = {
context : resolve(__dirname, 'client'),

entry : ['./index.js'],
output : {
filename : 'client.bundle.js', // output filename
path : resolve(__dirname, 'build'), // output path
publicPath : '/'
},

devtool : 'inline-source-map',

module : {
rules : [
{
test : /\.jsx?$/,
exclude : /node_modules/,
use : {
loader : 'babel-loader',
options : {
presets : ['env', 'react'] // ES2015, React를 이용해서 빌드한다.
},
}
}
],
}
};


 이제 간단한 Client와 Server Source를 작성해 보도록 하자. 우리는 Server에서 랜더링 될 땐 'Server-Side Rendering.' 글자가 보이고, Client 로딩이 끝났을 땐 'Client Loaded' 글자가 보이는 코드를 만들어 볼 것이다. 다음은 전체적인 폴더 구조이다.

Root
|-client
| |- app.js
| |- index.js
|-server
| |- Html.js
| |- index.js
|- .babel.rc
|- package.json
|- webpack.config.js

Client Source

 이제 간단한 Client Source를 작성해 보도록하자. index.js 파일을 만든 후 React를 DOM에 바인딩 하는 소스를 적어주자. App.js에서는 텍스트를 출력하는 Component를 만들어 주자. App은 text라는 state를 가지고 있으며 이를 <h1>태그로 출력해준다. ComponentDidMount() 함수는 React에서 제공하는 Lifecycle 함수이며, 컴포넌트가 Mount되었을 때 호출되는 함수이다. 지금은 비워두도록 하자.

client/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App'

ReactDOM.render(
<App/>,
document.getElementById('root')
);

client/App.js

import React, { Component } from 'react';

class App extends Component {

constructor(props){
super(props);
this.state = {
text : 'Server-Side Rendering'
}
}

render() {
return (
<div>
<h1>{this.state.text}</h1>
</div>
);
}

componentDidMount(){

}
}

export default App

 다음과 같이 작성 후 다음 명령어를 실행해 보자.

$ webpack

 루트에 build라는 폴더가 생성되고 그 안에 client.bundle.js라고 빌드된 번들 파일이 생성된 것을 확인할 수 있다. 제대로 작동하는지 확인해 보기 위해선 html파일을 생성한 후 bundle 파일을 실행시켜보면 된다. 다음과 같은 html을 생성한 후에 로컬 서버에 띄어보도록 하자. Server-Side Rendering 글자가 나타나면 제대로 build 된 것이라 할 수 있다.


text.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>React Server-Side Rendering</title>
</head>
<body>
<div id="root"></div>
</body>
<script src="build/client.bundle.js"></script>
</html>

[그림] 실행 결과


Server Source

 이제 Server Source를 작성해 보도록 하자. 간단한 개발 환경도 설정해 볼 것이다. 먼저 nodejs에서 많이 사용하는 express framework를 설치해 보도록하자.

$ npm install express --save

 자 이제 간단한 웹 서버 코드를 만들 것이다. http://localhost:3000/ 으로 접속하면 'Hello React Server-Side Rendering 글자를 보여주게 하자.  server 폴더 하위에 index.js 파일을 만든 후 다음과 같이 작성한다.


server/index.js

import express from 'express'

const app = express();
const port = 3000;

app.get('/',function(req, res, next){
res.send('<h1>Hello React Server-Side Rendering</h1>');
});

app.listen(port, ()=>{
console.log('http://localhost:3000')
});


 이제 서버를 실행시켜 보도록하자. 기존의 node server를 실행시키면 error가 발생할 것이다. 그 이유는 'import' 문법인데 현재 node에서는 module문법을 지원하고 있지 않기 때문에 babel을 이용해서 실행시켜 주어야 한다. 그럼 이제부터 babel-node를 이용해서 서버를 띄어보도록 하자. 그전에 babel 설정 파일을 작성해 주어야 한다. 루트 디렉토리 밑에 .babelrc 파일을 만든 후 다음과 같이 적어주도록 하자.


.babelrc

{
"presets": [
"env",
"react"
]
}


이제 terminal에서 다음 명령어를 실행시켜 준 후에 http://localhost:3000 으로 접속해 보도록 하자.

$ babel-node server/index.js

[그림] 서버 실행 결과



 본격적으로 Server-Side Rendering 소스를 작성하기에 앞서 간단한 서버 개발 환경을 만들어 보도록 하자. 소스코드 수정 후 일일이 서버를 재시작하며 확인하기란 여간 귀찮은 일이 아닐 수 없다. 따라서 js를 watch하고 있다가 자동적으로 reload해주는 nodemon이란 모듈을 사용해 보도록 하자. nodemon에 관해 더 자세한 내용은 npm-nodemon을 참고하도록 하자.

$ npm install nodemon -g

 설치 후 다음과 같이 실행한다. (node에서는 폴더명을 지정하면 폴더의 index.js를 찾아 실행시켜 준다.)

$ nodemon --exec babel-node server

 nodemon이 실행된 후 서버 소스코드를 수정해보자. index.js에서 느낌표 두개를 더 붙여보았다. 그 후 서버가 재시작되면서 변경된 부분이 반영되는 것을 확인할 수 있다.

res.send('<h1>Hello React Server-Side Rendering !!</h1>');


Server-Side Rendering Code

 이제 본격적으로 서버에서 React를 Rendering하는 코드를 작성해보도록 하자. server-side에서 component를 rendering하기 위해서는 react-dom에 있는 renderToString() Method를 사용한다. server에서 View를 그릴 때 초기값을 prealoadedState라 할때, client에서도 똑같은 props를 넘겨주어야 한다. 따라서 DOM이 모두 그려졌을때 window객체에 임시로 props를 전달한 후 이를 App의 props에 전달한다.


server/index.js

// Express
import express from 'express'

// React
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import App from '../client/App'
import Html from './Html'

const app = express();
const port = 3000;

app.get('/',function(req, res, next){

let preloadState = {
text : 'Server-Side Rendering'
};

let renderProps = {
preloadState: `window.__PRELOADED_STATE__ =${JSON.stringify(preloadState).replace(/</g, '\\u003c')}`,
script: 'http://localhost:3000/build/client.bundle.js',
appComponent: ReactDOMServer.renderToString(<App data={preloadState}/>)
};

const html = ReactDOMServer.renderToStaticMarkup(<Html {...renderProps}/>); // server-side Rendering

res.send(`<!doctype html>${html}`);
});

app.listen(port, ()=>{
console.log('http://localhost:3000')
});

 renderToStaticMarkup() method를 이용해서 html파일을 생성하도록하자. server폴더 밑에다가 html.js 파일을 만든 후 다음과 같이 적어주도록 하자. 정적 html파일에 미리 rendering된 App Component를 넣어준 후 res.send() method를 통해 클라이언트에게 응답을 보내준다.


server/html.js

import React from 'react';

const Html = ({preloadState, script, appComponent}) => (
<html className="no-js" lang="en">
<head>
<meta charSet="utf-8" />
<meta httpEquiv="x-ua-compatible" content="ie=edge" />
<title>React Server-Side Rendering</title>
<script dangerouslySetInnerHTML={{ __html: preloadState }}></script>
</head>
<body>
<div id="root" dangerouslySetInnerHTML={{ __html: appComponent }} />
<script src={script} />
</body>
</html>
);

export default Html;


 이제 preloadState를 이용해서 서버에서 랜더링된 결과엔 'Server-Side Rendering', 클라이언트가 로드 된 후엔 'Client Loaded' 글자가 보이도록 할 것이다. 먼저 View에서 보이는 글자를 props로 관리하도록 변경할 것이다. 먼저 클라이언트 App Component에 props를 전달해 주어야 하므로 client/index.js를 수정하도록 하자. 우리는 서버에서 랜더링 할때 prealodState를 window객체를 통해서 전달했다. 따라서 다음과 같이 수정한다.


client/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App'

ReactDOM.render(
<App data={window.__PRELOADED_STATE__}/>,
document.getElementById('root')
);

 다음으로 App.js에서 보여주는 글자는 state로 관리하고 있었다. constructor부분을 다음과 같이 수정한다. 다음으로 componentDidMount() 함수에서 setState함수를 이용해서 text state를 변경하도록 하자. 여기서 한가지 알아둬야 할 사항이 있다. server에서 rendering할 땐 React Lifecycle에서 comonentDidMount 직전까지 랜더링 된다. 따라서 client에서 js가 로드된 후에 checksum이 실행되고 이상이 없으면 componentDidMount() 함수가 실행이 된다. 정리하면 componentDidMount() 함수는 오직 client에서만 실행이 되는 것이다. 따라서 ajax call이나 rendering이 완료된 후에 실행할 로직들은 componentDidMount()에서 실행시켜야 한다.


client/App.js

import React, { Component } from 'react';

class App extends Component {

constructor(props){
super(props);
this.state = {
text : this.props.data.text
}
}

render() {
return (
<div>
<h1>{this.state.text}</h1>
</div>
);
}

componentDidMount(){
this.setState({
text : 'Client Loaded'
})
}
}

export default App

이제 webpack 명령어를 실행해서 client를 빌드한 후에 다시 서버를 실행시켜 보자. 서버를 띄운 후 브라우저로 확인해보면 아직 Server-Side Rendering 글자만 보인다는 것을 알 수 있다. inspector창을 열면 client.bundle.js 경로를 찾이못해 404 Not Found가 발생했다는 에러를 볼 수 있다. 로컬 경로의 client.bundle.js 경로를 잡아주어도 되지만 여기서는 webpack을 이용해서 client source도 자동으로 빌드 한 후 http://localhost:3000/build/client.bundle.js 경로에 직접 올려 보도록 하자.


WebpackDevMiddleware

 WebpackDevMiddleware는 nodejs express에서 Webpack Build를 자동화 해주는 middleware이다. npm을 이용해서 간단하게 설치할 수 있다.

$ npm install webpack-dev-middleware --save-dev


server폴더 아래에 WDM.js란 파일을 만든 후 다음과 같이 적어주도록 하자. WebpackDevMiddleware에 대한 자세한 사용법에 대해선 github/webpack-dev-middleware을 참고하도록 하자. 이번 포스팅에서는 간단하게만 설명하도록 하겠다. 이 미들웨어를 사용하면 client source를 watch하고 있다가 request가 들어오거나 소스가 변경되면 자동으로 webpack.config.js파일을 읽어 Webpack 설정대로 빌드하게 된다. 또 빌드된 파일은 메모리에 올려 따로 저장하지 않는다.


server/WDM.js

import webpack from 'webpack'
import webpackDevMiddleware from 'webpack-dev-middleware';
import webpackConfig from '../webpack.config.js'

const webpackCompiler = webpack(webpackConfig);

export default webpackDevMiddleware(webpackCompiler,{

// lazy가 true일 경우, request가 있을 때만 reload.
lazy: false,

// watch options (only lazy: false)
watchOptions: {
aggregateTimeout: 150,
poll: true
},

// 빌드된 소스가 올라갈 memory path
publicPath: '/build',

stats: {
colors: true
},
serverSideRender: true,
});


자 이제 server/index.js 에 다음과 같이 WDB을 불러온 후 middleware에 추가해 주도록 하자.

import WDM from './WDM'
app.use(WDM);


server/index.js

// Express
import express from 'express'

// React
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import App from '../client/App'
import Html from './Html'
import WDM from './WDM'

const app = express();
const port = 3000;

app.use(WDM);

app.get('/',function(req, res, next){

let preloadState = {
text : 'Server-Side Rendering'
};

let renderProps = {
preloadState: `window.__PRELOADED_STATE__ =${JSON.stringify(preloadState).replace(/</g, '\\u003c')}`,
script: 'http://localhost:3000/build/client.bundle.js',
appComponent: ReactDOMServer.renderToString(<App data={preloadState}/>)
};

const html = ReactDOMServer.renderToStaticMarkup(<Html {...renderProps}/>); // server-side Rendering

res.send(`<!doctype html>${html}`);
});

app.listen(port, ()=>{
console.log('http://localhost:3000')
});


 이제 'nodemon --exec babel-node server' 를 실행 한 후 서버에 접속해 보도록 하자. Server에서 rendering된 직후와 js가 로드됬을 때 글자가 달라지는 것을 볼 수 있다.



[그림] Server-Side Rendering이 반영된 모습


마치며

 처음 Server-Side Rendering 코드를 만들 땐 관련 예제를 찾기가 쉽지 않았다. 문서가 있어도 버전이 달라서 안되는 부분이 많았고, webpack이라던가 babel등 연동할때도 많은 삽질이 필요했다. 그러나 요즘엔 관련 포스팅이 많으므로 예전보단 쉽게 개발 환경을 만들 수 있지 않을까 한다. 위에서 만든 개발환경은 정말 초 간단 버전이므로 실제 개발 환경에서는 좀 더 추가적인 설정이 필요하다(react-hot-reload 라던가 좀 더 좋은 모듈들이 많이 존재한다. babel-node도 production에선 사용하면 안된다). 해당 부분에 대해서는 직접 찾아보길 바란다. 또 React하면 빼놓을 수 없는게 Redux인데 Redux를 사용한 Server-Side Rendering 방법은 다음 포스팅을 읽어보도록 하자(Redux Server Rendering). 엄청 친절하게 잘 써놓았다. 아무튼 이번 포스팅을 하면서 React와 Server Rendering에 관한 부분을 확실하게 정리할 수 있어서 뭔가 개운하다.


Source Code : https://github.com/haegul/study_example/tree/react/server-side-rendering

신고

'Language > JavaScript' 카테고리의 다른 글

TDD & Mocha - Javascript Test Framework  (0) 2017.05.14
Javascript Image Filter - convolution  (0) 2017.05.02
React Server Side Rendering  (4) 2017.04.30
nodejs error handling  (0) 2017.04.23
Javascript ES6 Proxy  (0) 2017.04.16
Javascript Image Filter 만들기  (5) 2017.04.15
댓글
  • 프로필사진 개발자 감사합니다, 잘 정리해주신 내용 잘 보고 있습니다

    webpackdevmiddleware 첫 부분에서

    $ webpack-dev-middleware --save-dev
    가 아니라
    $ npm install --save-dev webpack-dev-middle


    --

    WDM 파일의 주소가
    server/WDM.js 로




    이렇게 수정되어야 하지 않을까 생각합니다 ㅎㅎ
    2017.05.01 11:09 신고
  • 프로필사진 Dev-Momo 수정했습니다. 피드백 감사합니다 ㅎㅎ 2017.05.02 09:54 신고
  • 프로필사진 jason0853 포스팅 정말 감사합니다. 서버 사이드 렌더링 어느정도 이해하는데 도움이 되었습니다.

    질문이 있는데요. babel-node 를 production 에서는 사용하면 안된다고 했는데 그렇다면 es5로 전부 compile 한담에 production에 배포해야되는건가요?
    2017.05.02 16:52 신고
  • 프로필사진 Dev-Momo 네 webpack이나 babel로 컴파일 하셔서 배포하셔야합니다. 2017.05.05 21:37 신고
댓글쓰기 폼