かべブログ

その辺の大学生のブログです、フロントエンドやらの話をしたい

Recoil 素振り

はじめに

こんにちは、かべです。今日は話題の(?)状態管理ライブラリ Recoil を使って遊んでみようと思います。たぶん流行りには乗り遅れてますが、その辺は気にしない。

公式の todo アプリを作るチュートリアルがあるのですが、今回はもう少し簡単にさくっとアプリを作っていこうと思います。使う機能はほとんど変わりません。実装したい機能としては、

  • 好きな数字を選んでリストに追加し、表示する
  • 表示する数字を偶数か奇数かでフィルタリング出来る

完成イメージはこのようになります。

f:id:okb_okb:20210409172231p:plain
数字を追加できる

f:id:okb_okb:20210409172257p:plain
数字のフィルタリングができる

では、早速作って行きましょう。コードはこちらに置いてあります。

環境

今回の開発は以下の環境で行いました。

  • Recoil 0.2.0
  • Ubuntu 20.04.1
  • yarn 1.22.5
  • React 17.0.2
  • TypeScript 4.2.4

React アプリの作成、Recoil の導入

まずは npx create-react-app [好きな名前] --template typescript を叩いて React アプリを作成します。完成したら、yarn add recoil で Recoil を入れたり、使わない /public の画像等整理してあげたりしましょう。

まずは Recoil で状態管理が出来るように設定を行います。状態管理をしたい部分だけ<RecoilRoot></RecoilRoot> で囲めばよいのですが、今回は規模が小さいので雑に src/index.tsx に書きます。

src/index.tsx

import React from "react";
import ReactDOM from "react-dom";
import { RecoilRoot } from "recoil";
import App from "./App";

ReactDOM.render(
  <React.StrictMode>
    <RecoilRoot>
      <App />
    </RecoilRoot>
  </React.StrictMode>,
  document.getElementById("root")
);

atom, selector の作成

Recoil では atom と呼ばれるもので状態を持ち、selector と呼ばれるものでそれを分類したり変形した値を返したりすることが出来ます。

今回必要になる atom や selector の内容としては、次のようになります。

atom

  • 追加された数字を持つ配列
  • どのような数字を表示するか(偶数、奇数、全て)

selector

  • 数字の配列から指定された条件(偶数、奇数、全て)を満たす数字のみからなる配列

これらを src/recoil/ というディレクトリの中に作っていきます。atom はアプリ全体で一意の key とデフォルトの値を持ち、次のように書くことが出来ます。

src/recoil/atoms.ts

import { atom } from "recoil";

export const numbersState = atom({
  key: "numbersState",
  default: [] as number[],
});

export const numbersFilterState = atom({
  key: "numbersFilterState",
  // "all", "odd", "even" が入る
  default: "all",
});

selector はアプリ全体で一意の key と、他の atom や selector の値から求められる値を返す get などからなります。他にも set などのオプションもありますが、今回は使いません。今回使用する selector は次のように書くことが出来ます。

src/recoil/selectors.ts

import { selector } from "recoil";
import { numbersState, numbersFilterState } from "./atoms";

export const filteredNumbersState = selector({
  key: "filteredNumbersState",
  get: ({ get }) => {
    const filter = get(numbersFilterState);
    const numbers = get(numbersState);

    switch (filter) {
      case "odd":
        return numbers.filter((number) => number % 2 !== 0);
      case "even":
        return numbers.filter((number) => number % 2 === 0);
      default:
        return numbers;
    }
  },
});

atoms から取ってきた値を filter と numbers に入れ、switch 文で返り値を分岐させています。

コンポーネント内での使用

atom と selector が整ったので、実際にアプリ内で使用していきます。まずは新しい数字を追加するためのコンポーネントです。

src/components/AddNumbers.tsx

import { FC, useState } from "react";
import { useSetRecoilState } from "recoil";
import { numbersState } from "../recoil/atoms";

