7
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?

はじめに

今回は、ReactとNext.js(v15)を使ったポートフォリオサイトを制作しました。
「自分のスキルを伝えること」と、「実践的な構成を経験すること」を目的としています。

この記事では、制作を通して学んだ以下のような内容を整理しています:

  • Reactはどのような考え方で構築するのか?
  • propsをどう使って柔軟なUIを実装するのか?
  • App Router構成がもたらす設計上のメリットとは?

それらの観点を踏まえながら、本記事では実際に使用した技術や構成について、まとめています。

概要と使用技術

このポートフォリオ制作を通じて、主にフロントエンドの観点から Reactのコンポーネント設計・Next.jsのApp Router構成・TypeScriptによる型の管理 を学びました。
UIデザインにはTailwind CSSを用い、デプロイはVercel、CI/CDにはGitHub Actionsを使用しています。

ページ構成は以下の4つで構成しています:

  • Home:キャッチコピーとアバターアニメーションで印象を残しつつ、
    Frontend / Backend / DevOps & Cloud のスキルカードで技術スタックの全体像を表示
  • Profile:自己紹介・現在の学習状況に加え、スキルセットをChart.jsのレーダーチャートで可視化
  • Projects:個人開発やハッカソンで取り組んだプロジェクトの一覧
  • Articles:Qiita・Zennで執筆した技術記事を手動で一覧表示

公開URL:

使用技術

  • フロントエンド
    • React, TypeScript, Next.js, Tailwind CSS
  • インフラ・CI/CD
    • Vecel, GitHub Actions

デプロイについての詳細な手順については、以下の記事にまとめています。

コンポーネントとは?Reactの基本構成

Reactでは、UIを小さな部品(コンポーネント)に分けて組み合わせてアプリを作ります。
これは「1つの関心ごと(Separation of Concerns)を1つの部品にする」ことで、コードの見通しや再利用性を高める
ための考え方です。

このポートフォリオでは、UIコンポーネントを用途別に整理しています:

components/
├── ui/
│   ├── Navigation.tsx      // 全ページ共通で表示されるヘッダーナビゲーション
│   ├── Footer.tsx          // 全ページ共通で表示されるフッター
│   └── StarField.tsx       // 背景に宇宙演出を加えるビジュアルコンポーネント
└── charts/
    └── SkillRadarChart.tsx // スキルをレーダーチャートで可視化するコンポーネント(Chart.js)

これらをすべて、app/layout.tsx に組み込むことで、全ページ共通のレイアウトとして機能させています。layout.tsxは、Next.js App Router構成において、ヘッダー・フッターなど共通の外枠を定義するファイルです。

app/layout.tsx
import Navigation from '../components/ui/Navigation'
import Footer from '../components/ui/Footer'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ja">
      <body>
         {/* ナビゲーションバー(全ページ共通) */}
        <Navigation />

        {/* ルーティングされた app/配下の page.tsx の中身がここに表示される */}
        <main>{children}</main>

        {/* フッター(全ページ共通) */}
        <Footer />
      </body>
    </html>
  );
}

このように、1つの画面に複数の再利用可能なパーツを組み合わせてUIを構成するのが、Reactの基本的な考え方です。部品化することで、同じコンポーネントを複数のページで使い回したり、1箇所の修正で全体に反映させたりといったメリットがあります。
また、これらのコンポーネントで定義されたJSXは、最終的にブラウザ上ではHTMLとして描画される(=DOMになる)ため、UIの構造を適切に分離・設計しておくことは、表示の一貫性やメンテナンス性にも直結します。

propsとは?コンポーネント間のデータの受け渡し

Reactでは、「再利用できるUI部品をどう構築するか」が非常に重要です。
そのために使われる基本的な仕組みが、親コンポーネントから子コンポーネントへ情報を渡す propsです。
これにより、「UIの構造は同じでも表示内容が異なる」ようなパターンに柔軟に対応でき、部品化・共通化が可能になります。

