SODA Engineering Blog
🙆‍♀️

サクッとVercelにNext.jsで作成したサービスをデプロイ[Next.js][Vercel]

2024/08/27に公開

こんにちは。FEチームのMapleです。私たちのチームは、現在のシステムアーキテクチャを見直し、Reactを用いた新しいアーキテクチャへの移行を検討しています。Next.jsを使用してVercelに簡単なサービスのデプロイを行います。

はじめに

  • 今回はReact/Next.jsを使用して簡単なサービスを作成していこうと思います
  • Vercelへのデプロイを簡単に解説出来たらと思います

成果物

https://profile-card-generator.vercel.app/
https://github.com/fuuki12/profile-card-generator/tree/main
HTML埋め込みタグ

Vercelとは?

簡単なデプロイ

  • GitHubやGitLabなどのリポジトリと連携することで、コードをプッシュするだけで自動的にデプロイされます。ブランチごとのプレビューURLも自動生成されるため、開発プロセスがスムーズになります。

高速なパフォーマンス

  • エッジネットワークにより、ユーザーに近い場所からコンテンツを提供できるため、ページロードが高速化されます。

サーバーレス機能

  • APIルートやバックエンドロジックをサーバーレス関数として簡単に構築できます。

Vercelは、開発からデプロイ、パフォーマンスの最適化までを一貫して提供することで、フロントエンド開発者の生産性を大幅に向上させます。Next.jsとの相性が特に良いですが、React、Vue、Svelteなど、さまざまなフレームワークとも互換性があり、幅広いプロジェクトに対応できます。

Next.jsとは?

サーバーサイドレンダリング(SSR)

  • ページがリクエストされるたびにサーバーでレンダリングされるため、初回ロード時のパフォーマンスが向上し、SEO効果も高まります。

静的サイト生成(SSG)

  • ビルド時にあらかじめページが生成されるため、キャッシュによる高速なページ配信が可能です。ブログやポートフォリオサイトなど、静的なコンテンツに最適です。

APIルートのサポート

  • Next.jsにはバックエンド機能をサポートするAPIルートが組み込まれており、サーバーレス関数を使って簡単にAPIエンドポイントを作成できます。

自動的なコード分割

  • 各ページが独立してロードされるため、ユーザーが訪れたページだけをロードし、初期ロード時間を短縮できます。これにより、パフォーマンスが大幅に向上します。

画像の最適化

  • Next.jsには画像の自動最適化機能が組み込まれており、デバイスやネットワークの状況に応じて最適なサイズや形式で画像を配信します。

TypeScriptのサポート

  • TypeScriptとの強力な統合により、型安全なコードを書くことができ、大規模なプロジェクトでもバグを減らすことができます。

Next.jsは、パフォーマンスと開発効率を両立するために設計されており、エンタープライズレベルのウェブアプリケーションから個人ブログまで、さまざまなプロジェクトに対応できます。Reactのエコシステムとの強い統合により、既存のReactアプリケーションを簡単にNext.jsに移行することも可能です。

今回作成した簡単なサービス

  1. GitHub名前を入力してボタンを押下

  2. Copy to Clipboardを押下するとHTML埋め込みがクリップボードに保存されます。

  3. HTMLタグ埋め込みに対応しているブログ等にペーストを行うと以下のような感じでカードが表示されます。
    HTML埋め込みタグ

コード

"use client";

import React, {
  useState,
  useEffect,
  useRef,
  useCallback,
  useMemo,
} from "react";
import { Radar } from "react-chartjs-2";
import html2canvas from "html2canvas";
import { CopyToClipboard } from "react-copy-to-clipboard";
import useGitHubUserData from "../../hooks";
import {
  Chart as ChartJS,
  RadialLinearScale,
  PointElement,
  LineElement,
  Filler,
  Tooltip,
  Legend,
} from "chart.js";
import styles from "./styles";

