作って学ぶRails 8入門・後日談|Claude Codeとバイブコーディングで進化するShlink-UI-Rails(実践編)

技術

はじめに

前篇(環境構築編)では、要件をObsidianでMarkdownにまとめ、それをClaude Codeに読み込ませることで「実装 → RSpec → Rubocop」という流れをAIに任せる体制を作りました。人間はレビューと判断に集中し、AIは実装とテストを回す。これがいわゆる“バイブコーディング”の基本形です。

今回の後篇(実践編)では、Claude Codeと一緒に進めたShlink-UI-Railsの機能拡張を紹介します。公式UIに満足できなかった部分を自作UIで補うために、どのように機能を追加し、どのようにAIと対話して完成させていったのかを、開発の順序に沿って振り返っていきます。


QRコード生成から始めた理由

最初に取り組んだのは、短縮URLごとにQRコードを生成して表示する機能でした。Shlinkの公式UIにもQRコード表示はありますが、リンクとQRコードを同時に並べて提示したり、コピー機能と一緒に使うといったニーズは満たされていませんでした。そこで、まずはこの部分を手元のUIで実現することにしました。

Claudeに依頼したのは「Shlink APIのQRコード生成エンドポイントを呼び出し、ビューで表示するサービスクラスを作ること」。さらにRSpecでテストも書いてほしいと伝えました。返ってきたコードはサービスクラスとして成立していたものの、RSpecは最初に外部APIを直接叩いてしまい失敗しました。そこで「外部APIはテストで呼べないのでWebMockでスタブしてほしい」と指摘したところ、すぐに修正を行い、Greenで安定するところまで進みました。

こうして公式UIに足りなかった部分をRails側で補い、最初の成功体験を得ることができました。


コピー機能の実装と進化

次に取り組んだのが、短縮URLをワンクリックでコピーできる機能です。これは短縮URLツールにおいて必須の機能であり、ユーザー体験を左右する重要な要素でもあります。

最初にClaudeが提案してきたコードは非常にシンプルで、クリックするとコピーが実行され、横に「コピーしました!」と表示するだけのものでした。

// Claudeの最初の提案
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["source", "feedback"]

  copy() {
    navigator.clipboard.writeText(this.sourceTarget.value)
    this.feedbackTarget.textContent = "コピーしました!"
  }
}

これでも動作自体は問題なく、最低限の役割は果たしていましたが、実際に触ってみると少し味気なく感じられました。フィードバックが文字だけでは心許なく、もう少しユーザーが直感的に理解できる反応が欲しいと考えました。

そこでClaudeに追加要望を出しました。「クリック時にボタンの色や見た目を変え、成功時には緑色にしてチェックアイコンと『コピー済み!』の文字を表示し、数秒後には元に戻してほしい。また、エラーが発生した場合には赤いボタンに変えて『エラー』と表示してほしい」とリクエストしました。

すると返ってきたのは、まさにその要望を満たした完成度の高いStimulusコントローラでした。

// Claudeが出してきた完成版
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
  static values = { text: String }

  connect() {
    this.textValue ||= this.element.dataset.clipboardText
    this.element.addEventListener("click", async () => {
      if (!this.textValue) return
      const originalHTML = this.element.innerHTML

      try {
        await navigator.clipboard.writeText(this.textValue)

        this.element.innerHTML = `
          <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
          </svg>
          <span>コピー済み!</span>
        `
        this.element.classList.add("bg-green-600", "hover:bg-green-700")
        this.element.classList.remove("bg-blue-600", "hover:bg-blue-700")

        setTimeout(() => {
          this.element.innerHTML = originalHTML
          this.element.classList.remove("bg-green-600", "hover:bg-green-700")
          this.element.classList.add("bg-blue-600", "hover:bg-blue-700")
        }, 2000)
      } catch (err) {
        this.element.innerHTML = `
          <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
          </svg>
          <span>エラー</span>
        `
        this.element.classList.add("bg-red-600", "hover:bg-red-700")
        this.element.classList.remove("bg-blue-600", "hover:bg-blue-700")

        setTimeout(() => {
          this.element.innerHTML = originalHTML
          this.element.classList.remove("bg-red-600", "hover:bg-red-700")
          this.element.classList.add("bg-blue-600", "hover:bg-blue-700")
        }, 2000)
      }
    })
  }
}

