20210303のReactに関する記事は5件です。

FirebaseSDK初期化の書き方

firebaseSDKを初期化するときのメモです

client.js
import firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";
import "firebase/storage";
import "firebase/analytics";
import "firebase/performance";

const firebaseConfig = {
  apiKey: process.env.FIREBASE_API_KEY,
  authDomain: process.env.FIREBASE_AUTH_DOMAIN,
  databaseURL: process.env.FIREBASE_DATABASE_URL, // If you need rtdb
  projectId: process.env.FIREBASE_PROJECT_ID,
  storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.FIREBASE_APP_ID,
  // measurementId: process.env.MEASUREMENT_ID // If you need it
};

if (!firebase.apps.length) {
  firebase.initializeApp(firebaseConfig);
  if (typeof window !== "undefined") {
    if ("measurementId" in firebaseConfig) {
      firebase.analytics();
      firebase.performance();
    }
  }
}

export default firebase;

環境変数(自分用)

FIREBASE_API_KEY=
FIREBASE_AUTH_DOMAIN=
FIREBASE_PROJECT_ID=
FIREBASE_STORAGE_BUCKET=
FIREBASE_APP_ID=
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

『Django API× React 』でSNSアプリ作成 ~React徹底解剖・デプロイ編~

背景

話題のPython/Djangoに興味を持ったので勉強をはじめました。
ruby on railsを独学で勉強したので、ある程度の動的言語の仕組みは理解しています。
今回はフロントにreact.jsを使ったので、djangoとあまり関係ないですが徹底的に解剖して理解します。以前rails×vueでSPA開発したので、モダンなjavascriptフレームワークはある程度理解しているつもりです。

本記事の目標

前回作成したDjango restframeworkのapiを利用してSPAを作成する。フロントはreactを使うが、トレンドのtypescript, react hooks, redux toolkitを理解して使用する。かつDjango APIはAWSを使ってデプロイ、フロントはFirebaseを使ってデプロイしてそれぞれを連携させる。

【使用技術】
■バックエンド
Django restframework, Anaconda-Navigater, PyCharm
■フロントエンド
React Hooks, Redux Tool kit, TypeScript
■インフラ
firebase, AWS(EC2,VPC,ACM,ROUTE53)



【参考】
本記事はudemyの下記講座で学んだ内容のアウトプットです。なのでコード等は講座の内容がベースとなっております。コードは一部省略して、特に自分が引っかかったところを抜粋しています。pythonの基礎文法はprogateとpyqで学びました。

  • 『最速で学ぶTypeScript』
  • 『[基礎編]React Hooks + Django REST Framework API でフルスタックWeb開発』
  • 『[Redux編] Redux Tool KitとReact HooksによるモダンReact フロントエンド開発』
  • 『[Instagramクローン編] React Hooks + Django Restframework』

【前回までの関連記事】
Railsしか触ったことないけどPython・Djangoに挑戦してみた 〜環境構築・hello world編〜
Railsしか触ったことないけどPython・Djangoに挑戦してみた 〜簡易SNSアプリ作成編〜
『Django API× React 』でSNSアプリ作成 ~Django APIを徹底解剖編~

またaxiosなどに関してはrails×vueの記事で書いてあるのでそちらも参照してください。
【その他参考記事】


目次

  1. SNSアプリのフロントプロジェクト構造
  2. Typescriptについて
  3. react hooksについて
  4. redux toolkitについて
  5. AWSとFirebaseでデプロイ

では早速いきましょう。


1. SNSアプリのフロントプロジェクト構造

一部省略しますが大事なところだけ抜粋します。

sns_app
    ├ api_sns(django側)
        └ manage.py
        └ api(アプリケーション)/
            └ views.py
            └ urls.py
            └ models.py
        └ api_sns(プロジェクト)/
            └ settings.py
    ├ react_sns(react側)
        └ public/
        └ src/
            └ app/
            └ features/
                └ auth/
                    └Auth.tsx
                    └authSlice.tsx
                    └Auth.module.css
                └ core/
                └ post/
                └ types.ts
            └ features/
            └ index.tsx/

基本的にはreact側はcreate-react-appをしたファイル構造、django側はdjango-admin startprojectをしたファイル構造になっています。今回はreact側のファイル構造を主に説明します。基本的にはsrc/features以下を使います。srcには機能ごと(auth,postなど)ごとにフォルダが存在し、その中にredux定義用のsliceファイル、表示のためのコンポーネントファイルとcssファイルがあります。これらをapp/フォルダのなかにある、sotre.tsで一つにしています。


2.Typescriptとは

■Typescriptについて

javascriptの代替言語です。javascriptと何が違うと、Typescriptは型を定義してプログラミングをします。型を定義できると何が良いかというと綺麗なコードを効率的に書けることです。例えばTypescriptでは関数の引数や返り値の型を指定し、それ以外の指定されていない型を排除使えなくします。開発が大規模になればなるほどコードがごっちゃになりますが、型を定義しておくことで可読性が高まるとのことです。またTypescriptはmicrosoftが開発言語なので,visual studio codeをエディタで使用しているならその補完機能の恩恵を授かることができます。
まだ個人開発程度しかやっていないので思いっきりTypescriptの恩恵を受けれてはいないですが、visual studio codeを使用して返り値や引数として渡される値を確認しながらコードを打つことができたのは良いと思いました。

