React + Typescript + SSR + Code-splitting 환경설정하기

Minoo Joo
Minoo Joo
Sep 10, 2019 · 32 min read

안녕하세요. Typescript + React + Server-side rendering + Code-splitting 으로 환경설정하는 방법을 소개해드립니다. Next.js나 Razzle도 좋은 프로젝트라고 생각하지만, 커스터마이징이 쉽지 않고 프로덕션 레벨의 이슈에 대응하기 어려워져서 직접 설정을 해보았습니다.

각각의 프로젝트들이 문서와 예제가 잘 되어있지만, 이것들을 조합하는 방법은 찾기가 어렵더라구요. 특히 @loadable-component 는 문서와 예제가 잘 되어 있지만, Typescript와 HMR를 포함한 가이드가 없어서 이 부분을 보완해 환경설정을 소개해드립니다.
@loadable-component 는 React 공식문서에서 SSR용 Code-splitting 으로 추천하는 라이브러리 입니다. React의 lazy는 아직 server-side rendering을 지원하지 않고 있어서 @loadable-component 를 추천하는 것 같습니다. Suspense 기능도 지원합니다. :-)

혹시 비슷한 설정을 하시는 분이 계실까 싶어서 경험과 결과를 공유합니다. Webpack이나 Typescript 등에 익숙하지 않은 분들도 이해할수 있도록, 가능하면 설정코드를 한줄 한줄 읽어가며 진행하겠습니다. 혹시 제가 틀린 내용이나 오탈자가 있다면 알려주세요!

만약 결과만 빠르게 보고 싶다면 GitHub repository 를 참고해주세요.


1편
(1) Typescript 설정
(2) Webpack & babel 설정 (webpack-dev-server)
(3) react-router, react-helmet 더하기
(4) Server-side rendering (webpack-dev-middleware, webpack-hot-middleware)
(5) Code-splitting (@loadable-component)

2편
(1) React-hot-loader
(2) Styled-components
(3) Stylelint
(4) Prettier
(5) Redux
(6) Jest

글을 쓰다보니 꽤 길어져서 2편으로 나눴습니다. 1편은 필수 설정이고 2편은 HMR을 도와주는 React-hot-loader와 css-in-js, Stylelint, Prettier 설정 입니다. Typescript와 Webpack, babel 설정에 익숙하신 분들이라면 Code-splitting 부분만 집중적으로 보시면 편할 것 같습니다.

주요 패키지는 다음과 같습니다.

- React v16.9
- Typescript v3.5
- Webpack v4
- Babel v7
- React-router v5
- React-helmet v5
- @loadable-component v5 (Code-splitting)
- webpack-dev-middleware v3
- webpack-hot-middleware v2
- react-hot-loader v4
- styled-components v4 (css-in-js)
- Lint (tslint, stylelint, prettier)


폴더구조

저는 기본적인 폴더구성을 다음과 같이 할 예정입니다.

dist/src/
pages/
components/
App.tsx
index.tsx
server.tsx
...config files

dist/ 폴더는 webpack bundling 파일이 담기는 폴더입니다.

src/ 폴더는 저희가 직접 작성하게 될 파일들이 들어있습니다. pages/components/ 는 리액트 컴포넌트들이 들어있습니다 (이번 예제에서는 간단한 div와 text로 구성할 예정입니다). entry file인 index.tsxApp.tsx 가 있고 서버사이드 렌더링 서버파일 server.tsx 가 있습니다.

root 폴더에는 babel과 webpack 등 config 파일들이 들어갈 예정입니다.


1. Typescript 설정

일단 기본적으로 react와 typescript dependencies를 더해줍니다. tslint는 Typescript의 lint입니다.

yarn add react react-dom typescript
yarn add --dev @types/react @types/react-dom tslint

그리고 간단한 React entry file index.tsxApp.tsx 를 작성해보겠습니다.

그리고 Typescript 를 Javascript로 컴파일하는데 필요한 tsconfig.json 파일을 작성해줍니다. Typescript용 Lint인 tslint 도 함께 설정하겠습니다. 터미널에 다음 스크립트를 실행해주세요.

node_modules/.bin/tsc --init

이번 과정에서 Typescript 컴파일을 직접 할 필요는 없어서 .bin의 명령어를 직접 입력했습니다. npm script로 실행하실 분들은 다음과 같이 설정해주시면 됩니다.

