본문 바로가기
React

2021.08.21 React 서버 사이드 렌더링

by 해맑은 코린이 2021. 8. 21.

 

2021.08.21 React 서버 사이드 렌더링_ 정리노트 

 

https://korinkorin.tistory.com/70

 

2021.08.03 리액트 라우터로 SPA 개발하기

2021.08.03 리액트 라우터로 SPA 개발하기_정리노트 드디어....SPA 까지 왔다... 힘들어따...... SPA(Single Page Application) 말 그대로 한 개의 페이지로 이루어진 애플리케이션 기존에는 사용자가 다른 페이

korinkorin.tistory.com

 

오늘은 여기서 잠시 정리했던 서버사이드렌더링에 대해서 좀 더 심층적으로 알아보고, 리액트 프로젝트에 구현해볼 예정..!! 

 

그때 몰라서 구글링으로 혼자 찾아보고 간단하게 설레발쳐서 정리했었는데 ㅎㅅㅎ .. 책에도 나오는구나 조아써 

 

그 때 잠시 다뤘던 것처럼 리액트처럼 SPA 에서는 기본적으로 클라이언트 사이드 렌더링을 하고 있다. 얘는 브라우저에서 UI 렌더링을 모두 처리하는데, 자바스크립트를 실행해야, 우리가 만든 화면들이 사용자에게 보인다. 

 

 

리액트 프로젝트를 만들어서 개발서버를 실행하면 이렇게 네트워크 탭에 localhost 파일이 보임.

얘를 선택해서 Response 를 보면 index.js 파일에 있는 root 엘리먼트가 비어있는게 보일거임. 즉 이 페이지는 처음엔 빈 페이지라는 뜻!

 

그 이후에 자바스크립트가 실행되고 리액트 컴포넌트가 렌더링 되면서 우리에게 보이는 것이다. 

 

서버 사이드 렌더링을 구현하게 되면, 사용자가 웹 서비스에 방문 했을 때 서버 쪽에서 초기 렌더링을 대신해준다. 그리고 사용자가 html 을 전달받을 때 그 내부에 렌더링된 결과물이 보인다. 

 

그러면 본격적으로 서버사이드렌더링을 구현해보기 전에, 

다시 한 번 클라이언트 사이드 렌더링, 서버 사이드 렌더링에 대한 장단점 정리!


클라이언트 사이드 렌더링

첫 요청시 한 페이지만 불러와 사용자의 행동에 따른 필요한 데이터만 다시 읽어들이는 방식.

요즘은 웹에서 제공되는 정보가 정말 많기 때문에 새로고침이 일어나며 페이지를 로딩할 때마다 서버로부터 리소스를 전달받아 해석하고 화면에 렌더링하는 방식인 서버 사이드 렌더링에서 성능 이슈 때문에 나타난 방식.

필요시에 변경된 데이터만 받아서 트래픽을 감소할 수 있고, 새로 고침이 발생하지 않기 때문에 사용자가 네이티브 앱과 비슷한 경험을 할 수 있는 장점이 존재.

하지만 자바스크립트 위주로 돌아가는 프로젝트는 자바스크립트 엔진이 돌아가지 않으면 원하는 정보를 표시해주지 못한다. 위의 화면처럼 첫 렌더링시 root 엘리먼트가 비어있기 때문에, 자바스크립트 엔진이 없는 네이버, 다음 등의 검색 엔진은 이를 제대로 크롤링하지 못한다.

엔진이 있는 구글 또한 제대로 페이지를 크롤링해 갈 때도 있지만, 모든 페이지에 대해 자바스크립트를 실행해주지 않는다. 

그리고 앱의 규모가 커지면서 자바스크립트 파일도 같이 커지기 때문에 초기 구동속도가 느리다는 점도 단점이라 할 수 있겠다.

 

 

서버사이드 렌더링

전통적인 렌더링 방식.