■コード例

今回はreactでプロジェクトを作成していますが、Typescriptを使う際は生成時に下記のようにします。

ターミナル
npx create-react-app . --template redux-typescript

また関数宣言時にその都度、型宣言するのも間違いではないですが、別途型宣言用のファイルを作成するのが良いです。そうすることで型の管理が楽になりますし、必要なときはその型ファイルからimportするだけで使用可能です。今回はreactプロジェクトのなかのsrc/features以下にtypes.tsを作成しました。

types.ts
export interface File extends Blob {
  readonly lastModified: number;
  readonly name: string;
}

export interface PROPS_AUTHEN {
  email: string;
  password: string;
}

export interface PROPS_PROFILE {
  id: number;
  nickName: string;
  img: File | null;
}

export interface PROPS_NICKNAME {
  nickName: string;
}

.
.
/*一部省略*/

exportは出力、interfaceは型の定義をまとめるという意味です。そのあとのPROPS_AUTHENなどが型の名前です。また中身は、PROPS_AUTHENでいうとemailとpasswordがあり、それぞれの型を指定しています。またPROPS_PROFILEのimgのように「File | null」のように複数の型を指定することができます。

また使うときは下記のようにします。

auth/authSlice
.
.
.
import { PROPS_AUTHEN, PROPS_PROFILE, PROPS_NICKNAME } from "../types";

export const fetchAsyncLogin = createAsyncThunk(
  "auth/post",
  async (authen: PROPS_AUTHEN) => {
    const res = await axios.post(`${apiUrl}authen/jwt/create`, authen, {
      headers: {
        "Content-Type": "application/json",
      },
    });
    return res.data;
  }
);
.
.

async (authen: PROPS_AUTHEN)のところでさきほどtypes.tsで定義した型をimportしています。

■その他参考記事


3. react hooksについて

■react hooksの概要について

react hooksとは関数コンポーネントだけでデータの受け渡しを可能にしたAPIです。
reactには従来からclassコンポーネントと関数コンポーネントがあり、classコンポーネントはstateを保持することができますが記述が複雑になりがちというのが特徴で、関数コンポーネントはstateは保持できないものの記述は比較的簡単というのが特徴でした。つまりreact hooksとは関数コンポーネントの弱点を克服し、stateを保持できるようにしたものです。これによって簡単なものであればreduxを使わずに、またはreduxを最小限に抑えることを可能にし、より簡素に素早くアプリケーションの構築ができるようになります。

■useStateの使い方

今回のアプリケーションではuseStateとpropsくらいしか使っていないです。ただuseStateが一番大事かなと思っているのでそれを紹介します。その他のuseEffectやuseReducerは使い方だと意味だけ記述します。

EditProfile
import React, { useState } from "react";

 const [image, setImage] = useState<File | null>(null);

<input
  type="file"
  id="imageInput"
  hidden={true}
  onChange={(e) => setImage(e.target.files![0])} />

まずはuseStateをimportします。constの行でimageとsetImageを定義していますが、imageはstateの状態、setImageはstateの状態に変更を加えるアクションです。image,setImageをuseStateで定義しており、inputで画像が挿入されたときにuseStateの値が変動するというイメージです。useStateでは数値や文字列はもちろん、配列やオブジェクトを持つことも可能です。

■その他のHooksの概要

  • 『useEffect』:effectは副作用というニュアンスらしいです。つまりコンポーネントが呼ばれたとき、もしくは指定したアクションが起こったとき、特定に処理をするというものです。
  • 『useReducer』:redux、vueでいうとvuexと似た機能です。newState=reducer(currentState, action)、[state, dispatch] = useReducer(reducer, initialState)のように定義、usereducerの引数で特定のアクションとデフォルトの値を設定するという感じです。useStateと似ています。
  • 『useContext』:コンポーネントが複数層存在する際に、1つ1つ辿らなくてもデータの引き渡しができるようにするアクションです。
  • その他にもuseContext,useCallback,useMemoなどがあります。参考記事をみればだいたいわかると思います。

■その他参考記事


4. redux toolkitについて

■redux・redux toolkitについて

reduxはvueでいうvuexのことであり、つまり状態管理するためのものです。そしてredux toolkitはそのreduxを簡単に記述するためのツールです。2019年あたりに使われた技術なので、比較的新しいですが、最近良く使われているそうです。reduxの構造を理解すれば簡単ですが、vuexとか触ったことないと中々難解です。下記記事がすごいわかりやすかったので載せておきます。

Redux入門【ダイジェスト版】10分で理解するReduxの基礎

ざっくりとした流れは、viewからアクションを呼び出し→Reducerを呼び出す→stateの変更という流れです。reduxはsotreという大きなくくりがあり、各stateはslicerという単位で管理されています。今回はuser認証のreduxファイルをみていきます。

■コード例

