0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

外部API「SendBird」を使ってみた【学習記録】

Posted at

はじめに

こんにちは!
未経験からエンジニアを目指して学習中のReoです。

今回は作成しているマッチングアプリ風転職支援サイトの更新を行っていきたいと思います。
内容としては、Send Birdという外部APIを活用してマッチングした企業・求職者同士がチャットできるようにします。

これを行うことで、使用したことのあるSupabaseの外部API利用以外の実装経験が積めるので、外部APIの理解がより深まること間違いなしです!

そもそも「Sendbirdって何?」

Sendbird(センドバード)は、簡単にアプリにチャット機能を追加できるクラウドサービスです。
LINEのようなリアルタイムチャットを、自前でサーバーを用意せずに構築できます。

いざ実装!

まずは、Send Bird側でアカウント登録を行います。
次に、npmでSendbirdのUIKitをインストールしていきます。
インストールの選択肢として、以下の2パターンがあるようなので違いを理解していきます。

npm install @sendbird/uikit-react or @sendbird/chat
ライブラリ名 用途 開発難易度 カスタマイズ性 UI付きか
@sendbird/uikit-react UI付きチャットの即導入 △ やや低め ✅ あり
@sendbird/chat チャット機能の低レベルAPI 中〜高 ◎ 高い ❌ なし

今回は、あくまで外部APIとの連携がメインになってくるので、@sendbird/uikit-reactを使うことにします。
ここでインストールを試みると、以下のエラーが表示されました。
スクリーンショット 2025-07-14 8.18.48.jpeg

どうやらこのエラーは「依存関係のバージョンの衝突(dependency conflict)」によって発生しているようです。

Reactのバージョン(19.1.0)と、react-day-picker@8.10.1 が要求するバージョン(React 16〜18)に不整合があるためです。

→ バージョンが合わず、npm が依存関係の解決に失敗

以下を参考にして「--legacy-peer-deps 」をつければインストールはできることを確認しました。

バージョンの不整合を無視してインストールできるだけなので、後々エラーを起こす可能性があるようですが、「--legacy-peer-deps 」でインストールしてみようと思います。

npm install @sendbird/uikit-react --legacy-peer-deps

実行してみると、無事インストール完了したようです。
スクリーンショット 2025-07-16 6.49.00.jpeg
ライブラリに追加されていることを確認できました。
スクリーンショット 2025-07-16 6.55.00.jpeg
次に「.env_local」ファイルで以下のSendBirdのIDとTOKENの設定を行いました。
これでアプリ側のSendBirdの環境変数設定が完了です。

NEXT_PUBLIC_SENDBIRD_APP_ID=XXXXXXXXXXXX
SENDBIRD_API_TOKEN=XXXXXXXXXXXXXX

.env.localファイルとは?
パソコンにしか見えない「ひみつのメモ帳」です。
アプリを作るときに、人に見せたくない情報(パスワードやAPIキー)を入れておく場所です。

なにに使うの?
例えば、こんなことがあったとします:

「チャットアプリを作るけど、Sendbirdというサービスの“鍵”を使わないといけない!」

その“鍵”(APIキーやIDなど)をプログラムに直接書くのは危ないので、
.env.local というファイルにこっそり書いておく、というわけです。

注意点

  • .env.local はぜったいにGitにアップしないでください!(他の人に秘密がバレます)

  • .gitignore に .env.local が入っていればOKです

  • もし「画面(ブラウザ)でも使いたい」場合は、変数名を NEXT_PUBLIC_ で始めます(TOKENやROLEKEYなどの読み込まれたくない情報にはつけない)

しかし、結局実装のイメージ的にuikit-reactではなく、以下のコマンドでSDK単体をインストールし直すことにしました。
理由としては、UIKITではUIのカスタマイズ性に欠けており、事前に用意していたUIを使用できないと判断したからです。

npm install @sendbird/chat --legacy-peer-deps

下準備は完了したので、これからは全体の実装の流れを整理します。

✅ 全体の実装フロー ① マッチング成立時にSendbirdのチャネル作成 Supabaseのmatchesテーブルにchannel_urlカラムを追加。(完了) sendLikeActionやmatch判定ロジックの中で、マッチが成立した場合に以下を行う: SendbirdのAPIを叩いてチャネル作成(ユーザーIDはSupabaseと共通のIDを使用)。 生成されたchannel_urlをSupabaseのmatchesテーブルに保存。

② chat/page.tsx にSendbird SDKを導入して、チャットウィンドウでチャネルに接続
sendbird-uikit or sendbird-chat SDKで初期化。
ログインユーザーをSendbirdにログイン。
conversationsをマッチ済みユーザー一覧 + channel_urlに置き換える。

