DARK

Isomorphic

September 25, 2023

뭐든지 시작할때는 바닥부터 올라가야 이해할 수 있다고 느끼는 요즘이다.

1. react + typescript + babel

우선 간단한 리액트 앱을 만든다.

yarn add react react-dom

타입스크립트도 이왕에 챙겨준다.

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

yarn tsc --init

app.tsx 와 index.tsx 를 작성해준다.

index.tsx

import ReactDOM from 'react-dom';
import React from 'react';
import App from './App';
const rootElement = document.getElementById('root');

ReactDOM.render(<App />, rootElement);

tsconfig

{
  "compilerOptions": {
    "plugins": [{ "name": "typescript-tslint-plugin" }],
    "target": "esnext", // 타입스크립트가 컴파일러가 생성할 자바스크립트 코드 버전
    "jsx": "preserve",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "skipLibCheck": true
  }
}

여기서 target 과 module 을 esnext 로 설정하고
moduleResolution 을 node 로 설정한다

타입스크립트 파일은 컴파일 후에 자바스크립트로 변환되고 babel이 읽는다.

babel은 기본적으로 es모듈 구문을 처리하도록 설계되어 있다. 저렇게 설정하지 않으면 타입스크립트가 CommonJS 혹은 다른 모듈 시스템으로 변환할 수 있기 때문에 바벨이 정상적으로 작동하지 않을 수 있다.

물론 babel.config.json 에서도 @babel/preset-typescript 를 사용하여 타입스크립트를 처리하도록 설정해준다.

module.exports = {
  presets: [
    "@babel/preset-react",
    "@babel/preset-env",
    "@babel/preset-typescript",
  ],
}

2. webpack

webpack5로 깔아주었다.

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

그리고 webpack.cofing.js 와 webpack.dev.js 생성해준다.

webapck.config.js 에 먼저 도달해 어떤 파일로 갈지 라우팅 역할을 한다.
client와 server 는 ssr 용 dev 는 csr 로 쓰일것이다.

module.exports = function (env) {
  const { client, dev } = env;

  if (client) {
    return require(`./webpack.client`);
  } else if (dev) {
    return require(`./webpack.dev`);
  } else {
    return require(`./webpack.server`);
  }
};
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');

module.exports = {
  mode: 'development',

  entry: './src/index.tsx',

  devServer: {
    historyApiFallback: true,
    port: 3000,
    static: {
      directory: path.join(__dirname, 'public'), //개발 서버가 제공할 정적파일 디렉토리
      publicPath: '/',
    },
  },

  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: ['babel-loader', 'ts-loader'],
      },
    ],
  },

  resolve: {
    //WDS 가 서브하는 파일은 js 이므로 모두 ts, js 모두 넣어준다
    extensions: ['.js', '.jsx', '.ts', '.tsx'],
  },

  plugins: [
    new webpack.HotModuleReplacementPlugin(), // 새로고침 없이 변경사항 반영
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'public/index_dev.html',
    }), // html 파일을 자동으로 생성하고 관리
  ],
};

그리고 위 처럼 설정한대로 public 폴더에 index_dev.html 파일을 작성해준다

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" name="format-detection" content="telephone=no" />
    <meta name="viewport" content="width=device-width, user-scalable=no" />
    <meta
      name="description"
      content="React + Typescript + SSR + Code-splitting"
    />
    <meta name="google" content="notranslate" />
    <title>Hello World!</title>
  </head>

  <body>
    <div id="root"></div>
  </body>
</html>

++router
여기서 react-router 와 react-helmet 그리고 관련타입을 깔아주고

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

n개의 페이지를 만들고 각각 페이지마다 헬멧컴포넌트 안에 타이틀을 넣어주면
각 페이지마다 title 이 바뀌는 것을 볼 수 있다. csr과 ssr 에서 손쉽게 html 문서의 header 섹션을 관리해준다.
회사에서도 SEO 차원에서 비슷한 방식으로 사용하였고 봇이 각 페이지마다의 타이틀을 읽어 검색엔진에 우리 사이트를 효과적으로 노출해주었다.

3. SSR

이제 리액트 앱을 구성하였으니 서버사이드렌더링을 구현해본다.

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

Webpack-dev-middleware(WDM) 은 웹팩의 번들링 결과물을 실시간으로 메모리에 저장하고 개발중인 애플리케이션에 제공하는 미들웨어다. memory-fs 를 이용해 번들파일을 디스크가 아닌 메모리에 저장하기 때문에 빠른속도로 서빙할 수 있다

(memory-fs : Node.js 환경에서 파일 시스템을 메모리에 저장하여 사용하는 가상 파일 시스템 라이브러리. 이는 실제 디스크에 파일을 기록하지 않고, 대신 메모리에 파일을 저장하고 읽는 방식으로 작동)

webpack-hot-middleware(WHM) 변경된 파일정보를 브라우저에 갱신시킨다

  1. 파일 변경

  2. WDM 이 메모리에 파일 변경점을 메모리에 저장, 개발 서버에 빠르게 제공

  3. WHM 가 변경된 모듈을 브라우저에 제공

server.tsx

const webpack = require('webpack');
const webpackConfig = require('../webpack.client.js');

const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');

const compiler = webpack(webpackConfig);

app.use(
  //메모리상에 번들 파일을 저장하고 Express.js 서버와 통합하여 실시간으로 변화를 반영
  webpackDevMiddleware(compiler, {
    publicPath: webpackConfig.output.publicPath,
  }),
);
//핫 모듈 리플레이스먼트를 위한 미들웨어로, 변경된 모듈을 전체 페이지를 새로고침하지 않고도 실시간으로 업데이트
app.use(webpackHotMiddleware(compiler));