"scripts": {
"tsc": "tsc"
},

그리고 CLI 를 실행해주세요

yarn tsc --init

CLI를 실행하면 tsconfig.json 파일이 자동으로 생성되고 각 옵션에 대한 default 값과 설명이 함께 들어가서 매우 편리합니다. 각각에 대한 설명은 주석을 읽어보면 자세히 설명되어 있으니 저는 필요한 설정만 말씀드리겠습니다.

tsconfig.json

targetmodule은 “esnext”로, moduleResolution 값은 “node”로 해두셔야 합니다. Typescript는 Javascript의 슈퍼셋으로 babel이 하는 역할도 포함하고 있습니다. 따라서 이론적으로는 Typescript를 사용하면 babel을 쓰지 않아도 됩니다. 하지만 아직까지 대부분의 package 들이 babel 에서 돌아가는 것을 전제로 하고, babel plugin을 제공합니다. 따라서 Typescript 는 type checking에 집중하고, babel은 es6/es7 문법에 집중하게 할 것입니다.

Typescript로 작성된 파일들은 Typescript 컴파일 후에 babel이 읽는 순서로 진행됩니다. (babel은 Javascript 파일만 읽을 수 있기 때문에 Typescript 컴파일이 선행돼야 합니다) 이때 target이나 module을 “esnext”로 설정하지 않는다면 import 구문을 Typescript가 파싱하고, 몇몇 babel plugins이 정상작동을 하지 않을 수 있습니다.

이번에 code-splitting에 사용할 @loadable-component 역시 babel plugins을 통해 import 구문을 파싱하기 때문에 Typescript가 import 구문을 해석하면 정상적으로 작동하지 않습니다.

jsx 는 리액트 jsx 문법을 보존하도록 “preserve”로 설정하시면 됩니다.

plugins 에는 { name: "typescript-tslint-plugin" } 라고 명시하면 tslint 를 적용합니다. 저는 typescript-tslint-plugin 팀에서 디폴트로 설정한 값을 같이 추가했습니다.


3. Webpack & babel

일단 Client-side rendering 으로 webpack-dev-server 를 이용해 간단한 react 화면을 띄워보겠습니다. 사용할 dependecies를 설치해보겠습니다.

webpack-cli: npm script로 webpack을 실행할 수 있게 해줍니다.
webpack-dev-server(WDS): 간단한 서버를 만들어 browser에서 우리가 만든 파일을 실행할 수 있게 해줍니다.
html-webpack-plugin: Javascript bundling 파일을 html에 연결시켜줍니다.
babel-loader ,ts-loader: babel과 typescript 구문을 해석해줄 loader입니다.

yarn add --dev webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader ts-loader

webapck.config.js 와 webpack.dev.js 2개의 파일을 만들어 줍니다. webpack은 최종적으로 (1) 클라이언트 파일용 (2) 서버 파일용 (3) WDS용 으로 나눠지게 될텐데, webpack.config.js는 언제 어떤 config 파일을 써야할지 routing 해주는 역할을 하게 될 것입니다.

따라서 실제 config는 모두 webpack.dev.js에 담아보겠습니다.

webpack.config.js
webpack.dev.js

webpack.config.js : CLI로 webpack을 실행하면 기본적으로 webpack.config.js라는 파일을 봅니다. 지금은 env 설정을 받아서 해당 config를 대신 전달해주는 router 역할만 해주고 있습니다. WDS를 실행할 때 dev라는 parameter를 전달해주면 webpack.dev.js 설정으로 실행됩니다.

webpack.dev.js WDS는 개발모드이므로 mode는 development로, entry 파일은 index.tsx 로 해줍니다.
module : Typescript와 babel이 모두 작동하도록 설정해둡니다. loader는 오른쪽에서 왼쪽으로 실행되므로 babel-loader 이후에 ts-loader가 오도록 순서를 신경써주세요.
resolve : extensions의 경우 Javascript와 Typescript를 모두 읽을 수 있도록 해줍니다. 우리가 작성하는 클라이언트 파일은 모두 Typescript로 작성하지만 WDS가 serve하는 파일은 Javascript 이기 때문입니다.
plugins : Hot Module Replacement가 작동하도록 new webpack.HotModuleReplacement() 를 추가해줍니다. 또 html 파일 연결을 위해 new HtmlWebpackPlugin() 설정도 해주시면 됩니다. templete 은 실제로 우리가 작성한 html 파일의 경로를 써주시면 되고, filename 은 webpack이 bundling을 마치고 난 후의 html 파일 이름을 써주시면 됩니다. 서버와 브라우저가 실제로 사용하도록 원하는 이름을 써주시면 됩니다.

