第1章:Webアプリケーションの進化

はじめに

ft_transcendenceは、42 Common Coreの集大成プロジェクトです。フルスタックWebアプリケーションとして、リアルタイムPongゲーム、ユーザー認証、チャット機能を実装します。本章では、Web技術の歴史からモダンSPAアーキテクチャまでを学びます。

---

1. Webの歴史

1.1 World Wide Webの誕生

1989年、CERN:
Tim Berners-Lee が WWW を提案

"Information Management: A Proposal"

目的:
- 研究者間の情報共有
- ハイパーテキストによるリンク
- 分散型ドキュメントシステム

構成要素:
1. HTML (HyperText Markup Language)
2. HTTP (HyperText Transfer Protocol)
3. URL (Uniform Resource Locator)
4. ブラウザ (WorldWideWeb, 後の Nexus)

1.2 Web 1.0 — 静的Webの時代

1990年代:
特徴:
- 静的HTMLページ
- 読み取り専用
- CGIによる動的生成
- テーブルレイアウト

技術スタック:
- HTML 2.0 / 3.2 / 4.0
- CGI (Perl, C)
- Apache HTTP Server
- GIF アニメーション

典型的なサイト:
- 企業ホームページ
- 掲示板 (BBS)
- ディレクトリ (Yahoo!)

1.3 Web 2.0 — インタラクティブWebへ

2000年代:
特徴:
- ユーザー生成コンテンツ
- 双方向コミュニケーション
- AJAX (Asynchronous JavaScript and XML)
- リッチなUI

技術革新:
2004: Gmail (AJAX活用)
2005: Google Maps (動的地図)
2005: YouTube
2006: Twitter
2006: jQuery

AJAXの衝撃:
XMLHttpRequest (XHR) により
ページ遷移なしでサーバー通信可能に

var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data');
xhr.onload = function() {
    console.log(xhr.responseText);
};
xhr.send();

1.4 モダンWeb — SPA時代

2010年代以降:
特徴:
- Single Page Application (SPA)
- コンポーネントベース開発
- 仮想DOM
- 状態管理

主要フレームワーク:
2010: Backbone.js
2010: AngularJS
2013: React (Facebook)
2014: Vue.js
2016: Angular 2+
2020+: Svelte, SolidJS

SPAの原理:
1. 初回: HTML + JavaScript バンドルをロード
2. 以降: JavaScript がページを動的に更新
3. サーバーとは API で通信

---

2. モダンWebアーキテクチャ

2.1 フロントエンドとバックエンドの分離

従来(モノリシック):
+---------------------------+
|        サーバー           |
|  +---------------------+  |
|  |  テンプレート生成   |  |
|  |  (PHP, Rails, etc.) |  |
|  +---------------------+  |
|  |  ビジネスロジック   |  |
|  +---------------------+  |
|  |  データベース       |  |
|  +---------------------+  |
+---------------------------+
            ↓
      HTML ページ

モダン(分離型):
+-------------+         +-------------+
|  フロント   | ←API→  |  バックエンド |
| (SPA)       |         | (REST/GraphQL)|
+-------------+         +-------------+
      ↓                       ↓
  ブラウザ               データベース

利点:
- 独立した開発・デプロイ
- スケーラビリティ
- モバイルアプリと同じAPIを共有

2.2 RESTful API

REST (Representational State Transfer):
Roy Fielding 博士の論文 (2000年)

原則:
1. ステートレス
2. リソース指向
3. 統一インターフェース
4. クライアント-サーバー分離

HTTPメソッドとCRUD:
+--------+---------+------------------+
| Method | CRUD    | Example          |
+--------+---------+------------------+
| GET    | Read    | GET /users/1     |
| POST   | Create  | POST /users      |
| PUT    | Update  | PUT /users/1     |
| PATCH  | Partial | PATCH /users/1   |
| DELETE | Delete  | DELETE /users/1  |
+--------+---------+------------------+

レスポンス例:
GET /api/users/1

{
  "id": 1,
  "username": "player1",
  "email": "player1@example.com",
  "avatar": "/avatars/1.png",
  "wins": 10,
  "losses": 5,
  "created_at": "2024-01-15T10:30:00Z"
}

2.3 WebSocket

HTTP vs WebSocket:

HTTP:
クライアント → リクエスト → サーバー
クライアント ← レスポンス ← サーバー
(毎回接続を確立)

