ツイートアプリを作成しながら学習 1-1【Django + React】

## 初めに

こんにちは。
Django + React でツイートアプリみたいなのを作り
学んだことをアウトプットしていこうと思います。
長くなると思うので、投稿は何個かに分けます。

転職活動も始まり、ポートフォリオとして活用できる頑張ります。\
間違ってる部分などあればコメントで教えてください。

## 目次
ざっくりこんな感じで進めていきますが、
追加機能の追加など変わっていくと思います。

#### 1.リポジトリを新規作成
#### 2.Docker / Devcontainer セットアップ
#### 3.Djangoプロジェクト初期化

#### 4.User関連をセットアップ
#### 5.JWT 認証 + CORS 設定

## リポジトリを新規作成

tweetapp_ver1みたいな名前つける。

### Gitの初期化
git init
このコマンドで、このフォルダがGitリポジトリとして扱われるようになります。

.git/ という隠しフォルダが作られて、そこにGitの管理情報が入ります。
ここから先、ファイルを作ったり修正したら「git add」「git commit」でバージョン管理できるようになます。

#### .gitignore というファイルを作る
```python
tweetapp_ver1/  touch .gitignore
```
このファイルに「Gitで追跡したくないファイルやフォルダのパターン」を書いていく。
いったんこんな感じで。

```python

# python関連
__pycache__/
*.pyc
db.sqlite3

# Devcontainer関連
.devcontainer/*.log

# Node関連
node_modules/

環境変数ファイル
.env

```
## Docker / Devcontainer セットアップ

Visual Studio CodeのDevContainer機能を使う場合、.devcontainer/ というディレクトリを作り、その中に設定ファイルをまとめましょう。

```python
tweetapp_ver1/ mkdir .devcontainer
cd .devcontainer
tweetapp_ver1/.devcontainer/ touch devcontainer.json
```

```python
// .devcontainer/devcontainer.json

{
    "name": "TweetApp Devcontainer", //なんでもいい

    "build": {
        "dockerfile": "Dockerfile",
        // Dockerfileのパス （ここ読んでbuildします）
        // (相対パス指定なので同じ.devcontainerフォルダ内にあるDockerfileを使う)

        "context": ".."
        // Dockerイメージをビルドするときのコンテキスト(作業ディレクトリ)
        // ".." を指定することで、1つ上のフォルダ(=tweetapp_ver1ルート)全体がビルド対象になる
    },

    "customizations": {
        "vscode": {
            // VSCode拡張機能やエディタ設定をコンテナ内で使えるようにする
            "extensions": [
                "ms-python.python",
                "ms-python.vscode-pylance",
                "dbaeumer.vscode-eslint",
                "msjsdiag.vscode-react-native"
            ]
        }
    },

    "forwardPorts": [
        8000,
        3000
    ],
    // コンテナ内のポート 8000 (Django) と 3000 (React開発サーバー) を
    // ホスト側にフォワードするための設定

    "remoteUser": "vscode"
    // VSCode推奨のコンテナ内ユーザー
}

```

```python
tweetapp_ver1/.devcontainer/ touch Docekrfile
```

```python
# .devcontainer/Dockerfile

FROM mcr.microsoft.com/vscode/devcontainers/python:3.10
# DevContainer用のPython3.10イメージをベースにする

# apt-get で必要なものを入れる
RUN apt-get update && apt-get install -y \
    curl \
    iputils-ping \
    # Redisなどを使うならここでredis-serverもインストール可
    && curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
    && apt-get install -y nodejs

# Pythonパッケージをインストール
RUN pip install --upgrade pip
RUN pip install django \
    djangorestframework \
    django-cors-headers \
    djangorestframework-simplejwt \
    channels \
    channels-redis

# 必要に応じてnpmでグローバルインストールするものがあればここで
# たとえば、create-react-appとかyarnとか
# RUN npm install -g yarn

# ホントはここで "COPY ./frontend/package*.json /tmp/frontend/" して
# RUN cd /tmp/frontend && npm install 
# みたいな記述を入れることもあるけど、
# DevContainer内で直接 "cd frontend && npm install" でもOK。(僕はこれ)
```


- `apt-get`は、Debianとその派生物（Ubuntu、Linux Mintなど）で使用されるコマンドラインツールで、パッケージの管理を行うために使用されます。
apt-getを使用すると、ソフトウェアパッケージを検索、インストール、更新、削除することができます。また、依存関係の解決やパッケージのバージョン管理などの機能も提供されます。

- `Curl`とは、コマンドラインからHTTPリクエストを送信するためのオープンソースのライブラリやツールのことです。

