SlideShare a Scribd company logo
CRA + SSR.
(feat. TypeScript)
승형수
20@kross.kr
승형수
국문과 -> 개발자
(주) 한국어음중개 | Frontend Developer
React, IaC, Cloud Infrastructure...
• Server-Side Rendering?
• ImplementaDon
• Checklist
• 스파게티는 먹을 때에만 | 서버 코드를 분리하자
• asset-manifest.json | build 안에 담긴 보물
• 직선보다 빠른 굴절 | TypeScript를 사용하는 편이 좋은 이유
• Extra
• 필수 패키지 설치 이슈
https://github.com/
huskyhoochu/ssr-react-app
Server-Side Rendering?
기존 웹서버
전체 데이터 담긴
Full - HTML
멋대로 비유하기
백화점에서 완제품 소파를 사온다
Server-Side Rendering?
+
Client-Side Rendering
최소한의 HTML
+
화면 전체 렌더링을 위한 JS
멋대로 비유하기
이케아에서 조립형 소파를 사온다
Server-Side Rendering?
+
Server-Side Rendering
초기 렌더링 마친 HTML
+
일부 업데이트를 위한 JS
(AJAX, InteracEon...)
Implement
서버에서 한번:
1. ReactDOMServer.renderToString()
함수로 react app을 html string 으로 변환
2. 서버의 '^/$' route 에서 출력하게 함
클라이언트에서 또 한번:
3. 'build/index.html'에는 react 런타임 코
드와 페이지 코드가 붙어 있음!
4. 모든 리소스를 staDc 으로 서버에 제공하자
render | hydrate
전체 HTML
렌더링
기존 HTML과 비교 후
부분 업데이트
ReactDOM.hydrate(<App />, document.getElementById('root'));
Checklist
스파게티는 먹을 때에만 | 서버 코드를 분리하자
asset-manifest.json | build 안에 담긴 보물
직선보다 빠른 굴절 | TypeScript를 사용하는 편이 좋은 이유
스파게티는
먹을 때에만
renderToString() 함수가 서버 코드와 섞이기
마련
bootstrap.js 파일을 추가로 작성해 각자의 코드
를 따로 불러올 것
git submodule 운용 가능
Server Client
bootstrap.js
express app
객체
renderToString()
결과물
app.listen() ...
Bad
export default () => {
const app: express.Application = express();
const router: express.Router = express.Router();
router.use('^/$', (req, res) => {
const html = ReactDOMServer.renderToString(<App />);
...
});
...
};
서버 코드 안에서
React를 직접 호출
Front - Server
지나친 커플링
Folder Structure
.
/build
/src
App.tsx
html.tsx
...
/server
index.tsx
htmlRenderer.tsx
...
bootstrap.js
기존 CRA 구조
서버 관련 코드 모음
최종 실행 파일
Good
// src/html.tsx
export default ReactDOMServer.renderToString(<App />);
// server/index.tsx
export default (
html: string,
paths: { buildPath: string, htmlPath: string },
): express.Application => {
const app: express.Application = express();
const router: express.Router = express.Router();
router.use('^/$', htmlRenderer(html, paths.htmlPath));
router.use(express.static(paths.buildPath, { maxAge: '1y' }));
app.use(router);
return app;
};
Good
// bootstrap.js
const listener = require('./server').default;
const html = require('./src/html').default;
const buildPath = path.resolve(__dirname, 'build');
const htmlPath = path.resolve(buildPath, 'index.html');
listener(html, { buildPath, htmlPath }).listen(5000, () => {
console.log('server listening on http://localhost:5000 ...');
});
html & path
매개변수로 제공
Front 환경이 바뀌어도
Server가 영향 받지 않음
git submodules
.
/build
/src
App.tsx
html.tsx
...
/server
index.tsx
htmlRenderer.tsx
...
bootstrap.js
.gitmodules
submodule:
git repo 내부의 또 다른 repo
참조 파일을 통해 관리
[submodule "ssr-server"]
path = server
url = https://github.com/내저장소/ssr-server.git
asset-
manifest.json
build 폴더에 생성된 모든 자산의 일람표
html string을 만들 때 staDc file의 참조를 가져다
쓸 수 있다
asset-
manifest.json
<img
src={logo}
className="App-logo"
alt="logo"
/>
+
"/static/media/logo.5d5d9eef.svg"
<img
src="/static/media/logo.5d5d9eef.svg"
class="App-logo"
alt="logo"
/>
logo.svg 파일 import 문을
해석하지 못하여
빈 객체가 그대로 출력됨
key: build 이전 원본 파일명
value: webpack이 bundling을 마친 파일명
Overwrite asset module's path
// bootstrap.js
const path = require('path');
const register = require('ignore-styles').default;
const assetManifest = require('./build/asset-manifest.json');
const searchAssetUrl = (manifest, filename) =>
Object.keys(manifest.files)
.filter(asset => asset.replace('static/media/', '') === filename)
.map(fileKey => manifest.files[fileKey]);
register(undefined, (module, filename) => {
const imgExtensions = ['.png', '.jpg', 'jpeg', '.gif', '.svg'];
const isImg = imgExtensions.find(ext => filename.endsWith(ext));
if (!isImg) return;
const [assetUrl] = searchAssetUrl(assetManifest, path.basename(filename));
module.exports = assetUrl;
});
ignore-styles:
node 실행 중
asset import module을
무시하게 해 주어 에러 방지
asset import module 등장 시
후처리 callback 제공
직선보다
빠른 굴절
왜 TypeScript가 SSR에 도움이 될까?
tsc 컴파일을 통해 CommonJS 파일 생성 가능
-> babel을 쓰지 않아 성능 향상 🚀 페르마의 원리
빛은 최단시간으로 이동할 수 있는 경로를 택한다
In JavaScript
// bootstrap.js
require('@babel/polyfill');
require('@babel/register')({
extensions: ['.js', '.jsx'],
ignore: [/(node_modules)/],
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: ['dynamic-import-node'],
});
// src/html.jsx
export default ReactDOMServer.renderToString(<App />); 소스를 JSX 파일로 가져옴소스를 JSX 파일로 가져옴
JSX를 node에서 실행할 수
없으므로 babel register 사용
@babel/polyfill
(async/await, Promise 핸들링)
dynamic-import-node
동적 import 구문 처리 플러그인
// tsconfig.ssr.json
... (기존 CRA tsconfig.json과 동일)
"noEmit": false,
"jsx": "react",
"rootDir": "src",
"outDir": "app_compiled"
},
"include": [
"src"
],
"exclude": [
"src/*.test.tsx"
]
}
Extra
필수 패키지 설치 이슈
Redux
서버 렌더링 단계에서 데이터를 미리 주입하고 싶을
때만 사용
index.html에 인라인으로 window 전역 store
객체 삽입 -> 클라이언트 코드가 읽어들임
(공식 문서에서 권하는 방식)
https://redux.js.org/recipes/server-
rendering
<script>
window.__PRELOADED_STATE__={}
</script>
+
React-
Router-
DOM
html 생성 단계에서 StaticRouter 를 App 위에
Wrapping
서버 또한'*' 경로 라우터를 추가해 react-router
가 만든 path로 접속되더라도 html을 반환하도록 처
리
// server/index.ts
router.use('*', htmlRenderer);
// src/html.tsx
const routerContext = {};
<StaticRouter
location={req.baseUrl}
context={routerContext}
>
<App />
</StaticRouter>
React-
Loadable
모듈 이름을 특정해줘야
서버 렌더링 시에 모듈 추적을 할 수 있음!
const Index = Loadable({
loader: () => import(/* webpackChunkName: "IndexChunk" */
'../components/view/index'),
loading: () => null,
modules: ['IndexChunk'],
});
전체 패키지 적용 후
// src/html.tsx
module.exports = (req: any) => {
const csrfToken = req.csrfToken();
const store = configureStore();
store.dispatch(addCsrfToken(csrfToken));
const modules = [];
const routerContext = {};
const pushModule = (moduleName: string) => modules.push(moduleName);
const html = ReactDOM.renderToString(
<Capture report={pushModule}>
<Provider store={store}>
<StaticRouter
location={req.baseUrl}
context={routerContext}
>
<App />
</StaticRouter>
</Provider>
</Capture>,
);
const helmet = Helmet.renderStatic();
const preloadedState = JSON.stringify(store.getState());
return {
helmet,
html,
preloadedState,
};
};
Redux Store 정의
CSRF 토큰 전달받아 주입
Loadable.Capture
어떤 loadable 컴포넌트가 렌더링되는지
추적
StaEcRouter
locaEon이 변하지 않는 라우터
첫 접속 이후엔 SPA 형태가 될 것이므로 필요
// src/index.tsx
declare global {
interface Window {
__PRELOADED_STATE__: object;
}
}
const store = configureStore(window.__PRELOADED_STATE__);
ReactDOM.hydrate(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById('root'),
);
전역 객체 declare
// server/htmlRenderer.ts
return res.send(
htmlData
.replace('<div id="root"></div>', `<div id="root">${result.html}</div>`)
.replace(
'<title>React App</title>',
result.helmet.title.toString() + result.helmet.meta.toString(),
)
.replace(
'<script type="text/javascript">window.__PRELOADED_STATE__={}</script>',
`<script type="text/javascript">window.__PRELOADED_STATE__=${
result.preloadedState
}</script>`,
),
);
// bootstrap.js
Loadable.preloadAll().then(() => {
app.listen(PORT, '0.0.0.0', () => {
console.log(`Listening on http://localhost:${PORT} ...`);
});
});
성공보다 빠른 시행착오를
응원합니다