SkillRadarChart での props の使い方

このポートフォリオでは、各スキルカテゴリ(Frontend / Backend / DevOps & Cloud)をレーダーチャートで可視化するために、SkillRadarChart.tsx という共通コンポーネントを実装しました。このコンポーネントは、カテゴリ名・スキル一覧・チャートのカラーコードなどの情報を props として外部から受け取り、1つのUI構造をさまざまなデータで使い回せるように設計されています。

子コンポーネント側(props を受け取る)

このコンポーネントは、以下の3つの情報を props として外部から受け取って表示を切り替えます:

  • title:カテゴリ名
  • skills: 各スキルとレベルの配列
  • color:チャートのカラーコード

このように props を受け取ることで、SkillRadarChart は外部から渡された値に応じて内容を描画できる柔軟なコンポーネントになります。

components/charts/SkillRadarChart.tsx
interface Props {
  title: string;       // カテゴリ名
  skills: Skill[];     // 各スキルとレベルの配列
  color: string;       // チャートのカラーコード
}

export default function SkillRadarChart({ title, skills, color }: Props) {
  const data = {
    labels: skills.map(skill => skill.name),
    datasets: [
      {
        label: `${title} Skills`,
        data: skills.map(skill => skill.level),
        backgroundColor: `${color}33`,
        borderColor: color,
        // ...(省略)
      }
    ]
  };

  return (
    <div>
      <h3>{title}</h3>
      <Radar data={data} options={...} />
    </div>
  );
}

親コンポーネント(props を渡す)

スキルカテゴリごとのデータは data/skills.tsx に定義しており、skillCategories という配列として export されています。

data/skills.tsx
export const skillCategories = [
  {
    name: 'Frontend',
    skills: [
      { name: 'HTML/CSS', level: 80 },
      { name: 'JavaScript', level: 60 },
      { name: 'React.js', level: 55 },
      { name: 'Next.js', level: 50 },
      { name: 'TypeScript', level: 50 }
    ]
  },
  // Backend / DevOps & Cloud も同様に定義
];

この配列を .map() でループし、それぞれのカテゴリ情報を SkillRadarChart に渡しています。

app/profile/page.tsx
import { skillCategories } from '../../data/skills';

<section id="skills" className="mt-12">
  <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
    {/* スキルカテゴリごとにチャートを描画 */}
    {skillCategories.map((category, index) => {
      const colors = ['#06b6d4', '#3b82f6', '#6366f1'];

      return (
        <SkillRadarChart
          key={category.name}
          title={category.name}              {/* カテゴリ名を渡す */}
          skills={category.skills}           {/* スキル配列を渡す */}
          color={colors[index]}              {/* カラーコードを渡す */}
        />
      );
    })}
  </div>
</section>

以下は、実際に .map()SkillRadarChart をカテゴリごとに描画した結果です。
props によって「カテゴリ名・スキル配列・色」が切り替わることで、同じレイアウトのコンポーネントを再利用しています。

SkillRadarChart

なぜこの設計が重要なのか?

このように SkillRadarChart は、「表示する中身のロジック」を props によって外部から注入する設計にすることで、UIの構造を1つだけ定義しておけば、あとはデータを切り替えるだけで再利用できるようになります。
また、.map() によってカテゴリ配列を動的に展開しているため、将来的にカテゴリが増減しても、UIのコードを変更せずに柔軟に対応できる拡張性も確保できます。

React Hooksとは?(useState / useEffect)

Reactでは、状態管理画面表示後の処理初期化や副作用)を関数コンポーネント内で扱うために、Hooks という仕組みを使います。

このポートフォリオでは主に次の2つのHooksを使用しました:

  • useState:ユーザーの操作に応じて状態を管理し、UIを動的に更新する
  • useEffect:初回表示後に一度だけ実行したい処理や、DOM操作・API通信などを記述

useState:ユーザー操作に応じて状態(値)を管理する

