Go!Go!Movie!を支える技術

# これなに

2021年12月22日に個人でリリースした映画診断サイト「Go! Go! Movie!」の技術について書いてます。

| TOP | 診断 | 結果 |
|:-:|:-:|:-:|
| ![top.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/7b541bab-aff6-1337-aaa5-ad852fec10f1.png) | ![question.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/b7d051a5-839f-a17c-35af-9a1404cf2a18.png)  | ![result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/9319b05c-d765-9b80-b4d9-a5c5828363ec.png)  |

https://svelte-question.vercel.app/

いくつか質問に答えたら、おすすめの映画を紹介してくれます。

## なんで作った？

1. 先日、コーヒーサブスクの診断コンテンツがTwitterで表示されてやってみたところ、面白かったので 「Svelteで作るならどうなるだろう？」 と**衝動にかられて**
1. コロナでミニシアターが苦しんでいるので、どうせなら**映画館に足を運ぶきっかけ**になったらいいなと思って

# 構想
![構想.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/f641e7f7-0c45-9dcc-757b-92d39db4b986.jpeg)
ノートにばーっと書いたら、ポイントは2つ

- **質問/結果のノード**
- **回答を次の質問のノードに関連づける**

で作れそうと分かりました。

# Go!Go!Movie!を支える技術
## [TypeScript](https://www.typescriptlang.org/)

TypeScriptの型を記述することで、先に述べた2点を表現できて開発しやすかった

> - **質問/結果のノード**
> - **回答を次の質問のノードに関連づける**

<details><summary>**ノードの型のコード**（開くならクリック）</summary><div>

```ts:ノードの型
type NodeType = 'question' | 'result';

type BaseNode = {
  id: number;
  nodeType: NodeType;
  text: string;
  imagePath: string;
}

type Answer = { text: string; nextNodeId: number; };

/** 質問ノード */
export type QuestionNode = BaseNode & {
  answers: Answer[];
}

/** 結果ノード */
export type ResultNode = BaseNode & {
  url: string;
}

export type Node = QuestionNode | ResultNode;
```
</div>
</details>

## [Svelte](https://svelte.dev/)

- Svelteの魅力の一つは、**storeを簡単に扱えること**
- 今回の診断サイトでは回答するだけでなく戻ることもできるため、`nodeHistory`というカスタムストアを作成して処理を集約しました

<details><summary>**nodeHistoryのコード**（開くならクリック）</summary><div>

```ts:nodeHistory.ts（カスタムストア）
import { get, writable } from "svelte/store";

/** カスタムストアを作成する */
const createStore = () => {
  const INITIAL_VALUE = [1];
  const store = writable(INITIAL_VALUE);

  return {
    subscribe: store.subscribe,

    /** 現在のノードIDを取得 */
    current: () => {
      const value = get(store);
      return value[value.length - 1];
    },

    /** 回答したら次のノードIDをセットする */
    next: (nextNodeId: number) => store.update(n => [...n, nextNodeId]),

    /** 戻るときに最新のノードIDを削除する */
    back: () => store.update(n => n.slice(0, -1)),

    /** 初期化 */
    clear: () => store.set(INITIAL_VALUE),

    /** 最初のノードか判定 */
    isFirst: () => get(store).length === 1,
  }
}

export const nodeHistory = createStore();
```

</div></details>

## [SvelteKit](https://kit.svelte.dev/) + [Vercel](https://vercel.com/)

- **Viteを使用しておりビルドが高速なため開発体験が良い**
    - 以前、書いた「[Vite （ヴィート）はなぜ早い？](https://qiita.com/mkin/items/cd4789eb6a2a2cceb3b6#vite-%E3%83%B4%E3%82%A3%E3%83%BC%E3%83%88%E3%81%AF%E3%81%AA%E3%81%9C%E6%97%A9%E3%81%84)」もよろしければ読んでください
- Vercel
    - アダプターを切り替えるだけでVercelなどに最適なビルドをしてくれる
        - SvelteKitが公式に提供している`adapter-vercel`を使える
        - [デプロイ手順は以前書いたこちらの記事を参照](https://qiita.com/mkin/items/cd4789eb6a2a2cceb3b6#3-vercel-%E3%81%AB%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%81%99%E3%82%8B)
    - 無料でアプリケーションをホストしてくれる（個人開発なら十分）
    - GitHubにプルリクだすと自動で検証環境を用意してくれるから楽

## [Tailwind CSS](https://tailwindcss.com/)

- **マークアップやったことないエンジニアでもとっつきやすい**
- スタイル書いたことなくても[tailwindcomponents](https://tailwindcomponents.com/) を参考にすれば効率はよい
- class名を考える必要がないので開発効率UP
    - 参考「[デザイナが体感してわかった「コンポーネント設計×tailwindcss」が神という話](https://note.com/miinm030/n/ne02a9a86dfcf)」
- 下記のようなアニメーションも簡単に作れます
![tailwind.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/8f3a44f6-cf58-8d1e-e4f6-2b76f1026e37.gif)
<details><summary>**アニメーションの設定**（開くならクリック）</summary><div>

1. `gogo`というカスタムアニメーションを定義する

```diff:tailwind.config.cjs
const config = {
	content: ['./src/**/*.{html,js,svelte,ts}'],

	theme: {
		extend: {
+			animation: {
+				gogo: 'gogo 1s infinite',
+			},
+			keyframes: {
+				gogo: {
+					'0%, 100%': {
+						transform: 'translateX(25%)',
+						animationTimingFunction: 'cubic-bezier(0.8, 0, 1, 1)'
+					},
+					'50%': {
+						transform: 'translateX(0)',
+						animationTimingFunction: 'cubic-bezier(0, 0, 0.2, 1)'
+					},
+				}
+			}
		}
	},

	plugins: []
};

module.exports = config;
```

2. 呼び出し側はclassに`animate-gogo`と書くだけ

```html:呼び出し側（一部省略）
<div>
	<a href="/question">
		<span>無料</span>おすすめ映画診断をする
		<svg class="animate-gogo">
			<path d="..."/>
		</svg>
	</a>
</div>
```
</div></details>

## 画像、SVGの素材

無料のMITライセンスのものを選びました

- [Lorem Picsum](https://picsum.photos/) はキレイな画像がAPIで取得できる
    - 例） [https://picsum.photos/id/20/200/200](https://picsum.photos/id/20/200/200)
- [css.gg](https://css.gg/) はSVGアイコンなど

# おわりに

- お気づきかもしれませんが、今回は質問を取得するためのAPIなど使っていません。本当はCMSとか使ってやろうかなと思ったのですが、データの流し込みが面倒そうで...
- やはりSvelteは開発しやすいのでもっと普及してほしいですね〜
- **ぜひ、みなさんも「[Go!Go!Movie!](https://svelte-question.vercel.app/)」を触っていただき、映画館（特にミニシアター）に足を運んでいただければ幸いです**:relaxed:

# Appendix
## ディレクトリ構成

- 使用技術とは関係ないですが、今回のディレクトリ構成です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/de420d8e-cf8a-2b8a-da81-f1f7c2ea75b3.png)