사용자가 웹 서비스에 방문했을 때 서버 쪽에서 초기 렌더링을 대신해줌. 그러고 나서 사용자가 html 을 전달받을 때 그 내부에 렌더링 된 결과물을 브라우저는 표시해줌.

그렇기 때문에 초기 렌더링 성능이 개선되고, 사용자는 자바스크립트 파일 다운로드가 완료되지 않은 시점에서도 html 이 표시되므로, 대기 시간이 최소화되고, 이로 인한 사용자 경험은 증가하게 된다. 

또한 클라이언트 사이드 렌더링과 반대로 검색 엔진을 최적화 할 수 있다.

하지만 결국 브라우저가 해야할 일을 서버가 대신 처리하기 때문에 서버 리소스가 사용되고, 만약 수많은 사용자가 동시에 웹 페이지에 접속하면 서버에 과부하가 발생하게 된다. 따라서 사용자가 많은 서비스라면 캐싱, 로드 밸런싱을 통해 성능을 최적화를 해주어야 한다. 

그리고 , 서버 사이드 렌더링을 구현하게 된다면, 프로젝트의 구조가 좀 더 복잡해지고, 데이터 미리 불러오기 코드 스플리팅과의 호환 등 고려해야 할 사항 증가로 개발이 어려워진다는 단점이 있다. 

 

그 중 리액트와 같은 SPA 에서 코드 스플리팅과 서버 사이드 렌더링을 별도의 호환 작업 없이 두 기술을 함께 적용하면, 페이지에 깜빡임 현상이 발생한다. 

서버 사이드 렌더링된 결과물이 브라우저에 나타나고, 자바스크립트 파일이 그 다음으로 로딩. 이 때 자바스크립트는 실행하면서 아직 불러오지 않은 컴포넌트를 null 로 렌더링 해버린다. 

그러면 렌더링 된 결과물이 갑자기 사라졌다가 다시 코드 스플링이 된 컴포넌트들이 로딩이 되어 나타날 때까지 깜빡거리는 것이다!

 

그래서 이 문제점을 책에서는 Loadable Component 라이브러리에서 제공하는 기능을 써서 서버 사이드 렌더링 후 필요한 파일의 경로를 추철하여 렌더링 결과에 스크립트/스타일 태그를 삽입해 주는 방법으로 해결한다.


 

아마 길이가....매우 길 것 같아 서버 사이드 렌더링을 구현하는 포스팅/// 서버 사이드 렌더링과 코드 스플리팅 충돌시 해결 방법 포스팅

으로 나누어서 작성할 예정이다.

 

일단은 오늘도 프로젝트 생성!

 

$ yarn create react-app ssr-recipe
$ cd ssr-recipe
// 서버 사이드 렌더링 구현 프로젝트 준비하기 위해 라우터 설치
$ yarn add react-router-dom

 

src/components/Red.js

src/components/Bllue.js

(새로 생성)

 

src/components/Red.css

src/components/Blue.css

(새로 생성)

 

 

 

 

단순히 빨간색 네모, 파란색 네모 를 생성해주고, 

 

그 다음 각 링크로 이동할 수 있게 해주는 메뉴 컴포넌트도 작성.

 

src/components/Menu.js ( 새로 생성 )

 

 

 

그리고 페이지 라우트를 위한 페이지 컴포넌트 또한 생성.

 

src/pages/RedPage.js

src/pagesBluePage.js

 

( 새로 생성 )

 

 

 

 

후....... 페이지 컴포넌트도 다 만들었으니 App 컴포넌트에서 라우터만 설정해주면 끝!

 

 

src/App

 

아차차 BrowserRouter 로 리액트 라우터도 적용해야함.

 

src/index.js

 

 

 

실행 화면

 

후 이렇게 간단하게 서버 사이드 렌더링을 위한 프로젝트 세팅 끝!

 

혹시 라우터 부분 흐름이 이해가 안간다면