Reactで状態を管理するには useState を使います。
この状態が変わると、コンポーネントは自動的に再レンダリングされてUIが更新されるのがReactの特徴です。

モーダルウィンドウの開閉状態を管理する

projectsページでは、ユーザーがプロジェクトをクリックした際にモーダルを開く処理に useState を使っています。

app/projects/page.tsx
export default function Projects() {
  const [selectedProject, setSelectedProject] = useState<Project | null>(null);

この定義により、

  • selectedProjectにプロジェクトのデータが入っていればモーダルを表示
  • null に戻せばモーダルを非表示にする、という状態管理が可能

実際には、プロジェクトがクリックされたときに setSelectedProject() を呼び出すことで、モーダルにその内容を表示しています。
このように、Reactでは「状態を変えるだけでUIが再描画される」ため、ボタン1つでモーダルの開閉や表示の切り替えが実現できます。

実際のクリック処理(stateの更新)

プロジェクト一覧は .map() を使って表示され、クリック時に onSelect(project) が呼ばれます。つまり、この onClick={() => onSelect(project)} が実行されると、親から渡された onSelect 関数(= setSelectedProject)が呼ばれ、該当プロジェクトが state に保存されます。

app/projects/page.tsx
{projects.map(project => (
  <button
    key={project.id}
    onClick={() => onSelect(project)} // 実態は setSelectedProject(project)
    className="..."
  > 
  // ...省略
    {/* プロジェクトカードの内容 */}
    <div className="...">
      {project.title}
    </div>
  </button>
))}

onSelect 関数は、親コンポーネントから setSelectedProject を渡しているものです。

モーダルの表示切替

その結果、以下のようにモーダルが表示されるようになります:

app/projects/page.tsx
{selectedProject && (
<ModalPortal>
  <div className="...">
    <div className="...">
      <ProjectDetail
        project={selectedProject}
        onClose={() => setSelectedProject(null)} // 閉じるときは null に戻す
        getTagColor={getTagColor}
      />
    </div>
  </div>
</ModalPortal>
)}

表示の流れイメージ:

  1. ユーザーがプロジェクトをクリック
  2. setSelectedProject(project) が呼ばれて state が更新される
  3. selectedProject に値が入ることでモーダルが表示される
  4. 閉じるボタンで setSelectedProject(null) を呼んで state をクリア
  5. モーダルが消える

このように、ユーザーの操作に応じて動的にUIを更新するために useState が使われます。

ModalPortal

useEffect:DOM表示後に必要な処理を行う仕組み

Reactでは、UIの構造をJSXで宣言的に定義することで、状態に応じた自動更新を実現しています。しかし、実際のアプリ開発では「UIが表示されたあと」に実行したい処理も少なくありません。

JSX内では「UI構造」を記述できますが、以下のような処理はJSXでは完結できないため、useEffect の中で書く必要があります:

  • 初回だけ DOM に処理を加えたい
    • 例:スクロール連動のアニメーション、スクロールイベントの登録など
  • ページ表示時に外部APIからデータを取得したい
    • 例:Qiitaなど外部APIからデータを取得する、localStorage の値を読み込むなど
  • 一定時間後に処理を実行したい
    • 例:5秒後に通知を表示、タイマーのカウント開始など

これらの処理は「コンポーネントが表示されたあとに実行される必要がある」ため、useEffect にまとめて書くのがReactの基本です。useEffect にまとめて記述することで、Reactの描画ライフサイクルと連携できます。こうした「画面が表示された後に走らせる処理」のことを React では「副作用(side effect)」と呼び、それを書くための専用の仕組みが useEffect です。

app/page.tsxでの useEffect の使い方

app/page.tsx(Homeページ)では、スクロールによってスキルカードにアニメーションを付ける処理を useEffect を使って実装しています。

app/page.tsx
export default function Home() {
  useEffect(() => {
    const cards = document.querySelectorAll('.skill-card')
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) =>{
          if (entry.isIntersecting) {
            entry.target.classList.add('animate-in')
            observer.unobserve(entry.target)
          }
        })
      },
      { threshold: 0.3 }
    )
    cards.forEach((card) => observer.observe(card))
    return () => observer.disconnect() // コンポーネントが消えたときのクリーンアップ
  },[]) // 初回だけ実行する設定