authSlice.ts
// ①importの説明
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { RootState } from "../../app/store";
import axios from "axios";
import { PROPS_AUTHEN, PROPS_PROFILE, PROPS_NICKNAME } from "../types";

// ②urlの設定
const apiUrl = process.env.REACT_APP_DEV_API_URL;

// ③tokenの取得
export const fetchAsyncLogin = createAsyncThunk(
  "auth/post",
  async (authen: PROPS_AUTHEN) => {
    const res = await axios.post(`${apiUrl}authen/jwt/create`, authen, {
      headers: {
        "Content-Type": "application/json",
      },
    });
    return res.data;
  }
);

// ④ユーザー登録
export const fetchAsyncRegister = createAsyncThunk(
  "auth/register",
  async (auth: PROPS_AUTHEN) => {
    const res = await axios.post(`${apiUrl}api/register/`, auth, {
      headers: {
        "Content-Type": "application/json",
      },
    });
    return res.data;
  }
);

// ⑤プロフィールの作成
export const fetchAsyncCreateProf = createAsyncThunk(
  "profile/post",
  async (nickName: PROPS_NICKNAME) => {
    const res = await axios.post(`${apiUrl}api/profile/`, nickName, {
      headers: {
        "Content-Type": "application/json",
        Authorization: `JWT ${localStorage.localJWT}`,
      },
    });
    return res.data;
  }
);

// ⑥プロフィールの更新
export const fetchAsyncUpdateProf = createAsyncThunk(
  "profile/put",
  async (profile: PROPS_PROFILE) => {
    const uploadData = new FormData();
    uploadData.append("nickName", profile.nickName);
    profile.img && uploadData.append("img", profile.img, profile.img.name);
    const res = await axios.put(
      `${apiUrl}api/profile/${profile.id}/`,
      uploadData,
      {
        headers: {
          "Content-Type": "application/json",
          Authorization: `JWT ${localStorage.localJWT}`,
        },
      }
    );
    return res.data;
  }
);

// ⑦マイプロフィールの取得
export const fetchAsyncGetMyProf = createAsyncThunk("profile/get", async () => {
  const res = await axios.get(`${apiUrl}api/myprofile/`, {
    headers: {
      Authorization: `JWT ${localStorage.localJWT}`,
    },
  });
  return res.data[0];
});

// ⑧プロフィールの取得
export const fetchAsyncGetProfs = createAsyncThunk("profiles/get", async () => {
  const res = await axios.get(`${apiUrl}api/profile/`, {
    headers: {
      Authorization: `JWT ${localStorage.localJWT}`,
    },
  });
  return res.data;
});

// ⑨その他のアクションの設定
export const authSlice = createSlice({
  name: "auth",
  initialState: {
    openSignIn: true,
    openSignUp: false,
    openProfile: false,
    isLoadingAuth: false,
    myprofile: {
      id: 0,
      nickName: "",
      userProfile: 0,
      created_on: "",
      img: "",
    },
    profiles: [
      {
        id: 0,
        nickName: "",
        userProfile: 0,
        created_on: "",
        img: "",
      },
    ],
  },
  reducers: {
    fetchCredStart(state) {
      state.isLoadingAuth = true;
    },
    fetchCredEnd(state) {
      state.isLoadingAuth = false;
    },
    setOpenSignIn(state) {
      state.openSignIn = true;
    },
    resetOpenSignIn(state) {
      state.openSignIn = false;
    },
    setOpenSignUp(state) {
      state.openSignUp = true;
    },
    resetOpenSignUp(state) {
      state.openSignUp = false;
    },
    setOpenProfile(state) {
      state.openProfile = true;
    },
    resetOpenProfile(state) {
      state.openProfile = false;
    },
    editNickname(state, action) {
      state.myprofile.nickName = action.payload;
    },
  },
//⑩extrareducerの設定
  extraReducers: (builder) => {
    builder.addCase(fetchAsyncLogin.fulfilled, (state, action) => {
      localStorage.setItem("localJWT", action.payload.access);
    });
    builder.addCase(fetchAsyncCreateProf.fulfilled, (state, action) => {
      state.myprofile = action.payload;
    });
    builder.addCase(fetchAsyncGetMyProf.fulfilled, (state, action) => {
      state.myprofile = action.payload;
    });
    builder.addCase(fetchAsyncGetProfs.fulfilled, (state, action) => {
      state.profiles = action.payload;
    });
    builder.addCase(fetchAsyncUpdateProf.fulfilled, (state, action) => {
      state.myprofile = action.payload;
      state.profiles = state.profiles.map((prof) =>
        prof.id === action.payload.id ? action.payload : prof
      );
    });
  },
});

//⑪アクションの出力
export const {
  fetchCredStart,
  fetchCredEnd,
  setOpenSignIn,
  resetOpenSignIn,
  setOpenSignUp,
  resetOpenSignUp,
  setOpenProfile,
  resetOpenProfile,
  editNickname,
} = authSlice.actions;

//⑫userSelectorの定義
export const selectIsLoadingAuth = (state: RootState) =>
  state.auth.isLoadingAuth;