https://korinkorin.tistory.com/70

 

2021.08.03 리액트 라우터로 SPA 개발하기

2021.08.03 리액트 라우터로 SPA 개발하기_정리노트 드디어....SPA 까지 왔다... 힘들어따...... SPA(Single Page Application) 말 그대로 한 개의 페이지로 이루어진 애플리케이션 기존에는 사용자가 다른 페이

korinkorin.tistory.com

 

굽신 이것도 봐주세여

 


 

서버 사이드 렌더링을 구현하려면 웹팩 설정을 커스터마이징 해주어야 함. 근데 CRA 로 만든 프로젝트 에서는 웹팩 관련 설정이 기본적으로 모두 숨겨져있다. 얘를 yarn eject 로 끄집어 내주자.

 

웹팩

파일을 하나로 합치는 번들러. ( 의존성이 있는 모듈 코드를 하나 또는 여러 개의 파일로 만들어주는 도구)

http 요청의 비효율적 이슈 때문에 사용.

html,js,css,web-font,favicon,image,json data 등등 수많은 파일을 받아와야하는데, http/2 에서는 하나의 커넥션에 동시에 여러 파일들을 요청할 수 있지만, 여전히 파일의 개수가 적어야 좋은 경우들이 존재.

이때 웹팩은 여러 가지 기능을 통해 번들러의 역할을 수행한다.

 

 

// 변경 사항을 먼저 commit
$ git add .
$ git commit -m 'Commit before eject'

// CRA 의 웹팩 관련 설정이 기본적으로 모두 숨겨져있어 이를 밖으로 추출해주는 과정.
$ yarn eject

 

git 작업을 해주지 않으면,

이런 에러가 뜨니 commit 을 해주고 실행해주면 된다.

 

그리고 주의사항. yarn eject 를 한번 실행하게 되면 다시 되돌릴 수 없다는 경고문구가 나오는만큼, 한번 추출하면 이전 상태로 되돌릴 수 없다! 이 점에 유의. 

그리고 CRA 에서의 장점인 One build Dependency 의 장점을 잃게 된다. 작업 도중 하나의 패키지가 필요해서 설치한다거나,삭제할 때는 항상 다른 패키지들과의 의존성을 생각해야하고, Webpack 과 Babel 설정까지 익숙하지 않다면.... ㅎㅁㅎ.. 많은 설정을 변경해야하는 경우에만 씁시다

다른 커스터마이징을 제공하는 라이브러리가 있지만 eject 보다 자유롭지는 않고, eject의 단점인 안정성을 라이브러리가 완전히 보장해주지는 않는다. 

서버 사이드 렌더링을 하기 위한 과감한 선택.....yarn eject 실행.

 

 

 

 

실행을 하게 되면 이렇게 숨겨진 파일들이 톡하고 튀어나온다. 

이제 커스텀 ㄱ!

 

엔트리

웹팩에서 프로젝트를 불러올 때 가장 먼저 불러오는 파일. ( 현재 플젝에서는 index.js 파일을 엔트리로 사용 )

해당 파일부터 시작하여, 내부에 필요한 다른 컴포넌트와 모듈을 불러온다. 

 

서버 사이드 렌더링을 위해서는 서버를 위한 엔트리 파일을 따로 생성해야 한다. 

 

src/index.server.js ( 새로 생성 )

 

 

가장 기본적인 코드 형태.

 

 

이제 이 엔트리 파일을 웹팩으로 불러와서 빌드하려면 서버 전용 환경 설정을 만들어 주어야함.

 

 

src/paths.js

 

paths 파일 맨 밑에 서버 사이드 렌더링 엔트리 경로와, 웹팩 처리 후 저장 경로 이 두 가지를 추가해준다. ( 주석으로 단 부분 추가만 해주면 됨. )

 

 

config/webpack.config.server.js ( 새로 생성 )

 

