はじめに
はじめまして!
プログラミングスクールRUNTEQで転職を目指し、学習を始めて4ヶ月が経ちました。
最近、個人開発でRuby on Rails
のO/RマッパーであるActiveRecord
の学習サービスを作ってみました。
追記(2023/11/8)
こちらのWebサービスは、RUNTEQのWebアプリバトルイベントBATTLE OF RUNTEQにて最優秀賞とオーディエンス賞を受賞する事ができました!
目次
章 | タイトル | 備考 |
---|---|---|
1 | サービス概要 | このサービスについて |
2 | サービスを作成した背景 | 開発に至った理由について |
3 | 使い方 | |
4 | 練習問題について | 概要の説明 |
5 | 主要機能 | |
6 | 使用技術 | |
7 | インフラ構成 | |
8 | 選定理由 | 実装スピードと離脱率の意識 |
9 | 工夫したポイント | パフォーマンスとUI/UX |
10 | 今後の開発について | |
11 | これまで学習した内容 | |
12 | 終わりに | 結果の報告 |
サービス概要
現在メンテナンス中です。
サービスを作成した背景
スクールに入ってから勉強会を開催したり、初学者同士で勉強を教え合う機会が何度もありました。
その際、Ruby on Rails
を学習する中で初学者がつまずきやすいポイントがあることがわかりました。
- MVCのModelに関して、どのような役割があるかわからない
-
ActiveRecord
が何かわかっていない = 内部でO/Rマッパー
の仕組みによりSQLに変換されていることがわかっていない -
User.all
やPost.find_by
を暗記のように書いている人が多い
Rails
には良くも悪くもブラックボックスな部分が多く、ActiveRecord
の理解が浅くてもアプリを作れてしまうという点があります。
そのため、学習が進む中で複数テーブルの結合でレコードを取得する際に、つまずいてしまう状況を多く見かけました。
そのような中でSQLの学習サービスは複数存在しますが、ActiveRecordの学習サービスは現状存在しなかった為、今回開発することに挑戦してみました。
使い方
シンプルです!
問題集一覧からチャプターと好きな問題を選択すると、練習ページで問題を解くことができます。
練習問題について
問題 | 問題数 | 概要 |
---|---|---|
トライアル編 | 5問 | 基本的な操作方法について練習できます。 |
初級編 | 10問 | 一対多の基本的なリレーションを練習できます。 |
中級編 | 10問 | メソッドを用いて少し複雑なレコードの取得を練習できます。 |
上級編 | 5問 | 複数テーブルの複雑なレコードの取得を練習できます。 |
主要機能
SQL 変換機能 | コード判定機能 |
---|---|
コードを実行することで、ActiveRecord の SQL への変換と実行結果を確認することができます。 | 書いたコードを任意のタイミングで判定することができます。 |
学習記事閲覧機能 | メソッド検索機能 |
---|---|
QiitaAPI を用いて、 学習参考記事を表示しています。 | 取得系メソッドをオートコンプリート検索で確認することができます。 |
Twitter シェア機能 | ログイン/ログアウト機能 |
---|---|
OGP を設定しています。 | NextAuth.js を採用し、手軽な認証体験を実現しています。 |
コード実行時に結果タブがアクティブに遷移 | Active Record の説明用モーダル |
---|---|
UX を考慮しての実装です。 | ActiveRecord の基礎を説明することで理解しやすくしています。 |
使用技術
カテゴリ | 技術 |
---|---|
フロントエンド | TypeScript 5.2.2, React 18.2, Next.js 13.4 |
バックエンド | Ruby 3.2.2, Ruby on Rails 7.0.8(API モード) |
データベース | PostgreSQL |
認証 | NextAuth.js |
環境構築 | Docker, docker-compose |
CI/CD | Github Actions |
インフラ | Vercel, Render |
インフラ構成
選定理由
RUNTEQ内のBATTLE OF RUNTEQというWebアプリ大会の応募に合わせて作成した為、開発期間が3週間ほどしかありませんでした。その為、①実装スピードと②ユーザの離脱率を下げるようなUI/UXにしたいという2点から技術選定を行いました。
開発環境
環境ごとの差異をなくしたいこと、またDocker
での環境構築に慣れていた為、Docker
/ docker-compose
をベースの技術として選びました。
バックエンド
バックエンドにはカリキュラムで多く学んできたRuby on Rails
を採用する事でキャッチアップコストを最小限にしました。
フロントエンド
フロント側にはRails7系のHotwire
という選択肢もありましたが、
- 本番環境だと動作があまり速くないことを体感した。(個人開発のアプリ作成時)
- CSSのデザインを1から構築するには時間がなかった、自信がなかった。
- UI/UX、実装スピード、認証セキュリティの面などを考慮し、
Next.js
のNextAuth.js
を採用したかった。
以上の3点から、あまり触れた事がない技術ではありましたが、全体的な工数を考えたときにNext.js
を採用しました。
インフラ
デプロイ先であるRender
、Github Actions
等は導入コストが低かったため、Vercel
に関してはNext.js
とのデプロイ時の相性が良いこと、ブランチごとに新しいドメインでデプロイも行ってくれる為、build
時のエラーがわかりやすいことからも今回採用に至りました。
主要ライブラリ
monaco-editor
表示させるエディタには、monaco-editor
もしくはcodemirror
の選択肢がありました。調査すると、以下の違いがわかりました。
今回はエディタをカスタマイズする事での利点はあまりなかった為、軽量なmonaco-editor
を採用しました。
パッケージ | カスタマイズ性 | バンドルサイズ |
---|---|---|
monaco-editor | 低い、シンプル | 軽い |
code-mirror | 高い | 重い |
next-auth
react-hot-toast
framer-motion
工夫したポイント
1.パフォーマンス
キャッシュ管理ライブラリの1つであるSWRを採用し、レコード取得のパフォーマンスを意識しました。SWR は、まずキャッシュからデータを返し(stale)、次にフェッチリクエストを送り(revalidate)、最後に最新のデータを持ってくるというStale While Revalidateの略称です。
axios
やfetch
に比べて処理速度が速く、キャッシュを再利用することによりデータを即時反映できることから、サクサクとしたページ表示を実現する事ができます。問題がトライアル編、初級編、中級編とそれぞれありますが、一度のリクエストで該当の問題群を全て取得してくることで、2回目以降は全てキャッシュからデータを表示させるように設計しました。
const fetcher = (url: string) => axios.get(url).then((res) => res.data);
const { data, error } = useSWR(`https://xxxx.xxx.com/api/v1/practices?slug=${slug}`, fetcher);
2.快適なUI/UX
2-1.視覚的な導線
導線がわかりやすいようにアイコン
を多めにすること、Tooltip(アイコンhover時に説明の吹き出しが出る仕様)
を配置することで次のアクションを行いやすい設計にしました。
配色も意識し、落ち着いた配色のみを採用する事で視覚的な印象を最小限にする = 利用者を絞らせない、幅広い方に使ってもらえるように意識しました。
2-2.手軽で快適な認証
今回、UI/UX、セキュリティ面を考慮し、NextAuth.js
でのログイン機能を実装しました。
OAuth
ベースのNext.js
向けに作られたライブラリで、Google
やTwitter
、GitHub
など、認証やセッション管理を手軽に行うことができます。
PagesRouter
向けに作られたドキュメントなので、AppRouter
向けのドキュメントがなく調査に少し苦戦しました。
import NextAuth from 'next-auth';
import GithubProvider from 'next-auth/providers/github';
const handler = NextAuth({
providers: [
GithubProvider({
clientId: process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID || '',
clientSecret: process.env.NEXT_PUBLIC_GITHUB_CLIENT_SECRET || '',
}),
],
secret: process.env.NEXTAUTH_SECRET || '',
});
export { handler as GET, handler as POST };
'use client';
import { SessionProvider } from 'next-auth/react';
import { ReactNode } from 'react';
const NextAuthProvider = ({ children }: { children: ReactNode }) => {
return <SessionProvider>{children}</SessionProvider>;
};
export default NextAuthProvider;
とはいえ、セキュリティ周りがOAuth
ベースで保証されており、ユーザ側としても手軽にログインすることができ、離脱率を下げる上で効果的な選択だと感じました。
3.SQL 変換機能について
当初の設計では、to_sql
を使用して、実行結果の文字列を返すような考えでした。
実際に調査したところ、戻り値のクラスによって使えない仕様であったため、別の方法を考えました。
ActiveSupport::Nortifications
のイベントトリガーを用いて、ActiveRecord
の実行されたタイミングで、ログを検知できるようにしています。
とはいえ、綺麗に SQL
が吐かれるのではなく、schema
のバージョンを確認する内部クエリなども出力される為、実行時のクエリのみが取得できるようにロジックを組みました。
プレースホルダの部分を実際の値で置換したり、即座にクエリが吐かれないメソッドもあるので、配列にすることで、イベントが発火されるようにするなど、かなり泥臭く仮説検証を繰り返し、シンプルに表示させることができました。
ActiveSupport::Notifications.subscribe "sql.active_record" do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
sql = event.payload[:sql]
binds = event.payload[:binds].map(&:value)
log_entry = {
sql: sql,
}
unless logs.map { |log| log[:sql] }.include?(log_entry[:sql])
logs << log_entry
logger.debug(log_entry.to_json)
end
end
# ActiveRecordの内部クエリを無視
next unless sql.start_with?("SELECT \"") || sql.start_with?("SELECT COUNT")
# プレースホルダを実際の値で置換
binds.each_with_index do |value, i|
placeholder = "$#{i + 1}"
sql = sql.gsub(placeholder, value.to_s)
end
# クエリを即時発行するように調整
result = result.to_a if result.is_a?(ActiveRecord::Relation)
今後の開発について
1.問題数を増やす
現状のテーブル構成から作り直しを行い、より実践的な問題を増やしていきたいと考えています。
現状が15問なので、50問近くに増やしたいです。
2.管理画面の作成
現状seedのみでマスタデータを管理しているため、管理画面を作成し円滑に運用していきたいです。
3.ダッシュボード画面の作成
ユーザの滞在率、利用率を上げるためにダッシュボード機能の作成と関連機能の拡張を考えています。
4.テスト
テストがほとんど書けていないので、フロント、バック合わせて書いていきたいです。
これまで学習した内容
Next.js
、Vercel
は軽くキャッチアップしていた程度で、それ以外の技術に関しては、スクール内で学習をしてきました。これまでの学習記録に関しては、こちらの記事を良ければご覧ください。
このWebアプリを作成する前に、Next.js
をUdemy教材で学習しました。
体系的かつわかりやすく、はじめてキャッチアップするにはオススメできる教材でした。
終わりに
自分が作りたかったアプリをひとまず形にできたこと、締切駆動開発で実際のリリースのように緊張感を持って開発できたことは良い経験でした。
おかげさまで、こちらのWebサービスはBATTLE OF RUNTEQの予選を運良く勝ち抜き、決勝に進むことができました!
決勝は10月28日(土)15:00~より開催されます。
今後も自分を含め、初学者をサポートできるように開発を進めていきます!
最後まで読んでいただきありがとうございました!