はじめに
今回は、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構成において、ヘッダー・フッターなど共通の外枠を定義するファイルです。
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
は外部から渡された値に応じて内容を描画できる柔軟なコンポーネントになります。
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 されています。
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
に渡しています。
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
は、「表示する中身のロジック」を 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
を使っています。
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
に保存されます。
{projects.map(project => (
<button
key={project.id}
onClick={() => onSelect(project)} // 実態は setSelectedProject(project)
className="..."
>
// ...省略
{/* プロジェクトカードの内容 */}
<div className="...">
{project.title}
</div>
</button>
))}
onSelect
関数は、親コンポーネントから setSelectedProject
を渡しているものです。
モーダルの表示切替
その結果、以下のようにモーダルが表示されるようになります:
{selectedProject && (
<ModalPortal>
<div className="...">
<div className="...">
<ProjectDetail
project={selectedProject}
onClose={() => setSelectedProject(null)} // 閉じるときは null に戻す
getTagColor={getTagColor}
/>
</div>
</div>
</ModalPortal>
)}
表示の流れイメージ:
- ユーザーがプロジェクトをクリック
-
setSelectedProject(project)
が呼ばれてstate
が更新される -
selectedProject
に値が入ることでモーダルが表示される - 閉じるボタンで
setSelectedProject(null)
を呼んで state をクリア - モーダルが消える
このように、ユーザーの操作に応じて動的にUIを更新するために useState
が使われます。
useEffect:DOM表示後に必要な処理を行う仕組み
Reactでは、UIの構造をJSXで宣言的に定義することで、状態に応じた自動更新を実現しています。しかし、実際のアプリ開発では「UIが表示されたあと」に実行したい処理も少なくありません。
JSX内では「UI構造」を記述できますが、以下のような処理はJSXでは完結できないため、useEffect
の中で書く必要があります:
- 初回だけ DOM に処理を加えたい
- 例:スクロール連動のアニメーション、スクロールイベントの登録など
- ページ表示時に外部APIからデータを取得したい
- 例:Qiitaなど外部APIからデータを取得する、
localStorage
の値を読み込むなど
- 例:Qiitaなど外部APIからデータを取得する、
- 一定時間後に処理を実行したい
- 例:5秒後に通知を表示、タイマーのカウント開始など
これらの処理は「コンポーネントが表示されたあとに実行される必要がある」ため、useEffect
にまとめて書くのがReactの基本です。useEffect
にまとめて記述することで、Reactの描画ライフサイクルと連携できます。こうした「画面が表示された後に走らせる処理」のことを React では「副作用(side effect)」と呼び、それを書くための専用の仕組みが useEffect
です。
app/page.tsxでの useEffect の使い方
app/page.tsx(Homeページ)では、スクロールによってスキルカードにアニメーションを付ける処理を useEffect を使って実装しています。
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 クラスを追加してアニメーションを発火させています。
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 によるメニュー開閉状態の管理や条件付きレンダリングやクラス制御での表示切替の対応が必要
- 現在、ハンバーガーメニューの「ボタンUI」は存在するものの、実際のナビゲーションメニューの表示処理は未実装
- 記事データの静的管理から動的取得への移行(API/RSS)
- 現在は
data/articles.tsx
に手動でQiitaやZennの記事を記述しているが、記事が増えるたびに更新が必要なため保守性に課題がある - 今後は動的取得の仕組みを導入する予定
- Qiita API を用いて、自分の投稿記事を自動取得
- ZennのRSSフィードをfetchして、XMLから記事タイトル・リンク・日付などを抽出
- 現在は
今後も改善を重ねながら、実践的なアウトプットを通じてスキルの深化を図っていきたいと思います。