Hot Module Replacement는 변경된 파일 부분만 반영하도록 하는 기능입니다. webpack은 클라이언트 코드를 하나로 bundling 해주는 역할로 시작했습니다. bundling은 여러개의 파일을 하나로 합치는 기능인데, 코드가 바뀔때마다 즉시 반영해주는 Live reloading 기능이 도입되었습니다. 하지만 글자가 1개만 바뀌어도 전체 코드를 다시 refresh해서 다소 불편함이 있었는데요, 불필요한 전체 새로고침을 막고 바뀐 부분만 빠르게 갈아치우는 (=hot 하게 module을 replace하는) 기능이 HMR 입니다.

WDS에서 설정한대로 public 폴더에 index_dev.html 파일을 생성하겠습니다.

이제 마지막으로 babel config와 npm scripts 설정을 하고 우리가 만든 화면을 브라우저에 띄워보겠습니다.

"scripts": {  "start": "webpack-dev-server --env=dev --profile --colors"},

위에 말씀드린 것처럼 webpack.config.js 파일 대신 webpack.dev.js 파일로 실행하도록 env 파라미터를 dev로 전달해줍니다.

yarn start

이제 브라우저를 켜서 localhost:3000에 접속하면 흰 화면에 “App” 이라는 세 글자가 떠 있는 것을 보실 수 있습니다. 콘솔창에서는 WDS에 접속되어 있고 HMR 기능이 활성화된 것을 볼 수 있습니다.


4. React-router, React-helmet

React-router는 다들 알고 계시리라고 믿습니다. React-helmet은 클라이언트에서 손쉽게 meta 정보를 바꿀 수 있게 도와주는 핵심 라이브러리입니다. SEO를 위해 Server-side rendering을 사용하신다면 꼭 필요할 것입니다. 해당 패키지를 설치하는 것부터 시작해보죠.

yarn add react-router-dom react-helmet
yarn add --dev @types/react-router-dom @types/react-helmet

그리고 4개의 components를 추가하겠습니다. Header.tsxFooter.tsx 는 components/ 폴더에, Route 역할을 하는 Home.tsxNews.tsxpages/ 폴더에 추가하겠습니다.

그리고 App.tsxNews.tsx에는 추가로 React-helmet을 이용해 html title까지 변경시켜보겠습니다.

먼저 index.tsx에 React-router 를 쓸 수 있도록 BrowserRouter를 추가해주고, App.tsx에는 ‘/’ 에 Home.tsx를 ‘/news’에 News.tsx를 추가해줍니다.

그리고 Link가 있는 Header와 간단한 text가 있는 Footer를 작성해보겠습니다.

마지막으로 Home.tsxNews.tsx를 작성해줍니다.

이제 WDS를 껐다가 다시 실행시키면 상단 헤더에는 Home과 News 페이지로 이동하는 a tag, 밑에는 페이지 이름을 가르키는 components, 그리고 footer라고 쓰여진 Footer가 위치합니다.

Home이라는 텍스트를 ‘Home Sweet Home’ 으로 바꾸시면 WDS의 hot 설정에 의해 새로고침을 하지 않아도 글자가 바뀌는 것을 볼 수 있습니다. 스크린샷에는 없지만 헤더의 News를 눌러 해당 URL로 이동하면 react-helmet 코드에 의해 탭 이름이 News로 바뀌는 것을 볼 수 있습니다.


5. Server-side rendering (without Code-splitting)

이제 본격적으로 Server-side rendering에 들어가보겠습니다. 직접 간단한 express 서버를 구성하고, webpack-dev-middlewarewebpack-hot-middleware 조합으로 개발환경을 만들 것입니다.
webpack-dev-serverexpresswebpack-dev-middleware를 사용해서 쓰기 쉽게 만들어진 패키지인데 서버사이드 렌더링을 위해 조금 커스터마이징 한다고 생각하시면 됩니다.