More Related Content

What's hot

What's hot (20)

[125]웹 성능 최적화에 필요한 브라우저의 모든 것
[125]웹 성능 최적화에 필요한 브라우저의 모든 것[125]웹 성능 최적화에 필요한 브라우저의 모든 것
[125]웹 성능 최적화에 필요한 브라우저의 모든 것
 
프로그래머스 프론트엔드 아키텍처 변천사: 좋은 개발 경험을 찾아서
프로그래머스 프론트엔드 아키텍처 변천사: 좋은 개발 경험을 찾아서프로그래머스 프론트엔드 아키텍처 변천사: 좋은 개발 경험을 찾아서
프로그래머스 프론트엔드 아키텍처 변천사: 좋은 개발 경험을 찾아서
 
ReactJS
ReactJSReactJS
ReactJS
 
쿠키런 1년, 서버개발 분투기
쿠키런 1년, 서버개발 분투기쿠키런 1년, 서버개발 분투기
쿠키런 1년, 서버개발 분투기
 
Get Started with ReactJS 18 Development Services_ New Features and Updates.pptx
Get Started with ReactJS 18 Development Services_ New Features and Updates.pptxGet Started with ReactJS 18 Development Services_ New Features and Updates.pptx
Get Started with ReactJS 18 Development Services_ New Features and Updates.pptx
 