// paths module import
const paths = require('./paths');


module.exports = {
    mode:'production', // 프로덕션 모드로 설정하여 최적화 옵션들을 활성화
    entry:paths.ssrIndexJs, // 엔트리 파일 경로
    target:'node',          // node 환경에서 실행될 것이라는 점을 명시
    output:{                
        path:paths.ssrBuild, // 빌드 경로
        filename:'server.js', // 파일 이름
        chunkFilename:'js/[name].chunk.js', // chunk file 이름
        publicPath:paths.publicUrlOrPath    // 정적 파일이 제공될 경로
    }
}

 

그 다음 웹팩 환경설정 부분을 파일을 새로 생성해 적어주었다.

빌드시 어떤 파일에서 시작해 파일을 불러오는지, 또 어디에 결과물을 저장할건지를 정해준 것.

 

그 다음으로 설정해야할 것은 3가지!

 

첫번째로는 로더를 설정해야 하는데, 웹팩에서의 로더는 파일을 불러올 때 확장자에 맞게 필요한 처리를 해준다. 

보통 웹팩을 사용하면 이전 자바스크립트 코드를 변환해주고, JSX 같은 문법을 브라우저에서 사용하기 위해 babel 을 사용해준다.

또, CSS 는 모든 CSS 코드를 결합, 이미지 파일은 파일을 다른 경로에 따로 저장하여 그 파일에 대한 경로를 자바스크립트에서 참조할 수 있게 설정해준다. 

서버 사이드 렌더링을 할 때에는 CSS 혹은 이미지 파일이 엄청 중요도를 가지는 것은 아니지만 완전히 무시할 수는 없기에 ( 자바스크립트 내부에서 파일에 대한 경로 필요, CSS Module 에서 로컬 className 을 참고 할 때 등등) 해당 파일을 로더에서 별도로 설정하여 처리할 것은 처리하지만, 따로 결과물에 포함되지 않도록 구현할 수 있다. 

 

 

또한, 브라우저에서 사용할 때는 결과물 파일에 리액트 라이브러리와 나의 애플리케이션에 관한 코드가 공존하는데, 서버에서는 라이브러리가 굳이 결과물 파일안에 들어있지 않아도 된다. node_modules 를 통해 바로 불러와서 사용할 수 있기 때문 ㅇㅇ 그래서 서버를 위해 번들링할 때는 webpack-node-extension 이라는 라이브러리를 사용해서  node_modules에서 불러오는 것을 제외하고 번들링하게 설정할 수 있음.

이 라이브러리는 설치가 필요하니 yarn 명령어로 설치를 해주면 된다. 

 $ yarn add webpack-node-externals

 

마지막으로 환경변수를 주입해주어야 한다. 이를 주입해주면, 프로젝트 내에서 process.env_NODE_ENV 값을 참조하여 현재 개발 환경인지 아닌지를 알 수 있다. 

 

 

먼 말인지 나도 다는 이해 안됨 ㅎ 책에서 나온 설정 역시 

 

출처 - https://webpack.js.org/configuration/

 

요기를 참고했다고 하니 ..! 꼭 하나하나 다 알 필요는 없을 것 같다 ㅎ

 

 

config/webpack.config.server.js

// webpack-node-externals 라이브러리 import
const nodeExternals = require("webpack-node-externals");
const paths = require("./paths");
const getCSSModuleLocalIdent = require("react-dev-utils/getCSSModuleLocalIdent");
const webpack = require("webpack");
const getClientEnvironment = require("./env");

const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
// 환경변수 
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));

