React + Django + CORSを使ったフロントエンド / バックエンドのデータ連携

## 概要

現在再開している個人開発プロジェクトの一環で、以前実装した環境を一新して再構築しています。
その中で企業レベルのアプリ開発でも使われる機会が多そうな技術スタックを採用し取り入れているので、備忘も兼ねて投稿します。
JSONをデータ連携の手法として使うことを想定し、バックエンドで定義したデータ構成、生成データをフロントエンドで受け取り表示させる流れとしました。


### 実装機能

- Reactを使ったデータ表示
- ~~DjangoでのModel構築~~　[こちらの記事](https://qiita.com/xinome/items/f883ce4565cd9c56ad17)に移動しました
- CORSを使ったフロントエンド / バックエンドのデータ連携



| 項番 | 記事 |
| :--- | :--- |
| 1 | React + Django + CORSを使ったフロントエンド / バックエンドのデータ連携（本記事） |
| 2 | [Django 管理画面のカスタマイズ](https://qiita.com/xinome/items/f883ce4565cd9c56ad17) |
| 3 | [Django REST framework(DRF)を使ったAPIサーバーとReactとのデータ連携](https://qiita.com/xinome/items/a524522b6697a3d9ffa9) |
| 4 | [Django REST frameworkのserializersを使った外部キーモデルの参照](https://qiita.com/xinome/items/49fdbf5c8cac0bbbcf3a) |
| 5 | [React + Redux / Redux Toolkitを使った非同期通信の検証](https://qiita.com/xinome/items/7971ac9091d126f3861a) |
| 6 | [APIをテストツール「Postman」を使ったDjangoとのCRUD機能実装(設計編)](https://qiita.com/xinome/items/1eaac0c02bf3bf20f75b) |
| 7 | [APIをテストツール「Postman」を使ったDjangoとのCRUD機能実装(実装編)](https://qiita.com/xinome/items/9541aa8afe7d77ea0b1e) |


### 参考文献

- React Routerを使ったルーティング

https://qiita.com/jima-r20/items/c7d636262a9ebf0bedbd

- CORSでのデータ連携

https://qiita.com/shun198/items/9ebf19d8fd2c412396dd

https://morioh.com/a/18027196003c/react-json#google_vignette

- VS Code 拡張機能: Ascii Tree Generator
  - 実装必須ではありませんが、フォルダツリーの書き出しに役立ったのでメモ書き

https://marketplace.visualstudio.com/items?itemName=aprilandjan.ascii-tree-generator


## フォルダ構成

今回使用しているものをメインに抜粋

- フロントエンド(React / JavaScript)

```
[Reactプロジェクト]
├── src
    ├── App.css
    ├── App.js
    ├── App.test.js
    ├── BaseApp.css
    ├── BaseApp.js
    ├── index.css
    ├── index.js
    ├── logo.svg
    ├── reportWebVitals.js
    └── setupTests.js
├── .gitignore
├── package-lock.json
├── package.json
└── Readme.md
```

- バックエンド(Python / Django)

```
[Djangoプロジェクト]
├── [Djangoプロジェクト_meta]
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── [Djangoアプリ]
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
├── db.sqlite3
└── manage.py
```



## 実装方法

### フロントエンド

[React公式サイト](https://ja.react.dev/learn/start-a-new-react-project)でもReact フレームワークが推奨されており、Next.jsなどを使えばバックエンドも一括で実装できるのですが、今回はバックエンドの機能をDjangoに当てるため、ネイティブな[React](https://ja.legacy.reactjs.org/docs/create-a-new-react-app.html)を使って実装します。

- Reactプロジェクト作成 + サーバーの立ち上げ。今回はデフォルトのIPを参照します。
http://localhost:3000/

```bash:Reactプロジェクトフォルダ
npx create-react-app my-app
cd my-app
npm start
```

- 必要パッケージをインストール

```bash:Reactプロジェクトフォルダ
npm install axios
```

（補足）
Reactのバージョンサポート等でエラーが吐き出される場合は、以下を試してみるのも手です。

1. `--legacy-peer-deps` オプションを使用する
npm installコマンドに--legacy-peer-depsオプションを追加して、依存関係の競合を無視してインストールします。

    ```
    npm install axios --legacy-peer-deps
    ```

2. --forceオプションを使用する
依存関係の競合を強制的に解決するために、--forceオプションを使用することもできます。

    ```
    npm install axios --force
    ```

3. package.jsonの修正
    package.jsonの依存関係を手動で追加し、再度インストールを試みます。
    
    その後、以下のコマンドで依存関係をインストールします。
    
    ```
    npm install --legacy-peer-deps
    ```

これで、axiosがインストールされ、依存関係の競合が解消されるはずです。再度プロジェクトをビルドして、エラーが解消されたか確認してください。



- `index.js` ではデフォルトの `App.js` の代わりに `BaseApp.js` を新規作成して読み込んでいます。

```javascript:src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import BaseApp from './BaseApp';
  :

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <BaseApp />
  </React.StrictMode>
);

  :
```



- データ表示画面の実装

```javascript:src/BaseApp.js
import React, { useState, useEffect } from "react";
import Axios from 'axios';

const BaseApp = () => {

  // JSONデータを取得する
  const [projectList, setProjectList] = useState([]);

  const fetchData = async () => {
    const response = await Axios.get("http://localhost:8000/api/data");
    console.log(response);
    setProjectList(response.data);
  }

  useEffect(() => {
    fetchData();
  }, []);

  const filteredProjectList = projectList.filter((item) => {
    return item.id <= 3;
  });
  
  return (
    <div className="app">
      :
          <div className='section-wrapper'>
              {filteredProjectList.map((item) => (
                <dl key={item.id}>
                  <dt>{item.date}</dt>
                  <dd>{item.content}</dd>
                </dl>
              ))}
            </div>
          </div>

        </div>
      </div>
    </div>
  );
}

export default BaseApp;
```

画面表示時に必要なのは `useState` , `useEffect` で、バックエンドのデータ構成を非同期で受信し表示させる仕組みです。
なお `Axios.get` で指定しているURLは後述するDjango側の設定により変動します。


### バックエンド

- 環境設計: [Miniconda](https://docs.conda.io/projects/miniconda/en/latest/)を使用しています

```bash
# 環境作成
conda create -n pythonenv python=3.11
 
# 作成した環境に接続
conda activate pythonenv  # 環境名を設定
```

バックエンド用フォルダのルートディレクトリで、 `Djangoプロジェクト` 、 `Djangoアプリ` の2点を作成します。
今回のディレクトリは下記の構造となります。

- Djangoプロジェクト: `backend_django` 
- Djangoアプリ名: `django_app` 

```bash
django-admin startproject [プロジェクト名]
cd [プロジェクト名]
python manage.py startapp [アプリ名]
```

- 初回はモデルのmigrationが必要なので、ルートディレクトリで下記を実行

```bash
python manage.py makemigrations [アプリ名]
python manage.py showmigrations [アプリ名]  # migrate一覧を表示
python manage.py migrate [アプリ名]
```

- Djangoサーバーの立ち上げ。今回はデフォルトのIPを参照します。

```bash
python manage.py runserver
```

と入力することで、 `http://localhost:8000/` のURLでプレビューが可能になります。


### CORSを使ったフロントエンド / バックエンドのデータ連携

- バックエンド用フォルダのルートディレクトリで、フロントエンドとバックエンドを連携するためのパッケージ `django-cors-headers` をインストール

```bash
pip install django-cors-headers
```

- settings.pyでCORS連携用の設定項目を追記

```python:backend_django/backend_django/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
      :
    '[Djangoアプリ名]',
    'corsheaders',  # フロントエンドとの通信を許可するために追加
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # フロントエンドとの通信を許可するために追加
    'django.middleware.security.SecurityMiddleware',
      :
]

# CORSによるフロントエンドとの通信を許可するために追加
CORS_ORIGIN_WHITELIST = [
    'http://localhost:3000',  # フロントエンド側のIP
]

```

- `project/urls.py` でアプリごとのURL定義を追加

```python:backend_django/backend_django/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path("api/", include("django_app.urls")),
]
```

- `app/urls.py` を新規作成

```python:backend_django/django_app/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path("data/", views.data, name="data"),
]
```

- `app/views.py` でデータの中身を定義
    - 本記事では `data` の部分がJSONの仮想データとして扱う形を取っています。

```python:backend_django/django_app/views.py
from django.shortcuts import render
from django.http import JsonResponse

def data(request):
  data = [
    {
      "id": 1,
      "date": "2022-10-01",
      "content": "【テスト】[プロジェクト]「プロジェクト名1」デプロイされました。"
    },
    {
      "id": 2,
      "date": "2022-10-02",
      "content": "[プロジェクト]「プロジェクト名2」デプロイされました。"
    },
    {
      "id": 3,
      "date": "2022-10-03",
      "content": "[プロジェクト]「プロジェクト名3」デプロイされました。"
    },
    {
      "id": 4,
      "date": "2022-10-04",
      "content": "[プロジェクト]「プロジェクト名4」デプロイされました。"
    },
      :
  ]

  return JsonResponse(data, safe=False)
```

ここまで設定することで、pythonアプリのURL
http://localhost:8000/api/data
が構築できるようになります。


## 実装ビュー・まとめ
![スクリーンショット 2024-01-10 13.44.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76115/c5f3d218-65bf-e3e1-b0a1-39d6cf192559.png)

CORSを使ってデータ連携させるためには、最低限バックエンド側でのパッケージ追加・設定をしておけば実装できることがわかりました。
今回はフロント・バックともlocalhostを使っているため双方のローカルサーバーを立ち上げておく必要がありますが、実際の現場ではVPNやクラウドサーバーに代替して設定すると言うイメージでしょうか。
最終的な構成としては、以下のような形になります。

- バックエンド
  - `settings.py` でCORS設定
  - `app/views.py` で出力（+定義）したJSONデータをフロントで参照
  - `project/urls.py`, `app/urls.py` で参照用URLを定義
- フロントエンド
  - `Axios` パッケージを使用してバックエンド側URLをデータフェッチ
  - 取得データをフィルタリング、 `map` でループ表示