③ chat/[partnerId]/page.tsx で個別チャットルームを表示
Supabaseのmatchesテーブルからchannel_urlを取得。
Sendbird SDKで該当チャネルに参加し、MatchedChatで表示。

以下のようにファイルを実装していきます。

Sendbird SDK初期化ユーティリティの作成

client.jpeg

マッチ時にSendbirdチャネル作成&保存

api.jpeg

chat/page.tsxのチャネル一覧にSendbirdチャネルを使う

実装中に以下の警告文が表示されました。
①ChatConversationは見つかりません

理由:chat-dummy-data.tsで定義されているのに、インポートしていなかった。
→ファイル上部にインポート文を記載することで解消。

②プロパティ 'groupChannel' は型 'SendbirdChat' に存在しません

理由:
groupChannel は Sendbird SDK v4 で GroupChannelModule を使用したときに拡張されるプロパティですが、
型定義 (SendbirdChat) 上は デフォルトで groupChannel が存在しないように見える ことがあります。

これは型補完の限界によるもので、実行時には正しく存在します。

chat:page.jpeg
chat:page_2.jpeg

create-match-channel.tsの実装

サーバーサイドで動作するアクションで、マッチングが成立した際に呼び出されます。
Supabaseからマッチング情報を取得し、createSendbirdChannelを呼び出してSendbird上にチャットチャンネルを作成し、そのチャンネルURLをSupabaseのmatchesテーブルに保存する役割。
create-match-channel.jpeg

lib/sendbird-chat.tsの実装

Sendbirdを使ったチャットの主要な機能であるメッセージの取得(fetchMessages)と送信(sendMessageToChannel)を実装。
getSendbirdClientを内部で利用して、特定のチャンネルのメッセージ履歴を取得したり、新しいメッセージを送信したりします。
sendbird-chat.jpeg

lib/supabaseChat.tsの実装

Supabaseのmatchesテーブルから、指定されたユーザーIDが関わっているマッチング情報(特にSendbirdのchannel_url)を取得するためのgetMatchedConversations関数を定義。チャットページで会話リストを表示する際に使われます。
supabase-chat.jpeg

実際にローカル環境でうまく機能するかを確認します。(以降コマンド省略)

npm run dev

ここで問題が、、チャット画面に遷移しようとすると、読み込み中のまま進みません。

原因と変更内容 🧠原因

チャット画面は、以下のような「複数の準備」ができて初めて正しく表示されます:

  • ログインしているユーザーの情報取得(認証)
  • Supabaseからマッチング済みのチャネル一覧を取得
  • Sendbirdからチャット履歴を取得
  • 会話相手の情報(アイコンや名前など)を取得
    これらには 少し時間がかかることがあるため、処理が完了する前に画面を表示しようとすると、「データがない」状態で表示されてしまい、画面が固まる原因になります。

✏️ 変更したコードの中身

  1. try-catch-finally を使って安全に処理を実行
    try {
    // チャネルやメッセージの取得などの準備
    } catch (error) {
    // もしエラーが出ても止まらないように
    console.error("エラー:", error)
    } finally {
    // 最後に「準備完了」のフラグを立てる
    setIsInitialized(true)
    }
  2. 初期化が終わるまで表示を待つようにした
    if (isLoading || !isInitialized) {
    return <読み込み中... />
    }
  • isLoading: 認証(ログインチェック)がまだ終わっていない
  • isInitialized: チャットの初期準備が終わっていない
    どちらかが true の間は「読み込み中...」と表示し、すべて準備完了したらチャット画面を表示するようにしています。
    変更1.jpeg

次に、別の問題が発生しました。
内容としては、マッチングした後にSendBirdのチャットが作成されないことです。
原因を紐解いていくと、そもそもユーザーのSendBirdのアカウント作成が行われていないことがわかりました。(app/register/jobseeker/page.tsxというクライアントで利用しようとしていた)
そのため、app/actions/auth.ts の signUpJobseekerAction 関数内でSendBirdユーザーを作成するように変更します。

修正手順 1. lib/sendbird-api.ts に createSendbirdUser 関数を(再度)追加します。ただし、今回はサーバーサイドで実行されることを想定し、process.env.SENDBIRD_API_TOKEN を使用します。(NEXT_PUBLICをつけない) 2. app/actions/auth.ts の signUpJobseekerAction 関数を修正し、Supabaseユーザー作成が成功した後に createSendbirdUser を呼び出すようにします。 3. app/register/jobseeker/page.tsx から createSendbirdUser 関数と関連する呼び出しを削除します。(不要になった)