module.exports = {
  mode: "production", // 프로덕션 모드로 설정하여 최적화 옵션들을 활성화
  entry: paths.ssrIndexJs, // 엔트리 파일 경로
  target: "node", // node 환경에서 실행될 것이라는 점을 명시
  // 결과물이 어떻게 나올지를 명시
  output: {
    path: paths.ssrBuild, // 빌드 경로
    filename: "server.js", // 파일 이름
    chunkFilename: "js/[name].chunk.js", // chunk file 이름
    publicPath: paths.publicUrlOrPath, // 파일들이 위치할 서버 상의 경로 .
  },
//loader
  module: {
    rules: [
      {
        oneOf: [
          // 자바스크립트를 위한 처리
          // 기존 webpack.config.js 를 참고하여 작성
          {
            test: /\.(js|mjs|jsx|ts|tsx)$/,
            include: paths.appSrc,
            loader: require.resolve("babel-loader"),
            options: {
              customize: require.resolve(
                "babel-preset-react-app/webpack-overrides"
              ),
              presets: [
                [
                  require.resolve("babel-preset-react-app"),
                  {
                    runtime: "automatic",
                  },
                ],
              ],
              // import 에 대한 플러그인
              plugins: [
                [
                  require.resolve("babel-plugin-named-asset-import"),
                  {
                    loaderMap: {
                      svg: {
                        ReactComponent:
                          "@svgr/webpack?-svgo,+titleProp,+ref![path]",
                      },
                    },
                  },
                ],
              ],
              cacheDirectory: true,
              cacheCompression: false,
              compact: false,
            },
          },
          // CSS 를 위한 처리
          {
            test: cssRegex,
            exclude: cssModuleRegex,
            loader: require.resolve("css-loader"),
            options: {
              importLoaders: 1,
              modules: {
                //  exportOnlyLocals: true 옵션을 설정해야 실제 css 파일을 생성하지 않음.
                exportOnlyLocals: true,
              },
            },
          },
          // CSS Module 을 위한 처리
          {
            test: cssModuleRegex,
            loader: require.resolve("css-loader"),
            options: {
              importLoaders: 1,
              modules: {
                exportOnlyLocals: true,
                getLocalIdent: getCSSModuleLocalIdent,
              },
            },
          },
          // Sass 를 위한 처리
          {
            test: sassRegex,
            exclude: sassModuleRegex,
            use: [
              {
                loader: require.resolve("css-loader"),
                options: {
                  importLoaders: 3,
                  modules: {
                    exportOnlyLocals: true,
                  },
                },
              },
              require.resolve("sass-loader"),
            ],
          },
          // Sass + CSS Module 을 위한 처리
          {
            test: sassRegex,
            exclude: sassModuleRegex,
            use: [
              {
                loader: require.resolve("css-loader"),
                options: {
                  importLoaders: 3,
                  modules: {
                    exportOnlyLocals: true,
                    getLocalIdent: getCSSModuleLocalIdent,
                  },
                },
              },
              require.resolve("sass-loader"),
            ],
          },
          // url-loader 를 위한 설정
          {
            test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
            loader: require.resolve("url-loader"),
            options: {
              emitFile: false, // 파일을 따로 저장하지 않는 옵션
              limit: 10000, // 원래는 9.76KB가 넘어가면 파일로 저장하는데
              // emitFile 값이 false 일땐 경로만 준비하고 파일은 저장하지 않음.
              name: "static/media/[name].[hash:8].[ext]",
            },
          },
          // 위에서 설정된 확장자를 제외한 파일들은
          // file-loader 를 사용.
          {
            loader: require.resolve("file-loader"),
            exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
            options: {
              emitFile: false, // 파일을 따로 저장하지 않는 옵션
              name: "static/media/[name].[hash:8].[ext]",
            },
          },
        ],
      },
    ],
  },
  // react, react-dom/server 같은 라이브러리를 import 구문으로 불러오게 되면, node_modules 에서 찾아서 불러오기 위한 설정. 라이브러리를 불러오면 빌드할 때 결과물 파일 안에 해당 라이브러리 관련 코드가 함께 번들링.
  resolve: {
    modules: ["node_modules"],
  },
  // 서버를 위해 번들링할 때는 node-modules 에서 불러오는 것을 제외하고 번들링하는 것이 좋음. 이를 위해 webpack-node-extenrnals 라이브러리 사용
  externals: [nodeExternals()],
  plugins: [
    new webpack.DefinePlugin(env.stringified), // 환경변수를 주입.
  ],
};

 

 

