Blog AWS Indonesia

Membangun server-side rendering untuk aplikasi React di AWS Lambda

Tulisan ini disumbangkan oleh Roman Boiko, Solutions Architect.

React adalah framework front-end populer yang digunakan untuk membuat single page application (SPA). Framework ini di-render dan dijalankan di sisi klien di browser. Namun, untuk alasan SEO atau kinerja, Anda mungkin perlu melakukan rendering beberapa bagian dari aplikasi React di sisi server. Di sinilah server-side rendering (SSR) berguna.

Tulisan ini memperkenalkan konsep dan mendemonstrasikan cara rendering aplikasi React dengan AWS Lambda. Untuk menerapkan solusi ini dan untuk menyediakan resource di AWS, saya menggunakan AWS Cloud Development Kit (CDK). AWS CDK adalah open source framework, yang membantu Anda mengurangi jumlah kode yang diperlukan untuk membuat otomatisasi deployment.

Gambaran Umum

Solusi ini menggunakan Amazon S3, Amazon CloudFront, Amazon API Gateway, AWS Lambda, dan Lambda @ Edge. Kombinasi ini menciptakan implementasi SSR yang sepenuhnya serverless, dan secara otomatis dapan scale sesuai dengan beban kerja. Solusi ini membahas tiga skenario.

Skenario 1 Aplikasi React statis yang di-hosting dalam bucket S3 dengan distribusi CloudFront di depan situs web. Backend dari aplikasi ini berjalan di belakang API Gateway dan diimplementasikan sebagai fungsi Lambda. Di sini, aplikasi sepenuhnya diunduh ke klien dan ditampilkan di browser web. Aplikasi ini kemudian mengirimkan permintaan ke backend.

Skenario 2 Aplikasi React di-render dengan fungsi Lambda. Distribusi CloudFront dikonfigurasi untuk meneruskan permintaan dari path /ssr ke endpoint API Gateway. Aplikasi ini memanggil fungsi Lambda tempat proses rendering terjadi. Saat me-render halaman yang diminta, fungsi Lambda memanggil API backend untuk mengambil data. Ini mengembalikan halaman HTML statis dengan semua data. Halaman ini dapat di-cache di CloudFront untuk mengoptimalkan permintaan selanjutnya.

Skenario 3 Aplikasi React di-render dengan fungsi Lambda@Edge. Skenario ini serupa dengan skenario sebelumnya tetapi rendering terjadi di lokasi edge. Permintaan ke path /edgessr ditangani oleh fungsi Lambda@Edge. Ini mengirimkan permintaan ke backend dan mengembalikan halaman HTML statis.

Demonstrasi

Aplikasi contoh berikut menunjukkan bagaimana skenario sebelumnya diimplementasikan dengan AWS CDK. Solusi ini membutuhkan:

Solusi ini melakukan deployment fungsi Lambda@Edge sehingga harus disediakan di AWS Region AS Timur (Virginia U.).

Untuk memulai, unduh dan konfigurasikan sampel:

Langkah 1 Dari sebuah terminal, clone repositori GitHub berikut:

 git clone https://github.com/aws-samples/react-ssr-lambda

Langkah 2 Berikan nama unik untuk S3 bucket, yang dibuat oleh stack dan digunakan untuk meng-hosting aplikasi React. Ubah placeholder <your bucket name> menjadi nama bucket Anda. Untuk menginstal solusi, jalankan:

cd react-ssr-lambda
cd ./cdk
npm install
npm run build
cdk bootstrap
cdk deploy SSRApiStack --outputs-file ../simple-ssr/src/config.json

cd ../simple-ssr
npm install
npm run build-all
cd ../cdk
cdk deploy SSRAppStack --parameters mySiteBucketName=<your bucket name>