しかし、いまだにSendBirdアカウントは作成されません。
さらに調査を行います。するとアカウント作成時に実行されていた関数がsignUpJobseekerActionではなく、 createJobseekerProfileActionだったことがわかりました。

修正手順
  • app/actions/profile.ts の先頭で createSendbirdUser をインポートします。

  • createJobseekerProfileAction 関数内で、supabaseAdmin!.from("jobseeker_profiles").insert(profileData) の成功後、createSendbirdUser を呼び出すコードを追加します。userId は既に取得済みです。nickname として formData.get("name") as string を、profileUrl として photoUrl を渡します。 app/actions/profile.tsの確認が完了しました。

この修正により、アカウント作成を行なった際に、SendBirdのユーザー登録も行われるようになりました! 次に問題点として、SUPABASEのユーザーIDとSendBirdのユーザーIDが違うことがあります。

想定としては、同じIDで管理する予定だったので、まずは同じIDである必要性について再確認します。

同じIDが良い理由 1. マッチングとの整合性が取りやすい Supabaseのmatchesテーブルなどで管理しているユーザーID(例:user_id_jobseeker, user_id_company)と Sendbirdのチャネルに参加するユーザーIDが一致していれば、 → どのチャネルに誰が関わっているかが一目で分かります。
  1. チャネル作成・アクセス制御が簡単になる
    相手のIDだけでチャネルを特定できる

  2. ログイン時のSendbird接続がスムーズ

  3. 二重管理の必要がなくなる

ここで改めてIDを確認すると、SendBirdとSUPABASEのUsersのIDで一致していました。
SUPABASE側でIDを確認していた箇所がjobseeker_profilesテーブルだったことが不一致だと勘違いした理由でした。

企業側でも同じコードを実装してから、現状の実装でマッチングまで行なってチャンネルが作成されるかテストを行います。

テストを行うと、マッチング後にグループチャンネルが作成されることを確認できました。
しかし、チャット欄に遷移しても「マッチングしたユーザーがいません」と表示されてしまします。

マッチング成立後にチャットが開始できない問題を修正しました。

原因は、チャットページがマッチ一覧を取得する際に、自身の役割(求職者か企業か)を正しく認識できず、データベースへの問い合わせに失敗していたことでした。
そのため、マッチングなしの判定になってしまっていました。

この問題を解決するため、lib/supabaseChat.ts ファイルを修正し、ユーザーの役割を正しく取得してからデータベースに問い合わせるようにロジックを変更しました。

この修正により、マッチング同士のチャット画面が表示されるようになりました。

次に実際にチャットを送信できるのか確認します。
すると、送信時に以下のエラーが表示されてしまいました。

