❄️

Snowflake World Tour 2025 コミュニティブースアプリを作った話

に公開

こんにちは!Kirigayaです
今回はSnowflake World Tour 2025のコミュニティブースでアプリ開発を担当しました!!の記事です。
気がついたら実装班のリーダー(1人開発)になってました!
約1ヶ月どんな開発を行ったのかを記事にしました。
面白そうだと思ったら読んでみてください!!

それでは早速、

アプリの概要

今回イベントに参加したユーザーがSnowVillageに参加するきっかけを増やしたい目的で様々なミッションを楽しくこなしてもらえるようなAPPを作成しました。ユーザーはミッションをこなすうちにSnowVillageに参加している状態がゴールです。
実際のアプリケーションのイメージを貼っておきます
イベント会場で遊んでもらうためにスマホ画面用に最適化しています

使用した技術

  • Streamlit
  • Claude Code
  • PostgreSQL

それぞれ選定理由を解説します。
まずStreamlitですが最初に使用しようと考えていたのはRemixでした。しかしアプリをホストするインフラの構築から始めるとお金と実装工数が増えることもあり断念しました。
そこで普段から触っているStreamlitとStreamlit Cloudでアプリを作成することにしました。Streamlit Cloudを使えば無料でアプリを公開することができます。これでインフラのことを忘れることができました。

次にClaude Codeです。今回はSonnet 4を利用しました。利用場面としてはほぼ全てです。平日1時間〜2時間ほどしか作業時間が確保出来ないので使わない選択肢はありませんでした。もっと言うと今回のアプリ開発で私自身がコードを書いた部分はありません。
0から全て指示だけでアプリ開発できるのか気になる自分がいました...

次にPostgreSQLですが理由としてSnowflakeがCrunchy Dataを買収したことで、いつか使えるかもね!と夢を抱いていたところ日本PostgreSQLユーザ会の理事長 喜田 紘介氏 経由でCrunchy Dataから無料でDB提供いただいたのでありがたく使用させていただきました!

環境構築

最初に.devcontainer.jsonを用意します
{
  "name": "SNOW-PJ",
  "image": "mcr.microsoft.com/devcontainers/python:3.12",
  "features": {
    "ghcr.io/devcontainers/features/node:1": {
      "version": "22"
    }
  },
  "postCreateCommand": "npm install -g @anthropic-ai/claude-code && curl -L https://aka.ms/gcm/linux-install-source.sh | sh && git-credential-manager configure",
  "forwardPorts": [5173, 3000, 8080],
  "portsAttributes": {
    "5173": {
      "label": "Vite Dev Server",
      "onAutoForward": "notify"
    }
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "ms-python.python",
        "ms-python.pylint"
      ]
    }
  },
  "remoteUser": "vscode"
}

ここからClaude Codeに「ポスグレとstreamlitを使ってアプリを開発するdevcontainer環境を作成してください。DBのコンテナは分けてください。パッケージ管理にはuvを使ってください」と指示を出します。すると環境を作ってくれました!

.devcontainer.json
{
  "name": "SNOW-PJ",
  "dockerComposeFile": "docker-compose.dev.yml",
  "service": "devcontainer",
  "workspaceFolder": "/workspaces/snowvillage_go_pj/snowvillage-go-st",
  "features": {
    "ghcr.io/devcontainers/features/node:1": {
      "version": "22"
    },
    "ghcr.io/devcontainers/features/docker-in-docker:2": {}
  },
  "postCreateCommand": "npm install -g @anthropic-ai/claude-code && pip install uv && cd app && uv sync && uv add streamlit",
  "portsAttributes": {
    "5173": {
      "label": "Vite Dev Server",
      "onAutoForward": "notify"
    },
    "8501": {
      "label": "Streamlit App",
      "onAutoForward": "notify"
    },
    "5432": {
      "label": "PostgreSQL Database",
      "onAutoForward": "silent"
    }
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "ms-python.python",
        "ms-python.pylint",
        "ms-python.black-formatter",
        "ms-vscode.vscode-json",
        "bradlc.vscode-tailwindcss"
      ]
    }
  },
  "remoteUser": "vscode",
  "remoteEnv": {
    "DB_HOST": "postgres-dev",
    "DB_NAME": "snowvillage",
    "DB_USER": "postgres",
    "DB_PASSWORD": "devpassword",
    "DB_PORT": "5432"
  }
}

docker-compose.dev.yml
version: '3.8'