WebSocket:
クライアント ← → サーバー
(永続的な双方向通信)

ハンドシェイク:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

用途:
- リアルタイムチャット
- オンラインゲーム
- ライブ通知
- 株価フィード

2.4 OAuth 2.0

OAuth 2.0:
認可のための業界標準プロトコル

ロール:
1. Resource Owner: ユーザー
2. Client: アプリケーション
3. Authorization Server: 認可サーバー (42 API)
4. Resource Server: リソースサーバー

Authorization Code Flow:
+--------+                               +---------------+
|        |--(A)- Authorization Request ->|   Resource    |
|        |                               |     Owner     |
|        |<-(B)-- Authorization Grant ---|               |
|        |                               +---------------+
|        |
|        |                               +---------------+
|        |--(C)-- Authorization Grant -->| Authorization |
| Client |                               |     Server    |
|        |<-(D)----- Access Token -------|               |
|        |                               +---------------+
|        |
|        |                               +---------------+
|        |--(E)----- Access Token ------>|    Resource   |
|        |                               |     Server    |
|        |<-(F)--- Protected Resource ---|               |
+--------+                               +---------------+

42 APIの例:
1. ユーザーを https://api.intra.42.fr/oauth/authorize にリダイレクト
2. ユーザーが承認
3. コールバックURLに認可コードが返される
4. 認可コードをアクセストークンに交換
5. アクセストークンでユーザー情報を取得

---

3. ft_transcendence要件

3.1 必須機能

1. ユーザー管理:
   - OAuth 2.0 認証 (42 API)
   - プロフィール
   - アバター
   - 二要素認証 (2FA)
   - フレンド機能
   - ステータス (オンライン/オフライン)

2. チャット:
   - チャンネル (公開/非公開/パスワード保護)
   - ダイレクトメッセージ
   - ブロック機能
   - 管理者権限

3. Pongゲーム:
   - リアルタイムマルチプレイヤー
   - マッチメイキング
   - カスタマイズオプション

4. インフラ:
   - Docker Compose で全サービス構築
   - HTTPS (TLS)
   - SPA (Single Page Application)

3.2 技術選択

バックエンド:
- NestJS (TypeScript)
- Django (Python)
- Ruby on Rails
- 任意のフレームワーク

フロントエンド:
- 純粋JavaScript/TypeScript
- フレームワーク任意

データベース:
- PostgreSQL (必須)

その他:
- Docker / Docker Compose
- WebSocket
- OAuth 2.0

3.3 ディレクトリ構造

ft_transcendence/
├── docker-compose.yml
├── .env
├── Makefile
├── backend/
│   ├── Dockerfile
│   ├── package.json (or requirements.txt)
│   ├── src/
│   │   ├── main.ts
│   │   ├── app.module.ts
│   │   ├── auth/
│   │   ├── users/
│   │   ├── chat/
│   │   ├── game/
│   │   └── common/
│   └── test/
├── frontend/
│   ├── Dockerfile
│   ├── package.json
│   ├── src/
│   │   ├── index.html
│   │   ├── main.ts
│   │   ├── components/
│   │   ├── views/
│   │   ├── services/
│   │   └── styles/
│   └── public/
├── database/
│   ├── Dockerfile (optional)
│   └── init.sql
└── nginx/
    ├── Dockerfile
    └── nginx.conf

---

4. 技術スタック詳解

4.1 TypeScript

// JavaScriptに型を追加
// Anders Hejlsberg (C# 設計者) による開発

// 型安全性
interface User {
    id: number;
    username: string;
    email: string;
    avatar?: string;  // オプショナル
}

// 型チェック
function getUser(id: number): User {
    // コンパイル時に型エラーを検出
    return {
        id: id,
        username: "player1",
        email: "player1@example.com"
    };
}

// ジェネリクス
interface ApiResponse<T> {
    data: T;
    status: number;
    message: string;
}

// 使用例
const response: ApiResponse<User[]> = await fetchUsers();

4.2 NestJS

// Node.js のエンタープライズフレームワーク
// Angular に着想を得たアーキテクチャ

// モジュール
@Module({
    imports: [TypeOrmModule.forFeature([User])],
    controllers: [UsersController],
    providers: [UsersService],
    exports: [UsersService],
})
export class UsersModule {}

