yn2011's blog

技術メモ

TypeScript, Go, Docker Composeに入門するために約半年間かけてWebサービスを構築した話

この記事はチームスピリット アドベントカレンダー2019の22日目の記事です。

今年(2019年)の7月から12月にかけて約半年間、プライベートでコツコツ開発していたWebサービスが一応形になった。技術的な内容は別の記事で書く予定なので、今回は半年間を通じて感じられた個人開発という営み自体の難しさや継続のコツについて書いてみる。

作ったもの

これ何

  • ブログのような何か
    • 認証認可、記事一覧、投稿、削除ができる

構成

https://raw.githubusercontent.com/pokuwagata/portfolio-go-mysql-docker-webapp/master/docs/imgs/system-overview.png

モチベーション

  • 0からWebサービスを構築してみたい
  • (業務ではSalesforceを使っているので)たまにはSalesforceから離れたい
  • TypeScriptやGoで、それなりの量のコードを書きたい

なお、ブログサービスは題材として良さそうだっただけで、サービスとして何かやりたいことがあったわけではない...

所感

実際にやってみて感じたこと

時間確保の難しさ

  • 平日の日中は仕事があるので、平日の朝・夜か休日に開発
    • とにかく15分でもいいから毎日確実に作業することが凄く大事
      • 休日だけ開発をすると、始める際に「先週何をしていたか思い出す」負担が大きくて効率が悪い
    • 意外と良かったのが昼休みに開発することで、仕事モードのまま開発を始めると凄く集中力が高い状態で作業できる。昼休みはご飯も食べるので30分ぐらい開発できれば良いほうだが1,2コミットぐらいは確実にできたので十分良かった
  • 休日は自宅で開発するよりはカフェへ移動してしまった方が開発を始めやすい

モチベーション管理

  • 平日に仕事でもコードを書いて帰宅後もコードを書くのは気分的に辛い場合もあった(逆もあって、仕事で全然コードを書かない時期は個人開発が楽しかった)
  • Done is better than perfect. 大事
    • 個人開発は締め切りがない。いくらでも時間をかけられる
    • 自分は個人のモチベーションの絶対量には限界があると思っていて、細部に拘っている内に疲れて、最終的には開発自体に飽きてしまう(少なくとも自分はそう)
    • 特に自分の場合はCSS、Clean Architecture、Goらしい書き方、あたりに深入りしすぎる傾向があった
  • 日々の作業の区切りは、やり切る一歩手前ぐらいにする
    • 例えば、あとこの部分を修正したらコミットだなあと思ったら、そこを残しておいてその日は作業を切り上げる
    • 何をすればいいか明らかだと、別の日でも作業を始めやすい
    • たしか村上春樹職業としての小説家に書かれていたテクニックだった気がする
  • twitterで細かめに進捗を書くのもモチベーション高められたと思う(なお別に反応が貰えるわけではない)

開発の進め方

  • 機能定義→画面設計→テーブル設計→画面のコンポーネント設計→バックエンドのAPI定義→実際にコード書く
    • コードを書くまでの流れはよくある感じ
    • APIはswagger使ってきっちり定義しようともしたがswaggerの記法ルールとかバージョン互換がけっこう難しかったし、そもそもシンプルなAPIなので途中で諦めて開発しながら考えることにした(それであんまり問題なかった)
  • メモを取りまくる
    • 開発していて、「あれやらないとな」とか「これどういう意味なんだろう」とかすぐにやる必要がないタスクを思いついたら全てメモするように意識していた
    • 次にやらないといけないこと等もとにかく言語化してメモしていた
      • これは次に作業を始めるときにとても役立つし、全体の進捗を俯瞰して見れるので良い

実際には思うようにいかなかったこと

  • とにかくまずは空のページで良いので公開できることを目指した
    • が、現実にはDocker Composeをそのまま利用して外部にWebサービスを公開する手段はあまり確立されていない(というかメジャーじゃない)っぽくて調査や実験に相当な時間がかかって最終的には後回しにしてローカルだけで開発を続けた(辛すぎてモチベーションが枯れそうになったので)
    • そもそもDocker Composeは開発環境だけで使うものだよねって認識の人が多いということを初めて知ったりとか
  • 本当はDevOps周りきちんとしたかった
    • GithubにアップロードするとCycleCIでテストして自動的にデプロイしてくれるのを夢見ていた
    • CycleCIでテストとESLintで検査する部分までは、わりと開発初期に整備できていた
      • が、開発中盤まではGoのテストコードを書いていなかったしESLintの指摘も無視していたので常にBuildバッジは赤だった(残念)

