Twitterでフリー画像を投稿できるWebサービスをリリースしました【個人開発】

こんにちは。けいご（@hx_kei)と申します。

一年半前から未経験からWebを勉強して大阪でバックエンドエンジニアとして働いています。

半年前からGo言語を勉強し始めて、いい感じのWebサービスができたのでちょっとまとめてみました。

#作ったもの
[フリー画像投稿アプリpopular](https://popular-32pe64nwja-an.a.run.app/)

Twitterでフリー画像を投稿できるWebサービスです。
![ezgif.com-gif-maker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574607/32a5d594-32c0-05f6-d740-f43531372e6f.gif)
スマートフォン使用を想定しているため、レスポンシブ対応にも力を入れました。
![unnamed.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574607/085ac9ff-1758-caf8-1cd7-e6ca1b521e13.jpeg)


<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Twitter APIでついに画像アップロード＋ツイートできたー！😁<br>画像のエンコードで詰まったのがきつかった。。<br>完成したので、Cloud Run + Firestoreでまた公開します😊<a href="https://twitter.com/hashtag/%E9%A7%86%E3%81%91%E5%87%BA%E3%81%97%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%81%A8%E7%B9%8B%E3%81%8C%E3%82%8A%E3%81%9F%E3%81%84?src=hash&amp;ref_src=twsrc%5Etfw">#駆け出しエンジニアと繋がりたい</a><br><br>API＋「プログラミング」画像の送信 <a href="https://t.co/N8IZ4KFRQF">pic.twitter.com/N8IZ4KFRQF</a></p>&mdash; けいご/バックエンドエンジニア (@hx_kei) <a href="https://twitter.com/hx_kei/status/1407150167858442261?ref_src=twsrc%5Etfw">June 22, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><br>firestoreはこれ見たらすぐ理解できた。<a href="https://t.co/Nqh0VsusWO">https://t.co/Nqh0VsusWO</a><br><br>残りやること<br>1.スマホで動作確認<br>2.qiita記事作成<br>3.SPA化(Reactやる必要あり)<br><br>3は厳しいけどもうひとがんばり😤<a href="https://twitter.com/hashtag/%E9%A7%86%E3%81%91%E5%87%BA%E3%81%97%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%81%A8%E3%81%A4%E3%81%AA%E3%81%8C%E3%82%8A%E3%81%9F%E3%81%84?src=hash&amp;ref_src=twsrc%5Etfw">#駆け出しエンジニアとつながりたい</a> <a href="https://t.co/mJpNOcGaar">pic.twitter.com/mJpNOcGaar</a></p>&mdash; けいご/バックエンドエンジニア (@hx_kei) <a href="https://twitter.com/hx_kei/status/1407585337778442243?ref_src=twsrc%5Etfw">June 23, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#GitHubリポジトリ
READMEからDockerでも動かせるようにしてます。
設定かなり面倒ですが...😅
スターいただけたら嬉しいです！

https://github.com/keigooba/popular

#なぜ作ったか
ユーザー層の広いTwitterで何か作れないかと考えていたところ、美男子や美女が自撮りを投稿するだけでやたらといいねをもらえるという事実から着想を得ました。
悪くいえば「なんで大したこと書いてないのにそんな「いいね」もらえるんだ！ずりぃ！」という感じです笑
しかし、それは良い写真さえあれば多くのユーザー層にリーチできると感じ、調べたところ日本語仕様でのそういった写真投稿サービスがなかったので、作成してみることにしました。

コンセプトとしてはフリーで登録されている画像を投稿していいねやフォロワーを集めて人気者を目指そう！というコンセプトです。
自分で撮った写真ではない点が賛否分かれるところではありますが...きれいな景色やかわいい動物を気軽に投稿できるので、自分が良いと思ったものをフォロワーと簡単に共有することもできます。
検索すればどんな画像でも大体でてくるので、Twitterの運用目的での使用ができることも意識しました。

また、個人開発で有名な方が「自分で使わないサービスは使われない」と言っていたので、
今後も自分が使っていきたいと思えたサービスだったので作成しました！

#使った技術
##バックエンド
###Golang
今スタートアップでよく採用されているGo言語です。
バイナリファイルで動くので処理速度が高速です。PHPとの処理速度の違いに愕然としました。

APIの取得や並列処理を柔軟に行えるので、今後も使われるかと思っています。
個人的にも色々できるので、使っていきたいと思ってます。
今回はTwitterAPIの認証にoauth、ツイート・画像アップロードにanakondaを使用しました。

##フロントエンド
###Bootstrap4
レスポンシブ対応・グリッドシステムの利用に使用したcssのフレームワークです。
標準でcssも組まれているのでめっちゃ便利です。
フロントはこれと素のcssで書いたことしかないのでSass等の利用も検討したいです。

最近[Bootstrap5](https://getbootstrap.jp/)もリリースされたみたいです。
(ついにjQueryの利用がなくなったらしいです。）

###jQuery
シンプルにJavaScriptコードが記述できるJavaScriptライブラリです。
世間の評価はあまり良くないですが、やはり直感的に理解できてググれば大体実装できるので、すごく便利です。

DOM操作やAjax通信も簡単に記述できるので、色々アイデアが湧いてやってて一番楽しかったです。
しかしデメリットも多分にあるので、今後はVueやReactで実装していきたいです。
Pixabay APIの取得・ポップアップの表示・文字判定などに利用しました。

##インフラ
###Docker
今までGoは設定が簡単なので自分のOS上で動かしていましたが、どうせならDocker上で動かした方がいいだろうと思い利用しました。
それは結果的に大正解で自動ビルドしてくれるライブラリ[fresh](https://tsujitaku50.hatenablog.com/entry/2017/10/30/232659)に気づけたことや
ふわっと理解していたDockerfileの書き方、Dockerの運用方法について理解を深めることができました。

Dockerfileはまず[ドキュメント](https://docs.docker.com/engine/reference/builder/)を読むのが一番わかりやすそうです。
自分のOSを汚すことがなくなるため、今後も利用していきたいです。

###Cloud Firestore
いつもRDBでNoSQLで運用したことがなかったので、はじめて使用してみました。
auto incrementがうまく使えないという点はありましたが、データ保存する用途としては問題なく使えそうです。

また、一日あたりの読み取りに5万・書き込み及び削除に2万のオペレーションが無料枠で扱えるので、よっぽどで無い限り実運用でも無料で収まるのでは？と感じました。
お問い合わせデータの保存に利用しました。

###Cloud Build + Cloud Run
Cloud RunはGCPで用意されているフルマネジドなサーバレス実行環境です。
Cloud Run実行用に作成したDockerImageをデプロイすることでアプリケーションを実行できます。
そこにCloud Buildを利用して自動デプロイを構築することでGithubにmergeされた時点で更新されます。
実行ファイルのcloudbuild.yamlに自動テストを組み込めばCI/CDパイプラインといえるのですが...要修正ですね。

コード実行に対してのみ課金が行われ、一ヶ月あたり200万リクエストの無料枠がある（ちゃんと考えると200万リクエストの無料ってすごい！)ので、コード実行の高速なGoとも相性が良く、スタートアップでCloud Run + Firestore(Datastore)が採用される理由が理解できました。
今後Goを扱うときには積極的に利用していきたいです。

##その他
###[fontawesome](https://fontawesome.com/)
無料で使えるアイコンライブラリです。
アイコン使うときには毎回利用します。

###[かわいいフリー素材集 いらすとや](https://www.irasutoya.com/)
home画面の画像に利用しました。
はじめて利用しましたが、かわいいフリー素材が多いので、色々な用途でも扱えそうです。

#作成フロー
##Cloud Runの構築
経験上インフラ構築は先にやっておいた方が問題を摘出しやすいので、[超簡単。GCPのサーバーレス環境Cloud RunにGoアプリを自動デプロイ！](https://www.apps-gcp.com/deploy-go-app-to-cloud-run-by-cloud-build/)を元に[Cloud RunにGoアプリを自動デプロイ](https://qiita.com/wMETAw/items/48247ebc55f8aa1febce)でHello Worldをデプロイしました。
これについては大枠は理解しましたが、ぶっちゃけコピペしました。
しかし、設定ファイルが読み込めずpanicを起こしたり、環境変数の設定が必要になったので、Dockerfileに色々と追記しました。

```
FROM gcr.io/cloud-builders/gcloud:latest
WORKDIR /go/src
COPY . .
```

COPYを追記することでgo/src以下にルートディレクトリをコピーし、設定ファイルをルートディレクトリ上に作成しました。
環境変数の追記は[CloudBuildで設定した環境変数をDockerに渡す](https://qiita.com/kshibata101/items/0b31641b81d70cfb4ff0)が参考になりました。
実際のコードがこの辺りです。
https://github.com/keigooba/popular/blob/master/Dockerfile

##デザイン作成
デザインの作成にAdobe XDを利用しました。
[作成pdf](https://drive.google.com/file/d/1aN7H0yeMa71gYomcYdF7r-ptBRbgj00s/view?usp=sharing)
![ezgif.com-gif-maker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574607/10ebb7f2-7e1b-a346-05f0-ad7f4af2f71e.gif)

実運用を想定したWebサービス作成時には毎度作成しています。
デザインセンスは皆無なので、ツイッター通話アプリ[Twicall](https://twicall.net/)を参考にさせていただき、大枠のレイアウトを作成しました。

##Dockerによる開発環境の構築
[grpc](https://grpc.io/)に取り組んだときに[goでgRPCの4つの通信方式やってみた(Dockerのサンプルあり)](https://qiita.com/tomo0/items/310d8ffe82749719e029)のDockerfileが使えそうだったので参考にしました。
Dockerへの環境変数の追記は[docker-compose.ymlで.envファイルに定義した環境変数を使う](https://kitigai.hatenablog.com/entry/2019/05/08/003000)を参考に.envファイルを作成しました。
今回imageはstretchで作成しましたが、alpineの方が軽量っぽいのでまた挑戦したいです。

##Twitter認証
認証の実装は下記の記事を参考にしました。
[Go言語でGoogle,Twitter,FacebookのOauth認証をしてメールアドレスを取得するまで](https://qiita.com/wheatandcat/items/fe66c7ee2521a6966505)
流れとしては秘密鍵とシークレットキーを作成してTwitterAPIを利用し、リクエストトークンとトークンシークレットキーを返り値で受け取り、認証済みをoauth_verifierで判定します。

```
at, err := twitter.GetAccessToken(
    &oauth.Credentials{
        Token:  sess.Get("request_token").(string),
        Secret: sess.Get("request_token_secret").(string),
    },
    r.FormValue("oauth_verifier"),
)
if err != nil {
    log.Println(err)
}
```

認証について網羅的に記載してあり非常に参考になりました。ただGoのFW「beego」で書かれていたため、セッション回りを書き換えるのに少し苦労しました。

[goでWebサービス No.6(cookieとsession)](https://zenn.dev/tomi/articles/2020-10-24-go-web6)を参考にし、セッション保存をする仕組みを学びました。
偶然ですが、曖昧に理解していたセッションとcookieの違いについて学ぶいい機会になりました。

```
var GlobalSessions *session.Manager

func init() {
	// セッションの設定
	GlobalSessions, _ = session.NewManager("memory", "gosessionid", 3600)
	go GlobalSessions.GC()
}
```

この処理によって期限付きセッションの利用ができます。
実際のコードがこの辺りです。
https://github.com/keigooba/popular/blob/master/lib/twitter/main.go

##画像取得
[【JavaScript】「Pixabay API」を使ってフリー画像の検索機能を作ってみた](https://www.whizz-tech.co.jp/5449/)から画像取得を行いました。
Goで取得することも検討しましたが、ユーザー側の使いやすさを考えるとjQueryで実装する方が良いと思い、様々なDOM操作を追加しました。
DOM操作で追加したものにもイベントを持たせることができる[【jQuery入門】on()によるイベント処理の使い方まとめ！](https://www.sejuku.net/blog/38774)は非常に参考になり、工夫次第で様々なことができそうだなと感じました。

```
$('.text_set').on('input', 'textarea', function () {
  var count = $(this).val().length;
  $(".tweet_circle").text(count);
  if (count > 140) {
    $(this).addClass("border-red");
    $(".tweet_button").prop('disabled', true);
    $(".tweet_button").css('background-color', 'rgb(169, 169, 169)');
    $(".tweet_circle").css('border-color', "red");
  } else if (count > 110){
    $(this).removeClass("border-red");
    $(".tweet_circle").css('border-color', "rgb(247,173,33)");
    $(".tweet_button").css('background-color', '');
    $(".tweet_button").prop('disabled', false);
  } else {
    $(this).removeClass("border-red");
    $(".tweet_circle").css('border-color', "");
    $(".tweet_button").prop('disabled', false);
    $(".tweet_button").css('background-color', '');
  }
})
```
この処理では.text_setに追加したtextareaの文字数を判定し、110文字を超えると文字数判定の色を黄色に140文字を超えると赤にし送信ボタンをdisabledによって押せないようにしています。
実際のコードがこの辺りです。
https://github.com/keigooba/popular/blob/master/app/views/templates/layout.html

##ツイート+画像アップロード
調べてみると画像アップロードにはanakondaを使用した方が手軽と書かれていたため[情弱だがGoで画像付きツイートしたい](https://qiita.com/stenpel/items/01f6ef20c4691564faa5)を参考にしました。
GoでTwitterAPI使うならanakondaを使うのがポピュラーみたいです。
全部書き換えるのは手間だったのでツイート+画像アップロード部分のみ導入しました。

ここの画像をbase64エンコードする部分でうまくいかず結構萎えましたｗ
下記記事を見るとルートディレクトリのどこかに一度落としてから呼び出していたため、その方法でやってみることにしました。
https://github.com/abraham/twitteroauth/issues/572

```
url_img := r.FormValue("img")
// 拡張子取得
e := filepath.Ext(url_img)

response, err := http.Get(url_img)
if err != nil {
    log.Println(err)
}
defer response.Body.Close()
file, err := os.Create("save" + e)
if err != nil {
    log.Println(err)
}
io.Copy(file, response.Body)

// このままではヘッダーが含まれていない？ので再度開く
file, _ = os.Open("save" + e)

fi, _ := file.Stat() //FileInfo interface
size := fi.Size()    //ファイルサイズ
file_data := make([]byte, size)
file.Read(file_data)

// 画像のアップロード
base64String := base64.StdEncoding.EncodeToString(file_data)
media, err := api.UploadMedia(base64String)
if err != nil {
    log.Println(err)
}
// すぐに削除
if err := os.Remove("save" + e); err != nil {
    log.Println(err)
}
```

[#golang 画像ファイルをbase64 encode/decode するには](https://kwmt27.net/index.php/2013/09/29/golang-base64-encode-decode/)を参考にhttp.Get()で画像を取得し、拡張子を合わせてsaveファイルを作成します。
その後、再度ファイルを開き、base64エンコードすることで無事画像アップロードされました。saveファイルは必要ないためすぐに削除しています。
実際のコードがこの辺りです。
https://github.com/keigooba/popular/blob/master/app/controllers/twitter/tweet.go

##Firestoreの実装
主要機能にDB保存の必要性はなかったので、お問い合わせ部分に導入してみました。
[Cloud Firestore を使ってみる | Firebase](https://firebase.google.com/docs/firestore/quickstart?hl=ja)で導入部分、[【第3回】Go言語（Golang）入門～Firestoreデータ操作編～](https://rightcode.co.jp/blog/information-technology/golang-introduction-firestore-data-manipulator)で操作部分のざっくりとした理解を得ました。
色々したいのであれば、ドキュメントをじっくり読む必要がありそうです。
今回auto incrementをしたかったのですが、あまり理解できなかったため自前で下記の処理を作成しました。

```
iter := client.Collection("ContactList").Documents(ctx)
count := 1 //autoincrement分 +1しておく
for {
    _, err := iter.Next()
    if err == iterator.Done {
        break
    }
    if err != nil {
        log.Printf("繰り返しに失敗: %v", err)
    }
    count++
}
// 文字列変換
str_count := strconv.Itoa(count)
<中略>
// データ登録
_, err = client.Collection("ContactList").Doc(str_count).Set(ctx, contact)
```

コレクションのContactListをすべて取得し、for文によってカウントを取ります。
その後文字列に変換しDocで名前をつけます。
![スクリーンショット 2021-06-27 14.42.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574607/c080cec3-232a-b0f9-9855-a96365424206.png)

これによりauto incrementを実現し、Get()を使用した単一のドキュメント内容の取得も可能になります。
実際のコードはこの辺りです。
https://github.com/keigooba/popular/tree/master/app/models

#どれくらいの期間で作ったか
18日で完成しました。作業時間は約80時間。

まだまだ奥が深いですが、段々Goにも慣れてきました。
次はSPAに挑戦し、マイクロサービスとして利用を試みたいです。

#今回の振り返り
途中[grpc](https://grpc.io/)や[echo](https://github.com/labstack/echo)の導入を試みていたのですが、色々と詰まる箇所が多く結構時間を取られた上に断念しました。
勉強にはなりましたが、今回は「作りたいサービスを早くリリースすること」が目的だったため、手間がかかると感じたら早めに折り合いをつけてリリース後導入を検討する方が良いかと感じました。

今後はスピード感を最重要視してリリースを試みたいと思います。

#今後追加したい機能
・SPA化
・日時指定のオートツイート機能
・登録SNSすべてに同時ツイートできる機能

#さいごに
ぜひ一度[popular](https://popular-32pe64nwja-an.a.run.app/)使ってみてもらえたら嬉しいです！

よかったらポチッとLGTMもよろしくお願いします🙇🙇🙇

ここまで読んでくださりありがとうございました！

(現在転職活動中です。ご興味持っていただけましたら@hx_keiまでご連絡下さい。）