export const selectOpenSignIn = (state: RootState) => state.auth.openSignIn;
export const selectOpenSignUp = (state: RootState) => state.auth.openSignUp;
export const selectOpenProfile = (state: RootState) => state.auth.openProfile;
export const selectProfile = (state: RootState) => state.auth.myprofile;
export const selectProfiles = (state: RootState) => state.auth.profiles;

export default authSlice.reducer;
  • ①「createslice」とはstate,action,reducerをまとめて作成できるものです。今回は⑨のところででてきます。「createAsyncThunk」も同じですが、こちらは非同期のアクションの生成になります。その次の「Rootstate」とはreduxを管理している大元のことです。app/storeで管理していて、各slicerファイルはapp/storeで読み込むことで使えるようになります。あとはAPI通信用のaxios,typescriptの型を定義したファイルをimportしています。
  • ②はAPI通信用のURLの設定です。「.env」ファイルに環境変数REACT_APP_DEV_API_URLを登録します。REACT_APP_DEV_API_URLの変数の中身は、DjangoのURLです。「.env」の環境変数を呼び出すときはprocess.env.〜で呼び出します。
  • ③〜⑧は非同期通信のアクションを定義しています。呼び出すデータは違えど構造はほとんど同じです。「axios.{post or get}(パス名)」でAPIにアクセスします。第2引数にparamsの中身、第3引数にheader情報を指定しています。ログインしてないとできないアクションは第3引数でjwtの情報が必要です(今回はローカルストレージに保存しています。ログインでJWT取得時にローカルストレージにJWTが保存されます)。またasyncのあとに型をしています。⑥だけは画像の送信のためformDataを使っています。formDataはjavascriptの内容なので説明は割愛します。
  • ⑨は「createslice」でアクションを定義しています。ここは大きくinitial state, reducer, extrareducerに分かれています。initial stateはその名の通り初期値です。今回はmodalを使っているので、そのmodalを開くか開かないかをtrue,falseで決めています。reducerはそのstateの値を動かすアクションです。
  • ⑩は先程③~⑧で設定した非同期関数に追加して、行う処理を記載しています。ちなみにbuilderはjavascriptのデザインパターンです。最初の1つは「fetchAsyncLogin.fulfilled」でfetchAsyncLoginの処理が終わったあとに実行するという意味です。それ以下の「localStorage.setItem("localJWT", action.payload.access);」では取得したJWTをlocalに保存しています。「action.payload.access」は「return res.data;」の中身を取得しています。2つ目以降は取得したデータをstateに保存しています。
  • ⑪はアクションを他のコンポーネントで使えるようにexportしています。
  • ⑫はstateのデータ・値をコンポーネントから取得できるようにするための設定です。


Auth.tsx
 //①importの説明
import React from "react";
import { AppDispatch } from "../../app/store";
import { useSelector, useDispatch } from "react-redux";
import styles from "./Auth.module.css";
import Modal from "react-modal";
import { Formik } from "formik";
import * as Yup from "yup";
import { TextField, Button, CircularProgress } from "@material-ui/core";
import { fetchAsyncGetPosts, fetchAsyncGetComments } from "../post/postSlice";

//②アクションのimport
import {
  selectIsLoadingAuth,
  selectOpenSignIn,
  selectOpenSignUp,
  setOpenSignIn,
  resetOpenSignIn,
  setOpenSignUp,
  resetOpenSignUp,
  fetchCredStart,
  fetchCredEnd,
  fetchAsyncLogin,
  fetchAsyncRegister,
  fetchAsyncGetMyProf,
  fetchAsyncGetProfs,
  fetchAsyncCreateProf,
} from "./authSlice";

const customStyles = {
  overlay: {
    backgroundColor: "#777777",
  },
  content: {
    top: "55%",
    left: "50%",

    width: 280,
    height: 350,
    padding: "50px",

    transform: "translate(-50%, -50%)",
  },
};

