>웹 프론트엔드 >JS 튜토리얼 >스트리밍 및 동적 데이터를 사용한 고급 React SSR 기술

스트리밍 및 동적 데이터를 사용한 고급 React SSR 기술

Susan Sarandon
Susan Sarandon원래의
2025-01-05 03:49:38340검색

Advanced React SSR Techniques with Streaming and Dynamic Data

애플리케이션이 성장할수록 과제도 늘어납니다. 앞서 나가려면 원활한 고성능 사용자 경험을 제공하기 위해 고급 SSR 기술을 익히는 것이 필수적입니다.

이전 기사에서 React 프로젝트에서 서버 측 렌더링을 위한 기반을 구축한 후 프로젝트 확장성을 유지하고, 서버에서 클라이언트로 데이터를 효율적으로 로드하고, 하이드레이션 문제를 해결하는 데 도움이 되는 기능을 공유하게 되어 기쁩니다.

목차

  • SSR 스트리밍이란
  • 지연 로딩과 SSR
  • 지연 로딩으로 스트리밍 구현
    • React 구성 요소 업데이트
    • 스트리밍을 위한 서버 업데이트
  • 서버-클라이언트 데이터
    • 서버에 데이터 전달
    • 클라이언트에서 환경 변수 처리
  • 수분 공급 문제
    • 예시 시나리오
    • 수분 문제 해결
  • 결론

SSR 스트리밍이란?

서버 측 렌더링(SSR) 스트리밍은 전체 페이지가 준비될 때까지 기다리지 않고 서버가 생성되는 HTML 페이지의 일부를 청크로 브라우저에 보내는 기술입니다. 배달하기 전에. 이를 통해 브라우저는 콘텐츠 렌더링을 즉시 시작할 수 있어 로드 시간과 사용자 성능이 향상됩니다.

스트리밍은 다음과 같은 경우에 특히 효과적입니다.

  • 대형 페이지: 전체 HTML을 생성하는 데 상당한 시간이 걸릴 수 있습니다.
  • 동적 콘텐츠: 페이지의 일부가 외부 API 호출이나 동적으로 생성된 청크에 의존하는 경우.
  • 트래픽이 많은 애플리케이션: 사용량이 가장 많은 동안 서버 로드와 대기 시간을 줄입니다.

스트리밍은 기존 SSR과 최신 클라이언트측 상호 작용 간의 격차를 해소하여 사용자가 성능 저하 없이 의미 있는 콘텐츠를 더 빠르게 볼 수 있도록 합니다.

지연 로딩과 SSR

지연 로딩은 구성요소나 모듈이 실제로 필요할 때까지 로딩을 연기하여 초기 로딩 시간을 줄이고 성능을 향상시키는 기술입니다. SSR과 결합하면 지연 로딩이 서버와 클라이언트 워크로드를 모두 크게 최적화할 수 있습니다.

지연 로딩은 구성 요소를 Promise로 동적으로 가져오는 React.lazy를 사용합니다. 기존 SSR에서는 렌더링이 동기식입니다. 즉, 전체 HTML을 생성하여 브라우저에 보내기 전에 서버가 모든 약속을 해결해야 합니다.

스트리밍은 구성 요소가 렌더링될 때 서버가 HTML을 청크로 전송할 수 있도록 하여 이러한 문제를 해결합니다. 이 접근 방식을 사용하면 Suspense 폴백이 즉시 브라우저로 전송되어 사용자가 의미 있는 콘텐츠를 조기에 볼 수 있습니다. 지연 로드된 구성 요소가 해결되면 렌더링된 HTML이 점진적으로 브라우저로 스트리밍되어 대체 콘텐츠가 원활하게 대체됩니다. 이렇게 하면 렌더링 프로세스가 차단되는 것을 방지하고, 지연을 줄이고, 인지된 로드 시간을 개선할 수 있습니다.

지연 로딩으로 스트리밍 구현

이 가이드는 이전 기사인 생산 가능한 SSR React 애플리케이션 구축에서 소개된 개념을 기반으로 하며, 해당 기사는 하단에 링크되어 있습니다. React에서 SSR을 활성화하고 지연 로드 구성 요소를 지원하기 위해 React 구성 요소와 서버 모두에 몇 가지 업데이트를 적용할 예정입니다.

React 구성 요소 업데이트

서버 진입점

React의 renderToString 메서드는 SSR에 일반적으로 사용되지만 전체 HTML 콘텐츠가 준비될 때까지 기다렸다가 브라우저에 보냅니다. renderToPipeableStream으로 전환하면 생성되는 HTML의 일부를 보내는 스트리밍을 활성화할 수 있습니다.

