【個人開発】ToDoアプリをNext.js × AWSで作成した

# はじめに

Next.js × AWS で ToDo アプリを作りました🚀
ToDo アプリを作ろうと思った理由はほかにアイデアが浮かばなかったからです。

https://todo.oizumi-yuta.com/

アカウント登録はメール認証を行っていないため、 `test@examle.com` のようなダミーのメールアドレスで可能です。ぜひ使ってみてください！

# 💻 フロントエンド

## 🔑 ログイン画面の実装

シンプルなログイン画面を作りました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/0979c181-e3b5-19e7-7dab-a460ae8c41ae.png)

工夫したところはパスワードマスクの解除です。目のアイコンをクリックすると `type` を `text` にすることでマスクを解除します。

```react
<div className={styles.formGroup}>
  <label htmlFor="password">パスワード</label>
  <div className={styles.passwordContainer}>
    <input
      id="password"
      type={showPassword ? "text" : "password"} // textにすることでパスワードマスク解除
      value={password}
      onChange={(e) => setPassword(e.target.value)}
      required
      minLength={6}
      className={styles.input}
    />
    <span
      className={styles.passwordToggleIcon}
      onClick={() => setShowPassword(!showPassword)} // アイコンクリック時ステートを更新
    >
      {/* パスワードマスクアイコン切り替え */}
      {showPassword ? <PasswordEyeIcon /> : <PasswordCloseEyeIcon />}
    </span>
  </div>
</div>
```

## ✏️ ToDo登録画面

ToDo登録画面はサイドメニューにToDo一覧を表示し、右側にタスク詳細を表示するように実装しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/4b6acfe0-a1bc-3a6c-cf53-5507c44a9586.png)

画面サイズが 950 px 以下になったとき、サイドメニューをアイコンで表示/非表示を切り替えることができるようにしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/6d64b47d-bd13-0d97-264e-5c79e8f229a4.png)

アイコンは以下を利用しました。

https://fonts.google.com/icons

画面サイズの変更イベントをリッスンするために `useEffect` で `addEventListener` を行います。

```react
useEffect(() => {
  if (userInfo.userId) {
    getTodos();
  }

  // 画面サイズのステートを更新する関数
  const handleResize = () => {
    setWindowSize({
      width: window.innerWidth,
      height: window.innerHeight,
    });
  };

  handleResize();

  // イベントリスナーに追加
  window.addEventListener("resize", handleResize);

  return () => {
    window.removeEventListener("resize", handleResize);
  };
}, [userInfo.userId]);
```

参考: https://ja.react.dev/learn/separating-events-from-effects

メニューアイコンをクリックするとオーバーレイを施します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/71f38d84-97a6-e4d6-a46c-4c5bbcf3844c.png)

### ToDoチェックボックスをおしゃれにする

To-Do のチェックボックスをおしゃれにしてみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/eb02eef1-cb45-895c-6a7e-2b862fa16373.png)

以下を参考にしました。

https://getcssscan.com/css-checkboxes-examples

👉 こちらのサイトはとても便利でおしゃれな CSS の例が載っています。

### ローディングアイコンをおしゃれにする

ローディングアイコンもおしゃれにしてみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/2ac82bc6-2156-c0fa-0e31-52f9cb6010d2.png)

https://cssloaders.github.io/

👉 こちらのサイトは豊富なローディングアイコンの例が載っています。

# ⚡ バックエンド

バックエンドはコスト削減のためサーバーレス構成にしました。

## DynamoDB キー設計

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/109f5dfc-0eab-2a7c-362a-8435b1d4a4c7.png)

| 属性名      | 型  | 用途                    |
| ----------- | --- | ----------------------- |
| userId      | S   | ユーザー ID（UUID）     |
| todoId      | S   | ToDo ID（UUID）         |
| title       | S   | ToDo タイトル           |
| description | S   | ToDo 説明               |
| status      | N   | 0: 未完了、1: 完了      |
| createdAt   | S   | 作成日時（ISO8601 JST） |
| updatedAt   | S   | 更新日時（ISO8601 JST） |

## Lambda 関数による DynamoDB CRUD 操作

SDK for JavaScript を用いたデータ登録・更新・削除を実装しました。

- 共通設定

```js
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
  DynamoDBDocumentClient,
  GetCommand,
  PutCommand,
  QueryCommand,
  ScanCommand,
  UpdateCommand,
  DeleteCommand
} from "@aws-sdk/lib-dynamodb";

// DynamoDBクライアント設定
const client = new DynamoDBClient({ region: process.env.AWS_REGION });
const dynamoDb = DynamoDBDocumentClient.from(client);

// DynamoDBテーブル
const TABLE_NAME = 'next-todo-app';
const USER_TABLE_NAME = 'next-todo-app-users'
```

登録、更新、削除は以下にコード例があります。

https://docs.aws.amazon.com/ja_jp/sdk-for-javascript/v3/developer-guide/javascript_dynamodb_code_examples.html

https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/javascriptv3/example_code/dynamodb#code-examples



### ToDo登録

```js
const newItem = {
  userId,
  todoId,
  title,
  description: description || '',
  status,
  createdAt: timestamp,
  updatedAt: timestamp,
};

const params = {
  TableName: TABLE_NAME,
  Item: newItem,
};

await dynamoDb.send(new PutCommand(params));
```

### ToDo更新

```js
const params = {
  TableName: TABLE_NAME,
  Key: {
    userId: userId,
    todoId: todoId,
  },
  UpdateExpression:
    'set #title = :title, #description = :description, #status = :status, #updatedAt = :updatedAt',
  ExpressionAttributeNames: {
    '#title': 'title',
    '#description': 'description',
    '#status': 'status',
    '#updatedAt': 'updatedAt',
  },
  ExpressionAttributeValues: {
    ':title': title,
    ':description': description,
    ':status': status,
    ':updatedAt': updatedAt,
  },
  ReturnValues: 'ALL_NEW',
};

const result = await dynamoDb.send(new UpdateCommand(params));
res.json(result.Attributes);
```

### ToDo削除

```js
const params = {
  TableName: TABLE_NAME,
  Key: {
    userId: userId,
    todoId: todoId,
  },
};

await dynamoDb.send(new DeleteCommand(params));
```

## API Gateway

リソースはルートパスの直下に `{proxy+}` で作成します。 Lambda プロキシ統合を True にします。URL パスパラメータは設定していませんが、勝手に追加されていました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/01c5cc13-bb5c-5073-1d23-1c7ad294585d.png)

CORS 設定とデプロイを忘れがちなので注意が必要です。

# 最後に

個人開発はハマると楽しいです！しかしアイデアが浮かばなかったりモチベーション維持が難しいです。そんなときに無理にひねり出そうとすると結局何もやらないことになってしまいがちです。そんなときは初心に帰ってシンプルなアプリを作ってみると案外ハマります！