LoginSignup
14
7

More than 3 years have passed since last update.

React+Typescript+spring bootで勉強会情報をSlackに通知するサービスを作りました

Last updated at Posted at 2020-01-16

概要

タイトル通りReact, Typescript, spring bootを用いて個人開発を行いましたので、備忘録としてここに記します。

作ったもの

名前

勉強会情報通知サービス「noroshi」

noroshi

スクリーンショット 2020-01-15 21.49.37.png

サービス内容

使い方をnoteに書きました✨
https://note.com/uusu/n/n8441acfd64da

connpassやDoorkeeperの勉強会情報をSlackに通知するサービス。
地域・キーワードで絞り込みができるので、欲しい勉強会情報を取得できます。
また、通知設定は複数登録することができます。
自分は java勉強会情報チャンネルJS勉強会情報チャンネル それぞれに通知がいくようにnoroshiを使っています。

モチベーション

興味があるイベントを知ったときには既に募集が終了していたり、満員で参加できなかった経験からこのサービスを開発しました。
また、新たな技術に挑戦したいという気持ちも強かったので、モチベーション維持のためReactやTypescript、kotlinを採用しました。

開発期間

全部合わせて二か月くらいです。

使用技術・ツール

技術

アプリケーション
- React (コンポーネントは全てhooksを用いた関数型コンポーネントで作成しました)
- Typescript
- spring boot
- java
- kotlin (サービス層や一部モデルなどをkotlinで書きました)

インフラ
- heroku
- netlify

ツール

  • intellij
  • vscode
  • postman

  • illustrator (ロゴやトップ画面イラスト作成に使用)

システム構成図

ざっくり構成図(フロー図?)はこんな感じです.
archi.png

フロントはreact/tsで書いたものをbuildしてnetlifyでホスティングしています.
バックエンドはspring bootで書いたアプリケーションをherokuで動かしています.
バックエンドはAPIとバッチが共存している構成です.

認証周りはJWTを利用しています.

(あまり詳しくないため断言できませんが,JAMstackに近い構成になっているかと思います)

技術的な知見

個人的に詰まった所などを記します。

spring boot

複数の実装があるinterfaceのDI方法

今回、勉強会情報をconnpassさん、doorkeeperさんのAPIから取得しています。
それぞれAPIレスポンスが違いますが、最終的には取得した情報をサービス共通で利用する勉強会モデル詰めて利用しています。
そのため、共通のイベント情報モデルリストを返すメソッドを定義したインターフェースを、connpassイベント取得クラス・doorkeeperイベント取得クラスで実装しております

そのような複数実装のあるインターフェースをDIする際は、利用する側でList型でラップすると複数の実装クラスがインジェクションされます。

以下のように書きます。
(RequiredArgsConstructorを用いたコンストラクタインジェクションの場合)

@Service
@RequiredArgsConstructor
class StudyGroupService(

// StudyGroupSiteがインターフェース
val studyGroupService: List<StudyGroupSite>?
・
・
・

foreachなどで共通メソッドを呼び出しを行えます。

studyGroupService?.forEach {
                e ->
                e.インターフェースに定義したメソッド()}
            }

とても簡単なことですが、今まで知りませんでした。
こういった基礎的な知識の見落としに気づけるのは個人開発をするメリットですね。

React

hooksでステートを更新すると一部ステートがundefinedになってしまう

言葉で説明するのがめんどくさいので、簡単に再現してみます。
以下二つのファイルをHTMLファイルに張り付けてブラウザで開くと動作します。

classComponent.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>ClassComponent</title>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<!-- Don't use this in production: -->
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
</head>
<body>
<div id="root"></div>

<script type="text/babel">
class ClassComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
a: 0,
b: 0
};

this.handleChangeA = this.handleChangeA.bind(this);
}

handleChangeA(a) {
this.setState({ a: this.state.a + a });
}

render() {
console.log(`a: ${this.state.a}, b: ${this.state.b}`);
return (
<div>
<p>a: {this.state.a}</p>
<p>b: {this.state.b}</p>

<button onClick={() => this.handleChangeA(1)}>a+1</button>
</div>
);
}
}

ReactDOM.render(<ClassComponent />, document.getElementById("root"));
</script>
</body>
</html>