- `apt-get install -y nodejs`
Node.jsも入れる。Reactの開発やnpm実行に必要。

- `pip install django ... channels-redis`
Django本体やRESTFramework、JWT、Channelsなどを一括インストール。
channels-redis があるとRedisを使ったWebSocket通信のバックエンドをサポート。

#### コミットタイミングの例
ここまでの手順が終わったら、いったんGitにコミットしましょう。
現場のチーム開発では、ある程度意味のある単位でコミットするのがおすすめらしいです。
今回「プロジェクト初期化 + DevContainer設定」が1つの大きなまとまり。

```python
tweetapp/ git add .
tweetapp/ git commit -m "chore: initialize project and devcontainer setup"
```

`chore:` は機能変更ではないが、環境や設定に関する変更をまとめるときに使われる。
もし `feat:`, `fix:`, `docs:` など機能追加やバグ修正、ドキュメント更新ならそちらを。
"initialize project and devcontainer setup" → やったことを簡潔に要約。
コミットメッセージは英語にしている現場も多いけど、日本語で書くチームもある。
そこはプロジェクトの方針に合わせてOK。
大事なのは**「何をやったか」**がパッと分かるようにすること。

#### ここまで出来たら　vscodeの左下にある`><`みたいなマークから『新しい開発コンテンツ』を押してコンテナをbuildしてください。コンテナ内に入り開発していきましょう！

## Djangoプロジェクト初期化

```python
tweetapp_ver1/ mkdir backend
tweetapp_ver1/ cd backend
tweetapp_ver1/backend/ django-admin startproject backend .
```

すると以下のような構造になるはずです。
```
twitter-clone/
  ├─ .devcontainer/
  ├─ backend/
  │   ├─ backend/
  │   │   ├─ __init__.py
  │   │   ├─ asgi.py
  │   │   ├─ settings.py
  │   │   ├─ urls.py
  │   │   └─ wsgi.py
  │   ├─ manage.py
  └─ ... (その他は後で追加)
```

### settings.pyの基本設定

backend/backend/settings.py を開きます。
ここにDjangoのメイン設定が書かれています。
```python
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # ここに追加予定
    # 'rest_framework',
    # 'corsheaders',
    # 'channels',
    # など
]
```
以下にも corsheaders.middleware.CorsMiddleware を追加予定。
CORS(クロスオリジン)を扱うときに必須。ReactとDjangoが別ポートで動く場合など。
```python
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    # ここに 'corsheaders.middleware.CorsMiddleware', など
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
```
以下についても、デフォルトはSQLite。試験的に使うなら十分。
本番運用するならPostgreSQLやMySQLを選ぶことが多い。
とりあえず開発ではSQLiteでOK。
```python
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}
```
ユーザーがアップロードした画像を保存するときに MEDIA_ROOT を指定する。
MEDIA_URL でURLのパスを定義している。

```python
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'

MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
```
Django Channels を使うときに必要。
ここで 'backend.asgi.application' をASGIエントリポイントとして設定する。
```python
WSGI_APPLICATION = "backend.wsgi.application" #これは初めからある
ASGI_APPLICATION = 'backend.asgi.application'
```

#### 初回マイグレーション & 動作確認
設定が終わったら、一旦マイグレーションしてDBを準備します。
DevContainerが起動していれば以下コマンドをコンテナ内ターミナルで叩けるはずです。

```python
tweetapp_ver1/backend/ python manage.py migrate
```
#### 開発サーバーを起動してみましょう

```python
tweetapp_ver1/backend/ python manage.py runserver 0.0.0.0:8000
```

ブラウザ開いて以下のように表示されたら成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3895655/f7a46faa-3de6-05f9-e949-18f1aed21f04.png)

#### Git commitしよう
今回のステップだと、「Djangoプロジェクトを初期化して最低限の設定を入れた」 がまとまりになります。

```python
tweetapp_ver1/ git add .
tweetapp_ver1/ git commit -m "feat: create Django project." #みたいな
```

## User関連をセットアップ

**この辺りでGitHubに共有もわすれずに**

#### GitHubリポジトリの作成 & ローカル連携
まずはGitHubリポジトリをつくって、
今作業しているローカルリポジトリをプッシュできるようにしましょう。

**1.1 GitHubリポジトリを作る**
- GitHubにアクセスしてログイン
- `Repositories` から `New` をクリック
- `Repository name` に `twitter-clone` とか「好きなリポジトリ名」を入力
- `Public" or "Private`を選択
- `Create repository` ボタンを押す
- すると、「リモートリポジトリのURL」が表示される
- だいたい https://github.com/ユーザー名/リポジトリ名.git みたいなやつ

