【Go/React】WebSocketでリアルタイムメッセージングを実装する

こんにちは！フリーランスエンジニアのこたろうです。
今回は、GinとReactを使用したWebSocketによるリアルタイムメッセージング機能の実装方法について解説します。

## 1. バックエンド実装（Gin）

### WebSocketハンドラーの作成
```go
// handlers/websocket.go
package handlers

import (
    "github.com/gin-gonic/gin"
    "github.com/gorilla/websocket"
    "log"
    "sync"
)

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        return true  // 開発環境用の設定
    },
}

type WebSocketHandler struct {
    // クライアントの管理
    clients    map[*websocket.Conn]bool
    clientsMux sync.Mutex
}

func NewWebSocketHandler() *WebSocketHandler {
    return &WebSocketHandler{
        clients: make(map[*websocket.Conn]bool),
    }
}

func (h *WebSocketHandler) HandleWebSocket(c *gin.Context) {
    // WebSocketにアップグレード
    conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
    if err != nil {
        log.Printf("Websocket upgrade failed: %v", err)
        return
    }

    // クライアントの登録
    h.clientsMux.Lock()
    h.clients[conn] = true
    h.clientsMux.Unlock()

    // クライアント切断時の処理
    defer func() {
        h.clientsMux.Lock()
        delete(h.clients, conn)
        h.clientsMux.Unlock()
        conn.Close()
    }()

    // メッセージの受信ループ
    for {
        var msg Message
        err := conn.ReadJSON(&msg)
        if err != nil {
            if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
                log.Printf("websocket error: %v", err)
            }
            break
        }

        // 全クライアントにブロードキャスト
        h.broadcastMessage(msg)
    }
}

func (h *WebSocketHandler) broadcastMessage(msg Message) {
    h.clientsMux.Lock()
    defer h.clientsMux.Unlock()

    for client := range h.clients {
        err := client.WriteJSON(msg)
        if err != nil {
            log.Printf("broadcast error: %v", err)
            client.Close()
            delete(h.clients, client)
        }
    }
}
```

### ルーティングの設定
```go
// main.go
func main() {
    r := gin.Default()
    
    wsHandler := handlers.NewWebSocketHandler()
    
    r.GET("/ws", wsHandler.HandleWebSocket)
    
    r.Run(":8080")
}
```

## 2. フロントエンド実装（React）

### WebSocketコンポーネント
```typescript
// components/Chat.tsx
import React, { useEffect, useState } from 'react';

interface Message {
    id: string;
    content: string;
    sender: string;
    timestamp: string;
}

export const Chat: React.FC = () => {
    const [messages, setMessages] = useState<Message[]>([]);
    const [ws, setWs] = useState<WebSocket | null>(null);
    const [inputMessage, setInputMessage] = useState('');

    useEffect(() => {
        // WebSocket接続の確立
        const socket = new WebSocket('ws://localhost:8080/ws');

        socket.onopen = () => {
            console.log('Connected to WebSocket');
        };

        socket.onmessage = (event) => {
            const message = JSON.parse(event.data);
            setMessages(prev => [...prev, message]);
        };

        socket.onclose = () => {
            console.log('Disconnected from WebSocket');
        };

        setWs(socket);

        // クリーンアップ
        return () => {
            socket.close();
        };
    }, []);

    const sendMessage = (e: React.FormEvent) => {
        e.preventDefault();
        if (!ws || !inputMessage.trim()) return;

        const message = {
            content: inputMessage,
            sender: 'User',
            timestamp: new Date().toISOString(),
        };

        ws.send(JSON.stringify(message));
        setInputMessage('');
    };

    return (
        <div className="chat-container">
            <div className="messages">
                {messages.map((msg, index) => (
                    <div key={index} className="message">
                        <span className="sender">{msg.sender}</span>
                        <span className="content">{msg.content}</span>
                        <span className="timestamp">
                            {new Date(msg.timestamp).toLocaleTimeString()}
                        </span>
                    </div>
                ))}
            </div>
            <form onSubmit={sendMessage} className="input-form">
                <input
                    type="text"
                    value={inputMessage}
                    onChange={(e) => setInputMessage(e.target.value)}
                    placeholder="メッセージを入力..."
                />
                <button type="submit">送信</button>
            </form>
        </div>
    );
};
```

### スタイリング
```css
/* styles/Chat.css */
.chat-container {
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
}

.messages {
    height: 500px;
    overflow-y: auto;
    border: 1px solid #ccc;
    padding: 10px;
    margin-bottom: 20px;
}

.message {
    margin-bottom: 10px;
    padding: 10px;
    background-color: #f5f5f5;
    border-radius: 5px;
}

.sender {
    font-weight: bold;
    margin-right: 10px;
}

.timestamp {
    color: #666;
    font-size: 0.8em;
    margin-left: 10px;
}

.input-form {
    display: flex;
    gap: 10px;
}

input {
    flex: 1;
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
}

button {
    padding: 10px 20px;
    background-color: #007bff;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
}

button:hover {
    background-color: #0056b3;
}
```

## 3. エラーハンドリングと再接続機能

### 再接続機能の実装
```typescript
// hooks/useWebSocket.ts
import { useState, useEffect, useCallback } from 'react';

const RECONNECT_INTERVAL = 3000; // 3秒

export const useWebSocket = (url: string) => {
    const [ws, setWs] = useState<WebSocket | null>(null);
    const [isConnected, setIsConnected] = useState(false);

    const connect = useCallback(() => {
        const socket = new WebSocket(url);

        socket.onopen = () => {
            setIsConnected(true);
            console.log('Connected to WebSocket');
        };

        socket.onclose = () => {
            setIsConnected(false);
            console.log('Disconnected from WebSocket');
            // 再接続
            setTimeout(() => {
                connect();
            }, RECONNECT_INTERVAL);
        };

        socket.onerror = (error) => {
            console.error('WebSocket error:', error);
        };

        setWs(socket);
    }, [url]);

    useEffect(() => {
        connect();
        return () => {
            if (ws) {
                ws.close();
            }
        };
    }, [connect]);

    const sendMessage = useCallback((message: any) => {
        if (ws && isConnected) {
            ws.send(JSON.stringify(message));
        }
    }, [ws, isConnected]);

    return { ws, isConnected, sendMessage };
};
```

## 4. セキュリティ対策

### バックエンドでのバリデーション
```go
// handlers/websocket.go
func (h *WebSocketHandler) validateMessage(msg Message) error {
    if len(msg.Content) > 1000 {
        return errors.New("message too long")
    }
    if msg.Content == "" {
        return errors.New("message cannot be empty")
    }
    return nil
}

// メッセージ送信前にバリデーション
if err := h.validateMessage(msg); err != nil {
    conn.WriteJSON(gin.H{"error": err.Error()})
    continue
}
```

## まとめ

実装のポイント：
1. WebSocket接続の適切な管理
2. エラーハンドリングと再接続機能
3. メッセージのバリデーション
4. UIの使いやすさ

これらの実装により、リアルタイムで双方向のコミュニケーションが可能なチャット機能を実現できます。