Qiita Advent Calender 2024に参加された皆さんお疲れ様でした.Qiitaのイベントごとに完走してる私ですが,そろそろ疲れてきたこの頃です...私のorganizationも参加者の多くが完走したみたいで...素晴らしいですね.
25記事って結構しんどかったのですが,Engneer Festa 2024の38記事に比べたら少なかったのか...といった感想です
今日は12/1に紹介した以下の作品のフロントエンドとAWSのフロント部ホスティングについて説明します.
本記事で紹介するツールは以下のサイトで公開中です!是非みなさんのAdventCalenderの記事数でクリスマスツリーを成長させ,飾り付けをしましょう!
24記事投稿時点ではこんな感じです.
作成したい作品概要
Qiita のユーザーID を入力すると、Qiita Advent Calendar の記事数を取得し、それに応じてクリスマスツリーの画像を表示するサンプルコードである.
フロントエンド紹介
今回のgithubリポジトリは以下の通りである.
SVGファイルはここに書くには多すぎるので上記のリポジトリから参照してほしい
src以下の説明
next.jsの詳しい説明は本記事では省略しますが,src以下にフロントエンドのソースコードを記載していきます.
コードは以下のような構成になっている.
- UI コンポーネント(Button, Card 等)
- カスタム Loading コンポーネント
- ページ関係(_app.tsx, index.tsx)
- グローバルスタイル(globals.css)
- Next.js の設定ファイル(next.config.mjs)
- ユーティリティ関数(lib/utils.ts)
./src
├── components
│ ├── Loading.tsx
│ └── ui
│ ├── button.tsx
│ └── card.tsx
├── lib
│ └── utils.ts
├── pages
│ ├── _app.tsx
│ └── index.tsx
└── styles
└── globals.css
それぞれのファイルを追いながら、どのような仕組みで動作しているかを見ていく.
1. src/components/ui/button.tsx
class-variance-authority (CVA) を使って、ボタンのバリエーション管理を簡略化している.
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
// CVA とは、Tailwind CSS のクラス名を体系的に管理するためのライブラリである.
// バリエーション(たとえばボタンの色や大きさ)をまとめて定義し、組み合わせて柔軟にクラスを生成できる.
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
// ButtonProps では、標準の button 要素のプロップスと、CVA で定義したバリエーションの型を合成している.
// asChild が true の場合は、button の代わりに Radix UI の Slot を使う.
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
// Button コンポーネント本体.
// cn 関数を使って、buttonVariants から取り出したクラスと外部からのクラスをまとめてマージしている.
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
2. src/components/ui/card.tsx
Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter など、カードレイアウトに適したコンポーネントを提供している.
import * as React2 from "react"
import { cn as cn2 } from "@/lib/utils"
const Card = React2.forwardRef<
HTMLDivElement,
React2.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn2("rounded-xl border bg-card text-card-foreground shadow", className)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React2.forwardRef<
HTMLDivElement,
React2.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn2("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React2.forwardRef<
HTMLDivElement,
React2.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn2("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React2.forwardRef<
HTMLDivElement,
React2.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn2("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React2.forwardRef<
HTMLDivElement,
React2.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn2("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React2.forwardRef<
HTMLDivElement,
React2.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn2("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
3. カスタム Loading コンポーネント
読み込み中にスノーマンのアイコンを出すシンプルなコンポーネント.
import React from 'react'
import { FaSnowman } from 'react-icons/fa'
const Loading = () => {
return (
<div className="flex flex-col items-center">
{/* 大きめのスノーマンアイコンをバウンドさせる */}
<FaSnowman className="text-6xl animate-bounce mb-4" />
<p className="text-xl">読み込み中...</p>
</div>
)
}
export default Loading
4.ユーティリティ関数
clsx と tailwind-merge を組み合わせた cn 関数.
複数のクラスをまとめる際に便利である.
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
pages関係
1. src/pages/_app.tsx
グローバルなレイアウトやスタイルを設定するための Next.js カスタム App コンポーネント.
import '../styles/globals.css'
import type { AppProps } from 'next/app'
import { Kaisei_Decol } from 'next/font/google'
// Google Fonts の Kaisei_Decol をインポートし、クラスを適用している.
const kaiseiDecol = Kaisei_Decol({
weight: ['400'],
subsets: ['latin'],
display: 'swap',
})
function MyApp({ Component, pageProps }: AppProps) {
return (
// グローバルにフォントを反映させるため、ルート要素にフォントのクラスを適用
<div className={kaiseiDecol.className}>
<Component {...pageProps} />
</div>
)
}
export default MyApp
2. src/pages/index.tsx
Qiita Advent Calendar の記事数を取得し、その数に対応したクリスマスツリー画像を表示するメインページである.
import { useState } from 'react'
import Image from 'next/image'
import Loading from '../components/Loading'
import { FaSnowflake } from 'react-icons/fa'
import { Kaisei_Decol as KaiseiDecol } from 'next/font/google'
// 同様に、Kaisei Decol フォントを使う
const kaiseiDecol2 = KaiseiDecol({
weight: ['400'],
subsets: ['latin'],
display: 'swap',
})
const Home = () => {
// count: 記事数, userId: Qiita ID, loading: API コール中かどうか
const [count, setCount] = useState<number | null>(null)
const [userId, setUserId] = useState('')
const [loading, setLoading] = useState(false)
// Qiita API (実際は AWS Lambda 経由) から記事数を取得.
// Part1で作成したLambdaのエンドポイントを入力する
const fetchArticleCount = async () => {
try {
setLoading(true)
const response = await fetch(
`https://yourlambdaendpoint-api.ap-northeast-1.amazonaws.com/QiitaAdventCalenderAPI?user_id=${encodeURIComponent(userId)}`
)
const data = await response.json()
// サンプルでは 24 を固定で代入しているが、本来は data から取得
setCount(24)
} catch (error) {
console.error(error)
setCount(0)
} finally {
setLoading(false)
}
}
// 雪の結晶アイコンをランダム配置で落下させるコンポーネント
const Snowflakes = () => {
const snowflakes = Array.from({ length: 50 })
return (
<>
{snowflakes.map((_, index) => {
const style = {
left: `${Math.random() * 100}vw`,
fontSize: `${Math.random() * 10 + 10}px`,
animationDelay: `${Math.random() * 5}s`,
animationDuration: `${Math.random() * 5 + 5}s`,
}
return (
<FaSnowflake key={index} className="snowflake" style={style} />
)
})}
</>
)
}
return (
<div
// 背景画像やテキストカラーを Tailwind で設定
className={`flex flex-col items-center justify-center min-h-screen bg-christmasGreen text-blue-300 ${kaiseiDecol2.className}`}
style={{
backgroundImage: 'url("/images/snow_background.jpg")',
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
>
<Snowflakes />
<h1 className="text-6xl md:text-8xl font-bold mb-8 text-christmasRed flex items-center z-10">
<FaSnowflake className="mr-4" />
クリスマスツリー
<FaSnowflake className="ml-4" />
</h1>
<div className="mb-8 z-10">
<input
type="text"
value={userId}
onChange={(e) => setUserId(e.target.value)}
placeholder="Qiita IDを入力"
className="p-2 rounded border"
/>
<button
onClick={fetchArticleCount}
className="ml-4 p-2 bg-christmasRed text-white rounded"
>
記事数を取得
</button>
</div>
{loading && <Loading />}
{count !== null && !loading && (
<>
<p className="text-2xl md:text-3xl mb-8 z-10">現在の記事数:{count} 件</p>
<div className="w-full max-w-md md:max-w-lg lg:max-w-xl z-10">
{/* 数値に応じてツリー画像を切り替える. 0 以下は最低 1, 最大 25 でクリップ */}
<Image
src={`/svg/day_${Math.min(Math.max(count, 1), 25)}.svg`}
alt={`Day ${Math.min(Math.max(count, 1), 25)} SVG`}
width={400}
height={500}
/>
</div>
</>
)}
</div>
)
}
export default Home
グローバルスタイル (Tailwind CSS)
Tailwind CSS の基本セットアップとともに、雪の結晶アイコン用のアニメーションを定義.
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
position: relative;
overflow: hidden;
}
.snowflake {
position: absolute;
top: -10px;
color: white;
font-size: 1em;
animation-name: fall;
animation-duration: 10s;
animation-timing-function: linear;
animation-iteration-count: infinite;
opacity: 0.8;
}
@keyframes fall {
0% {
transform: translateY(0) translateX(0);
}
100% {
transform: translateY(110vh) translateX(10vw);
}
}
Next.js の設定ファイル
ページ末尾にスラッシュを付ける設定や、静的エクスポートの設定が書かれている.
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
trailingSlash: true, // パス末尾のスラッシュ
output: 'export', // 静的エクスポート
images: {
unoptimized: true, // 画像の最適化機能を無効
},
};
export default nextConfig;
AWSにフロントエンド部分をデプロイしよう
ここからは作成したフロントエンド部分をAWSホスティングをする.
S3に静的ファイルをアップロード
まずS3→「バケットを作成」 を選択する
作成したバケットを選択し,「アクセス許可」を選択し,以下のパケットポリシーを入力する
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowPublicReadAccess",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "作成したS3バケットのARN/*"
}
]
}
作成したS3バケットのARNはパケットポリシー記述欄の上部にあります.またResourceには最後に/*
を忘れなないようにしてね.
次に,作成したバケットを選択→「プロパティ」→「静的ウェブサイトホスティング」を開き,静的ウェブサイトホスティングを有効にする.また,インデックスドキュメントはindex.html
とする.
フロントエンドのビルドファイルをS3にアップロードする
開発したフロントエンドプロジェクトのルートに移動し,以下のコマンドを実行する.
npm run build
これによってout
ディレクトリが作成されるのでその中のすべてのファイルやディレクトリをS3にアップロードする.
作成したS3バケットを選択し,「アップロード」を選択し,out
ディレクトリ以下のすべてのファイルやディレクトリをアップロードする.
独自ドメインの導入
「お名前.com」にアクセスして,任意のドメインを探す.
その後お名前.comのログインや支払い方法の登録をしてドメイン取得となる.
Route53の設定
Amazon Route 53はAWSが提供する信頼性の高いDNSサービスで,ドメイン名の登録・管理とDNSクエリの処理を行う.
DNSサービスにドメインを登録する意味
DNSサービスにドメインを登録することは,ユーザーが覚えやすい名前でウェブサイトやサービスにアクセスできるようにし,その名前をIPアドレスと結びつけることができるようになる.これにより,インターネット上のどこからでもドメインを利用したアクセスが可能となり,サーバーの移動や設定変更時も柔軟に対応できる.
DNSサービスをRoute53にする意味
AWS Route 53を利用する利点は,AWSの他のサービス(EC2,S3,CloudFrontなど)との統合が容易である.また,高速で信頼性の高いDNS解決,トラフィックの最適な分散,フェイルオーバー機能などを備えており,大規模なトラフィックにも対応可能である.これにより,運用が効率化され,安定したサービス提供が実現する.AWS環境でインフラを構築する場合,Route 53を利用することで一元管理が可能となり,コストパフォーマンスと柔軟性の両方を向上させられるのが大きな利点である.
Route53から「ホストゾーン」→「ホストゾーンの作成」を選択する.
作成したドメインを「ドメイン名」に入力し,「ホストゾーンの作成」を選択する
ホストゾーンが作成できたら,作成したドメインを選択し,レコード欄からNSレコードを探す.
ns-〇〇〇〇.awsdns-〇〇.org
NSレコードとは
そのドメインの権威DNSサーバーを指定するDNSレコードである.
- DNSの階層構造において、特定のドメインやサブドメインの管理を委任
- 問い合わせを受けたDNSサーバーに対して、次に参照すべきDNSサーバーを教える
- 複数のNSレコードを設定することで、冗長性を確保できる
example.com のNSレコード:
ns1.example.com
ns2.example.com
これにより,example.comのDNSクエリは,これらのネームサーバーに転送される.
4種類あるので最後の.(ドット)を除いてメモしておくこと
お名前.comを開き,お名前.com Naviのページのネームサーバー設定のページにアクセスする.
取得したドメインの横のチェックボックスにチェックを入れて「その他のサービス」を選択し,ネームサーバー1,2,3,4に先ほどメモをした4種類のNSレコードを入力する.その後「確認」を押してネームサーバーの登録を行う.
AWS Certificate Manager(ACM)の設定
SSL/TLS証明書を発行する意味
ウェブサイトやサービスの通信を暗号化し,第三者による盗聴や改ざんを防ぐことである.また,証明書はウェブサイトの信頼性を保証し,ユーザーが安心して利用できる環境を提供する.これにより,HTTPS接続が可能となり,セキュリティが強化される.
HTTPS通信とは?
HTTP(Hypertext Transfer Protocol)にSSL/TLS(Secure Sockets Layer / Transport Layer Security)を組み合わせたプロトコルである.ウェブブラウザとウェブサーバー間の通信を暗号化することで,データの盗聴や改ざんを防ぎ,安全な通信を実現する.
HTTPS通信では,サーバーにインストールされたSSL/TLS証明書を利用して,暗号化のための鍵を生成する.通信の開始時に行われるSSL/TLSハンドシェイクで,鍵が安全に共有され,その後の通信内容が暗号化される.この仕組みにより,ユーザーの入力情報やウェブサイトから送信されるデータが保護される.
AWS Certificate Manager(ACM)を使用する理由
証明書の発行,管理,更新が簡単に行える.Route 53と連携することで,DNS認証が自動化され,手動設定の負担が軽減される.さらに,CloudFrontを利用することで,証明書を適用したグローバルなコンテンツ配信が実現できる.これにより,安全でスケーラブルなウェブサイト運用が可能となる.
AWS Certificate Managerを開き,リージョンを「バージニア北部」にする.
「リクエスト」を選択し,「パブリック証明書」を選択.「完全修飾ドメイン名」に取得したドメインを入力する.
「リクエスト」を選択し,証明書の発行を待つ.
結構時間かかる場合もあるので気長に待ちましょう
証明書が発行されたら,発行された証明書を選択し,「Route53 でレコードを作成」を選択.
お名前.comにCNAMEレコードを登録
お名前.com Naviにアクセスし,取得したドメインを選択し「次へ」を選択する.
「DNSレコード設定を利用する」を選択する.
CNAMEを選択し,ホスト名とVALUEを入力する.
ホスト名はACMの発行された証明書のドメインに記載されている.
CNAME名がホスト名,CNAME値がVALUEである.
ACMの証明書検証に使用するCNAMEレコードの詳細
ACMがこのCNAMEレコードの存在を確認し,レコードが正しく設定されていれば、ドメインの所有権が証明される
CNAME名(ホスト)の形式
_○○.ドメイン名
例:
_a79c0231a.example.com
CNAME値(VALUE)の形式
ランダムな文字列.acm-validations.aws
例:
8a7b6c5d4e3f.acm-validations.aws
CloudFrontの設定
CloudFrontを通して配信するメリット
キャッシュによる高速化
CloudFront はグローバルに分散したエッジロケーションでコンテンツをキャッシュする.ユーザーは地理的に近いエッジロケーションからデータを取得できるため,S3 に直接アクセスさせるよりもレスポンスが速くなる.
セキュリティ強化
S3 バケットが直接インターネットに公開されるよりも,CloudFront を経由するほうが安全な構成がとれる.例えば,バケットポリシーで「CloudFront からのアクセスのみ許可」する設定を行うことで,S3 バケットへの直接アクセスをブロックできる.また、CloudFront を通じて HTTPS を簡単に導入でき,AWS Certificate Manager(ACM) で無料の SSL/TLS 証明書を管理できる.,
オリジンは作成したS3バケット
名前は任意の名前
キャッシュポリシーは「Caching Disabled」
ウェブアプリケーションファイアウォール (WAF)は任意の設定
代替ドメイン名 (CNAME)は作成したドメイン名
Custom SSL certificateは作成したACM証明書を選択する
Route53にAレコードを設定
最後にRoute53を開き「ホストゾーン」→自分で作成したレコードを選択→「レコードを作成」を選択
エイリアスをONにしてレコードタイプはAにする.
エンドポイントは「CloudFrontディストリビューションへのエイリアス」
ディストリビューションは先ほど作成したディストリビューションにする.
Aレコードとは
ドメイン名(ホスト名)をIPv4アドレスに紐付けるDNSレコード.
例:
example.com → 192.0.2.1
mail.example.com → 192.0.2.2
これでアクセスできるようになった.取得したドメインに対して以下のようにアクセスできる
https://<取得したドメイン名>
これによってフロントエンドのホスティングが完成.これとPart1のバックエンドを合わせて立派なWebアプリとなる.
それでは25記事目のクリスマスツリーです.完走お疲れ様でした.
みなさんも以下のサイトでぜひクリスマスツリーをみてみましょう!