React workshop
React workshopReact workshop
React workshop
 
오딘: 발할라 라이징 MMORPG의 성능 최적화 사례 공유 [카카오게임즈 - 레벨 300] - 발표자: 김문권, 팀장, 라이온하트 스튜디오...
오딘: 발할라 라이징 MMORPG의 성능 최적화 사례 공유 [카카오게임즈 - 레벨 300] - 발표자: 김문권, 팀장, 라이온하트 스튜디오...오딘: 발할라 라이징 MMORPG의 성능 최적화 사례 공유 [카카오게임즈 - 레벨 300] - 발표자: 김문권, 팀장, 라이온하트 스튜디오...
오딘: 발할라 라이징 MMORPG의 성능 최적화 사례 공유 [카카오게임즈 - 레벨 300] - 발표자: 김문권, 팀장, 라이온하트 스튜디오...
 
The Benefits of Using React JS for Web Development!
The Benefits of Using React JS for Web Development!The Benefits of Using React JS for Web Development!
The Benefits of Using React JS for Web Development!
 
[Spring Camp 2018] 11번가 Spring Cloud 기반 MSA로의 전환 : 지난 1년간의 이야기
[Spring Camp 2018] 11번가 Spring Cloud 기반 MSA로의 전환 : 지난 1년간의 이야기[Spring Camp 2018] 11번가 Spring Cloud 기반 MSA로의 전환 : 지난 1년간의 이야기
[Spring Camp 2018] 11번가 Spring Cloud 기반 MSA로의 전환 : 지난 1년간의 이야기
 