//③stateの定義
const Auth: React.FC = () => {
  Modal.setAppElement("#root");
  const openSignIn = useSelector(selectOpenSignIn);
  const openSignUp = useSelector(selectOpenSignUp);
  const isLoadingAuth = useSelector(selectIsLoadingAuth);
  const dispatch: AppDispatch = useDispatch();

//④サインアップ用のモーダル
  return (
    <>
      <Modal
        isOpen={openSignUp}
        onRequestClose={async () => {
          await dispatch(resetOpenSignUp());
        }}
        style={customStyles}
      >
        <Formik
          initialErrors={{ email: "required" }}
          initialValues={{ email: "", password: "" }}
          onSubmit={async (values) => {
            await dispatch(fetchCredStart());
            const resultReg = await dispatch(fetchAsyncRegister(values));

            if (fetchAsyncRegister.fulfilled.match(resultReg)) {
              await dispatch(fetchAsyncLogin(values));
              await dispatch(fetchAsyncCreateProf({ nickName: "anonymous" }));

              await dispatch(fetchAsyncGetProfs());
              await dispatch(fetchAsyncGetPosts());
              await dispatch(fetchAsyncGetComments());
              await dispatch(fetchAsyncGetMyProf());
            }
            await dispatch(fetchCredEnd());
            await dispatch(resetOpenSignUp());
          }}
          validationSchema={Yup.object().shape({
            email: Yup.string()
              .email("email format is wrong")
              .required("email is must"),
            password: Yup.string().required("password is must").min(4),
          })}
        >
          {({
            handleSubmit,
            handleChange,
            handleBlur,
            values,
            errors,
            touched,
            isValid,
          }) => (
            <div>
              <form onSubmit={handleSubmit}>
                <div className={styles.auth_signUp}>
                  <h1 className={styles.auth_title}>SNS clone</h1>
                  <br />
                  <div className={styles.auth_progress}>
                    {isLoadingAuth && <CircularProgress />}
                  </div>
                  <br />

                  <TextField
                    placeholder="email"
                    type="input"
                    name="email"
                    onChange={handleChange}
                    onBlur={handleBlur}
                    value={values.email}
                  />
                  <br />
                  {touched.email && errors.email ? (
                    <div className={styles.auth_error}>{errors.email}</div>
                  ) : null}

                  <TextField
                    placeholder="password"
                    type="password"
                    name="password"
                    onChange={handleChange}
                    onBlur={handleBlur}
                    value={values.password}
                  />
                  {touched.password && errors.password ? (
                    <div className={styles.auth_error}>{errors.password}</div>
                  ) : null}
                  <br />
                  <br />

                  <Button
                    variant="contained"
                    color="primary"
                    disabled={!isValid}
                    type="submit"
                  >
                    Register
                  </Button>
                  <br />
                  <br />
                  <span
                    className={styles.auth_text}
                    onClick={async () => {
                      await dispatch(setOpenSignIn());
                      await dispatch(resetOpenSignUp());
                    }}
                  >
                    You already have a account ?
                  </span>
                </div>
              </form>
            </div>
          )}
        </Formik>
      </Modal>
      .
      .
      .
      //一部省略
    </>
  );
};

export default Auth;
  • ①AppDispatchはapp/storeの内容をimportし、authslicerなどの内容を使えるようにしています。useSelectorはreduxからstateを参照するもの、useDispatchはreduxからdispatchを使うために必要です。Modalはログインやサインインなどに使うフォームみたいなものです。Formikはフォームの管理に使えるライブラリで、Yupがバリデーションに使えるライブラリです。@material-ui/coreでマテリアルUIをimportすると簡単におしゃれなコンポーネントが使えるようになります。
  • ②authSliceでexportしたアクションをimportして使えるようにしています。
  • ③authSliceで定義しているstateの状態を定義・管理しています。
  • ④がサインアップ用のモーダルの記述です。基本的にはdispatchと呼ばれている箇所は、authSliceで定義したアクションを呼び出しています。dispatchで呼び出されたreducerをauthSliceで追い、どのstateが動かされているかを確認するという流れでコードが理解できるようになります。

以上で終わりです。ちなみにchromeの拡張機能を使えば、ブラウザでreduxのstateなどが確認できるので非常に便利です。


5. AWSとFirebaseでデプロイ

フロントのreactプロジェクトはfirebase hostingでデプロイし、バックエンドのDjangoAPIはAWSのEC2でデプロイして、それを結合します。

■firebase hostingでのデプロイ

10分で終わります。本記事では省略しますが、下記記事で簡単にデプロイできるかと思います。
(初心者向け)Firebase HostingへReactプロジェクトを公開する手順
デプロイで来たら一旦完了です。

■AWSでのデプロイ

こちらも本記事で省略します。以前railsでのAWSに関する記事を書いたのでそちらを参照ください。ただ1点注意があります。それはfirebaseとAPI通信するためにはSSL証明を取って、HTTPS化する必要があります。そのためROUTE53,ACMの設定が必要です。
【前書いた記事】
【0からAWSに挑戦】EC2とVPCを使ってRailsアプリをAWSにデプロイする part1
【0からAWSに挑戦】ROUTE53を使ってドメイン設定 && ACMによるSSL化

EC2内でDjangoに必要なパッケージ等のインストールが必要ですが、下記記事が役に立ちました。djangoではwebサーバーにnginx,アプリケーション・サーバーにgunicornを使うのがスタンダードらしいです。
【20分でデプロイ】AWS EC2にDjango+PostgreSQL+Nginx環境を構築してササッと公開

■APIとフロントを連携させる

あとはそれぞれのファイルのURL設定をデプロイしたURLに変えるだけです。

(EC2上)setting.py
.
.
CORS_ORIGIN_WHITELIST = [
    "firebase側のURLを設定する"
]
.
.
(firebase側).env
REACT_APP_DEV_API_URL="APIのURL"

ここでAWSのEC2をHTTPSにしていないとエラーが起こります。firebase側はもう一度deployコマンドを打つと反映されます。

以上で完成!!! firebase側のurlにアクセスして完了!!!
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
スクリーンショット 2021-03-03 12.54.06.png


まとめ

vue.jsを学習済みだったのでreact.jsを比較的すんなり理解できました。一応これでもバックエンド・インフラ系の志望なので、APIでどう連携して、どういう仕組で動いているかは意識していたいです。おそらく今後の開発に役立つと信じて、、、
あとはrailsでも思いましががjwtあたりの認証がすごい大事だなと感じたので、改めて別途記事を書きたいなと思います。

