1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Flask + Python-socketioでシンプルなチャットアプリ「EasyChat」を作ってみた

Last updated at Posted at 2025-03-25

はじめに

必要最小限の機能に絞った直感的に使えるチャットアプリ「EasyChat」を開発しました。本記事では、Flaskとpython-socketioを使った開発過程と実装の詳細をご紹介します。

アプリケーション概要

EasyChatは以下の特徴を持ったシンプルなチャットアプリケーションです:

  • 必要最小限の機能による直感的な操作性
  • 登録からチャット開始までの手順が簡単(メールアドレス不要)
  • 軽量で高速な動作
  • シンプルで見やすいUI/UX
  • レスポンシブデザインによるモバイル対応
  • ダークモード・ライトモード切替機能

技術スタック

バックエンド

  • Flask: 軽量かつ高速な開発が可能なPythonフレームワーク
  • python-socketio: WebSocketによるリアルタイム通信の実装
  • SQLAlchemy: ORMによるデータベース操作とマイグレーション管理

Flaskを選んだ理由は、最小限の機能から始めて必要に応じて拡張できる柔軟性と、開発速度の速さです。特にプロトタイピングから本格的な実装までのスピードを重視しました。

フロントエンド

  • HTML/CSS/JavaScript
  • Jinja2テンプレートエンジン

フロントエンドは特別なフレームワークを使わず、基本技術で実装しました。これにより、余計な依存関係を減らし、軽量なアプリケーションを実現しています。

データベース

  • 開発環境: MySQL 8.0
  • 本番環境: PostgreSQL (Render)

SQLAlchemyを用いることで、異なる環境間でのデータベース移行をスムーズに行えるようにしました。

開発ツール

  • Cursor: AIアシスト機能付きコードエディタ
  • MySQL Sequel Ace: データベース管理
  • Git/GitHub: バージョン管理

特に、Cursorを活用したことで開発効率が大幅に向上しました。コード生成やデバッグのサポートにより、繰り返しのタスクを効率化できました。

主な機能

1. ユーザー認証機能

Image from Gyazo
Image from Gyazo

  • シンプルなログインフォーム
  • ユーザー名とパスワードのみで登録(メールアドレス不要)
  • 登録後は自動的にログイン状態に
  • セッション維持によるログイン状態の記憶
  • 明確なエラーメッセージと具体的なガイダンス

2. チャンネル管理機能

Image from Gyazo

  • チャンネルの作成・編集・削除
  • モーダルウィンドウでの操作
  • 作成者のみが編集・削除可能な権限管理

3. メッセージング機能

Image from Gyazo

  • リアルタイムメッセージ送受信
  • 絵文字リアクション機能
  • メッセージの編集・削除
  • メンション機能
    Image from Gyazo
  • リアクション操作
    Image from Gyazo

4. レスポンシブデザイン

Image from Gyazo
Image from Gyazo

  • スマートフォン・タブレット対応
  • ハンバーガーメニューとサイドバー
    Image from Gyazo
  • タップしやすいボタンサイズとスワイプ操作

5. ダークモード

Image from Gyazo

  • 切り替え可能なテーマ設定
  • ブラウザ保存による設定維持
  • システム設定(OS)との連動オプション

6. その他の機能

  • エラー処理とフィードバック
    Image from Gyazo
  • 画像共有機能
    Image from Gyazo
  • メッセージ検索機能
    Image from Gyazo
  • チャンネル検索機能
    Image from Gyazo
  • ユーザープロフィール機能
    Image from Gyazo

データベース設計

アプリケーションのコアとなる4つのモデルを実装しました

データベース設計

画面遷移図

画面遷移図も書きました

画面遷移図

コード解説

WebSocket通信の実装

# サーバーサイド
from flask import Flask
from flask_socketio import SocketIO, join_room

app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins="*")

# メッセージ送信イベントのハンドラ
@socketio.on('send_message')
def handle_message(data):
    message = Message(
        content=data['content'],
        user_id=data['user_id'],
        channel_id=data['channel_id']
    )
    db.session.add(message)
    db.session.commit()
    
    # チャンネルの全メンバーにブロードキャスト
    socketio.emit('receive_message', {
        'id': message.id,
        'content': message.content,
        'username': message.user.username,
        'channel_id': message.channel_id,
        'created_at': message.created_at.strftime('%Y-%m-%d %H:%M:%S')
    }, room=data['channel_id'])