그리고 현재 파일이 위치한 디렉토리를 정적파일을 제공하는 디렉토리로 설정한다

app.use(express.static(path.resolve(__dirname)));

renderToString을 react-dom/server 에서 가져와 React 컴포넌트를 HTML 문자열로 렌더링한다.

const html = renderToString(
  //서버사이드 렌더링에서 BrowserRouter 대신 사용하는 React-router
  <StaticRouter location={req.url}>
    <App />
  </StaticRouter>,
);

csr에서는 BrowserRouter 라우터를 사용하는데 대신에
StaticRouter는 서버사이드의 경로를 처리하는데 사용된다. req.url 을 prop으로 받아 해당 url에 맞는 라우트를 렌더링한다.

그리고 렌더링된 react 컴포넌트에서 수집된 메타 데이터를 가져온다.

const helmet = Helmet.renderStatic();

이제 내가 렌더링할 html과 메타데이터의 준비가 끝났다면 보낼차례이다.

res.set('content-type', 'text/html');
res.send(`
    <!DOCTYPE html>
      <html lang="en">
        <head>
          <meta name="viewport" content="width=device-width, user-scalable=no">
          <meta name="google" content="notranslate">
     
          ${helmet.title.toString()}
        </head>
        <body>
          <div id="root">${html}</div>
          <script type="text/javascript" src="main.js"></script>
        </body>
      </html>
  `);

이제 작성한 server.tsx 를 번들링할 웹팩 파일을 작성한다. server.tsx

const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
  target: 'node',
  node: false, // it enables '__dirname' in files. If is not, '__dirname' always return '/'.
  entry: {
    server: './src/server.tsx',
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
    chunkFilename: '[name].js',
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: ['babel-loader', 'ts-loader'],
      },
    ],
  },
  resolve: {
    extensions: ['.js', '.jsx', '.ts', '.tsx'],
  },

  externals: [nodeExternals()],
};

express 코드를 컴파일 하므로 entry는 server.tsx

webpack.server.js

const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',

  target: 'node',

  node: false, // it enables '__dirname' in files. If is not, '__dirname' always return '/'.

  entry: {
    server: './src/server.tsx',
  },

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
    chunkFilename: '[name].js',
  },

  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: ['babel-loader', 'ts-loader'],
      },
    ],
  },

  resolve: {
    extensions: ['.js', '.jsx', '.ts', '.tsx'],
  },

  externals: [nodeExternals()],
};

webpack은 entry 포인트 부터 앱을 시작하여 종속성을 따라가며 번들링한다. hotMiddlewareScript 을 추가 하며 webpack HMR 기능을 초기화하고 변경 사항을 감지하여 브라우저에 전달한다.

webpack.client.js 는 이전 작성했던 webpack.dev.js 의 ssr 용이라 할 수 있다. 개발할때 클라이언트사이드 코드가 변경될 때 변경사항을 감지하고 빠르게 번들링 해주어 브라우저에 전달한다.

server.tsx 코드 중 일부 참고

if (process.env.NODE_ENV !== 'production') {
  //개발 중에 코드 변경사항을 실시간으로 반영하여 개발 효율성을 높입
  const webpack = require('webpack');
  const webpackConfig = require('../webpack.client.js'); // !!

  const webpackDevMiddleware = require('webpack-dev-middleware');
  const webpackHotMiddleware = require('webpack-hot-middleware');

  const compiler = webpack(webpackConfig);

  app.use(
    //메모리상에 번들 파일을 저장하고 Express.js 서버와 통합하여 실시간으로 변화를 반영
    webpackDevMiddleware(compiler, {
      publicPath: webpackConfig.output.publicPath,
    }),
  );
  //핫 모듈 리플레이스먼트를 위한 미들웨어로, 변경된 모듈을 전체 페이지를 새로고침하지 않고도 실시간으로 업데이트
  app.use(webpackHotMiddleware(compiler));
}

자 그리고 준비는 끝 !
스크립트를 작성한다.

"scripts": {
    "start": "yarn build:dev && node ./dist/server.js", //SSR
    "start:wds": "webpack-dev-server --env=dev --profile --color", //CSR
    "build:dev": "rm -rf dist/ && NODE_ENV=development yarn build:client && NODE_ENV=development yarn build:server",
    "build:server": "webpack --env=server --progress --profile --color",
    "build:client": "webpack --env=client --progress --profile --color"
  },
  • start:wds : 처음에 작업했던 webpack.dev.js 를 구동시키고

  • start :
    webpack.server.js webpack.client.js 를 구동시켜
    각각 서버와 클라이언트의 파일을 번들링하고 dist 폴더안으로 파일을 내보낸다.

    DIST

    main.js : webpack.client.js 번들파일

따로 main 이란 이름을 지정하진 않았지만 이 배열에서 단일 엔트리 포인트로 취급하여 기본적으로 main 이라는 이름을 사용한다.

entry: [hotMiddlewareScript, "./src/index.tsx"],

server.js : webpack.server.js 번들파일

4. 실행결과

CSR

CSR

SSR

SSR

초기 로드시 CSR 에서는 빈껍데기인 div 태그만 보이고 SSR 에서는 헤더가 잘 노출된다.
SSR 의 대표적인 장점을 여기서 확인할 수 있다. 이제 기본적인 구성을 마쳤다.
이 프로젝트로 다양한것들을 확장해볼 수 있겠다.


Profile picture

남에게 쉽게 설명해줄 수 있을 때
비로소 안다고 할 수 있다.