参考

下記udemy講座を参考にしています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Cognitoを使ってAPI Gatewayのアクセス認証をしてみた

はじめに

これまでS3のバケットポリシーとAPI Gatewayのリソースポリシーでアクセス元のIP制限をかけていたため、許可されているIPアドレスからでないと、アクセスし、APIをたたけないようになっていました。しかし、今回訳あってIP制限を外すことになってしまい、APIを誰でも叩き放題になってしまったので、解決策を考えました。

方法検討

API Gatewayで使えるアクセスを認証

APIのアクセスを認証する方法を調べたところ以下の3つが出てきました。

  • Cognito
  • Lambdaオーソライザー
  • IAM認証

Cognito

Cognitoユーザープールで認証時にユーザープールトークンが発行され、そのトークンを使用して認証する方法です。

わたしが思うユースケース
  • ユーザー認証にCognitoを使用しているとき

Lambdaオーソライザー

API Gatewayを叩いた時に、認証用のLambda関数を呼び、認証が通れば、実行したいAPI(今回だとLambda関数)が実行されるようになるという方法です。

わたしが思うユースケース
  • Auth0などのCognito外の認証プラットフォームを使っているとき

参考

IAM認証

APIの実行権限を付与したIAMユーザーを作成し、IAMユーザーのアクセスキー、シークレットキーを使ってAWS Signature V4 署名を作成し認証する方法です。
ユーザーにIAMロールが付与されていれば、それも使用することができます。

わたしが思うユースケース
  • サーバーからAPIをたたくとき(EC2などIAMロールが使えるときはIAMロールを利用)
  • CognitoのグループでIAMロールを付与しているとき

参考

現在の構成図

アプリケーション部分の構成図は下記の通りです。

Cognito.png

ユーザー認証部分はCognito + Amplifyフレームワークで構築しています。構築の基本部分については「【React】ユーザー認証をCognito + Amplifyで構築してみた」の構築準備編構築完成編をご覧ください。
そして、アプリケーション部分はLambda + RDS Proxy + RDSで実装しています。この構築方法については「祝GA‼︎【Go】Lambda + RDS 接続にRDS Proxyを使ってみた」をご覧ください。

結論

現状、Cognitoユーザープールを使ってユーザー認証をしているので、API Gatewayのアクセス認証にもCognitoを使うことにしました!

手順

既存の構成にAPIのアクセス認証をつけていくので、Cognitoユーザープールを使ってのユーザー認証、API Gatewayを使ってLambdaを実行する部分については既に構築できていることを前提として、下記の流れで進めていきます。ただ、今回はDB操作は行わず、メッセージを送り、メッセージをそのまま返すLambda関数を実行するようにしています。

  1. API Gatewayの設定
  2. フロントの実装

やってみる

1. API Gatewayの設定

まず、API Gatewayのオーソライザーを作成していきます。
API Gatewayのコンソールから、[オーソライザー]を開きます。

スクリーンショット 2021-02-03 21.05.58.png

新規でオーソライザーを作成します。
名前、タイプ、Cognitoユーザープール、トークンのソースを入力し、作成ボタンをクリックします。トークンのソースAuthorizationはリクエストのヘッダーとしてトークンを送るときに使います。

スクリーンショット 2021-02-09 23.10.34.png

次に、作成したオーソライザーはメソッド単位で設定していきます。つまり、複数メソッドがある場合はそれぞれに設定しないとトークンなしでAPI Gatewayを叩けてしまうので注意です。

次のように、[リソース]→[オーソライザーを設定したいメソッド]→[メソッドリクエスト]を開きます。

スクリーンショット 2021-02-13 14.59.33.png

許可の部分に先ほど作ったcognito-authorizerを設定します。選択肢に出てこない場合はリロードなどすると選択肢に出てきます!

スクリーンショット 2021-02-09 23.26.40.png

そして最後にデプロイします!
これでオーソライザーの設定は完了です。

2. フロントの実装

取得したユーザープールトークンをヘッダーにつけてAPI Gatewayをたたく処理を実装します。

axiosのインストール

API Gatewayを叩くのにaxiosを使うために、プロジェクトにaxiosを追加します。

$ yarn add axios

ソースコード

認証時に必要なトークンは下記の方法で取得可能です。

const user = Auth.currentAuthenticatedUser()
const idToken = user.signInUserSession.idToken.jwtToken

このidTokenAuthorizationキーのバリューとしてヘッダーに持たせることで、リクエストが可能になります。

App.js
import React from "react";
import Amplify, {Auth} from 'aws-amplify';
import awsconfig from './aws-exports';
import {withAuthenticator} from "@aws-amplify/ui-react";
import axios from "axios";
import "./App.css"

Amplify.configure(awsconfig);