React-hot-loader는 Hot Module Replacement가 실행될 때 리액트 state의 값을 유지시켜주는 아주 편리한 기능을 제공합니다. 이번에는 다루지 않지만 코드 스플리팅과 서버사이드 렌더링을 마친 후에 2편에서 적용해보겠습니다.

먼저 depencies를 설치하는 것으로 시작하겠습니다.

yarn add express
yarn add --dev webpack-dev-middleware webpack-hot-middleware webpack-node-externals @types/express @types/webpack-dev-middleware @types/webpack-hot-middleware @types/webpack-env

express : 다들 아실거라고 믿습니다 :-)

webpack-dev-middleware(WDM) : express로 만들어진 서버에서, 파일이 수정될 때마다 변경된 파일과 그 정보를 만들어주는 개발용 서버 도우미입니다.

webpack-hot-middleware(WHM) : WDM에서 변경된 파일 정보를 생성하면, 그 파일들을 불러와서 브라우저에 갱신시킵니다. 이름처럼 WDS의 hot 기능을 담당한다고 보면 됩니다. 내부적으로는 EventSource를 써서 작동하는데 WebSocket과 유사한 통신방법입니다. SSR에서 HMR(hot module replacement)를 담당합니다.

webpack-node-externals : 번들 파일에 불필요한 node_modules 를 빼주는 역할을 합니다. SSR은 js 파일이 클라이언트에게 전송되어 실행되기 전까지, 초기화면의 html string만 렌더링해 먼저 보여주는 개념입니다. 따라서 서버에서 렌더링된 파일에는 node_modules 이 필요 없게 되는것이지요.

@types/webpack-env : HMR을 활성화 시킬때 webpack이 module에 hot 이라는 속성을 주입합니다. 이러한 webpack용 env의 type을 담당합니다.

이제 렌더링용 express 서버를 만들고 관련된 webpack 설정들을 바꿔보겠습니다. 지금까지 과정을 같이 해오셨다면 폴더 구조가 다음과 같을 것입니다.

public/
index_dev.html
src/
pages/
components/
App.tsx
index.tsx
babel.config.js
package.json
tsconfig.json
webpack.config.js
webpack.dev.js

root/webpack.client.jswebpack.server.js 를 추가하고 src/ 폴더에 server.tsx 를 추가해주겠습니다. 이제 webpack.dev.js는 사용하지 않을 것이므로 삭제해도 무방합니다. 저는 일단 남겨두고 진행하겠습니다.

renderToString : 리액트에서 지원하는 서버사이드 렌더링 메소드입니다. 클라이언트 사이드에서도 ReactDOM.render 대신 hydrate를 사용하는 것과 연관이 있는데 해당 코드에서 설명하겠습니다.

helmet : 우리가 클라이언트 사이드에서 사용한 react-helmet의 정보를 가져오는 method 입니다. 꼭! renderToString 다음에 선언해주셔야 helmet의 정보를 정상적으로 가져올 수 있습니다. res.send 부분을 보시면 이번에는 title 정보를 주입해주고 있습니다. react-helemt은 head에 사용할 수 있는 대부분의 태그를 지원하니 필요하시면 공식 문서를 참고해 진행하시면 됩니다.

StaticRouter : 서버사이드 렌더링에서 BrowserRouter 대신 사용하는 React-router 입니다. 접속 Url의 Router 정보를 클라이언트 사이드 파일에 전달해주는 역할을 합니다. location 에 req.url, context 에 object를 넣어 유저가 클라이언트의 BrowserRouter에게 전달해줍니다.

res.send : body 태그에 앞에서 뽑아낸 html string을 주입해줍니다. 그리고 script tag로 bundle파일의 uri를 넣어주시면 됩니다. 아직은 Code-splitting을 하지 않았으므로 main.js 하나의 번들파일을 넣어주시면 됩니다.

if (process.env.NODE_ENV !== 'production') 부분에는 WDM과 WHM 설정을 해줍니다. webpack.client.js 파일을 weppack 으로 실행시켜주면 컴파일러가 됩니다. express.static 처럼 request가 들어왔을 경우 파일을 보내주기 위해 app.use 해주시면 됩니다.