매우 매우 매우 길군.....ㅎ..... 책에는 정말 간단하게 주석으로 정리해놔서 책부분 + 웹팩 부분 구글링 해서 쪼오오금의 주석을 더 추가했다. 나머지는 주절주절로 위에 줄글로 정리해보아쑴 ㅎ

 

 

이제 환경설정은 끝!

 

이제 이 환경설정을 사용해서 웹팩으로 프로젝트를 빌드하는 스크립트를 작성.

 

src/scripts/build.js 이 경로에 있는 파일은 클라이언트에서 사용할 빌드 파일이다. 우리는 얘랑 비슷하게 서버에서 사용할 빌드 파일을 만드는 build.server.js 스크립트를 작성하면 됨.

 

src/scripts/build.server.js ( 새로 생성 )

 

process.env.BABEL_ENV = 'production';
process.env.NODE_ENV = 'production';

process.on('unhandledRejection', (err) => {
  throw err;
});

require('../config/env');
const fs = require('fs-extra');
const webpack = require('webpack');
const config = require('../config/webpack.config.server');
const paths = require('../config/paths');

function build() {
  console.log('Creating server build...');
  fs.emptyDirSync(paths.ssrBuild);
  let compiler = webpack(config);
  return new Promise((resolve, reject) => {
    compiler.run((err, stats) => {
      if (err) {
        console.log(err);
        return;
      }
      console.log(stats.toString());
    });
  });
}

build();

 

요롷게 빌드 파일 작성해서 명령어를 통해 빌드가 잘되는지를 확인.

 

$ node scripts/build.server.js

 

 

요롷게 잘 빌드가 된다면, 이제 실행을 시험삼아 해보자.

 

node dist/server.js

 

갸악 세상에 index.server.js 에서 썼던대로 문자열 형태로 렌더링되서 출력해주는군 

 

근데 실행할 때마다 파일 경로 치기 넘나 귀차느니까 설정을 해줘서 간단하게 명령어를 칠 수 있도록 하자.

 

 

package.json 

 

scripts 부분에 이렇게 적어쥼~~~ 

 

 

굿 이제 간단하게 빌드랑 서버 명렁어도 잘 설정됐음. 


 

ㅎ...........이제 구현 준비 완료가 된 것뿐 이제 시작임...갈 길이 구만리에요.....................ㅠ

 

서버 사이드 렌더링을 처리할 서버를  Node.js 의 프레임워크인 Express 를 사용해서 만들겠음.

다른 Koa, Hapi 등등 connect 라이브러리를 사용하면 구현할 수 있지만서도 Express 가 사용률이 가장 높고, 추후 정적 파일들을 호스팅 할 때에도 쉽게 구현할 수 있기 때문!

 

$ yarn add express

 

src/index.server.js

 

import React from "react";
import ReactDOMServer from "react-dom/server";
// express import
import express from "express";
// StaticRouter import
import { StaticRouter } from "react-router-dom";
// App import
import App from "./App";

const app = express();

// 서버 사이드 렌더링을 처리할 핸들러 함수.
// 이 함수는 page not found 404가 떠야 하는 상황에서 404 에러를 띄우지 않고 서버 사이드 렌더링을 해줌.
const serverRender = (req, res, next) => {
  const context = {};
  const jsx = (
    <StaticRouter location={req.url} context={context}>
      <App />
    </StaticRouter>
  );
  const root = ReactDOMServer.renderToString(jsx); // 렌더링 후
  res.send(root); // 클라이언트에게 결과물 응답
};

app.use(serverRender);

