Woongjae Lee
NHN Dooray - Frontend Team
2woongjae@gmail.com
import * as ReactDOM from 'react-dom';
ReactDOM.render(/*React.Element*/, document.querySelector('#root'));
// type definition
export function render<P extends DOMAttributes<T>, T extends Element>(
element: DOMElement<P, T>,
container: Element | null,
callback?: (element: T) => any
): T;
export function render<P>(
element: SFCElement<P>,
container: Element | null,
callback?: () => any
): void;
export function render<P, T extends Component<P, ComponentState>>(
element: CElement<P, T>,
container: Element | null,
callback?: (component: T) => any
): T;
export function render<P>(
element: ReactElement<P>,
container: Element | null,
callback?: (component?: Component<P, ComponentState> | Element) => any
): Component<P, ComponentState> | Element | void;
export function render<P>(
parentComponent: Component<any>,
element: SFCElement<P>,
container: Element,
callback?: () => any
): void;
import * as ReactDOMServer from 'react-dom/server';
ReactDOMServer.renderToString(/*React.Element*/);
ReactDOMServer.renderToStaticMarkup(/*React.Element*/);
// type definition
export function renderToString(element: ReactElement<any>): string;
export function renderToStaticMarkup(element: ReactElement<any>): string;
<div class="App" data-reactroot="" data-reactid="1" data-react-checksum="-272470372">
<div class="App-header" data-reactid="2">
<h2 data-reactid="3">Server</h2>
</div>
<p class="App-intro" data-reactid="4">
<!-- react-text: 5 -->To get started, edit
<!-- /react-text -->
<code data-reactid="6">src/App.tsx</code>
<!-- react-text: 7 --> and save to reload.
<!-- /react-text -->
</p>
</div>
/*
1. data-react-checksum 를 이용해서 클라이언트에서 이 html 을 재사용한다.
*/
<div class="App">
<div class="App-header">
<h2>Server</h2>
</div>
<p class="App-intro">To get started, edit
<code>src/App.tsx</code> and save to reload.
</p>
</div>
/*
1. 추가하는 어트리뷰트(data-react-checksum) 없이 리턴
2. 용량이 적겠죠?
3. 클라이언트에서 바뀐부분만 바꿔치기 하지 않을때
*/
Marks:ssr-basic mark$ serve -h
Usage: serve [options] [command]
Commands:
help Display help
Options:
-a, --auth Serve behind basic auth
-c, --cache <n> Time in milliseconds for caching files in the browser (defaults to 3600)
-n, --clipless Don't copy address to clipboard (disabled by default)
-C, --cors Setup * CORS headers to allow requests from any origin (disabled by default)
-h, --help Output usage information
-i, --ignore Files and directories to ignore
-o, --open Open local address in browser (disabled by default)
-p, --port <n> Port to listen on (defaults to 5000)
-S, --silent Don't log anything to the console
-s, --single Serve single page apps with only one index.html
-u, --unzipped Disable GZIP compression
-v, --version Output the version number
server {
listen 80 default_server;
server_name /var/www/example.com;
root /var/www/example.com;
index index.html index.htm;
location ~* \.(?:manifest|appcache|html?|xml|json)$ {
expires -1;
# access_log logs/static.log; # I don't usually include a static log
}
location ~* \.(?:css|js)$ {
try_files $uri =404;
expires 1y;
access_log off;
add_header Cache-Control "public";
}
# Any route containing a file extension (e.g. /devicesfile.js)
location ~ ^.+\..+$ {
try_files $uri =404;
}
# Any route that doesn't have a file extension (e.g. /devices)
location / {
try_files $uri $uri/ /index.html;
}
}
import * as path from 'path';
import * as express from 'express';
const app = express();
app.use(express.static(__dirname + '/build'));
app.get('*', (req, res) => {
res.sendFile(path.resolve(__dirname, 'build', 'index.html'));
});
app.listen(3000);
// tsconfig.server.json
{
"compilerOptions": {
"outDir": "build-server",
"module": "commonjs",
"target": "es6",
"noImplicitAny": true,
"sourceMap": true,
"jsx": "react",
"moduleResolution": "node",
"baseUrl": "src"
}
}
Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.
{
"scripts": {
"start": "react-scripts-ts start",
"build": "react-scripts-ts build && tsc -p tsconfig.server.json",
"test": "react-scripts-ts test --env=jsdom",
"eject": "react-scripts-ts eject",
"serve": "yarn build && node build-server/server.js"
}
}
// tsc -p tsconfig.server.json 사용을 위해
yarn add typescript -D
// express
yarn add express @types/express
import * as http from 'http';
import * as fs from 'fs';
import * as path from 'path';
import * as express from 'express';
import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';
import App from './App';
const app = express();
const server = http.createServer(app);
const staticFiles = [
'/static/*',
'/asset-manifest.json',
'/manifest.json',
'/service-worker.js',
'/favicon.ico'
];
staticFiles.forEach(file => {
app.get(file, (req, res) => {
const filePath = path.join(__dirname, '../build', req.url);
console.log(filePath);
res.sendFile(filePath);
});
});
app.get('*', (req, res) => {
const html = path.join(__dirname, '../build/index.html');
const htmlData = fs.readFileSync(html).toString();
const ReactApp = ReactDOMServer.renderToString(React.createElement(App));
const renderedHtml = htmlData.replace('{{SSR}}', ReactApp);
res.status(200).send(renderedHtml);
});
server.listen(3000);
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root">{{SSR}}</div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
module.js:471
throw err;
^
Error: Cannot find module './App.css'
at Function.Module._resolveFilename (module.js:469:15)
at Function.Module._load (module.js:417:25)
at Module.require (module.js:497:17)
at require (internal/module.js:20:19)
at Object.<anonymous> (/Users/mark/Project/react-with-typescript-test/react-ts-ssr-basic-cra/build-
server/App.js:4:1)
at Module._compile (module.js:570:32)
at Object.Module._extensions..js (module.js:579:10)
at Module.load (module.js:487:32)
at tryModuleLoad (module.js:446:12)
at Function.Module._load (module.js:438:3)
error Command failed with exit code 1.
module.js:471
throw err;
^
Error: Cannot find module './logo.svg'
at Function.Module._resolveFilename (module.js:469:15)
at Function.Module._load (module.js:417:25)
at Module.require (module.js:497:17)
at require (internal/module.js:20:19)
at Object.<anonymous> (/Users/mark/Project/react-with-typescript-test/react-ts-ssr-basic-cra/build-
server/App.js:4:14)
at Module._compile (module.js:570:32)
at Object.Module._extensions..js (module.js:579:10)
at Module.load (module.js:487:32)
at tryModuleLoad (module.js:446:12)
at Function.Module._load (module.js:438:3)
error Command failed with exit code 1.
app.get('*', (req, res) => {
console.log(req.url);
const html = path.join(__dirname, '../build/index.html');
const htmlData = fs.readFileSync(html).toString();
const ReactApp = ReactDOMServer.renderToString(React.createElement(App, {}, req.url));
const renderedHtml = htmlData.replace('{{SSR}}', ReactApp);
res.status(200).send(renderedHtml);
});
app.get('*', (req, res) => {
console.log(req.url);
const html = path.join(__dirname, '../build/index.html');
const htmlData = fs.readFileSync(html).toString();
const ReactApp = ReactDOMServer.renderToString(React.createElement(App, {}, req.url));
const renderedHtml = htmlData.replace('<div id="root">{{SSR}}</div>', `<div id="root">${ReactApp}</div><script id="initial-data" type="text/plain" data-json="${req.url}"></script>`);
res.status(200).send(renderedHtml);
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="manifest" href="/manifest.json">
<link rel="shortcut icon" href="/favicon.ico">
<title>React App</title>
<link href="/static/css/main.cacbacc7.css" rel="stylesheet">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<div class="App" data-reactroot="" data-reactid="1" data-react-checksum="1624593946">
<div class="App-header" data-reactid="2">
<img src="logo.svg" class="App-logo" alt="logo" data-reactid="3"/>
<h2 data-reactid="4">Welcome to React</h2>
</div>
<p class="App-intro" data-reactid="5">/hello</p>
</div>
</div>
<script id="initial-data" type="text/plain" data-json="/hello"></script>
<script type="text/javascript" src="/static/js/main.80616c3f.js"></script>
</body>
</html>
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import './index.css';
import './App.css';
const initialDataDom = document.querySelector('#initial-data');
let data: string = '';
if (initialDataDom !== null) {
const dataJson = initialDataDom.getAttribute('data-json');
if (dataJson !== null) {
data = dataJson;
}
}
ReactDOM.render(
<App>{data}</App>,
document.getElementById('root') as HTMLElement
);
registerServiceWorker();
BrowserRouter
서버
react-router
StaticRouter
// path="/"
const Home = () => {
return (
<div>
<h2>Home</h2>
<p><Link to="/">Home</Link></p>
<p><Link to="/hello">Hello</Link></p>
<p><Link to="/hi">Hi</Link></p>
</div>
);
};
// path="/hello"
const Hello = () => {
return (
<div>
<h2>Hello</h2>
<p><Link to="/">Home</Link></p>
<p><Link to="/hello">Hello</Link></p>
<p><Link to="/hi">Hi</Link></p>
</div>
);
};
class App extends React.Component<{}, {}> {
render() {
return (
<Switch>
<Route exact path="/" component={Home} />
<Route path="/hello" component={Hello} />
<Redirect from="*" to="/" />
</Switch>
);
}
}
import {BrowserRouter} from 'react-router-dom';
ReactDOM.render(
<BrowserRouter>
<App/>
</BrowserRouter>,
document.getElementById('root') as HTMLElement
);
import {StaticRouter} from 'react-router';
const ReactApp = ReactDOMServer.renderToString(
React.createElement(
StaticRouter,
{location: req.url, context: context},
React.createElement(App)
)
);
app.get('*', (req, res) => {
const html = path.join(__dirname, '../build/index.html');
const htmlData = fs.readFileSync(html).toString();
const context: {url?: string} = {};
const ReactApp = ReactDOMServer.renderToString(
React.createElement(
StaticRouter,
{location: req.url, context: context},
React.createElement(App)
)
);
if (context.url) {
res.redirect(301, '/');
} else {
const renderedHtml = htmlData.replace('{{SSR}}', ReactApp);
res.status(200).send(renderedHtml);
}
});
componentWillMount() {
console.log('App componentWillMount');
}
componentDidMount() {
console.log('App componentDidMount');
}
componentWillUnmount() {
console.log('App componentWillUnmount');
}
componentWillReceiveProps(nextProps: {}) {
console.log(`App componentWillReceiveProps : ${JSON.stringify(nextProps)}`);
}
shouldComponentUpdate(nextProps: {}, nextState: {}): boolean {
console.log(`App shouldComponentUpdate : ${JSON.stringify(nextProps)}, ${JSON.stringify(nextState)}`);
return true;
}
componentWillUpdate(nextProps: {}, nextState: {}) {
console.log(`App componentWillUpdate : ${JSON.stringify(nextProps)}, ${JSON.stringify(nextState)}`);
}
componentDidUpdate(prevProps: {}, prevState: {}) {
console.log(`App componentDidUpdate : ${JSON.stringify(prevProps)}, ${JSON.stringify(prevState)}`);
}
// App componentWillMount 만 발동
// 서버 랜더링과 클라이언트 랜더링 다 사용
componentWillMount() {
console.log('App componentWillMount');
}
// 클라이언트 랜더링만
componentDidMount() {
console.log('App componentDidMount');
}
// 클라이언트 랜더링만
componentWillUnmount() {
console.log('App componentWillUnmount');
}
public static async getInitialState(): Promise<string> {
const res = await request.get('https://api.github.com/users');
// error 처리를 해서 props 를 넘길수도 있다.
return res.body[0].login;
}
const user = await App.getInitialState();
const ReactApp = ReactDOMServer.renderToString(React.createElement(App, {user}));
const renderedHtml = htmlData.replace('<div id="root">{{SSR}}</div>', `<div id="root">${ReactApp}</div><script id="initial-data" type="text/plain" data-json="${user}"></script>`);
res.status(200).send(renderedHtml);
const initialDataDom = document.querySelector('#initial-data');
let data: string = '';
if (initialDataDom !== null) {
const dataJson = initialDataDom.getAttribute('data-json');
if (dataJson !== null) {
data = dataJson;
}
}
ReactDOM.render(
<App user={data} />,
document.getElementById('root') as HTMLElement
);
componentDidMount() {
console.log('App componentDidMount');
if (!this.props.user) {
App.getInitialState().then(user => {
this.setState({user: user});
});
}
}
// server 에서 이런 요소를 넣어서 내려줍니다.
<script>
window.__PRELOADED_STATE__ = "값";
</script>
// client 에서 preloadedState 를 만들어 Redux 의 초기 스토어 값으로 넣습니다.
const preloadedState = window.__PRELOADED_STATE__;
By Woongjae Lee
타입스크립트 한국 유저 그룹 리액트 스터디 201706