RangeError: Invalid time value
    at formatDistance (webpack-internal:///(app-pages-browser)/./node_modules/date-fns/formatDistance.js:107:34)
    at formatDistanceToNow (webpack-internal:///(app-pages-browser)/./node_modules/date-fns/formatDistanceToNow.js:93:78)
    at formatMessageTime (webpack-internal:///(app-pages-browser)/./lib/chat-dummy-data.ts:26:116)
    at eval (webpack-internal:///(app-pages-browser)/./components/chat/ChatSidebar.tsx:72:130)
    at Array.map (<anonymous>)
    at ChatSidebar (webpack-internal:///(app-pages-browser)/./components/chat/ChatSidebar.tsx:21:37)
    at ChatPage (webpack-internal:///(app-pages-browser)/./app/chat/page.tsx:235:104)
    at ClientPageRoot (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/client-page.js:20:50)
修正内容

原因
Sendbirdから取得したメッセージのタイムスタンプが、コンポーネント間で受け渡される際に Date オブジェクトから文字列に変換され、日付フォーマット関数が無効な値を処理しようとしたことでした。

修正
app/chat/page.tsxにおいて、Sendbirdから取得したメッセージのタイムスタンプを、new Date() を用いて明示的に Date オブジェクトに変換するようにしました。これにより、データ転送中にタイムスタンプが文字列に変換されてしまっても、エラーが発生しなくなります。
型が不明確だった msg パラメータに ChatMessage 型を定義し、TypeScriptの型エラーを解消しました。

app/chat/page.tsx ファイルを修正し、メッセージデータをコンポーネントに渡す直前に、タイムスタンプを明示的に Date オブジェクトに再変換する処理を追加しました。
スクリーンショット 2025-08-14 7.02.40.png

次はチャットを送信しようとした際に、別のエラーが発生しました。

Error: Encountered two children with the same key, `undefined`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.
    at createUnhandledError (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/errors/console-error.js:27:71)
    at handleClientError (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/errors/use-error-handler.js:45:56)
    at console.error (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/globals/intercept-console-error.js:47:56)
    at eval (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:7113:23)
    at runWithFiberInDEV (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:1511:30)
    at warnOnInvalidKey (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:7112:13)
    at reconcileChildrenArray (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:7154:23)
    at reconcileChildFibersImpl (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:7502:30)
    at eval (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:7607:33)
    at reconcileChildren (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:8049:13)
    at beginWork (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:10293:13)
    at runWithFiberInDEV (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:1511:30)
    at performUnitOfWork (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:15120:22)
    at workLoopSync (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:14944:41)
    at renderRootSync (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:14924:11)
    at performWorkOnRoot (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:14411:13)
    at performSyncWorkOnRoot (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:16290:7)
    at flushSyncWorkAcrossRoots_impl (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:16138:21)
    at processRootScheduleInMicrotask (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:16175:7)
    at eval (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:16309:13)
    at eval (webpack-internal:///(app-pages-browser)/./components/chat/ChatMessages.tsx:65:120)
    at Array.map (<anonymous>)
    at eval (webpack-internal:///(app-pages-browser)/./components/chat/ChatMessages.tsx:65:30)
    at Array.map (<anonymous>)
    at ChatMessages (webpack-internal:///(app-pages-browser)/./components/chat/ChatMessages.tsx:38:43)
    at ChatWindow (webpack-internal:///(app-pages-browser)/./components/chat/ChatWindow.tsx:144:88)
    at ChatPage (webpack-internal:///(app-pages-browser)/./app/chat/page.tsx:287:108)
    at ClientPageRoot (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/client-page.js:20:50)
修正内容

修正
・lib/sendbird-chat.ts の堅牢化: メッセージ送信後、Sendbirdサーバーから返されるデータに messageId が含まれていない場合、これを明確なエラーとして処理するように変更しました。以前は、id がない不正なメッセージオブジェクトが作成され、エラーの原因となっていました。

・型定義エラーの修正: 関連するTypeScriptの型エラーを解消し、コードの安定性を向上させました。
画像
スクリーンショット 2025-08-14 7.10.13.png

次のチャット送信時に以下のエラーが発生し、日付や送信文章が「Invalid Date」と表示されてしまいました。

Error: Failed to send message or receive messageId.
    at sendMessageToChannel (webpack-internal:///(app-pages-browser)/./lib/sendbird-chat.ts:45:15)
    at async handleSendMessage (webpack-internal:///(app-pages-browser)/./app/chat/page.tsx:184:33)
修正内容

原因
Error: Failed to send message or receive messageId. というエラーは、まさに先ほど lib/sendbird-chat.ts に仕込んだエラーチェック throw new Error("Failed to send message or receive messageId.") が作動したことを意味します。

これは、Sendbirdの channel.sendUserMessage(params) が返す sentMessage オブジェクトに、messageId が含まれていない(null0、または undefined である)ことを明確に示しています。

なぜ messageId がないのか?

Sendbird SDKの sendUserMessage は、メッセージ送信をリクエストした後、サーバーからの完全な応答を待たずに、ローカルで生成された一時的なメッセージオブジェクトを返すことがあります。この一時的なオブジェクトには、まだサーバーで採番されていないため、messageId0null になっている場合があります。

修正
lib/sendbird-chat.ts の sendMessageToChannel 関数を、Promiseでラップしたコールバックスタイルに書き換えます。これにより、messageId がサーバーで正常に採番され、取得が完了した時点で初めて、そのメッセージオブジェクトを返すようになります。

画像
スクリーンショット 2025-08-14 7.29.36.png

最後に以下の複数のエラーを修正すれば、無事チャットを利用できるようになりました。

複数のエラー
・文章送信時にEnterキーで日本語の変換を確定すると、意図せずメッセージが送信されてしまう
・企業側のチャット画面で、チャット対象者の名前が正しく表示されず、企業自身の名前が表示されてしまう
・チャットの吹き出しが左右に正しく表示されず、どちらの発言か分かりにくい問題
・チャットメッセージが送信者ごとにグループ化されてしまい、時系列に表示されていない問題

複数の修正内容

チャット入力コンポーネントを修正し、日本語入力の変換を確定するためにEnterキーを押してもチャットが送信されないようにしました。isComposingという状態を導入し、文字変換中にEnterキーが押された場合はメッセージ送信処理が実行されないように変更しました。

・企業側のチャット画面で、チャット相手の名前が正しく表示されるように app/chat/page.tsx を修正しました。

ログインしているユーザーの種類(企業か求職者か)を正しく判定するようにしました。
チャット相手の情報を取得する際に、Sendbirdのニックネームではなく、データベースに登録されている正しい名前とプロフィール画像を取得するように修正しました。

ChatWindow.tsx でダミーのユーザーIDを使用していたのをやめ、実際のログインユーザーIDを渡すように修正しました。
これにより、ChatMessage.tsx で自分のメッセージか相手のメッセージかを正しく判定できるようになり、自分のメッセージは右側に、相手のメッセージは左側に表示されるようになります。
自分のメッセージの吹き出しの背景色を、より見分けやすいように濃い紫色に変更しました。

チャットのメッセージが送信者ごとに表示され、時系列が分かりにくくなっていた問題を修正しました。

components/chat/ChatMessages.tsx ファイルを修正し、メッセージを表示する直前に必ずタイムスタンプでソートするように変更しました。これにより、企業と求職者のメッセージが混在していても、常に正しい時系列で表示されるようになります。

後はこれをGithubにプッシュすれば完成です!

完成品

スクリーンショット 2025-08-14 7.51.38.png

学んだこと・つまずいた点

学んだこと

  • UIKitとSDK単体の違い

    • @sendbird/uikit-react はUI付きで導入が早いが、UIカスタマイズ性は低め。
    • @sendbird/chat は低レベルAPIでカスタマイズ自由度が高いが、実装難易度は高め。
  • 依存関係のバージョン不一致問題

    • React 19 と react-day-picker の対応バージョンが不一致で --legacy-peer-deps が必要だった。
  • .env.local の使い方と管理方法

    • APIキーやIDは .env.local に保存し、NEXT_PUBLIC_ 接頭辞でブラウザ側でも利用可能にできる。
  • SDK初期化の流れ

    • GroupChannelModule を利用して SendbirdChat.init()connect() で接続。
  • SupabaseとSendbirdのID統一の重要性

    • ユーザーIDが一致していると、マッチング管理やチャネル特定が容易になる。
  • マッチ成立時のチャネル作成フロー

    • Supabaseの matches テーブルに channel_url を保存しておき、チャットページで利用する。
  • チャット初期化の遅延対策

    • isLoading / isInitialized フラグで認証やデータ取得が終わるまで待機表示にする。
  • 日付のInvalid Date問題

    • Next.js のコンポーネント間データ渡しで Date が文字列化されるため、new Date() で再変換が必要。
  • React key重複エラーの原因と対策

    • 新規送信メッセージは一時ID(例: Date.now())を使い、key の重複を防ぐ。
  • sendUserMessageのmessageId未取得問題

    • コールバックを使ってサーバー採番後のメッセージを返すことで解決。
  • UX改善ポイント

    • 日本語変換中のEnterキーで送信されないように isComposing を導入。
    • 自分/相手の吹き出しを正しく左右表示するためにログインユーザーIDを利用。
    • チャットメッセージをタイムスタンプでソートし、常に時系列順に表示する。

つまづいたこと

  • UIKitインストール後にUIカスタマイズが想定より難しく、SDK単体に切り替えた。
  • groupChannel が型上存在しないエラー(実行時は動くが型補完では未定義)。
  • チャット画面が初期化中に固まる問題(フラグ制御で解決)。
  • SendBirdユーザー作成の呼び出し箇所が誤っていて、マッチング時にチャネルが作成されなかった。
  • SUPABASEの jobseeker_profiles のIDと users のIDを混同し、ID不一致だと誤認。
  • メッセージ送信時に Invalid Datekey 重複エラーが頻発。
  • sendUserMessage の即時返却で messageId が未設定なケースに遭遇。
  • 企業側の画面で自分の名前が相手として表示される不具合。
  • メッセージ表示が送信者ごとにグループ化され、時系列が崩れる問題。

まとめ

今回はかなりエラーの数が多く、実装まで時間がかかってしまいました。
なのでもう少しスピード感を意識して、実装期限を明確にして行いたいと思いました。
そうすることで、自分の管理能力や実力の認識を確認できると思います!

エラーの修正にはClineを活用しているのですが、まだまだ分からないことが多いので
繰り返し実装を行って、Clineを活用しつつも、修正方針が自分で定められるような知識や経験を積んでいこうと思います!(精進しますということですね…!)

次回は、
未定ですが、もう少し投稿しやすいものにしようかと考えています!

最後まで読んでいただきありがとうございました!

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?