Introduction to Selenium Automation
Introduction to Selenium AutomationIntroduction to Selenium Automation
Introduction to Selenium Automation
 
Workshop React.js
Workshop React.jsWorkshop React.js
Workshop React.js
 
Spring I/O 2012: Natural Templating in Spring MVC with Thymeleaf
Spring I/O 2012: Natural Templating in Spring MVC with ThymeleafSpring I/O 2012: Natural Templating in Spring MVC with Thymeleaf
Spring I/O 2012: Natural Templating in Spring MVC with Thymeleaf
 
[SOSCON 2017] 주니어 개발자 5000명, 개발 해서 남 주자
[SOSCON 2017] 주니어 개발자 5000명, 개발 해서 남 주자[SOSCON 2017] 주니어 개발자 5000명, 개발 해서 남 주자
[SOSCON 2017] 주니어 개발자 5000명, 개발 해서 남 주자
 
MSA 전략 2: 마이크로서비스, 어떻게 구현할 것인가?
MSA 전략 2: 마이크로서비스, 어떻게 구현할 것인가?MSA 전략 2: 마이크로서비스, 어떻게 구현할 것인가?
MSA 전략 2: 마이크로서비스, 어떻게 구현할 것인가?
 
NEXT.JS
NEXT.JSNEXT.JS
NEXT.JS
 
Angular
AngularAngular
Angular
 
Introduction to react js
Introduction to react jsIntroduction to react js
Introduction to react js
 
test_automation_POC
test_automation_POCtest_automation_POC
test_automation_POC
 
파이썬 플라스크 이해하기
파이썬 플라스크 이해하기 파이썬 플라스크 이해하기
파이썬 플라스크 이해하기
 
[112]rest에서 graph ql과 relay로 갈아타기 이정우
[112]rest에서 graph ql과 relay로 갈아타기 이정우[112]rest에서 graph ql과 relay로 갈아타기 이정우
[112]rest에서 graph ql과 relay로 갈아타기 이정우
 

Similar to Create-React-App으로 SSR을 구현하며 배운 점 (feat. TypeScript)

Similar to Create-React-App으로 SSR을 구현하며 배운 점 (feat. TypeScript) (20)

Node.js and react
Node.js and reactNode.js and react
Node.js and react
 
4-3. jquery
4-3. jquery4-3. jquery
4-3. jquery
 
Nodejs express
Nodejs expressNodejs express
Nodejs express
 
Startup JavaScript 8 - NPM, Express.JS
Startup JavaScript 8 - NPM, Express.JSStartup JavaScript 8 - NPM, Express.JS
Startup JavaScript 8 - NPM, Express.JS
 
Nodejs, PhantomJS, casperJs, YSlow, expressjs
Nodejs, PhantomJS, casperJs, YSlow, expressjsNodejs, PhantomJS, casperJs, YSlow, expressjs
Nodejs, PhantomJS, casperJs, YSlow, expressjs
 
[NDC17] Unreal.js - 자바스크립트로 쉽고 빠른 UE4 개발하기
[NDC17] Unreal.js - 자바스크립트로 쉽고 빠른 UE4 개발하기[NDC17] Unreal.js - 자바스크립트로 쉽고 빠른 UE4 개발하기
[NDC17] Unreal.js - 자바스크립트로 쉽고 빠른 UE4 개발하기
 