ここで重要なのは、[]依存配列)が空である点です。これは「初回レンダリング時のみ処理を1回実行する」という意味になります。

ここでは、.skill-card 要素がスクロールで画面内に入ったとき、animate-in クラスを追加してアニメーションを発火させています。

Skill Card

App Routerとは?Next.jsのページ設計の考え方

Next.jsのApp Router(App Directory構成)では、ページ単位のルーティングと、共通レイアウトの切り分けが明確に行える構造になっています。App Router構成は、従来の Pages Router(pages/ ディレクトリ)と比べて、レイアウトやデータフェッチの責務がより明確に分離されているのが特徴です。
このポートフォリオでは、app/ ディレクトリ配下に各ページごとの page.tsx を設置しつつ、ナビゲーション、フッター、背景などの全ページ共通の外枠を layout.tsx にまとめています。これにより、ページごとに中身を切り替えながらも、一貫性のあるUI構成を実現しています。

ディレクトリ構成

app/
├─ layout.tsx             // 全ページ共通レイアウト(ヘッダー、フッターなど)
├─ page.tsx               // トップページ
├─ profile/
│   └─ page.tsx           // /profile ページ
├─ projects/
│   └─ page.tsx           // /projects ページ
├─ articles/
│   └─ page.tsx           // /articles ページ

このように、layout.tsx に共通レイアウトをまとめつつ、各 page.tsx に個別のページ内容を記述することで、ページ単位の責務が明確になり、拡張性や可読性の高い構成となっています。

まとめ・今後の展望

このポートフォリオ制作を通じて、Reactの基本構文から、Next.jsのApp Router設計、TypeScriptによる型の活用まで一通りの実装を経験することができました。また、Next.jsのApp Router構成とTypeScriptによる型安全な開発を通じて、ページ設計と型設計の重要性を実感しました。App Routerによりページごとの責務を明確に分離できた一方で、page.tsx や layout.tsx の役割を正しく理解するには慣れが必要でした。TypeScriptでは、propsやstateに適切な型を定義することで開発中のバグを早期に発見できる安心感がありました。特に、型定義によってコードの意図が明確になり、レビューや保守がしやすくなる点は、チーム開発において大きなメリットだと感じています。

特に以下の点は大きな学びとなりました:

  • UIの再利用性を高めるためのコンポーネント設計(propsと状態の管理)
  • App Router構成におけるページ責務の分離と共通レイアウトの整理
  • useEffectによるDOM操作の制御や副作用の管理

今後に向けて

現在の構成で一通りの機能は実現できているものの、より実用的なWebアプリケーションとしての完成度を高めるために、以下のような課題と改善ポイントが明確になっています:

  • Projects・Articlesページのレスポンシブ対応
    • スマートフォンやタブレットでの閲覧時にレイアウトが崩れる部分の最適化が必要
  • ハンバーガーメニューのナビゲーション機能の追加
    • 現在、ハンバーガーメニューの「ボタンUI」は存在するものの、実際のナビゲーションメニューの表示処理は未実装
      • useState によるメニュー開閉状態の管理や条件付きレンダリングやクラス制御での表示切替の対応が必要
  • 記事データの静的管理から動的取得への移行(API/RSS)
    • 現在は data/articles.tsx に手動でQiitaやZennの記事を記述しているが、記事が増えるたびに更新が必要なため保守性に課題がある
    • 今後は動的取得の仕組みを導入する予定
      • Qiita API を用いて、自分の投稿記事を自動取得
      • ZennのRSSフィードをfetchして、XMLから記事タイトル・リンク・日付などを抽出

今後も改善を重ねながら、実践的なアウトプットを通じてスキルの深化を図っていきたいと思います。

7
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
7
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?