const AddNumbers: FC = () => {
  const [inputNumber, setInputNumber] = useState(0);
  const setNumbers = useSetRecoilState(numbersState);

  const increment = () => {
    setInputNumber((num) => num + 1);
  };
  const decrement = () => {
    setInputNumber((num) => num - 1);
  };

  // atom 内の value を変更
  const addNumber = () => {
    setNumbers((oldNumbers) => {
      return [...oldNumbers, inputNumber];
    });
    setInputNumber(0);
  };

  return (
    <>
      <button onClick={decrement}>-1</button> {inputNumber}{" "}
      <button onClick={increment}>+1</button>{" "}
      <button onClick={addNumber}>追加</button>
    </>
  );
};

export default AddNumbers;

useSetRecoilState を使うと atom などの値を書き換えることが出来ます。書き換えたい atom と前の値を受け取って新しい値を返す関数を与えると、書き換えが行われます。

次にどのような数字を表示するかユーザーが決められるセレクトボックスを作ります。

src/components/FilterNumbers.tsx

import { FC } from "react";
import { useRecoilState } from "recoil";

import { numbersFilterState } from "../recoil/atoms";

const FilterNumbers: FC = () => {
  const [filter, setFilter] = useRecoilState(numbersFilterState);

  // atom 内の value を変更
  const updateFilter = (event: React.ChangeEvent<{ value: unknown }>) => {
    setFilter(event.target.value as string);
  };

  return (
    <>
      <select value={filter} onChange={updateFilter}>
        <option value="all">全て</option>
        <option value="odd">奇数</option>
        <option value="even">偶数</option>
      </select>
    </>
  );
};

export default FilterNumbers;

useRecoilState を使うと atom の読み書きが出来ます。

最後に数字を表示するコンポーネントを作ります。

src/components/ShowNumbers.tsx

import { FC } from "react";
import { useRecoilState } from "recoil";

import { numbersFilterState } from "../recoil/atoms";

const FilterNumbers: FC = () => {
  const [filter, setFilter] = useRecoilState(numbersFilterState);

  // atom 内の value を変更
  const updateFilter = (event: React.ChangeEvent<{ value: unknown }>) => {
    setFilter(event.target.value as string);
  };

  return (
    <>
      <select value={filter} onChange={updateFilter}>
        <option value="all">全て</option>
        <option value="odd">奇数</option>
        <option value="even">偶数</option>
      </select>
    </>
  );
};

export default FilterNumbers;

値だけ欲しい時は useRecoilValue を使います。あとはコンポーネントを並べれば完成です!

src/App.tsx

import { FC } from "react";
import AddNumbers from "./components/AddNumbers";
import FilterNumbers from "./components/FilterNumbers";
import ShowNumbers from "./components/ShowNumbers";

const App: FC = () => {
  return (
    <>
      <AddNumbers /> <FilterNumbers />
      <ShowNumbers />
    </>
  );
};

export default App;

yarn start で実行し、localhost:3000 を確認してみましょう。

終わりに

体感 Redux よりかなり楽に状態管理が出来た気がします。今(2021/4/9) 0.2.0 なので、これから機能の拡充等楽しみです。

参考

Recoil

Firebase Authentication(+ Next.js + TypeScript) 素振り

はじめに

こんばんは、かべです。

ちゃんとした Web アプリを作るにはやっぱりユーザー認証機能が欲しい!ということで新規ユーザー作成、ログイン機能を実装する練習をしました。先日 Firebase の Realtime Database を使うことがあったので、流れで FIrebase Authentication を使うことにしました。

いろいろやり方を探していると良さげな Zennの記事 を見つけたため、これをなぞりながら自分でも少し機能を足していこうと思います。今回の目標は次の通りです。

  • 新規ユーザー登録がメールアドレスとパスワードで出来る
  • 登録したユーザーのメールアドレスとパスワードでログインできる
  • ログインするとログイン後のページに飛び、「ようこそ、〇〇さん!」のように表示される
  • ログインしていない時にログイン後のページを見ようとしても戻される

構成としては、Next.js + TypeScript で行います。

環境

今回の開発は以下の環境で行いました。

  • Ubuntu 20.04.1
  • yarn 1.22.5
  • firebase 8.3.2
  • Next 10.1.3
  • React 17.0.2
  • TypeScript 4.0

Firebase の設定

  1. Firebase のコンソールから「プロジェクトを追加」
  2. 適当にプロジェクト名等入力
  3. 左サイドバーの「Authentication」をクリック
  4. Sign-in method」から「メール/パスワード」を有効にする
  5. 左サイドバーの「プロジェクトを設定」から Web アプリを追加し、Firebase SDK を追加する