ChartJS.register(
  RadialLinearScale,
  PointElement,
  LineElement,
  Filler,
  Tooltip,
  Legend
);
const Profile: React.FC = () => {
  const [username, setUsername] = useState("");
  const [embedCode, setEmbedCode] = useState("");
  const { fetchUserData, userData, isLoading, error } = useGitHubUserData();
  const cardRef = useRef<HTMLDivElement | null>(null);

  const radarData = useMemo(
    () => ({
      labels: ["Repos", "Followers", "Following", "Gists"],
      datasets: [
        {
          label: "GitHub Stats",
          data: userData
            ? [
                userData.public_repos,
                userData.followers,
                userData.following,
                userData.public_gists,
              ]
            : [0, 0, 0, 0],
          backgroundColor: "rgba(255, 99, 132, 0.2)",
          borderColor: "rgba(255, 99, 132, 1)",
          borderWidth: 2,
        },
      ],
    }),
    [userData]
  );

  const captureCardAsImage = useCallback(
    async (
      ref: React.RefObject<HTMLDivElement>,
      width: number,
      height: number
    ): Promise<string> => {
      if (ref.current) {
        const canvas = await html2canvas(ref.current, {
          useCORS: true,
          width,
          height,
          scale: 2,
        });
        return canvas.toDataURL("image/png");
      }
      return "";
    },
    []
  );

  const generateEmbedCode = useCallback(async () => {
    const base64Image = await captureCardAsImage(cardRef, 460, 708);
    setEmbedCode(`
      <a href="https://github.com/${username}">
        <div style="width: 460px; padding: 20px; text-align: center; border: 1px solid #eaeaea; border-radius: 10px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);">
          <img src="${base64Image}" alt="GitHub Profile Card" style="width: 100%;" />
        </div>
      </a>
    `);
  }, [captureCardAsImage]);

  useEffect(() => {
    if (userData) {
      setTimeout(generateEmbedCode, 500);
    }
  }, [userData, generateEmbedCode]);

  return (
    <div style={styles.container}>
      <input
        type="text"
        placeholder="GitHub Username"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
        style={styles.input}
      />
      <button
        type="button"
        onClick={() => fetchUserData(username)}
        style={styles.input}
        disabled={isLoading}
      >
        Generate
      </button>

      {isLoading && <p style={styles.loadingText}>Loading...</p>}
      {error && <p style={styles.errorText}>{error}</p>}

      {userData && (
        <>
          <div style={styles.card} ref={cardRef}>
            <img
              src={userData.avatar_url}
              alt={userData.name}
              style={styles.avatar}
            />
            <h2 style={styles.name}>{userData.name}</h2>
            <p style={styles.bio}>{userData.bio}</p>
            <div style={styles.radarChartContainer}>
              <Radar data={radarData} options={{ responsive: true }} />
            </div>
          </div>

          <div style={styles.embedContainer}>
            <h3>Embed Code</h3>
            <textarea readOnly value={embedCode} style={styles.textarea} />
            <CopyToClipboard text={embedCode}>
              <button style={styles.copyButton}>Copy to Clipboard</button>
            </CopyToClipboard>
          </div>
        </>
      )}
    </div>
  );
};
export default Profile;
"use client";

import { useState } from "react";
import { User } from "../types";

const useGitHubUserData = () => {
  const [userData, setUserData] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const fetchUserData = async (userId: string) => {
    setIsLoading(true);
    setError(null);

    try {
      const response = await fetch(`https://api.github.com/users/${userId}`);
      if (!response.ok) {
        throw new Error("Failed to fetch user data");
      }

      const data = await response.json();
      setUserData({
        login: data.login,
        id: data.id,
        node_id: data.node_id,
        avatar_url: data.avatar_url,
        gravatar_id: data.gravatar_id,
        url: data.url,
        html_url: data.html_url,
        followers_url: data.followers_url,
        following_url: data.following_url,
        gists_url: data.gists_url,
        starred_url: data.starred_url,
        subscriptions_url: data.subscriptions_url,
        organizations_url: data.organizations_url,
        repos_url: data.repos_url,
        events_url: data.events_url,
        received_events_url: data.received_events_url,
        type: data.type,
        site_admin: data.site_admin,
        name: data.name,
        company: data.company,
        blog: data.blog,
        location: data.location,
        email: data.email,
        hireable: data.hireable,
        bio: data.bio,
        twitter_username: data.twitter_username,
        public_repos: data.public_repos,
        public_gists: data.public_gists,
        followers: data.followers,
        following: data.following,
        created_at: data.created_at,
        updated_at: data.updated_at,
      });
    } catch (error) {
      setError("不正なレスポンスです");
    }

    setIsLoading(false);
  };

  return { fetchUserData, userData, isLoading, error };
};

export default useGitHubUserData;

データをGitHubApiから取得しています。 特に複雑な処理はしていませんが、アーキテクチャを社内で実装しようとしているアーキテクチャに寄せています。

Vercelへのデプロイ

  • こちらから登録して進めば問題なくデプロイできるかと思います。
  • Vercelへのデプロイは特に意識せずとも実際に登録を行いリポジトリを設定すると特に問題なくデプロイができます。
  • 公式のドキュメントに沿って行けば、特に問題なく導入できるのが大きなメリットですね。

まとめ

  • Next.jsとVercelの親和性は高く何も難しい設定なしでデプロイができます。
  • ありがとうございました。
SODA Engineering Blog
SODA Engineering Blog

Discussion