esbuild 사용해보기(with React SSR)

신석진( Seokjin Shin)·2024년 2월 28일
0

esbuild를 톺아볼건데 이거 핑계로 React SSR과 hydration도 겸사겸사 어떤지 맛만 봐보자

개요

esbuild의 주요기능은 다음과 같다.

  • 엄청 빠른 실행 속도
  • JavaScript, CSS, TypeScript 및 JSX 내장
  • CLI, JS, Go를 위한 간단한 API
  • ESM 및 CommonJS 모듈 번들
  • CSS 모듈을 포함한 CSS 번들
  • 트리 쉐이킹, 축소 및 소스 맵
  • 로컬 서버, 시계 모드 및 플러그인

많은 기능들을 하고 있다. 여기서는 중점적으로 속도와 typescript와 jsx의 빌드, 번들링 기능을 직접 체험해보자.

엄청 빠른 속도는 일단 빌드를 돌려보면 된다. 빌드당 30ms 안쪽이고 사실 코드 베이스가 크지 않아서 더 그렇겠지만 그럼에도 빠르다. 빌드와 번들링을 해주는데 클라이언트 쪽의 hydrate 로직 때문에 클라이언트 쪽 번들 사이즈가 크다. 번들 사이즈가 크다면 minify를 해주고 source map으로 이슈 발생시 검사하면 된다.

esbuild app.jsx --bundle --minify --sourcemap --target=chrome58,firefox57,safari11,edge16

어떤 명령어가 있는지 모를 때 help를 입력하면 친절한 예제까지 보여준다.

Examples:
  # Produces dist/entry_point.js and dist/entry_point.js.map
  esbuild --bundle entry_point.js --outdir=dist --minify --sourcemap

  # Allow JSX syntax in .js files
  esbuild --bundle entry_point.js --outfile=out.js --loader:.js=jsx

  # Substitute the identifier RELEASE for the literal true
  esbuild example.js --outfile=out.js --define:RELEASE=true

  # Provide input via stdin, get output via stdout
  esbuild --minify --loader=ts < input.ts > output.js

  # Automatically rebuild when input files are changed
  esbuild app.ts --bundle --watch

  # Start a local HTTP server for everything in "www"
  esbuild app.ts --bundle --servedir=www --outdir=www/js

이정도만 알아도 실제로 쓰는데 문제가 없을 것 같다. 바로 실전으로 가보자.

예시

코드 컨셉은 React에서 SSR과 hydrate하는 것을 목표로 한다. 시간이 없다면 그냥 예제코드 클론하고 돌려보면 된다.
예제코드: https://github.com/ssj9685/esbuild-example

우선 간단하게 hello world를 보여주는 앱을 만들어준다.

// App.js
import React from "react";

function App() {
  return (
    <html>
      <head></head>
      <body>
        <h1
          onClick={() => {
            console.log("Hello, world!");
          }}
        >
          Hello, world!
        </h1>
      </body>
    </html>
  );
}

export default App;

현재 코드에서 명시적으로는 React가 사용되지 않더라도 빌드한 후에 변수로 쓰이기 때문에 미리 선언해줘야 에러가 나지 않는다.
간단하게 Hello, world!를 보여주는데 hydration을 체험하기 위해 onClick 콜백을 배치시켰다.

이제 우리가 작성한 jsx파일을 html에 맞게 반환해줄 서버가 필요하다.(상남자답게 express 같은건 쓰지 않는다.)

//server.js
import React from "react";
import { renderToPipeableStream } from "react-dom/server";
import App from "./app.jsx";
import { createReadStream } from "fs";
import { createServer } from "http";

const server = createServer(async (req, res) => {
  if (req.url !== "/") {
    try {
      const stream = createReadStream(`.${req.url}`);
      stream.pipe(res);
    } catch (e) {
      res.end();
    }

    return;
  }
  
  const stream = renderToPipeableStream(<App />, {
    bootstrapScripts: ["client.js"],
  });

  res.setHeader("content-type", "text/html");
  stream.pipe(res);
});

server.listen(8080);

간단하게 코드를 설명하자면 http 요청을 받아주는 서버를 만들고 /가 아닌 경로로 들어오면 전부 readableStream으로 만들어서 응답을 보내준다. root경로일 경우 App.js를 text/html 형식의 stream으로 응답해준다. esbuild에서는 server.js파일을 빌드하면 import 경로인 app.jsx를 살피고 형식이 jsx이니 이를 빌드하여 하나로 번들링해준다.

React에서 제공하는 renderToPipeableStream을 사용하면 요청이 왔을 때 서버에서 해당 컴포넌트를 pipe를 통하여 response에 흘려줄 수 있다.

이때 bootstrapScripts로 지정해주면 해당 값으로 스크립트 태그를 자동으로 붙여준다.

이제 hydration을 진행할 client코드가 필요하다.

import React from "react";
import { hydrateRoot } from "react-dom/client";
import App from "./app.jsx";

hydrateRoot(document, <App />);

이렇게만 보면 클라이언트 코드는 매우 간결해보인다. 하지만 내부에서 하는 많은 동작들 때문에 빌드 + 번들을 하고 나면 1.5MB로 꽤 크기가 크다. --minify 옵션을 넣으면 1.5MB -> 200kb 수준으로 만들어준다.

작성한 코드들을 빌드해줄 스크립트를 작성한다.

{
  "scripts": {
    "start": "bun run build:pipe && bun server.js",
    "build:pipe": "bun run build:client && bun run build:server",
    "build:client": "./node_modules/esbuild/bin/esbuild client.jsx --outfile=client.js --bundle --minify --sourcemap",
    "build:server": "./node_modules/esbuild/bin/esbuild server.jsx --outfile=server.js --bundle --minify --sourcemap --platform=node"
  },
  "dependencies": {
    "esbuild": "^0.20.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  }
}

build:server 단계에서 platform을 명시적으로 골라주지 않으면 browser 파일을 기본으로 고르면서 에러가 발생한다.

다 작성한 후에 bun run start를 통해서 빌드와 서버를 동시에 띄운다. 필자는 bun이지만 다른 런타임을 써도 무방하다. 그렇다면 스크립트만 수정하면 된다.

위와 같이 처음에 ssr로 빠르게 첫 응답을 준 후에 client.js를 가져오고 나서 이벤트가 동작하는 것을 확인할 수 있다. 이벤트를 사용할 수 있게 되는 것, 이것이 hydration이다.

결론

사실상 ssr과 hydration 내용이 많지만 실제로 예제를 기반으로 esbuild를 사용하다 보면 번들링을 어떻게 해야하는지 또 어떤 것들을 할 수 있는지 알 수 있는게 많아지는 것 같다. 필자는 이번 예시를 통해 모든 것을 다 제공해주는 환경에서 벗어나 필요한 것들만 집어서 개발을 진행하는 것도 나쁘지 않고 오히려 단순해서 좋았다. 사이드 프로젝트는 나중에 이렇게 진행해볼 생각이다.

0개의 댓글