본문 바로가기
IT/react

[react] ssr 프로젝트 만들기 (1) - hello world 찍기

by 내일은교양왕 2024. 7. 1.

 

목표

webpack 5 사용

stream을 이용해서 ssr 제공

react-router 적용하기

react-query로 api pretech 하기

local 환경에서 hmr 적용하기

js로 결과물을 ts로 변경하기

production과 local & dev 빌드 결과물 다르게 설정

번들링 분석 플러그인 설치

 

코드는 여기에서 확인

https://github.com/insidedw/react-webpack5-ssr/commit/b1f1132a20fbbbcb6d6e67861be91e7d9305ef74

 

print hello world · insidedw/react-webpack5-ssr@b1f1132

insidedw committed Jul 7, 2024

github.com

 

 

시작 전

linting 관련된 세팅은 생략

 

프로젝트 세팅

mkdir my-ssr-app
cd my-ssr-app
npm init -y
npm install react react-dom express
npm install -D webpack webpack-cli webpack-node-externals @babel/core @babel/preset-env @babel/preset-react babel-loader

 

 

프로젝트 구조

my-ssr-app/
├── public/
│   └── index.html
├── src/
│   ├── components/
│   │   └── App.js
│   ├── client.js
│   └── server.js
├── webpack/
│   ├── webpack.client.js
│   └── webpack.server.js
├── package.json
└── babel.config.js

 

바벨 설정

react 코드를 브라우저에서 읽어야 하니까

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

 

서버 webpack 설정

webpack/webpack.server.js 파일을 생성하고 다음 내용을 추가

nodeExternals() 를 추가한 이유는 빌드에 node기본 라이브러리를 추가하지 말라고. 서버에는 이미 node가 설치되어 있으니까 그거 쓰면 된다.

서버 번들 파일 이름은 고정해도 된다. 배포 시 덮어씌우거나 지우고 새로운 파일을 생성 됨

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

module.exports = {
  mode: 'development',
  entry: './src/server.js',
  target: 'node',
  externals: [nodeExternals()],
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: 'server.bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      }
    ],
  },
};

 

클라이언트 Webpack 설정

webpack/webpack.client.js 파일을 생성하고 다음 내용을 추가

const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/client.js',
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: 'client.bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      }
    ],
  },
};

 

React 컴포넌트 작성

src/components/App.js 파일을 생성하고 간단한 React 컴포넌트를 작성

import React from 'react';

const App = () => {
  return (
    <div>
      <h1>Hello, SSR!</h1>
    </div>
  );
};

export default App;

 

클라이언트 엔트리

src/client.js 파일을 생성하고 다음 코드를 추가

import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';

ReactDOM.hydrate(<App />, document.getElementById('root'));

 

서버 엔트리

src/server.js 파일을 생성하고 다음 코드를 추가

import path from 'path';
import fs from 'fs';
import React from 'react';
import express from 'express';
import { renderToString } from 'react-dom/server';
import App from './components/App';

const app = express();

app.use(express.static('dist'));

app.get('*', (req, res) => {
  const appString = renderToString(<App />);

  const indexFile = path.resolve('./public/index.html');
  fs.readFile(indexFile, 'utf8', (err, data) => {
    if (err) {
      console.error('Something went wrong:', err);
      return res.status(500).send('Oops, better luck next time!');
    }

    return res.send(
      data.replace('<div id="root"></div>', `<div id="root">${appString}</div>`)
    );
  });
});

app.listen(3000, () => {
  console.log('Server is listening on port 3000');
});

 

HTML 파일 작성

public/index.html 파일을 생성하고 다음 내용을 추가합니다.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>React SSR</title>
</head>
<body>
  <div id="root"></div>
  <script src="/client.bundle.js"></script>
</body>
</html>

 

NPM 스크립트 추가 

package.json 파일의 scripts 섹션을 수정하여 빌드 및 서버 실행 스크립트를 추가합니다.

"scripts": {
  "build": "webpack --config webpack/webpack.client.js && webpack --config webpack/webpack.server.js",
  "start": "node dist/server.bundle.js"
}

 

프로젝트 빌드 및 실행 

이제 프로젝트를 빌드하고 실행할 준비가 되었습니다.

npm run build
npm start