実際に動かしてみると、コピーに成功すれば緑のボタンに変化して「コピー済み!」と表示され、チェックアイコンが現れる。数秒後には元に戻る。エラー時には赤いボタンに変わって「エラー」と表示される。このように、ただ文字を出すだけだった最初の実装とは比べものにならないほど分かりやすいUIになりました。

こうしたやり取りを通して、「AIに任せるだけではなく、要望を伝えることで実装が一気に洗練されていく」という感覚を強く実感しました。


UIをシンプルに整える

コピー機能まで揃ったところで、UI全体をもう少しモダンで気持ちよいものに整えることにしました。派手な装飾を加えるのではなく、あくまで使いやすさと視認性を重視し、カード風のフォームを画面中央に配置するスタイルを目指しました。

Claudeに「グラスモーフィズム風にしてほしい」と依頼したところ、bg-white/20backdrop-blur-lg を用いた半透明のカードデザインを提案してきました。背景はシンプルに保ち、フォーム部分を半透明のカードとして浮かせ、角を丸めて影を付ける。これにより、全体がごちゃつかず、シンプルでモダンな見た目に仕上がりました。

ClaudeのTailwindクラスの選び方は的確で、最小限の調整を加えるだけでそのまま使えるほど実用的でした。


認証とOAuthの導入

その後はユーザーごとの認証を導入しました。Deviseを用いて基本的なログイン機能を追加し、さらにGoogle OAuthによるログインを実現しました。認証周りのRSpecでは何度も失敗しましたが、エラーを渡すとClaudeは素直に修正提案を返してきました。ここでは人間のRails経験も必要でしたが、AIと人間の協働で乗り越えることができました。


マイページと統計表示

続いて、ユーザーが自分の短縮URLを管理できるマイページを実装しました。ここではShlink APIを呼び出し、一覧と検索、ページネーションを提供しました。さらにクリック数や訪問数といった統計情報を表示することで、アプリに「使った成果が見える」楽しさを加えることができました。


Claudeと一緒に開発して感じたこと

Claudeと一緒に開発を進めると、実装スピードは驚くほど速く、次々に機能が形になっていきます。ただし、そのままでは動かないコードが返ってくることもあります。特に、処理の抜けやバリデーション忘れ、Tailwindクラスの適用によるデザイン崩れといった考慮漏れは頻繁にありました。

しかし、それらを指摘して修正を依頼すると、Claudeは素直に対応し、すぐに改善されたコードを提示してくれます。つまり、Claudeは八割完成のコードを瞬時に出し、残り二割を人間との対話で仕上げる存在として非常に有用だと感じました。

また、Rubocopを最後にかけることは必須でした。Claudeのコードは動作はしてもスタイルが揃っていないことが多く、そのままではメンテナンス性に欠けます。Rubocopを通すことでチーム開発にも耐えられる品質に整えることができました。


今後の展望

今回の開発で基本機能は揃いましたが、まだ発展の余地は大きく残っています。統計をグラフ化して可視化することや、SlackやLINEとの連携で通知を飛ばす機能、多言語対応や権限管理、モバイル向けに最適化したUIなど、今後の課題は尽きません。


まとめ

こうして後篇では、Claude Codeと一緒にShlink-UI-Railsを進化させていった開発の過程を振り返りました。QRコード生成から始まり、コピー機能の改善、UIデザインの整備、認証とOAuthの導入、マイページや統計表示の実装までを経て、アプリは公式UIを超えて「自分が欲しかったUI」に近づきました。

AIは爆速で実装を進める一方で考慮漏れも多く、人間のレビューが不可欠です。しかし、ログを渡せばすぐに修正案を返してくれる柔軟さがあり、AIと人間が役割分担することで高い生産性を発揮できました。公式UIに満足できなかったことをきっかけに始めたこの開発は、AIを開発パートナーとして迎える可能性を強く感じさせるものになりました。

👉 完成版コードはこちら
https://github.com/enjoydarts/shlink-ui-rails

コメント

タイトルとURLをコピーしました