처음부터 기본 React SSR 프레임워크 구축하기
Grace Collins
Solutions Engineer · Leapcell

React와 함께하는 서버 측 렌더링 소개
웹 개발의 역동적인 환경에서 사용자 경험과 검색 엔진 최적화(SEO)는 매우 중요합니다. React와 같은 라이브러리로 구축된 최신 단일 페이지 애플리케이션(SPA)은 풍부한 대화형 경험을 제공하지만, 초기 로딩 성능과 검색 엔진 인덱싱에 어려움을 겪는 경우가 많습니다. 바로 이때 서버 측 렌더링(SSR)이 등장합니다. SSR을 사용하면 서버에서 React 컴포넌트를 정적 HTML로 렌더링하여 완전히 구성된 페이지를 클라이언트로 보낼 수 있습니다. 이 접근 방식은 사용자가 즉시 콘텐츠를 볼 수 있고 검색 엔진이 선호하는 크롤링 가능한 HTML 구조를 제공하므로 인식되는 로딩 시간을 크게 개선합니다. 이 글에서는 간단하면서도 기능적인 React SSR 프레임워크를 처음부터 구축하는 과정을 안내하며, 내부 메커니즘을 명확히 설명하고 이점을 활용하도록 돕겠습니다.
SSR의 핵심 개념 이해하기
구현에 들어가기 전에 React SSR과 관련된 주요 개념을 명확히 이해하는 것이 중요합니다.
React 컴포넌트
모든 React 애플리케이션의 핵심에는 컴포넌트가 있습니다. JSX를 사용하여 작성된 재사용 가능하고 독립적인 UI 조각입니다. SSR의 경우, 이러한 동일한 컴포넌트가 서버에서 렌더링됩니다.
ReactDOMServer
이것은 React에서 제공하는 중요한 패키지로, React 컴포넌트를 정적 HTML 문자열로 렌더링할 수 있게 해줍니다. 특히 renderToString
및 renderToStaticMarkup
함수를 주로 사용하게 될 것입니다. renderToString
은 일반적으로 data-reactid
속성을 포함하여 클라이언트 측에서 React가 컴포넌트를 "hydrate"할 수 있게 해주므로 전체 DOM을 다시 렌더링하지 않고도 상호 작용이 가능하게 만들어주기 때문에 선호됩니다.
클라이언트 측 Hydration
서버가 HTML을 보낸 후, 클라이언트 측 React 코드가 이 사전 렌더링된 HTML에 "연결"됩니다. Hydration으로 알려진 이 과정은 정적 HTML을 완전히 상호 작용하는 React 애플리케이션으로 변환하며, 서버 렌더링된 DOM 구조를 보존하고 깜박임이나 다시 렌더링을 방지합니다.
Express.js
Node.js를 위한 미니멀리즘 웹 프레임워크인 Express.js는 우리의 서버 역할을 하며 HTTP 요청을 처리하고 React 애플리케이션을 렌더링하여 결과 HTML을 클라이언트로 다시 보냅니다.
Babel
JSX와 잠재적으로 최신 JavaScript 기능을 사용하여 React 컴포넌트를 작성할 것이므로, Babel은 코드를 Node.js와 클라이언트 측 브라우저가 이해할 수 있는 형식으로 변환하는 데 필수적입니다.
미니멀리즘 React SSR 프레임워크 구축하기
단계별로 SSR 프레임워크를 구축해 봅시다.
프로젝트 설정
먼저 새 프로젝트 디렉토리를 만들고 초기화합니다.
mkdir react-ssr-framework cd react-ssr-framework npm init -y
다음으로 필요한 종속성을 설치합니다.
npm install react react-dom express @babel/core @babel/preset-env @babel/preset-react babel-loader webpack webpack-node-externals npm install --save-dev nodemon
서버(Node.js 기능 사용 및 브라우저 호환성에 덜 신경 써도 됨)와 클라이언트(브라우저 호환성이 필요함)에 대한 두 가지 별도의 Babel 구성이 필요합니다. 이 예제에서는 단순화를 위해 두 가지 모두에 단일 .babelrc
를 사용하여 클라이언트 측 코드에 대한 브라우저 호환성을 보장합니다.
.babelrc
파일을 만듭니다:
{ "presets": ["@babel/preset-env", "@babel/preset-react"] }
첫 React 컴포넌트
서버와 클라이언트 모두에서 렌더링될 간단한 App.js
컴포넌트를 만들어 봅시다.
// src/components/App.js import React from 'react'; const App = ({ message }) => { return ( <div> <h1>Hello from React SSR!</h1> <p>{message}</p> <button onClick={() => alert('This is an interactive button!')}> Click Me </button> </div> ); }; export default App;
서버 측 렌더링 로직
이제 Express.js를 사용하여 App
컴포넌트를 렌더링하도록 서버를 설정해 봅시다.
// src/server/index.js import express from 'express'; import React from 'react'; import ReactDOMServer from 'react-dom/server'; import App from '../components/App'; const app = express(); app.get('/', (req, res) => { const initialProps = { message: 'This content was rendered on the server!' }; const appString = ReactDOMServer.renderToString(<App {...initialProps} />); res.send(` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>React SSR App</title> </head> <body> <div id="root">${appString}</div> <script> window.__INITIAL_PROPS__ = ${JSON.stringify(initialProps)}; </script> <script src="/client_bundle.js"></script> </body> </html> `); }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); });
몇 가지 주요 사항에 주목하세요:
- HTML 문자열을 얻기 위해
ReactDOMServer.renderToString
을 사용하고 있습니다. appString
을 HTML 응답에 직접 내장하고 있습니다.window.__INITIAL_PROPS__
는 서버 렌더링에 사용된 것과 동일한 props를 보유하는 전역 변수입니다. 이는 클라이언트 측 hydration에 매우 중요합니다.script src="/client_bundle.js"
는 클라이언트 측 JavaScript 번들을 제공할 것임을 나타냅니다.
클라이언트 측 Hydration
클라이언트 측 번들은 애플리케이션을 다시 hydration하는 책임을 맡게 됩니다.
// src/client/index.js import React from 'react'; import ReactDOM from 'react-dom'; import App from '../components/App'; // 서버에서 보낸 초기 props 검색 const initialProps = window.__INITIAL_PROPS__; ReactDOM.hydrate(<App {...initialProps} />, document.getElementById('root'));
여기서 ReactDOM.hydrate
는 ReactDOM.render
대신 사용됩니다. hydrate
는 React에게 모든 것을 다시 렌더링하는 대신 기존 HTML 마크업에 연결을 시도하도록 지시합니다.
Webpack으로 번들링하기
브라우저에 제공되도록 클라이언트 측 JavaScript를 번들링해야 합니다. 또한 서버 측 코드를 컴파일해야 합니다. Node.js는 사전 처리가 없으면 JSX나 ES 모듈의 import
/export
를 기본적으로 이해하지 못하기 때문입니다.
webpack.config.js
파일을 만듭니다.
// webpack.config.js const path = require('path'); const nodeExternals = require('webpack-node-externals'); const clientConfig = { mode: 'development', entry: './src/client/index.js', output: { path: path.resolve(__dirname, 'public'), filename: 'client_bundle.js', }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', }, }, ], }, }; const serverConfig = { mode: 'development', target: 'node', // 서버 번들에 필수적 externals: [nodeExternals()], // node_modules 종속성 번들링 방지 entry: './src/server/index.js', output: { path: path.resolve(__dirname, 'build'), filename: 'server_bundle.js', }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', }, }, ], }, }; module.exports = [clientConfig, serverConfig];
두 가지 구성이 있습니다: clientConfig
는 클라이언트 측 코드를 public/client_bundle.js
로 번들링하고, serverConfig
는 서버 측 코드를 build/server_bundle.js
로 번들링합니다. serverConfig
의 target: 'node'
및 externals: [nodeExternals()]
는 Node.js 환경에 중요합니다.
정적 자산 제공
서버는 클라이언트 측 번들을 제공해야 합니다. src/server/index.js
파일의 app.get
경로 전에 줄을 추가합니다.
// src/server/index.js (이 줄을 추가하세요) app.use(express.static('public')); // 'public' 디렉토리에서 정적 파일 제공
npm 스크립트
애플리케이션을 빌드하고 실행하기 위해 package.json
에 스크립트를 추가합니다.
// package.json { "name": "react-ssr-framework", // ... 다른 필드 "scripts": { "build:client": "webpack --config webpack.config.js --env.target=client", "build:server": "webpack --config webpack.config.js --env.target=server", "build": "webpack --config webpack.config.js", "start": "node ./build/server_bundle.js", "dev": "npm run build && nodemon --watch build --exec \"npm start\"" }, // ... 다른 필드 }
이제 npm run build
를 실행하여 초기 번들을 생성한 다음 npm run dev
를 실행하여 서버를 시작하고 서버 번들 변경 시 자동 재시작하세요.
SSR 테스트
브라우저에서 http://localhost:3000
으로 이동하세요. "Hello from React SSR!" 및 "This content was rendered on the server!"가 보여야 합니다. 중요하게도 페이지 소스(요소 검사 말고)를 보면 React HTML이 소스에 직접 포함된 것을 확인할 수 있으며, 이는 서버 측 렌더링을 확인합니다. 버튼도 상호 작용이 가능해야 하며, 이는 클라이언트 측 hydration을 보여줍니다.
SSR의 애플리케이션 시나리오
이 기본 프레임워크는 간단하지만 다양한 시나리오에 적용 가능한 핵심 원칙을 보여줍니다.
- 향상된 SEO: 검색 엔진 크롤러는 사전 렌더링된 HTML을 쉽게 구문 분석할 수 있어 더 나은 인덱싱 및 순위를 얻을 수 있습니다.
- 더 빠른 초기 로딩: 사용자는 의미 있는 콘텐츠를 훨씬 더 빨리 보게 되어 인식되는 성능을 향상시키고 이탈률을 줄입니다.
- 더 나은 접근성: 초기 HTML은 즉시 사용할 수 있으므로 인터넷 연결이 느리거나 구형 장치를 사용하는 사용자에게도 유용합니다.
- Open Graph 태그 및 소셜 공유: 소셜 미디어 공유(예: og
, og)를 위한 특정 메타데이터는 특정 페이지 콘텐츠를 기반으로 서버에서 동적으로 렌더링될 수 있습니다.
결론
React 컴포넌트, 서버 렌더링을 위한 ReactDOMServer, 제공을 위한 Express.js, 클라이언트 측 hydration 간의 필수적인 상호 작용을 보여주면서 기초적인 React SSR 프레임워크를 성공적으로 구축했습니다. 이 설정은 정적인 초기 보기를 상호 작용 가능한 애플리케이션으로 변환하여 React 애플리케이션의 성능 및 SEO 개선 방법을 이해하는 기초를 제공합니다. 서버에서 HTML을 렌더링한 다음 클라이언트에서 해당 HTML을 hydration함으로써 사용자 경험을 향상시키고 검색 엔진 알고리즘을 만족시켜 SSR을 현대적이고 고성능 웹 개발의 초석으로 만듭니다.