ものを作ることの重要性

  • 技術書を読んだり、チュートリアルを写経したりするのも大事だが、何かを実際に作らないと分からないことが沢山ある
    • 例えばClean Architectureも実装してみて初めて気づくこと・理解できること・疑問に思うことも多い
    • あとWebサービスのようにフロントからインフラまで1人で開発していると、自分が疎い分野も明らかになるなと思った
      • 例えば自分はDBが弱くて、どういうSQLを書けばインデックスを効かせられるのか、インデックスが効いていることをどう調べて評価するのか、そもそもインデックスの仕組みとはみたいなところに興味を持つきっかけにもなった

今後

  • 今回は使用技術の学習も兼ねていたので開発期間が長くなってしまった。今後はWebサービスを素早く立ち上げるためのベストプラクティスを自分の中で整理して持っておきたいなと思った(多分Railsになりそう)

border-spacingで表の行列間の余白を調整する

この前初めて使った時に少しハマったのでまとめておく。

何をしたいのか

表の行列間に余白を作りたい。↓のように行と列で別々の余白を指定したい。

f:id:pokuwagata:20191022213739p:plain

MDN border-spacingより引用

marginpaddingはダメなのか

w3cによると

all elements except internal table elements

marginは効かない模様。

paddingはtdに指定できるので、これが解決策となる場合もある。

border-spacingを使う

表の行列間の余白を指定するために存在するプロパティ。行、列を個別に指定することも可能。

table {
  border-spacing: 1em .5em;
  border-collapse:separate;
}

border-collapse: collapseだとborder-spacingは効かないので注意。

border-spacingで表の位置がずれる問題

border-spacingは行間、列間だけでなく周りにも適用されるので、表の表示位置がずれる問題があるが、nagative marginで解決できる。

table {
  margin-top: -0.5em;
  margin-left: -1em;
}

参考

Can't perform a React state update on an unmounted component... エラーにConetextが原因でハマった話

環境

  • react 16.8.6

事象

React.useEffectを使用しているコンポーネントContext.Provider配下に配置したところ以下のエラーが発生した。

Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function

コード例は省略するが、上記のエラー発生時にReact.useEffect内でAPIコール、その結果をsetState等すると何故かuseEffectの処理が無限ループする。

原因

本来はfunctional componentの外側でContextを定義するべきだが、以下のようにfunctional componentの内側で定義していた。

