これまで学んだ知識を統合し、実践的なWebサイトを制作します。
- プロジェクト企画
- 設計とワイヤーフレーム
- 実装
- テストとデバッグ
- 公開と改善サイクル
成果物: 個人Webサイト完成
これまでの章では、手順に従ってコードを書いてきました。しかし、自分で何かを作るとなると、まったく違うスキルが必要になります。
チュートリアル形式
┌──────────────────────────────────────┐
│ 1. 手順が決まっている │
│ → 次に何をするか明確 │
│ │
│ 2. 答えが用意されている │
│ → 詰まっても解決策がある │
│ │
│ 3. スコープが限定的 │
│ → ToDoアプリだけ、など │
│ │
│ 4. エラーハンドリングは最小限 │
│ → ハッピーパスのみ │
└──────────────────────────────────────┘
実際のプロジェクト
┌──────────────────────────────────────┐
│ 1. 白紙の状態から始まる │
│ → 何を作るか自分で決める │
│ │
│ 2. 正解がない │
│ → トレードオフを判断する │
│ │
│ 3. スコープが曖昧 │
│ → どこまで作るか決める │
│ │
│ 4. 現実的な制約がある │
│ → 時間、予算、技術的制約 │
└──────────────────────────────────────┘
「いきなりコードを書く」の落とし穴
計画なしで作り始めた場合
┌──────────────────────────────────────┐
│ Day 1: よし、ブログを作ろう! │
│ → トップページのコード書く │
│ │
│ Day 2: あれ、記事のデータどうする? │
│ → データ構造を考え直す │
│ → トップページを修正 │
│ │
│ Day 3: カテゴリー機能も欲しい... │
│ → また設計変更 │
│ → 既存コードを大幅修正 │
│ │
│ Day 7: コードがぐちゃぐちゃ... │
│ → モチベーション低下 │
│ → プロジェクト中断 │
└──────────────────────────────────────┘
設計してから作った場合
┌──────────────────────────────────────┐
│ Day 1-2: 企画・設計 │
│ → 何を作るか明確化 │
│ → 必要な画面を洗い出し │
│ → データ構造を決定 │
│ │
│ Day 3-6: 実装 │
│ → 設計通りにコード書く │
│ → 手戻りが少ない │
│ │
│ Day 7: テスト・デプロイ │
│ → 完成! │
└──────────────────────────────────────┘
プロのエンジニアは、いきなりコードを書きません。プロセスに従います。
ウォーターフォール vs アジャイル
ウォーターフォール(伝統的手法)
┌──────────────────────────────────────┐
│ 1. 要件定義 │████████│ (2週間) │
│ 2. 設計 │████████│ (2週間) │
│ 3. 実装 │████████████│ (4週間) │
│ 4. テスト │████│ (1週間) │
│ 5. リリース │██│ (数日) │
└──────────────────────────────────────┘
特徴:
✅ 計画的
❌ 柔軟性がない
❌ 後半で問題発覚すると大変
アジャイル(モダンな手法)
┌──────────────────────────────────────┐
│ Sprint 1 (2週間) │
│ 企画 → 設計 → 実装 → テスト → リリース │
│ → 最小限の機能をリリース │
│ │
│ Sprint 2 (2週間) │
│ 企画 → 設計 → 実装 → テスト → リリース │
│ → 機能追加 │
│ │
│ Sprint 3 (2週間) │
│ 企画 → 設計 → 実装 → テスト → リリース │
│ → さらに機能追加 │
└──────────────────────────────────────┘
特徴:
✅ 柔軟
✅ 早期にフィードバック取得
✅ リスク分散
個人プロジェクトでのおすすめアプローチ
MVP (Minimum Viable Product) 思考
┌──────────────────────────────────────┐
│ Phase 1: MVP(最小限の機能) │
│ ├─ 記事を表示できる │
│ ├─ 1ページだけ │
│ └─ デザインは最低限 │
│ → まず動くものを作る │
│ │
│ Phase 2: 機能拡張 │
│ ├─ カテゴリー機能追加 │
│ ├─ 検索機能追加 │
│ └─ デザイン改善 │
│ → フィードバックを得ながら改善 │
│ │
│ Phase 3: 磨き上げ │
│ ├─ パフォーマンス最適化 │
│ ├─ SEO対策 │
│ └─ アニメーション追加 │
│ → 完成度を高める │
└──────────────────────────────────────┘
ワイヤーフレーム = 画面の設計図
ワイヤーフレームなしで作った場合
┌──────────────────────────────────────┐
│ 頭の中のイメージ: │
│ "記事カードを並べよう" │
│ │
│ 実装中の悩み: │
│ - 何カラムにする? │
│ - カード内に何を表示? │
│ - 画像のサイズは? │
│ - ホバーエフェクトは? │
│ │
│ → 実装しながら決めるので手戻りが多い │
└──────────────────────────────────────┘
ワイヤーフレームを描いた場合
┌──────────────────────────────────────┐
│ 紙に描いたスケッチ: │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │画像 │ │画像 │ │画像 │ │
│ │Title│ │Title│ │Title│ │
│ │Date │ │Date │ │Date │ │
│ └─────┘ └─────┘ └─────┘ │
│ │
│ 実装: │
│ → 設計通りにコード書くだけ │
│ → 迷わない │
└──────────────────────────────────────┘
ワイヤーフレームの粒度
Low-Fidelity(ざっくり)
┌──────────────────────┐
│ [Header] │
│ ┌──────────────────┐ │
│ │ Hero Section │ │
│ └──────────────────┘ │
│ [Cards] [Cards] │
│ [Footer] │
└──────────────────────┘
Mid-Fidelity(そこそこ詳細)
┌──────────────────────┐
│ Logo [Nav] [Nav] │
│ ┌──────────────────┐ │
│ │ Title │ │
│ │ Subtitle │ │
│ │ [CTA Button] │ │
│ └──────────────────┘ │
│ ┌────┐ ┌────┐ │
│ │Img │ │Img │ │
│ │Text│ │Text│ │
│ └────┘ └────┘ │
└──────────────────────┘
High-Fidelity(デザインモックアップ)
実際の色、フォント、画像を使用
→ これはデザイナーの領域
型定義から始める習慣
プロのTypeScript開発者は、まず型を定義します。
// ❌ 悪い例: いきなり実装
function BlogPost() {
const [post, setPost] = useState(null);
// post の中身は何?型が分からない
// post.title? post.name? post.content?
}
// ✅ 良い例: 型定義から
interface Post {
id: string;
title: string;
content: string;
publishedAt: Date;
author: Author;
tags: string[];
}
interface Author {
id: string;
name: string;
avatar: string;
}
function BlogPost() {
const [post, setPost] = useState<Post | null>(null);
// post の構造が明確
// post.title にアクセスできることが分かる
}なぜ型定義が先なのか?
型定義がコントラクト(契約)になる
┌──────────────────────────────────────┐
│ バックエンドチーム │
│ → Post 型のデータを返すAPIを作る │
│ │
│ フロントエンドチーム │
│ → Post 型を受け取る前提で実装 │
│ │
│ → 同時並行で作業できる │
│ → 統合時にエラーが出ない │
└──────────────────────────────────────┘
リレーションの設計
// 1対多の関係
interface User {
id: string;
name: string;
posts: Post[]; // 1人のユーザーが複数の投稿
}
// 多対多の関係
interface Post {
id: string;
title: string;
tags: Tag[]; // 1つの投稿が複数のタグ
}
interface Tag {
id: string;
name: string;
posts: Post[]; // 1つのタグが複数の投稿に紐づく
}Atomic Design(原子デザイン)
コンポーネントを5つの階層に分ける考え方:
Atoms(原子)- 最小単位
├─ Button
├─ Input
├─ Label
└─ Icon
Molecules(分子)- 原子の組み合わせ
├─ SearchBox (Input + Button)
├─ FormField (Label + Input + ErrorMessage)
└─ Card (Image + Text + Button)
Organisms(有機体)- 分子の組み合わせ
├─ Header (Logo + Navigation + SearchBox)
├─ PostList (複数の Card)
└─ CommentSection (複数の Comment)
Templates(テンプレート)- レイアウト
└─ BlogTemplate (Header + Content + Sidebar + Footer)
Pages(ページ)- 実際のデータを入れたもの
└─ BlogPage (BlogTemplate + 実際の記事データ)
実例: ブログカードの分解
// ❌ 悪い例: 全部1つのコンポーネント
function BlogCard({ post }) {
return (
<div className="card">
<img src={post.image} alt={post.title} />
<div className="content">
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
<div className="meta">
<span>{post.date}</span>
<span>{post.author}</span>
</div>
<button>読む</button>
</div>
</div>
);
}
// ✅ 良い例: 責任を分離
// Atoms
function Button({ children, onClick }) {
return <button onClick={onClick}>{children}</button>;
}
function Badge({ text }) {
return <span className="badge">{text}</span>;
}
// Molecules
function CardImage({ src, alt }) {
return <img src={src} alt={alt} className="card-image" />;
}
function CardMeta({ date, author }) {
return (
<div className="meta">
<span>{date}</span>
<span>{author}</span>
</div>
);
}
// Organisms
function BlogCard({ post }) {
return (
<article className="card">
<CardImage src={post.image} alt={post.title} />
<div className="content">
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
<CardMeta date={post.date} author={post.author} />
<Button onClick={() => navigate(post.url)}>
読む
</Button>
</div>
</article>
);
}なぜこうするのか?
利点
┌──────────────────────────────────────┐
│ 1. 再利用性 │
│ Button は他のコンポーネントでも使える │
│ │
│ 2. テストしやすい │
│ 小さい単位でテストできる │
│ │
│ 3. 変更に強い │
│ Button のデザイン変更は1箇所だけ │
│ │
│ 4. チーム開発しやすい │
│ 担当を分けられる │
└──────────────────────────────────────┘
どこに状態を置くか?
状態のスコープ
┌──────────────────────────────────────┐
│ ローカル状態 (useState) │
│ ├─ モーダルの開閉状態 │
│ ├─ フォームの入力値 │
│ └─ ローディング状態 │
│ │
│ コンポーネント間の共有 (Context) │
│ ├─ テーマ(ライト/ダーク) │
│ ├─ 言語設定 │
│ └─ ユーザー情報 │
│ │
│ グローバル状態 (Zustand/Jotai) │
│ ├─ ショッピングカート │
│ ├─ 通知 │
│ └─ 複雑なフォーム │
│ │
│ サーバー状態 (SWR/React Query) │
│ ├─ APIから取得したデータ │
│ ├─ キャッシュ │
│ └─ リアルタイムデータ │
└──────────────────────────────────────┘
Props Drilling問題
// ❌ 悪い例: Props を何階層も渡す
function App() {
const [user, setUser] = useState(null);
return <Layout user={user} />;
}
function Layout({ user }) {
return <Header user={user} />;
}
function Header({ user }) {
return <UserMenu user={user} />;
}
function UserMenu({ user }) {
return <Avatar user={user} />;
}
function Avatar({ user }) {
return <img src={user.avatar} />;
}
// ✅ 良い例: Context で共有
const UserContext = createContext(null);
function App() {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={user}>
<Layout />
</UserContext.Provider>
);
}
function Avatar() {
const user = useContext(UserContext);
return <img src={user.avatar} />;
}なぜテストを書くのか?
テストなしの開発
┌──────────────────────────────────────┐
│ Day 1: 新機能Aを実装 │
│ → 動作確認OK │
│ │
│ Day 2: 新機能Bを実装 │
│ → Aが壊れていることに気づかない │
│ │
│ Day 3: バグ報告 │
│ → 原因調査に時間がかかる │
│ → 修正したら別の機能が壊れる │
└──────────────────────────────────────┘
テストありの開発
┌──────────────────────────────────────┐
│ Day 1: 新機能A実装 + テスト書く │
│ → テストパス │
│ │
│ Day 2: 新機能B実装 │
│ → テスト実行 │
│ → Aのテストが失敗! │
│ → すぐに問題を発見して修正 │
└──────────────────────────────────────┘
テストの種類
テストピラミッド
△
/E2E\ ← 少ない
/─────\
/ 統合 \ ← 中程度
/─────────\
/ ユニット \ ← 多い
/─────────────\
ユニットテスト:
→ 個別の関数・コンポーネントをテスト
→ 実行が速い
→ たくさん書く
統合テスト:
→ 複数のコンポーネントの連携をテスト
→ 中程度の速度
→ 重要な部分をテスト
E2Eテスト:
→ ユーザー視点で全体をテスト
→ 実行が遅い
→ 主要なフローのみテスト
問題解決のアプローチ
デバッグの手順
┌──────────────────────────────────────┐
│ 1. 問題を再現する │
│ → どの操作で起きる? │
│ → 毎回起きる?時々起きる? │
│ │
│ 2. 問題を切り分ける │
│ → データの問題?ロジックの問題? │
│ → どのコンポーネントで起きる? │
│ │
│ 3. 仮説を立てる │
│ → 「このif文が原因では?」 │
│ │
│ 4. 検証する │
│ → console.log で値を確認 │
│ → デバッガーでステップ実行 │
│ │
│ 5. 修正する │
│ → テストを追加 │
│ → 同じ問題が起きないようにする │
└──────────────────────────────────────┘
効果的なログの取り方
// ❌ 悪い例
console.log('ここ');
console.log('あそこ');
console.log(data);
// ✅ 良い例
console.log('UserProfile: データ取得開始');
console.log('UserProfile: 取得したデータ:', {
userId: data.id,
userName: data.name,
timestamp: new Date()
});
console.log('UserProfile: レンダリング開始');初級:
- 個人ブログ
- ポートフォリオサイト
- ランディングページ
中級:
- ToDoアプリ(高機能版)
- レシピ共有サイト
- 読書記録アプリ
上級:
- SNS風アプリ
- Eコマースサイト
- SaaS(Software as a Service)
チェックリスト:
- ✅ 自分が使いたいものか
- ✅ 技術的に実現可能か
- ✅ 2-4週間で完成できるか
- ✅ ポートフォリオに載せたいか
機能要件:
- ユーザーができること
- 必要な画面
- データの種類
非機能要件:
- レスポンシブ対応
- パフォーマンス
- アクセシビリティ
簡単な画面設計図を作成します。
ツール:
- Figma(無料)
- Excalidraw(無料)
- 紙とペン
例: ブログサイトの画面構成
┌─────────────────────────────┐
│ Header (ナビゲーション) │
├─────────────────────────────┤
│ │
│ Hero Section │
│ - タイトル │
│ - サブタイトル │
│ │
├─────────────────────────────┤
│ 記事一覧 │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │Card │ │Card │ │Card │ │
│ └─────┘ └─────┘ └─────┘ │
├─────────────────────────────┤
│ Footer │
└─────────────────────────────┘
必要なデータ構造を定義します。
// 例: ブログの型定義
interface Post {
id: string;
title: string;
content: string;
excerpt: string;
coverImage: string;
date: string;
author: {
name: string;
avatar: string;
};
tags: string[];
}
interface Category {
id: string;
name: string;
slug: string;
}project/
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ ├── blog/
│ │ │ ├── page.tsx
│ │ │ └── [slug]/
│ │ │ └── page.tsx
│ │ └── api/
│ │ └── posts/
│ │ └── route.ts
│ ├── components/ # UIコンポーネント
│ │ ├── ui/ # 汎用UIパーツ
│ │ │ ├── Button.tsx
│ │ │ ├── Card.tsx
│ │ │ └── Input.tsx
│ │ ├── layout/ # レイアウト
│ │ │ ├── Header.tsx
│ │ │ ├── Footer.tsx
│ │ │ └── Navigation.tsx
│ │ └── features/ # 機能別
│ │ ├── blog/
│ │ └── auth/
│ ├── lib/ # ユーティリティ
│ │ ├── api.ts
│ │ └── utils.ts
│ ├── hooks/ # カスタムフック
│ │ ├── useAuth.ts
│ │ └── usePosts.ts
│ ├── types/ # 型定義
│ │ └── index.ts
│ └── styles/ # グローバルスタイル
│ └── globals.css
├── public/ # 静的ファイル
│ ├── images/
│ └── fonts/
├── .env.local # 環境変数
├── next.config.js
├── tailwind.config.ts
├── tsconfig.json
└── package.json
Single Responsibility(単一責任):
// ❌ 悪い例: 1つのコンポーネントで全部やる
function BlogPage() {
// データ取得、状態管理、UI、全部ここ
}
// ✅ 良い例: 責任を分離
function BlogPage() {
const { posts } = usePosts(); // データ取得
return <PostList posts={posts} />; // UIは別コンポーネント
}Props Drilling を避ける:
// Context APIを使う
import { createContext, useContext } from 'react';
const ThemeContext = createContext('light');
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}ローカル状態 vs グローバル状態:
// ローカル: 1つのコンポーネント内のみで使う
const [open, setOpen] = useState(false);
// グローバル: 複数のコンポーネントで共有
// Context API、Zustand、Jotaiなどを使用// app/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-bold mb-4">エラーが発生しました</h2>
<p className="text-gray-600 mb-6">{error.message}</p>
<button
onClick={reset}
className="px-6 py-2 bg-blue-500 text-white rounded"
>
再試行
</button>
</div>
</div>
);
}
// app/loading.tsx
export default function Loading() {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin h-12 w-12 border-4 border-blue-500 border-t-transparent rounded-full" />
</div>
);
}機能テスト:
- ✅ 全ページが正しく表示される
- ✅ リンクが正しく動作する
- ✅ フォームの送信が正しく動作する
- ✅ エラーハンドリングが動作する
レスポンシブテスト:
- ✅ モバイル(375px)
- ✅ タブレット(768px)
- ✅ デスクトップ(1024px以上)
ブラウザテスト:
- ✅ Chrome
- ✅ Safari
- ✅ Firefox
- ✅ Edge
パフォーマンス:
- ✅ Lighthouse スコア 90以上
- ✅ 画像最適化
- ✅ 不要なJavaScriptの削除
// 開発環境でのみログ出力
if (process.env.NODE_ENV === 'development') {
console.log('Debug:', data);
}
// React Developer Tools
// Chrome拡張機能をインストール機能:
- 記事の一覧表示
- 記事の詳細表示
- カテゴリー別表示
- タグ検索
- お問い合わせフォーム
技術スタック:
- Next.js 14 (App Router)
- TypeScript
- Tailwind CSS
- Markdown (記事管理)
実装の流れ:
- プロジェクト作成
npx create-next-app@latest my-blog
cd my-blog
npm run dev- 記事データの管理
# posts/first-post.md
---
title: "最初の投稿"
date: "2025-01-01"
tags: ["tech", "nextjs"]
---
記事の本文...- 記事の読み込み
// lib/posts.ts
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
export function getAllPosts() {
const postsDirectory = path.join(process.cwd(), 'posts');
const filenames = fs.readdirSync(postsDirectory);
return filenames.map((filename) => {
const filePath = path.join(postsDirectory, filename);
const fileContents = fs.readFileSync(filePath, 'utf8');
const { data, content } = matter(fileContents);
return {
slug: filename.replace('.md', ''),
...data,
content
};
});
}- 一覧ページ
// app/blog/page.tsx
import { getAllPosts } from '@/lib/posts';
import { PostCard } from '@/components/PostCard';
export default function BlogPage() {
const posts = getAllPosts();
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-4xl font-bold mb-8">ブログ</h1>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{posts.map((post) => (
<PostCard key={post.slug} post={post} />
))}
</div>
</div>
);
}- 友人・家族に使ってもらう
- SNSで公開してフィードバックをもらう
- Google Analyticsでユーザー行動を分析
- 問題点の特定
- 優先順位付け
- 実装
- テスト
- デプロイ
以下のいずれかを選んで、完成させてください:
-
ポートフォリオサイト
- 自己紹介
- プロジェクト紹介
- スキル一覧
- お問い合わせフォーム
-
個人ブログ
- 記事の一覧・詳細
- カテゴリー分類
- 検索機能
- RSS配信
-
趣味のWebアプリ
- 読書記録
- 映画レビュー
- レシピ管理
要件:
- ✅ レスポンシブデザイン
- ✅ SEO対策
- ✅ Vercelにデプロイ
- ✅ GitHubで公開
- ✅ README.mdに説明を記載
このコース全体で学んだこと:
- ✅ 第1章: 環境構築
- ✅ 第2章: HTML & CSS
- ✅ 第3章: JavaScript基礎
- ✅ 第4章: TypeScript
- ✅ 第5章: React基礎
- ✅ 第6章: Next.js
- ✅ 第7章: UX向上
- ✅ 第8章: 公開と運用
- ✅ 第9章: 総合制作
おめでとうございます!あなたはWebフロントエンド開発の基礎から応用まで学び、実際にWebサイトを公開できるようになりました。
フロントエンド:
- アニメーションライブラリ(Framer Motion)
- 状態管理(Zustand, Jotai)
- テスト(Jest, React Testing Library)
バックエンド:
- Next.js API Routes
- データベース(Prisma + PostgreSQL)
- 認証(NextAuth.js)
DevOps:
- Docker
- CI/CD
- モニタリング
学習を完了したら、以下の項目をチェックしてください:
- チュートリアルと実践の違いを理解している
- MVP思考でプロジェクトを計画できる
- プロジェクトのスコープを定義できる
- 要件を明確化できる
- ワイヤーフレームを描ける(紙でもツールでも)
- データ構造を設計できる
- TypeScriptの型定義から設計を始められる
- コンポーネント構成を考えられる
- Atomic Designの概念を理解している
- コンポーネントを適切に分割できる
- 状態管理の設計ができる
- Props Drilling問題を理解し、対処できる
- ウォーターフォールとアジャイルの違いを理解している
- Git のブランチ戦略を理解している
- コミットメッセージを適切に書ける
- テストの重要性を理解している
- console.logで効果的にデバッグできる
- ブラウザの開発者ツールを使いこなせる
- エラーメッセージから問題を特定できる
- 単一責任の原則を理解している
- コードの可読性を意識できる
- 再利用可能なコンポーネントを作成できる
- 適切な命名ができる
- ゼロから個人プロジェクトを立ち上げられる
- プロジェクト構成を自分で決められる
- 問題を分解して解決できる
- 完成したプロジェクトをデプロイできる
- フィードバックを収集できる
- 優先順位をつけて改善できる
- パフォーマンスを測定できる
- アクセシビリティを考慮できる
- さらに学ぶべき技術を特定できる
- 自分の強みと弱みを理解している
- 継続的な学習の重要性を理解している
- Next.js公式ドキュメント
- React公式ドキュメント
- TypeScript Handbook
- MDN Web Docs
- Web.dev - パフォーマンスとベストプラクティス
おつかれさまでした!これからも学び続けて、素晴らしいWebサイトを作っていきましょう!