# チャンネル参加
@socketio.on('join')
def on_join(data):
    room = data['channel_id']
    join_room(room)
// クライアントサイド
const socket = io();

// メッセージ受信イベント
socket.on('receive_message', function(data) {
    if (data.channel_id === currentChannelId) {
        appendMessage(data);
    }
});

// メッセージ送信関数
function sendMessage() {
    const content = messageInput.value.trim();
    
    if (content !== '') {
        socket.emit('send_message', {
            content: content,
            user_id: currentUserId,
            channel_id: currentChannelId
        });
        messageInput.value = '';
    }
}

// チャンネル参加時
function joinChannel(channelId) {
    socket.emit('join', {channel_id: channelId});
    loadMessages(channelId);
}

リアクション機能の実装

@app.route('/api/reactions', methods=['POST'])
@login_required
def add_reaction():
    data = request.json
    message_id = data.get('message_id')
    emoji = data.get('emoji')
    
    # 既存のリアクションをチェック
    existing_reaction = Reaction.query.filter_by(
        user_id=current_user.id,
        message_id=message_id,
        emoji=emoji
    ).first()
    
    if existing_reaction:
        # 既存のリアクションを削除(トグル機能)
        db.session.delete(existing_reaction)
        db.session.commit()
        socketio.emit('reaction_removed', {
            'message_id': message_id,
            'emoji': emoji,
            'user_id': current_user.id
        })
        return jsonify({'status': 'removed'})
    else:
        # 新しいリアクションを追加
        reaction = Reaction(
            user_id=current_user.id,
            message_id=message_id,
            emoji=emoji
        )
        db.session.add(reaction)
        db.session.commit()
        socketio.emit('reaction_added', {
            'message_id': message_id,
            'emoji': emoji,
            'user_id': current_user.id
        })
        return jsonify({'status': 'added'})

ダークモードの実装

// テーマ設定の読み込み
function loadThemePreference() {
    const savedTheme = localStorage.getItem('theme');
    
    if (savedTheme === 'dark') {
        document.documentElement.classList.add('dark-mode');
        document.getElementById('theme-toggle').checked = true;
    } else if (savedTheme === 'light') {
        document.documentElement.classList.remove('dark-mode');
        document.getElementById('theme-toggle').checked = false;
    } else {
        // システム設定に従う
        if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
            document.documentElement.classList.add('dark-mode');
            document.getElementById('theme-toggle').checked = true;
        }
    }
}

// テーマ切り替え
function toggleTheme() {
    const isDarkMode = document.documentElement.classList.toggle('dark-mode');
    localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
}

開発プロセスと工夫したポイント

テスト駆動開発

import pytest
from app.models import Message
import uuid

class TestMessageFeatures:
    def test_create_message(self, auth_client, test_channel):
        """メッセージ作成のテスト"""
        client, user_id = auth_client
        
        response = client.post('/chat/send', data={
            'message': 'テストメッセージ',
            'channel_id': test_channel['id']
        })
        
        assert response.status_code == 302
        # DBに保存されていることを確認
        message = Message.query.filter_by(content='テストメッセージ').first()
        assert message is not None
        assert message.user_id == user_id
    
    def test_edit_message(self, auth_client, db):
        """メッセージ編集のテスト"""
        client, user_id = auth_client
        
        # テスト用メッセージを作成
        message = Message(id=str(uuid.uuid4()), content="元の内容", user_id=user_id)
        db.session.add(message)
        db.session.commit()
        
        # 編集リクエスト
        client.post(f'/chat/messages/{message.id}/edit', data={'content': '編集後の内容'})
        
        # 変更が反映されていることを確認
        updated = Message.query.get(message.id)
        assert updated.content == '編集後の内容'
        assert updated.is_edited == True

WebSocket接続の安定化

// 接続が切れた時の再接続処理
socket.on('disconnect', function() {
    console.log('接続が切断されました。再接続を試みます...');
    setTimeout(function() {
        socket.connect();
    }, 1000);
});