webpack-dev-middleware : 첫 번째 파라미터로 compiler를, 두 번째 파라미터로 options를 설정해주실 수 있습니다. options의 다른 설정은 모두 optional 이지만 publicPath는 필수인데요. 보통 { publicPath: compiler.output.publicPath } 형태로 컴파일러의 설정을 그대로 씁니다.

webpack-hot-middleware : WDM과 마찬가지로 두 번째 파라미터로 options를 전달할 수 있습니다. 저는 webpack.client.js 에서 query string 방식으로 설정하겠습니다.

이제 서버사이드 렌더링을 위한 webpack 설정을 해보겠습니다. Typescript로 작성한 server.tsx 파일을 컴파일 해줄 webpack.server.js 파일을 먼저 작성해보겠습니다. Typescript 컴파일을 사용해도 되고, js로 작성해서 babel로 컴파일 해도되고, 컴파일이 필요없는 es5 등의 문법으로 작성하셔도 됩니다. 저는 webpack에서 지원하는 몇 가지 기능을 사용하고자 webpack으로 컴파일 합니다.

target : node.js 환경에서 돌아갈 파일을 컴파일할 때 쓰는 설정입니다. 보통 프론트엔드에서 쓰는 리액트같은 경우 브라우저에서 실행하기 때문에 target: web 환경에서 컴파일 됩니다. server.tsx 파일은 node.js의 express 서버를 실행하기 때문에 target 을 node로 바꿔주시면 됩니다.

node : node.js의 property를 사용할지 말지에 대한 내용입니다. boolean 혹은 object 값을 할당하시면 되는데, false일 경우 모든 항목을 쓰지않는 것이고 object는 각각을 설정해주실 수 있습니다.
false로 설정된 항목은 node.js가 아니라 webpack 규칙으로 처리하는데 __dirname이 대표적입니다. __dirname을 node.js가 처리할 경우 항상 ‘/’를 반환합니다. 저희는 webpack을 이용해 파일위치를 설정할 것이므로 false 혹은 { __dirname: false } 라고 해주시면 됩니다.

entry : react 코드가 아닌 express 코드를 컴파일 하니까, src/server.tsx 의 위치를 입력해주세요.

output : 저는 dist/ 폴더에 배포할 모든 파일을 모아놓을 것입니다. 다음에 나올 클라이언트 번들파일과 함께 server 파일 역시 dist 폴더에 컴파일 하겠습니다.

externals : 앞서 말씀드린대로 서버에서 필요없는 node_modules 를 제외하기 위해 nodeExternals() 를 넣어주세요.

앞서 작성했던 webpack.dev.js 의 내용을 서버사이드 렌더링에 맞춰 바꿔보겠습니다. 파일 이름도 webpack.client.js 에 새로 작성해보겠습니다. 달라진 부분만 잠깐 보죠.

entery : 새로운 녀석이 눈에 뜹니다. hotMiddlewareScript 인데요. 앞서 WDS는 HMR 기능을 쓰기 위해서 { hot: true } 만 입력해주면 자동으로 되던것과는 달리 webpack-hot-middleware 에서는 약간의 설정이 더 필요합니다. const로 선언한 hotMiddlewareScript 를 넣어줍니다. 꼭 const로 선언할 필요 없이 직접 입력하셔도 됩니다.
뒤에 query string으로 입력한 옵션값들은 server.tsx 에서 할당해줄 수도 있습니다. 그럴경우 webpack-hot-middleware/client 까지만 입력해주시면 됩니다. 자세한 방법은 webpack-hot-middleware repo를 참고해주세요.

output : 마찬가지로 dist/ 폴더에 번들링을 하겠습니다.

devServer : express, dev-server, HMR 기능을 한번에 해결해주던 webapck-dev-server 를 각각의 패키지를 써서 대체하겠습니다. 앞서 말씀드린대로 서버사이드 렌더링에서 WDS의 지원이 제한적이기 때문입니다. webpack config에서는 삭제해주시면 됩니다.

우와 이제 최초의 서버사이드 렌더링까지 얼마 남지 않았습니다. npm scripts 를 조금 변경해서 화면을 띄워보죠

"scripts": {  "start": "yarn build:dev && node ./dist/server.js",  "start:wds": "webpack-dev-server --env=dev --profile --colors",  "build:dev": "rm -rf dist/ && NODE_ENV=development yarn build:client && NODE_ENV=development yarn build:server",  "build:server": "webpack --env=server --progress --profile --colors",  "build:client": "webpack --env=client --progress --profile --colors"},

