dog_app of development_log
dog_appの開発記録をここに残します。
参考サイト
環境構築の流れ
流れとしては、次のように開発環境を構築していきます。
- 最初にローカルでNext.jsをインストール
- .gitignoreなどは自分で作る必要はない(Next.jsはでデフォルトで記述済み)
- 次にGit管理するためにリモートリポジトリへプッシュする。
Next.jsをインストール
$ cd ~/workspace/create
$ npx create-next-app@latest --typescript
Next.jsインストール時に聞かれる項目については以下のように回答しました。
-
✔ What is your project named? … dog_app
-
✔ Would you like to use ESLint? … No / Yes
-
✔ Would you like to use Tailwind CSS? … No / Yes
-
✔ Would you like to use
src/directory? … No / Yes
-
✔ Would you like to use App Router? (recommended) … No / Yes
-
✔ Would you like to customize the default import alias (@/*)? … No / Yes
-
pageRouterを使います。
-
TailwindCSSは使いません。
-
Homeディレクトリを@で表現してくれるインポートエイリアス機能は変更したくないのでNoを選択します。
Gitバージョン管理
今までとは逆で、最初にローカルで作ったリポジトリをリモートへプッシュするという手順を取っていきます。
$ cd ~/workspace/create/dog_app
$ git branch (mainブランチは既に生成されています。)
$ git remote add origin git@github.com:******/cat_app.git
$ git branch -M main (マスターブランチを「main」とすることを定義するみたいな感じ)
$ git push -u origin main (ローカルで作ったリポジトリをリモートにプッシュしてGitHubにリモートリポジトリを作る。)
$ npm run dev (localhost:3000でサーバーが起動することを確認します。)
$ git checkout -b dev(開発用のブランチを作成します。)
dog APIについて調査
犬画像を取得するAPIを提供するこちらのサイトを使用させていただきます。
- たぶん無料で使えるっぽいのですが、よく分からないのでいろいろ調べてみました。
- 今回は柴犬の画像だけをランダムに取得するAPIを利用したいと思っています。(完全に好み)
- どうやら、自力で柴犬を画像の数を調べてみたら、19枚あるようです。
https://images.dog.ceo/breeds/${dogName}/${dogName}-${number}.jpg
また、既に同じWebアプリを開発されている方の記事を見つけてしまいました。もろ被ってしまいましたが、こちらの開発記事も参考にさせていただきます。
メインとなるindex.tsxの雛形を作成
メモ
- index.tsxページをメインとしてここに実装していきます。
- 今回はpageRouterを使ったファイルシステムルーティングを採用しています。
- pagesディレクトリ配下にindex.tsxがデフォルトで作成されていますが、こちらの既存のコードごっそり削除して、イチから作っていきます。
- アロー関数でページコンポーネントを表現する時は型注釈(NextPage)を指定してあげる必要があるっぽいです。
- アロー関数にする時は、ページ最下部に
export default Home;
が必要になるみたいです。
ここまでのコミット内容
- メインとなる
index.tsx
の雛形を作成 -
index.tsx
の既存コードを削除してHomeコンポーネントをアロー関数で定義 - 不要な
_document.tsx
を削除 -
Home.module.css
のコードを全て削除して最低限のスタイルだけ定義 -
globals.css
のスタイルも全て削除 - ここまでの開発ログを更新
参考記事
Dog APIを取得する実装
続いて、DogApiによる画像取得の機能を実装します。
柴犬の画像は19枚しかなかった
- このAPIの使い方がよく分からないので、自力で画像枚数を調べたところ、柴犬画像のidは
1~19
までであることがわかった。 - これをもとに1〜19までのidをランダムで生成する関数
const random
を定義しました。 - いったん
console.log
で出力してみます。 - 場所はいったん、Homeページコンポーネントの外に配置しました。
- メソッド
Math.random
は0未満の小数点以下の数値をランダムで生成するJavaScriptの標準メソッド。 - メソッド
Math.floor
は小数点以下の数値を整数に直すJavaScriptの標準メソッドとなります。 - TypeScriptはJavaScriptの上位互換であるため、素のJavaScript構文も使用できます。
- ただし
var
は現在はあまり使われないようなので、const
で定義しました。
const random = Math.floor( Math.random() * 19 ) + 1;
console.log( random );
- 上記のように、最小値が1、最大値が19までのランダムな数値を取得することができました。
- この変数を、dog apiのURLのid部分に式展開して代入すれば良さそうです。
わかった!URLはこれだ!
- やはり上記のやり方は違うっぽい。
- このURLがが正しいようだ。
- https://dog.ceo/api/breed/shiba/images/random/1
- URLの
rondom
は文字通りランダムに取得する。 - 最後の
1
は返してくれるJSON情報の数を表しているようだ。 - 返すのはランダムな1枚だけでよいので、
1
とすれば良さそうだ。 - 一旦、先に実装したランダムな数値を返す
Math.random
ロジックはコメントアウトしておきます。
DogApiによる画像取得
- ボタンを押すとAPIから画像を取得するようにしたい
- まずはbuttonタグにonClick属性を付与し、そこに関数を渡す
- 関数はfetchDogImageとして、APIからURLを取ってくる
- 上記の正しいURLにアクセスすると、APIが叩かれてJSON形式のデータがレスポンスされる。
{
"message":["https:\/\/images.dog.ceo\/breeds\/shiba\/shiba-13.jpg"],
"status":"success"
}
- レスポンスのJSONデータを
result
変数に代入 -
console.log(result.mesasge[0]);
とすることで、URLだけを抽出できた。
ここまでののコミット内容
- 未使用のコンポーネントの
import文
を削除 -
DogApi
による画像URLの取得機能を実装- 関数
fetchDogImage
を定義 - ランダムな数値を取得する
Math.random
ロジックは一旦コメントアウト
- 関数
handleClick関数を定義
-
onClick
属性から渡す関数をfetchDogImage
からhandleClick
に変更します。 - 新たに定義した
handleClick
の中でfetchDogImage
を呼び出すようにします。 - 最初から
handleClick
で定義しても良い気はするけれど、まぁ、これまでに学習した通りにやります。 - たぶん、このようにする理由としては、Clickに対するイベント処理と、画像を取得するというイベント、それぞれの役割を明確に分ける意味合いが強いというと思います。
- 定義する場所は、一旦、ページコンポーネント
Home
の外側に記述しておきます。 - 本来は中の方が良さそうだけれど、一旦、
fetchDogImage
と同じ場所に定義しておきます。必要なら後でリファクタリングします。 -
console.log(result.message[0]);
としていたfetchDogImage
の出力をコメントアウトします。 - コメントアウトした代わりに、
return result.message[0];
として結果を返すだけにして、出力はhandleClick
のほうに記述します。 - 一旦、こんな感じに仕上がりました。
const fetchDogImage = async () => {
const res = await fetch("https://dog.ceo/api/breed/shiba/images/random/1");
const result = await res.json();
// console.log(result.message[0]);
return result.message[0];
};
const handleClick = async () => {
const dogImage = await fetchDogImage();
console.log(dogImage);
};
const Home: NextPage = () => {
return (
<div className={styles.container}>
<h1>今日のHACHI</h1>
<img src="https://images.dog.ceo/breeds/shiba/shiba-1.jpg" alt="shiba image" />
<button onClick={handleClick}>ワンワン !</button>
</div>
);
};
- なお、
async
やawait
といったメソッドはJavaScriptの機能。 - 使い方については、こちらの記事が参考になりました。
ここまでののコミット内容
- 【Add】DogApiによる画像URLの取得機能を実装02
-
handleClick
関数を定義 -
onClick
の渡す関数をfetchDogImage
からhandleClick
に変更 - ここまでの開発記録を更新
-
APIによる画像取得の関数にTypeScriptで型を指定する
-
fetchDogImage
に対して、TypeScriptで型を指定します。 - この実装は、TypeScriptの特長を生かして静的型付けをすることで、保守性・セキュリティ性を高める意味があります。
- まずは
interface SearchDogImage
という関数を定義し、そこにキー
とデータ型
を記述していきます。 - 場所はページコンポーネント関数の外側上に配置します。
- ここで定義して
SearchDogImage
はGenerics(ジェネリックス)
と呼ばれ、複数のデータ型を含んだお手製の関数として利用できます。 -
fetchDogImage
のアロー関数の引数?にPromiseメソッドを記述します。 - そして
<SearchDogImage>
とすることで、その関数で定義されたデータ型のものだけを呼び出せるように制限を設けることができます。 - このように記述することで、コンパイル〜ブラウザ出力となる前にエラーに気づけるようになる、といったメリットが生まれます。
interface SeachDogImage {
message: string;
status: string;
}
const fetchDogImage = async (): Promise<SeachDogImage> => {
const res = await fetch("https://dog.ceo/api/breed/shiba/images/random/1");
const result = await res.json();
return result.message[0];
};
ここまでののコミット内容
- 【Add】DogApiによる画像URLの取得機能を実装03
- 型注釈
interface SearchDogImage
を定義 -
fetchDogImage
関数にPromise
型でジェネリックスSearchDogImage
を指定 - ここまでの開発記録を
development_log.md
に追記
- 型注釈
ボタンクリックの度にAPIで画像を取得 & 出力する実装
- 状態変数を取り扱うためのReact機能
useState
をここで扱います。 -
useState
の使い方については、こちらの記事が大変参考になりました。
- ボタンを押すたびにAPI取得した画像を更新出力する実装します。
- まずはreturn文の
<img src>
タグに状態変数dogImageUrl
を定義します。
<img src={dogImageUrl} alt="shiba image" />
-
React
関数のuseState
を定義します。(これはuseStateを記述すると自動補完されます。) - 記述する場所はページコンポーネント関数の内部です。(ただし、return文の中に直接ロジックを記述するのはNGです。)
-
useState
の引数はいったん空の状態で実装しておきます。(のちに実装するSSRを実現する際にココの第二引数の空配列に変数を記述する予定です。) -
useState
の引数の中身をを一旦、空の状態にしておく際は、ダブルクォーテーション("")
をつけないとエラーになるので注意が必要です。
import { useState } from "react";
// 中略
const [dogImageUrl, setDogImageUrl] = useState("");
- 最後に、ボタンを押した時に状態変化する配列の変数
setDogImageUrl
に対して、取得した画像dogImageUrl
を代入して呼び出すよう、handleClick
関数に記述していきます。
const handleClick = async () => {
const dogImage = await fetchDogImage();
setDogImageUrl(dogImage);
};
エラーが発生
- この実装をしているときにエラーが発生。
- ボタンをクリックすると画像が出力されるはずがエラー表示がでてChromeから怒られてしまいました。
VM406 index.tsx:16 Uncaught (in promise)
ReferenceError: fetchDogImage is not defined
- 理由は先に実装していた関数の記述場所が問題だったようです。
- はじめはページコンポーネント関数
Home
の外側に記述していたのですが、それだとダメっぽいです。 - 画像を取得する
fetchDogImage
と、クリック時の挙動を指示するhandleClick
。 - それぞれの関数を、
Homeコンポーネントの中
に記述してあげることで、無事に画像取得ができました。 - これまで、Chromeのコンソール上でしか、挙動を確認していなかったのが理由なのか、この実装をやるまで気付きませんでした。
- 以下のようにコードの記述場所を修正してことなきを得ました。
const Home: NextPage = () => {
const [dogImageUrl, setDogImageUrl] = useState("");
const fetchDogImage = async (): Promise<SeachDogImage> => {
const res = await fetch("https://dog.ceo/api/breed/shiba/images/random/1");
const result = await res.json();
return result.message[0];
};
const handleClick = async () => {
const dogImage = await fetchDogImage();
// console.log(dogImage);
setDogImageUrl(dogImage);
};
return (
<div className={styles.container}>
<h1>今日のHACHI</h1>
<img src={dogImageUrl} alt="shiba image" />
<button onClick={handleClick}>ワンワン !</button>
</div>
);
};
export default Home;
この状態から、ボタンを押すと、、、
こうなります。
- ひとまず、画像の出力まで成功しました。
- ここでコミット・プルリクエストをしておきます。
デプロイ先のVercelでエラーが発生
Type error: Argument of type 'SeachDogImage' is not assignable to
parameter of type 'SetStateAction<string>'.
型エラー: 'SeachDogImage' 型の引数は、'SetStateAction<string>' 型の
パラメータに割り当てることができません。
この問題については別記事としてまとめました。
SSR(サーバーサイドレンダリング)
を使い、サイトのロード時にもAPIを走らせ画像を出力する
- ここまでに、ボタンクリックを発火タイミングとした画像取得・出力のイベントを実装することができました。
- しかしながら現状、index.tsxページが読み込まれた段階では、ボタンをクリックしていないので、画像は出力されません。
- ページアクセス時に固定の画像を置くこともできますが、今回はページロード・リロード時にもAPIが走るように実装していきます。
- せっかくNext.jsフレームワークを使っているので、特長のひとつでもあるサーバーサイドレンダリング(SSR)機能を用いていきます。
順番としてはこんな感じで行っていきます。
-
SSR
でgetServerSideProps
関数を実装 -
IndexPageProps
と命名したinterface
を実装 -
Home関数コンポーネント
にinitialCatImageUrl
を指定し、リロード時にAPIが走るように実装
SSR
でgetServerSideProps
関数を実装
- まずはNext.jsが提供するメソッド
getServerSideProps
を定義します。 - 場所はHomeページコンポーネントの外側に記述します。今回は、最下部付近に実装しました。
export const getServerSideProps: GetServerSideProps = async () => {};
- なお、
export
をつけないといけない理由はよく分かりません🙇 -
GetServerSideProps
を記述すると、自動的にimport { GetServerSideProps, NextPage } from "next";
が補完されます。 -
GetServerSideProps
はこれだけで一種の型なのだそうです。
interface
でGetServerSideProps
に渡すデータ型を指定する
- 続いて先にもやった通り、SSRにも型付けを行っていきます。命名は
IndexPageProps
とします。 - これもジェネリクスと言える、、、、のだと思います。
-
GetServerSideProps
の後につづけて<IndexPageProps>
と記述することで、ジェネリクスの型が引数みたいに渡され、IndexPageProps
で定義したデータ型だけを受け付けるvalidationみたいなものが出来上がる、、、みたいなニュアンスで覚えておきます。🙇
export const getServerSideProps: GetServerSideProps<
IndexPageProps
> = async () => {
// ここに実行したいイベント処理を記述します。
};
- そして定義したジェネリクス型にはこのように記述し、string型のみを受け取るように指定します。
- データ型のキー命名は
initialDogImageUrl
としました。 - 先に行った
interface SearchDogImage
と同じ要領です。
interface IndexPageProps {
initialDogImageUrl: string;
}
getServerSideProps
関数にイベント処理を記述する
- ここまでできたら、土台が出来上がりみたいな感じです。
- 定義した
getServerSideProps
に対して先と同じように画像を取得(フェッチ)してくる構文を記述します。 - これは先に実装した
handleClick
に記述したやつをコピペでOK。
export const getServerSideProps: GetServerSideProps<IndexPageProps> = async () => {
const dogImage = await fetchDogImage();
- ただし、上記で画像をフェッチしてきただけではブラウザには何も映りません。
- return文を記述する必要があります。
- 書き方には決まりがあり、
props: {};
と記述する必要があるそうです。 - そして、
IndexPageProps
で定義した変数initialDogImageUrl
をここで持ってきて、フェッチ画像dogImage
を代入すればOKです。 - 以下のようになりました。
interface IndexPageProps {
initialDogImageUrl: string;
}
//中略
// Run API even when page loads with SSR
export const getServerSideProps: GetServerSideProps<IndexPageProps> = async () => {
const dogImage = await fetchDogImage();
return {
props: {
initialDogImageUrl: dogImage,
},
};
};
- と、これで完成したかのように見えますが、これだとうまくいきません。
Home関数コンポーネント
にinitialCatImageUrl
を指定し、リロード時にAPIが走るように実装
- 今回は、サイトがレンダリングされたタイミングで、handleClickと同じように画像を出力したいので、SSRで
getServerSideProps
を定義し、それに対応した型IndexPageProps
を定義し、最終的にinitialDogImageUrl
という変数にdogImage
を代入しました。 - これらを最後にどうするかというと、ページ出力元であるページコンポーネント関数
Home
にこれらの関数を渡してあげなければならないのです。 - 修正前と修正後をコードを記載します。
//修正前
const Home: NextPage = () => {
const [dogImageUrl, setDogImageUrl] = useState("");
//中略
};
//修正後
const Home: NextPage<IndexPageProps> = ( {initialDogImageUrl} ) => {
const [dogImageUrl, setDogImageUrl] = useState(initialDogImageUrl);
//中略
};
その他修正〜fetchDogImage
関数をページコンポーネントの外側に配置〜
- なぜか、上記実装では
fetchDogImage
がgetServerSideProps
で読み取ってくれませんでした。 - 結論からいうと、
fetchDogImage
関数を、これまでページコンポーネント関数Home
の内側に記述していたのですが、それが良くなかったようです。
interface IndexPageProps {
initialDogImageUrl: string;
}
+ const fetchDogImage = async (): Promise<string> => {
+ const res = await fetch("https://dog.ceo/api/breed/shiba/images/random/1");
+ const result = await res.json();
+ return result.message[0];
+ };
const Home: NextPage<IndexPageProps> = ( {initialDogImageUrl} ) => {
const [dogImageUrl, setDogImageUrl] = useState(initialDogImageUrl);
- ページコンポーネントの外側に配置を移したら、うまくSSRが実行され、サイトのアクセス・リロードの時にもAPIが走って画像が動的に出力されるようになりました。
- 以上で、Webアプリケーションの実装はおおむね完成しました。
- ここまで実装したメインページ
~/pages/index.tsx
のソースコード全体を掲載します。
// ~/pages/index.tsx
import { Inter } from "next/font/google";
import styles from "@/styles/Home.module.css";
import { GetServerSideProps, NextPage } from "next";
import { useState } from "react";
const inter = Inter({ subsets: ["latin"] });
// interface SearchDogImage {
// message: string;
// status: string;
// }
interface IndexPageProps {
initialDogImageUrl: string;
}
const fetchDogImage = async (): Promise<string> => {
const res = await fetch("https://dog.ceo/api/breed/shiba/images/random/1");
const result = await res.json();
return result.message[0];
};
const Home: NextPage<IndexPageProps> = ( {initialDogImageUrl} ) => {
const [dogImageUrl, setDogImageUrl] = useState(initialDogImageUrl);
const handleClick = async () => {
const dogImage = await fetchDogImage();
setDogImageUrl(dogImage);
};
return (
<div className={styles.container}>
<h1>今日のHACHI</h1>
<img src={dogImageUrl} alt="shiba image" />
<button onClick={handleClick}>ワンワン !</button>
</div>
);
};
// Run API even when page loads with SSR
export const getServerSideProps: GetServerSideProps<IndexPageProps> = async () => {
const dogImage = await fetchDogImage();
return {
props: {
initialDogImageUrl: dogImage,
},
};
};
export default Home;
- 以上で柴犬の画像を出力する個人開発Webアプリ開発の本編は終了となります。
追加機能とリファクタリングとスタイリングを考える
- 続いては、これまで学んだ技術の中から、やってみたいことにチャレンジしていきます。
- 具体的には、
リファクタリング
と、CSS
によるスタイリング
です。 - Reactの自己学習で学んだ
コンポーネント化による保守性の維持
、useCallback
などのパフォーマンス向上の機能が使えるかなど。そしてスタイルにおいては、CSS module
を用いて、もう少し凝った見た目にチャレンジしていきます。
これから挑戦してみたい事をまとめる
追加したい機能やリファクタリング内容、スタイリングのグレードアップなど、やりたい事について一旦、まとめておきます。
-
追加機能
- indexページにアクセスした時に、いい感じのロゴマークを最初に出してみたい。
- 柴犬画像のAPIだけでなく、秋田犬の画像取得するページ
~/pages/akita.tsx
を作成
-
リファクタリング
- ボタンクリックのイベント処理
handleClick
に対してuseCallback
を適用する。 - https://dog.ceo/api/breed/akita/images/random/1
- index.tsxページのreturn文のスタイルをコンポーネントで分けて保存する。
- 柴犬・秋田犬に関するスタイルを
components
ディレクトリにまとめる。
- ボタンクリックのイベント処理
-
スタイリング
- レスポンシブデザインを適用させる。
- 画像取得ボタンのデザインをもう少しリッチにしたい。
- 柴犬ページの背景色、秋田犬ページの背景色をuseEffectで切り替える。
自分の作ったコードに対して、useCallback
を使いパフォーマンス向上のリファクタリング
ができるか?
- コンポーネント外の場所にイベントを処理を書く場合、引数に渡す変数が多くなりがち。
- よって、コンポーネントの内側(return文の直上)にイベント処理のコードを書きたいのですが、、、。
- それだと、ページが再レンダリングされた時、メソッドも再生成されてしまい、パフォーマンスが比較的悪くなるというデメリットがある。
- それを回避したい場合は、
useCallBack
というReactがサポートする機能
を使ってあげる事で、再レンダリング時の無駄なメソッド再生成を防ぐ事が出来る。 -
React
学習をこれまでやってきたなかでuseCallback
について学んできたので、実際に個人開発で使ってみたいと思いました。 - しかし、今回作成している画像をAPIで取得するというWebアプリケーションにおいては、その使い所があるのかは、イマイチ分からないです。
- これは自身のネットワークに対する基礎知識が不足するところであり、恥ずかしい限りだが、いろいろ調べたり、試したりしてみたいと思います。
- 自分の現状のコーディング上、Homeページコンポーネント内部に実装しているイベント処理は
handleClick
です。 - このイベント処理に対して
useCallback
を使って余計な際レンダリングを防ぐリファクタリングができるか試してみます。
- 現在、index.tsxページにアクセすると、もろもろのファイルが読み込まれます。これ自体は普通。
- APIで画像を取得するボタンを押すと、、、
- 画像URLだけが呼び出される感じになっています。
読み取り速度を確認
- useCallbackなし
- useCallbackあり
- うーん、、、。さほど良い影響を与えると思えなくもないですが、よく分かりません。
- 一応こんな感じにコーディングしてみたのですが、これで合っているのか分からない。。。
- useCallbackの第二引数の空配列には何かいれないと意味がないと思っているのですが、どうなんでしょうか。。。
import { useCallback, useState } from "react";
// 中略
const handleClick = useCallback( async () => {
const dogImage = await fetchDogImage();
setDogImageUrl(dogImage);
}, []);
useCallback
いらねぇんじゃね?
- useCallback必要なさそうな気もします。。。
- このAPIを叩くhandleClickでは画像を引っ張ってくるだけですし、、、。
- 現状、ヘッダーやフッターなどは実装していないため、他にレンダリングするコンポーネントもない状況。
- 結論、useCallbackを使うほどの実装はないよなぁ、、、と考えました。
- 一旦、上記コードで先に進めることにします。
追加機能を実装
- 柴犬画像のAPIだけでなく、秋田犬の画像取得するページ
~/pages/akita.tsx
を作成してみます。 -
useEffect
を用いて柴犬ページと秋田犬ページとで、背景色を変えます。 - それぞれのページに遷移するリンクを設置します。
- 画面遷移として、トップページindex.tsxをSHIBAの画像出力ページではなく、サイトトップとしての役割に置き換えます。
- index.tsx => トップページとして、SHIBAページとAKITAページへのリンクを置く
- 追加の遷移先としてshiba.tsxとakita.tsxファイルを作成
- 上記のようにした後に、リファクタリングとして各種コンポーネントやHooksに切り出します。
useEffectで背景色をベージュに変更その他
indexページにuseEffectで背景色を定義
h1タグのタイトルを「SHIBA」に変更
コメントアウトしていたhandleClick 関数を削除
いったん、この内容でコミットしておきます。
Headerコンポーネントを作成しリンクを設置
- Headerと命名するコンポーネントを作成します。中身は各ページへのリンク群です。
- indexページに秋田犬ページへのリンクボタンを設置
LinkコンポーネントはNext.jsの機能であり、Reactではないので注意。
参考になった動画はコチラ
動画07分14秒から
- まずはヘッダーリンクを作成していきます。
- CSSスタイリングでかなり苦戦したが、何とか意図するものはできました。
- 以下がコミットしたコード。
// Header.tsx
import Link from "next/link";
import styles from "@/components/Header.module.css";
const Header = () => {
return (
<header className={styles.header}>
<Link href="/">SHIBA</Link>
<Link href="/akita">AKITA</Link>
</header>
);
}
export { Header }
ここでめちゃめちゃ躓きました💧
-
const Header = () => {}
のように、アロー関数で表現する時は、関数の外、最終行あたりにexport
文を入れないとエラーになるので注意。 -
Link
コンポーネントはimport Link from "next/link";
としないと使えないので注意。
/* components/Header.module.css */
.header {
border-bottom: 1px, sienna;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.header a {
display: inline-block;
color: brown;
padding: 5px 12px;
text-decoration: none;
transition: background-color .50s;
}
.header > a:hover {
background-color: lightblue;
}
-
border-bottom: 1px, sienna;
の箇所はたぶん設置場所を間違えている気がするので、後で直します。 - 今回は.headerクラスを命名したのですが、一口にheaderクラスといっても、その中の子要素としてaタグが使われていたりします。
- 親である
.header
に対して:hover
を適用させようとしても、行全体、すなわち.header
クラスの全体に対してhoverが当たってしまい、変な感じになってしまいました。 - 上記のように、classの親子関係を理解しておかないと、スタイルが当たらなくて沼にハマってしまうので肝に銘じておきます。
import { Header } from "@/components/Header";
// 中略
return (
<div className={styles.container}>
<Header />
<h1>今日のSHIBA</h1>
<img src={dogImageUrl} alt="shiba image" />
<button onClick={handleClick}>ワンワン !</button>
</div>
// 中略
<Header />
の配置場所によってスタイルが当たらないことがあるので注意
pages/akita.tsxファイルを作成
続いて、新たに秋田犬のページを作成します。主に以下のような事を実施します。
- indexページをコピペしてakita.tsxファイルを作成
- 秋田犬の画像を同じ要領で取得できるように、akita.tsxのコードを修正
- akita.tsxファイルのCSSmoduleを作成してスタイルを実装
index.tsx
のコードをそのままakita.tsx
に丸コピし、以下のような箇所を修正していきます。
-
fetchDogImage
の取得するURLを変更 https://dog.ceo/api/breed/akita/images/random/1
-
h1
のテキストを今日のAKITA
に変更 -
useEffect
で実装した背景色をlightblue
に変更 - ページコンポーネントの命名を
Home
からAkita
に変更 - CSSモジュールのインポート文を
Akita.module.css
に変更
もっと関数の命名とかを変更しなきゃいけないのかと思っていたのですが、上記の微修正のみでほぼ希望の挙動になってくれて、ホッとしました😌
- なお、CSSは
Akita.module.css
を作成し、中身はHome.module.css
をそのまま丸コピしていけました。 - また、スタイルのインポート文は
import styles from "@/styles/Akita.module.css";
に変更しました。 - これでコミットしておきます。
ヘッダーのホバー時の色がAkitaページでは背景色とかぶってしまう
- ヘッダーのホバー時の色がAkitaページでは背景色とかぶってしまうので、ここは変えないといけないですね。
- 背景色に応じて、ホバーの色をlightblueとbeigeを逆転させるような実装がしたいなぁとは思ったのですが、良いアイデアが浮かびません。
- 別のCSSmoduleを作成してSHIBAページとAKITAページでそれぞれのCSSをインポートする、くらいの方法しか思いつきません。それで良いのだろうか、、、。
- いや、ちょっと面倒なので、
hover
の色を変えちゃいます。 - ここは別途コミットしておきました。
次にやることを考察
次にやりたいこと。やるべき事を考察します。
-
Googleフォント
が読み込まれていないので修正する -
index.tsx
をshiba.tsx
に変更する。 -
Home.module.css
をShiba.module.css
に変更する - 別途
index.tsx
ページを用意する。 - 新たに作成した
index.tsx
ページにちょっとしたいい感じのデザインを実装する。 - Headerのナビゲーションリンクを分割配列?分割代入?して、mapメソッドで回す。
-
HeadLine.tsx
コンポーネントを作成し、見出し「今日のSHIBA」と「今日のAKITA」を出し分ける。 - カスタムフックを使って
BgColor
の出し分けるロジックをコンポーネント化する。 - カスタムフックを使って
SHIBAページ
とAKITAページ
両方で使っているロジック群をまとめる。
リファクタリングでの参考になる講座
動画17分49秒から 👆
- リファクタリング後の完成形が確認できます。
- 綺麗なコード設計が確認できてとても参考になりました。
動画07分09秒から 👆
- ページのHeadlineタイトル(見出し)を動的に出し分けるためのコンポーネント化
- そしてpropsの技術がわかりやすく解説されています。
動画10分05秒から 👆
- カスタムフックを使って関数群をまとめたりする技術がわかりやすく解説されています。
- またコンポーネントにするか、カスタムフックにするかの使い所の違いについても解説されています。
- Hooksにはルールがあります。
- 必ずページコンポーネント関数のreturn文の前(トップレベル)で呼び出してください。
- React関数以外では呼び出さないでください。(素のJavaScriptの関数で呼び出してはいけない)
- 関数の命名は必ず
use
から始めなければなりません。 - 逆に、JavaScriptで
use
と使ってしまうと、Reactなのか区別がつきにくくなってしまうので、JavaScript関数の命名ではuse
は使ってはいけません。
リファクタリングを開始
上記でまとめたリファクタリング内容を元に、実装を開始していきます。
indexページ
をshibaページ
に変更
TOPページを別に作りたいので、現在のindexページをshibaページとし、それに応じたルーティングやリンク遷移先を変更していきます。
-
index.tsx
=>shiba.tsx
-
Home.module.css
=>Shiba.module.css
// shiba.tsx
import styles from "@/styles/Shiba.module.css";
// 中略
const Shiba: NextPage<IndexPageProps> =
// 中略
export default Shiba;
// Header.tsx
<Link href="/shiba">SHIBA</Link>
Googleフォント
が読み込まれていないので修正する
-
Googleフォント
が読み込まれていないので修正します。 - 参考になった公式ドキュメントや外部サイト記事をここに掲載しておきます。
- 全てのページにフォントを適用させたいので、親である
pages/_app.tsx
にgoogleフォントを読み込む定義を実装します。 - ポイントとしては、_app.tsxに定義すること
- 可変フォントというのが推奨されているということ。
- 公式ドキュメントの通りに実装しておけばとりあえずOK。
// pages/_app.tsx
import "@/styles/globals.css";
import type { AppProps } from "next/app";
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export default function App({ Component, pageProps }: AppProps) {
return (
<main className={inter.className}>
<Component {...pageProps} />
</main>
)
}
追記
- SPAの場合に、各tsxファイルでGoogleフォントを呼び出す文は、おそらく不要です。
-
_app.tsx
で全ページに適用するように指定しているため。 - そのため、
shiba.tsx
とakita.tsx
にて定義していたconst inter
文を削除しました。
新たにindex.tsx
ページを用意する
- 以前のindexページはshiba.tsxに変更してルーティングも別に用意したため、ルートページが無い状態です。
- なので改めてindexページを作ります。
- 何かいい感じのデザインを当てたいのですが、それはまた後でやるとして。
- ひとまずは箱だけ用意する感じにします。
$ touch index.tsx
$ touch Home.module.css
- フリー素材のイラストをDLして、
public
ディレクトリ配下に保存します。 - ひとまずこんな感じで実装できました。
import styles from "@/styles/Home.module.css";
import { Header } from "@/components/Header";
import Image from "next/image"
const Home = () => {
return (
<div className={styles.container}>
<Header />
<h1>今日のDOG</h1>
<Image
src="/dog.png"
alt="dog image"
width={300}
height={300}
priority
/>
</div>
);
};
export default Home;
- これもたぶん、コンポーネント化してそこから呼び出すみたいな感じにするのが良さそうです。
- 後でやるとして、ひとまずはこれだけにしておきます。
- 素の
img
プロパティを使うより、next/image
を使用する事が推奨となっているようです。 - こちらも
Next.js公式ドキュメント
に実装の仕方が解説されていました。
他にリファクタリングできる項目をピックアップ
そのほかにリファクタリングできそうな要素をピックアップしていきます。
コンポーネント関連のリファクタリング
-
<h1>
タグのタイトルをコンポーネント<Headline />
としてまとめます。 -
<img>
タグ、<button>
タグの塊を<Main />
コンポーネントとしてまとめます。 - 最終的にコンポーネントは
Header
、Headline
、Main
の3つにまとめることができそうです。 - さらに、Header.module.cssとHeader.tsxファイルをまとめて
Header
ディレクトリを生成してその中に移します。 - なのでルートディレクトリからだと
~/components/Header/Header.module.css
と~/components/Header/index.tsx
というディレクトリ構造にすることで、コンポーネントディレクトリをさらに綺麗にまとめることができそうです。 - また、
Header.tsx
というファイル名ではなく、index.tsx
と命名を共通化しても多分大丈夫だと思うので、コンポーネント化ファイルはindex.tsx
で共通化します。
カスタムフックとしてまとめるリファクタリング
-
useEffect
で実装した背景色を変えるロジックをカスタムフック専用のhooks
ディレクトリを作り、その配下にまとめます。 - 背景色ということなので命名は
BgColor
という名前で良いでしょう。 -
handleClick
も複数のページで作られているロジックなのでカスタムフックにしたいですが、ページコンポーネント関数の外側に配置しているfetchDogImage
関数も関わっているものなので、できるかどうかはまだ分かりませんが、一応検討しておきます。
デザイン関連の改善
- ボタンのデザインがちょっと簡素すぎるので、もう少し凝ったデザインにします。
- トップページに遷移してきた時、何かいい感じのアクションをつけたいと思っています。(これは余裕があったらやります。)
残りはざっとこんな感じになります。
早速やっていきます。
Headerコンポーネントのリファクタリング
以下、ディレクトリのまとめ、およびコードン軽微な加筆・修正を行いました。
-
Header
ディレクトリを作成し、配下にHeader関連2ファイルを移行 -
Header.tsx
をindex.tsx
に命名を変更 - ヘッダーのリンク先に
HOME
を追加 -
Header
リファクタリングの記録をmdファイルに追記
見出しをHeadline
と名付けてコンポーネント化
componentsディレクトリ配下は、こんな感じの構造にしようと思います。
- components
- Headline
- Headline.module.css
- index.tsx
props引数に入れて、出し分け出来るようにしようと思います。
// components/Headline/index.tsx
import styles from "@/components/Headline/Headline.module.css";
const Headline = (props) => {
console.log(props);
return (
<div>
<h1 className={styles.title}>
{props.title}
</h1>
</div>
);
}
export { Headline }
上記では、このようなエラーが出ました。
Parameter 'props' implicitly has an 'any' type. TS7006
Reactの構文を.tsxファイル、すなわち TypeScriptで書こうとしていることに起因していると思われる?ようです。
props
に対して型を指定してあげることでエラーを解消させることができました。
以下のように修正しました。👇👇
import styles from "@/components/Headline/Headline.module.css";
type Props = {
title: string
}
const Headline = (props: Props) => {
return (
<div>
<h1 className={styles.title}>
{props.title}
</h1>
</div>
);
}
export { Headline }
参考サイト
コンポーネントを親で呼び出す時はこんな感じで良いと思います。
// index.tsx
<Headline title="今日のDOG" />
// shiba.tsx
<Headline title="今日のSHIBA" />
// akita.tsx
<Headline title="今日のAKITA" />
Headline周りのCSS Module
を修正
- HeadlineのCSSがうまく聞いていなかったので修正
- また、Headlineに共通化したCSSスタイルの記述
.container h1 {}
を、Home.module.cssなどの親CSSファイルから削除しました。
/* Headline.module.cssを修正 */
- .title h1 {
+ .title {
margin-bottom: 15px;
}
/* Home.module.cssからh1のスタイルを削除 */
/* Shiba.module.cssからh1のスタイルを削除 */
/* Akita.module.cssからh1のスタイルを削除 */
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
}
- .container h1 {
- margin-bottom: 15px;
- }
.container button {
margin-top: 20px;
}
- これでHeadlineコンポーネントのスタイリング共通化がうまくできたと思います。
画像出力ボタンのスタイルをまともにする
- 現状、ボタンをスタイルがダサいので、まともな感じにしたいです。
- 追加でCSS moduleでボタンに装飾を施します。
/* Shiba.module.css */
/* Akita.module.css */
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
}
.container img {
width: 325px;
height: auto;
}
.container button {
margin-top: 20px;
font-size: 1.2rem;
color: #333;
font-weight: bold;
background-color: lightsalmon;
border-radius: 5px;
box-shadow: 5px 5px 0 #bbb;
transition: box-shadow .50s;
}
.container button:hover {
box-shadow: 0 0 0
}
buttonプロパティをコンポーネント化
-
onClick
によるイベント関数が入っているbuttonプロパティをコンポーネント化する時は、一筋縄ではいかないようです。
- いったん、このボタンのコンポーネント化は保留とします。
imgプロパティからImageコンポーネントに変更
- 現状、
img
プロパティを使用していますが、これだとNext.js的には推奨していないようです。 -
<Image />
という感じで、next/image
を使用したいと思います。 - ですが、このイメージ画像はAPIで取得する動的な画像で、関数が定義されています。
- 動的に変わる画像に対して、
next/image
は使えるのでしょうか? - とりあえず試してみます。
- 普通に実装すると怒られてしまいました,,,,
Unhandled Runtime Error
Error: Invalid src prop (https://images.dog.ceo/breeds/shiba/mamehiko03.jpg) on `next/image`,
hostname "images.dog.ceo" is not configured under images in your `next.config.js`
See more info: https://nextjs.org/docs/messages/next-image-unconfigured-host
未処理のランタイムエラー
エラー: `next/image` の無効な src prop (https://images.dog.ceo/breeds/shiba/mamehiko03.jpg)、
ホスト名「images.dog.ceo」が `next.config のイメージの下に設定されていません .js`
詳細については、https://nextjs.org/docs/messages/next-image-unconfigured-host を
参照してください。
- こちらの公式ドキュメントがヒントになりそうです。
このエラーが発生した理由
コンポーネントを利用するページの 1 つがnext/image、srcURL 内で定義されていないホスト名を使用する値をimages.remotePatterns渡しましたnext.config.js。
-
next.config.js
に、ホストするサイト(ここではDogApi)の情報を追記してあげると良いっぽい?と思われるのでやってみます。
// next.config.mjs
// Next.js version 14.1.0
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.dog.ceo',
port: '',
pathname: '/breeds/**',
},
],
},
};
export default nextConfig;
// shiba.tsx
// akita.tsx
return (
<div className={styles.container}>
<Image
src={dogImageUrl}
alt="shiba image"
width={300}
height={300}
priority
/>
</div>
);
- いろいろ試行錯誤しましたが、上記コードに修正したら出来ました。
- Imageコンポーネントでは、widthとheightを指定しないとエラーになってしまうので指定しましたが、
- 画面に出力される段階では、CSS modulesのスタイルに定義した幅が適用されていましたね。
- とりあえず、Imageコンポーネントの実装はひとまずこれで完成とします。
- リファクタリングが必要かはまた後で考えます。
参考サイト
各ロジックをカスタムフックにまとめるリファクタリングを開始
- まずはHomeディレクトリに
hooks
ディレクトリを作成 - その配下にカスタムフックのファイルを作っていきます。
- 背景色を変えるロジックを
useBgBeige
やuseBgLightblue
というカスタムフックにまとめました。
// ~/hooks/useBgBeige.tsx
import { useEffect } from "react";
export const useBgBeige = () => {
useEffect(() => {
document.body.style.backgroundColor = "beige";
return () => {
document.body.style.backgroundColor = "";
}
}, []);
};
// ~/hooks/useBgLightblue.tsx
import { useEffect } from "react";
export const useBgBeige = () => {
useEffect(() => {
document.body.style.backgroundColor = "beige";
return () => {
document.body.style.backgroundColor = "";
}
}, []);
};
- これに伴ない、親コンポーネントにて、importします。
// ~/hooks/useBgBeige.tsx
import { useBgBeige } from "@/hooks/useBgLightblue";
useBgBeige();
// ~/hooks/useBgLightblue.tsx
import { useBgLightblue } from "@/hooks/useBgLightblue";
useBgLightblue();
その他のカスタムフック化は断念
- その他、肝心な画像出力に関するロジックをカスタムフックとしてまとめようとしましたが、うまくいかず。
- 結果的に今回は断念し、見送ることにします。
- 良くわからないが、親コンポーネントの外側、内側にあるロジックをまとめるのはダメ?
- 良くわからないが、SSRはカスタムフックにまとめることはできない、、、かも???
あまり時間をかけてもいられないので、今回は深追いせず、カスタムフック化のリファクタリングは、ここで終わりとします。
HOMEページ遷移のタイミングにローディングデザインを実装(JavaScript)
検討中...
2024/03/16
ここまで1週間程度、JQueryでのフェードアウトの実装や、その他、Next.jsでよく使われているライブラリなどを、諸々学んでいました。
学んだことをこちらのWebアプリケーションに活かして、ルートページにきた時にロゴ画像が表示され、一定時間でフェードアウトし、そこからルートページの内容がフェードインするという表現を実装してみたいと思います。
要件定義をするとざっくり以下のようになりました
- 最初に、ロゴが下から上にアニメーションする👉CSS Modules
- ロゴが3秒くらい表示される👉ReactでPromiseを使う 3000ms待機
- そこからロゴが徐々にフェードアウトする👉fadeoutはどうやる?
- 最後にindexページがフェードインしてくる👉fadeinはどうやる?
最初に、ロゴが下から上にアニメーションする実装
- せっかくReactで作っているので、コンポーネントとして実装してみます
-
components
ディレクトリ配下にAnimation
ディレクトリを作ります - その配下に
index.tsx
とAnimation.module.css
を作ります - ここに、見た目のデザインとロゴが
fadeUp
する動きを実装してみます - また、使用したいロゴイメージを
public
ディレクトリ配下に保存しました
コンポーネントの命名でAnimation
としたNext.jsから怒られてしまいました。たぶん、標準のメソッドで使われていたりするから、この命名は使えないのでしょうかね、、、?
よくわからないですが、一旦、命名をLoading
に変更しました。
// components/Loading/index.tsx
import Image from "next/image"
import styles from "@/components/Loading/Loading.module.css";
const Loading = () => {
return (
<div className={styles.loading}>
<div className={styles.loading_logo}>
<div className={styles.fadeUp}>
<Image
src="/logo.png"
alt="logo image"
width={200}
height={200}
priority
/>
</div>
</div>
</div>
);
}
export { Loading }
/* components/Loading/Loading.module.css */
@charset "utf-8";
/* ========= LoadingのためのCSS =============== */
/* Loading背景画面設定 */
.loading {
/*fixedで全面に固定*/
position: fixed;
margin-top: -8px;
margin-left: -8px;
width: 100%;
height: 100%;
z-index: 999;
background:#ccc;
text-align:center;
}
/* Loading画像中央配置 */
.loading_logo {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* Loading アイコンの大きさ設定 */
.loading_logo img {
width: 250px;
height: 250px;
}
/* fadeUpをするアイコンの動き */
.fadeUp{
animation-name: fadeUpAnime;
animation-duration: 1.0s;
animation-fill-mode:forwards;
opacity: 0;
}
@keyframes fadeUpAnime{
from {
opacity: 0;
transform: translateY(100px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
// pages/index.tsx
import styles from "@/styles/Home.module.css";
import { Header } from "@/components/Header";
import { Headline } from "@/components/Headline";
import Image from "next/image"
import { Loading } from "@/components/Loading";
const Home = () => {
return (
<div>
<div className={styles.loading}>
<Loading />
</div>
<div className={styles.container}>
<Header />
<Headline title="今日のDOG" />
<Image
src="/dog.png"
alt="dog image"
width={300}
height={300}
priority
/>
</div>
</div>
);
};
export default Home;
- ここまで、一旦コミットします
- ひとまず、動きながら出現するロゴ画像とグレイの背景を表示させることに成功しました
ロゴが3秒くらい表示された後フェードアウトする
もう一度要件を確認します。
- ロゴが3秒間表示される(3000ms待機)
- 3秒後まずはロゴがフェードアウトする
- 1秒遅れ、(合計4秒)で背景がフェードアウトする
- フェードアウトした結果、indexページのレイアウトがフワッと表示される(はず)
もう少し分解して考えていきます。
-
pages/index.tsx
のホームコンポーネントconst Home = () => {}
のreturn
には、現在、<div className={styles.container}>
が呼び出されている - この
{styles.container}
が呼ばれる前に、<Animation />
コンポーネントが呼ばれるようにすればよさそう、、? - なお、最後にindexページがフェードインしてくる に関しては、これ、実際は、前の画面の背景がフェードアウトすることによって、結果的に、indexページが徐々に現れるフェードインみたいな表現になる、、、ということだと思う
- つまり、ロゴ画像の背景をフェードアウトさせればよいということだと思う
- なので、フェードインするという実装は考えなくて良いはずだ
3秒待機するというメソッドをJQuery無しでどうやって表現する?
- まずは3秒待機するというメソッドをJQuery無しでどうやって表現する?
- そっか、フェードアウト自体は、CSSでも出来るんだな
- なので、待機するメソッドさえクリアできれば、Framer Motionなしでも実現できそう?
- いや、、、まだわからない
使えそうなメソッドについていろいろ調べてみました。
タイマー関数
setTimeout(() => {
console.log("こんにちは!3秒経ちました");
}, 3000);
ライブラリ「Framer Motion」
$ npm install framer-motion
import { motion } from "framer-motion"
opacity: 1
は不透明
opacity: 0
は透明
display:none
は、CSSのプロパティの一つで、指定すると要素を完全に非表示にできる
transition-property
変化対象のCSSプロパティを指定
transition-duration
変化の時間
transition-timing-function
変化の速度
transition-delay
変化開始までの時間
ひとまず完成
- いろいろ沼にハマりましたが、なんとか期待する挙動にできました
- はじめにくるトップページに遷移したとき、ロゴ画像がフェードアップしながら現れ、3.5秒後にフェードアウト
- そして背景が再背面に移動し、トップページのメインコンテンツが現れる
- 完璧とは言えませんが、今回はこれにて終了とします
// ~/components/Loading/index.tsx
import Image from "next/image"
import styles from "@/components/Loading/Loading.module.css";
const Loading = () => {
return (
<div className={styles.loading}>
<div className={styles.loading_logo}>
<div className={styles.fadeUp}>
<div className={styles.fadeOut}>
<Image
src="/logo.png"
alt="logo image"
width={200}
height={200}
priority
/>
</div>
</div>
</div>
</div>
);
}
export { Loading }
/* ~/components/Loading/Loading.module.css */
@charset "utf-8";
/* ========= LoadingのためのCSS =============== */
/* Loading背景画面設定 */
.loading {
/*fixedで全面に固定*/
position: fixed;
margin-top: -8px;
margin-left: -8px;
width: 100%;
height: 100%;
z-index: 999;
background-color: #ccc;
text-align: center;
animation-name: fadeOutBgAnime;
animation-duration: 1000ms;
animation-fill-mode:forwards;
animation-delay: 5000ms;
}
/* 背景を再背面に移動し、メインコンテンツを前面に表示させるアニメーション*/
@keyframes fadeOutBgAnime {
100% {
z-index: -1;
}
}
/* Loading画像中央配置 */
.loading_logo {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* Loading アイコンの大きさ設定 */
.loading_logo img {
width: 250px;
height: 250px;
}
/* fadeUpをするアイコンの動き */
.fadeUp {
animation-name: fadeUpAnime;
animation-duration: 1500ms;
animation-fill-mode:forwards;
opacity: 0;
}
@keyframes fadeUpAnime {
0% {
opacity: 0;
transform: translateY(100px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
/* fadeOutをするアイコンの動き */
.fadeOut {
animation-name: fadeOutAnime;
animation-duration: 1000ms;
animation-fill-mode:forwards;
animation-delay: 3500ms;
opacity: 1;
}
@keyframes fadeOutAnime {
100% {
opacity: 0;
display: none;
}
}
作成した結果こんな感じになりました。
開発を終えて
今回、React・TypeScript・Next.jsを使ったWebアプリケーションを初めて1から自分で考えて開発しました。
これまで学習してきた言語・フレームワークはRuby、Railsをメインにやってきましたが、今流行りのモダンな技術を扱うことで、様々なことを学びました。
CSSアニメーションや非同期処理などのJavaScript構文など、フロントエンドの部分へのUI/UXの考えた実装にも取り組み、ユーザー目線での開発がいかに難しいかということが身に染みて実感する良いきっかけにもなったと思います。
また、ReactやTypeScriptを学んだことで、RubyやRailsといったサーバーサイドのスクリプト・フレームワークとの違いにも気づけて、双方のメリット・デメリットの理解にもつながったのも大きな収穫でした。
今後も、バックエンド・フロントエンド双方の知識や技術を磨き、精進していきます。
最後に、完成したWebアプリケーションのデプロイページURLを掲載いたしますので、よかった触ってみてください。
マークダウン記事執筆でよく使うタグ
**<font color="Orange">見出し2</font>**
<img src="" alt="" width=50% height=50%>
<a href="" target="_blank">テキスト</a>