[번역] 번(Bun)과 리액트로 서버사이드 렌더링(SSR) 구현하기

eunbinn·2023년 9월 24일
31

FrontEnd 번역

목록 보기
24/38
post-thumbnail
post-custom-banner

출처: https://alexkates.dev/server-side-rendering-ssr-with-bun-and-react

많은 분들이 기다려온 자바스크립트의 맥가이버, 번(Bun)이 드디어 1.0 버전으로 출시되며 판도를 바꾸고 있습니다. 처음 들으신 분들을 위해 설명하자면, 번은 빠른 속도를 위해 설계된 올인원 자바스크립트 런타임 및 툴킷입니다. 번들러, 테스트 러너, 네이티브 타입스크립트 및 JSX 지원뿐만 아니라 Node.js 호환 패키지 매니저까지 포함되어 있습니다.

이 가이드에서는 1.0의 잠재력을 최대한 활용하기 위해 번 1.0의 세계로 뛰어들어보려 합니다. 다음 내용을 다룰 예정입니다.

  • 🛠️ 번 설치하기
  • 🌱 첫 번째 번 프로젝트
  • 🖥️ 첫 번째 번 서버 생성하기
  • 🎭 번 스트림과 리액트를 사용한 서버사이드 렌더링
  • 📦 서드파티 데이터 불러오기 및 서버사이드 렌더링하기

이 가이드에서 사용된 모든 코드는 아래 링크에서 확인하실 수 있습니다.
https://github.com/alexkates/ssr-bun-react

프로젝트 설정

번 설치하기

다른 리포지토리를 건드리지 않고도 현재 설치된 노드와 함께 번을 설치할 수 있습니다.

# 번 설치하기
curl -fsSL https://bun.sh/install | bash

번 프로젝트 기본 세팅하기

다음으로 새로운 번 프로젝트를 세팅해 보겠습니다.

# 프로젝트 셋업
mkdir bun-httpserver
cd bun-httpserver
bun init

다음 스크린샷에서 볼 수 있듯이 bun init을 사용하면 프로젝트의 기본 구조를 만들 수 있습니다. yarn, npm 또는 pnpm lock 파일을 대신하는 새로운 bun.lockb 파일이 생긴 것을 확인할 수 있습니다. 또한 index.tstsconfig.json이 기본적으로 생성되므로 추가 설정 없이 타입스크립트를 지원합니다.

image

첫 번째 번 서버 생성하기

믿기 어렵겠지만, 첫 번째 번 서버를 생성하는 것은 매우 간단합니다. 코드 몇 줄만 입력하는 것 만으로 바로 시작할 수 있습니다.

const server = Bun.serve({
  port: 3000,
  fetch(req) {
    return new Response(`Bun!`);
  },
});

console.log(`Listening on http://localhost:${server.port} ...`);