서버사이드 렌더링의 순서는 다음과 같습니다.
(1) build:client : 클라이언트 파일 웹팩으로 번들링
(2) build:server : 서버 파일 웹팩으로 컴파일
(3) node ./dist/server.js : 컴파일된 서버 파일을 node 로 실행

정해진 방법은 아닙니다. (2)번을 Typescript나 babel로 컴파일 하거나, 컴파일이 필요없는 server.js 파일을 작성하셔도 됩니다. 또 (1)번과 (2)번의 순서는 바뀌어도 무방합니다. (3)번을 node 이외에도 nodemon과 babel-loader로 실행하는 방법, nodemon과 ts-node로 실행하는 방법 등이 있습니다.

이번에는 웹팩으로 클라이언트와 서버를 컴파일하고 node로 실행해보겠습니다. 이 모든 스크립트를 “start”에 담았습니다. 혹시 스크립트를 바꿔서 실행하더라도 NODE_ENV = 'development' 를 해주셔야 WDM이 실행됩니다.

yarn start

짜잔! 드디어 최초의 서버사이드 렌더링에 성공했습니다. WDS로 실행했을 때는 콘솔창에 WDS log가 떴지만 지금은 보이지 않습니다. HMR 로그는 잘 보이네요. 실제로 잘 작동하는지 ‘Home Sweet Home’을 ‘My Home’으로 바꿔보겠습니다. (WDS에서는 포트 3000을 사용했지만, 여기서는 3003을 사용했습니다)

HMR이 파일변경을 감지하고 자동으로 업데이트 시켜주는군요. 화면도 자동으로 바뀌고 오른쪽 콘솔도 정상적으로 뜨네요. terminal에도 현재 버전의 webpack hash를 출력해주고 있습니다.

진짜 서버사이드 렌더링이 잘 된건지 서버에서 내려주는 html 파일을 chrome network 탭에서 확인해보겠습니다. 먼저 WDS로 클라이언트 사이드 렌더링을 한 파일과 비교해보겠습니다.

client-side rendering
server-side rendering

<body> 태그 안의 결과를 보시면 서버사이드 렌더링은 우리가 짠 코드들이 들어가 있습니다. 이렇게 되면 검색봇이 사이트의 내용을 정확하게 파악할 수 있고, 최초에 파일을 받고나서 기본적인 화면이 바로 렌더링되는 장점이 있습니다.

'/' 이 아니라 '/news' url에 접근해보시면 차이가 더욱더 극명하게 드러납니다.

client-side rendering: ‘/news’
server-side rendering: ‘/news’

chrome tab의 title은 동일하게 ‘News’로 <body> 의 내용도 다르지만 <head><title> 내용도 다릅니다.

head의 title 태그를 바꿔주는 코드가 클라이언트 파일에 있기 때문입니다. 클라이언트 렌더링은 최초의 html 파일에 관여할 수 없습니다. 하지만 서버사이드 렌더링은 관련 내용을 추가해줄 수 있고 react-helmet 을 이용하면 SEO에 필요한 대부분의 meta 태그를 쉽게 컨트롤 할 수 있습니다. 이번 예제에서는 title 태그를 바꾸면서 차이점을 확인해보았습니다.


6. Code-splitting (including Server-side rendering)

자 이제 서버사이드 렌더링이 모두 완료됐을까요? 아닙니다. 아직 Code-splitting이 남아 있습니다. 지금은 아주 작은 프로젝트라서 문제가 없지만, 기능과 페이지가 추가될 수록 번들파일의 크기가 커지게 됩니다. 서버사이드 렌더링으로 첫 화면을 띄우더라도 클라이언트 파일을 받아서 실행할때까지 유저가 기다리는 시간이 길어지게 되죠.

위의 스크린샷을 보면 클라이언트 사이드 렌더링이나 서버 사이드 렌더링 모두 main.js 라는 하나의 번들만 사용합니다. ( backend.js__webpack_hmr 은 webpack middleware 때문에 실행되는 것이라 신경쓰지 않아도 됩니다)