export const HogeContextProvider = (props: HogeProps) => {
  const HogeContext = React.createContext(null);
  return (...

以下が正しい。

export const HogeContext = React.createContext(null);

export const HogeContextProvider = (props: HogeProps) => {
  return (
      <HogeContext.Provider value={hoge}>
        {props.children}
      </HogeContext.Provider>
  );

functional componentの内側は、stateの更新等で何度もコールされるのでその度にContextを再作成していたことが原因の模様。

最初はContextをProvider配下でReact.useContextしていないことが原因かなと思って調べたら、そもそもContextをexportしていないことに気づいて、定義位置の間違いに思い当たった。

反省

DOMがstateに対応して更新されることは当然だが、functional componentの内部の処理が何度もコールされるイメージがなかった。どういうときにコールされるのか整理したい。

React、こういう単純なミスの原因特定が難しくてハマると解決に1,2時間ぐらいは必要になってしまうこともあり辛さがある。

Go modules利用時にvscode-goのinferGopathが無効化される理由

環境

事象

The "inferGopath" setting is disabled for this workspace because Go modules are being used.

原因

  • Go modulesを使用している場合は$GOPATHの設定が不要だから
  • Go modulesを使用する利点の1つは$GOPATHを通さず、$GOPATH配下外に作成されたディレクトリ上で外部の依存パッケージを扱えること
    • しかし、Goのimport$GOPATH/src$GOROOTからパッケージを探す仕様だったはず...
      • ちなみにgo.modが直下になるように$GOPATHを設定すると$GOPATH/go.mod exists but should notとエラーが表示される
    • 自分で定義したパッケージをimportするときはどうすればいいのか?

Go modules使用時に自分で定義したパッケージをimportしたい

その他

  • VSCodeでこの手の問題が発生すると、実際にGoが認識している$GOPATHVSCodeが認識しているものが違う場合もあり調査に手間がかかりがち(ちなみにVSCodeのコマンドパレットからgopathで検索するとVSCodeが認識ている$GOPATHが確認できることを今日初めて知った)
  • でもこれからはGo Modulesを使えば$GOPATHのことはあまり考えなくてもよくなるはず。めでたい。

使用しているAlfred Workflows一覧

環境

  • Alfred 4.0.3

Alfred Powepackを利用している場合は2台のMacでAlfredの設定を同期することも可能(ドキュメント)だが、1台からの片方向同期かつWorkflowsだけを同期するのは難しそうなので、業務で使用するMacとは手作業で同期を行うことにしようと思った。(JIRAチケットへのジャンプ等、業務でしか使わない設定も少なくないので...)

今後も良さそうなWorkflowsがあったら、このドキュメントに追加していく。

Workflows

github.com

github.com

github.com

github.com

(自作)フォルダを検索してターミナルで開く

  • filefilter(public.folder) → Browse in Terminal

Docker Compose上で起動したwebpack-dev-serverに設定されたプロキシが動作しなくてハマった話

環境

  • macOS 10.14.4
  • docker-compose version 1.23.2
  • webpack-dev-server 3.7.2
  • webpack 4.37.0

フロントエンド(webpack-dev-server)、バックエンドをそれぞれDocker Compose上にコンテナとして起動して開発していた際にハマったお話。

事象

  • webpack-dev-serverはホストするjs内で発行されるHTTPリクエストをプロキシすることができる

    • 今回なぜプロキシが必要だったかというと、Chromelocalhost上ではfetch('http://localhost')...のようにlocalhostに対してリクエストを行うとCORSエラーが発生するため
    • jsのソースコード上ではfetch('/api/hoge')..のようにしておいて、webpack-dev-serverにlocalhostへプロキシさせることによって回避できる。(他にもブラウザの設定を変更する等の回避策があるが、webpack-dev-serverに設定されている方が他の人に親切)
  • webpack.config.jsを以下のように設定(抜粋)

    • APIサーバーは5000番ポートを開いているとする
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    compress: true,
    port: 8080,
    historyApiFallback: true,
    proxy: {
      '/api': 'http://localhost:5000',
    }
  • 上記設定を反映したwebpack-dev-serverを起動し(docker-compose up)、実際にlocalhost:8080にアクセスしてfetch('/api')...をブラウザから実行すると、webpack-dev-serverが以下のエラーを出力した
[HPM] Error occurred while trying to proxy request /api from localhost:8080 to http://localhost:5000 (ECONNREFUSED) (https://nodejs.org/api/errors.html#errors_common_system_errors)
  • httpレスポンスは以下(抜粋)
504 Gateway Timeout

Error occured while trying to proxy to: localhost:8080/api

原因

  • Docker Compose上のコンテナが参加するネットワークでは、docker-compose.yml上のサービス名で名前解決が行われるため。

  • 従って、webpack.config.jsを下記のように書き換えると上手くいく

...
 proxy: {
      '/api': 'http://api:5000',
    }
  • なお、docker-compose.ymlでは以下のようにサービス名をapiとしている
version: "3.4"
services:

  api:
    build:
      context: ./api

...

敗因

  • 書いてみるとこれだけのことだが、多分2,3時間ぐらいハマっていた
  • サービス名で名前解決ができること自体はDocker Composeのドキュメントに書いてあることだし、以前から知っていた。なぜ気づけなかったのか...
    • 多分だが、ブラウザからDocker Compose上のwebpack-dev-serverにアクセスする際は、
      • localhost:8080をDocker Composeのポート8080と接続していて
      • かつ実際にクライアントサイドのjsが動作しているのがローカルだし
      • バックエンドのapiサーバーもlocalhost:5000とDocker Compose上の5000を接続していた
    • のでwebpack-dev-serverが動作しているのがDocker Compose上であることを忘れていたというか、しっかり認識できていなかったような気がする。
    • また、Docker Compose上のネットワーク内のコンテナ間もlocalhostで通信できるようなイメージだったのも原因かもしれない。
  • あと、ステータスコード504 Gateway Timeoutの理解が不足している感もある。webpack-dev-serverがlocalhost:5000が存在しないためにレスポンスを得られない→ポート5000で動作しているはずのHTTPサーバーのログを確認する→ログがない→接続先がおかしい?というような手順をスムーズに踏めたら良かった。
    • Goで書いているHTTPサーバは外部ファイルへのログ出力を未実装で、結局調査のために書いたんだけど、ここを面倒くさがらずに早く実行するべきだったかな...

github.com

bashコマンドは読取権限のあるファイルなら処理できる

環境

Permission denied

  • $ touch hoge.sh && ./hoge.shするとbash: ./hoge.sh: Permission deniedになる
    • 実行権限がない
  • しかし$ touch hoge.sh && bash hoge.shは正常終了するのはなぜか?

読取権限

  • $ chmod u-r hoge.shして所有者の読取権限を剥奪するとbash hoge.shbash: ./hoge.sh: Permission deniedとなる
  • bashコマンドはファイルから読み取った内容をbashコマンドが実行しているのに対して、.コマンドはファイルから読み取った内容を実行する際にOSの実行権限が必要になっている模様(推論、あまり詳しくないので違っていたら教えてください)