app.listen(5000, () => {
  console.log("Runninng on http://localhost:5000");
});

 

여기서 사용된 StaticRouter 컴포넌트는 주로 서버 사이드 렌더링 용도로 사용되는 라우터인데, props 로 넣어주게 되는 location 값에 따라 라우팅해준다. req 객체는 요청에 대한 정보를 지니는데, 이의 url 을 props 로 넣어주었음.

또 context props 는 이 값을 사용해서 나중에 렌더링한 컴포넌트에 따라 HTTP 상태코드를 설정 해줄 수 있도록 해준 것이다. 

 

또 맨 마지막 함수는 5000번의 포트로 서버를 가동하기 위해 사용되었다. 

리액트는 3000번 포트, 장고는 8000번 포트를 사용했던 것처럼 따로 우리 서버는 5000번 포트를 가동하도록 설정해준 것! ( 8000번... 갑자기 그리워짐.... 장고 돌아와......)

 

어질어질하다.........

 

당장은 JS,CSS 파일을 웹 페이지에 불러오는 것을 생략하고, 리액트 서버 사이드 렌더링을 통해 만들어진 결과만 보여주도록 처리했다. 

// build 
$ yarn build:server
// 실행
$ yarn start:server

 

해주고 localhost:5000 경로로 접속.

 

 

실행 화면

흑..흑 CSS 불러오지는 않고, 브라우저에서 자바스크립트도 실행되지 않지만 이 모든 것들은 서버 사이드에서 렌더링된 결과물이다. 

 

근데 자바스크립트가 클라이언트 딴에서 실행된건지 아니면 서버에서 렌더링된 건지 분간이 어렵다면,

 

 

개발자 도구의 Network 탭에서 새로고침을 하고 맨 위의 파일을 클릭하고 해당 Response 를 보면 이렇게 컴포넌트의 렌더링 결과가 문자열로 전달된 것을 볼 수 있다. 

 

 

참고로 이 {} 버튼 누르면 들여쓰기 때문에 더 편하게 봐짐.

 

 

 

이제 CSS, JS 정적 파일들에 접근할 수 있도록 Express 에 내장된 미들웨어 static 을 사용!

 

import React from "react";
import ReactDOMServer from "react-dom/server";
// express import
import express from "express";
// StaticRouter import
import { StaticRouter } from "react-router-dom";
// App import
import App from "./App";
//path import
import path from 'path';
const app = express();

// 서버 사이드 렌더링을 처리할 핸들러 함수.
// 이 함수는 page not found 404가 떠야 하는 상황에서 404 에러를 띄우지 않고 서버 사이드 렌더링을 해줌.
const serverRender = (req, res, next) => {
  const context = {};
  const jsx = (
    <StaticRouter location={req.url} context={context}>
      <App />
    </StaticRouter>
  );
  const root = ReactDOMServer.renderToString(jsx); // 렌더링 후
  res.send(root); // 클라이언트에게 결과물 응답
};

const serve = express.static(path.resolve('./build',{
    index:false // "/" 경로에서 index.html 을 보여주지 않도록 설정.
}))
//// 반드시 serverRender 위에 위치할것.
app.use(serve);
/////////////////
app.use(serverRender);

app.listen(5000, () => {
  console.log("Runninng on http://localhost:5000");
});

 

여기서 serve 부분만 참고. 캡쳐본을 올리니까 더 헷갈려하는 사람들이 쩜 있길래 ㅎㅅㅎ path import 도 잊지말기.

 

그 다음에는 JS , CSS 파일을 불러오도록 html 에 코드를 삽입해 주어야 하는데, 불러와야 할 파일 이름은 매번 빌드를 할 때마다 바뀌기 때문에 빌드하고 나서 만들어지는 파일을 참고하여 불러오도록 해야 하는데, 그 파일은 build/asset-mainfest.json 임!

 