각 줄의 코드를 조금 더 자세히 살펴보겠습니다.

  • const server = Bun.serve({ ... });: 이 코느는 Bun.serve()를 사용해 서버를 초기화하고 3000번 포트에서 수신 대기하도록 설정합니다.
  • port: 3000,: 서버가 포트 3000에서 수신 대기하도록 지정합니다.
  • fetch(req) { ... }: 들어오는 모든 HTTP 요청을 처리하는 함수를 정의합니다. 요청이 들어오면 "Bun!"이라는 텍스트가 포함된 새 HTTP 응답을 반환합니다.
  • return new Response(Bun!);: "Bun!" 텍스트로 새 HTTP 응답 객체를 생성합니다.
  • console.log(Listening on http://localhost:${server.port} ...);: 서버가 수신 대기 중임을 나타내는 메시지를 콘솔에 기록합니다. 템플릿 리터럴을 사용하여 포트 번호를 동적으로 삽입합니다.

이제 전체 프로젝트가 다음 스크린샷과 같이 되었을 것입니다.

image

리액트와 번을 사용하여 서버 사이드 렌더링 구현하기

이제부터가 진짜 재미있는 부분입니다. 리액트와 번으로 서버 사이드 렌더링(SSR)을 구현해 볼 것입니다. 이 섹션에서는 서버 사이드 렌더링(줄여서 SSR이라고도 합니다)의 복잡한 부분을 리액트와 번을 사용해 자세히 살펴보겠습니다.

번 방식으로 패키지 추가하기

yarn에 익숙하다면 여기에서도 편안함을 느낄 수 있을 것입니다. 번에서 패키지를 추가하려면 add 명령을 사용하기만 하면 됩니다. dev 모드에 종속적인 패키지를 추가하고 싶으시다면 -d 플래그를 추가하면 됩니다.

bun add react react-dom
bun add @types/react-dom -d

JSX로 전환하기

다음으로 기존 index.ts 서버 파일을 index.tsx로 전환하겠습니다. 이렇게 하면 JSX 요소를 직접 반환할 수 있습니다.

mv index.ts index.tsx

image

새로운 index.tsx 살펴보기

변경된 index.tsx 파일에서는 react-dom/serverrenderToReadableStream을 사용해 Pokemon 컴포넌트를 렌더링하고 있습니다. 그 후 이 스트림을 Response 객체로 감싸고 콘텐츠 타입을 "text/html"로 설정합니다.

import { renderToReadableStream } from "react-dom/server";
import Pokemon from "./components/Pokemon";

Bun.serve({
  async fetch(request) {
    const stream = await renderToReadableStream(<Pokemon />);

    return new Response(stream, {
      headers: { "Content-Type": "text/html" },
    });
  },
});

console.log("Listening ...");

자, 더 많은 일이 벌어지고 있습니다. 한번 살펴보겠습니다.

  • import { renderToReadableStream } from "react-dom/server";: 리액트 서버 사이드 렌더링을 위해 react-dom/server 패키지에서 renderToReadableStream 함수를 가져옵니다.
  • import Pokemon from "./components/Pokemon";: 상대 경로에서 Pokemon이라는 이름의 리액트 컴포넌트를 가져옵니다.
  • Bun.serve({ ... });: Bun.serve(): 메소드를 사용해서 HTTP 서버를 설정합니다. 여기에는 들어오는 HTTP 요청을 처리하기 위한 비동기 fetch 함수가 포함되어 있습니다.
  • async fetch(request) { ... }: 서버로 들어오는 HTTP 요청에 대해 트리거되는 비동기 함수입니다.
  • const stream = await renderToReadableStream(<Pokemon />);: 비동기적으로 Pokemon 리액트 컴포넌트를 읽을 수 있는 스트림으로 렌더링합니다.
  • return new Response(stream, { ... });: 읽을 수 있는 스트림이 포함된 새 HTTP 응답 객체를 반환하고 "Content-Type" 헤더를 "text/html"로 설정합니다.
  • console.log("Listening ...");: 서버가 들어오는 요청을 수신 대기 중임을 나타내는 메시지를 콘솔에 출력합니다.

스트리밍 가능한 리액트 컴포넌트 만들기

마지막으로 간단한 리액트 컴포넌트를 만들어 보겠습니다. 이 컴포넌트는 서버 사이드 렌더링(SSR)되어 클라이언트로 바로 스트리밍됩니다.

image

import React from "react";

type PokemonProps = {
  name?: string;
};

function Pokemon() {
  return <div>Bun Forrest, Bun!</div>;
}

export default Pokemon;

번 서버 시작하기

다음이 흥미로운 부분입니다. 번 서버를 실행하여 모든 것이 함께 작동하는 것을 확인해 보겠습니다!

bun index.tsx

http://localhost:3000 로 이동하면 SSR Pokemon 컴포넌트가 표시되는 것을 확인할 수 있습니다!

image

포켓몬을 이용한 동적 라우트 만들기

좀 더 어려운 것을 시도해 볼 준비가 되셨나요? 이 섹션에서는 /pokemon/pokemon/[pokemonName] 두 가지 라우트를 만들어 보겠습니다.

  • /pokemon으로 이동하면 Pokemon API에 페치 요청이 발생하여 클릭 가능한 앵커 태그 목록으로 렌더링됩니다.
  • 이러한 앵커 태그 중 하나를 클릭하면 /pokemon/[pokemonName]으로 이동하고 특정 포켓몬을 페칭하여 서버 사이드 렌더링(SSR)한 다음 클라이언트로 다시 스트리밍한 페이지를 보게 됩니다.

개선된 index.tsx 자세히 살펴보기

이 업데이트 버전에서는 index.tsx가 상당한 작업을 하고 있습니다. 이제 동적 라우팅이 포함되어 Pokemon API에서 가져온 포켓몬 리스트를 표시하거나 URL에 따라 특정 포켓몬을 표시할 수 있습니다. 리스트든 개별 포켓몬이든 컴포넌트는 서버 사이드에서 렌더링된 후 클라이언트로 다시 스트리밍됩니다.

import { PokemonResponse } from "./types/PokemonResponse";
import { PokemonsResponse } from "./types/PokemonsResponse";
import { renderToReadableStream } from "react-dom/server";
import Pokemon from "./components/Pokemon";
import PokemonList from "./components/PokemonList";

Bun.serve({
  async fetch(request) {
    const url = new URL(request.url);

    if (url.pathname === "/pokemon") {
      const response = await fetch("https://pokeapi.co/api/v2/pokemon");

      const { results } = (await response.json()) as PokemonsResponse;

      const stream = await renderToReadableStream(
        <PokemonList pokemon={results} />
      );

      return new Response(stream, {
        headers: { "Content-Type": "text/html" },
      });
    }

    const pokemonNameRegex = /^\/pokemon\/([a-zA-Z0-9_-]+)$/;
    const match = url.pathname.match(pokemonNameRegex);

    if (match) {
      const pokemonName = match[1];

      const response = await fetch(
        `https://pokeapi.co/api/v2/pokemon/${pokemonName}`
      );

      if (response.status === 404) {
        return new Response("Not Found", { status: 404 });
      }

      const {
        height,
        name,
        weight,
        sprites: { front_default },
      } = (await response.json()) as PokemonResponse;

      const stream = await renderToReadableStream(
        <Pokemon
          name={name}
          height={height}
          weight={weight}
          img={front_default}
        />
      );

      return new Response(stream, {
        headers: { "Content-Type": "text/html" },
      });
    }

    return new Response("Not Found", { status: 404 });
  },
});

console.log("Listening ...");

image

많은 일이 벌어지고 있습니다. 흥미로운 부분을 더 자세히 살펴보겠습니다.

  • 번으로 HTTP 서버 초기화하기: Bun.serve() 메서드는 HTTP 서버를 세팅하고 들어오는 비동기 fetch 함수로 모든 HTTP 트래픽의 진입점 역할을 수행합니다.
  • 모든 포켓몬의 라우트: URL 경로가 /pokemon인 경우 서버는 외부 API에서 포켓몬 목록을 가져와 PokemonList React 컴포넌트를 HTML로 렌더링합니다. 그런 다음 이 HTML은 클라이언트로 다시 전송됩니다.
  • 특정 포켓몬의 라우트: 이 코드는 정규 표현식을 사용하여 특정 포켓몬의 이름(예: /pokemon/pikachu)을 지정하는 URL 경로를 확인합니다. 이러한 경로가 감지되면 서버는 특정 포켓몬에 대한 세부 정보를 가져와서 Pokemon 리액트 컴포넌트를 사용하여 렌더링합니다.
  • 서버 사이드 리액트 렌더링: 모든 포켓몬 라우트와 특정 포켓몬 라우트 모두에 대해 renderToReadableStream 함수는 리액트 컴포넌트를 읽을 수 있는 스트림으로 변환 후 HTML 응답으로 반환합니다.
  • 에러 핸들링: 이 코드에는 404 오류에 대한 구체적인 처리 방법이 포함되어 있습니다. API에서 포켓몬을 찾을 수 없거나 URL이 예상 경로와 일치하지 않는 경우, 404 상태 코드와 함께 "찾을 수 없음" 메시지가 반환됩니다.

PokemonList 컴포넌트

이 컴포넌트는 포켓몬의 목록을 가져와 클릭 가능한 리스트 아이템으로 변환합니다. 각 리스트 아이템은 클릭 시 사용자를 /pokemon/[name]으로 라우팅하여 개별 포켓몬 세부 정보를 렌더링하는 앵커 태그입니다.

import React from "react";

function PokemonList({
  pokemon,
}: {
  pokemon: { name: string; url: string }[];
}) {
  return (
    <ul>
      {pokemon.map(({ name }) => (
        <li key={name}>
          <a href={`/pokemon/${name}`}>{name}</a>
        </li>
      ))}
    </ul>
  );
}

export default PokemonList;

Pokemon 컴포넌트

Pokemon 컴포넌트는 개별 포켓몬의 키, 몸무게, 이름, 이미지 URL을 가져와서 하나의 포켓몬을 표시하고 싶은대로 나타내는 역할을 담당합니다.

import React from "react";

function Pokemon({
  height,
  weight,
  name,
  img,
}: {
  height: number;
  weight: number;
  name: string;
  img: string;
}) {
  return (
    <div>
      <h1>{name}</h1>
      <img src={img} alt={name} />
      <p>Height: {height}</p>
      <p>Weight: {weight}</p>
    </div>
  );
}

export default Pokemon;

HMR을 사용하여 서버 다시 실행하기

이제 서버를 재시작해야 합니다. 이번에는 핫 모듈 리로딩(HMR)을 위해 --watch 플래그를 추가해 보겠습니다. 좋은 소식은 번이 HMR을 지원하기 때문에 이제 nodemon과는 작별 인사할 수 있다는 것입니다.

bun --watch index.tsx

동적 라우트의 실제 동작

첫 번째 스크린샷은 /pokemon으로 이동했을 때 어떤 일이 일어나는지 보여줍니다. 보시다시피 포켓몬 목록이 표시되며, 각 포켓몬은 클릭 가능한 링크입니다. 이 모든 것은 클릭 가능한 이름을 가져와 표시하는 PokemonList 컴포넌트 덕분입니다.

image

두 번째 스크린샷은 /pokemon/charmander로 이동합니다. 이번에는 Pokemon 컴포넌트가 중심이 되어 파이리(Charmander)의 키, 몸무게, 이미지가 표시됩니다. 물론 모든 것은 서버 사이드에서 렌더링된 결과입니다.

image

여기까지입니다, 여러분!

지금까지 이 글을 따라 코딩을 같이 해왔다면 바로 스스로에게 박수를 보내주세요! 여러분은 방금 아래의 일들을 해냈습니다.

  • 🛠️ 멋진 새 번 프로젝트를 설치하고 초기화했습니다.
  • 🌐 나만의 HTTP 서버를 생성했습니다.
  • 🖼️ 서버 사이드 렌더링(SSR)을 활용하여 간단한 리액트 컴포넌트를 스트리밍했습니다.
  • 🗺️ 데이터를 가져오는 두 개의 라우트와 서로 다른 리액트 컴포넌트를 SSR로 구성했습니다.
post-custom-banner

2개의 댓글

comment-user-thumbnail
2023년 10월 5일

작성하신거 보고 따라해봤는데 재밌네요!

답글 달기
comment-user-thumbnail
2023년 10월 14일

재밌게 잘 봤습니다~

답글 달기