services:
  # 開発用PostgreSQLデータベース
  postgres-dev:
    image: postgres:15
    container_name: snowvillage-postgres-dev
    environment:
      POSTGRES_DB: snowvillage
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: devpassword
    ports:
      - "5432:5432"
    volumes:
      - postgres_dev_data:/var/lib/postgresql/data
      # 開発用の初期化スクリプト(必要に応じて)
      # - ./init-dev-db.sql:/docker-entrypoint-initdb.d/init-dev-db.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  # 開発用コンテナ(devcontainer)
  devcontainer:
    image: mcr.microsoft.com/devcontainers/python:3.12
    container_name: snowvillage-devcontainer
    depends_on:
      postgres-dev:
        condition: service_healthy
    environment:
      - DB_HOST=postgres-dev
      - DB_NAME=snowvillage
      - DB_USER=postgres
      - DB_PASSWORD=devpassword
      - DB_PORT=5432
    volumes:
      - ../..:/workspaces:cached
    command: sleep infinity
    networks:
      - default

volumes:
  postgres_dev_data:

アプリ作成

環境構築ができたので早速アプリケーション作成に移っていきます。アプリのイメージ的に緑色の鳥がイメージの学習アプリがコミュニティの中での話し合いで上がっていたのでそのように依頼をしました。
すると似たようなものはできるのですがStreamlitで作っているのでボタンが反応しなかったりマルチページなのに遷移しなかったりと、何度指示を出してもうまく行くことはありませんでした。
そこで改めてふわふわしていたアプリイメージをちゃんと言語化することにしました。
さらに、なるべくStreamlitの標準機能を使いcssでデザインをリッチにしてく方針にしました。
要件を書き出しマークダウンにして保存します。

要件.md
## app概要
- このappはイベントの回遊をより楽しんでもらうためのものです。プレイヤーにはミッションが与えられます。ミッションをクリアし一定量のミッションをクリアすると報酬をゲットできます。報酬は実際のシールだったりします。

## 要件
- プレイヤーは名前だけでログインできること
- プレイヤーのミッション進捗を管理できること
- リッチなUIであること
- ランキング機能があること

## 詳細
### プレイヤーは名前だけでログインできること
- プレイヤーは遊びに行くボタンから名前を登録して遊ぶことができる。DBに問い合わせを行いマッチしない場合にDBに名前を登録してダッシュボード画面へ遷移できる。マッチした場合は警告popupを表示
- 同じ名前での登録はできない
- 同じ名前を入力しつづきを遊ぶボタンを押すとDBに問い合わせを行いマッチした場合はログイン後のダッシュボード画面へ遷移できる。マッチしない場合は警告popupを表示

### プレイヤーのミッション進捗を管理できること
- ダッシュボード画面にはミッションカードが並び挑戦ボタンから挑戦することができる
- ミッションには2種類あり技術クイズ系とブースを訪れてSNS投稿を促す系
- ミッションはymlファイルに記述された内容から作成しミッションIDをDBに登録し進捗管理を行う
- 挑戦ボタンをクリックするとクイズ形式なら別ページへ移動。SNS投稿系ならpopupで内容を表示し完了ボタンが押されるとDBの完了フラグを更新する
- 完了したミッションは完了済みとしクリックできないようにUIを変更し次のミッションの挑戦ボタンを出現させる。難しい場合は最初から全てのミッションの下に挑戦ボタンを表示させておく
- ミッションは全部で30用意する
- ミッションを5つクリアするごとに報酬をゲットpopupを表示し報酬ゲット確認ページのカードの色を変化させる

### できる限り綺麗なUIにする
- できる限りUIをcssやhtmlをつかって綺麗にする
- 背景画像を必ずセットする

## ランキング機能 (優先順位 低)
- ランキングページを作成し他のプレイヤーと順位を競う
- ランキングは達成したミッション数によって決定する
- ランキングページにアクセスするとDBに問い合わせを行い一番ミッションを達成したユーザー名上位10位が表示される
- 同一順位が発生する場合はプレイヤーの進捗の最終更新日時で決定する

## 使用技術
- streamlit
- uv
- Postgres DB
- コンテナ環境開発

## その他要件
- appエントリーポイント main.py
- 極力ファイル数を減らせる実装
- 各ページには必ずログアウトボタンがある
- 開発環境はこれ以上変更しない
- 現在のログイン画面やダッシュボードのデザインを極力変更しない

あとはClaude Codeに要件をもとにアプリを作成してもらい、気になるところを指示して修正してもらう作業を繰り返していくことでアプリが完成します。作成されていく過程を語りたい気持ちはあるのですが長くなるので割愛します...

対処した問題紹介

ここからはClaude Codeと開発するにあたり遭遇した問題と対処方法を紹介します。
※ベストプラクティスではないので効率の良い方法があればコメントください...

リージョンによるDBへのクエリ遅延

