μλ νμλκΉ,
Googleμ κ²μ μ½μμ λ‘κ·ΈμΈνκ³ Googleλ‘ κ°μ Έμ€λ©΄ μΉμ¬μ΄νΈκ° λΉ νμ΄μ§λ‘ λ λλ§λλ λ¬Έμ λ₯Ό λ°κ²¬νμ΅λλ€.
λλ μ½κ°μ μ°κ΅¬λ₯Ό νκ³ κ·Έ μ€ μΌλΆλ λλ₯Ό μ΄ κΈ°μ¬λ‘ μ΄λμμ΅λλ€.
https://medium.com/@gajus/react -application-seen-as-a-blank-page-via-fetch-as-google-afb11dff8562
κ²μ μμ§κ³Ό fetch as google μμ²΄κ° κ°μ λ°©μμΌλ‘ μλνμ§ μμ κ°λ₯μ±μ΄ λ§€μ° λμ΅λλ€. κ·Έλ μ§ μμΌλ©΄ μ§κΈμ―€ μμκ° λ¨μ΄μ‘μ κ²μ λλ€. κ·ΈλΌμλ λΆκ΅¬νκ³ μ΄ λ¬Έμ λ₯Ό ν΄κ²°νκ³ μΆμ΅λλ€. μΉμ¬μ΄νΈ μμ μκ° κΉ¨μ΄ μκΈ° λλ¬Έμ λλ€.
phantomjsλ₯Ό λ€μ΄λ‘λνλλ° Googleκ³Ό fetchκ° κ°λ€κ³ κ°μ νκ³ λ λλ§νλ €κ³ νλ©΄ μ¬λ¬ μ€λ₯κ° λ°νλ©λλ€.
μ΄ λ¬Έμ λ μ»΄νμΌλλ λμ λΈλΌμ°μ λ₯Ό λ³κ²½νλ κ²μ²λΌ κ°λ¨νκ² ν΄κ²°ν μ μμ§λ§ λ¨μν babel/polyfilμ μ€μΉνκ³ κ°μ Έμ€κΈ°κ° μλνμ§ μμ΅λλ€.
λꡬλ μ§ μ¬κΈ°μμ μ¬λ°λ₯Έ λ°©ν₯μΌλ‘ λλ₯Ό κ°λ¦¬ν¬ μ μμ΅λκΉ?
νμ΄μ§κ° 곡백μΌλ‘ νμλλ©΄ SSR λλ μν κ³νμ μλ°ν κ²μ λλ€. Razzleμ κΈ°λ³Έμ μΌλ‘ μ€λͺ λ λ΄μ©μΌλ‘ μΈν΄ μ΄λ €μμ κ²ͺμ§ μμΌλ©° μ€μ λ‘ BBC.comκ³Ό κ°μ SEO μ€μ¬ μ¬μ΄νΈμμ μ¬μ©λ©λλ€. server.js νμΌμ λΆμ¬λ£μ μ μμ΅λκΉ? μλ²μμ λ°μ΄ν°λ₯Ό κ°μ Έμ€κ³ μμ΅λκΉ?
μλ νμΈμ Jared,
μ μν λ΅λ³μ κ°μ¬λ립λλ€. μ, μλ²μμ λ°μ΄ν°λ₯Ό κ°μ Έμ΅λλ€.
μ¬κΈ° λ΄ server.jsκ° μμ΅λλ€.
import App from './App';
import React from 'react';
import Helmet from 'react-helmet';
import { StaticRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import express from 'express';
import compression from 'compression';
import { renderToString } from 'react-dom/server';
import configureStore from './store/configureStore';
import { remoteLoader } from './api/remoteLoader';
import serialize from 'serialize-javascript';
import { Capture } from 'react-loadable';
import { getBundles } from 'react-loadable/webpack';
import stats from '../build/react-loadable.json';
import { IS_PRODUCTION } from './components/shared/constants';
const assets = require(process.env.RAZZLE_ASSETS_MANIFEST);
const server = express();
server.use(compression());
if (!IS_PRODUCTION) {
server.set('cache', false);
}
server
.disable('x-powered-by')
.use(express.static(process.env.RAZZLE_PUBLIC_DIR, { Expires: '30d' }))
.get('/*', (req, res) => {
//Ensures clients with old css paths are served the current file
if (req.path.indexOf('/static/css') > -1 && assets.client.css) {
const currentCssFile = `${process.env.RAZZLE_PUBLIC_DIR}${assets.client.css}`;
return res.sendFile(currentCssFile);
}
remoteLoader(apiResult => {
const responseCode = typeof apiResult.status === 'undefined' ? 404 : apiResult.status;
if (responseCode === 301) {
return res.redirect(responseCode, apiResult.headers.location);
}
// Compile an initial state
const initialState = {
remote: {
cms: {
result: apiResult ? apiResult.data : false,
loading: false
},
myDrewberry: {
searchResults: null,
loading: false,
failed: false
}
}
};
// Create a new Redux store instance
const store = configureStore(initialState);
const context = {};
const modules = [];
const markup = renderToString(
<Capture report={moduleName => modules.push(moduleName)}>
<StaticRouter context={context} location={req.url}>
<Provider store={store}>
<App />
</Provider>
</StaticRouter>
</Capture>
);
const helmet = Helmet.renderStatic();
if (context.url) {
res.redirect(context.url);
} else {
const bundles = getBundles(stats, modules);
const chunks = bundles.filter(bundle => bundle.file.endsWith('.js'));
res.status(responseCode).send(
`<!doctype html>
<html ${helmet.htmlAttributes.toString()}>
<head>
${assets.client.css ? `<link rel="stylesheet" href="${assets.client.css}">` : ''}
${helmet.title.toString()}
${helmet.meta.toString()}
${helmet.link.toString()}
<link rel="icon" type="image/png" href="/favicon16.png" sizes="16x16"/>
<link rel="icon" type="image/png" href="/favicon32.png" sizes="32x32"/>
<link rel="icon" type="image/png" href="/favicon96.png" sizes="96x96"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/> <!--320-->
<meta http-equiv="expires" content="0">
</head>
<body class="drewberry-preload">
<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-TKPXWB');</script>
<!-- End Google Tag Manager -->
<div id="root">${markup}</div>
<script>
window.__PRELOADED_STATE__ = ${serialize(initialState)}
</script>
${
IS_PRODUCTION
? `<script src="${assets.client.js}"></script>`
: `<script src="${assets.client.js}" crossorigin></script>`
}
${chunks
.map(chunk =>
IS_PRODUCTION
? `<script src="/${chunk.file}"></script>`
: `<script src="http://${process.env.HOST}:${parseInt(process.env.PORT, 10) + 1}/${
chunk.file
}"></script>`
)
.join('\n')}
<script>window.main();</script>
</body>
</html>`
);
}
}, req.path);
});
export default server;
λλ μ¦μ Razzleμ΄ κ·Έκ²μ κ²ͺμ§ μκ³ μλ‘ μ€μΉλ₯Ό λ‘λνκ³ phantomjsλ₯Ό μ¬μ©νμ¬ κ²½κ³ κ° νμλμ§ μμμμ νμΈν μ μμ΅λλ€. μ΄λ μλ§λ μ°λ¦¬κ° μ€μΉν λΌμ΄λΈλ¬λ¦¬μΌ μ μμμ μλ―Έν©λλ€.
μμ λ΄μ©μμ λΆλͺ ν κ²μ΄ 보μ΄μλκΉ?
μκ° κΈ°λ¬κΈ° μ«λ κΈ°λΆ! :-)
λ°λΌμ λ΄ μ μ(κ·Έλ¦¬κ³ μ΄μ λν λ¬Έμμμ λ¬Έμ ν΄κ²° κ°μ΄λκ° νμν μ μμ)μ server.js
, client.js
, App.js
λ° μ μ¬μ μΌλ‘ Razzle μ ν리μΌμ΄μ
μ "hello world"μ κ°μ μνλ‘ λλ리기 μν΄ webpackμ μ¬λ¬ μΈ‘λ©΄μ μ¬μ μν©λλ€(μλ κ²½μ°). λν Razzleμ HTML ν
νλ¦Ώμ server.js
νμΌλ‘ λ€μ 볡μ¬νμ¬ λͺ¨λ μ€ν¬λ¦½νΈκ° μμμμ μ λλ‘ λ‘λλμλμ§ νμΈνλ κ²μ΄ μ’μ΅λλ€. μ€νλλ©΄ Chrome κ°λ° λꡬλ₯Ό μ΄κ³ "μ±λ₯" νμΌλ‘ μ΄λνμ¬ "μ€ν¬λ¦°μ·" νμΈλμ μ νν λ€μ μλ‘ κ³ μΉ¨ μμ΄μ½μ λλ¦
λλ€. μλ GIFλ₯Ό μ°Έμ‘°νμΈμ.
λͺ©νλ νμλΌμΈ μμμ λΉ ν°μ νλ μμ΄ μλμ§ νμΈνλ κ²μ λλ€. νλμ μμ΄ μ μ ν λ λλ§μ μ»μ νμλ μ΄μ μ½λλ₯Ό μ μ λ μΆκ°ν΄μΌ ν©λλ€. κ±Έμλ§λ₯Ό λΌμΈμ. μ΄κ²μ μ£Όμνμ§ μμΌλ©΄ μλ§μ΄ λκΈ° μ½μ΅λλ€.
κ·νμ νΉμ μ¬λ‘μ κ΄ν΄μλ λͺ¨λ Google νκ·Έ κ΄λ¦¬μ νλͺ©, λͺ¨λ CSS λ° μν μ€μΈ μ½λ λΆν μ μ κ±°νλ κ²μΌλ‘ μμνκ² μ΅λλ€. λ λλ§νκ³ κ±°κΎΈλ‘ μμ νκΈ°λ§ νλ©΄ λ©λλ€. λ€μμΌλ‘ Reduxλ₯Ό μλμν€μμμ€. κ·Έλ° λ€μ react-helmetμ λ€μ μΆκ°νκ³ , μ λλ‘ λμλ€κ³ μκ°λλ©΄ Lighthouse ν μ€νΈλ μννμ¬ λ€μ νμΈν©λλ€(κ°μ¬ ν). λμ€μ λ€μ λμκ° μ μλλ‘ μ΄ μνλ₯Ό μ λΆκΈ°μ 컀λ°ν©λλ€. κ·Έλ° λ€μ λΆμ μ€ν¬λ¦½νΈλ₯Ό λ€μ μΆκ°νκ³ λ λλ§μ λ€μ νμΈν©λλ€. λ§μ§λ§μΌλ‘ μ½λ λΆν μ λ€μ μΆκ°ν΄ 보μμμ€.
μ°Έκ³ : CSSλ₯Ό μ μ§νκ³ κ°λ° λͺ¨λμμ Razzleλ‘ μμ λΆμμ μννλ©΄ Razzleμ΄ κ°λ° μ€μ μλ²μμ
.css
μ€νμΌμ μ²λ¦¬νμ§ μκΈ° λλ¬Έμ FOUCλ₯Ό μ»μ μ μμ΅λλ€(ν΄λΌμ΄μΈνΈμμλ§). λ°λΌμ.css
νμΌμ μ¬μ©νλ κ²½μ° μ€νμΌ μνλ‘ μ μ ν SSRμ μννκ³ μλμ§ μμ ν ν μ€νΈνλ €λ©΄ νλ‘λμ μ© Razzleμ λΉλνκ³ νλ‘λμ μλ²λ₯Ό λ‘컬μμ μ€νν΄μΌ ν©λλ€. κ·Έλ¬λ λ¨μν μλ²μμ HTMLμ΄ μλμ§ νμΈνλ κ²½μ° κ°λ° λͺ¨λμμ μ€νν μ μμ΅λλ€.
μ’μμ, Razzleκ³Ό ν¨κ»ν λ©μ§ μμ μ κ°μ¬λ립λλ€. μ΄μ μ΄μ λν΄ κ°μ¬ν©λλ€. κ°μ¬ν μ λ³΄κ³ λ€μ μ¬λ¦¬κ² μ΅λλ€.
λ¬Όλ‘ "λΉμ΄ μλ"(κ³΅λ°±μ΄ μλλΌ μ€λ₯μ) κ²μμμ§ μ΅μ ν νμ΄μ§μμ κ°μ₯ λ¨Όμ νμΈν΄μΌ ν μ¬νμ νμ΄μ§κ° νμν λ°μ΄ν°λ₯Ό λ‘λνκΈ° μν΄ componentDidMount
μ μμ‘΄ν©λκΉ? curl
λ₯Ό μ¬μ©νμ¬ λΉ λ₯΄κ² νμΈν μ μμ΅λλ€. κ·Έλ λ€λ©΄ μ¬μ λ‘λνλ €λ©΄ After.jsμ κ°μ κ²μ΄ νμν©λλ€.
1000ms νμ μ£Όμμ λΉ νμ΄μ§κ° μλ κ²½μ° μ±λ₯ νμ μ¬μ©νμ¬ λλ²κΉ νλ λ°©λ²μ μ‘°μΈν΄ μ£Όμκ² μ΅λκΉ? λ΄ λ¬Έμ κ° μλ²μ λ‘λλμ§ μλ μμ΄μ½ κΈκΌ΄ λΌμ΄λΈλ¬λ¦¬μ κ΄λ ¨λμ΄ μμΌλ―λ‘ μ΄κΈ° λ‘λ μ μ΄λ¬ν λΉ λΉ μ¬κ°νμ΄ νμλλ€λ κ²μ μκ³ μμ΅λλ€...
μ, λ΄ λͺ¨λ μ¬μ©μ μ μ κΈκΌ΄μ ν¨μ¬ λμ€μκΉμ§ λ‘λλμ§ μμ΅λλ€. CSSμ @font-face {}λ₯Ό ν΅ν΄ κΈκΌ΄μ λ‘λνκ³ μμ΅λλ€.
@font-face {
font-family: 'SFProText';
font-display: auto;
src: url('fonts/SFPro/SF-Pro-Display-Regular.otf') format('truetype');
font-weight: normal;
font-style: normal;
}
@jaredpalmer λλ κ°μ λ¬Έμ κ° μμ΅λλ€. κΈκΌ΄ λ° μμ°μ΄ λ΄ ν΄λΌμ΄μΈνΈ νμ΄μ§μ λ‘λλμ§ μμ΅λλ€.
λ΄ μμ© νλ‘κ·Έλ¨ μ€μ μ webpack.config νμΌμ μΆκ°νμ§ μμμ΅λλ€. μ΄ νμΌμ κ·νμ 리ν¬μ§ν 리μμ 볡μ νμ΅λλ€. λ°©κΈ μ μ₯μμμ 볡μ¬λ³Έμ κ°μ Έ μμ npm λͺ¨λμ μ€μΉν λ€μ npm start λͺ
λ Ήμ μ€ννμ¬ μμ© νλ‘κ·Έλ¨μ μ¬μ©νκΈ° μμνμ΅λλ€.
μ΄ λ¬Έμ λ₯Ό ν΄κ²°νκΈ° μν΄ κ΅¬μ±νκΈ° μν΄ λμΉ κ²μ΄ μκ±°λ μ μΉν© νμΌμ μ¬μ©ν΄μΌ ν©λκΉ?
μ΄ λ¬Έμ λ₯Ό ν΄κ²°νλλ‘ λμμ£ΌμΈμ.
λ΄ App.css νμΌμ λ€μκ³Ό κ°μ΅λλ€.
@κΈκΌ΄ μΌκ΅΄ {
font-family: 'Roboto-medium';
src: url("./static/fonts/Roboto-Medium-webfont.eot");
src: url("./static/fonts/Roboto-Medium-webfont.eot?#iefix") format("embedded-opentype"), url("./static/fonts/Roboto-Medium-webfont.woff") format("woff"), url("./static/fonts/Roboto-Medium-webfont.ttf") format("truetype"); }
@κΈκΌ΄ μΌκ΅΄ {
font-family: 'Roboto-regular';
src: url("./static/fonts/robot-regular-webfont.eot");
src: url("./static/fonts/roboto-regular-webfont.eot?#iefix") format("embedded-opentype"), url("./static/fonts/roboto-regular-webfont.woff") format("woff"), url("./static/fonts/roboto-regular-webfont.ttf") format("truetype"); }
κ°μ¬ν©λλ€ inadvane :)
κ°μ₯ μ μ©ν λκΈ
λ°λΌμ λ΄ μ μ(κ·Έλ¦¬κ³ μ΄μ λν λ¬Έμμμ λ¬Έμ ν΄κ²° κ°μ΄λκ° νμν μ μμ)μ
server.js
,client.js
,App.js
λ° μ μ¬μ μΌλ‘ Razzle μ ν리μΌμ΄μ μ "hello world"μ κ°μ μνλ‘ λλ리기 μν΄ webpackμ μ¬λ¬ μΈ‘λ©΄μ μ¬μ μν©λλ€(μλ κ²½μ°). λν Razzleμ HTML ν νλ¦Ώμserver.js
νμΌλ‘ λ€μ 볡μ¬νμ¬ λͺ¨λ μ€ν¬λ¦½νΈκ° μμμμ μ λλ‘ λ‘λλμλμ§ νμΈνλ κ²μ΄ μ’μ΅λλ€. μ€νλλ©΄ Chrome κ°λ° λꡬλ₯Ό μ΄κ³ "μ±λ₯" νμΌλ‘ μ΄λνμ¬ "μ€ν¬λ¦°μ·" νμΈλμ μ νν λ€μ μλ‘ κ³ μΉ¨ μμ΄μ½μ λλ¦ λλ€. μλ GIFλ₯Ό μ°Έμ‘°νμΈμ.λͺ©νλ νμλΌμΈ μμμ λΉ ν°μ νλ μμ΄ μλμ§ νμΈνλ κ²μ λλ€. νλμ μμ΄ μ μ ν λ λλ§μ μ»μ νμλ μ΄μ μ½λλ₯Ό μ μ λ μΆκ°ν΄μΌ ν©λλ€. κ±Έμλ§λ₯Ό λΌμΈμ. μ΄κ²μ μ£Όμνμ§ μμΌλ©΄ μλ§μ΄ λκΈ° μ½μ΅λλ€.
κ·νμ νΉμ μ¬λ‘μ κ΄ν΄μλ λͺ¨λ Google νκ·Έ κ΄λ¦¬μ νλͺ©, λͺ¨λ CSS λ° μν μ€μΈ μ½λ λΆν μ μ κ±°νλ κ²μΌλ‘ μμνκ² μ΅λλ€. λ λλ§νκ³ κ±°κΎΈλ‘ μμ νκΈ°λ§ νλ©΄ λ©λλ€. λ€μμΌλ‘ Reduxλ₯Ό μλμν€μμμ€. κ·Έλ° λ€μ react-helmetμ λ€μ μΆκ°νκ³ , μ λλ‘ λμλ€κ³ μκ°λλ©΄ Lighthouse ν μ€νΈλ μννμ¬ λ€μ νμΈν©λλ€(κ°μ¬ ν). λμ€μ λ€μ λμκ° μ μλλ‘ μ΄ μνλ₯Ό μ λΆκΈ°μ 컀λ°ν©λλ€. κ·Έλ° λ€μ λΆμ μ€ν¬λ¦½νΈλ₯Ό λ€μ μΆκ°νκ³ λ λλ§μ λ€μ νμΈν©λλ€. λ§μ§λ§μΌλ‘ μ½λ λΆν μ λ€μ μΆκ°ν΄ 보μμμ€.