서버사이드 렌더링에서도 작동하는 Code-splitting을 시작해보겠습니다. 리액트에서는 lazy 로 코드 스플리팅을 공식 지원하지만 아직 서버사이드 렌더링은 지원하지 않습니다. 공식문서에서도 서버사이드 렌더링에 코드 스플리팅을 사용할 경우 @loadable-component를 사용하도록 추천하고 있습니다.

서버사이드 렌더링에 코드스플리팅이 적용되는 것을 알아보기 위해, 단계적으로 진행해보겠습니다. 먼저 dependencies부터 설치하죠

yarn add @loadable-component
yarn add --dev @types/loadable__component

그리고 App.tsx 파일을 수정해보겠습니다.

와우 너무 간단합니다. es6의 importloadable 함수로 넘겨주기만 하면 됩니다. 그럼 실행결과를 브라우저로 볼까요?

<title> 태그도 url에 맞게 잘 내려오고… <body> 안의 html 내용도 잘 채워져 있습니다. 그리고 code-splitting의 결과에 따라 0.js, 1.js 3.js 파일들 모두 나눠져서 내려옵니다. 정말 이대로 괜찮은 걸까요? 파일이 다운되는 순서를 보시죠.

흠…waterfall을 보면 코드스플리팅된 파일들이 main.js 와 WDM 파일들을 모두 로드하고 나서야 로드되는 것을 볼 수 있습니다. 이게 바로 서버사이드 렌더링용 코드 스플리팅이 필요한 이유입니다. 현재는 코드를 나누기만 했을 뿐 결국 하나의 번들과 똑같은 로딩시간을 필요로 합니다. 오히려 서버에 요청하는 request 횟수만 늘린 꼴이 되었습니다.

이제 제대로된 서버사이드 렌더링용 코드 스플리팅을 해보겠습니다. @loadable-component에서 추천하는 방식이 다소 복잡하기도 하거니와, HMR 설정까지 따라할 수 있는 가이드가 없어서 많이 애먹었습니다. 애초에 이 글을 작성하게 된 계기도 HMR을 적용하면서 WDM과 WHM 코드를 뜯어보는데서 시작하게 됐습니다.

일단 @loadable-component 의 SSR 가이드와 예제를 저희 환경에 맞게 적용해보겠습니다.

getConfig : 기존에는 config를 그대로 export 하는 방식과 다르게 module.exports = [getConfig(‘web’), getConfig(‘node’)] 라는 방식을 사용합니다.
이건 webpack의 multi-compiler 방식입니다. 상황에 맞게 다른 종류의 compiler를 쓸 수 있게 해주는 방식인데요. webpack.server.js 처럼 다른 파일을 작성하지 않고 하나의 webpack config 파일로 설정할때 쓰는 방식입니다. 이 부분이 code-splitting의 핵심이라고도 할 수 있습니다.

target : export 하는 부분을 보면 webnode 를 사용합니다. webpack.server.js 에서 설명드렸듯이 컴파일된 파일이 사용될 환경을 가정하는 것입니다. client 설정이지만 node 를 쓰는 이유는 server.tsx 에서 만든 express 서버가 코드를 읽어 html에 삽입해주어야 하기 때문입니다.

name : 컴파일된 파일에 이름을 붙이는건데, 이번 예제에서는 @loadable과 WHM이 해당 파일의 형식을 알 수 있도록 webnode 로 구분해줍니다. 그리고 hotMiddlewareScript 에도 name=web& 이라는 query string을 더해줍니다. 마지막에 &을 빼먹지 말아 주세요.

getEntryPoint : 서버사이드 렌더링에서는 entry 파일도 변경해줍니다. index.tsx 의 역할은 server.tsx 에서 담당해주니 그에 맞게 변경해줍니다. 그리고 서버사이드 렌더링용 string만 뽑는 node 환경에서는 WHM이 필요없으니 제외시켜 줍니다.

output : 서버사이드 렌더링에 필요한 파일과 클라이언트 사이드 렌더링에 필요한 파일을 나누기 위해 target 이름에 따라 폴더를 구분해주겠습니다. libraryTargetnode 일 경우 commonjs2 로 설정해둡니다. node.js는 module 시스템에서 commonjs 방식을 채택했기 때문입니다. web 은 default var 설정을 명시해주거나 undefined 를 넣어줍니다.

plugins : 서버사이드 렌더링을 도와줄 @loadable/webpack-plugin 을 넣어줍니다. node 타겟의 경우 HMR plugin은 제외시켜줍니다.