// コントローラー
@Controller('users')
export class UsersController {
    constructor(private readonly usersService: UsersService) {}

    @Get()
    findAll(): Promise<User[]> {
        return this.usersService.findAll();
    }

    @Get(':id')
    findOne(@Param('id') id: string): Promise<User> {
        return this.usersService.findOne(+id);
    }

    @Post()
    create(@Body() createUserDto: CreateUserDto): Promise<User> {
        return this.usersService.create(createUserDto);
    }
}

// サービス
@Injectable()
export class UsersService {
    constructor(
        @InjectRepository(User)
        private usersRepository: Repository<User>,
    ) {}

    findAll(): Promise<User[]> {
        return this.usersRepository.find();
    }
}

4.3 PostgreSQL

-- リレーショナルデータベース
-- ACID 準拠
-- JSON サポート

-- ユーザーテーブル
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    intra_id INTEGER UNIQUE,
    avatar_url VARCHAR(255),
    two_factor_enabled BOOLEAN DEFAULT FALSE,
    two_factor_secret VARCHAR(255),
    status VARCHAR(20) DEFAULT 'offline',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- ゲーム履歴
CREATE TABLE game_history (
    id SERIAL PRIMARY KEY,
    player1_id INTEGER REFERENCES users(id),
    player2_id INTEGER REFERENCES users(id),
    player1_score INTEGER NOT NULL,
    player2_score INTEGER NOT NULL,
    winner_id INTEGER REFERENCES users(id),
    played_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- フレンド関係
CREATE TABLE friendships (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),
    friend_id INTEGER REFERENCES users(id),
    status VARCHAR(20) DEFAULT 'pending',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(user_id, friend_id)
);

