1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

柴犬の画像を取得する個人開発Webアプリdog_app

Last updated at Posted at 2024-03-20

dog_app of development_log

dog_appの開発記録をここに残します。

参考サイト

環境構築の流れ

流れとしては、次のように開発環境を構築していきます。

  1. 最初にローカルでNext.jsをインストール
  2. .gitignoreなどは自分で作る必要はない(Next.jsはでデフォルトで記述済み)
  3. 次に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>
  );
};
  • なお、asyncawaitといったメソッドはJavaScriptの機能。
  • 使い方については、こちらの記事が参考になりました。

ここまでののコミット内容

  • 【Add】DogApiによる画像URLの取得機能を実装02
    • handleClick関数を定義
    • onClickの渡す関数をfetchDogImageからhandleClickに変更
    • ここまでの開発記録を更新

APIによる画像取得の関数にTypeScriptで型を指定する

  • fetchDogImageに対して、TypeScriptで型を指定します。
  • この実装は、TypeScriptの特長を生かして静的型付けをすることで、保守性・セキュリティ性を高める意味があります。
  • まずはinterface SearchDogImage という関数を定義し、そこにキーデータ型を記述していきます。
  • 場所はページコンポーネント関数の外側上に配置します。
  • ここで定義してSearchDogImageGenerics(ジェネリックス)と呼ばれ、複数のデータ型を含んだお手製の関数として利用できます。
  • 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)機能を用いていきます。

順番としてはこんな感じで行っていきます。

  • SSRgetServerSideProps関数を実装
  • IndexPagePropsと命名したinterfaceを実装
  • Home関数コンポーネントinitialCatImageUrlを指定し、リロード時にAPIが走るように実装

SSRgetServerSideProps関数を実装

  • まずはNext.jsが提供するメソッドgetServerSidePropsを定義します。
  • 場所はHomeページコンポーネントの外側に記述します。今回は、最下部付近に実装しました。
export const getServerSideProps: GetServerSideProps = async () => {};
  • なお、exportをつけないといけない理由はよく分かりません🙇
  • GetServerSidePropsを記述すると、自動的にimport { GetServerSideProps, NextPage } from "next";が補完されます。
  • GetServerSidePropsはこれだけで一種の型なのだそうです。

interfaceGetServerSidePropsに渡すデータ型を指定する

  • 続いて先にもやった通り、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関数をページコンポーネントの外側に配置〜

  • なぜか、上記実装ではfetchDogImagegetServerSidePropsで読み取ってくれませんでした。
  • 結論からいうと、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あり
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.tsxshiba.tsxに変更する。
  • Home.module.cssShiba.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.tsxakita.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 />コンポーネントとしてまとめます。
  • 最終的にコンポーネントはHeaderHeadlineMainの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.tsxindex.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ディレクトリを作成
  • その配下にカスタムフックのファイルを作っていきます。
  • 背景色を変えるロジックをuseBgBeigeuseBgLightblueというカスタムフックにまとめました。

// ~/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.tsxAnimation.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;
  }
}

作成した結果こんな感じになりました。

Loading.gif


開発を終えて

今回、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>

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?