はじめに
必要最小限の機能に絞った直感的に使えるチャットアプリ「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. ユーザー認証機能
- シンプルなログインフォーム
- ユーザー名とパスワードのみで登録(メールアドレス不要)
- 登録後は自動的にログイン状態に
- セッション維持によるログイン状態の記憶
- 明確なエラーメッセージと具体的なガイダンス
2. チャンネル管理機能
- チャンネルの作成・編集・削除
- モーダルウィンドウでの操作
- 作成者のみが編集・削除可能な権限管理
3. メッセージング機能
4. レスポンシブデザイン
5. ダークモード
- 切り替え可能なテーマ設定
- ブラウザ保存による設定維持
- システム設定(OS)との連動オプション
6. その他の機能
データベース設計
アプリケーションのコアとなる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の活用による主なメリット:
- コードの自動生成:基本的なCRUD操作やルーティングなどの定型コードを短時間で生成
- ドキュメント参照の効率化:Flaskやpython-socketioのドキュメントを参照しながらコーディング
- リファクタリング支援:コードの問題点を指摘し、改善案を提案
- デバッグ支援:エラーメッセージの解析と解決策の提案
- 学習ツールとしての活用:FlaskやWebSocketなどの基礎概念をコード生成から学習
開発する中で得た気づき:
- 明確な指示(「WebSocketでリアルタイム更新機能を実装して」など)→ 的確なコード生成
- 曖昧な指示(「チャット機能を作って」など)→ より一般的/保守的なコード提案
- 学習目的の質問(「WebSocketの基本概念と実装方法を説明して」など)→ 教育的なレスポンスとサンプルコード
基礎を学びながらの開発プロセス
本プロジェクトでは、Cursorを単なるコード生成ツールではなく、「対話型学習環境」として活用しました:
-
概念理解から実装へ:
- 最初に「WebSocketとは何か?」「Flask-SocketIOの基本的な使い方」などの基礎知識を質問
- 得られた説明を基に、チャットアプリケーションでの実装方法を段階的に学習
-
エラーからの学習:
- 発生したエラーについて「なぜこのエラーが起きるのか」を質問
- 原因と解決策の説明から、背景にある技術的な概念を深く理解
-
コード解説を通じた学習:
- 生成されたコードの各部分について「この部分は何をしているのか」を質問
- コードの意図と動作原理を理解することで、自分の知識として定着
例えば、WebSocketの実装においては、最初は基本的な概念(コネクション、イベント、ルーム)を学び、次に簡単なエコーサーバーを作成し、最終的にチャットルーム機能を実装するというステップで学習を進めました。
具体的な機能とその実装方法を明確に指示することで、より効率的にAIの支援を受けながら、同時に技術的な基礎知識を身につけることができました。
今後の改善予定
- リアルタイム通知機能: 新着メッセージをポップアップで通知
- お気に入りチャンネル機能: よく使うチャンネルをピン留め
- 簡易ファイル共有機能: PDF・文書ファイルのアップロード
- チャンネル作成・更新のリアルタイム反映: 現状はページリロードが必要
技術的な改善課題
- モデル定義の統合: 時刻処理の一貫化
- 認証ロジックの集約と簡素化: 複数箇所に分散した処理を整理
- 大規模ファイルの分割: ビュー関数と長大なテンプレートの整理
- フロントエンドコードの最適化: JS/CSSの分離とモジュール化
まとめ
「EasyChat」は、シンプルなデザインと必要最小限の機能に絞ったUI/UXで、誰でも直感的に使えるチャットアプリケーションを目指して開発しました。SQLAlchemyを使ったデータモデリング、python-socketioによるリアルタイム通信、レスポンシブデザインとダークモードなど、モダンなWeb技術を取り入れつつも、シンプルさを損なわない設計に力を入れました。