-- チャットメッセージ
CREATE TABLE messages (
    id SERIAL PRIMARY KEY,
    channel_id INTEGER REFERENCES channels(id),
    sender_id INTEGER REFERENCES users(id),
    content TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

4.4 WebSocket (Socket.io)

// サーバーサイド (NestJS)
@WebSocketGateway({
    cors: {
        origin: '*',
    },
})
export class GameGateway {
    @WebSocketServer()
    server: Server;

    @SubscribeMessage('joinGame')
    handleJoinGame(
        @MessageBody() data: { gameId: string },
        @ConnectedSocket() client: Socket,
    ) {
        client.join(data.gameId);
        this.server.to(data.gameId).emit('playerJoined', {
            playerId: client.id,
        });
    }

    @SubscribeMessage('paddleMove')
    handlePaddleMove(
        @MessageBody() data: { gameId: string; y: number },
        @ConnectedSocket() client: Socket,
    ) {
        client.to(data.gameId).emit('opponentMove', { y: data.y });
    }
}

// クライアントサイド
const socket = io('http://localhost:3000');

socket.on('connect', () => {
    console.log('Connected to server');
    socket.emit('joinGame', { gameId: 'room-123' });
});

socket.on('playerJoined', (data) => {
    console.log('Player joined:', data.playerId);
});

socket.on('opponentMove', (data) => {
    updateOpponentPaddle(data.y);
});

---

5. 開発環境セットアップ

5.1 プロジェクト初期化

# プロジェクトディレクトリ作成
mkdir ft_transcendence
cd ft_transcendence

# バックエンド (NestJS)
npm i -g @nestjs/cli
nest new backend
cd backend
npm install @nestjs/websockets @nestjs/platform-socket.io
npm install @nestjs/typeorm typeorm pg
npm install @nestjs/passport passport passport-42

# フロントエンド
mkdir frontend
cd frontend
npm init -y
npm install typescript webpack webpack-cli webpack-dev-server
npm install socket.io-client

5.2 docker-compose.yml

services:
  nginx:
    build: ./nginx
    ports:
      - "443:443"
    depends_on:
      - backend
      - frontend
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro

  backend:
    build: ./backend
    expose:
      - "3000"
    depends_on:
      - db
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/transcendence
      - INTRA_CLIENT_ID=${INTRA_CLIENT_ID}
      - INTRA_CLIENT_SECRET=${INTRA_CLIENT_SECRET}
      - JWT_SECRET=${JWT_SECRET}

  frontend:
    build: ./frontend
    expose:
      - "80"

  db:
    image: postgres:15-alpine
    volumes:
      - db_data:/var/lib/postgresql/data
      - ./database/init.sql:/docker-entrypoint-initdb.d/init.sql
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=transcendence

volumes:
  db_data:

networks:
  default:
    name: transcendence

5.3 Makefile

NAME = transcendence

all: build up

build:
	docker-compose build

up:
	docker-compose up -d

down:
	docker-compose down

logs:
	docker-compose logs -f

shell-backend:
	docker-compose exec backend sh

shell-db:
	docker-compose exec db psql -U user -d transcendence

clean: down
	docker system prune -af

fclean: clean
	docker volume rm -f $(docker volume ls -q) 2>/dev/null || true

re: fclean all

.PHONY: all build up down logs shell-backend shell-db clean fclean re

---

6. SPAの基礎

6.1 ルーティング

// クライアントサイドルーティング
// History API を使用

class Router {
    constructor() {
        this.routes = new Map();
        window.addEventListener('popstate', () => this.handleRoute());
    }

    addRoute(path, handler) {
        this.routes.set(path, handler);
    }

    navigate(path) {
        history.pushState(null, '', path);
        this.handleRoute();
    }

    handleRoute() {
        const path = window.location.pathname;
        const handler = this.routes.get(path) || this.routes.get('/404');
        handler();
    }
}

// 使用例
const router = new Router();

router.addRoute('/', () => renderHome());
router.addRoute('/game', () => renderGame());
router.addRoute('/profile', () => renderProfile());
router.addRoute('/chat', () => renderChat());
router.addRoute('/404', () => render404());

// ナビゲーション
document.querySelectorAll('a').forEach(link => {
    link.addEventListener('click', (e) => {
        e.preventDefault();
        router.navigate(link.getAttribute('href'));
    });
});

6.2 コンポーネント設計

// シンプルなコンポーネントシステム
class Component {
    constructor(props = {}) {
        this.props = props;
        this.state = {};
    }

    setState(newState) {
        this.state = { ...this.state, ...newState };
        this.render();
    }

    render() {
        throw new Error('render() must be implemented');
    }
}

// 例: ユーザーカード
class UserCard extends Component {
    constructor(props) {
        super(props);
        this.state = { loading: true };
    }

    async connectedCallback() {
        const user = await fetchUser(this.props.userId);
        this.setState({ loading: false, user });
    }

    render() {
        if (this.state.loading) {
            return '<div class="loading">Loading...</div>';
        }

        const { user } = this.state;
        return `
            <div class="user-card">
                <img src="${user.avatar}" alt="${user.username}">
                <h3>${user.username}</h3>
                <p>Wins: ${user.wins} | Losses: ${user.losses}</p>
                <span class="status ${user.status}">${user.status}</span>
            </div>
        `;
    }
}

6.3 状態管理

// シンプルな状態管理 (Redux風)
class Store {
    constructor(reducer, initialState = {}) {
        this.reducer = reducer;
        this.state = initialState;
        this.listeners = [];
    }

    getState() {
        return this.state;
    }

    dispatch(action) {
        this.state = this.reducer(this.state, action);
        this.listeners.forEach(listener => listener(this.state));
    }

    subscribe(listener) {
        this.listeners.push(listener);
        return () => {
            this.listeners = this.listeners.filter(l => l !== listener);
        };
    }
}

// リデューサー
function appReducer(state, action) {
    switch (action.type) {
        case 'SET_USER':
            return { ...state, user: action.payload };
        case 'UPDATE_GAME_STATE':
            return { ...state, game: action.payload };
        case 'ADD_MESSAGE':
            return {
                ...state,
                messages: [...state.messages, action.payload]
            };
        default:
            return state;
    }
}

// 使用
const store = new Store(appReducer, {
    user: null,
    game: null,
    messages: []
});

store.subscribe((state) => {
    console.log('State updated:', state);
    renderApp(state);
});

store.dispatch({ type: 'SET_USER', payload: { id: 1, name: 'Player1' } });

---

まとめ

本章で学んだこと:

  • Webの歴史: Web 1.0 から SPA へ
  • モダンアーキテクチャ: フロント/バック分離
  • REST API: リソース指向設計
  • WebSocket: リアルタイム双方向通信
  • OAuth 2.0: 認可フロー
  • 技術スタック: TypeScript, NestJS, PostgreSQL
  • SPA基礎: ルーティング、コンポーネント、状態管理

次章では、システムアーキテクチャの詳細設計を学びます。