// ./src/entry-server.tsx
import { renderToPipeableStream, RenderToPipeableStreamOptions } from 'react-dom/server'
import App from './App'

export function render(options?: RenderToPipeableStreamOptions) {
  return renderToPipeableStream(<app></app>, options)
}

지연 로드 구성요소 생성

이 예에서는 개념을 보여주기 위해 간단한 카드 구성 요소를 만들어 보겠습니다. 프로덕션 애플리케이션에서 이 기술은 일반적으로 성능을 최적화하기 위해 더 큰 모듈이나 전체 페이지에 사용됩니다.

// ./src/Card.tsx
import { useState } from 'react'

function Card() {
  const [count, setCount] = useState(0)

  return (
    <div classname="card">
      <button onclick="{()"> setCount((count) => count + 1)}>
        count is {count}
      </button>
      <p>
        Edit <code>src/App.tsx</code> and save to test HMR
      </p>
    </div>
  )
}

export default Card

앱에서 지연 로드 구성요소 사용

지연 로드 구성 요소를 사용하려면 React.lazy를 사용하여 동적으로 가져오고 Suspense로 래핑하여 로드 중에 대체 UI를 제공하세요

// ./src/App.tsx
import { lazy, Suspense } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

const Card = lazy(() => import('./Card'))

function App() {
  return (
    
      <div>
        <a href="https://vite.dev" target="_blank">
          <img src="%7BviteLogo%7D" classname="logo" alt="Vite logo">
        </a>
        <a href="https://react.dev" target="_blank">
          <img src="%7BreactLogo%7D" classname="logo react" alt="React logo">
        </a>
      </div>
      <h1>Vite + React</h1>
      <suspense fallback="Loading...">
        <card></card>
      </suspense>
      <p classname="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    >
  )
}

export default App

스트리밍을 위해 서버 업데이트

스트리밍을 활성화하려면 개발 및 프로덕션 설정 모두 일관된 HTML 렌더링 프로세스를 지원해야 합니다. 두 환경 모두 프로세스가 동일하므로 재사용 가능한 단일 기능을 생성하여 스트리밍 콘텐츠를 효과적으로 처리할 수 있습니다.

스트림 콘텐츠 함수 생성

// ./server/constants.ts
export const ABORT_DELAY = 5000

streamContent 함수는 렌더링 프로세스를 시작하고, HTML의 증분 청크를 응답에 쓰고, 적절한 오류 처리를 보장합니다.

// ./server/streamContent.ts
import { Transform } from 'node:stream'
import { Request, Response, NextFunction } from 'express'
import { ABORT_DELAY, HTML_KEY } from './constants'
import type { render } from '../src/entry-server'

export type StreamContentArgs = {
  render: typeof render
  html: string
  req: Request
  res: Response
  next: NextFunction
}