function App() {
    const API_URL = "<API Gatewayで取得したURL>"
    const [message, setMessage] = React.useState("");
    const [response, setResponse] = React.useState("");

    const handleChange = event => {
        setMessage(event.target.value);
    };

    const handleSubmit = async(event) => {
        const user = await Auth.currentAuthenticatedUser()
        const idToken = user.signInUserSession.idToken.jwtToken
        const headers = {headers: {"Authorization": idToken}};
        axios.post(API_URL, {message: message}, headers)
            .then((response) => {
                if(response.data.message === message){
                    setResponse(response.data.message);
                } else {
                    throw Error(response.data.errorMessage)
                }
            }).catch((response) => {
                alert("登録に失敗しました。もう一度送信してください。");
                console.log(response);
        });
        event.preventDefault();
    }

    return (
        <fieldset>
            <form onSubmit={handleSubmit}>
                <label >
                    <input type="text" value={message} onChange={handleChange} />
                </label>
                <input type="submit" value="送信" />
            </form>
            <div>{response}</div>
        </fieldset>
    );
}

export default withAuthenticator(App);

実行結果

ヘッダーあり

入力欄の下に、Lambdaから返ってきたメッセージが表示されるようになっています。入力した値がLambdaを介して返ってきています!

画面収録-2021-02-09-23.37.09.gif

ヘッダーなし

ちなみに、ヘッダーにidトークンを付けずに実行してみました。

※ API Gatewayをたたくところのみ抜粋

App.js
        axios.post(API_ADD_URL, {message: message})
            .then((response) => {
                if(response.data.message === message){
                    setResponse(response.data.message);
                } else {
                    throw Error(response.data.errorMessage)
                }
            }).catch((response) => {
                alert("送信に失敗しました。もう一度送信してください。");
                console.log(response);
        });

ソースを上記のように変更し、実行すると・・

スクリーンショット 2021-02-03 20.14.36.png

エラーが出て、Lambdaが実行できないことがわかりました!

おわりに

無事に、API Gatewayにアクセス認証をつけることができました!今回はもともとCognitoユーザープールを使ってユーザー認証をやっていたので、Cognitoのオーソライザーを使って簡単に設定することができました。既存のシステムの構成によってこれでIP制限を外しても、セキュリティを担保することができたのではないかと思います!めでたし!

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【TypeScript】React.VFCとは何ぞや

はじめに

ReactにHooksが導入され、最近ではFunction Componentを採用することがほとんどになってきたように思います。

Function Componentを定義するための型としてReact.FCが提供されているのですが、いくらかのデメリット(というより使う必要のなさ)があり、敢えて使っていないプロジェクトも数多く存在しています。
そんな中、デメリットを少し解消したReact.VFCが生み出されていたため共有します。

React.FCとは

先述したとおりFunction Componentを定義するための型で、以下のように使用します。

interface Props {
  hoge: string;
}

const Component: React.FC<Props> = (props) => {
  return (
    <div>
      <p>{props.hoge}</p>
    </div>
  );
};

const Parent: React.FC = () => {
  return (
    <div>
      <Component hoge="piyo" />
    </div>
  );
};

デメリットの一つとして、「children(タグの間の要素)の有無がわからない」というものがあります。
React.FCではchildrenが最初から暗黙的に定義されてしまっているため、必要ないときに渡してしまってもエラーにならない仕様になっています。

interface Props {
  hoge: string;
  // childrenを定義していない
}

const Component: React.FC<Props> = (props) => {
  // childrenは必要ない
  return (
    <div>
      <p>{props.hoge}</p>
    </div>
  );
};

const Parent: React.FC = () => {
  return (
    <div>
      {/* childrenがあるのにエラーが出ない */}
      <Component hoge="piyo" >
        children
      </Component>
    </div>
  );
};

これではTypeScriptの良さが半減してしまいます。

React.VFCの登場

React.VFCではchildrenが含まれておらず、先程のように渡そうとするとエラーになります。
Type '{ children: string; hoge: string; }' is not assignable to type 'IntrinsicAttributes & Props'.
Property 'children' does not exist on type 'IntrinsicAttributes & Props'.ts(2322)

interface Props {
  hoge: string;
  // childrenを定義していない
}
// React.VFCに変更
const Component: React.VFC<Props> = (props) => {
  // childrenは必要ない
  return (
    <div>
      <p>{props.hoge}</p>
    </div>
  );
};

const Parent: React.VFC = () => {
  return (
    <div>
      <Component hoge="piyo">
        children
      </Component>
      {/* ERROR: Type '{ children: string; hoge: string; }' is not assignable to type 'IntrinsicAttributes & Props'.
          Property 'children' does not exist on type 'IntrinsicAttributes & Props'.ts(2322) */}
    </div>
  );
};

childrenが必要な場合は、以下のようにちゃんと明記してあげればOKです。

interface Props {
  hoge: string;
  children: React.ReactNode;  // childrenを定義
}

const Component: React.VFC<Props> = (props) => {
  return (
    <div>
      <p>{props.hoge}</p>
      <p>{props.children}</p>  {/* "children"と表示される */}
    </div>
  );
};

const Parent: React.VFC = () => {
  return (
    <div>
      <Component hoge="piyo">
        children
      </Component>
    </div>
  );
};

