Next.js14でプレイリストアプリ作ってみた話

# はじめに

色々と忙しくてNext14のキャッチアップができていなかったのでそれを兼ねてちょっとしたアプリを作ってみようと作りました。
最初にハッカソンで原型を作ったためアーキテクチャはこうしたほうがよかったななどの思い入れもありますがその辺りはご愛嬌。
サイト: https://scp-log.vercel.app/
GitHub: https://github.com/yoshi-non/scp-log

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2706288/81002923-cf71-061c-e1b3-86bb1cc57a1a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2706288/f71d83aa-d4e2-c5c6-7060-188ad068dd37.png)

# アプリ概要
Youtubeの動画を検索してその動画をプレイリストに追加して再生できる一般的な再生アプリ。
オフライン環境への音声ダウンロードも可。
※プレイリストのデータは全てローカルストレージに保存されます。

# 環境
Next.js: 14.1.0 
Node: 18.17.0

## フォルダ構成
```
├ api // Next.js環境で作成できなかったapi
├ src
│ ├ app // Next.jsのルーティング
│ ├ components // コンポーネント
│ │ ├ features // 特定の機能を実現するコンポーネント
│ │ │ ├ sample // サンプルコンポーネント
│ │ │ │ ├ hooks // サンプルコンポーネントのみで使用するhook(不要な場合もある)
│ │ │ │ ├ logics // サンプルコンポーネントのみで使用するロジック(不要な場合もある)
│ │ │ │ ├ tests // テスト専用
│ │ │ │ ├ index.tsx // エントリーポイント
│ │ ├ functions // UIとして表示されないコンポーネント
│ │ ├ ui // UIコンポーネント,shadcn/uiで使用
│ ├ constants // 全体に共通の定数ファイルを配置する
│ ├ libs // ライブラリのラッパーや使いまわしやすいようにする
│ ├ types // 全体に共通の型定義ファイルを配置する
│ ├ utils // 使い回すロジックなど
```
理想としてはsrc配下にapi層のapiフォルダを作成したかったのだがデプロイするvercelの環境と使用するapiの性質からsrcと同じディレクトリに存在する。
app下のAPI Routeのフォルダ名をapiにしてしまうとvercelで使用するapiと名前が重複しまい、API Routeは動かなくなります。ルートのapiフォルダはフォルダ名をapi以外にするとvercelで動かないです。
ここではAPI Routeをapisというフォルダ名にして回避しています。
※api(apis)フォルダが複数存在するかの詳細は[関連動画検索](#関連動画検索)で説明

## shadcn/ui
shadcn/uiは使ってみたかったので導入した。
MUIみたいな「らしさ」がないのとカラーテーマの設定や作るのが面倒なResizableを簡単に実装できたりするところには感動した。ライト・ダークではデフォルトでローカルストレージにデータを保存までやってくれるのでかなり助かる。
一方でDialogは若干コードが煩雑になってしまうところだったり最近できたDrawerは部分的な幅で使えるものがあったらいいなと感じた。

https://ui.shadcn.com/docs

# 今回話すこと
- [Google Analytics](#google-analytics)
- [ドラッグアンドドロップ](#ドラッグアンドドロップ)
- [関連動画検索](#関連動画検索)
- [オフラインダウンロード](#オフラインダウンロード)

## Google Analytics
https://kt-tech.blog/blogs/t0xx4djphp

基本的に上記のサイトを使えば簡単に導入できたが開発当初はNext14.0.4であったが14.1.0にバージョンを上げる時にビルドエラーで`useSearchParams() should be wrapped in a suspense boundary`のように出てしまうので以下のようにGoogleAnalyticsタグをSuspenseタグで囲えばエラーは消える。

```tsx:layout.tsx
<head>
    <Suspense>
        <GoogleAnalytics />
    </Suspense>
</head>
```
導入も手軽で以下の画像のように初心者でもわかりやすくデータが見える。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2706288/400a01f9-d9c4-97ee-4c89-511f456d3aa9.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2706288/8cfdb440-68b8-e6a3-c200-745ea01c511f.png)

## ドラッグアンドドロップ

![640x400画面収録](https://github.com/yoshi-non/scp-log/assets/83369665/1dcb02f2-b03e-473a-9f68-4f978998b645)

React 18に対応していることと現在伸びてることを加味して`@dnd-kit/core`を採用

![image (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2706288/9e77708c-f8f9-8b0d-9a79-c36b0751e302.png)

細かい使い方は省くが簡単に言えば[DndContextWrapper](https://github.com/yoshi-non/scp-log/blob/main/src/components/functions/DndKit/DndContextWrapper.tsx)でドラッグ&ドロップを反映させたい箱を用意して[SortableItemWrapper](https://github.com/yoshi-non/scp-log/blob/main/src/components/functions/DndKit/SortableItemWrapper.tsx)で移動させたいものをラッピングして使用する。

## 関連動画検索
動画を検索して追加する分には簡単なのだがYoutube Data APIには関連動画の検索APIなるものは存在しないので自作する必要があった。

### 処理の流れ
1. プレイリストに存在する動画タイトルを形態素解析して頻繁に使われている単語を検索する
2. 頻繁に使われている単語を繋げて検索APIにかける

日本語の形態素解析には代表的に`MeCab`と`Kuromoji`があるが今回はNext.jsで作っている観点からNodeの資料が多い`Kuromoji`を採用。

### ぶちあたった問題
APIを作る段階でNext.jsのAPI Routeで`./node_modules/kuromoji/dict`というパスがvercelでは認識されないというエラーに遭遇した。調べてみるとvercelはNext.jsを用いたgzipファイルをサポートしていないということらしいのでsrcと同ディレクトリにapiフォルダを作成して無理やり動かすことにした。
ただしこれはvercel環境でしか動かないためローカル環境は別で作る必要があるためapiファイルを2つ作る必要がある。

`npm run dev` or `npm run build`で動くapiファイル

https://github.com/yoshi-non/scp-log/blob/main/src/app/apis/kuromoji/%5BMorphologicalText%5D/route.ts

`vercel dev` or 本番環境で動くapiファイル

https://github.com/yoshi-non/scp-log/blob/main/api/kuromoji/index.js

## オフラインダウンロード
やっぱりオフラインでも使えたらいいなという思いがあったのでオフラインダウンロードにも挑戦。

しかしながら以下の流れで音声だけのダウンロードしかできなかった。

1. YoutubeのダウンロードAPIには高画質(1080p)の動画と音声が合成しているファイルをダウンロードするものが存在せず動画と音声を別々でダウンロードして合成する必要がある
2. 合成にはffmpegというライブラリがあるのだが、vercel環境では`errorType":"Error","errorMessage":"spawn /var/task/node_modules/ffmpeg-static/ffmpeg ENOENT","code"`というエラー、わかりやすくいえばそんなファイル見つかんないよ。と言われる※ローカル環境は動く
3. 画質を下げればffmpegをしようせずともいけると考えたがvercelの無料枠の[Serverless Functionは10sでタイムアウト](https://vercel.com/docs/limits/overview)を起こすためそもそも無理っぽい
4. 高画質(1080p)動画ダウンロードは関連動画のようにapiフォルダに記載して実行すれば本番環境でも動くようになりましたが3のタイムアウトでサービスとしては機能してない

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2706288/747476c3-ffdf-08e3-a3b0-064857142e8b.png)

`vercel dev`で動くapiファイル

https://github.com/yoshi-non/scp-log/blob/main/api/youtube-download/index.js

# おわりに
普段ハッカソンはその場で作って終わることが多いですが今回は継続開発含めいろいろと学ぶことも多くありNext.jsの理解を深めながら触ったことのない新しいライブラリや処理を模索できて楽しかったです。ぜひ参考になったら嬉しいです。
※テストコードを書く用の設計をしましたがめんどくて書いてないです。懺悔...