[初学者の備忘録]ReactとDjango Rest Frameworkの画像アップロード実装まで(プレビュー機能付き)

# はじめに
現在`React`と`Django REST framework`を用いて成果物を制作しています。
画像アップロード機能を実装するまでに、多少苦労をしたので、他の学習者の方がより簡単に実装できるように一連の流れをまとめておきたいと思って執筆させていただいております。

画像アップロード機能は詰まることが多いと聞いたことがあるので、この記事が少しでもお役に立てれば幸いです。

# 参考
参考にした記事は以下のものです。
バックエンドの`django`側の**画像アップロード機能実装**のためと、フロントエンドの**画像プレピュー機能実装**のために参照をしました。

[はじめてのDjango (7) 画像データの管理やページへの表示，アップロードの方法などについて知ろう](https://qiita.com/j54854/items/1f0560142e39d888251c)
[Reactで超簡単な画像ビューアを作る - FileReader](https://qiita.com/sand/items/9992c7dea7c985b86075)
# バックエンド
まず、バックエンドから簡潔に説明していきたいと思います。

画像を扱うための、`Pillow`というパッケージを仮想環境にダウンロードをしなければいけません。
なので、まずはじめに仮想環境をアクティベートした後に以下のコードを打ってダウンロードしてください。

```
pip install pillow
```

## models.py

続いて、画像を扱うモデルを作成していきます。今回は私が実際に使ったモデルを用いて説明を進めていきます。
画像を扱うためには、`ImageField`を設定しなければいけません。

```models.py

class Item_Image(models.Model):
    image = models.ImageField(upload_to="images/")
    item = models.ForeignKey(
        Give_Item, on_delete=models.CASCADE, related_name="item_image")

    def __str__(self):
        return self.item.parent_item.name

    class Meta:
        db_table = "item_images"
```
`upload_to`というのは、`settings.py`で設定した`MEDIA_ROOT`からの相対パスを示しています。画像はデータベースで直接保存されるわけではなく、この指定したディレクトリにアップロードされているというのが実際のロジックです。

特にモデルでは書くことはないので、このまま`settings.py`の説明に移ります。

## settings.py
追記する内容は以下の通りです。

```
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
```
`__file__`は実行中のファイル(ここでは`settings.py`)を参照していて、`os.path.dirname`は簡単に言うと一個上のディレクトリを参照するので、`BASE_DIR`はプロジェクトやアプリを全て格納しているフォルダ名を参照していることになります。
これによって、`media`ディレクトリのパスを作成することができました。

続いて、プロジェクトの方の`urls.py`に移ります。

## urls.py
記述はとても簡単で、一種のおまじないみたいなものです。
`import`を忘れないようにしてください。

```urls.py
from django.conf import settings # New
from django.contrib.staticfiles.urls import static # New
from django.contrib.staticfiles.urls import staticfiles_urlpatterns # New


urlpatterns += staticfiles_urlpatterns() # New
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # New

```

`urlpatterns`に関しては、他のエンドポイントを書いた後の下に書いて大丈夫です。具体的には以下のようになります。

```urls.py
urlpatterns = [
    path("api/", include("app.urls")),
    path('admin/', admin.site.urls),
    url('rest-auth/', include('rest_auth.urls')),
    url('rest-auth/registration/', include('rest_auth.registration.urls'))
]

#　これで準備完了です
urlpatterns += staticfiles_urlpatterns()
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
```

あとは、`React`を使うなら`Django REST framework`も必要となるの`serializers.py`, `views.py`も設定しておきましょう。


## `serializers.py`, `views.py`
ここは特に画像アップロードで加筆することはありません。
`models.py`を作成しているなら、`ModelSerializer`,`ModelViewSet`を使えば楽にAPI実装ができます。

```serialize.py

class Item_ImageSerializer(serializers.ModelSerializer):

    class Meta:
        model = Item_Image
        fields = "__all__"
```

```views.py

class CommentViewSet(viewsets.ModelViewSet):
    queryset = Comment.objects.all()
    permission_classes = [
        permissions.AllowAny
    ]
    serializer_class = CommentSerializer
```

これにてバックエンドの設定は完了です。
フロントエンドに移ります。

# フロントエンド
## `input`フォームの作成
画像をアップロードしてもらうことになるので、`input`を作成する必要があります。
ここで大切なのは**`form`タグを使用することです。**
`button`で`onSubmit`に関数を代入した方が`e.preventDefault()`を書かなくて楽だと思われるかもしれませんが、画像アップロードの際に必要になるので、必ず`form`タグで囲ってあげてください。

ちなみに、今回は複数投稿での実装となります。

```form.jsx

<div className="imageForm">
            <form onSubmit={this.handleSubmit}>

             //
                   省略
             //              

              <label>商品画像</label>
　　　　　　　　　 //
                // 複数アップロードする際は、multipleをつける必要があります
                //
              　<input type="file" multiple onChange={this.handleImageSelect} />
                //
                // 下記はプレビュー機能のためのコードなので後で説明を加えます
                //
              　　{this.state.imgUrls.length === 0
                　　? null
                　　: this.state.imgUrls.map((img, idx) => {
                   return <img key={idx} src={img}></img>;
                  })}
              
            <form/>
```

## プレビュー機能の実装
先にコードを書きます。

```form.jsx

  readImageUrl = () => {
    const files = Array.from(this.state.info.images);
    Promise.all(
      files.map((file) => {
       //
       //   3
       //
        return new Promise((resolve, reject) => {
          const reader = new FileReader();
          reader.addEventListener('load', (event) => {
            resolve(event.target.result);
          });
          reader.addEventListener('error', reject);
          reader.readAsDataURL(file);
        });
      })
    )
      .then((images) => {
        this.setState({ imgUrls: images });
      })
      .catch((err) => console.log(err));
  };


  handleImageSelect = async (e) => {
  // 
  //   1
  //
    await this.setState({
      info: { ...this.state.info, images: [...this.state.info.images, ...e.target.files] },
    });
    this.setState({
      message: {...this.state.message, images: this.validator("images", this.state.info.images )}
    })
    //
    //  2
    //
    this.readImageUrl();
  };
```
順番に説明をしていきます。
私のコードのValidationの実装方法に関連して、少し記述がごちゃごちゃになっております。申し訳ございません。


**1.`state`内に選択されたファイルを格納する**
一番はじめに行うことは極めて単純です。選択されたファイルをSubmitするために`state`に入れ込むだけです。私の場合、後のValidationの都合で`asycn/await`で全ファイルが代入されるまで待っていますが、この非同期処理への対応は必須ではないのでお任せします。(後にわかったことですが`setState`を`async/await`に付けても特に効果はないようです。)

重要なことは、**選択されたファイルをスプレッド構文を用いて、配列にまとめて代入する**ということです。

**2. ファイルをインラインで埋め込む`data:URL`に変更する**
(この表現が正しいかはわかりませんが)inputから得られる`File`は`Blob`を継承しているため中のデータに直接アクセスすることはできません。`File`に格納されたデータにアクセスするための一つの方法が**`File`を`data:URL"として読み込むこと**であるので、この関数を使って配列内の`File`達を変換しているということになります。

**3. `FileReader`を使って画像のURLを取得し`state`に入れる**
このプレビュー機能の実装方法において、肝となるのは`FileReader`というオブジェクトです。今回は複数投稿での実装ということで、`input`から得た`files`arrayを`map`しています。書かれてる順番が前後しますが、`Promise`の中で行われているのは、まず`readAsDataURL`メソッドを使って選択されたファイルを読み込むことです。名の通り読み込まれたファイルは上述の`data:URL`に変換されます。そして、読み込みが終わったと同時に発火するのが、その上の`load`イベントです。ちなみにこれは`addEventListener`を使う必要はなく、`onload`というプロパティを使ってより簡潔に記述することもできます。書かれてる順がややこしくさせますが、この`load`イベントは読み込みが完了され他あと、`result`として読み込まれたファイルを返してくれます。今回はプレビューとして画像を描画したいので、`state`に入れます。

```
{
  this.state.imgUrls.length === 0
    ? null
    : this.state.imgUrls.map((img, idx) => {
        return <img key={idx} src={img} alc="アップロード写真" height="150px"></img>;
       })
}
```

今回は複数投稿なので、先ほど変換された`data:URL`が入ったファイルを`map`します。後は、`img`タグの`src`に受け取った`URL`を入れるだけです。

## 画像アップロードの実装
```
this.state.info.images.map((image) => {
      let data = new FormData();
      data.append('image', image);
      data.append('item', giveItem_id);

      axios
        .post(this.props.axiosUrl + 'image/', data, authHeader)
        .then((res) => console.log(res.data))
        .catch((err) => console.log(err));
    });
```
画像アップロードに関連するコードだけ抜粋して書いていますが、説明に大きく影響はないのでそのまま使用させていただきます。

`React`, `Django Rest Framework`において画像をアップロードする肝となるのは`FormData`です。他の`CharField`,`IntegerField`モデルでは可能な`axios`の`data`部分に値を入れてPOSTリクエストを送ってもエラーが返ってきます。`ImageField`は`FileField`を継承しているからか理由は定かではありませんが、少なくとも画像アップロードには`FormData`オブジェクトとしてリクエストを送らないとモデルを作成できないのは間違い無いと思います。

`FormData`を使うことさえわかれば実装は至極単純です。`.append(name, value)`を用いて`FormData()`にリクエストを送りたい値を入れるだけです。

私の場合、複数投稿された画像一枚ごとにモデルを新規作成したかったので`input`から得た`file`を格納した配列を`map`しています。

豆知識となりますが、`FormData`にちゃんと値が入っているか確認したい場合は以下の方法を使えば可能です(上記のコードから`let data = new FormData()`として代入されている前提です)。

```
console.log(...data)
```

## まとめ
上記が私が実装した方法です。

1. **`input`から得られる`file`は`Blob`形式である**
2. **`Blob`形式のファイルを`data:URL`等に変換するのに`FileReader()`が有効である**
3. **フロントからバックエンドへの`ImageField`を持つモデルを作る場合、`FormData`として送信しなければならない**


以上がまとめです。
他にもより良い実装方法があると思いますが、あくまでも一つの方法として参考にしていただければ幸いです。

アドバイスや間違っている点含めコメントをいただければとても嬉しいです。
拙く読み辛い文章だったとは思いますが最後までご覧いただき誠にありがとうございました。