**1.2 ローカルリポジトリをGitHubにPush**
今のローカルプロジェクトを、作ったリポジトリにPushする。
```python
# 1) ローカルで現在いるブランチを確認
git branch

# 2) GitHubのリポジトリを"origin"という名前で追加
git remote add origin "https://github.com/ユーザー名/リポジトリ名.git"

# 3) プッシュ
git push -u origin main
```
#### DjangoでカスタムUserモデルを作るための準備

新しいアプリを作る
慣例的に`users`とか`accounts`といった名前でアプリを作ることが多いです。
わたしは**users**ですすめます。
```python
tweetapp_ver1/backend/ python manage.py startapp users
```

#### settings.py で INSTALLED_APPS に追加
backend/backend/settings.py の `INSTALLED_APPS` に **users** を追加。

#### AUTH_USER_MODELの設定
デフォルトの django.contrib.auth.models.User を使わず、
独自Userを使うときは、`settings.py` に

```python
# backend/backend/settings.py

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    ...
    "users",  # カスタムユーザーアプリ
]

AUTH_USER_MODEL = "users.User"

```
これは「ユーザーモデルはusersアプリのUserクラスを使いますよ」という宣言。

注意: プロジェクトで最初のマイグレーションより前に設定しておく必要があります。
既にマイグレート済みの場合でも、まだ実際にUserを使っていないなら大丈夫だけど、
本番運用後に変えるのは本当に面倒になります。

#### AbstractUserを継承したUserモデルの実装

```python
# backend/users/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models

# Create your models here.

class User(AbstractUser):
    
    # DjangoのAbstractUserを継承したカスタムUserモデル。
    # AbstractUserは username, email, first_name, last_name, password など
    # すでに多くのフィールドが定義されている。
    
    # 追加でプロフィール画像や自己紹介欄などを1つのモデルにまとめてもOK
    # 例:
    # profile_image = models.ImageField(upload_to='profiles/', null=True, blank=True)
    # bio = models.TextField(blank=True)

    # ここに追加フィールドを書ける
    pass

    def __str__(self):
        return self.username
    #のように書くと、このUserインスタンス（レコード）を文字列として扱う際には
    # "ユーザー名(self.username)" が返ってくるということになる。
    #もし__str__を定義しないと、User object (1)みたいな、ちょっと味気ない表記になる。
    #__str__を定義すると、管理画面やデバッグ時のprintなどに、
    # usernameが表示されるようになり、分かりやすいというメリットがある。
```
なぜAbstractUserを使うかというと
- AbstractUser は DjangoのデフォルトUser機能（認証、パスワードハッシュ、権限管理など）をまとめて持っている
- それを継承するだけで「username」「email」「password」などのフィールドが使えるし、さらに自由にフィールド追加できる
- AbstractBaseUser を使う場合は「usernameやpasswordなどを自分で定義する」必要があるため、より細かい制御ができる代わりに設定が大変
- 今回は「標準のusernameとか使いたい＋プロフィール情報を同じテーブルにまとめたい」ならAbstractUserが楽
- django.contrib.auth.modelsの標準Userでもよいが、後で追加フィールドするときにsignal作ったりと面倒に感じたので、今回はCustomで試す

####  admin.py の編集
```python
# backend/users/admin.py

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from.models import User
#.は同じ階層のという意味

# Register your models here.

@admin.register(User)
class CustomUserAdmin(UserAdmin):

    # カスタムUserモデルを管理画面で扱うためのAdmin設定
    # Django標準のUserAdminを継承すると、基本的なUIが確保される。

    pass
```
こうするとDjango管理画面でもusers.Userが表示されるようになり
あとは管理画面にアクセスしたときに、標準Userと同じような画面が利用可能です。

#### マイグレーション & テスト実行

```python
rm db.sqlite3
python manage.py makemigrations
python manage.py migrate
```

#### スーパーユーザー作成
管理画面で確認するためにスーパーユーザーを1人作っておきましょう。

