作ったサービス
概要
オートマトンのパズルアプリです。
お題のオートマトンをユーザが作成していくサービスです。
ここでは、動機や使用技術、ユーザの反応を中心に記載していきます。
※詳しいサービスの内容は、WebサービスのURLから見ることができます。
プレイイメージ
WebサービスのURL
ソースコード
動機
以前、オートマトンに関する本を読んだときに、正規言語とその状態遷移図が記載されていました。
そのときに「正規言語から状態遷移図を作成する過程は、非常に面白いのではないか!?」と思いました。
他の理由
- オートマトンの描画を実装できるか検証したい
- オートマトンの面白さを他の人にも伝えたい!!
そこで、実際につくってみることにしました。
機能概要
- オートマトン描画機能
- オートマトン判定機能
- ログイン機能
- オートマトンの保存/取得機能(ログイン済みのユーザのみ)
技術スタック
概要
今回は、フロントエンドとバックエンドを分離した SPA + BFF の構成にしました。
理由
- これまでMVCフレームワークを使ったWeb開発が多かったのですが、その際にテンプレートエンジン(Thymeleaf)とJavaScript(jQuery)のそれぞれの変数が混在していて、可読性があまり高くないと感じたため
- SPAを採用した場合の認証やログインの実装方法に興味があったため
フロントエンド
React, (D3)
SPAフレームワークには、Reactを採用しました。
また、一部にD3を利用しました。(ドラッグ&ドロップの処理)
バックエンド
Flask, SQLAlchemy
バックエンドのAPIサーバのフレームワークに、Flaskを利用しました。
また、ORMには、SQLAlchemyを利用しました。
marshmallowというライブラリを利用することで、簡単にモデル⇔JSONを相互変換できるのが便利でした。
CI
CIには、GitHub Actionsを利用しました。
また、バックエンド(Flask)はpytestを使用してテストコードを作成しました。
インフラ
GitHub Pages
フロントエンド(React)はGitHub Pages上で、運用しています。
GitHub PagesはPublicリポジトリであれば、無料で利用できる点が魅力的でした。
また、gh-pagesというライブラリ利用することで、簡単にデプロイできるのも良いと思いました。
AWS (CloudFront, Elastic Beanstalk(EC2))
バックエンド(Flask)はElastic Beanstalk(EC2)上で、運用しています。
CloudFrontにSSL証明書を設定して、HTTPSにしています。
クライアント(ブラウザ) - GitHub Pages 間にCloudFrontを配置することで、フロントエンドとバックエンドのoriginを同じにしています。
それによって、CookieにSameSite属性(=Lax)を指定して、CSRFの対策をおこなえるようにしています。(Cookieベースの認証を利用している)
インフラ構成
リクエストパスが /api
から始まる場合、CloudFrontでEC2にリバースプロキシするようにしています。
それ以外のリクエストパスの場合、Github Pagesにリバースプロキシするようにしています。
認証
概要
今回は、ログイン機能にGoogleのOpenID ConnectとTwitterのOAuth1.0を利用しました。
Google / OpenID Connect の場合
- 外部サービスのログイン後に、APIサーバにリダイレクトする
- リダイレクト時のパスに含まれるコード値から、id_tokenを取得する外部APIを実行する
-
- 取得したid_tokenのsubがユーザテーブルに登録されていれば(未登録の場合、新規登録)、SessionにユーザIDを設定して、認証済みのユーザとする
Twitter / OAuth1.0 の場合
- 外部サービスのログイン後に、APIサーバにリダイレクトする
- リダイレクト時のパスに含まれるoauth_token, oauth_verifierから、アクセストークンを取得する外部APIを実行する
- 取得したトークンのuser_idがユーザテーブルに登録されていれば(未登録の場合、新規登録)、SessionにユーザIDを設定して、認証済みのユーザとする
フロントエンド
React
初回ロード時に、ユーザ情報取得のAPIを実行します。
認証済みの場合のみ、ユーザ情報を取得できます。
取得したユーザ情報を、UserContext
に設定して他のコンポーネントから参照できるようにしています。
function App() {
const [user, setUser] = useState(null);
useEffect(() => {
const api = async () => {
const user = await UserService.getUser();
// 未ログインの場合、空のobjectが返却されるのでnullを設定
if (Object.keys(user).length === 0) {
setUser(null);
} else {
setUser(user);
}
}
api();
}, []);
return (
<div className="App">
<CssBaseline/>
<Suspense fallback="loading">
<UserContext.Provider value={user}>
{/* Other Components... */}
</UserContext.Provider>
</Suspense>
</div>
);
)
UserContextの参照例(該当箇所のみ抜粋)
ログイン状態に応じて、プロフィール欄とログインボタンの表示を切り替える
function Header(props) {
const user = React.useContext(UserContext);
const profileArea = user ? UserProfile : LoginButton;
return (
<Container>
<Grid>
{profileArea()}
{/* Other Components */}
</Grid>
</Container>
}
API (axios)
APIを実行するライブラリとして、axiosを利用しています。
リクエストの際にCookieも送付するように、withCredentials
をの設定をします。
export default axiosBase.create({
baseURL: Constant.API_ENDPOINT,
headers: {
'Content-Type': 'application/json',
},
withCredentials: true,
responseType: 'json'
});
バックエンド
認証状態をチェックするデコレータ
今回は認証用のライブラリを利用せずに自前で実装しました。
Sessionに user_id が設定されている場合、認証済みとしています。
from functools import wraps
from flask import session, jsonify
def authentication_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if session.get('user_id') is None:
return jsonify({'message': 'Unauthorized'}), 401
return f(*args, **kwargs)
return decorated_function
上記デコレータの利用例
作成したオートマトンを保存できるAPI
認証済みのユーザのみ、このAPIを呼び出すことができる
@bp.route('/user/save_automaton/<int:question_id>', methods=['POST'])
@authentication_required
def save_automaton(question_id):
user_id = session.get('user_id')
req_body = request.json
schema = AutomatonSchema()
automaton_data = schema.load(req_body)
created_automaton = CreatedAutomaton(
user_id=user_id, question_id=question_id,
automaton_data=schema.dump(automaton_data))
AutomatonService.create(created_automaton)
return make_response(jsonify({'message': 'success'}))
印象に残っていること
タイトル/デザインについて
当初は、タイトルを「オートマトンの部屋」にして、Mine◯raftのような配色にすることを想定していました。
開発中に、「恋する寄◯虫」という漫画から着想を得て、「恋するオートマトン」が思いつき、語呂がけっこう良かったので採用しました。
デザインもタイトルに合わせて、ピンクと水色を基調としたPOPな配色にしました。
エフェクトについて
当初は、回答時のアニメーションにエフェクトをつけることは予定していませんでした。
しかし、アニメーション実装後に(エフェクトなしで)プレイしてみたところ、爽快感や気持ちよさをあまり感じませんでした。
そこで、正解時に花火のようなエフェクトを設定しました。
多少なりとも、爽快感を感じるようになったと思います。
エフェクトのアニメーションはSVGのanimate, animateMotion, animateTransformを利用して実装しました。
CSSの Key frame と異なり、動的にアニメーションの時間や変化量、角度を設定できます。
実際のソースは以下で確認できます。
https://github.com/threeislands/automaton-app/blob/master/frontend/src/components/ClearEffect.js
リリース後の反応
リリースして、友人や知り合いなどだいたい20人ぐらいの人にプレイしてもらうことができました。
ポジティブなフィードバック
- 面白い
- 楽しかった
エンジニアやゲームが好きな人、数学好きな人からは好意的なフィードバックをもらうことが多く、とても嬉しかったです。
また、レベル7や8(結構むずかしい問題)でもあっさりとクリアしてしまう人がいたことには、とても驚かされました。
どちらかというとネガティブなフィードバック
- 難しすぎる
- 意味分かんない!
- 説明は理解できたけど、どうやっていいかわからない
- 出会い系サイトっぽい
まず、プレイするにあたって理解しておくルールが多いため、ユーザの認知的な負荷が大きくなってしまいました。
改善案として、インタラクティブなオンボーディングを導入することを検討したいです。(ユーザの操作ごとに説明文が更新される等)
出会い系サイトっぽいという意見はちょっと意外でした。笑
素敵なオートマトンに出会えるので、ある意味出会い系サイトかもしれません!?
感想
今回、初めて個人でサービスを開発してリリースまでおこなうことができました。
自分の開発したサービスをユーザが利用してくれることはとても嬉しく、フィードバックからも多くの洞察を得ることができました。
今回作成したサービスは、自分の作りたいサービスでしたが、次回はユーザの潜在的なニーズに合致するサービスを作りたいと思います。