일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- c++
- 브라우저 동작
- Next.js
- react
- 자바스크립트 객체
- 네이버 부캠
- 코딩테스트
- Next/Image 캐싱
- 씨쁠쁠
- 비디오 스트리밍
- 부스트캠프
- 파이썬 웹크롤링
- 네이버 부스트캠프 멤버십
- 프로그래머스
- PubSub 패턴
- 자바 프로젝트
- beautifulsoup
- 네이버 부스트캠프
- 멘션 추천 기능
- Image 컴포넌트
- 자바스크립트 컴파일
- React.js
- 파이썬
- React ssr
- 파이썬 코딩테스트
- Server Side Rendering
- git checkout
- 자바스크립트
- 웹크롤링
- 스택
- Today
- Total
코린이의 개발 일지
React로 Server Side Rendering 구현하기 - 1. 웹팩 설정 본문
Next.js 쓰면 되는데, SSR을 직접 구현하는 이유?
- Next.js를 다뤄 보면서 내가 SSR에 제대로 이해하지 못하고, 구현에 급급하여 사용하고 있다는걸 느꼈다.
- Server Side Rendering을 조금 더 깊게 이해해보고자 React로 직접 구현해보기로 했다.
프로젝트 목표
- 기존에 구현했던 CSR 프로젝트를 SSR로 바꿀 것이다.
- 구현이 완료된 이후에는 기존 CSR 프로젝트와 SSR 프로젝트의 성능을 비교해볼 예정이다.
순전히 궁금증과 학습을 위해서 시작한 프로젝트이기 때문에 SSR을 처음 접하는 분이라면 Next.js를 사용하는걸 추천한다.
Next.js가 리액트 라우터와 호환이 안되는게 불만이라면 Remix라는 프레임워크를 추천한다.
우선 서버 사이드 렌더링의 기본 동작 과정부터 살펴보자
Server Side Rendering 동작 과정
- 현재 Next.js 내부에서 동작하는 Server Side Rendering은 위와 같이 동작한다.
- Client에서 Server로 HTTP요청을 보낸다.
- Server는 요청을 받고, API 서버 혹은 database로부터 Data를 fetching해온다.
- Server는 JavaScript Components를 컴파일해서 정적 HTML 문서를 생성하고, fetching해온 데이터가 있다면 해당 데이터도 함께 포함해서 HTML문서를 생성한다.
- Server는 Client로 HTML문서를 보낸다.
- Client는 HTML 문서를 다운로드 하여 정적 페이지를 렌더링한다.
- Client는 JavaScript 파일을 다운로드하고 정적 페이지에 이벤트 리스너를 붙인다. (이 과정을 Hydration이라 부른다)
나는 전에 만들어 놓은 Todo앱을 사용할 예정이다.
이 앱이 SSR로 동작하게 하려면 우선 다음과 같은 순서로 구현해야 한다.
- index.html을 생성할 서버를 구축한다.
- 서버에서는 투두 목록 Data를 fetching해오고
- JavaScript Components를 컴파일해서 정적 HTML 문서를 생성한다.
- Client에서 JavaScript 파일을 다운로드하고 응답으로 받은 HTML에 Hydration한다.
따라서 서버 구축이 제일 중요하다.
서버에서 Components를 컴파일해서 HTML을 생성 하려면 Client와 마찬가지로 웹팩으로 빌드를 해야한다.
그리고 생성한 HTML을 Client로 보내주어야한다.
api 서버에 Data fetching 해오는 것은 우선 나중에 다룰 예정이다.
우선 데이터 없이 정적 페이지만 생성해서 보내주는 작업을 해보자.
기본 세팅
웹팩 설정 꺼내주기
- 웹팩 설정 커스터마이징
npm run eject
yarn eject
웹팩 설정 커스터마이징을 위해 위 명령어를 통해 웹팩 설정을 모두 꺼내준다.
💡 import 구문에서 NODE_ENV 환경 변수가 설정되어 있지 않다는 ESLint오류 발생시 package.json에서 아래 와 같이 수정해준다.
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
],
"parserOptions": {
"babelOptions": {
"presets": [
[
"babel-preset-react-app",
false
],
"babel-preset-react-app/prod"
]
}
}
},
SSR용 엔트리 파일 생성
- src 디렉토리에 아래 파일을 생성해준다.
- 서버 사이드 렌더링 할 때, 서버를 위한 엔트리 파일이다.
// ./src/index.server.tsx
import React from 'react';
import ReactDOMServer from 'react-dom/server';
const html = ReactDOMServer.renderToString(<div>Server Side Rendering</div>);
console.log(html);
SSR용 웹팩 설정
- 웹팩 설정은 ./config 디렉토리에서 설정할 수 있다.
path 설정
- 하단에 ssr관련 설정을 추가해준다.
- 서버사이드 렌더링 할때 엔트리 포인트가 어딘지, 빌드된 결과물을 어디에 저장할지 설정한다.
// ./config/paths.js
module.exports = {
dotenv: resolveApp(".env"),
appPath: resolveApp("."),
...
appTsBuildInfoFile: resolveApp("node_modules/.cache/tsconfig.tsbuildinfo"),
swSrc: resolveModule(resolveApp, "src/service-worker"),
publicUrlOrPath,
ssrIndexJs: resolveApp("src/index.server.tsx"),
ssrBuild: resolveApp("dist"),
};
webpack.config.server.js
- 서버 사이드 렌더링 용 웹팩 설정을 해줄것이다.
// ./config/webpack.config.server.js
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", // 청크 파일이름
publicPath: paths.servedPath, // 정적 파일이 제공 될 경로 => 앞서 설정해둔 설정이다.
},
}
- 기본 설정을 했으니 이제 로더를 설정해 주어야한다.
- 서버에서 실행하는 자바스크립트 코드 역시 바벨을 사용하여 트랜스파일링 해주어야한다.
- SSR할때 CSS나 이미지 파일은 빌드 결과물에 포함되지 않도록 한다.
- 그러나 자바스크립트 내부에서 파일에 대한 경로가 필요하거나 CSS Module 처럼 로컬 className을 참조해야 할 수도 있기 때문에 처리를 위한 로더 설정은 필요하다.
const paths = require("./paths");
const webpack = require("webpack");
const getClientEnvironment = require("./env");
const getCSSModuleLocalIdent = require("react-dev-utils/getCSSModuleLocalIdent");
const cssRegex = /\\.css$/;
const cssModuleRegex = /\\.module\\.css$/;
const publicUrl = paths.publicUrlOrPath.slice(0, -1);
const env = getClientEnvironment(publicUrl);
module.exports = {
mode: "production", // 프로덕션 모드로 설정하여 최적화 옵션들을 활성화
entry: paths.ssrIndexJs, // 엔트리 경로
target: "node", // node 환경에서 실행 될 것이라는 것을 명시
output: {
path: paths.ssrBuild, // 빌드 경로
filename: "server.js", // 파일이름
chunkFilename: "js/[name].chunk.js", // 청크 파일이름
publicPath: paths.servedPath, // 정적 파일이 제공 될 경로
},
module: {
rules: [
{
oneOf: [
// 타입스크립트를 위한 처리
{
test: /\\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
// 자바스크립트를 위한 처리
// 기존 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"
),
plugins: [
[
require.resolve("babel-plugin-named-asset-import"),
{
loaderMap: {
svg: {
ReactComponent: "@svgr/webpack?-svgo![path]",
},
},
},
],
],
cacheDirectory: true,
cacheCompression: false,
compact: false,
},
},
// CSS 를 위한 처리
{
test: cssRegex,
exclude: cssModuleRegex,
// exportOnlyLocals: true 옵션을 설정해야 실제 css 파일을 생성하지 않는다.
loader: require.resolve("css-loader"),
options: {
exportOnlyLocals: true,
},
},
// CSS Module 을 위한 처리
{
test: cssModuleRegex,
loader: require.resolve("css-loader"),
options: {
modules: true,
exportOnlyLocals: true,
getLocalIdent: getCSSModuleLocalIdent,
},
},
// 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]",
},
},
],
},
],
},
};
- 자 이제 라이브러리를 node module에서 불러올 수 있도록 처리를 해주어야한다.
resolve: {
modules: ["node_modules"], // import 구문으로 불러오면 node_modules에서 불러옴
},
원래는 라이브러리를 불러오면 빌드할 때 결과물 파일 안에 해당 라이브러리 관련 코드가 함께 번들링된다.
하지만 서버에서는 굳이 번들링된 결과물 내에 라이브러리 관련 코드가 들어있지 않아도 된다. 위의 설정으로 라이브러리를 node_modules에서 바로 불러올 수 있기 때문이다.
따라서 서버에서 번들링할 때는 node_modules에서 불러오는 코드는 제외하고 번들링 하는 것이 좋다.
webpack-node-externals 라는 라이브러리를 사용할 것이다.
- webpack-node-externals 설치, 설정
npm -D install webpack-node-externals
const nodeExternals = require("webpack-node-externals");
...
externals: [
nodeExternals({
allowlist: [/@babel/],
}),
],
- 환경변수 주입
- 환경변수를 주입하면 프로젝트 내에서 process.env.NODE_ENV 값을 참조할 수 있다.
plugins: [
new webpack.DefinePlugin(env.stringified), // 환경변수를 주입해줍니다.
],
- 빌드 스크립트 작성하기
- 서버에서 사용할 빌드 파일이다.
- 빌드시 호출할 파일.
// 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();
마지막으로 typescript 관련 설정이 있는데,
tsconfig 파일에서 noEmit 프로퍼티를 false로 바꿔줘야한다.
그래야 ts파일이 컴파일 되어 결과물로 js파일이 나오고, 그 js파일을 서버가 읽을 수 있다.
https://stackoverflow.com/questions/55304436/webpack-with-typescript-getting-typescript-emitted-no-output-error
// ./tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": false,
"jsx": "react-jsx"
},
"include": ["src"]
}
자 이제 환경 설정은 다 끝났다. 바로 실행해 볼 수 있는데 실행 스크립트를 생성해서 편하게 실행 해보자
// package.json
"scripts": {
"start": "node scripts/start.js",
"build": "node scripts/build.js",
"test": "node scripts/test.js",
"start:server": "node dist/server.js",
"build:server": "node scripts/build.server.js"
},
다음 글에서 서버 구축하고 SSG까지 구현해볼 예정이다.
'웹 (web) > 프론트엔드' 카테고리의 다른 글
React로 Server Side Rendering 구현하기 - 3. Data fetching (0) | 2023.03.30 |
---|---|
React로 Server Side Rendering 구현하기 - 2. 서버 구축 (1) | 2023.03.09 |
React Query 살펴보기 (0) | 2023.01.12 |
[Next.js] Next 동작 원리를 알아보자 (0) | 2022.12.03 |
[React] 리액트에서 Canvas API로 애니메이션 구현하기 (0) | 2022.10.21 |