Langkah 3 Catat nilai-nilai berikut dari output:

  • SSRAppStack.CFURL – URL distribusi CloudFront. Akses ke root path / akan mengembalikan aplikasi React yang disimpan di S3.
  • SSRAppStack.LambdaSSRURL – URL distribusi CloudFront /ssr, yang mengembalikan halaman yang di-render oleh fungsi Lambda.
  • SSRAppStack.LambdaEdgeSSRURL – URL distribusi CloudFront /edgessr, yang mengembalikan halaman yang di-render oleh fungsi Lambda@Edge.

Langkah 4 Di browser, buka setiap URL dari langkah 3. Anda melihat halaman yang sama dengan footer berbeda, yang menunjukkan bagaimana halaman tersebut ditampilkan.

Memahami aplikasi React contoh

Aplikasi dibuat oleh tool create-react-app. Anda dapat menjalankan dan menguji aplikasi ini secara lokal dengan menavigasi ke direktori simple-ssr dan menjalankan perintah npm start.

Aplikasi kecil ini terdiri dari dua komponen yang akan me-render daftar produk yang diterima dari backend. File App.js mengirim permintaan, melakukan parsing hasilnya, dan meneruskannya ke komponen React.

import React, { useEffect, useState } from "react";
import ProductList from "./components/ProductList";
import config from "./config.json";
import axios from "axios";

const App = ({ isSSR, ssrData }) => {
  const [err, setErr] = useState(false);
  const [result, setResult] = useState({ loading: true, products: null });
  useEffect(() => {
    const getData = async () => {
      const url = config.SSRApiStack.apiurl;
      try {
        let result = await axios.get(url);
        setResult({ loading: false, products: result.data });
      } catch (error) {
        setErr(error);
      }
    };
    getData();
  }, []);
  if (err) {
    return <div>Error {err}</div>;
  } else {
    return (
      <div>
        <ProductList result={result} />
      </div>
    );
  }
};

export default App;

Menambahkan server-side rendering

Untuk mendukung SSR, saya mengubah aplikasi sebelumnya menggunakan beberapa fungsi Lambda untuk implementasinya. Saat saya mengubah cara data diambil dari backend, saya menghapus kode ini dari App.js. Sebagai gantinya, data diambil di fungsi Lambda dan dimasukkan ke dalam aplikasi selama proses rendering.

File baru SSRApp.js mencerminkan perubahan ini:

import React, { useState } from "react";
import ProductList from "./components/ProductList";

const SSRApp = ({ data }) => {
  const [result, setResult] = useState({ loading: false, products: data });
  return (
    <div>
      <ProductList result={result} />
    </div>
  );
};

export default SSRApp;

Selanjutnya, saya menerapkan logika SSR dalam fungsi Lambda tersebut. Untuk kesederhanaan, saya menggunakan metode renderToString bawaan React, yang mengembalikan string HTML. Anda dapat menemukan file yang sesuai di simple-ssr/src/server/index.js. Fungsi handler mengambil data dari backend, membuat komponen React, dan memasukkannya ke dalam template HTML. Ini mengembalikan respons ke API Gateway, yang merespons klien.

const handler = async function (event) {
  try {
    const url = config.SSRApiStack.apiurl;
    const result = await axios.get(url);
    const app = ReactDOMServer.renderToString(<SSRApp data={result.data} />);
    const html = indexFile.replace(
      '<div id="root"></div>',
      `<div id="root">${app}</div>`
    );
    return {
      statusCode: 200,
      headers: { "Content-Type": "text/html" },
      body: html,
    };
  } catch (error) {
    console.log(`Error ${error.message}`);
    return `Error ${error}`;
  }
};

Untuk me-render kode yang sama di Lambda@Edge, saya mengubah kode agar bisa memproses event CloudFront dan juga mengubah format respons. Fungsi ini mencari path khusus (/edgessr). Semua logika lainnya tetap sama. Anda bisa melihat kode lengkapnya di simple-ssr/src/edge/index.js:

const handler = async function (event) {
  try {
    const request = event.Records[0].cf.request;
    if (request.uri === "/edgessr") {
      const url = config.SSRApiStack.apiurl;
      const result = await axios.get(url);
      const app = ReactDOMServer.renderToString(<SSRApp data={result.data} />);
      const html = indexFile.replace(
        '<div id="root"></div>',
        `<div id="root">${app}</div>`
      );
      return {
        status: "200",
        statusDescription: "OK",
        headers: {
          "cache-control": [
            {
              key: "Cache-Control",
              value: "max-age=100",
            },
          ],
          "content-type": [
            {
              key: "Content-Type",
              value: "text/html",
            },
          ],
        },
        body: html,
      };
    } else {
      return request;
    }
  } catch (error) {
    console.log(`Error ${error.message}`);
    return `Error ${error}`;
  }
};

Tool create-react-app mengonfigurasi tool seperti Babel dan webpack untuk aplikasi React sisi klien. Namun, tool ini tidak dirancang untuk bekerja dengan SSR. Untuk membuat fungsi berfungsi seperti yang diharapkan, saya mentranspilasinya ke dalam format CommonJS sebagai tambahan untuk transpilasi file JSX React. Tool standar untuk tugas seperti ini adalah Babel. Untuk menambahkannya ke proyek ini, saya membuat file konfigurasi .babelrc.json dengan instruksi untuk mentranspilasi fungsi ke dalam format Node.js v12:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "node": 12
        }
      }
    ],
    "@babel/preset-react"
  ]
}

Saya juga menyertakan semua dependensi. Saya menggunakan tool frontend populer yakni webpack, yang juga dapat bekerja dengan fungsi Lambda. Tool ini hanya menambahkan dependensi yang diperlukan dan meminimalkan ukuran paket. Untuk tujuan ini, saya membuat konfigurasi untuk kedua fungsi tersebut. Anda dapat menemukannya di file webpack.edge.js dan webpack.server.js:

const path = require("path");

module.exports = {
  entry: "./src/edge/index.js",

  target: "node",

  externals: [],

  output: {
    path: path.resolve("edge-build"),
    filename: "index.js",
    library: "index",
    libraryTarget: "umd",
  },

  module: {
    rules: [
      {
        test: /\.js$/,
        use: "babel-loader",
      },
      {
        test: /\.css$/,
        use: "css-loader",
      },
    ],
  },
};

Hasil dari menjalankan webpack adalah satu file untuk setiap build. Saya menggunakan file ini untuk mendeploy fungsi Lambda dan Lambda@Edge. Untuk mengotomatiskan proses build, saya menambahkan beberapa skrip ke package.json.

"build-server": "webpack --config webpack.server.js --mode=development",
"build-edge": "webpack --config webpack.edge.js --mode=development",
"build-all": "npm-run-all --parallel build build-server build-edge"

Jalankan proses build dengan perintah npm run build-all.

Melakukan deployment aplikasi

Setelah proses build aplikasi berhasil, saya akan melakukan deployment ke AWS Cloud. Saya menggunakan AWS CDK sebagain pendekatan infrastructure as code. Kode terletak di cdk/lib/ssr-stack.ts.

Pertama, saya membuat bucket S3 untuk menyimpan konten statis dan saya meneruskan nama bucket sebagai parameter. Untuk memastikan hanya CloudFront yang dapat mengakses bucket S3 saya, saya menggunakan konfigurasi origin access identity:

const mySiteBucketName = new CfnParameter(this, "mySiteBucketName", {
      type: "String",
      description: "The name of S3 bucket to upload react application"
    });

const mySiteBucket = new s3.Bucket(this, "ssr-site", {
      bucketName: mySiteBucketName.valueAsString,
      websiteIndexDocument: "index.html",
      websiteErrorDocument: "error.html",
      publicReadAccess: false,
      //only for demo not to use in production
      removalPolicy: cdk.RemovalPolicy.DESTROY
    });

new s3deploy.BucketDeployment(this, "Client-side React app", {
      sources: [s3deploy.Source.asset("../simple-ssr/build/")],
      destinationBucket: mySiteBucket
    });

const originAccessIdentity = new cloudfront.OriginAccessIdentity(
      this,
      "ssr-oia"
    );
    mySiteBucket.grantRead(originAccessIdentity);

