LoginSignup
2
1

More than 1 year has passed since last update.

個人開発でWebサービス(パズルアプリ)をリリースしたときの話

Last updated at Posted at 2021-01-06

作ったサービス

概要

オートマトンのパズルアプリです。
お題のオートマトンをユーザが作成していくサービスです。

ここでは、動機や使用技術、ユーザの反応を中心に記載していきます。
※詳しいサービスの内容は、WebサービスのURLから見ることができます。

プレイイメージ

play.gif

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ベースの認証を利用している)

インフラ構成

cluod_infra.jpg

リクエストパスが /api から始まる場合、CloudFrontでEC2にリバースプロキシするようにしています。
それ以外のリクエストパスの場合、Github Pagesにリバースプロキシするようにしています。

認証

概要

今回は、ログイン機能にGoogleのOpenID ConnectとTwitterのOAuth1.0を利用しました。

Google / OpenID Connect の場合

  1. 外部サービスのログイン後に、APIサーバにリダイレクトする
  2. リダイレクト時のパスに含まれるコード値から、id_tokenを取得する外部APIを実行する
    1. 取得したid_tokenのsubがユーザテーブルに登録されていれば(未登録の場合、新規登録)、SessionにユーザIDを設定して、認証済みのユーザとする

Twitter / OAuth1.0 の場合

  1. 外部サービスのログイン後に、APIサーバにリダイレクトする
  2. リダイレクト時のパスに含まれるoauth_token, oauth_verifierから、アクセストークンを取得する外部APIを実行する
  3. 取得したトークンのuser_idがユーザテーブルに登録されていれば(未登録の場合、新規登録)、SessionにユーザIDを設定して、認証済みのユーザとする

フロントエンド

React

初回ロード時に、ユーザ情報取得のAPIを実行します。
認証済みの場合のみ、ユーザ情報を取得できます。
取得したユーザ情報を、UserContext に設定して他のコンポーネントから参照できるようにしています。

App.js
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の参照例(該当箇所のみ抜粋)
ログイン状態に応じて、プロフィール欄とログインボタンの表示を切り替える

Header.js
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をの設定をします。

base-service.js
export default axiosBase.create({
  baseURL: Constant.API_ENDPOINT,
  headers: {
    'Content-Type': 'application/json',
  },
  withCredentials: true,
  responseType: 'json'
});

バックエンド

認証状態をチェックするデコレータ

今回は認証用のライブラリを利用せずに自前で実装しました。
Sessionに user_id が設定されている場合、認証済みとしています。

authentication_required.py
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を呼び出すことができる

user_api.py
@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な配色にしました。

エフェクトについて

当初は、回答時のアニメーションにエフェクトをつけることは予定していませんでした。

しかし、アニメーション実装後に(エフェクトなしで)プレイしてみたところ、爽快感や気持ちよさをあまり感じませんでした。

そこで、正解時に花火のようなエフェクトを設定しました。
多少なりとも、爽快感を感じるようになったと思います。

animation2.gif

エフェクトのアニメーションはSVGのanimate, animateMotion, animateTransformを利用して実装しました。
CSSの Key frame と異なり、動的にアニメーションの時間や変化量、角度を設定できます。

実際のソースは以下で確認できます。
https://github.com/threeislands/automaton-app/blob/master/frontend/src/components/ClearEffect.js

リリース後の反応

リリースして、友人や知り合いなどだいたい20人ぐらいの人にプレイしてもらうことができました。

ポジティブなフィードバック

  • 面白い
  • 楽しかった

エンジニアやゲームが好きな人、数学好きな人からは好意的なフィードバックをもらうことが多く、とても嬉しかったです。
また、レベル7や8(結構むずかしい問題)でもあっさりとクリアしてしまう人がいたことには、とても驚かされました。

どちらかというとネガティブなフィードバック

  • 難しすぎる
  • 意味分かんない!
  • 説明は理解できたけど、どうやっていいかわからない
  • 出会い系サイトっぽい

まず、プレイするにあたって理解しておくルールが多いため、ユーザの認知的な負荷が大きくなってしまいました。
改善案として、インタラクティブなオンボーディングを導入することを検討したいです。(ユーザの操作ごとに説明文が更新される等)

出会い系サイトっぽいという意見はちょっと意外でした。笑
素敵なオートマトンに出会えるので、ある意味出会い系サイトかもしれません!?

感想

今回、初めて個人でサービスを開発してリリースまでおこなうことができました。

自分の開発したサービスをユーザが利用してくれることはとても嬉しく、フィードバックからも多くの洞察を得ることができました。

今回作成したサービスは、自分の作りたいサービスでしたが、次回はユーザの潜在的なニーズに合致するサービスを作りたいと思います。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1