FunctionComponent.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>FuntionComponent</title>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<!-- Don't use this in production: -->
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
</head>
<body>
<div id="root"></div>

<script type="text/babel">
const FuncComponent = props => {
const [state, setState] = React.useState({ a: 0, b: 0 });

const handleChangeA = a => {
setState({ a: state.a + a });
};

console.log(`a: ${state.a}, b: ${state.b}`);

return (
<div>
<p>a: {state.a}</p>
<p>b: {state.b}</p>

<button onClick={() => handleChangeA(1)}>a+1</button>
</div>
);
};

ReactDOM.render(<FuncComponent />, document.getElementById("root"));
</script>
</body>
</html>

二つは同じ処理をするクラス型コンポーネントと関数型コンポーネントになります。
処理内容はaとbというステートを持ち、a+1というボタンを押すとaのみ1加算されるといったものです。

実際に動かしてみると、setStateの内容は同じなのに関数型コンポーネントではbがundefinedになってしまいます。

このステート書き換えでundefinedになってしまう問題のせいで1日潰れました。
しかし,改めて公式ドキュメントを読むと以下のような記述があります。

しかしクラスでの this.setState とは異なり、state 変数の更新は、マージではなく必ず古い値を置換します。

hooksでは、古いステートが引き継がれないとしっかり公式のドキュメントで書いていました..

クラスコンポーネントと同じ動作にするためには、ステート更新処理を以下のように修正する必要があります。

 const handleChangeA = a => {
 // スプレッド構文を用いて、古いステートも記述する
          setState({ ...state, a: state.a + a });
        };

これで無事hooksを用いた関数型コンポーネントでステート更新を行うことができました。
これが起きてしまった原因は、自分自身Reactを触るのがこれで初で、最初はクラスコンポーネントベースの開発の勉強をしていたため、知識がごちゃごちゃになってしまったことと
単純に公式ドキュメントをしっかり読まなかったことですね。

これを反省し、新たなものを学ぶ際はできるだけ公式ドキュメントや公式が用意してるチュートリアルをやろうと思います。。

useEffectで無限にAPIをコールしてしまう

今回関数型コンポーネント+hooksで全て開発しています。
そこで何らかのリストなどをAPIから取得する際は、useEffect内でAPIコールを行っております。
useEffectは、コンポーネントのレンダーが終わるたびに実行されるメソッドです。
詳しくは公式ドキュメントを確認してください

最初に以下のようなコードを書きました.

useEffect(() => {
    APIをコールするメソッド()
    .then(success => {
        setState({...state, ステートの更新})
    })
})

コンポーネントはステートが更新される度に再レンダーされます。
そのため、このコードではレンダー→useEffect内でAPIコール→ステートが変わるので再レンダー→useEffect内でAPIコール→ステートが変わるので再...
といったように無限ループが発生してしまいます。

この問題は以下のスタックオーバーフローを発見し解決することができました。
https://stackoverflow.com/questions/53070970/infinite-loop-in-useeffect

useEffect(() => {
    APIをコールするメソッド()
    .then(success => {
        setState({...state, ステートの更新})
    })
}, []) // useEffectの第二引数に[]を置く

このように第二引数に[]を指定すると一度だけ実行される副作用となるため、無限ループを回避できます。

このスタックオーバーフローを見つけた後に公式ドキュメントを見ると、同じような事が書いてあり思わず自分を殴りました。

最後に

コードの品質などまだまだ改善できるところがあるので、暇を見つけてちょこちょこと修正していきたいです。
まず、もっとテストをしっかり書きたいですね。バックエンドはもちろんですが、フロントエンドのテストは書いたことがないので挑戦してみたいです。

あとは、使用技術の強みを活用できていない点も直したいです。
バックエンドだとkotlinを使用していますが、いまいちkotlinの強みを引き出せていません。
他にもreactでは関数型コンポーネントの「ステートを扱うロジックの再利用性が高い」などの利点をまだ活用しきれていません。

開発プロセスや(最低限のUXを考えた)機能設計などの意思決定プロセスなどは別記事にしてまとめようと思います。

もし誤字脱字やQiitaの利用規約に反するような内容あればご指摘をお願いします

14
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
7