【React】ドラッグアンドドロップでS3に画像アップしたい

ドラッグアンドドロップでフロントエンド(React)からサーバーサイド(Node.js)に
リクエストを送ってS3に画像を保存するやり方を纏めました。

## やること
フロントエンドで画像データを取得、サーバーサイドで受け取った画像データをS3に保存する

※リクエストを投げる、受け取る辺りの処理は省いています


## 使用ライブラリ
- [react-dropzone](https://react-dropzone.js.org/)
- [AWS SDK for JavaScript](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-started-nodejs.html)

## フロントエンドで画像データを取得する

### 画像をドロップアンドドラッグする領域を作成

```App.tsx
import "./styles.css";

export default function App() {
  return (
    <div className="drop-area">
      <h1>画像をドラッグ</h1>
    </div>
  );
}

```

```styles.css
.drop-area{
  color: gray;
  font-weight: bold;
  font-size: 1.2em;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 400px;
  height: 250px;
  border: 5px solid gray;
  border-radius: 15px;
  margin-bottom: 20px;
}
img{
  height: 240px;
}
```

### react-dropzoneでドラッグアンドドロップできるようにする

1.[react-dropzone](https://react-dropzone.js.org/)をインストールし`useDropzone`をインポートする
2.画像アップで発火するonDropイベントを作成する。ここではアップされた画像の情報をログに出してみます。
3.ドラッグアンドドロップで使用する`getRootProps`とボタンでファイル選択をする`getInputProps`を`useDropzone`から取得
4.ドロップする領域のタグの属性に`{...getRootProps()}`を追加する事でドラッグアンドドロップしてファイル情報を取得する
5.選択して画像をアップするボタンを作成し属性に`{...getRootProps()}`を設定しタグ内にinputタグを設置。
inputタグにの属性には`{...getInputProps()}`を設定する
6.画像がアップされている状態かを判別するフラグを用意しアップされていれば画像を表示する(ここでは一旦空にします）

```App.tsx
import "./styles.css";
import { useDropzone } from "react-dropzone";
import { useCallback, useState } from "react";

export default function App() {
  const [isUpload, setIsUpload] = useState(false);

  const onDrop = useCallback((acceptedFiles) => {
    const file = acceptedFiles[0];
    setIsUpload(true);

    console.log(file);
  }, []);
  const { getRootProps, getInputProps } = useDropzone({ onDrop });

  return (
    <div>
      <div className="drop-area" {...getRootProps()}>
        {isUpload ? <img src="" /> : <h1>画像をドラッグ</h1>}
      </div>
      <button {...getRootProps()}>
        画像を選択
        <input {...getInputProps()} />
      </button>
    </div>
  );
}
```

ここまでで画像の情報が取得できました
![画面収録 2021-08-06 8.20.33.mov.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/626314/90991492-a91d-b922-4627-d5bcce3a8136.gif)

### ドラッグした画像をbase64に変換する
取得したファイルの内容にアクセスする為に、FileReaderAPIを使用する必要があります。

-  FileReaderAPIからbase64にエンコードして画像を読み込みます

```App.tsx
  const encodeToBase64 = async (file: File) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => {
      const base64 = reader.result as string;
      console.log(base64)
    };
  };
```

### 画像をリサイズしてプレビューを表示する
- base64に変換した画像をそのままサーバーサイドに送ると容量が大きすぎて`HTTP 413 Payload Too Large`エラーが発生するのでcanvasを使ってリサイズします
- リサイズしたbase64をimgタグに割り当てる変数に格納すればプレビューが表示されるはずです

```App.tsx
  const resizeUpload = (base64: string) => {
    const imgType = base64.substring(5, base64.indexOf(';'))
    const img = new Image()
    img.onload = () => {
      const canvas = document.createElement('canvas')
      const width = img.width * 0.5
      const height = img.height * 0.5
      canvas.width = width
      canvas.height = height
      const ctx = canvas.getContext('2d')!
      ctx.drawImage(img, 0, 0, width, height)
      const reSizeData = canvas.toDataURL(imgType)
      setImgSrc(reSizeData);
      console.log(reSizeData)
    }
    img.src = base64
  }

```

あとは変数`imgSrc`に格納した画像データをサーバに送ればフロント側の実装は完了

```App.tsx
import "./styles.css";
import { useDropzone } from "react-dropzone";
import { useCallback, useState } from "react";

export default function App() {
  const [isUpload, setIsUpload] = useState(false);
  const [imgSrc, setImgSrc] = useState("");

  const resizeUpload = (base64: string) => {
    const imgType = base64.substring(5, base64.indexOf(';'))
    const img = new Image()
    img.onload = () => {
      const canvas = document.createElement('canvas')
      const width = img.width * 0.5
      const height = img.height * 0.5
      canvas.width = width
      canvas.height = height
      const ctx = canvas.getContext('2d')!
      ctx.drawImage(img, 0, 0, width, height)
      const reSizeData = canvas.toDataURL(imgType)
      setImgSrc(reSizeData);
    }
    img.src = base64
  }

  const encodeToBase64 = async (file: File) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => {
      const base64 = reader.result as string;
      resizeUpload(base64)
      console.log(reSizeData)
    };
  };

  const onDrop = useCallback((acceptedFiles) => {
    const file = acceptedFiles[0];
    setIsUpload(true);
    encodeToBase64(file);
  }, []);


  const { getRootProps, getInputProps } = useDropzone({ onDrop });

  return (
    <div>
      <div className="drop-area" {...getRootProps()}>
        {isUpload ? <img className="image" alt="投稿画像" src={imgSrc} /> : <h1>画像をドラッグ</h1>}
      </div>
      <button {...getRootProps()}>
        画像を選択
        <input {...getInputProps()} />
      </button>
    </div>
  );
}

```

こんな感じの動きになるはずです


![画面収録 2021-08-07 11.47.42.mov.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/626314/427dd877-8418-5f24-4c43-6b9d8ca77348.gif)

このリサイズしたbase64をサーバーサイドにリクエストを投げます

## サーバーサイドでフロントから受け取った画像データをS3に保存する


### aws-sdkでプログラムからAWSにアクセス

1. [AWS SDK for JavaScript](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-started-nodejs.html)をインストール
2. [S3にバケットを作成](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/create-bucket-overview.html)
3. フロントエンドから受け取ったbase64をデコードする
4. AWSのアクセスキーとシークレットキーを使ってS3にアクセスする


```index.ts
import AWS, { AWSError } from 'aws-sdk'

const s3 = new AWS.S3({
  accessKeyId:アクセスキー,
  secretAccessKey: シークレットキー
})

const uploadS3 = (image: string) => {
  const fileData = image.replace(/^data:\w+\/\w+;base64,/, '')
  const decodedFile = new Buffer(fileData, 'base64')
  const fileExtension = createFileExtension(image)
  const contentType = image
    .toString()
    .slice(image.indexOf(':') + 1, image.indexOf(';'))

  const params = {
    Bucket: バケット名,
    Key: [`KeyName`, fileExtension].join('.'),
    Body: decodedFile,
    ContentType: contentType,
    ACL: 'public-read-write'
  }
  s3.putObject(params, (err: AWSError) => {
    if (err) {
      throw err
    }
  })
}

```

### 備考
※S3に保存するparamsオブジェクトの中のKeyは一意でないといけないので複数のデータを保存するときはidなどをKeyに付与しておく
これでS3のバケットのにオブジェクトが作成できたのでurlをimgタグのsrcに指定すればフロントエンドでS3の画像が読み込める。
直接URLを指定することは少ないと思うので、DBにidと拡張子を保存しておいて下記のようにURLを作成すれば動的に画像を読み込めます。
`https://バケット名.amazonaws.com/KeyName${id}.${fileExtension}`


## 参考
- [react-dropzone](https://react-dropzone.js.org/)
- [AWS SDK for JavaScript](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-started-nodejs.html)
- [[JavaScript] 画像リサイズ＆回転](https://qiita.com/yasumodev/items/ec684e81ee2eac4bdddd)
- [JavaScriptで画像をbase64形式のURLに変換するやり方](https://pisuke-code.com/js-way-to-convert-img-to-base64/)