yarn build ( yarn build:server 아님 )후에 build 디렉토리가 생성되면, 그 안의 asset-mainfest.json 파일을 살펴보자.

 

build/asset-mainfest.json 

 

여기서 몇개의 부분을 html 내부에 적어주어야 한다. 

 

src/index.server.js

import React from "react";
import ReactDOMServer from "react-dom/server";
// express import
import express from "express";
// StaticRouter import
import { StaticRouter } from "react-router-dom";
// App import
import App from "./App";
//path import
import path from 'path';
// fs import
import fs from 'fs';

// asset-manifest.json에서 파일 경로들을 조회
const manifest = JSON.parse(
  fs.readFileSync(path.resolve('./build/asset-manifest.json'), 'utf8')
);


const chunks = Object.keys(manifest.files)
  .filter(key => /chunk\.js$/.exec(key)) // chunk.js로 끝나는 키를 찾아서
  .map(key => `<script src="${manifest.files[key]}"></script>`) // 스크립트 태그로 변환하고
  .join(''); // 합침

function createPage(root, tags) {
    return `<!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="utf-8" />
        <link rel="shortcut icon" href="/favicon.ico" />
        <meta
          name="viewport"
          content="width=device-width,initial-scale=1,shrink-to-fit=no"
        />
        <meta name="theme-color" content="#000000" />
        <title>React App</title>
        <link href="${manifest.files['main.css']}" rel="stylesheet" />
      </head>
      <body>
        <noscript>You need to enable JavaScript to run this app.</noscript>
        <div id="root">
          ${root}
        </div>
        <script src="${manifest.files['runtime-main.js']}"></script>
        ${chunks}
        <script src="${manifest.files['main.js']}"></script>
      </body>
      </html>
        `;
}
  
const app = express();

// 서버 사이드 렌더링을 처리할 핸들러 함수.
// 이 함수는 page not found 404가 떠야 하는 상황에서 404 에러를 띄우지 않고 서버 사이드 렌더링을 해줌.
const serverRender = (req, res, next) => {
  const context = {};
  const jsx = (
    <StaticRouter location={req.url} context={context}>
      <App />
    </StaticRouter>
  );
  const root = ReactDOMServer.renderToString(jsx); // 렌더링 후
  res.send(createPage(root)); // 클라이언트에게 결과물 응답
};

const serve = express.static(path.resolve('./build'),{
    index:false // "/" 경로에서 index.html 을 보여주지 않도록 설정.
});
//// 반드시 serverRender 위에 위치할것.
app.use(serve);
/////////////////
app.use(serverRender);

app.listen(5000, () => {
  console.log("Runninng on http://localhost:5000");
});

 

 

CSS,JS 까지 모두 불러온 기본 서버 사이드 렌더링 최종본 ..! html 부분은 복붙해서 그냥 스윽 보는 것을 추천...ㅎ..!! 불러오는 거뿐이니까 말이당. import 부분은 fs 만 추가해서 asset-manifest.json 에서 파일 경로를 조회할 수 있도록 했다. 

 

 

이제 빌드, 다시 시작.

 

$ yarn build:server
$ yarn start:server

 

실행 화면

 

네트워크 탭에서 보면 이렇게 스크립트태그도 잘 들어가 있고, css 파일도 적용이 잘되어 나타남 짝짜ㅏㅏ짜짝 

그리고 주의할 점은 처음 렌더링은 서버를 통해 하게 되지만, red,blud 링크를 이동할 때의 렌더링은 브라우저에서 해야함. 다른 페이지로 이동할 때 네트워크의 추가적인 요청이 개발자 도구에 뜨면 안됨!

 

 


 

와! 기본적인 서버 사이드 렌더링 끝ㅌㅌㅌㅌ!!!!! 아직 갈길이 멀지만 기초만 잘 다져도 될 것....후.... 화이팅화이팅...흑...흑 쨋든 리액트에서의 서버 사이드 렌더링 확인-!

댓글