React.VFCを用いることにより、childrenの有無がひと目で分かるようになりました!

React.FCの今後

React.VFCの登場によってReact.FCを使う理由がなくなってしまったのですが、このままReact.FCをなくしていくわけではないようです。
むしろReactとしては、今後もReact.FCを主流として扱っていくようです。

というのも、React.FCchildrenが含まれないべきだという考え方はもとから存在しており、@types/react 18からは含まれなくなる予定です。
これは破壊的な変更であることから、移行措置として導入されたものが今回のReact.VFCであるようです。

これからの開発では、一旦すべてReact.VFCで定義し、必要に応じてchildrenを定義するようにしましょう。
@types/react 18がリリースされればどちらも同じものになる(はず)なので、その際にReact.VFCReact.FCに置換すると良いでしょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScriptをなんとなく理解するための記事④ ~構造型~

この記事は何?

TypeScriptとは何か、何がいいのかを伝えようと頑張った記事の第4弾です!

1~3弾は、TypeScriptの書き方にフォーカスしましたが、
この記事では、TypeScriptの特徴であるStructural Type System(構造型システム)について説明していきます。

@ 過去の記事?も合わせて御覧ください!
 1. JavaScriptを知っている方がTypeScriptをなんとなく理解するための記事① ~はじめに~
 2. TypeScriptをなんとなく理解するための記事② ~型を組み合わせる: Union~
 3. TypeScriptをなんとなく理解するための記事③ ~汎用的な型を作成する: Generics~
 4. TypeScriptをなんとなく理解するための記事④ ~構造型~ ← この記事
@ TypeScriptがなんとなく理解できたら、是非インストールして、触ってみてください!
 ? 【画像で説明】シンプルにTypeScriptを導入して使う方法

構造型って何だ?

よく比較されるものとして、公称型と構造型があります。
公称型に関しては、JavaやPHPの型システムに利用されており、クラス名の一致で型の互換性(同じような型かどうか)を識別します。

一方で、TypeScriptの構造型は何なのでしょうか?
構造型(Structural Type)は、型の構造さえ同じであれば互換性があると判断する仕組みのことです。

この記事では、TypeScriptの大きな特徴である、構造型について理解していきます。

コードから構造型の理解を深める

では、実際にコードを見ながら構造型について理解していきましょう。
以下に、型の構造が同じコードを書いてみました。

// { name: string, cry: string }の構造を持つAnimal型を作成
interface Animal {
  name: string;
  cry: string;
}

const animalCryInfo = (animal: Animal) => {
  console.log(`${animal.name}の鳴き声は、${animal.cry}`);
}

// vividMorayは{ name: string, cry: string }と同じ構造 → Animal型と同じ
const vividMoray = { name: 'ハナヒゲウツボ', cry: 'ありません' };
// vividMorayは、引数がAnimal型と構造が一致するので、動作する!
animalCryInfo(vividMoray); // ハナヒゲウツボの鳴き声は、ありません

最初にinterfaceを利用して、{ name: string, cry: string }の構造を持つAnimal型を作成しています。

interface Animal {
  name: string;
  cry: string;
}

次に、Animal型を引数に持つ関数animalCryInfoを定義しています。

const animalCryInfo = (animal: Animal) => {
  console.log(`${animal.name}の鳴き声は、${animal.cry}`);
}

次に、オブジェクトvividMorayを定義します。このオブジェクトの型は推論により{ name: string, cry: string }と解釈されます。

const vividMoray = { name: 'ハナヒゲウツボ', cry: 'ありません' };

最後にanimalCryInfo関数の引数に、オブジェクトvividMorayを入れてみます...。
もし、Animal型とオブジェクトvividMorayが同じ型(構造)であれば、問題なく実行されますが。。。

animalCryInfo(vividMoray); // ハナヒゲウツボの鳴き声は、ありません

問題なくハナヒゲウツボの鳴き声は、ありませんと出力されます!

このように、型の構造に焦点を当てて、型チェックする仕組みが構造型システム(Structural Type System)です。

TypeScriptを記述するときは、この構造型を意識して書いたほうが良さそうです!
さもなくば以下のようなエラーが出てしまいます。?

interface Animal {
  name: string;
  cry: string;
}

const animalCryInfo = (animal: Animal) => {
  console.log(`${animal.name}の鳴き声は、${animal.cry}`);
}


const vividMoray = { name: 'ハナヒゲウツボ'};
animalCryInfo(vividMoray);
// タイプの引数 '{ name: string; } 'はタイプ'Animal'のパラメータに割り当てることはできません。
// プロパティ'cry'がタイプ '{ name: string;} 'にありませんが、タイプ' Animal'.ts(2345)では必須です

終わりに

今回は第1~3回と異なったTypeScriptの特徴を紹介しました。
この記事を見て、TypeScriptを触ってみよう!という人が増えていただければ幸いです?

わかりやすかったら、是非LGTM✨お願いします!(励みになります!)

@ ハンズオンも書いたので、さっそく使ってみたい人は是非!
 ?【TypeScriptハンズオン①】男もすなるTypeScriptといふものを、女もしてみむとてするなり

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む