```python
python manage.py createsuperuser
```
フォローする情報（username, email, passwordなど）を聞かれるので答えます。
emailは無視。
成功すると、管理画面(http://localhost:8000/admin/)にログイン。


#### Git コミットしておきましょう

## JWT 認証 + CORS 設定
ここでは以下をやります。

#### Django REST Framework と JWT を settings.py に追加
まずは backend/backend/settings.py を開いて、下記のように修正します。

```python
INSTALLED_APPS = [
    # 既存のDjango公式アプリ
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",

    # 追加するアプリ
    "rest_framework",           # Django REST Framework
    "corsheaders",             # CORSヘッダ処理
    "rest_framework_simplejwt", # JWT

    # 自作アプリ
    "users",
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",

    # CORS (フロントからのリクエストを許可)
    "corsheaders.middleware.CorsMiddleware",

    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]
```

#### なぜここに書くのか
- INSTALLED_APPS に書くことで、Django がそれらのアプリを認識してマイグレーションなどを正しく処理できる。
- MIDDLEWARE は、HTTPリクエストが Django に届いたとき、どんな順番で処理するかを指定するリスト
- corsheaders.middleware.CorsMiddleware を入れると、別ドメイン・別ポートからのアクセスを受け入れたり、ヘッダーに Access-Control-Allow-Origin: ... を含められるようになる

**この[記事](https://qiita.com/kenta1760/items/8f706b25ccfcc4891deb)を読んでCORSを理解してください。**

#### REST_FRAMEWORK の設定
同じく settings.py の下のほうに、REST_FRAMEWORK の設定をしましょう。

```python
from datetime import timedelta

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticatedOrReadOnly",
    ],
}

SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=30),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=1),
}
```

- `DEFAULT_AUTHENTICATION_CLASSES` で JWT 認証をメインに使う宣言
- `DEFAULT_PERMISSION_CLASSES` は「認証されてない場合は読み取り専用だけ許可、書き込み系は不可」にする設定
- API 全体に対してのデフォルト方針なので、後ほど個別ビューで上書きできる
- `SIMPLE_JWT` でアクセストークンとリフレッシュトークンの有効期限を定義
- ここではアクセストークン 30分、リフレッシュトークン 1日としているけど、好みに合わせて変えてOK

**JWTの[記事](https://qiita.com/kenta1760/items/01f8935a2e692fe7da71)とリフレッシュトークンの[記事](https://qiita.com/kenta1760/items/47c748974c3dc1825f07)も用意してます。**

#### CORSの設定

```python
# backend/backend/settings.py

CORS_ALLOW_ALL_ORIGINS = False
CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",
    "http://localhost:3001",
]
```
- `CORS_ALLOW_ALL_ORIGINS = True` だと「あらゆるオリジンからのリクエストを許可」
- ポート3000でReactを動かすことが多いので、必要なら上記のように localhost:3000 だけ許可

#### ユーザー登録 & JWTトークン発行のエンドポイントを作る
"カスタムUserモデル" を使っているので、「新規ユーザーを登録するView」を1つ作ります。

```python
# backend/users/views.py

from rest_framework import generics, permissions
from.models import User
from.serializers import UserCreateSerializer

# Create your views here.

class UserCreateView(generics.CreateAPIView):
    # 新規ユーザーを作成するだけのAPI

    queryset = User.objects.all()
    serializer_class = UserCreateSerializer
    permission_classes = [permissions.AllowAny]
```

- `generics.CreateAPIView` は「POST でデータを作るAPI」のための汎用ビュー
- 下で`UserCreateSerializer` というシリアライザーを後で作る
- `permission_classes = [permissions.AllowAny]` で、認証なしでもユーザー登録できるようにする

**この[記事](https://qiita.com/kenta1760/items/2c93f244ed0c23a9966d)読むとDRFの全体像なんとなくつかめますよ**

```python
# backend/users/serializers.py

from rest_framework import serializers
from.models import User

class UserCreateSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True)

    class Meta:
        model = User
        fields = ["id", "username", "email", "password"]

    def create(self, validated_data):
        # create_user() でパスワードをハッシュ化して登録できる
        user = User.objects.create_user(
            username = validated_data["username"],
            email = validated_data.get("email", ""), #emailは任意。メソッド呼び出し
            password = validated_data["password],
        )
        return user
```

#### ルーティング設定
```python
# backend/users/urls.py

from django.urls import path
from.views import UserCreateView

urlpatterns = [
    path('users/register/', UserCreateView.as_view(), name="user_register"),
]
```

- `api/users/register/` というURLで登録できるようにするイメージ。
実際には `backend/urls.py` で `include("users.urls")` してあげる必要がある。

#### JWT発行APIの設定
`rest_framework_simplejwt` には、以下のようにトークン発行用の標準ビューがあります。


```python
# backend/backend/urls.py

from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include("users.urls")),
    path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
    path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
]
```
- `api/token/` に POST すると "username" と "password" を送って access token と refresh token が返ってくる
- `api/token/refresh/` に POST すると、リフレッシュトークンを使って新しいアクセストークンを取得できる

#### Gitコミット忘れずに！

**今日はここまで。ありがとうございました。**