という手順で簡単に設定できます。最後の手順で出てくる var firebaseConfig は保存しておいてください。接続の際に必要です。

Next.js の初期設定と Firebase への接続

いつもの create-next-app で作ります。今回は TypeScript を使いたいので npx create-next-app --example with-typescript [好きなプロジェクト名] で行きます。完成したらいらないファイルを消して、そこにいろいろ書いていきましょう。

まず、Firebase SDK にある Firebase への接続キーを Next.js 上で読めるようにします。Next.js では .env ファイルに書くだけでは環境変数をよしなに読み取ってくれないようで、next.config.js ファイルにも書く必要があります。生成されたコードのルートディレクトリに次のコードを追加しましょう。

.env.local

FIREBASE_API_KEY='hoge'
FIREBASE_AUTH_DOMAIN='hoge'
FIREBASE_PROJECT_ID='hoge'
FIREBASE_STORAGE_BUCKET='hoge'
FIREBASE_MESSAGING_SENDER_ID='hoge'
FIREBASE_APP_ID='hoge'

hoge には適宜得られた値を代入します。

next.config.js

module.exports = {
  env: {
    FIREBASE_API_KEY: process.env.FIREBASE_API_KEY,
    FIREBASE_AUTH_DOMAIN: process.env.FIREBASE_AUTH_DOMAIN,
    FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID,
    FIREBASE_STORAGE_BUCKET: process.env.FIREBASE_STORAGE_BUCKET,
    FIREBASE_MESSAGING_SENDER_ID: process.env.FIREBASE_MESSAGING_SENDER_ID,
    FIREBASE_APP_ID: process.env.FIREBASE_APP_ID,
  },
};

環境変数が設定出来たら、次は Next.js 内で firebase のあれこれを使えるように firebase 利用の大元となるファイルを作ります。

/utils/firebase.ts

import "firebase/auth";
import "firebase/firestore";

import firebase from "firebase/app";

const firebaseConfig = {
  apiKey: process.env.FIREBASE_API_KEY,
  authDomain: process.env.FIREBASE_AUTH_DOMAIN,
  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,
};

// 複数回呼ばれるとエラー
if (!firebase.apps.length) {
  firebase.initializeApp(firebaseConfig);
}

export const auth = firebase.auth();

ユーザー登録

今回は localhost:3000/ をログイン用のページ、/signup をユーザー登録用のページ、/contents をログイン成功後のページとして作成していきます。

まずはユーザー情報を Context として持つための Provider などを /context/Auth.tsx に作ります。

/context/Auth.tsx

import firebase from "firebase/app";
import { FC, createContext, useEffect, useState } from "react";
import { auth } from "../utils/firebase";

// 現在のユーザーを持つ Context の型
type AuthContextProps = {
  currentUser: firebase.User | null | undefined;
};

const AuthContext = createContext<AuthContextProps>({ currentUser: undefined });

// Context を全体で使えるようにする Provider
const AuthProvider: FC = ({ children }) => {
  const [currentUser, setCurrentUser] = useState<
    firebase.User | null | undefined
  >(undefined);

  // ログイン状態が変わった時に発火
  useEffect(() => {
    auth.onAuthStateChanged((user) => {
      setCurrentUser(user);
    });
  }, []);

  return (
    <AuthContext.Provider value={{ currentUser }}>
      {children}
    </AuthContext.Provider>
  );
};

export { AuthContext, AuthProvider };

/pages/_app.tsx で全体に当てるのも忘れずに。

/pages/_app.tsx

import { AppProps } from "next/app";
import { AuthProvider } from "../context/Auth";

const MyApp = ({ Component, pageProps }: AppProps) => {
  return (
    <AuthProvider>
      <Component {...pageProps} />
    </AuthProvider>
  );
};

export default MyApp;

ここまで下準備が出来たら、登録用のフォームを作ります。これは firebase.auth() が持つ createUserWithEmailAndPassword を使って簡単に実装出来ます。

/pages/signup.tsx

import { NextPage } from "next";
import { useRouter } from "next/router";
import Link from "next/link";
import { FormEvent, useEffect, useState } from "react";