export function streamContent({ render, html, res }: StreamContentArgs) {
  let renderFailed = false

  // Initiates the streaming process by calling the render function
  const { pipe, abort } = render({
    // Handles errors that occur before the shell is ready
    onShellError() {
      res.status(500).set({ 'Content-Type': 'text/html' }).send('<pre class="brush:php;toolbar:false">Something went wrong
') }, // Called when the shell (initial HTML) is ready for streaming onShellReady() { res.status(renderFailed ? 500 : 200).set({ 'Content-Type': 'text/html' }) // Split the HTML into two parts using the placeholder const [htmlStart, htmlEnd] = html.split(HTML_KEY) // Write the starting part of the HTML to the response res.write(htmlStart) // Create a transform stream to handle the chunks of HTML from the renderer const transformStream = new Transform({ transform(chunk, encoding, callback) { // Write each chunk to the response res.write(chunk, encoding) callback() }, }) // When the streaming is finished, write the closing part of the HTML transformStream.on('finish', () => { res.end(htmlEnd) }) // Pipe the render output through the transform stream pipe(transformStream) }, onError(error) { // Logs errors encountered during rendering renderFailed = true console.error((error as Error).stack) }, }) // Abort the rendering process after a delay to avoid hanging requests setTimeout(abort, ABORT_DELAY) }

개발 구성 업데이트 중

// ./server/dev.ts
import { Application } from 'express'
import fs from 'fs'
import path from 'path'
import { StreamContentArgs } from './streamContent'

const HTML_PATH = path.resolve(process.cwd(), 'index.html')
const ENTRY_SERVER_PATH = path.resolve(process.cwd(), 'src/entry-server.tsx')

// Add to args the streamContent callback
export async function setupDev(app: Application, streamContent: (args: StreamContentArgs) => void) {
  const vite = await (
    await import('vite')
  ).createServer({
    root: process.cwd(),
    server: { middlewareMode: true },
    appType: 'custom',
  })

  app.use(vite.middlewares)

  app.get('*', async (req, res, next) => {
    try {
      let html = fs.readFileSync(HTML_PATH, 'utf-8')
      html = await vite.transformIndexHtml(req.originalUrl, html)

      const { render } = await vite.ssrLoadModule(ENTRY_SERVER_PATH)

      // Use the same callback for production and development process
      streamContent({ render, html, req, res, next })
    } catch (e) {
      vite.ssrFixStacktrace(e as Error)
      console.error((e as Error).stack)
      next(e)
    }
  })
}

프로덕션 구성 업데이트 중

// ./server/prod.ts
import { Application } from 'express'
import fs from 'fs'
import path from 'path'
import compression from 'compression'
import sirv from 'sirv'
import { StreamContentArgs } from './streamContent'

const CLIENT_PATH = path.resolve(process.cwd(), 'dist/client')
const HTML_PATH = path.resolve(process.cwd(), 'dist/client/index.html')
const ENTRY_SERVER_PATH = path.resolve(process.cwd(), 'dist/ssr/entry-server.js')

// Add to Args the streamContent callback
export async function setupProd(app: Application, streamContent: (args: StreamContentArgs) => void) {
  app.use(compression())
  app.use(sirv(CLIENT_PATH, { extensions: [] }))

  app.get('*', async (req, res, next) => {
    try {
      const html = fs.readFileSync(HTML_PATH, 'utf-8')

      const { render } = await import(ENTRY_SERVER_PATH)

      // Use the same callback for production and development process
      streamContent({ render, html, req, res, next })
    } catch (e) {
      console.error((e as Error).stack)
      next(e)
    }
  })
}

익스프레스 서버 업데이트

streamContent 함수를 각 구성에 전달합니다.

// ./server/app.ts
import express from 'express'
import { PROD, APP_PORT } from './constants'
import { setupProd } from './prod'
import { setupDev } from './dev'
import { streamContent } from './streamContent'

export async function createServer() {
  const app = express()

  if (PROD) {
    await setupProd(app, streamContent)
  } else {
    await setupDev(app, streamContent)
  }

  app.listen(APP_PORT, () => {
    console.log(`http://localhost:${APP_PORT}`)
  })
}

createServer()

이러한 변경 사항을 구현한 후 서버는 다음을 수행합니다.

  • HTML을 브라우저로 점진적으로 스트리밍하여 첫 번째 페인트 시간을 단축합니다.
  • 지연 로드 구성요소를 원활하게 처리하여 성능과 사용자 경험을 모두 향상시킵니다.

서버-클라이언트 데이터

HTML을 클라이언트에 보내기 전에 서버에서 생성된 HTML을 완전히 제어할 수 있습니다. 이를 통해 필요에 따라 태그, 스타일, 링크 또는 기타 요소를 추가하여 구조를 동적으로 수정할 수 있습니다.

특히 강력한 기술 중 하나는 <script> 태그를 HTML에 추가하세요. 이 접근 방식을 사용하면 동적 데이터를 클라이언트에 직접 전달할 수 있습니다.</script>

이 예에서는 환경 변수를 전달하는 데 중점을 두지만 필요한 JavaScript 개체를 전달할 수도 있습니다. 환경 변수를 클라이언트에 전달하면 해당 변수가 변경될 때 전체 애플리케이션을 다시 빌드하는 것을 방지할 수 있습니다. 하단에 링크된 예시 저장소에서 프로필 데이터가 어떻게 동적으로 전달되는지 확인할 수도 있습니다.

서버에서 데이터 전달

API_URL 정의

서버에 API_URL 환경 변수를 설정하세요. 기본적으로 이는 jsonplaceholder를 가리킵니다. __INITIAL_DATA__는 전역 창 개체에 데이터를 저장하기 위한 키 역할을 합니다.

// ./src/entry-server.tsx
import { renderToPipeableStream, RenderToPipeableStreamOptions } from 'react-dom/server'
import App from './App'

export function render(options?: RenderToPipeableStreamOptions) {
  return renderToPipeableStream(<app></app>, options)
}

HTML에 초기 데이터 삽입

초기 데이터를 클라이언트에 보내기 전에 HTML 문자열에 삽입하는 유틸리티 함수를 만듭니다. 이 데이터에는 API_URL과 같은 환경 변수가 포함됩니다.

// ./src/Card.tsx
import { useState } from 'react'

function Card() {
  const [count, setCount] = useState(0)

  return (
    <div classname="card">
      <button onclick="{()"> setCount((count) => count + 1)}>
        count is {count}
      </button>
      <p>
        Edit <code>src/App.tsx</code> and save to test HMR
      </p>
    </div>
  )
}

export default Card

streamContent 업데이트

applyInitialData 함수를 사용하여 초기 데이터를 HTML에 삽입하고 클라이언트에 보냅니다.

// ./src/App.tsx
import { lazy, Suspense } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

const Card = lazy(() => import('./Card'))

function App() {
  return (
    
      <div>
        <a href="https://vite.dev" target="_blank">
          <img src="%7BviteLogo%7D" classname="logo" alt="Vite logo">
        </a>
        <a href="https://react.dev" target="_blank">
          <img src="%7BreactLogo%7D" classname="logo react" alt="React logo">
        </a>
      </div>
      <h1>Vite + React</h1>
      <suspense fallback="Loading...">
        <card></card>
      </suspense>
      <p classname="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    >
  )
}

export default App

클라이언트에서 환경 변수 처리

전역 창 유형 확장

__INITIAL_DATA__ 키와 해당 구조를 포함하도록 전역 유형 선언을 업데이트하세요.

// ./server/constants.ts
export const ABORT_DELAY = 5000

Window 객체에서 API_URL에 액세스

// ./server/streamContent.ts
import { Transform } from 'node:stream'
import { Request, Response, NextFunction } from 'express'
import { ABORT_DELAY, HTML_KEY } from './constants'
import type { render } from '../src/entry-server'

export type StreamContentArgs = {
  render: typeof render
  html: string
  req: Request
  res: Response
  next: NextFunction
}

export function streamContent({ render, html, res }: StreamContentArgs) {
  let renderFailed = false

  // Initiates the streaming process by calling the render function
  const { pipe, abort } = render({
    // Handles errors that occur before the shell is ready
    onShellError() {
      res.status(500).set({ 'Content-Type': 'text/html' }).send('<pre class="brush:php;toolbar:false">Something went wrong
') }, // Called when the shell (initial HTML) is ready for streaming onShellReady() { res.status(renderFailed ? 500 : 200).set({ 'Content-Type': 'text/html' }) // Split the HTML into two parts using the placeholder const [htmlStart, htmlEnd] = html.split(HTML_KEY) // Write the starting part of the HTML to the response res.write(htmlStart) // Create a transform stream to handle the chunks of HTML from the renderer const transformStream = new Transform({ transform(chunk, encoding, callback) { // Write each chunk to the response res.write(chunk, encoding) callback() }, }) // When the streaming is finished, write the closing part of the HTML transformStream.on('finish', () => { res.end(htmlEnd) }) // Pipe the render output through the transform stream pipe(transformStream) }, onError(error) { // Logs errors encountered during rendering renderFailed = true console.error((error as Error).stack) }, }) // Abort the rendering process after a delay to avoid hanging requests setTimeout(abort, ABORT_DELAY) }

동적 API_URL을 사용하여 요청하기

// ./server/dev.ts
import { Application } from 'express'
import fs from 'fs'
import path from 'path'
import { StreamContentArgs } from './streamContent'

const HTML_PATH = path.resolve(process.cwd(), 'index.html')
const ENTRY_SERVER_PATH = path.resolve(process.cwd(), 'src/entry-server.tsx')

// Add to args the streamContent callback
export async function setupDev(app: Application, streamContent: (args: StreamContentArgs) => void) {
  const vite = await (
    await import('vite')
  ).createServer({
    root: process.cwd(),
    server: { middlewareMode: true },
    appType: 'custom',
  })

  app.use(vite.middlewares)

  app.get('*', async (req, res, next) => {
    try {
      let html = fs.readFileSync(HTML_PATH, 'utf-8')
      html = await vite.transformIndexHtml(req.originalUrl, html)

      const { render } = await vite.ssrLoadModule(ENTRY_SERVER_PATH)

      // Use the same callback for production and development process
      streamContent({ render, html, req, res, next })
    } catch (e) {
      vite.ssrFixStacktrace(e as Error)
      console.error((e as Error).stack)
      next(e)
    }
  })
}

이제 클라이언트측 코드에서 동적 환경 변수를 사용할 수 있으므로 JavaScript 번들을 다시 빌드하지 않고도 서버-클라이언트 데이터를 관리할 수 있습니다. 이 접근 방식은 구성을 단순화하고 앱을 더욱 유연하고 확장 가능하게 만듭니다.

수분 공급 문제

이제 서버에서 클라이언트로 데이터를 전달할 수 있으므로 이 데이터를 구성 요소 내에서 직접 사용하려고 하면 수화 문제가 발생할 수 있습니다. 이러한 오류는 서버에서 렌더링된 HTML이 클라이언트의 초기 React 렌더링과 일치하지 않기 때문에 발생합니다.

예시 시나리오

구성 요소에서 API_URL을 간단한 문자열로 사용하는 것을 고려하세요

// ./src/entry-server.tsx
import { renderToPipeableStream, RenderToPipeableStreamOptions } from 'react-dom/server'
import App from './App'

export function render(options?: RenderToPipeableStreamOptions) {
  return renderToPipeableStream(<app></app>, options)
}

이 경우 서버는 API_URL이 포함된 구성 요소를 빈 문자열로 렌더링하지만 클라이언트에서는 API_URL이 이미 창 개체의 값을 갖고 있습니다. 이러한 불일치로 인해 React는 서버에서 렌더링된 HTML과 클라이언트의 React 트리 간의 차이를 감지하므로 하이드레이션 오류가 발생합니다.

사용자는 콘텐츠 업데이트를 빠르게 볼 수 있지만 React는 콘솔에 수분 공급 경고를 기록합니다. 이 문제를 해결하려면 서버와 클라이언트가 동일한 초기 HTML을 렌더링하거나 API_URL을 서버 진입점에 명시적으로 전달하는지 확인해야 합니다.

수분 공급 문제 해결

오류를 해결하려면 서버 진입점을 통해initialData를 앱 구성 요소에 전달하세요.

streamContent 업데이트

// ./src/Card.tsx
import { useState } from 'react'

function Card() {
  const [count, setCount] = useState(0)

  return (
    <div classname="card">
      <button onclick="{()"> setCount((count) => count + 1)}>
        count is {count}
      </button>
      <p>
        Edit <code>src/App.tsx</code> and save to test HMR
      </p>
    </div>
  )
}

export default Card

렌더링 함수에서 데이터 처리

// ./src/App.tsx
import { lazy, Suspense } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

const Card = lazy(() => import('./Card'))

function App() {
  return (
    
      <div>
        <a href="https://vite.dev" target="_blank">
          <img src="%7BviteLogo%7D" classname="logo" alt="Vite logo">
        </a>
        <a href="https://react.dev" target="_blank">
          <img src="%7BreactLogo%7D" classname="logo react" alt="React logo">
        </a>
      </div>
      <h1>Vite + React</h1>
      <suspense fallback="Loading...">
        <card></card>
      </suspense>
      <p classname="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    >
  )
}

export default App

앱 구성 요소에서 초기 데이터 사용

// ./server/constants.ts
export const ABORT_DELAY = 5000

이제 서버에서 렌더링된 HTML이 클라이언트의 초기 React 렌더링과 일치하여 수화 오류를 제거합니다. React는 서버와 클라이언트 트리를 올바르게 조정하여 원활한 경험을 보장합니다.

API_URL과 같은 동적 데이터의 경우 React Context를 사용하여 서버와 클라이언트 간에 기본값을 관리하고 전달하는 것을 고려해 보세요. 이 접근 방식은 구성 요소 간 공유 데이터 관리를 단순화합니다. 하단에 연결된 저장소에서 구현 예를 찾을 수 있습니다.

결론

이 기사에서는 스트리밍 구현, 서버-클라이언트 데이터 관리 및 수화 문제 해결에 중점을 두고 React의 고급 SSR 기술을 살펴보았습니다. 이러한 방법을 사용하면 애플리케이션의 확장성과 고성능을 보장하고 원활한 사용자 경험을 만들 수 있습니다.

코드 탐색

  • : 반응-ssr-고급-예
  • 템플릿: 반응-ssr-스트리밍-템플릿
  • Vite Extra 템플릿: template-ssr-react-streaming-ts

관련 기사

이것은 React를 사용한 SSR 시리즈의 일부입니다. 더 많은 기사를 기대해주세요!

  • 생산 준비가 완료된 SSR React 애플리케이션 구축
  • 스트리밍 및 동적 데이터를 사용한 고급 React SSR 기술(현재 위치)
  • SSR React 애플리케이션에서 테마 설정(출시 예정)

연결 유지

저는 항상 피드백, 협업 또는 기술 아이디어 논의에 열려 있습니다. 언제든지 연락주세요!

  • 포트폴리오: maxh1t.xyz
  • 이메일: m4xh17@gmail.com

위 내용은 스트리밍 및 동적 데이터를 사용한 고급 React SSR 기술의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.