Mean 스택을 사용한 IoT 개발
Mean 스택을 사용한 IoT 개발Mean 스택을 사용한 IoT 개발
Mean 스택을 사용한 IoT 개발
 
조은 - AMP PWA 101 [WSConf.Seoul.2017. Vol.2]
조은 - AMP PWA 101 [WSConf.Seoul.2017. Vol.2]조은 - AMP PWA 101 [WSConf.Seoul.2017. Vol.2]
조은 - AMP PWA 101 [WSConf.Seoul.2017. Vol.2]
 
[Codelab 2017] ReactJS 기초
[Codelab 2017] ReactJS 기초[Codelab 2017] ReactJS 기초
[Codelab 2017] ReactJS 기초
 
20131217 html5
20131217 html520131217 html5
20131217 html5
 
요즘웹개발
요즘웹개발요즘웹개발
요즘웹개발
 
7가지 동시성 모델 람다아키텍처
7가지 동시성 모델  람다아키텍처7가지 동시성 모델  람다아키텍처
7가지 동시성 모델 람다아키텍처
 
RHQ 공감 Seminar 6th
RHQ 공감 Seminar 6thRHQ 공감 Seminar 6th
RHQ 공감 Seminar 6th
 
Html5 performance
Html5 performanceHtml5 performance
Html5 performance
 
막하는스터디 두번째만남 Express(20151025)
막하는스터디 두번째만남 Express(20151025)막하는스터디 두번째만남 Express(20151025)
막하는스터디 두번째만남 Express(20151025)
 
11.react router dom
11.react router dom11.react router dom
11.react router dom
 
Vue.js 기초 실습.pptx
Vue.js 기초 실습.pptxVue.js 기초 실습.pptx
Vue.js 기초 실습.pptx
 
Express framework tutorial
Express framework tutorialExpress framework tutorial
Express framework tutorial
 
Front-end Development Process - 어디까지 개선할 수 있나
Front-end Development Process - 어디까지 개선할 수 있나Front-end Development Process - 어디까지 개선할 수 있나
Front-end Development Process - 어디까지 개선할 수 있나
 
PHP Slim Framework with Angular
PHP Slim Framework with AngularPHP Slim Framework with Angular
PHP Slim Framework with Angular
 

Create-React-App으로 SSR을 구현하며 배운 점 (feat. TypeScript)

  • 1. CRA + SSR. (feat. TypeScript) 승형수 20@kross.kr
  • 2. 승형수 국문과 -> 개발자 (주) 한국어음중개 | Frontend Developer React, IaC, Cloud Infrastructure...
  • 3. • Server-Side Rendering? • ImplementaDon • Checklist • 스파게티는 먹을 때에만 | 서버 코드를 분리하자 • asset-manifest.json | build 안에 담긴 보물 • 직선보다 빠른 굴절 | TypeScript를 사용하는 편이 좋은 이유 • Extra • 필수 패키지 설치 이슈
  • 5. Server-Side Rendering? 기존 웹서버 전체 데이터 담긴 Full - HTML 멋대로 비유하기 백화점에서 완제품 소파를 사온다
  • 6. Server-Side Rendering? + Client-Side Rendering 최소한의 HTML + 화면 전체 렌더링을 위한 JS 멋대로 비유하기 이케아에서 조립형 소파를 사온다
  • 7. Server-Side Rendering? + Server-Side Rendering 초기 렌더링 마친 HTML + 일부 업데이트를 위한 JS (AJAX, InteracEon...)
  • 8.
  • 9.
  • 10. Implement 서버에서 한번: 1. ReactDOMServer.renderToString() 함수로 react app을 html string 으로 변환 2. 서버의 '^/$' route 에서 출력하게 함 클라이언트에서 또 한번: 3. 'build/index.html'에는 react 런타임 코 드와 페이지 코드가 붙어 있음! 4. 모든 리소스를 staDc 으로 서버에 제공하자
  • 11.
  • 12.
  • 13. render | hydrate 전체 HTML 렌더링 기존 HTML과 비교 후 부분 업데이트 ReactDOM.hydrate(<App />, document.getElementById('root'));
  • 14. Checklist 스파게티는 먹을 때에만 | 서버 코드를 분리하자 asset-manifest.json | build 안에 담긴 보물 직선보다 빠른 굴절 | TypeScript를 사용하는 편이 좋은 이유
  • 15. 스파게티는 먹을 때에만 renderToString() 함수가 서버 코드와 섞이기 마련 bootstrap.js 파일을 추가로 작성해 각자의 코드 를 따로 불러올 것 git submodule 운용 가능 Server Client bootstrap.js express app 객체 renderToString() 결과물 app.listen() ...
  • 16. Bad export default () => { const app: express.Application = express(); const router: express.Router = express.Router(); router.use('^/$', (req, res) => { const html = ReactDOMServer.renderToString(<App />); ... }); ... }; 서버 코드 안에서 React를 직접 호출 Front - Server 지나친 커플링
  • 18. Good // src/html.tsx export default ReactDOMServer.renderToString(<App />); // server/index.tsx export default ( html: string, paths: { buildPath: string, htmlPath: string }, ): express.Application => { const app: express.Application = express(); const router: express.Router = express.Router(); router.use('^/$', htmlRenderer(html, paths.htmlPath)); router.use(express.static(paths.buildPath, { maxAge: '1y' })); app.use(router); return app; };
  • 19. Good // bootstrap.js const listener = require('./server').default; const html = require('./src/html').default; const buildPath = path.resolve(__dirname, 'build'); const htmlPath = path.resolve(buildPath, 'index.html'); listener(html, { buildPath, htmlPath }).listen(5000, () => { console.log('server listening on http://localhost:5000 ...'); }); html & path 매개변수로 제공 Front 환경이 바뀌어도 Server가 영향 받지 않음
  • 20. git submodules . /build /src App.tsx html.tsx ... /server index.tsx htmlRenderer.tsx ... bootstrap.js .gitmodules submodule: git repo 내부의 또 다른 repo 참조 파일을 통해 관리 [submodule "ssr-server"] path = server url = https://github.com/내저장소/ssr-server.git
  • 21. asset- manifest.json build 폴더에 생성된 모든 자산의 일람표 html string을 만들 때 staDc file의 참조를 가져다 쓸 수 있다 asset- manifest.json <img src={logo} className="App-logo" alt="logo" /> + "/static/media/logo.5d5d9eef.svg" <img src="/static/media/logo.5d5d9eef.svg" class="App-logo" alt="logo" />
  • 22. logo.svg 파일 import 문을 해석하지 못하여 빈 객체가 그대로 출력됨
  • 23. key: build 이전 원본 파일명 value: webpack이 bundling을 마친 파일명
  • 24. Overwrite asset module's path // bootstrap.js const path = require('path'); const register = require('ignore-styles').default; const assetManifest = require('./build/asset-manifest.json'); const searchAssetUrl = (manifest, filename) => Object.keys(manifest.files) .filter(asset => asset.replace('static/media/', '') === filename) .map(fileKey => manifest.files[fileKey]); register(undefined, (module, filename) => { const imgExtensions = ['.png', '.jpg', 'jpeg', '.gif', '.svg']; const isImg = imgExtensions.find(ext => filename.endsWith(ext)); if (!isImg) return; const [assetUrl] = searchAssetUrl(assetManifest, path.basename(filename)); module.exports = assetUrl; }); ignore-styles: node 실행 중 asset import module을 무시하게 해 주어 에러 방지 asset import module 등장 시 후처리 callback 제공
  • 25.
  • 26. 직선보다 빠른 굴절 왜 TypeScript가 SSR에 도움이 될까? tsc 컴파일을 통해 CommonJS 파일 생성 가능 -> babel을 쓰지 않아 성능 향상 🚀 페르마의 원리 빛은 최단시간으로 이동할 수 있는 경로를 택한다
  • 27. In JavaScript // bootstrap.js require('@babel/polyfill'); require('@babel/register')({ extensions: ['.js', '.jsx'], ignore: [/(node_modules)/], presets: ['@babel/preset-env', '@babel/preset-react'], plugins: ['dynamic-import-node'], }); // src/html.jsx export default ReactDOMServer.renderToString(<App />); 소스를 JSX 파일로 가져옴소스를 JSX 파일로 가져옴 JSX를 node에서 실행할 수 없으므로 babel register 사용 @babel/polyfill (async/await, Promise 핸들링) dynamic-import-node 동적 import 구문 처리 플러그인
  • 28.
  • 29. // tsconfig.ssr.json ... (기존 CRA tsconfig.json과 동일) "noEmit": false, "jsx": "react", "rootDir": "src", "outDir": "app_compiled" }, "include": [ "src" ], "exclude": [ "src/*.test.tsx" ] }
  • 31. Redux 서버 렌더링 단계에서 데이터를 미리 주입하고 싶을 때만 사용 index.html에 인라인으로 window 전역 store 객체 삽입 -> 클라이언트 코드가 읽어들임 (공식 문서에서 권하는 방식) https://redux.js.org/recipes/server- rendering <script> window.__PRELOADED_STATE__={} </script> +
  • 32. React- Router- DOM html 생성 단계에서 StaticRouter 를 App 위에 Wrapping 서버 또한'*' 경로 라우터를 추가해 react-router 가 만든 path로 접속되더라도 html을 반환하도록 처 리 // server/index.ts router.use('*', htmlRenderer); // src/html.tsx const routerContext = {}; <StaticRouter location={req.baseUrl} context={routerContext} > <App /> </StaticRouter>
  • 33. React- Loadable 모듈 이름을 특정해줘야 서버 렌더링 시에 모듈 추적을 할 수 있음! const Index = Loadable({ loader: () => import(/* webpackChunkName: "IndexChunk" */ '../components/view/index'), loading: () => null, modules: ['IndexChunk'], });
  • 35. // src/html.tsx module.exports = (req: any) => { const csrfToken = req.csrfToken(); const store = configureStore(); store.dispatch(addCsrfToken(csrfToken)); const modules = []; const routerContext = {}; const pushModule = (moduleName: string) => modules.push(moduleName); const html = ReactDOM.renderToString( <Capture report={pushModule}> <Provider store={store}> <StaticRouter location={req.baseUrl} context={routerContext} > <App /> </StaticRouter> </Provider> </Capture>, ); const helmet = Helmet.renderStatic(); const preloadedState = JSON.stringify(store.getState()); return { helmet, html, preloadedState, }; }; Redux Store 정의 CSRF 토큰 전달받아 주입 Loadable.Capture 어떤 loadable 컴포넌트가 렌더링되는지 추적 StaEcRouter locaEon이 변하지 않는 라우터 첫 접속 이후엔 SPA 형태가 될 것이므로 필요
  • 36. // src/index.tsx declare global { interface Window { __PRELOADED_STATE__: object; } } const store = configureStore(window.__PRELOADED_STATE__); ReactDOM.hydrate( <Provider store={store}> <BrowserRouter> <App /> </BrowserRouter> </Provider>, document.getElementById('root'), ); 전역 객체 declare
  • 37. // server/htmlRenderer.ts return res.send( htmlData .replace('<div id="root"></div>', `<div id="root">${result.html}</div>`) .replace( '<title>React App</title>', result.helmet.title.toString() + result.helmet.meta.toString(), ) .replace( '<script type="text/javascript">window.__PRELOADED_STATE__={}</script>', `<script type="text/javascript">window.__PRELOADED_STATE__=${ result.preloadedState }</script>`, ), );
  • 38. // bootstrap.js Loadable.preloadAll().then(() => { app.listen(PORT, '0.0.0.0', () => { console.log(`Listening on http://localhost:${PORT} ...`); }); });