import { auth } from "../utils/firebase";

const SignUp: NextPage = () => {
  const router = useRouter();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  // 既にサインインしている時はサインアップの必要が無いので
  // ログイン後のページに飛ばす
  useEffect(() => {
    auth.onAuthStateChanged((user) => {
      user && router.push("/contents");
    });
  }, []);

  // 新規ユーザー登録
  const createUser = async (e: FormEvent) => {
    // form の動作を止める
    e.preventDefault();

    try {
      // firebase のユーザー登録用メソッド
      await auth.createUserWithEmailAndPassword(email, password);
      router.push("/");
    } catch (error) {
      alert(error.message);
    }
  };

  return (
    <>
      <form onSubmit={createUser}>
        <div>
          <label htmlFor="email">メールアドレス: </label>
          <input
            id="email"
            type="email"
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <div>
          <label htmlFor="password">パスワード: </label>
          <input
            id="password"
            type="password"
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <button type="submit">登録</button>
      </form>
      <Link href="/">ログイン</Link>
    </>
  );
};

export default SignUp;

ログイン

次にログイン機能を実装していきましょう。これも firebase.auth() が持つ signInWithEmailAndPassword で簡単に実装出来ます。

/pages/index.tsx

import { NextPage } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import { FormEvent, useEffect, useState } from "react";
import { auth } from "../utils/firebase";

const IndexPage: NextPage = () => {
  const router = useRouter();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  // 既にサインインしている時はサインアップの必要が無いので
  // ログイン後のページに飛ばす
  useEffect(() => {
    auth.onAuthStateChanged((user) => {
      user && router.push("/contents");
    });
  }, []);

  // ログイン処理
  const logIn = async (e: FormEvent) => {
    // form の動作を止める
    e.preventDefault();

    try {
      await auth.signInWithEmailAndPassword(email, password);
      router.push("/contents");
    } catch (err) {
      alert(err.message);
    }
  };

  return (
    <>
      <form onSubmit={logIn}>
        <div>
          <label htmlFor="email">メールアドレス: </label>
          <input
            id="email"
            type="email"
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <div>
          <label htmlFor="password">パスワード: </label>
          <input
            id="password"
            type="password"
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <button type="submit">ログイン</button>
      </form>
      <Link href="/signup">新規登録</Link>
    </>
  );
};

export default IndexPage;

ログインが出来たら /contents に飛ばしてあげます。/contents ではログインしたユーザーのメールアドレスを表示してみましょう。ここでは context を使って実装しています。

/pages/contents.tsx

import { NextPage } from "next";
import { useRouter } from "next/router";
import { useContext, useEffect } from "react";

import { auth } from "../utils/firebase";
import { AuthContext } from "../context/Auth";

const Contents: NextPage = () => {
  const router = useRouter();
  // Context を使用
  const currentUser = useContext(AuthContext);

  useEffect(() => {
    // ログインしていない状態で見ようとした時にログイン画面へ戻す
    auth.onAuthStateChanged((user) => {
      user || router.push("/");
    });
  }, []);

  const logOut = async () => {
    try {
      await auth.signOut();
      router.push("/");
    } catch (error) {
      alert(error.message);
    }
  };

  // ログインしていない時に見ようとしても見られないようにする
  return (
    <>
      {currentUser.currentUser ? (
        <>
          <div>ようこそ、{currentUser.currentUser?.email}さん</div>
          <button onClick={logOut}>ログアウト</button>
        </>
      ) : (
        <></>
      )}
    </>
  );
};

export default Contents;

JSX 部分で3項演算子での分岐をしているのは、ログインしていない状態で見ようとした時に何もレンダリングされないようにするためです。この部分を消すと localhost:3000/contents を直接叩いたときに「ようこそ、さん」と一瞬表示されてしまいます。

完成

これで完成です!ユーザー登録、ログインなどが出来るか確認してみましょう。

f:id:okb_okb:20210407235710p:plain
新規登録

f:id:okb_okb:20210407235744p:plain
ログイン

f:id:okb_okb:20210407235759p:plain
ログイン後の画面

終わりに

Firebase Authentication でログイン機能を簡単に実装することが出来ました。次は NextAuth,js でも同じようなことをして比べてみたいですね。

