呼吸を管理するサービス

健康管理は大事ですね。僕は日中プログラミングの仕事をし、仕事が終わってからも趣味で開発をしています。11月に子どもも産まれ、子育てをしながら、仕事も更に頑張らなきゃととにかく奮闘しています。

そんな中で、ついつい健康管理がおざなりになってしまうことは多いのではないでしょうか。家族を守るため、仕事だけではなく健康管理もしっかりしなくちゃ、ということで健康管理系のサービスを作ってみました。呼吸を管理するサービス、その名も「呼吸エディタ」です。

## サービスの詳細

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136506/a95110f3-b752-9f30-9667-09f1a015be6e.png)

自分の考えた呼吸法を投稿しておけるサービスです。

下記のようにぱぱっと入力して登録することができます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136506/9073341d-04df-e26e-695b-5f47f7f9a772.png)

良いものができたらSNSにOGP画像つきでシェアすることもできます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136506/c48419d5-54ae-d710-d379-ef1469a9d8db.png)

## 作った構成

今回はNext.jsとFirebaseで作りました。Vercelにお手軽デプロイしています。デザインはBootstrapです。

### Next.js

Next.jsはReactを用いたフレームワークです。Next.jsとVercelは同じ開発元で、非常に親和性が高いです。GitHubにプロジェクトをpushしてあればそのままデプロイすることができます。

ReactベースのためTypeScriptとの親和性も高く、とにかく半分寝てても実行しなくても開発できるので非常に楽です。

フロントエンドだけでなくAPIも作ることができますので、今回のような小さなサービスの場合はNext.js単体だけで完結させてサービスを作ることもできます。

下記のようにペタペタとpagesフォルダ内にjsxを配置していくだけでページやAPIが作れてしまいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136506/2c7ac180-9ee2-822c-ccad-0429c26afe70.png)

`[id].tsx` のようにすることで、動的なURLも作れます。Next.js10以前はリンクなどを作るためにごちゃごちゃとパラメータ指定が必要でしたが、Next.js10からは何も気にせずリンクなども貼れるようになり、更に便利になりました。


### Firebase

FirebaseはGoogleの提供するサービスです。今回もデータベースとしてFirestoreを利用しています。

## 作り方

### ファイル構成

最近個人で作る時はこんな構成にしています。たとえば呼吸作成ページ `kokyu/create.tsx` の例です。

#### 状態管理

個人だともうRecoilが簡単＆楽すぎるのでRecoilばっか使っています。フォームの状態は全てRecoilのステートとして保存します。どのコンポーネントからも使い回せるよう、例えば `kokyu_edit.ts` というファイルを作ってそこに `useKokyuEdit` というカスタムフックを使い、全てそこからステートを取得したり更新したりできるようにしています。

```tsx
export default function CreateKokyu() {
  const router = useRouter()
  const { kokyu, setKokyu } = useKokyuForm()
```

とりあえずこれでステートのバケツリレーは防げますのでむちゃくちゃ楽になります。各パーツのコンポーネント化も気兼ねなく行なえます。

#### jsxとロジックを分離する

適当に書く時はjsxの記述の上辺りに色々関数を書いていくと思うのですが、それらは別にそこにある必要はないので全部 `kokyu_form.ts` というファイルに `useKokyuForm` のようなカスタムフックを作成してそちらに書きます。これでコンポーネントファイルがjsxだけになるのでだいぶスッキリします。

## オートフォーカス

今回使い勝手にこだわったところは、呼吸の型を追加するボタンを押した時に自動的に追加された入力欄にフォーカスを合わせるところです。これによりかなり楽に追加していけるようになっているのではないかと思います。おそらく。

どうやって実装するのが良いのかな、とちょっと悩んだのですが、よくよく考えると各行の入力欄は個別のコンポーネントになっているため、そこでuseEffectを使い、自分が１つ目の入力欄でなかったらフォーカスする、とするだけで可能でしたので簡単でした。

```typescript
  const inputRef = useRef()

  useEffect(() => {
    if (!inputRef.current || props.index == 0) {
      return
    }
    const input = inputRef.current as HTMLElement
    input.focus()
  }, [])
```

## 匿名認証

今回はFirebaseの匿名認証を使っています。これだとブラウザを変えたりブラウザのデータを削除した時に認証ができないのですが、こういったサービスの場合ログイン自体が利用者への障壁となり離脱の原因となってしまうことが多いため、バッサリと切りました。クソアプリですし。

## OGP用画像作成

Next.jsはAPIも使えるため、OGP用画像は別サーバーとかもなくNext.jsだけで作成しています。作成にはnode-canvasを使っています。以前はフォントなどの読み込みがVercel上でできなかったのですが、今はできるようになったため本当に楽です。下記のような感じで背景画像に文字を乗せて出力しているだけです。

```typescript
  const width = 600
  const height = 315
  const canvas = createCanvas(width, height)
  const context = canvas.getContext('2d')

  const backgroundImage = await loadImage(
    path.resolve('./images/ogp_background.png')
  )
  context.drawImage(backgroundImage, 0, 0, width, height)

  context.font = '40px mouhitsu'
  context.fillStyle = '#424242'
  context.textAlign = 'center'
  context.textBaseline = 'middle'
  context.fillText(`${kokyu.name}の呼吸`, 300, 157)

  const buffer = canvas.toBuffer()

  res.writeHead(200, {
    'Content-Type': 'image/png',
    'Content-Length': buffer.length,
  })
  res.end(buffer, 'binary')
```

## まとめ

以上になります。みなさんも健康のため、色々とよい呼吸を思いついたら登録してみてください！

良いと思う部分や参考になる部分などあれば是非LGTMをお願いします！

呼吸エディタ
<https://kokyu.appllis.net>