Saya men-deploy fungsi Lambda dari direktori build dan mengonfigurasi integrasi dengan API Gateway. Saya juga mencatat nama domain API Gateway untuk digunakan nanti dalam distribusi CloudFront.

const ssrFunction = new lambda.Function(this, "ssrHandler", {
      runtime: lambda.Runtime.NODEJS_12_X,
      code: lambda.Code.fromAsset("../simple-ssr/server-build"),
      memorySize: 128,
      timeout: Duration.seconds(5),
      handler: "index.handler"
    });

const ssrApi = new apigw.LambdaRestApi(this, "ssrEndpoint", {
      handler: ssrFunction
    });

const apiDomainName = `${ssrApi.restApiId}.execute-api.${this.region}.amazonaws.com`;

Saya mengkonfigurasi fungsi Lambda@Edge. Penting untuk membuat versi fungsi secara eksplisit untuk digunakan dengan CloudFront:

const ssrEdgeFunction = new lambda.Function(this, "ssrEdgeHandler", {
      runtime: lambda.Runtime.NODEJS_12_X,
      code: lambda.Code.fromAsset("../simple-ssr/edge-build"),
      memorySize: 128,
      timeout: Duration.seconds(5),
      handler: "index.handler"
    });

const ssrEdgeFunctionVersion = new lambda.Version(
      this,
      "ssrEdgeHandlerVersion",
      { lambda: ssrEdgeFunction }
    );

Terakhir, saya mengonfigurasi distribusi CloudFront untuk berkomunikasi dengan semua sumber:

const distribution = new cloudfront.CloudFrontWebDistribution(
      this,
      "ssr-cdn",
      {
        originConfigs: [
          {
            s3OriginSource: {
              s3BucketSource: mySiteBucket,
              originAccessIdentity: originAccessIdentity
            },
            behaviors: [
              {
                isDefaultBehavior: true,
                lambdaFunctionAssociations: [
                  {
                    eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST,
                    lambdaFunction: ssrEdgeFunctionVersion
                  }
                ]
              }
            ]
          },
          {
            customOriginSource: {
              domainName: apiDomainName,
              originPath: "/prod",
              originProtocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY
            },
            behaviors: [
              {
                pathPattern: "/ssr"
              }
            ]
          }
        ]
      }
    );

Template sekarang siap untuk di-deploy. Pendekatan ini memungkinkan Anda menggunakan kode ini di pipeline Continuous Integration and Continuous Delivery / Deployment (CI / CD) untuk mengotomatiskan deployment aplikasi SSR Anda. Selain itu, Anda dapat membuat konstruksi CDK untuk menggunakan kembali kode ini dalam aplikasi yang berbeda.

Pembersihan

Untuk menghapus semua sumber daya yang digunakan dalam solusi ini, jalankan:

cd react-ssr-lambda/cdk
cdk destroy SSRApiStack
cdk destroy SSRAppStack

Kesimpulan

Tulisan ini mendemonstrasikan dua cara Anda dapat mengimplementasikan dan menerapkan solusi untuk rendering sisi server di aplikasi React, dengan menggunakan Lambda atau Lambda@Edge.

Saya juga menunjukkan cara menggunakan alat sumber terbuka dan AWS CDK untuk mengotomatiskan pembuatan dan penerapan aplikasi semacam itu.

Untuk lebih banyak sumber belajar untuk aplikasi serverless, kunjungi Serverless Land.


Tulisan ini diterjemahkan dari tulisan Building server-side rendering for React in AWS Lambda oleh Roman Boiko, Solutions Architect.

Petra Barus

Petra Barus

Petra Novandi Barus is Developer Advocate at Amazon Web Services based in Jakarta. He is passionate in helping startups and developers in Indonesia to reinvent on behalf their customers. Prior to AWS, Petra co-founded UrbanIndo.com as CTO. The startup became the largest real-estate portal in Indonesia and then was acquired by 99.co. During that time Petra had been a happy AWS customer for 8 years. Petra is also very active in local tech communities