モバイル環境での最適化

// キーボード表示時のレイアウト調整
const messageInput = document.getElementById('message-input');
const chatContainer = document.getElementById('chat-container');

messageInput.addEventListener('focus', function() {
    if (window.innerWidth <= 767) {
        setTimeout(function() {
            chatContainer.scrollTop = chatContainer.scrollHeight;
        }, 300);
    }
});

効率的なデータベース設計

class Message(db.Model):
    __tablename__ = 'messages'
    id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
    content = db.Column(db.Text, nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True)
    channel_id = db.Column(db.Integer, db.ForeignKey('channels.id'), nullable=False, index=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow, index=True)
    is_edited = db.Column(db.Boolean, default=False)
    
    # リレーションシップ
    user = db.relationship('User', backref=db.backref('messages', lazy=True))
    channel = db.relationship('Channel', backref=db.backref('messages', lazy=True))
    reactions = db.relationship('Reaction', backref='message', lazy=True, cascade='all, delete-orphan')

AIコーディングアシスタント「Cursor」を活用した開発と学習プロセス

Cursorの活用による主なメリット:

  1. コードの自動生成:基本的なCRUD操作やルーティングなどの定型コードを短時間で生成
  2. ドキュメント参照の効率化:Flaskやpython-socketioのドキュメントを参照しながらコーディング
  3. リファクタリング支援:コードの問題点を指摘し、改善案を提案
  4. デバッグ支援:エラーメッセージの解析と解決策の提案
  5. 学習ツールとしての活用:FlaskやWebSocketなどの基礎概念をコード生成から学習

開発する中で得た気づき:

  • 明確な指示(「WebSocketでリアルタイム更新機能を実装して」など)→ 的確なコード生成
  • 曖昧な指示(「チャット機能を作って」など)→ より一般的/保守的なコード提案
  • 学習目的の質問(「WebSocketの基本概念と実装方法を説明して」など)→ 教育的なレスポンスとサンプルコード

基礎を学びながらの開発プロセス

本プロジェクトでは、Cursorを単なるコード生成ツールではなく、「対話型学習環境」として活用しました:

  1. 概念理解から実装へ

    • 最初に「WebSocketとは何か?」「Flask-SocketIOの基本的な使い方」などの基礎知識を質問
    • 得られた説明を基に、チャットアプリケーションでの実装方法を段階的に学習
  2. エラーからの学習

    • 発生したエラーについて「なぜこのエラーが起きるのか」を質問
    • 原因と解決策の説明から、背景にある技術的な概念を深く理解
  3. コード解説を通じた学習

    • 生成されたコードの各部分について「この部分は何をしているのか」を質問
    • コードの意図と動作原理を理解することで、自分の知識として定着

例えば、WebSocketの実装においては、最初は基本的な概念(コネクション、イベント、ルーム)を学び、次に簡単なエコーサーバーを作成し、最終的にチャットルーム機能を実装するというステップで学習を進めました。

具体的な機能とその実装方法を明確に指示することで、より効率的にAIの支援を受けながら、同時に技術的な基礎知識を身につけることができました。

今後の改善予定

  1. リアルタイム通知機能: 新着メッセージをポップアップで通知
  2. お気に入りチャンネル機能: よく使うチャンネルをピン留め
  3. 簡易ファイル共有機能: PDF・文書ファイルのアップロード
  4. チャンネル作成・更新のリアルタイム反映: 現状はページリロードが必要

技術的な改善課題

  1. モデル定義の統合: 時刻処理の一貫化
  2. 認証ロジックの集約と簡素化: 複数箇所に分散した処理を整理
  3. 大規模ファイルの分割: ビュー関数と長大なテンプレートの整理
  4. フロントエンドコードの最適化: JS/CSSの分離とモジュール化

まとめ

「EasyChat」は、シンプルなデザインと必要最小限の機能に絞ったUI/UXで、誰でも直感的に使えるチャットアプリケーションを目指して開発しました。SQLAlchemyを使ったデータモデリング、python-socketioによるリアルタイム通信、レスポンシブデザインとダークモードなど、モダンなWeb技術を取り入れつつも、シンプルさを損なわない設計に力を入れました。

参考リンク

1
2
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
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?