Streamlitの挙動として表示するタスクを読み込むときにDBに問い合わせてユーザーの進捗状況を確認します。その際にページが1秒以上ロードが入る挙動が確認されました。この問題を見つけた時に最初はAPP側のキャッシュやフラグメント(部分的に動かす)を適切に設定できていないと考えていました。
しかし、Claude Codeにキャッシュとフラグメントでappの最適化を行ってもらったのですが特に怪しい点はソースコードからパッと見つけることができませんでした...
そこで気になったのがアプリケーションとDBへの通信でした。そもそもDBは日本に建ててるけどStreamlit Cloudはどこにサーバーがあるのだろうか?
確認してみるとGoogle CloudのUS(米国)リージョンだとわかりました。そこでDBのリージョンもUSに設定してみるとAPPのもっさり感がなくなりました。リージョンはちゃんと意識しないとダメだと学びました...

アプリケーションの挙動がブレる

この問題はアプリケーションの作成指示をプロンプトと画面のイメージ(スクリーンショット)のみで伝えていた時に発生しました。これは追加の機能を開発したりバグを直すための指示を行うと意図しない機能がついたり新たなバグを大量に発生させる事態になりました。最終的にはリポジトリごと消すことになりました。開発期間としては中間くらいだったので焦りました...
対策として最初にアプリケーションの要件を整理しマークダウンファイルに書き出し常にClaude Codeに参照させるようにしました。設計の必要性と仕様書駆動開発やドキュメント駆動開発が必要な理由がわかりました...

スクリーンショットでデバッグしたりデザインを変更する方法で限界が来た

最初はバグが出たり修正したいデザインなどは「〜ボタンの色を〜へ変更して」とかこの部分のUIズレをなくしてなどスクリーンショットと共に伝えていました。しかしこの方法では意図しない部分まで書き換えたり、何度行っても修正されないことがあるとわかりました。そこでデベロッパーツールを使いカーソルで要素を指定しコピー&ペーストで伝えることで確実に修正してくれるようになりました。
ただ、この方法だと人間が介在するので完璧な自動化はできません...どのようにすれば良いのか検討が必要です。

新しい機能を追加するたびに同じエラーを作る

同じような機能を追加で作らせた時に最初に作った時と同じバグを発生させていることに気がつきました。そこで機能を追加する時に同じバグを発生させないようにknowledgeとして貯めるように指示をだしました。CLAUDE.mdを導入していなかったのでこの時初めて作成しました。すると一度発生したバグは発生しなくなり、デバッグする回数が大幅に減りました。knowledgeを貯める必要性を学びました。

エラーに対して同じ順序でデバッグする

↑と同じことですが同じエラーに遭遇する場合はknowledgeとしてバグの修正手順も貯める必要があると学びました。クリティカルなバグでない限り調査させる時間とトークンが無駄になってしまうからです。

シークレットの情報が漏れる

環境変数として設定していたシークレットがテストコードの中にハードコーディングされていることに気がつきました。この問題がなぜ起きたのかClaude Codeのログを遡ってみるとプロンプトやファイルのリード権限などが原因だとわかりました。DBへの通信を確認するテストコードを書かせている時に何度か通信に失敗して環境変数の中身を確認するように指示したことがあります。その結果、.streamlitのシークレットや環境変数の中身を確認し内容をハードコーディングしてしまうケースがあるとわかりました。
CLAUDE.mdにシークレットの内容をハードコーディングしないように指示を追加しました。
もっといい方法があるとおもいつつ...

最後に

今回はClaude Codeで開発を行いました!
指示を出すだけで思い通りのアプリケーションが作れる世界線ではありますがしっかりエンジニアとしてサボってはいけない部分がまだまだたくさんあると感じました。特に設計部分...
今回はこれでアプリケーション開発としては終わりですがせっかくなので今後のことを考えてみました。
①spec-workflow-mcpなどの仕様書駆動開発ツールを導入する
②PlaywrightやChrome DevTools MCPなどのツールを導入する
③MCPツールSelenaなどの導入
④カスタムコマンドの作成
順番に説明します。
①に関して今回の大きな問題として設計をせず、ゆるふわな状態で開発を進めた結果、最悪の事態を招いてしまいました。意図したアプリをより高精度で作成するためには仕様書駆動開発が必須だと考えています。
②は、今回手動でUIのテストや動作確認を行なっていました。できることならClaude Codeにやってもらいたいですw
③は開発を効率化するツールとしてMCPを活用することがありませんでした。導入してみたい...
④はカスタムコマンドを作って指示を効率化することができませんでした。同じような機能や挙動を確認する指示をコピペしていたのでここらへんは効率化のためにもコマンド化しておきたいです。

今後の課題や改善点が多く見つかりました。最後までありがとうございました!

Discussion