https://qiita.com/sand/items/80a67da0a44b042f0bc3


Reactアプリから Django Rest API を叩いてみる

1.Django Rest API と Reactアプリ

以前、「DjangoのページをReactで作る - Webpack4」という記事を書きました。DjangoのページでReactを使うための、開発環境の構築を紹介したものですが、これはどちらかと言えば、Djangoの開発環境にReactの開発環境を「従わせた」ものでした。BabelやWebpackの設定はDjangoの環境に合わせる形で手動で行いました。

今回はDjangoReactの開発環境を完全に独立させます。特にReactではcreate-react-appを使いますので、簡単に開発環境を構築できます。

  • (1)サーバは、DjangoプロジェクトでRest APIを開発・単体テスト
  • (2)クライアントは、create-react-appで開発・単体テスト
  • (3)サーバ側でCORS設定を行い、クライアントからRest APIにアクセスする

(1)と(2)はそれぞれ独立して開発を行い、それぞれに動作確認します。
その後(3)のCORSの設定を行い、クライアントとサーバの連結を確認します。

今回はTodoアプリを作成していきます。

環境としては、todo-reactというディレクトリの下に、djangotodoというDjangoプロジェクトと、frontendというcreate-react-appのプロジェクトを作成します。

todo-react
│
├── djangotodo
│   ├── db.sqlite3
│   ├── djangotodo
│   ├── manage.py
│   └── todos
├── frontend
    ├── db.json
    ├── node_modules
    ├── package-lock.json
    ├── package.json
    ├── public
    └── src

2.サーバサイド - djangotodo

サーバサイドでは、だいたい以下のような作業を行います。

  • DjangoでTodoプロジェクトを作る
  • djangorestframeworkをインストールしRest APIを構築する
  • 単体テストを行う
  • 来たるべき総合テストに備えて、django-cors-headersをインストールしCORSの設定を行っておく

2-1.Djangoプロジェクト作成

venvで環境を作ってから、Djangoのプロジェクトを開始します。

python -m venv todo-react
source todo-react/bin/activate
cd todo-react
pip freeze
pip install django
django-admin startproject djangotodo
cd djangotodo/

私の環境は、DjangoはRemoteサーバに構築していますので、サーバのドメイン名を入力してアクセスを許可します。

djangotodo/settings.py
---
ALLOWED_HOSTS = ["www.mypress.jp"]
---

DBを初期化します。

python manage.py migrate

ここまででDjangoのアプリを立ち上げます。

python manage.py runserver 0:8080

http://www.mypress.jp:8080/ で、Djangoの初期画面を確認できます。

2-2.Todoアプリ作成

DjangoでTodoアプリを作成します

django-admin startapp todos

TodoアプリのModelを定義します。

todos/models.py
# todos/models.py
from django.db import models

class Todo(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField()
    status = models.CharField(default='Unstarted', max_length=100)

    def __str__(self):
        """A string representation of the model."""
        return self.title

TodoアプリをINSTALLED_APPSに追加します。

djangotodo/settings.py
---
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'todos', # New
]
---

migrationファイルを作成して、DBに反映させます。

python manage.py makemigrations todos
python manage.py migrate todos

adminを設定して、管理画面からTodoテーブルの操作を行えるようにします。

todos/admin.py
# todos/admin.py
from django.contrib import admin

from .models import Todo

admin.site.register(Todo)

管理者を追加します

python manage.py createsuperuser

サーバを起動します。

python manage.py runserver 0:8080

管理画面にアクセスします。

http://www.mypress.jp:8080/admin/

ログインします。
image.png

Todoを追加し、テストデータを作っておきます。
image.png

2-3.Django Rest Frameworkの設定

Djangoには、djangorestframeworkというRest APIを簡単に構築できるライブラリがあります。
Django Rest Framework with React Tutorial

インストールします。

pip install djangorestframework

INSTALLED_APPSを更新します。

djangotodo/settings.py
---
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework', # New
    'todos',
]

# New
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ],
    'EXCEPTION_HANDLER': 'djangotodo.todos.utils.custom_exception_handler'
}
---

EXCEPTION_HANDLERはデバッグのために設定しました。これを使うためには、以下のコードも必要になります。

todos/utils.py
from rest_framework.views import exception_handler

def custom_exception_handler(exc, context):
    # Call REST framework's default exception handler first,
    # to get the standard error response.
    response = exception_handler(exc, context)

    # Now add the HTTP status code to the response.
    if response is not None:
        response.data['status_code'] = response.status_code

    return response

Django Rest Frameworkの設定のため、以下の3つを定義します。

  • urls.py :URLルート
  • serializers.py :dateをJSONに変換
  • views.py :APIエンドポイントにロジックを適用

この辺を詳しく知るためには、以下の公式ドキュメントを最初に読みましょう。
Tutorial 1: Serialization

URL Pathの定義です。リクエスト時のパスは末尾がスラッシュ(/)で終わっている必要があります。

djangotodo/urls.py
from django.urls import path, include  # New
from django.contrib import admin

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('todos.urls')), # New
]

todoのURL Pathの定義です

todos/urls.py
# todos/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.ListTodo.as_view()),
    path('<int:pk>/', views.DetailTodo.as_view()),
]

serializersとは、ざっくり言って、modelデータをJSONで出力するための機能です。ここではModelSerializer classを利用しているので、とてもシンプルに定義できます。SnippetSerializer classを利用する方法もありますが、この場合createやupdateの明示的な定義が必要になり複雑です。

todos/serializers.py
# todos/serializers.py
from rest_framework import serializers
from .models import Todo

class TodoSerializer(serializers.ModelSerializer):
    class Meta:
        fields = (
            'id',
            'title',
            'description',
            'status',
        )
        model = Todo

rest_frameworkを使って、viewsを定義します。

todos/views.py
# todos/views.py
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.parsers import JSONParser
from todos.models import Todo
from todos.serializers import TodoSerializer


@csrf_exempt
def todo_list(request):
    """
    List all todos, or create a new todo.
    """
    if request.method == 'GET':
        todos = Todo.objects.all()
        serializer = TodoSerializer(todos, many=True)
        return JsonResponse(serializer.data, safe=False)

    elif request.method == 'POST':
        data = JSONParser().parse(request)
        serializer = TodoSerializer(data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data, status=201)
        return JsonResponse(serializer.errors, status=400)


@csrf_exempt
def todo_detail(request, pk):
    """
    Retrieve, update or delete a todo.
    """
    try:
        todo = Todo.objects.get(pk=pk)
    except Todo.DoesNotExist:
        return HttpResponse(status=404)

    if request.method == 'GET':
        serializer = TodoSerializer(todo)
        return JsonResponse(serializer.data)

    elif request.method == 'PUT':
        data = JSONParser().parse(request)
        serializer = TodoSerializer(todo, data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data)
        return JsonResponse(serializer.errors, status=400)

    elif request.method == 'DELETE':
        todo.delete()
        return HttpResponse(status=204)

今回はロジックを明示的に記述する仕方でviews.pyを定義しましたが、慣れたらgeneric class-based viewsを使った方が良いでしょう。コーディング量を劇的に減らすことが可能です。
Tutorial 3: Class-based Views

2-4.単体テスト

ブラウザからアクセスしてみます。

http://www.mypress.jp:8080/api/

先ほど管理画面から入力したテストデータが表示されます。
image.png

rest_frameworkはTodoの追加フォームも表示してくれます。便利です。

HTTPieコマンドを使っても簡単にテストできます。
HTTPie—aitch-tee-tee-pie—is a command line HTTP client with an intuitive UI

例えば以下のコマンドで「タスク追加」を確認できます。

http POST http://www.mypress.jp:8080/api/  title=a description=b status=Unstarted

2-5.CORS

これは本来なら、frontendのreactアプリ作成後の、最後に設定し確認するものです。しかしサーバでの設定ですので、ここでやっておきます。

また、CORSの確認テストで試行錯誤する時には、その都度必ずブラウザのキャッシュをクリアーすることを強くお勧めします。私はこれを怠り嵌りました!

Access to XMLHttpRequest at 'http://www.mypress.jp:8080/api' from 
origin 'http://www.mypress.jp:3000' has been blocked by 
CORS policy: No 'Access-Control-Allow-Origin' header is present on 
the requested resource.

以上のエラーを回避するためにサーバ側でCORSの設定を行う必要があります。django-cors-headersをインストールします。

pip install django-cors-headers

settings.pyを更新します。newコメントがついている4か所が修正箇所です。CorsMiddlewareはトップに置く必要があります。

djangotodo/settings.py
---
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'corsheaders', # new
    'todos',
]

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ],
    'EXCEPTION_HANDLER': 'djangotodo.todos.utils.custom_exception_handler'
}

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware', # new topに置く
    'django.middleware.common.CommonMiddleware', # new
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]


# new
CORS_ORIGIN_WHITELIST = [
    'http://www.mypress.jp:3000',
]
# CORS_ORIGIN_ALLOW_ALL = False
---

以上でCORSの設定は終わりです。動作確認はReactアプリ完成後に行います。

3.フロントエンド - frontend

Reactプログラムは、reduxとredux-thunkを使い、action(非同期関数)から、Rest APIを叩きます。APIはaxiosで実装します。また最低限のUIを実装し、CSSを含めたコーディング量を減らすため、antdも利用します。一応最後に、全ソースを掲載します。少し長くなるのですが、不明な点を無くすため。

3-1.Reactプロジェクト作成

create-react-appを使って、Reactプロジェクトを作成します。必要なパッケージをインストールします。

create-react-app frontend
cd frontend
npm install --save axios react-redux redux redux-thunk redux-devtools-extension antd

package.jsonは以下の通りです。

package.json
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "antd": "^3.20.5",
    "axios": "^0.19.0",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "react-redux": "^7.1.0",
    "react-scripts": "3.0.1",
    "redux": "^4.0.4",
    "redux-devtools-extension": "^2.13.8",
    "redux-thunk": "^2.3.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

3-2.redux-devtools-extension

redux-devtools-extensionはReduxアプリの開発ツールの一つで、ブラウザの拡張機能からReduxの状態管理を可視化してくれます。別途、Chromeの拡張機能を設定する必要があります。

以下の画面のように、Chromeの拡張機能で専用ウィンドが開き、Reduxのactionやstateが可視化されます。
image.png

3-3.antd

React のUI libraryであるantdを使います。特にListコンポネントを使うことで、ソースコードをとても簡潔にすることができました。入力フォームにはFormコンポーネントを使いました。

React UI library の antd について (1) - Button

個人的には、antdを使うことにより、面倒なstyleを指定することが少なくなるので助かります。

3-4.単体テスト

Djangoと結合する前に、json-serverを使って単体テストを行います。

frontendディレクトリ直下にdb.jsonファイルを作ります。

db.json
{
  "api": [
    {
      "id": 1,
      "title": "Reduxのお勉強",
      "description": "特に非同期actionについて",
      "status": "In Progress"
    },
    {
      "id": 2,
      "title": "ES6のお勉強",
      "description": "Promiseについて",
      "status": "In Progress"
    },
    {
      "id": 3,
      "title": "朝食",
      "description": "忘れずに食べること",
      "status": "Completed"
    },
    {
      "title": "掃除",
      "description": "要らない本は捨てる",
      "status": "In Progress",
      "id": 4
    },
    {
      "title": "草刈り",
      "description": "夏草に要注意!",
      "status": "Unstarted",
      "id": 5
    }
  ]
}

frontendディレクトリ直下で、json-serverを起動します。

json-server --host www.mypress.jp --watch db.json -p 3003

状態「Unstarted」、「In Progress」、「Completed」毎にTodo一覧が表示されます。以下の画面になります。

image.png

3-5.ソースコード

frontend/src直下のソースツリーです。

├── App.js
├── actions
│   └── index.js
├── api
│   └── index.js
├── components
│   ├── FlashMessage.js
│   ├── TaskList.js
│   └── TasksPage.js
├── constants
│   └── index.js
├── index.js
└── reducers
    └── index.js

(1)トップ

主に、Reduxの設定を行い、App.jsを呼びます。

src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import tasksReducer from './reducers';
import App from './App';

const rootReducer = (state = {}, action) => {
  return {
    tasks: tasksReducer(state.tasks, action),
  };
};

const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(thunk))
);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

App.jsはメイン画面の枠組みの定義です。

src/App.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import TasksPage from './components/TasksPage';
import FlashMessage from './components/FlashMessage';
import { createTask, editTask, deleteTask, fetchTasks } from './actions';
import 'antd/dist/antd.css';

class App extends Component {
  componentDidMount() {
    this.props.dispatch(fetchTasks());
  }

  onCreateTask = ({ title, description }) => {
    this.props.dispatch(createTask({ title, description }));
  };

  onStatusChange = (id, status) => {
    this.props.dispatch(editTask(id, { status }));
  };

  onDeleteTask = (id) => {
    this.props.dispatch(deleteTask(id));
  };

  render() {
    return (
      <div>
        {this.props.error && <FlashMessage message={this.props.error} />}
        <div>
          <TasksPage
            tasks={this.props.tasks}
            onCreateTask={this.onCreateTask}
            onStatusChange={this.onStatusChange}
            isLoading={this.props.isLoading}
          />
        </div>
      </div>
    );
  }
}

function mapStateToProps(state) {
  const { tasks, isLoading, error } = state.tasks;
  return { tasks, isLoading, error };
}

export default connect(mapStateToProps)(App);

(2)Reducer & Action

reducerの定義です

src/reducers/index.js
const initialState = {
  tasks: [],
  isLoading: false,
  error: null,
};

export default function tasks(state = initialState, action) {
  switch (action.type) {
    case 'FETCH_TASKS_STARTED': {
      return {
        ...state,
        isLoading: true,
      };
    }
    case 'FETCH_TASKS_SUCCEEDED': {
      return {
        ...state,
        tasks: action.payload.tasks,
        isLoading: false,
      };
    }
    case 'FETCH_TASKS_FAILED': {
      return {
        ...state,
        isLoading: false,
        error: action.payload.error,
      };
    }
    case 'CREATE_TASK_SUCCEEDED': {
      return {
        ...state,
        tasks: state.tasks.concat(action.payload.task),
      };
    }
    case 'EDIT_TASK_SUCCEEDED': {
      const { payload } = action;
      const nextTasks = state.tasks.map(task => {
        if (task.id === payload.task.id) {
          return payload.task;
        }

        return task;
      });
      return {
        ...state,
        tasks: nextTasks,
      };
    }
    case 'DELETE_TASK_SUCCEEDED': {
      const { payload } = action;
      const nextTasks = state.tasks.filter(task => task.id !== payload.id)
      return {
        ...state,
        tasks: nextTasks,
      };
    }
    default: {
      return state;
    }
  }
}

actionの定義です

src/actions/index.js
import * as api from '../api';

function fetchTasksSucceeded(tasks) {
  return {
    type: 'FETCH_TASKS_SUCCEEDED',
    payload: {
      tasks,
    },
  };
}

function fetchTasksFailed(error) {
  return {
    type: 'FETCH_TASKS_FAILED',
    payload: {
      error,
    },
  };
}

function fetchTasksStarted() {
  return {
    type: 'FETCH_TASKS_STARTED',
  };
}

export function fetchTasks() {
  return dispatch => {
    dispatch(fetchTasksStarted());

    api
      .fetchTasks()
      .then(resp => {
        dispatch(fetchTasksSucceeded(resp.data));
      })
      .catch(err => {
        dispatch(fetchTasksFailed(err.message));
      });
  };
}

function createTaskSucceeded(task) {
  return {
    type: 'CREATE_TASK_SUCCEEDED',
    payload: {
      task,
    },
  };
}

export function createTask({ title, description, status = 'Unstarted' }) {
  return dispatch => {
    api.createTask({ title, description, status }).then(resp => {
      dispatch(createTaskSucceeded(resp.data));
    });
  };
}

function editTaskSucceeded(task) {
  return {
    type: 'EDIT_TASK_SUCCEEDED',
    payload: {
      task,
    },
  };
}

export function editTask(id, params = {}) {
  return (dispatch, getState) => {
    const task = getTaskById(getState().tasks.tasks, id);
    const updatedTask = Object.assign({}, task, params);
    api.editTask(id, updatedTask).then(resp => {
      dispatch(editTaskSucceeded(resp.data));
    });
  };
}


function getTaskById(tasks, id) {
  return tasks.find(task => task.id === id);
}


function deleteTaskSucceeded(id) {
  return {
    type: 'DELETE_TASK_SUCCEEDED',
    payload: {
      id,
    },
  };
}

export function deleteTask(id) {
  return (dispatch, getState) => {
    api.deleteTask(id).then(resp => {
      console.log(resp)
      dispatch(deleteTaskSucceeded(id));
    });
  };
}

statusの定数の定義です。

src/constants/index.js
export const TASK_STATUSES = ['Unstarted', 'In Progress', 'Completed'];

(3)API

Rest APIのインターフェースモジュールです。ここで注意が必要なのは、DjangoのPOSTの場合、パスの末尾に、'/api/' のように、スラッシュが必要だということです。 '/api' ではだめです。

'/api' のGETの場合、自動的に末尾にスラッシュを付け直してリダイレクトしてOKになります。POSTでもリダイレクトしてくれますが、リダイレクト時にPOST dataが落ちてしまい、結果的にエラーとなります。
json-serverではテストが通ってもDjangoではNGになるので注意が必要です。

POSTでBAD Requestエラーとなる場合は、paramsの中身もチェックしてみましょう。私はここで躓いて、actionが正しいデータを渡してくれているのかを確認せずに、半日も悩んでしまいました。

src/api/index.js
import axios from 'axios';

// const API_BASE_URL = 'http://www.mypress.jp:3003'; // json-server用
const API_BASE_URL = 'http://www.mypress.jp:8080'; // Django用

const client = axios.create({
  baseURL: API_BASE_URL,
  headers: {
    'Content-Type': 'application/json'
  },
});

export function fetchTasks() {
  return client.get('/api/');
}

export function createTask(params) {
  console.log(params)
  return client.post('/api/', params);
}

export function editTask(id, params) {
  return client.put(`/api/${id}`, params);
}

export function deleteTask(id) {
  return client.delete(`/api/${id}/`);
}

(4)components

TasksPage.jsはTasksPageクラスの他に。タスク追加フォームであるAddTaskFormクラスを定義しています。別ファイルにすべきかと思いましたが、面倒なので一緒にしました。

タスク追加フォームには、antdForm componentを使っています。validateが統一的に行えるので便利ですが、少しコードが複雑になります。詳しくは「React UI library の antd について (3) - redux-form」も参照してください。

src/components/TasksPage.js
import React, { Component } from 'react';
import { Form, Input, Icon, Button } from 'antd';

import TaskList from './TaskList';
import { TASK_STATUSES } from '../constants';

class TasksPage extends Component {
  constructor(props) {
    super(props);
    this.state = {
      showNewCardForm: false,
    };
  }

  toggleForm = () => {
    this.setState({ showNewCardForm: !this.state.showNewCardForm });
  };

  render() {
    if (this.props.isLoading) {
      return (
        <div>
          Loading...
        </div>
      );
    }

    return (
      <div>
        <div>
          <Button type="primary" onClick={this.toggleForm}>+タスク追加</Button>
        </div>
        {this.state.showNewCardForm && <WrappedAddTaskForm onCreateTask={this.props.onCreateTask} />}
        <div>
          {TASK_STATUSES.map(status => {
            const statusTasks = this.props.tasks.filter(
              task => task.status === status
            );
            return (
            <div style={{ margin: "25px 20px 25px 20px" }}>
              <h2>{status}</h2>
              <TaskList
                key={status}
                status={status}
                tasks={statusTasks}
                onStatusChange={this.props.onStatusChange}
                onDeleteTask={this.props.onDeleteTask}
              />
            </div>
            );
          })}
        </div>
      </div>
    );
  }
}

export default TasksPage;



class AddTaskForm extends React.Component {
  componentDidMount() {
    // To disabled submit button at the beginning.
    this.props.form.validateFields();
  }

  handleSubmit = e => {
    e.preventDefault();
    this.props.form.validateFields((err, values) => {
      if (!err) {
        console.log('Received values of form: ', values);
        this.props.onCreateTask(values)
      }
    });
  };

  render() {
    const { getFieldDecorator, getFieldError, isFieldTouched } = this.props.form;

    // Only show error after a field is touched.
    const taskError = isFieldTouched('task') && getFieldError('task');
    const descriptionError = isFieldTouched('description') && getFieldError('description');
    const buttonDisable = getFieldError('task') || getFieldError('description')

    return (
      <Form layout="inline" onSubmit={this.handleSubmit}>
        <Form.Item validateStatus={taskError ? 'error' : ''} help={taskError || ''}>
          {getFieldDecorator('task', {
            rules: [{ required: true, message: 'taskを入力してください!' }],
          })(
            <Input
              prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
              placeholder="task"
            />,
          )}
        </Form.Item>
        <Form.Item validateStatus={descriptionError ? 'error' : ''} help={descriptionError || ''}>
          {getFieldDecorator('description', {
            rules: [{ required: true, message: 'descriptionを入力してください!' }],
          })(
            <Input
              prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
              placeholder="description"
            />,
          )}
        </Form.Item>
        <Form.Item>
          <Button type="primary" htmlType="submit" disabled={buttonDisable}>
            タスク追加
          </Button>
        </Form.Item>
      </Form>
    );
  }
}

const WrappedAddTaskForm = Form.create({ name: 'add_task_form' })(AddTaskForm);

タスク一覧の表示です。antdのList componentを使っているので、とても簡潔に記述できています。

src/components/TaskList.js
import React from 'react';
import { List, Card } from 'antd';
import { TASK_STATUSES } from '../constants';


const TaskList = props => {
  return (
    <List
      grid={{ gutter: 16, column: 4 }}
      dataSource={props.tasks}
      renderItem={item => (
        <List.Item>
          <Card title={item.title}>{item.description}</Card>
          <select value={item.status} onChange={(e) => {onStatusChange(e, item.id)}}>
            {TASK_STATUSES.map(status => (
              <option key={status} value={status}>{status}</option>
            ))}
          </select>
          <Button type="danger" onClick={()=>{props.onDeleteTask(item.id)}}>
            タスク削除
          </Button>
        </List.Item>
      )}
    />
  );

  function onStatusChange(e, id) {
    props.onStatusChange(id, e.target.value);
  }
};

export default TaskList;

actionでエラーが発生した場合に、表示されるメッセージです。

src/components/FlashMessage.js
import React from 'react';

export default function FlashMessage(props) {
  return (
    <div>
      {props.message}
    </div>
  );
}

FlashMessage.defaultProps = {
  message: 'An error occurred',
};

4.ReactとDjangoの結合

現在は以下の状況です

  • 【サーバサイド】Django単体での動作を確認済み
  • 【フロントエンド】React単体での動作を確認済み

最後にサーバサイドとフロントエンドを結合して動作を確認します。
単体で成功しても、結合で失敗し時間を費やすことになるのは、よくあることです。今回も以下の2点でだいぶ時間を浪費してしまいました。

  • CORSの設定のデバッグに時間を要した(ブラウザキャッシュの問題)
  • POSTリクエストエラーに時間を要した(リクエストパスの末尾のスラッシュを忘れた & POSTデータの属性"title"が間違っていた)

特にjson-serverはサーバ側は、特にチェック無しで通りますが、Djangoの場合はデータの属性名が違っていたりすると、当然はじかれます。この点を忘れて迷路に迷うことにならないように注意します。

単体の時と同じですが、結合時の画面です。
image.png

今回は以上です。


https://superhahnah.com/redux-directory-petterns/


Reduxでのディレクトリ構成3パターンに見る「分割」と「分散」


Redux を使っていいて、ディレクトリ構成に悩んだことはないだろうか。
もしくは、見かけるディレクトリ構成が多様で、どれがいいのか分からないなんてことはなかっただろうか。

この記事では Redux を用いる際の代表的な3パターンの構成を紹介するとともに、それぞれをソースコードの分割・分散の度合いで比べてみる。

  1. redux-way
  2. ducks
  3. re-ducks

パターン1. redux-way (あるいは Rails-style)

redux-way では、 “Redux によって導入される概念” ごとにディレクトリを分ける。
以下のようにcomponents/,containers/,reducers/,actions/,types/などとディレクトリを設けるケースが多いようだ。
それぞれのディレクトリでは、対応する component 毎にさらにファイルを分ける (以下の例ではcomponent1.js, component2.js, …)。

src/
  ├ components/
  |    ├ component1.js
  |    └ component2.js
  ├ containers/
  |    ├ component1.js
  |    └ component2.js
  ├ reducers/
  |    ├ component1.js
  |    └ component2.js
  ├ actions/
  |    ├ component1.js
  |    └ component2.js
  └ types/
       ├ component1.js
       └ component2.js

Redux公式のFAQ には Rails-style というものが紹介されているが、これとほぼ同じ。

Rails-style: separate folders for “actions”, “constants”, “reducers”, “containers”, and “components”

redux-way の問題点

redux-way は至って普通のディレクトリ構成ではあるが、閲覧性が悪く関連性を理解しづらいという問題があり、これは 過剰な分割 と 過剰な分散 に起因している。

reducers, action creators, action types の3つは密結合になっているにも関わらず、それぞれが異なるファイルに 分割 されており、さらには異なるディレクトリに 分散 している。そのため関連性を把握しづらくなってしまう。

例えば、reducers/component1.js に定義する reducer は 受け取った action を計算に用いるのだが、どのような action を受け取り得るのかは action creator を定義する actions/component1.js を見なければ分からず、また action creator が返す action の構成要素である action type が取りうる値は types/component1.js を見なければ分からないようになっている。

パターン2. ducks

先に挙げた redux-way の 過剰な分割 と 過剰な分散 を解消するようなディレクトリ構成に、 ducks と呼ばれるものがある。

参考:

redux-way において reducers, action creators, actions types の関数・定数定義はreducers/actions/types/のディレクトリに分散していたが、 ducks ではこれら3つを単に1つのmodules/ ディレクトリとしてまとめ、 過剰な分散 が解消される。

このときmodules/配下において component1に関する reducers, action creators, action types の定義は別々のファイルに分割することもできるが、ducks においては 過剰な分割 の解消のため、単一のファイルにまとめる(modules/component1.js)。

src/
  ├ components/
  |    ├ component1.js
  |    └ component2.js
  ├ containers/
  |    ├ component1.js
  |    └ component2.js
  └ modules/
       ├ component1.js
       └ component2.js

ducks のディレクトリ構成では密結合な reducers, action creators, action types の定義が1ファイルで記述され、非常に簡潔で見通しが良くなる。

ducks の課題と対策

特に小規模なプロダクトにおいては ducks のシンプルな構成がマッチすると思われるが、中・大規模になってくると1ファイルがどうしても大きくなり、ファイル内での閲覧性が悪くなってしまう。

そうした場合には以下のようなファイル分割を行うのが良さそうだ。
ファイルを分割したものの 3つとも同一ディレクトリにあり、 分散はしていない。

modules/
  ├ component1/
  |    ├ reducer.js
  |    ├ actions.js
  |    └ types.js
  └ component2/
       ├ reducer.js
       ├ actions.js
       └ types.js

これと似たディレクトリ構成として re-ducks と呼ばれるものがあるので、次章で紹介する。
上記のように分割したくなるケースでは、re-ducks の構成にするのがより一般的だろう。

パターン3. re-ducks

先にも述べたように、中・大規模のプロダクトになってくるとファイル1つが大きくなり、ducks の構成が辛くなってくる。
その解消のために re-ducks という構成が考え出された。

参考:

re-ducks では次のようなディレクトリ構成となり、ducksにおける modules/ は duck/ で置き換わる。

src/
  ├ components/
  |    ├ component1.js
  |    └ component2.js
  ├ containers/
  |    ├ component1.js
  |    └ component2.js
  └ duck/
       ├ component1/
       |    ├ index.js
       |    ├ recucers.js
       |    ├ actions.js
       |    ├ types.js
       |    ├ operations.js
       |    └ selectors.js
       └ component2/
            ├ index.js
            ├ recucers.js
            ├ actions.js
            ├ types.js
            ├ operations.js
            └ selectors.js

duck/ の下では component 毎にディレクトリが分かれ、それぞれにreducer.jsactions.jstypes.jsを置く。これにより、ducks の構成で問題となり得る 長大な単一ファイル を分割することで解消しつつも、密結合なファイルたちが同一ディレクトリに集まっているので 分散 はしていない。

re-ducks では新しくoperations.jsselectors.jsが登場するが、ここまでの話とは異なる動機で追加されるものなので、説明は省く (というか、そもそも私が詳しく知らないので上手く説明できない)。

ちなみにindex.jsはそれぞれのファイルに散らばった定義を import して export し直すだけのファイル。
外から使う際にはindex.jsから必要な全てを import できるようになる。

まとめ

  • Redux を用いたプロダクトのディレクトリ構成には、代表的なものとして redux-way, ducks, re-ducks の3つがある。
  • redux-way では、 reducers, action creators, action types について 過剰な分割 と 過剰な分散が起こり得る。
  • ducks では分割も分散もなく、非常に簡潔にまとまる。小規模プロダクトに向く。
  • re-ducks では 分割はするが分散はしない。中・大規模プロダクトに向く。

ducks の構成でプロダクトを始め、成長にあわせて re-ducks に切り替えるのが良さそうだ。

また、Redux を使う場合に限らず、 ディレクトリ構成を考える際には 分割 と 分散 の度合いを意識することでより良い構成へ近づけるだろう。

'frameworks > react' 카테고리의 다른 글

How the useEffect Hook Works  (0) 2020.03.28
Using the Effect Hook  (0) 2020.03.28
Using the State Hook  (0) 2020.03.28
React tutorial 강의 요약  (0) 2020.02.12
React vs Angular vs Vue.js — What to choose in 2019? (updated)  (0) 2020.01.30

https://qiita.com/h-yoshikawa/items/610ffea888f13275cde8

前回、DockerでReact Native環境を作成した記事を書いて多くの方に見ていただけたようなのですが、結局ホットリロードがうまく働かない、エミュレータ環境との連携がうまくいかない等、使い勝手がイマイチだったのでWSLで環境を作り直しました。
前回の記事:DockerでReact Native環境作成から、Expo Clientで実機確認するまで

基本的な作成手順は一緒なので内容が重複するところもありますが、エミュレータに関する記述も少しあわせて書いておきます。

前提

  • Node.jsがインストール済み(自分はanyenv + nodenvでいれてます)
  • npm or yarnコマンドが実行できる

環境構築手順

事前準備

React Nativeの開発支援サービスであるExpoを使用します。
Expoに関する説明は前回も書いたので省略します。

Expoアカウントを作成

expo.png

  1. expo.ioにいき、「Create an account」を選択。
  2. e-mail、ユーザ名、パスワードを入力して「Create your account」を選択。

Expo Clientをインストール

expo-client.png

  1. 使用するiOS/Android端末にApp Store/Google playからインストール。
  2. 作成したExpoアカウントでログインしておく。

Expo-Cliのインストール

npmもしくはyarnでインストールします。

# npm
$ npm install -g expo-cli

# yarn
$ yarn global add expo-cli

Expoプロジェクト

作成

appの部分は任意のExpoプロジェクトフォルダ名を指定します。

$ expo init app

テンプレートを選択します。

# expo init app
? Choose a template: (Use arrow keys)
  ----- Managed workflow -----
❯ blank                 a minimal app as clean as an empty canvas
  blank (TypeScript)    same as blank but with TypeScript configuration
  tabs                  several example screens and tabs using react-navigation
  ----- Bare workflow -----
  minimal               bare and minimal, just the essentials to get you started
  minimal (TypeScript)  same as minimal but with TypeScript configuration

Expoプロジェクトの表示名を聞かれます。

? Choose a template: expo-template-blank
? Please enter a few initial configuration values.
  Read more: https://docs.expo.io/versions/latest/workflow/configuration/ ‣ 0% completed
 {
   "expo": {
     "name": "<The name of your app visible on the home screen>",
     "slug": "app"
   }
 }

yarnを使ってパッケージをインストールするか聞かれます。Yでインストール実行。(nを選択するとnpmでパッケージをインストールします)

? Yarn v1.17.3 found. Use Yarn to install dependencies? (Y/n)

これでExpoプロジェクトのひな型が作成されました。
ちなみにExpoプロジェクト作成後のディレクトリ構成は以下のようになります。

例
ReactNative(大元のプロジェクトフォルダ)
├─ app(Expoプロジェクトフォルダ)
  ├─ .expo
  ├─ .expo-shared
  ├─ assets
  ├─ node_moodules
  ├─ .gitignore
  ├─ .watchmanconfig
  ├─ App.js
  ├─ app.json
  ├─ babel.config.js
  ├─ package.json
  ├─ yarn.lock

起動

環境変数

Expoサーバ起動時のIPを設定します。
デフォルトではUbuntuの方のIPを使用し、CONNECTIONをLANで接続するため、Expo Clientから接続することができません。(一応、サーバ起動後にCONNECTIONをTunnelにすることで接続することは可能です)

Ubuntu側で以下を~/.profileなど各種profileのいずれかに追記

export REACT_NATIVE_PACKAGER_HOSTNAME=(Windows側のIP)

各種profileの読み込み

例
source ~/.profile

Expoサーバ起動

以下のいずれかでExpoサーバとして起動。

$ expo start

$ npm start

$ yarn start

しばらくするとQRコードが表示されるとともに、Expo DevTools(localhost:19002)がブラウザで自動的に立ち上がります。

expo-start.png

動作確認

実機確認

Expoサーバ起動時に表示されたQRを、iOS/Android端末で読み込むことで、Expo Clientが立ち上がりビルドが始まります。
なお、注意点として、ExpoサーバになるPCとiOS/Android端末は同じネットワークにつないでいる必要があります。
しばらくしてビルドが終わると初期ガイドが表示されます。

app-guide

ガイドを消すと、Expoアプリの画面が表示されます。これが実行結果になります。

app-preview.png

なお、この状態でコードを変更すると、即座に再読み込みが走り画面に反映されます。
ライブリロードが走っているそうです。(ホットリロードとの違いがよくわかってません)

エミュレータでの確認

Windowsなので、Androidのエミュレータのみを書いておきます。

Android Studioのインストール

android-studio.png

1.公式からダウンロード。

2.ダウンロードしたインストーラを起動して、インストール。(少し時間がかかります)

3.インストールしたAndroid Studioを起動。初回はセットアップウィザードがあるので、案内に沿って進めていきます。
必要に応じてインストールするSDKを選択します。Andoroid SDK Build-ToolsAndroid EmulatorAndoroid SDK Platform-ToolsAndroid SDK Toolsなどは最初からチェックが入っているかと思います。
(選択したSDKをインストールするのに時間がかかります)

あとからSDKを追加したい場合は、トップ画面の右下の「Configure」→「SDK Manager」からSDKの一覧画面へ行けます。

環境変数の設定

1.Android Studioのトップ画面の右下の「Configure」→「SDK Manager」からSDKの一覧画面へ

2.Android SDK Locationを控えておく

3.環境変数を追記
Windowsのシステム環境変数に追記

ANDROID_SDK ... (※2でコピーしたパス)
PATH ... %ANDROID_SDK%\emulator と %ANDROID_SDK%\pratform_tools を追加

4.emulatorコマンドとadbコマンドが使えるか確認

Virtual Deviceの用意

1.Android Studioトップ画面の「Configure」→「AVD Manager」を選択

2.「Create Virtual Device」を選択

3.デバイスを選択して、システムイメージを選択して作成

エミュレータの起動

1.Android Studioトップ画面の「Configure」→「AVD Manager」からデバイス一覧へ

2.Actionsのプレイボタンを選択して起動

なお、コマンドでエミュレータを起動する場合は以下のコマンドでいけます。

# デバイス名の一覧確認
$ emulator -list-avds

# 指定デバイスのエミュレータの起動
$ emulator -avd (デバイス名)

3.Expoサーバを起動し、Expo DevTools(localhost:19002)の「Run on Android device/emulator」を選択
初回のみエミュレータにExpo Clientのインストールがあります。
また、エミュレータでも同様に、コードを変更すると再読み込みが走ります。


WSLで無事環境が作れたのはよかったのですが、Expoサーバを起動後にしばらくしてネットが繋がらなくなる?(一応繋がってはいるが、ネット上のページにアクセスできない)状態になるのが謎です...。PCを再起動したら直ります。
もしかしたらExpo関係ないかもしれないですけど、Expoのサーバ立ち上げたときしかその現象起きないんですよね。これさえなければいいのになぁ。

2019/10/7追記
これについて調べたことを記事に書きました。
WSLでexpo startして20~30分後にネットワーク不具合が起こる現象について調べたこと

https://qiita.com/karintou/items/52ee1f7c5fa641980188

はじめに

「とりあえずなんとかする方法は知っているけど、なんでそれでいいのかわからない」という疑問を解消していきます。
今回はCORS編です。
長くなるので、時間がない方は先に結論を見てください。

問題となるエラーについて

APIサーバーとWebサーバーをそれぞれ別ポートで立てている時、以下のようなエラーが出る場合があります。

CORS-Error

これは、開発環境でAngular(localhost:4200)からDjango(localhost:8000)へAPIリクエストを投げた時に起きるエラーです。
「APIサーバー(localhost:8000)へのCross-Originなリダイレクトは、Cross-Origin Resource Sharing policyによって拒否されました」と怒られます。
このエラーの意味するところから理解し、対策・解決方法を探っていきます。

Cross-Origin

オリジン

オリジンとは、「プロトコル」+「ホスト」+「ポート番号」の組み合わせのことです。
上記の例でいうと

サーバー プロトコル ホスト ポート番号
Angular http localhost 4200
Django http localhost 8000

ポート番号が異なるので、別のオリジンとなります。

Cross-Origin

では、なぜ別のオリジン間(Cross-Origin)の通信が問題になるのでしょうか。
CSRF(Cross Site Request forgeries)が考えられるからです。

CSRFは以下の手順で実行されます。

  1. 不正なアクセスを行うスクリプトを仕込んだページを用意する
  2. ユーザーにアクセスさせ、攻撃用ページを踏ませる
  3. アクセスしたユーザーから不正なアクセスが行われる

不正なアクセス自体はユーザーから送信されるので攻撃者は発覚しない、という手法です。

この攻撃方法は、あるドメインから読み込まれたページから、別のドメインへのアクセスが許可されている脆弱性をついています。
そのため、通常はオリジン間のHTTPリクエストは制限されます。
今回のエラーも、localhost:4200から読み込まれたページで実行される、localhost:8000へのHTTPリクエストが制限に引っかかったため生じました。

DjangoのCSRF対策

次に、DjangoのCSRF対策について確認します。
公式によると

  • settings.pyのMIDDLEWAREに 'django.middleware.csrf.CsrfViewMiddleware'が入っていると有効になる(デフォルトで有効)
  • POST, PUT, DELETEのような変更を伴うリクエストに対してチェックを行う
  • レスポンス時、クッキーに「csrftoken」というキーでトークンを発行する
  • リクエストに「X-CSRFToken」というヘッダーに発行したトークンが入っているかをチェック
    • 入っていたらアクセス許可
    • 入っていない、またはトークンが違う場合、403(Forbidden)エラーを返す

となっています。

CORS

次に、対策・解決方法を考えていきます。
オリジン間の制限を変更することで、別オリジン間のHTTPリクエストを許可すれば良いはずです。
この仕組みをCORS(Cross-Origin Resource Sharing)と言います。
設定可能なヘッダーについては公式ぺージを参考にしてください。

対策

基本的にデータを取得される側(APIサーバー)でアクセス許可設定を行います。
エラーの文面をもう一度見ると、
「localhost:4200はAccess-Control-Allow-Originで許可されていません」と怒られています。
なので、レスポンスに「Access-Control-Allow-Origin」というヘッダーを追加します。
後ほど別の方法は紹介しますが、テストのためにDjangoのViewを以下のように変更します。

# なんらかのレスポンスを返すView関数
def hoge(request):
    # ~~~なんらかのデータを取得してJsonResponseで返す~~~~
    response = JsonResponse(data)
    response['Access-Control-Allow-Origin'] = 'localhost:4200'
    return response

これでレスポンスが正常に返ってきます。
レスポンスヘッダーは以下のようになっています。

Access-Control-Allow-Origin Header

ただし、POST、DELETEなどのメソッドだとうまくいかないケースがあります。
エラーとしては以下のようになります。

Preflight Error

 

Preflight Header Error

見知らぬ単語「Preflight」が出てきました。次はこのエラーに取り掛かりましょう。

Preflight

特定のメソッドや、特定のヘッダーがリクエストに入っていると、実際のリクエストを投げる前に、別ドメインの送信相手に安全確認をとる仕様となっています。
この事前リクエストをPreflightリクエストと言います。
別ドメインからのPreflightリクエストに対し、APIサーバーがどのようなリクエストならアクセスを許可するかを返し、そのレスポンスが返ってきてから実際のリクエストを投げます。

上記のエラーは、

  1. Preflightリクエストに対するレスポンスが返ってきていない
  2. 実際のリクエストのContent-Typeヘッダーが許可されていない
    ことからエラーになっているようです。

先ほど確認したDjangoのCSRF対策と合わせて考えると、

  1. Preflightメソッドに対してレスポンスを返す
  2. クッキーの「csrftoken」をヘッダーに追加する

ことで解決するはずです。

これもまた別の方法がありますが、テストのためにViewを以下のように変更します。

# View関数
def hoge(request):
    # Preflightに対するレスポンスを返す(1)
    if request.method == 'OPTIONS':
        response = HttpResponse()
        response['Access-Control-Allow-Origin'] = 'http://localhost:4200'
        response['Access-Control-Allow-Credentials'] = 'true'
        response['Access-Control-Allow-Headers'] = "Content-Type, Accept, X-CSRFToken"
        response['Access-Control-Allow-Methods'] = "POST, OPTIONS"
        return response
    else:
        # ~~~なんらかの処理をしてJsonResponseで返す~~~~
        response = JsonResponse(data)
        response['Access-Control-Allow-Origin'] = 'localhost:4200' 
        response['Access-Control-Allow-Credentials'] = 'true'
        return response

また、Angular側で、ヘッダーにトークンを追加します。

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpXsrfTokenExtractor } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class HttpXsrfInterceptor implements HttpInterceptor {

  constructor(private tokenExtractor: HttpXsrfTokenExtractor) {
  }
  // httpリクエストに対してヘッダーを追加する(2)
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const headerName = 'X-CSRFToken';
    const token = this.tokenExtractor.getToken() as string;
    if (token !== null && !req.headers.has(headerName)) {
      req = req.clone({ headers: req.headers.set(headerName, token) });
    }
    return next.handle(req);
  }
}

また、app.module.tsも変更します。

@NgModule({

  imports: [
    // 追加。トークンが入っているクッキー名とヘッダー名を指定(2)
    HttpClientXsrfModule.withOptions({cookieName: 'csrftoken', headerName: 'X-CSRFToken'})
  ],
  providers: [
    // サービスの登録
    { provide: HTTP_INTERCEPTORS,useClass: HttpXsrfInterceptor,  multi: true },
  ],
}

また、HttpClientのpostメソッドのoptionsに{withCredentials: true}を追加します。
これでPOSTリクエストも成功します。

django-cors-headers

以上で別オリジンからのリクエストを正常に処理できるようになりました。
ただ、Djangoには便利なライブラリがあります。django-cors-headersです。
今までは確認のためにviewで返すリクエストに直接ヘッダーを追加していましたが、django-cors-headersを使用し、設定をすれば勝手にヘッダーを追加してくれます。

利用法

インストール
pip install django-cors-headers

設定追加

# 追加分のみ
INSTALLED_APPS = [
    'corsheaders'
]

# 上から順に実行されるので、CommonMiddleWareより上に挿入
MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
 ]

# 許可するオリジン
CORS_ORIGIN_WHITELIST = [
    'localhost:4200',
]
# レスポンスを公開する
CORS_ALLOW_CREDENTIALS = True

これだけです。
そのほかにも設定があるので、状況によって追加・変更を行います。

結論

  • django-cors-headersを使う
    • またはレスポンスにヘッダーを追加する
  • Angularで
    • クッキーに入っている「csrftoken」を「X-CSRFToken」ヘッダーに追加
    • HttpClientのメソッドオプションの「withCredentials」をtrueにする
    • HttpClientXsrfModule.withOptionsを設定する

以上で解決するはずです。
自分はAngular側の設定で時間を溶かしてしまいました...。
何かあればコメント・指摘等お願いします。

参考ページ

https://developer.mozilla.org/ja/docs/Web/HTTP/HTTP_access_control
https://www.trendmicro.com/ja_jp/security-intelligence/research-reports/threat-solution/csrf.html
https://stackoverflow.com/questions/46040922/angular4-httpclient-csrf-does-not-send-x-xsrf-token
https://github.com/ottoyiu/django-cors-headers

django-rest-framework(DRF)뭐야?

DRF는 장고프레임워크를 사용해 api를 간단하게 만들 수 있는 프레임워크다. aws를 해본사람은 알겠지만, api gateway같은 걸 장고프레임워크를 통해 자작, 커스터마이징한다고 생각하면 된다.

그런데, 간단히 사용할 수 있는 api프레임워크도 많다. 가령 flask 같은 거. 실제 현재 로컬 개발 중 flask를 통해 api를 반환시키고 있다. DRF는 왜 사용되는 걸까?

각종 사이트에서 소개하는 django-rest-framework(DRF)를 사용하는 이유

django-rest-framework공식 홈페이지에서 설명하는 DRF를 사용하는 이유

Django REST framework is a powerful and flexible toolkit for building Web APIs.

Some reasons you might want to use REST framework:

data-flair에서 설명하는 DRF를 사용하는 이유https://data-flair.training/blogs/django-rest-framework/

DRF makes it easier for a developer to debug their APIs. The other big feature of DRF is that it converts the Models into serializers. What we have done in this Django REST Framework tutorial is simply made classes and extended them with built-in classes. If we needed to have this kind of architecture, we would need so much more code in place. DRF made it so easy for us to serialize data.
 
The serializers not only take our models and transmit them as JSON objects. They also provide data from users to the backend. They automatically clean the data or validate data. This is a plus from DRF. Cleaned data removes all the security issues.
 
Believe me, the security provided by DRF is well established. Django REST Framework has many more features than explained here. They go well beyond the scope of just Django. You will need a better understanding of APIs and REST architecture. It is not so difficult but will take time.

내가 개인적으로 생각하는 DRF를 사용하는 이유

  1. serialize가 편하게 된다. 이거 편하다.
  2. api gateway는 managed서비스 이기 때문에 여러가지 중요 기능들이 사용자도 모르는 사이에 이미 잘 구현되어 있습니다. 가령, validation이나 security같은 것들이죠. 이걸 일일이 개발자가 구현해 api를 만든다고 하면 꽤나 복잡해집니다. validation이나 security같은 고급 기능 없이 flask는 단순히 http request를 패싱해주는 기능만 합니다. 여기에 validation이나 security기능들을 붙인다고 생각하면 개발자의 부담은 상당히 늘어나게 됩니다.
  3. rest api를 개발 하는데 있어서, rest api 규약을 지킬수 있도록 편리 기능들을 제공해줍니다.

React 주요 커맨드 알아보기

$> npm install -g create-react-app

$> cd {application directory}

$> create-react-app .

$> npm run start

$> npm run build

$> npx serve -s build
  • 디펜던시 관리, 디플로이, jslint등 모든 것을 알아서 해준다는게 참 편하다.
  • 스크립트는 package.json에 기재되어 있다.

react라이브러리에서 Component 오브젝트 상속받아 사용해보기 : Component Class

  • component 상속한 후, render함수를 override 한다.

    import React, {Component} from 'react';
    import './App.css';
    
    class Subject extends Component {
      render (){
          return (
              <header>
                  <h1>WEB</h1>
                  world wide web!
              </header>
          );
      }
    }
    
    class App extends Component {
      render (){
          return (
              <Subject></Subject>
          );
      }
    }
    
    export default App;
  • Component를 상속한 클래스를 태그 <> 안에 넣고 객체처럼 사용할 수 있다

  • react문법을 jsx문법이라고 부른다.

  • 계속해서 html을 Component를 이용해 객체화해보자.

    import React, {Component} from 'react';
    import './App.css';
    
    class Subject extends Component {
      render (){
          return (
              <header>
                  <h1>WEB</h1>
                  world wide web!
              </header>
          );
      }
    }
    
    class TOC extends Component {
      render() {
          return (
              <nav>
                  <ul>
                      <li><a href="1.html">HTML</a></li>
                      <li><a href="2.html">CSS</a></li>
                      <li><a href="3.html">JS</a></li>
                  </ul>
              </nav>
          );
      }
    }
    
    class Content extends Component {
      render() {
          return(
              <article>
                  <h2>HTML</h2>
                  HTML is Hyper Text Markup Language
              </article>
          )
      }
    }
    
    class App extends Component {
      render (){
          return (
              <div>
                  <Subject/>
                  <TOC/>
                  <Content/>
              </div>
          );
      }
    }
    
    export default App;
  • 각 태그들이 class에 의해 객체화 된 것을 볼 수 있다.

상위 Component에서 하위 Compoment로 값 넘겨주기 : props

위의 연습코드에서 App 클래스는 Subject클래스를 태그로 받았다.

이때 태그에 파라미터를 입력하고, Subject클래스에서 받을수는 없을까? 그렇게 할수만 잇다면 파라미터에 의해 내용이 변하는 재사용성이 높은 오브젝트를 만들 수 잇을 텐데말이다.

물론 가능하다. 아래 수정 전후의 소스코드를 눈여거 보길 바란다.

참고: react doc- Components and Props

수정전

...

class Subject extends Component {
    render (){
        return (
            <header>
                <h1>WEB</h1>
                world wide web!
            </header>
        );
    }
}

class App extends Component {
    render (){
        return (
            <div>
                <Subject/>
                <TOC/>
                <Content/>
            </div>
        );
    }
}

수정후

class Subject extends Component {
    render (){
        return (
            <header>
                <h1>{this.props.title}</h1>
                {this.props.sub}
            </header>
        );
    }
}

class App extends Component {
    render (){
        return (
            <div>
                <Subject title="WEB" sub="world wide web"/>
                <TOC/>
                <Content/>
            </div>
        );
    }
}

React developer tool

  1. chrome extention for react : React developer tool(https://reactjs.org/community/debugging-tools.html)
    • react 로 작성된 페이지에 대해, html태그만 으로는 파악하기 어려운 react components들을 보여준다. 대.박

Compoent파일로 분리하기

  • componets라는 디렉토리를 src직하에 만든후, class 별로 구분한 각 component 객체들을 파일별로 분리한다.
  • 이때 각 클래스의 export 방법은 다음과 같은 두 가지가 있다.
    1. export default {ClassName} : 디폴트로 export하는 방법으로, 각 모듈에서 하나의 모듈만 default 키워드를 사용할 수 있다.
    2. export {ClassName}

state 개념 살펴보기

  • Component를 다룰 때, 외부적으로는 Props를 사용한다는 것을 위에서 배웠다.
    state란 Component를 움직이게해주는 내부논리이다.

  • 코드상 state는 그저 초기화함수(constructor)에 포함된 프로퍼티로 보일 것이다. 하지만 내부적으로, state라는 이름의 프로퍼티는 특별하게 사용된다. 예를 들어, 생성자 바깥에서 setState과 같은 함수로 state의 내용을 바꿀 수 있다.

    수정전

    class App extends Component {
      render (){
          return (
              <div>
                  <Subject title="WEB" sub="world wide web"/>
                  <TOC/>
                  <Content/>
              </div>
          );
      }
    }

수정후

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            subject: {
                title: 'WEB',
                sub: 'world wide web'
            },
            content: {
                title: 'HTML',
                desc: 'HTML is Hyper Text Markup Language'
            }
        }
    }

    render (){
        return (
            <div>
                <Subject title={this.state.subject.title} sub={this.state.subject.sub}/>
                <TOC/>
                <Content title={this.state.content.title} desc={this.state.content.desc}/>
            </div>
        );
    }
}

export default App;
  • render에 하드코딩 된 부분을 state 인스턴트 프로퍼티를 이용해 은닉화했다.
  • jsx문법 내부에 자바스크립트를 사용하기 위해서는 {}중괄호를 사용하면 된다.

동적으로 html component 생성하기

App.js

import React, {Component} from 'react';
import Subject from './components/Subject'
import Content from './components/Content'
import TOC from './components/TOC'
import './App.css';

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            subject: {
                title: 'WEB',
                sub: 'world wide web'
            },
            toc: [
                {
                    id: 1,
                    title: 'HTML',
                    desc: 'HTML is Hyper Text Markup Language'
                },
                {
                    id: 2,
                    title: 'CSS',
                    desc: 'CSS is for design'
                },
                {
                    id: 3,
                    title: 'JAVASCRIPT',
                    desc: 'Javascript is for control'
                }
            ],
            content: {
                title: 'HTML',
                desc: 'HTML is Hyper Text Markup Language'
            },
        }
    }

    render (){
        return (
            <div>
                <Subject title={this.state.subject.title} sub={this.state.subject.sub}/>
                <TOC data={this.state.toc}/>
                <Content title={this.state.content.title} desc={this.state.content.desc}/>
            </div>
        );
    }
}

export default App;

TOC.js

import {Component} from "react";
import React from "react";

class TOC extends Component {
    render() {
        const data = this.props.data;

        const list = [];
        data.forEach((obj) => {
            list.push(
                <li><a href={`/content/${obj.id}`}>{obj.title}</a></li>
            )
        });

        return (
            <nav>
                <ul>
                    {list}
                </ul>
            </nav>
        );
    }
}

// export {TOC};
export default TOC;
  • 여러개의 list를 자동으로 생성할때는 아래와 같은 오류가 발생한다.

    Each child in a list should have a unique "key" prop.
  • 이때 각 리스트가 다른 리스트와 구별될 수 있는 식별자를 attribute로 주면 해결 된다.

    TOC.js

    import {Component} from "react";
    import React from "react";
    
    class TOC extends Component {  
    render() {  
    const data = this.props.data;
    
        const list = [];
        data.forEach((obj) => {
            list.push(
                <li key={obj.id}><a href={`/content/${obj.id}`}>{obj.title}</a></li>
                      )
                  });
    
                  return (
                      <nav>
                          <ul>
                              {list}
                          </ul>
                      </nav>
                  );
              }
          }
    
          // export {TOC};  
          export default TOC;

event의 onClick 구현하기

  • 먼저 이벤트의 이해를 쉽게하기 위해, 기존 Subject 클래스의 Component지정을 멈추고, tag를 하드 코딩하여 보도록하자.

app.js

...
    render (){
        let _title, _desc = null;
        if(this.state.mode === 'welcome') {
            _title = this.state.welcome.title;
            _desc = this.state.welcome.desc;
        }else {
            _title = this.state.content.title;
            _desc = this.state.content.desc;
        }

        return (
            <div>
                {/*<Subject title={this.state.subject.title} sub={this.state.subject.sub}/>*/}
                {/* Temporary subject tag */}
                <header>
                    <h1 href="/" onClick={(e) => {
                        alert('hi');
                        e.preventDefault();
                        debugger;
                    }}>{this.state.subject.title}</h1>
                    {this.state.subject.sub}
                </header>
                <TOC data={this.state.toc}/>
                <Content title={_title} desc={_desc}/>
            </div>
        );
    }
...
  • jsx코드 내부에 debugger라는 함수를 넣으면 개발자도구에서 멈춘다.
  • onClick attribute를 활용해 클릭 이벤트를 넣을 수 있다.
  • e는 이벤트가 사용할 수 있는 다양한 프로퍼티를 갖는다.
    • e.preventDefault()는 태그가 갖는 기본적인 움직임을 막는 역할은 한다. 위의 코드에서는 href를 무력화하여 페이지 이동을 막는다.

event에서 state 변경하기

  • 클릭 이벤트에 의해 mode값이 변경되는 코드를 짜보도록하자.
    app.js

    ...
      render (){
          let _title, _desc = null;
          if(this.state.mode === 'welcome') {
              _title = this.state.welcome.title;
              _desc = this.state.welcome.desc;
          }else {
              _title = this.state.content.title;
              _desc = this.state.content.desc;
          }
    
          return (
              <div>
                  {/*<Subject title={this.state.subject.title} sub={this.state.subject.sub}/>*/}
                  {/* Temporary subject tag */}
                  <header>
                      <h1 href="/" onClick={function(e) {
                          alert('hi');
                          e.preventDefault();
                          debugger;
                      }}>{this.state.subject.title}</h1>
                      {this.state.subject.sub}
                  </header>
                  <TOC data={this.state.toc}/>
                  <Content title={_title} desc={_desc}/>
              </div>
          );
      }
    ...
    
  • 위의 코드는 두 가지의 문제점을 가지고 있다.
    • 첫번째 : this.state.mode = 'welcome';에서 this는 클래스의 인스턴스가 아닌 아무것도 아닌 것이다.
      • -> bind()로 해결한다.
    • 두번째 : react는 state를 변경하기 위해 setState라는 함수를 가지고 있으므로, 그 함수를 사용하여야 한다.
      • 이때 포인트는 생성자에 들어있는 state프로퍼티가 단순한 프로퍼티가 아니라는 점이다.
        react내에서 state프로퍼티는 특별한 취급을 받는다. setState를 하면 단순히
        생성자의 값을 바꾸는 것이 아니라 setState가 호출된 후 render들을 다시 호출하여
        새로운 state로 화면을 구성한다. 예에서는, App -> TOC -> Content 의 render함수를
        새로 호출하여 변경된 값으로 새로운 화면을 구성한다.
...
    render (){
        let _title, _desc = null;
        if(this.state.mode === 'welcome') {
            _title = this.state.welcome.title;
            _desc = this.state.welcome.desc;
        }else {
            _title = this.state.content.title;
            _desc = this.state.content.desc;
        }

        return (
            <div>
                {/*<Subject title={this.state.subject.title} sub={this.state.subject.sub}/>*/}
                {/* Temporary subject tag */}
                <header>
                    <h1 href="/" onClick={function(e) {
                        alert('hi');
                        e.preventDefault();
                        this.setStart({
                            mode: 'welcome'
                        });
                        debugger;
                    }.bind(this)}>{this.state.subject.title}</h1>
                    {this.state.subject.sub}
                </header>
                <TOC data={this.state.toc}/>
                <Content title={_title} desc={_desc}/>
            </div>
        );
    }
...

event에서 bind함수 이해하기

  • 아래의 Component를 상속받은 App클래스 내에서 this는 무엇을 가르킬까

App.js

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            ...
        }    
    }

    render (){
        console.log('what is this?', this);

        ...

        return (

            ...
        );
    }
}
  • react에서 Component를 상속받을 경우 this는 App클래스를 가르키고 있다는 것을 알 수 있다.

  • (참고)일반적으로 this는 세가지의 다른 상황에 의해서 바인드 된다.
    this가 바인드되는 세 가지의 경우

    1. 객체 내부의 메서드에 바인딩 되는 경우
    2. 메서드 내부에서 바인딩 되는 경우
    3. 생성자 new로 생성해 그 인스턴스에 바인딩 되는 경우
  • 이때 onClick 내부에 this는 아무것도 가르키지 않는다.

     <h1 href="/" onClick={function(e) {
          alert('hi');
          e.preventDefault();
          this.setState({
              mode: 'welcome'
          });
          debugger;
      }.bind(this)}>{this.state.subject.title}</h1>
    
  • 따라서 상위 블록에 존재하는 this를 가져와 bind를 해줄 수 있다.

event에서 setState함수 이해하기

  • 동적으로 state값을 변경할 경우 this.setState를 사용하여야 한다.
  • 포인트는 생성자에 들어있는 state프로퍼티가 단순한 프로퍼티가 아니라는 점이다.
    react내에서 state프로퍼티는 특별한 취급을 받는다. setState를 하면 단순히
    생성자의 값을 바꾸는 것이 아니라 setState가 호출된 후 render들을 다시 호출하여
    새로운 state로 화면을 구성한다. 예에서는, App -> TOC -> Content 의 render함수를
    새로 호출하여 변경된 값으로 새로운 화면을 구성한다.

component 이벤트 만들기

  • 소스보고 알아서하기 별로 어렵지 않음

App.js

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            mode: 'read',
            subject: {
                title: 'WEB',
                sub: 'world wide web'
            },
            welcome: {
                title: 'welcome',
                desc: 'Hello, React'
            },
            toc: [
                {
                    id: 1,
                    title: 'HTML',
                    desc: 'HTML is Hyper Text Markup Language'
                },
                {
                    id: 2,
                    title: 'CSS',
                    desc: 'CSS is for design'
                },
                {
                    id: 3,
                    title: 'JAVASCRIPT',
                    desc: 'Javascript is for control'
                }
            ],
            content: {
                title: 'HTML',
                desc: 'HTML is Hyper Text Markup Language'
            },
        }
    }

    render (){
        let _title, _desc = null;
        if(this.state.mode === 'welcome') {
            _title = this.state.welcome.title;
            _desc = this.state.welcome.desc;
        }else {
            _title = this.state.content.title;
            _desc = this.state.content.desc;
        }
        console.log('what is this?', this);
        return (

            <div>
                <Subject
                    title={this.state.subject.title}
                    sub={this.state.subject.sub}
                    onChangePage={function() {
                        if(this.state.mode === 'welcome') {
                            this.setState({
                                mode: 'read'
                            })
                        } else {
                            this.setState({
                                mode: 'welcome'
                            })
                        }

                    }.bind(this)}
                />
                <TOC data={this.state.toc}/>
                <Content title={_title} desc={_desc}/>
            </div>
        );
    }
}

Subject.js

import {Component} from "react";
import React from "react";

class Subject extends Component {
    render (){
        return (
            <header>
                <h1 href="/" onClick={function(e) {
                    e.preventDefault();
                    this.props.onChangePage();
                }.bind(this)}>{this.props.title}</h1>
                {this.props.sub}
            </header>
        );
    }
}

export default Subject;

html의 어트리뷰트 값을 받아 서버에서 처리하기 : 클라이언트에서 값을 받아 서버로 전달하기

App.js

import React, {Component} from 'react';
import Subject from './components/Subject'
import Content from './components/Content'
import TOC from './components/TOC'
import './App.css';

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            mode: 'read',
            subject: {
                title: 'WEB',
                sub: 'world wide web'
            },
            content: {
                title: 'welcome',
                desc: 'Hello, React'
            },
            toc: [
                {
                    id: 1,
                    title: 'HTML',
                    desc: 'HTML is Hyper Text Markup Language'
                },
                {
                    id: 2,
                    title: 'CSS',
                    desc: 'CSS is for design'
                },
                {
                    id: 3,
                    title: 'JAVASCRIPT',
                    desc: 'Javascript is for control'
                }
            ]
        }
    }

    render (){
        console.log('App');
        return (
            <div>
                <Subject
                    title={this.state.subject.title}
                    sub={this.state.subject.sub}
                />
                <TOC
                    data={this.state.toc}
                    onChangePage={function(id) {
                        debugger;
                        for (var key of this.state.toc) {
                            debugger;
                            if (key.id === id) {
                                this.setState({
                                    content: {
                                        title: key.title,
                                        desc: key.desc
                                    }
                                });
                                break;
                            }

                        }
                    }.bind(this)}
                />
                <Content
                    title={this.state.content.title}
                    desc={this.state.content.desc}
                />
            </div>
        );
    }
}

export default App;

TOC.js

import {Component} from "react";
import React from "react";

class TOC extends Component {
    render() {
        console.log('TOC');
        const data = this.props.data;

        const list = [];
        data.forEach((obj) => {
            list.push(
                <li key={obj.id}
                    onClick={function(e) { // Event Listener
                        debugger;
                        e.preventDefault();
                        this.props.onChangePage(obj.id);
                    }.bind(this)}>
                    <a href={`/content/${obj.id}`}>{obj.title}</a>
                </li>
            )
        });

        return (
            <nav>
                <ul>
                    {list}
                </ul>
            </nav>
        );
    }
}

// export {TOC};
export default TOC;

이벤트 setState함수 이해하기

  • 상위 컴퍼넌트 (APP)가 하위 컴퍼턴트(TOC, Subject, Content)를 조작할 때는 props 사용
  • 컴퍼넌트가 자기 자신의 상태를 바꿀 때는 state
  • 하위 컴퍼턴트(TOC, Subject, Content)가 상위 컴퍼넌트 (APP)를 조작할 때는 event 사용
  • (props vs state 이미지)

create 구현, 소개 : 각 버튼 클릭시 mode 변경하기

App.js

import React, {Component} from 'react';
import Subject from './components/Subject'
import Content from './components/Content'
import TOC from './components/TOC'
import Control from './components/Control'
import './App.css';

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            mode: 'read',
            subject: {
                title: 'WEB',
                sub: 'world wide web'
            },
            content: {
                title: 'welcome',
                desc: 'Hello, React'
            },
            toc: [
                {
                    id: 1,
                    title: 'HTML',
                    desc: 'HTML is Hyper Text Markup Language'
                },
                {
                    id: 2,
                    title: 'CSS',
                    desc: 'CSS is for design'
                },
                {
                    id: 3,
                    title: 'JAVASCRIPT',
                    desc: 'Javascript is for control'
                }
            ]
        }
    }

    render (){
        console.log('App');
        return (
            <div>
                <Subject
                    title={this.state.subject.title}
                    sub={this.state.subject.sub}
                />
                <TOC
                    data={this.state.toc}
                    onChangePage={function(id) {
                        debugger;
                        for (var key of this.state.toc) {
                            debugger;
                            if (key.id === id) {
                                this.setState({
                                    content: {
                                        title: key.title,
                                        desc: key.desc
                                    }
                                });
                                break;
                            }

                        }
                    }.bind(this)}
                />
                <Control
                    onChangeMode={function(mode){
                        this.setState({
                           mode: mode
                        });
                    }.bind(this)}
                />
                <Content
                    title={this.state.content.title}
                    desc={this.state.content.desc}
                />
            </div>
        );
    }
}

export default App;

Control.js : 새로 생성된 파일

import {Component} from "react";
import React from "react";

class Control extends Component {


    render (){
        // 함수내의 this는 전역 windows를 가르키기 때문에 Control 클래스의 this로 바인드해주어야함
        const onClick = function(e, mode){
                e.preventDefault();
                this.props.onChangeMode(mode)
            }.bind(this);

        return (
            <div>
                <button href="/create" id="create" onClick={function(e){onClick(e, 'create')}}>create</button>
                <button href="/update" id="update" onClick={function(e){onClick(e, 'update')}}>update</button>
                <button id="delete" onClick={function(e){onClick(e, 'delete')}}>delete</button>
            </div>
        );
    }
}

export default Control;

동적으로 컴포넌트 바꿔넣기

App.js

import React, {Component} from 'react';
import Subject from './components/Subject'
import ReadContent from './components/ReadContent'
import CreateContent from './components/CreateContent'
import TOC from './components/TOC'
import Control from './components/Control'
import './App.css';

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            mode: 'read',
            subject: {
                title: 'WEB',
                sub: 'world wide web'
            },
            content: {
                title: 'welcome',
                desc: 'Hello, React'
            },
            toc: [
                {
                    id: 1,
                    title: 'HTML',
                    desc: 'HTML is Hyper Text Markup Language'
                },
                {
                    id: 2,
                    title: 'CSS',
                    desc: 'CSS is for design'
                },
                {
                    id: 3,
                    title: 'JAVASCRIPT',
                    desc: 'Javascript is for control'
                }
            ]
        }
    }

    render (){
        // Change Component according to mode
        let _article = null;
        if(this.state.mode === 'read') {
            _article = <ReadContent
                title={this.state.content.title}
                desc={this.state.content.desc}
            />
        } else if(this.state.mode === 'create') {
            _article = <CreateContent
                title={this.state.content.title}
                desc={this.state.content.desc}
            />
        }

        return (

            <div>
                <Subject
                    title={this.state.subject.title}
                    sub={this.state.subject.sub}
                />
                <TOC
                    data={this.state.toc}
                    onChangePage={function(id) {
                        for (var key of this.state.toc) {
                            if (key.id === id) {
                                this.setState({
                                    mode: 'read',
                                    content: {
                                        title: key.title,
                                        desc: key.desc
                                    }
                                });
                                break;
                            }

                        }
                    }.bind(this)}
                />
                <Control
                    onChangeMode={function(mode){
                        this.setState({
                           mode: mode
                        });
                    }.bind(this)}
                />
                { _article }
            </div>
        );
    }
}

export default App;
  • _article변수에 mode에 따라 Component객체가 바뀔 수 있도록 설정해줍니다.
    • 위의 코드에서는 mode에따라 ReadContent, CreateContent가 동적으로 _article에 담깁니다.
  • TOC component 내부에 mode: 'read'를 추가하여 list를 클릭했을 때 mode가 read로 바뀌도록 합니다.
  • 재밌는건 { _article } 부분인데, 변수에 Component객체를 넣고 그 변수를 사용할 수 있다는 점입니다.

input의 값을 추출해 서버로 가져오기

CreateContents.js

import {Component} from "react";
import React from "react";

class CreateContent extends Component {
    render() {
        console.log('CreateContent');
        return(
            <article>
                <h2>Create</h2>
                <form action="/create" method="post"
                      onSubmit={function(e) {
                            e.preventDefault();
                            debugger;
                            let title = e.target.title.value;
                            let desc = e.target.desc.value;
                            this.props.onSubmit(title, desc);
                            alert('submit');
                        }.bind(this)}>

                    <p>
                        <input type="text" name="title" placeholder="title"/>
                    </p>
                    <p>
                        <textarea name="desc" placeholder="description"/>
                    </p>
                    <p>
                        <input type="submit"/>
                    </p>

                </form>
            </article>
        )
    }
}

export default CreateContent;
  • html의 값들은 e를 경유해서 받을 수 있다. 크롬 개발자 모드에서 e를 탐색해 보면
    많은 값들이 들어있는데, form값은 target 변수 안에 들어있다.
  • 이때 중요한 것은 e는 각 태그의 name attribute 값을 보고 있는 것이다.
  • 첫번째 input 태그의 name은 title 이였기 때문에 e.target.title.value으로 그 값을 추출할 수 있다.

App.js

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            mode: 'read',
            subject: {
                title: 'WEB',
                sub: 'world wide web'
            },
            content: {
                title: 'welcome',
                desc: 'Hello, React'
            },
            toc: [
                {
                    id: 1,
                    title: 'HTML',
                    desc: 'HTML is Hyper Text Markup Language'
                },
                {
                    id: 2,
                    title: 'CSS',
                    desc: 'CSS is for design'
                },
                {
                    id: 3,
                    title: 'JAVASCRIPT',
                    desc: 'Javascript is for control'
                }
            ]
        }
    }

    render (){
        // Change Component according to mode
        let _article = null;
        if(this.state.mode === 'read') {
            _article = <ReadContent
                title={this.state.content.title}
                desc={this.state.content.desc}
            />
        } else if(this.state.mode === 'create') {
            _article = <CreateContent
                title={this.state.content.title}
                desc={this.state.content.desc}
                onSubmit={function(title, desc) {
                    let lToc = this.state.toc;

                    let addedConent = lToc.concat(
                        {
                            id: (lToc.length)+1,
                            title: title,
                            desc: desc
                        }
                    );

                    this.setState({
                            toc: addedConent
                        }
                    );
                    console.log(this.state.toc);
                    this.forceUpdate();
                }.bind(this)
                }
            />
        }

        return
        (
            <div>
                <Subject
                    title={this.state.subject.title}
                    sub={this.state.subject.sub}
                />
                <TOC
                    data={this.state.toc}
                    onChangePage={function(id) {
                        for (var key of this.state.toc) {
                            if (key.id === id) {
                                this.setState({
                                    mode: 'read',
                                    content: {
                                        title: key.title,
                                        desc: key.desc
                                    }
                                });
                                break;
                            }

                        }
                    }.bind(this)}
                />
                <Control
                    onChangeMode={function(mode){
                        this.setState({
                           mode: mode
                        });
                    }.bind(this)}
                />
                { _article }
            </div>
        );
    }
}

export default App;
  • 여기서 this.setState의 또다른 특징을 알 수 있다.

     this.setState({
             toc: addedConent
         }
     );
    • setState로 toc에 새로운 값을 넣어줬음에도 불구하고, console.log(this.state.toc);
      변화되기 전의 toc 값을 나타내고 있다. 이는 state가 불변성을 유지하기 때문이다.
    • 따라서 toc값을 변화시킬때, push와 같은 본 객체의 불변성을 훼손시키는 값으로 객체를 변화시키는 것보다
      새로운 독립된 객체를 만들어서 setState로 값을 전달하는 것을 추천한다.

shouldComponentUpdate 함수

  • shouldComponentUpdate함수의 특징
    1. render 이전에 shouldComponentUpdate함수가 실행된다.
    2. shouldComponentUpdate함수의 return값이 true이면 render가 호출되고,
      false이면 render는 호출되지 않는다.
    3. shouldComponentUpdate의 인수를 통해,
      새롭게 업데이트된 props의 값과 초기props의 값에 접근할 수 있다.

TOC.js

import {Component} from "react";
import React from "react";

class TOC extends Component {
    shouldComponentUpdate(newProps, newState) {
        console.log('shouldComponentUpdate');
        if(newProps.data == this.props.data) {
            return false;
        }else {
            return true;
        }
    }

    render() {
        console.log('TOC');
        const data = this.props.data;

        const list = [];
        data.forEach((obj) => {
            list.push(
                <li key={obj.id}
                    onClick={function(e) { // Event Listener
                        e.preventDefault();
                        this.props.onChangePage(obj.id);
                    }.bind(this)}>
                    <a href={`/content/${obj.id}`}>{obj.title}</a>
                </li>
            )
        });

        return (
            <nav>
                <ul>
                    {list}
                </ul>
            </nav>
        );
    }
}
// export {TOC};
export default TOC;
  • 앞에서 본바에 의하면 setState로 인해 state값에 변화가 인지될시
    모든 render값이 재호출 되게 된다. 하지만 이것은 값이 변화하지 않은 Component에 대해서도
    render을 호출하기 때문에 매우 비효율적이다.
    • shouldComponentUpdate의 3번의 특징을 이용해 값이 변화하지 않은 Component값에
      대해서는 render가 시행되지 않도록 하게 컨트롤할 수 있다.
  • state값의 원본을 초기값을 유지하지 않으면 위와같은 컨트롤이 불가능하므로
    state값은 초기값을 불변값으로써 유지하는 것이 중요하다.

immutable 불변성

  • 불변성을 유지할 수 있도록 값을 복사해주는 함수들

    • 배열 : Array.from(a)

      const a = [1,2,3,4];
      const b = Array.from(a);
      console.log(b, a===b)
      // Array from은 배열값을 복사하지만 단순히 a를 참조하는 참조값이 아닌 deepCopy이다.
    • 객체 : Object.assign({}, a)

      const a = {a:1, b:2};
      const b = Object.assign({}, a)
      console.log(b, a===b)
      //  Object.assign는 객체값을 복사하지만 단순히 a를 참조하는 참조값이 아닌 deepCopy이다.

update기능 추가하기

app.js

import React, {Component} from 'react';
import Subject from './components/Subject'
import ReadContent from './components/ReadContent'
import CreateContent from './components/CreateContent'
import UpdateContent from './components/UpdateContent'
import TOC from './components/TOC'
import Control from './components/Control'
import './App.css';

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            mode: 'read',
            subject: {
                title: 'WEB',
                sub: 'world wide web'
            },
            content: {
                id: 0,
                title: 'welcome',
                desc: 'Hello, React'
            },
            toc: [
                {
                    id: 1,
                    title: 'HTML',
                    desc: 'HTML is Hyper Text Markup Language'
                },
                {
                    id: 2,
                    title: 'CSS',
                    desc: 'CSS is for design'
                },
                {
                    id: 3,
                    title: 'JAVASCRIPT',
                    desc: 'Javascript is for control'
                }
            ]
        }
    }

    getContent() {
        // Change Component according to mode
        let _article = null;
        if(this.state.mode === 'read') {
            _article = <ReadContent
                title={this.state.content.title}
                desc={this.state.content.desc}
            />
        }else if(this.state.mode === 'create') {
            _article = <CreateContent
                onSubmit={function(title, desc) {
                    let lToc = this.state.toc;

                    let addedConent = lToc.concat(
                        {
                            id: (lToc.length)+1,
                            title: title,
                            desc: desc
                        }
                    );

                    this.setState({
                            toc: addedConent
                        }
                    );
                    console.log(this.state.toc);
                }.bind(this)
                }
            />
        }else if(this.state.mode === 'update') {
            _article = <UpdateContent
                title={this.state.content.title}
                desc={this.state.content.desc}
                id={this.state.content.id}

                onSubmit={function(id, title, desc) {
                    const toc = Array.from(this.state.toc);
                    console.log(typeof(toc));
                    toc.forEach(function(obj) {
                        if (obj.id === id) {
                            obj.title = title;
                            obj.desc = desc;
                            return;
                        }
                    });

                    const content = Array.from(this.state.content);
                    content.id = id;
                    content.title = title;
                    content.desc = desc;

                    this.setState({
                        mode: 'read',
                        content: content,
                        toc: toc
                    });
                }.bind(this)
                }
            />

        }

        return _article;
    }

    render() {
        return(
            <div>
                <Subject
                    title={this.state.subject.title}
                    sub={this.state.subject.sub}
                />
                <TOC
                    data={this.state.toc}
                    onChangePage={function(id) {
                        for (var key of this.state.toc) {
                            if (key.id === id) {
                                this.setState({
                                    mode: 'read',
                                    content: {
                                        id: id,
                                        title: key.title,
                                        desc: key.desc
                                    }
                                });
                                break;
                            }
                        }
                    }.bind(this)}
                />
                <Control
                    onChangeMode={function(mode){
                        this.setState({
                            mode: mode
                        });
                    }.bind(this)}
                />
                { this.getContent() }
            </div>
        );
    }
}

export default App;
  • update후에는 바뀐 내용을 content에 덮어씌우고 setState로 re-rendering한다.

UpdateContent.js

import {Component} from "react";
import React from "react";

class UpdateContent extends Component {
    render() {
        console.log('CreateContent');
        return(
            <article>
                <h2>Update</h2>
                <form action="/create" method="post"
                      onSubmit={function(e) {
                            e.preventDefault();
                            debugger;
                            let id = this.props.id;
                            let title = e.target.title.value;
                            let desc = e.target.desc.value;
                            this.props.onSubmit(id, title, desc);
                            alert('submit');
                        }.bind(this)}>

                    <p>
                        <input type="text" name="title" placeholder="title" defaultValue={this.props.title}/>
                    </p>
                    <p>
                        <textarea name="desc" placeholder="description" defaultValue={this.props.desc}/>
                    </p>
                    <p>
                        <input type="submit"/>
                    </p>

                </form>
            </article>
        )
    }
}

export default UpdateContent;

https://medium.com/@seungha_kim_IT/typescript-enum%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0-3b3ccd8e5552




TypeScript enum을 사용하는 이유


(본 글은 TypeScript 입문자 중 enum 기능이 있는 다른 언어를 사용해 본 경험이 없는 분들을 위해 쓰여졌습니다. 예제 코드를 TypeScript playground에 붙여 넣고, 마우스 포인터를 변수 위에 둬서 변수의 타입이 어떻게 지정되는지 확인해보세요.)

TypeScript enum은 JavaScript에는 없는 개념으로, 일견 상수, 배열, 혹은 객체와 비슷해보이기도 합니다. 그냥 상수, 배열, 객체를 써도 될 것 같은데, 굳이 enum을 쓰는 이유가 뭘까요?

Enum은 추상화의 수단

다국어 지원을 위해 언어 코드를 저장할 변수를 만들어야 하는 상황을 생각해봅니다.

const code: string = 'en' // string?

제품이 지원할 수 있는 언어는 사전에 정해져있는데, 이 값이 할당될 변수를 string 타입으로 지정하는 것은 범위가 너무 넓은 것 같다는 느낌이 듭니다. 이를 개선해봅시다.

일단은 리터럴 타입과 유니온을 이용해 code 변수에 대한 타입의 범위를 좁혀줄 수 있습니다.

type LanguageCode = 'ko' | 'en' | 'ja' | 'zh' | 'es'const code: LanguageCode = 'ko'

이제 code 변수에 ‘zz’ 같이 이상한 값을 대입하면 컴파일 에러가 납니다. 타입의 범위는 원하는대로 좁혀졌습니다.

하지만 코딩을 하다보니, 제품이 어떤 언어를 지원하기로 했었는지도 가물가물하고, 특정 국가 코드가 정확히 어떤 언어를 가리키는지 일일이 외우기도 쉽지 않습니다. 이 때 상수를 여러 개 둬서 문제를 해결할 수는 있지만, 그닥 깔끔한 느낌은 아닙니다.

const korean = 'ko'
const english = 'en'
const japanese = 'ja'
const chinese = 'zh'
const spanish = 'es'
// 이렇게 하면 언어 코드가 위아래에 중복되고
type LanguageCode = 'ko' | 'en' | 'ja' | 'zh' | 'es'
// 이렇게 하면 코드가 너무 길어집니다
// type LanguageCode = typeof korean | typeof english | typeof japanese | typeof chinese | typeof spanish
const code: LanguageCode = korean

이런 경우에 enum을 사용해 리터럴의 타입과 값에 이름을 붙여서 코드의 가독성을 크게 높일 수 있습니다.

export enum LanguageCode {
korean = 'ko',
english = 'en',
japanese = 'ja',
chinese = 'zh',
spanish = 'es',
}
// 여기서
// LanguageCode.korean === 'ko'
// (의미상) LanguageCode === 'ko' | 'en' | 'ja' | 'zh' | 'es'
const code: LanguageCode = LanguageCode.korean

짧은 코드로 타입의 범위도 좁히고, 가독성도 높일 수 있게 되었습니다.

enum은 객체

TypeScript enum은 그 자체로 객체이기도 합니다.

const keys = Object.keys(LanguageCode) // ['korean', 'english', ...]const values = Object.values(LanguageCode) // ['ko', 'en', ...]

그렇다면 그냥 객체를 사용하는 것과는 어떤 차이점이 있을까요?

1. 객체는 (별다른 처리를 해주지 않았다면) 속성을 자유로이 변경할 수 있는데 반해, enum의 속성은 변경할 수 없습니다.

2. 객체의 속성은 (별다른 처리를 해주지 않았다면) 리터럴의 타입이 아니라 그보다 넓은 타입으로 타입 추론이 이루어집니다. enum은 항상 리터럴 타입이 사용됩니다.

3. 객체의 속성 값으로는 JavaScript가 허용하는 모든 값이 올 수 있지만, enum의 속성 값으로는 문자열 또는 숫자만 허용됩니다.

정리하자면, 같은 ‘종류’를 나타내는 여러 개의 숫자 혹은 문자열을 다뤄야 하는데, 각각 적당한 이름을 붙여서 코드의 가독성을 높이고 싶다면 enum을 사용하세요. 그 외의 경우 상수, 배열, 객체 등을 사용하시면 됩니다.

ps. 다만, 객체 리터럴에 대해 상수 단언(const assertion)을 해준다면 이 객체를 enum과 비슷한 방식으로 사용할 수 있습니다. 상수 단언에 대해서는 제가 쓴 다른 글도 참고해주세요.

const languageCodes = {
korean: 'ko',
english: 'en',
japanese: 'ja',
chinese: 'zh',
spanish: 'es',
} as const // const assertion
// 속성 값을 변경할 수 없음
// 속성의 타입으로 리터럴 타입이 지정됨
type LanguageCode = typeof languageCodes[keyof typeof languageCodes]const code: LanguageCode = languageCodes.korean

ps. TypeScript에는 enum의 속성 값을 명시적으로 지정해주지 않아도 자동으로 0부터 시작하는 정수들이 지정되는 기능이 있습니다. 한 번 검색해보세요.

'frameworks > typescript' 카테고리의 다른 글

TypeScript 핸드북 1 - 기본 타입  (0) 2019.04.24
tsconfig 컴파일 옵션 정리  (0) 2019.04.05
interface와 class의 상속관계 증명  (0) 2019.02.07
[typescript]decorator  (0) 2018.12.11
[typescript]iterator  (0) 2018.12.11

https://medium.com/@TechMagic/reactjs-vs-angular5-vs-vue-js-what-to-choose-in-2018-b91e028fa91d

React vs Angular vs Vue.js — What to choose in 2019? (updated)

TechMagic
TechMagic
Mar 16, 2018 · 5 min read

Some time ago we published an article with a comparison of Angular and React. In that article, we showed the pros and cons of these frameworks and suggested what to choose in 2018 for particular purposes. So, what is the situation in the frontend garden in 2019?

JavaScript frameworks are developing at an extremely fast pace, meaning that today we have frequently updated versions of Angular, React and another player on this market — Vue.js.

We analyzed the number of open positions worldwide that require a specific knowledge of a certain framework. As a source, we took Indeed.com and got the following distribution according to more than 60,000 job offers.

Taking into account the following data, we decided to share the main advantages and disadvantages of every frontend framework and help tech professionals or engineers to choose the best one for their development needs.

Pros and cons of Angular

Angular is a superheroic JavaScript MVVM framework, founded in 2009, which is awesome for building highly interactive web applications.

Benefits of Angular:

  • Angular’s created to be used alongside with Typescript. And has exceptional support for it.
  • Angular-language-service — which allows intelligence and autocomplete inside of component external HTML template files.
  • New features like a generation of Angular based npm libraries from CLI, generation, and development of WebComponents based on Angular.
  • Detailed documentation that allows getting the all necessary information for the individual developer without asking his colleagues. However, this requires more time for education.
  • One-way data binding that enables singular behaviour for the app which minimized risks of possible errors.
  • MVVM (Model-View-ViewModel) that allows developers to work separately on the same app section using the same set of data.
  • Dependency injection of the features related to the components with modules and modularity in general.
  • Structure and architecture specifically created for great project scalability

Drawbacks of Angular:

  • Variety of different structures(Injectables, Components, Pipes, Modules etc.) makes it a bit harder to learn in comparison with React and Vue.js, which have an only “Component” in mind.
  • Relatively slower performance, according to different benchmarks. On the other hand, it can be easily tackled by utilizing so-called “ChangeDetectionStrategy”, which helps to control the rendering process of components manually.

Companies that use Angular: Companies that use Angular: Microsoft, Autodesk, MacDonald’s, UPS, Cisco Solution Partner Program, AT&T, Apple, Adobe, GoPro, ProtonMail, Clarity Design System, Upwork, Freelancer, Udemy, YouTube, Paypal, Nike, Google, Telegram, Weather, iStockphoto, AWS, Crunchbase.

Pros and cons of React

React is a JavaScript library, open sourced by Facebook in 2013, which is great for building modern single-page applications of any size and scale.

Benefits of React:

  • Easy to learn, thanks to its simple design, use of JSX (an HTML-like syntax) for templating, and highly detailed documentation.
  • Developers spend more time writing modern JavaScript, and less time worrying about the framework-specific code.
  • Extremely fast, courtesy of React’s Virtual DOM implementation and various rendering optimizations.
  • Great support for server-side rendering, making it a powerful framework for content-focused applications.
  • First-class Progressive Web App (PWA) support, thanks to the `create-react-app` application generator.
  • Data-binding is one-way, meaning less unwanted side effects.
  • Redux, the most popular framework for managing application state in React, is easy to learn and master.
  • React implements Functional Programming (FP) concepts, creating easy-to-test and highly reusable code.
  • Applications can be made type-safe with either Microsoft’s TypeScript or Facebook’s Flow, with both featuring native support for JSX.
  • Migrating between versions is generally very easy, with Facebook providing “codemods” to automate much of the process.
  • Skills learned in React can be applied (often directly) to React Native development.

Drawbacks of React:

  • React is unopinionated and leaves developers to make choices about the best way to develop. This can be tackled by strong project leadership and good processes.
  • The community is divided on the best way to write CSS in React, split between traditional stylesheets (CSS Modules) and CSS-in-JS (i.e. Emotion and Styled Components).
  • React is moving away from class-based components, which may be a barrier for developers more comfortable with Object Oriented Programming (OOP).
  • Mixing templating with logic (JSX) can be confusing for some developers at first.

Companies that use React: Facebook, Instagram, Netflix, New York Times, Yahoo, Khan Academy, Whatsapp, Codecademy, Dropbox, Airbnb, Asana, Atlassian, Intercom, Microsoft, Slack, Storybook, and many more.

Pros and cons of Vue.js

Vue.js is a JavaScript framework, launched in 2013, which perfectly fits for creating highly adaptable user interfaces and sophisticated Single-page applications.

Benefits of Vue.js:

  • Empowered HTML. This means that Vue.js has many similar characteristics with Angular and this can help to optimize HTML blocks handling with the use of different components.
  • Detailed documentation. Vue.js has very circumstantial documentation which can fasten learning curve for developers and save a lot of time to develop an app using only the basic knowledge of HTML and JavaScript.
  • Adaptability. It provides a rapid switching period from other frameworks to Vue.js because of the similarity with Angular and React in terms of design and architecture.
  • Awesome integration. Vue.js can be used for both building single-page applications and more difficult web interfaces of apps. The main thing is that smaller interactive parts can be easily integrated into the existing infrastructure with no negative effect on the entire system.
  • Large scaling. Vue.js can help to develop pretty large reusable templates that can be made with no extra time allocated for that according to its simple structure.
  • Tiny size. Vue.js can weight around 20KB keeping its speed and flexibility that allows reaching much better performance in comparison to other frameworks.

Drawbacks of Vue.js:

  • Lack of resources. Vue.js still has a pretty small market share in comparison with React or Angular, which means that knowledge sharing in this framework is still in the beginning phase.
  • Risk of over flexibility. Sometimes, Vue.js might have issues while integrating into huge projects and there is still no experience with possible solutions, but they will definitely come soon.

Companies that use Vue.js: Xiaomi, Alibaba, WizzAir, EuroNews, Grammarly, Gitlab and Laracasts, Adobe, Behance, Codeship, Reuters.

CONCLUSION

For a real engineer, there is no substantial difference in which framework to choose, because it just takes some time to get used to the new one. In our company, we grow expertise in mostly React and Angular, but Vue.js is also on board. Every framework has its own pros and cons, meaning that there should be just the right choice for every single case during product development.

'frameworks > react' 카테고리의 다른 글

How the useEffect Hook Works  (0) 2020.03.28
Using the Effect Hook  (0) 2020.03.28
Using the State Hook  (0) 2020.03.28
Redux도입시 디렉토리 구조의 3가지 베스트 프렉티스 패턴  (0) 2020.03.16
React tutorial 강의 요약  (0) 2020.02.12

+ Recent posts