externals : 서버사이드 렌더링은 node 환경에서 실행되므로 webpack-node-externals 를 추가해주시되, 코드 스플리팅된 파일은 읽어야 하므로 @loadable-component 는 추가시켜 줍니다.

isWebTarget : webpack 에서 설정 target을 인식합니다. useBuiltIns 은 babel polyfill 설정인데, 'entry' 를 해줄 경우 @core-js 모듈로 import을 파싱합니다. target: { node: 'current' } 설정은 현재 node 버전에 맞게 최적화를 해줍니다. 나중에 더 많은 브라우저를 대응해야 할때 web에 설정된 undefined 대신 browserlist 방식으로 ie 등을 대응하시면 됩니다.

plugins : 역시나 코드 스플리팅을 인식하기 위해 @loadable/babel-plugin 을 추가해줍니다. 주의할 점은 import 구문을 babel이 처리할 수 있도록 해야합니다. 맨 처음 Typescipt 설정을 할 때 module: "esnext" 부분을 기억하시나요? 만약 module 시스템을 타입스크립트가 컴파일 할 경우 @loadable/babel-plugin 이 정상적으로 작동하지 않습니다.

server.tsx 에도 코드 스플리팅 설정을 추가해보죠.

webpackDevMiddleware : 큰 변화는 없지만 publicPath 부분이 변경되었습니다. 앞서 말씀드린대로 webpack.client.js 가 Array 형식의 multi compiler가 되었으니 0번째 compiler의 설정을 따라가게 합니다. getConfig('web') 을 먼저 export 해주기 때문에 0번째 입니다.

webExtractor : @loadable-component 제공하는 서버사이드 렌더링용 API 입니다. collectChunk 로 splitting 된 컴포넌트 정보를 취합하고 getLinkTag , getStyleTag , getScriptTag 로 로드할 파일 정보를 전달합니다.

loadableReady : 서버사이드 코드스플리팅을 하면 우리가 스플리팅한 js 파일들을 병렬로, 비동기적으로 불러옵니다. 이 API는 스플리팅된 js 파일들이 모두 불러와질때까지 기다렸다가 렌더링해주는 역할을 합니다.

hydrate : 직역하면 수분을 채운다는 뜻인데요. 서버사이드 렌더링의 원리는 우리가 사용하는 함수에서 유추할 수 있습니다. 첫 렌더링에 필요한 string만 뽑아서 html에 삽입시켜주고 (renderToString) 클라이언트 js 파일이 도착하면 그 html 태그에 이벤트를 달아주는 것이죠. 서버사이드 렌더링에서의 클라이언트 코드는 첫 로딩에서는 더 이상 렌더링할 필요가 없는 것이죠. 그래서 hydrate 로 이벤트만 채워넣어주면 되도록 method를 변경해줍니다.

마지막으로 App.tsx 에 webpack magic comment를 달아줍시다. magic comment는 코드 스플리팅된 파일에 이름을 달아주는 것입니다. 아까 만들어진 0.js, 1.js 등의 이름을 webpackChunkName 에 할당된 값으로 바꿔줍니다. 만약에 webpack에서 output.filename에 hash 설정을 하셔도 잘 작동합니다.

자 이제 정말 끝났습니다. script를 다시 실행해봅시다.

yarn start

<head><link> 가, <body><script> 가 잘 삽입된게 보이시나요? preload 설정도 자동으로 해주네요. react-helmet 이 잘 작동하는지 /news url로 접속했는데 <title> 에 News 라고 잘 들어가네요.

그럼 네트워크 탭에서도 정말 잘 작동하는지 볼까요?

네 보시는것 처럼 맨 위에 document 파일 로딩이 끝나면 필요한 js 파일들을 병렬로 로딩합니다. 저는 예제를 위해 Header와 Footer도 나눠 놓았지만 불필요하게 잘게 쪼개지는 마세요. ajax request도 한번에 처리할 수 있는 갯수가 제한되어 있답니다.


이렇게 Typescript + React + Server-side rendering + Code-splitting 에 필수적인 과정이 끝났습니다. 쓰다보니 너무 길어져서 React-hot-loader와 Styled-components, Stylelint, Prettier 설정은 다음 편에 업데이트 하겠습니다.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade