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

https://blueshw.github.io/2016/03/03/django-using-custom-templatetags/



커스텀 템플릿태그(templatetags) 활용하기
March 03, 2016
웹 개발을 하다보면, html 코드 상에서 다양한 연산을 해야하는 경우가 발생합니다. 그래서 php, jsp, asp, jade 등 각 언어별 웹 프레임워크에서 이와 같은 경우를 처리해주기 위한 기능을 제공하고 있습니다. 장고(django) 템플릿(template)에서도 위와 같은 웹 프레임워크와 같이 동일한 기능을 지원하는 템플릿태그(templatetags)라는 것이 있습니다. 장고의 템플릿태그는 다른 웹 프레임워크와 마찬가지로 기본적으로 개발자가 필요한 기능은 대부분 제공하고 있습니다.

웹 프레임워크가 기본적인 기능을 대부분 제공하고 있지만, 개발을 하다보면 자신이 원하는 기능이 없는 경우가 간혹 있습니다. 그래서 장고에서는 개발자가 커스텀으로 템플릿태그를 만들수 있는 기능을 제공하고 있습니다.

우선 아래와 같이 앱(app) 아래에 templatetags 라는 폴더를 만들어 줍니다. temaplatetags 라는 폴더 이름은 고정값이므로 반드시 동일하게 생성합니다.

proj/
	app/
		__init__.py
		models.py
		view.py
		templatetags/
			__init__.py
			custom_tags.py    (커스텀 템플릿태그를 저장할 모듈 파일)

이때, app 은 반드시 setting 파일의 INSTALLED_APPS 에 추가가 되어 있어야 합니다. 그리고 한가지 주의할 점은 여러 앱에 각각 templatetags 가 있는 경우, 모듈의 이름이 겹치지 않도록해야 합니다. 이유는, template 에서 커스텀 태그는 앱의 위치와 상관없이 모듈 이름으로 로드되므로 이름이 겹치게 되면 충돌이 발생하게 됩니다. 즉,

proj/
	app/
		templatetags/
			custom_tags.py
	common/
		templatetags/
			common_tags.py    (태그모듈 이름이 겹치지 않도록 주의!!!)

태그 모듈을 사용하는 방법은 간단합니다. 커스텀 태그를 사용하고자하는 템플릿 파일의 상단에 아래와 같이 한줄만 추가해주면 됩니다.

{% load custom_tags %}

그렇다면 실제로 커스텀 태그를 만들어서 사용하는 예제를 만들어 보도록 하겠습니다. custom_tags.py 파일을 열어 사용하려는 태그 이름으로 메서드 이름으로 지정하여 만들어줍니다.

@register.filter            # 1
def add_str(left, right):
return left + right

@register.simple_tag            # 2
def today():
return datetime.now().strftime("%Y-%m-%d %H:%M")

@register.assignment_tag            # 3
def max_int(a, b):
return max(int(a), int(b))

@register.inclusion_tag(div.html, takes_context=True)            # 4
def include_div(context):
return {
'div_param': context['param']
}

대략 위의 4 가지 태그로 구분할 수 있는데, 각 태그에 따라서 사용법이 조금씩 다릅니다. 이 4 가지 커스텀 태그만 이용하면 웬만한 기능은 다 만들어 낼 수 있습니다. 번호별 사용법은 아래와 같습니다.

# 1.
# filter 태그는 앞의 값(left)에다가 뒤의 값(right)을 연산하는 태그입니다.
# filter이기 때문에 여러개의 필터를 붙여서 사용가능합니다.
# add_str 메서드의 left 파라미터가 prefix에 해당하고, right 파라미터가 url에 해당합니다.
# 결과적으로 prefix + url이 add_str 메서드를 통해 div의 text가 되는 것이지요.

<div>
{{ prefix | add_str: url | add_str: name | add_str: params }}
</div>

# 2.
# simple_tag는 단순히 어떤 특정값을 출력합니다.
# 아래와 같이 today를 입력하면, "2016-3-2 10:00"과 같이 현재 시간이 출력됩니다.

<div>
{{ today }}
</div>

# 3.
# assignment_tag는 템플릿에서 사용가능한 변수에 결과를 저장하는 역할을 합니다.
# 어찌보면 with 태그와 유사한 형태라 할 수 있으나, with과는 다르게 {% endwith %} 처럼 끝을 맺어줄 필요가 없습니다.
# 즉, 좀 더 간편하게 변수를 설정해 줄 수 있고, 필요한 기능을 태그 모듈에 별도로 삽입할 수 있다는 장점이 있습니다.

{% max_int first_count second_count as max_count %}

# 4.
# inclusion_tag는 저도 프로젝트에 직접 사용해 보진 않았지만, 테스트는 해보았습니다.
# 간략히 설명해서 inclusion_tag를 사용하면 데코레이터의 첫번째 파라미터인 템플릿을 호출하여 부모 템플릿에 출력합니다.
# 이때, 호출되는 템플릿에 부모 템플릿(호출하는 템플릿)의 각종 파라미터를 전달해 줄 수 있습니다.
# 데코레이터의 takes_context=True로 설정해주면,
# 부모 템플릿의 context의 값을 가져와 호출하는 템플릿으로 전달할 수 있습니다.

parent.html
{{ include_div }}

div.html
<div>{{ div_param }}</div>

이상 커스텀 태그의 종류와 사용법에 대해서 알아보았습니다. 제가 설명드린 커스텀 태그는 아주 기초적인 부분이라 제작 및 사용법이 아주 간단한데요. 커스텀 태그 파일은 파이썬 모듈이기 때문에 파이썬에서 사용할 수 있는 내장함수와 모든 확장 모듈을 사용할 수 있기 때문에, 얼마든지 복잡하고 파워풀한 기능을 가진 태그를 만들어 낼 수 있습니다.

하지만, 복잡한 연산을 처리하는 것은 템플릿보다는 웹서버 단에서 처리하는 것이 우선이고, 서버에서 처리가 곤란하거나 불가피한 상황인 경우에 태그를 사용해서 처리하는 것이라 생각합니다. 아마 장고에서도 사용가능한 기본 태그를 최소한으로 만들어 놓은 것도 같은 이유 때문일 거라 생각이 드네요.


+ Recent posts