Successfully reported this slideshow.
Your SlideShare is downloading. ×

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

Ad

CRA + SSR.
(feat. TypeScript)
승형수
20@kross.kr

Ad

승형수
국문과 -> 개발자
(주) 한국어음중개 | Frontend Developer
React, IaC, Cloud Infrastructure...

Ad

• Server-Side Rendering?
• ImplementaDon
• Checklist
• 스파게티는 먹을 때에만 | 서버 코드를 분리하자
• asset-manifest.json | build 안에 담긴 보물
•...

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Check these out next

1 of 39 Ad
1 of 39 Ad

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

Download to read offline

프로덕션 환경에서 클라이언트 사이드 렌더링을 고집하기란 힘든 일입니다. 서버를 통해 웹사이트를 제공하면서도 React의 편리함을 누리려면 서버 사이드 렌더링(SSR)을 구현해야 하는데요. Create-React-App을 그대로 유지하면서 SSR을 구현하는 과정을 보여드리고자 합니다. TypeScript로도 가능합니다!

프로덕션 환경에서 클라이언트 사이드 렌더링을 고집하기란 힘든 일입니다. 서버를 통해 웹사이트를 제공하면서도 React의 편리함을 누리려면 서버 사이드 렌더링(SSR)을 구현해야 하는데요. Create-React-App을 그대로 유지하면서 SSR을 구현하는 과정을 보여드리고자 합니다. TypeScript로도 가능합니다!

Advertisement
Advertisement

More Related Content

Slideshows for you (19)

Advertisement

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

  1. 1. CRA + SSR. (feat. TypeScript) 승형수 20@kross.kr
  2. 2. 승형수 국문과 -> 개발자 (주) 한국어음중개 | Frontend Developer React, IaC, Cloud Infrastructure...
  3. 3. • Server-Side Rendering? • ImplementaDon • Checklist • 스파게티는 먹을 때에만 | 서버 코드를 분리하자 • asset-manifest.json | build 안에 담긴 보물 • 직선보다 빠른 굴절 | TypeScript를 사용하는 편이 좋은 이유 • Extra • 필수 패키지 설치 이슈
  4. 4. https://github.com/ huskyhoochu/ssr-react-app
  5. 5. Server-Side Rendering? 기존 웹서버 전체 데이터 담긴 Full - HTML 멋대로 비유하기 백화점에서 완제품 소파를 사온다
  6. 6. Server-Side Rendering? + Client-Side Rendering 최소한의 HTML + 화면 전체 렌더링을 위한 JS 멋대로 비유하기 이케아에서 조립형 소파를 사온다
  7. 7. Server-Side Rendering? + Server-Side Rendering 초기 렌더링 마친 HTML + 일부 업데이트를 위한 JS (AJAX, InteracEon...)
  8. 8. Implement 서버에서 한번: 1. ReactDOMServer.renderToString() 함수로 react app을 html string 으로 변환 2. 서버의 '^/$' route 에서 출력하게 함 클라이언트에서 또 한번: 3. 'build/index.html'에는 react 런타임 코 드와 페이지 코드가 붙어 있음! 4. 모든 리소스를 staDc 으로 서버에 제공하자
  9. 9. render | hydrate 전체 HTML 렌더링 기존 HTML과 비교 후 부분 업데이트 ReactDOM.hydrate(<App />, document.getElementById('root'));
  10. 10. Checklist 스파게티는 먹을 때에만 | 서버 코드를 분리하자 asset-manifest.json | build 안에 담긴 보물 직선보다 빠른 굴절 | TypeScript를 사용하는 편이 좋은 이유
  11. 11. 스파게티는 먹을 때에만 renderToString() 함수가 서버 코드와 섞이기 마련 bootstrap.js 파일을 추가로 작성해 각자의 코드 를 따로 불러올 것 git submodule 운용 가능 Server Client bootstrap.js express app 객체 renderToString() 결과물 app.listen() ...
  12. 12. 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 지나친 커플링
  13. 13. Folder Structure . /build /src App.tsx html.tsx ... /server index.tsx htmlRenderer.tsx ... bootstrap.js 기존 CRA 구조 서버 관련 코드 모음 최종 실행 파일
  14. 14. 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; };
  15. 15. 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가 영향 받지 않음
  16. 16. 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
  17. 17. 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" />
  18. 18. logo.svg 파일 import 문을 해석하지 못하여 빈 객체가 그대로 출력됨
  19. 19. key: build 이전 원본 파일명 value: webpack이 bundling을 마친 파일명
  20. 20. 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 제공
  21. 21. 직선보다 빠른 굴절 왜 TypeScript가 SSR에 도움이 될까? tsc 컴파일을 통해 CommonJS 파일 생성 가능 -> babel을 쓰지 않아 성능 향상 🚀 페르마의 원리 빛은 최단시간으로 이동할 수 있는 경로를 택한다
  22. 22. 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 구문 처리 플러그인
  23. 23. // tsconfig.ssr.json ... (기존 CRA tsconfig.json과 동일) "noEmit": false, "jsx": "react", "rootDir": "src", "outDir": "app_compiled" }, "include": [ "src" ], "exclude": [ "src/*.test.tsx" ] }
  24. 24. Extra 필수 패키지 설치 이슈
  25. 25. Redux 서버 렌더링 단계에서 데이터를 미리 주입하고 싶을 때만 사용 index.html에 인라인으로 window 전역 store 객체 삽입 -> 클라이언트 코드가 읽어들임 (공식 문서에서 권하는 방식) https://redux.js.org/recipes/server- rendering <script> window.__PRELOADED_STATE__={} </script> +
  26. 26. 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>
  27. 27. React- Loadable 모듈 이름을 특정해줘야 서버 렌더링 시에 모듈 추적을 할 수 있음! const Index = Loadable({ loader: () => import(/* webpackChunkName: "IndexChunk" */ '../components/view/index'), loading: () => null, modules: ['IndexChunk'], });
  28. 28. 전체 패키지 적용 후
  29. 29. // 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 형태가 될 것이므로 필요
  30. 30. // 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
  31. 31. // 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>`, ), );
  32. 32. // bootstrap.js Loadable.preloadAll().then(() => { app.listen(PORT, '0.0.0.0', () => { console.log(`Listening on http://localhost:${PORT} ...`); }); });
  33. 33. 성공보다 빠른 시행착오를 응원합니다

×