今回のコードはこちらにアップロードされています。

参考

最適化インターフェース PICOS の行列インデックスに翻弄された話

はじめに

こんにちは。かべです。

研究で最適化問題を解く必要があり、そのソルバーとのインターフェースである PICOS を使用したのですが、その行列の扱い方で詰まったポイントがあったのでまとめてみました。

PICOS とは

最適化問題のソルバーへの入力を、人が書きやすい形で受け取ってから上手く変形してソルバーに渡してくれる優れものです。最適化問題というのは、ここでは線形計画問題とか二次錐計画問題とかその手の問題です。ソルバーの返却値も持ってきてくれるので答えをそのままプログラム内で利用することができます。その返却値で今回引っかかったわけなんですが…

開発環境

以下の環境で動作させています。

  • Python 3.7.6
  • PICOS 2.0.15
  • numpy 1.18.1

詰まったこと

行列から選んだインデックスの値を取り出す際に失敗しました。例えば、次のような行列を使いたいとします。

>>> import numpy as np
>>> import picos as pic
>>> A = pic.RealVariable("A", np.array([[0,1], [2,3]]))

なんてことはない2次元実行列です。一応代入できているか確認しましょう。

>>> A
<2×2 Real Constant: A>
>>> print(A)
[ 0.00e+00  1.00e+00]
[ 2.00e+00  3.00e+00]

代入できていますね。さあ、1行2列目の値が欲しいとき、あなたならどうしますか?

ほとんどの方がこうすると思います。

print(A[0][1])

すると…

IndexError: Invalid expression slice A[0][1]: index out of range

配列の外を参照していると言われました。どうすれば良いのでしょうか?

解決策

どうやら2次元配列ではないっぽい。試しに1次元配列と仮定してみて回してみます。

>>> for i in range(4):
...   print(A[i])
...
0.0
2.0
1.0
3.0

まじか。行列を左上から縦に読んでいっているようです。どの分野の文化なんだろう…

なので問題の解などをnp.arrayで用いる場合は、次のように変形してあげることが必要です。

>>> X = np.zeros((2,2)) # 適当に初期化
>>> for i in range(2):
...   for j in range(2):
...     X[i][j] = A[i+j*2]
...
>>> X
array([[0., 1.],
       [2., 3.]])

まとめ

今回は PICOS で行列を扱う際の注意点についてまとめました。こうなった経緯やうまく直感的に扱う方法等ご存じの方がいらっしゃれば、ぜひ教えてください。

参考

CA Tech Challenge 2days Web フロント向け開発型インターンシップ ONLINE に参加してきました

はじめに

こんばんは、かべです。 サイバーエージェント様が2021年3月27,28日に開催されていた 2days Webフロント向け開発型インターンシップ ONLINE に参加させていただきました。

とても楽しく、運営の方も「ブログ書いてね!」とおっしゃっていたので、備忘録を兼ねて開設したまま放置していたブログに書いていこうと思います。

内容

毎回このインターンのテーマは変わるようですが、今回のテーマは「ネットショップ」でした。商品情報などを返してくれる API が提供されており、これを用いて各自思い思いのネットショップを作っていきます。「商品情報を正しく表示する」などの最低限のレギュレーションはありますが、デザインや機能などのこだわるポイントはそれぞれ異なるため最後の発表会はとても面白かったです。

始めに考えたこと

認証やユーザーごとのカート機能、デザインなどを作り込んだしっかりしたネットショップサイトを作ると自分の実力では2日では足りないだろうという懸念があったため、ネットショップの課題を解決することの出来る機能にこだわったものを作ろうと考えました。

とはいえせっかくメンターの方がついてくださっているのに今まで使ったことのあるものだけで作るのももったいないと思ったので、使ったことのなかったデータベースやログイン周りに触れようと考えていました。

1日目

テーマが発表されたあと、グループメンバーと自己紹介や雑談しながらどんなアプリを作るか考えていました。いろいろ考えた結果、ネットショッピングで困りがちなこととして次の2つを思いつきました。

  • 気軽に手元で合わせることが出来ない
  • コーデの組み合わせを店員さんに聞けない

そこで、実店舗で買っている感覚で商品を見ながらオンライン上で気軽につながれる良さを活かしたアプリを作ろうと考え、商品の閲覧に加えて次の3つの機能を加えることにしました。

  • 服を選んでマネキンに着せるように並べられる
  • 自分が考えたコーデを投稿できる
  • 他の人のコーデを見られる

やることが決まったので次は技術選定です。ここは「選定」と呼べるほどしっかり考えてはおらず、直前まで参加していたハッカソン(これもいつかブログにしたい)で Next.js を使っていたため Next.js + TS でやることにしました。みんなの投稿情報を保持するためのデータベースは Firebase を使いました。理由はちょっと前に Firebase のデータベースを触るハンズオンに参加しており、実戦投入してやろうと考えたからです。

技術も決まったので開発開始です。直前まで Next.js を使っていたこともあり、ハッカソンの時とディレクトリ構成も合わせたろと思って create-next-app で出来たもののディレクトリ構成をいじっていると、useState を呼ぶとアプリが死ぬという謎のバグで苦しみました。メンターさんの環境でも再現できず解決法が分からなかったため、元のテンプレに乗っかってやりました。ハッカソンのチームメイトの偉大さを感じました。

時間のロスはあったものの、そこからはひたすら開発に集中することが出来ました。ちょくちょくメンターさんに相談していたのですが、毎回即レス即解決の神対応でした。ありがとうございました。結局その日はAM2時か3時くらいまでやって、商品情報を表示してマネキンに着せるという機能まで完成させることが出来ました。

2日目

2日目はコーデの投稿機能とデザインに注力しました。Firebase を使ったデータベースの作成から投稿機能の実装は特に詰まることなく出来て良かったです。デザインについては、もともとのセンスのなさと CSS の下手さからあまりうまく出来なかったため最低限で終わりました。あとは軽く発表の準備をして開発終了です。

発表

参加されたみなさんの発表を聞きました。最初にも書いたのですが、同じ題材と API でもそれぞれ個性がありとても面白かったです。デザインがきれいな方が多くとてもうらやましかったです。みんな何で勉強してるんだろう…

あと自分も発表しました。チャットでリアルタイムで反応がもらえるとやっぱり嬉しいですね。

作った物

画像の感じで作りました。左で商品を選んで、右のマネキンに着せられる感じです。

f:id:okb_okb:20210329233105p:plain
画面左で商品詳細や価格を見られる

f:id:okb_okb:20210329233110p:plain
右画面のマネキンに服を着させることが出来る

自分が作ったコーデの投稿をしたり、他の人の投稿を見たりすることが出来ます。

f:id:okb_okb:20210329233533p:plain
コーデを投稿したり、他の人の投稿が見られる

触ってみたい方はこちらへどうぞ。API を開放していただいている間だけですが…

フィードバック

メンターの方から2日間を通したフィードバックをいただきました。どれも「ですよね~~~」となる内容でこれからの励みになりました。具体的には

辺りが改善点です。どうしても短い期間での個人開発ということでコードは汚くなってしまいがちですが、しっかりと意識して書いていきたいです。あとはインターンハッカソンでチーム開発の経験も積んでいきます。

表彰、懇親会

発表の後は表彰です。3人グループの内自分以外の2人の方が選ばれており、「これ自分だけ賞もらえんやつや…」と悲しんでいたのですが、審査員賞ということでアイデア賞をいただくことが出来ました!!いろいろ考えて作ったサービスなので、そのアイデアを評価していただけたのはとても嬉しかったです。1位の方の作品は圧巻でした。2日のクオリティじゃなかったですね。

その後の懇親会では、社員の方の LT を聞いたり他の参加者の方としゃべったりしました。オンラインでもいろいろ交流できるのはやっぱり楽しいですね。かじってたポテチが美味しかったです。

感想

2日間という短い期間でしたが、学びや課題がたくさん出てきた密度の濃いインターンでした。次はもっと長い期間のインターンでも来れるよう頑張っていきます。

運営の方をはじめ、優しくなんでも解決してくださったメンターの方、楽しいつよつよチームメイトの方々と一緒に素晴らしい2日間を過ごすことが出来ました!ありがとうございました!!

おまけ

詰まった時に救ってくれたサイトたち