【アプリ開発日記50週目】useSWRでクイズ更新・仮公開へ
おはようございます、ちゅーりんです。いよいよ50週目ですね。図らずも演習機能が一通り揃ってきた今回、いよいよウェブアプリとしてデプロイ・仮公開して速度測定やスマホでの動作を確認できるようにしていきます。
と言ってもまだ「お気に入り」「非公開」「指定した人にだけ公開」「演習後一定期間に通知が来る」など未実装の機能も多く、みんなに使ってもらうというよりかは、あくまで最低限の「問題を作成」「それを解いて履歴を残せるようにする」「間違えた問題だけ2周目」といった演習機能の使い心地を確かめる、といったイメージです。
このまだ足りない「問題を作成」部分を今回実装し、仮公開していきます!
1,useSWRの導入
チュートリアルで一瞬触れたけど、その後グーグル認証やらSSG?SSR?みたいなレンダリング論やらで手を付ける余裕のなかったuseSWR。当時はnextjsを知ったばかりだったこともあり、getStaticPropsやPathsと混ざって非常に混乱しました。
けど、getStaticPropsやAPI、axiosも慣れてきた現在。何より「データを更新したら感知して自動で目の前のブラウザを書き換えてくれる」という大きな魅力もあり、いよいよuseSWRを実装していきます!
すごく不思議なロゴ。ヘビ?鎖?こんなに横長なロゴも珍しいですね。でもnextjsと同じ開発元ということもあり、今では非常にすんなり頭に入ってきます! すごく感動です!!
今回は、SWRだけ抜粋すると以下のようになりました。つまり、今までの「getStaticProps」に少し書き加えるだけでいいんですね。
import useSWR from 'swr'
import TaskForm from '../components/TaskForm';
const fetcher = (url) => fetch(url).then((res) => res.json());
const apiUrl = `${process.env.NEXT_PUBLIC_RESTAPI_URL}api/list-task/`
export default function TaskPage({ staticfilterdTasks }) {
const { data: tasks, mutate } = useSWR(apiUrl, fetcher, {
fallbackData: staticfilterdTasks,
});
// mutate()が行われるとキャッシュデータも更新
const filteredTasks = tasks?.sort(
(a, b) => new Date(b.created_at) - new Date(a.created_at)
);
useEffect(() => {
mutate(); // useSWRのところの情報に最新のものを入れる
}, []);
return (
<StateContextProvider>
<Layout title="task-page">
<TaskForm taskCreated={mutate} />
<ul>
{filteredTasks && filteredTasks.map((task) =>
<Task key={task.id} task={task} taskDeleted={mutate} />
)}
</ul>
</Layout>
</StateContextProvider>
)
}
export async function getStaticProps() {
const staticfilterdTasks = await getAllTaskData();
return {
props: { staticfilterdTasks },
revalidate: 3,
};
}
で、無事保存と同時に最新データをとって来れるようになりました!
2,getStaticPropsでユーザーidで絞ってデータを取得する
一方、getStaticPropsで、ユーザーidで絞ってデータを取得しようとしたものの中々うまく行かず。冷静に考えると「ビルド時にデータ取得」なので、その時点ではセッション情報があるはずもなかったのです。
ユーザーidで絞る必要があるものは登録した問題、演習履歴…ほぼ全部ですよね。これはさすがに今のうちに解決したほうがよさそう。
いろいろ方法を探したのですが、今のところはURLに入れる方向で行きます。
▼
結局、どう調べたらいいか分からない・見つからない内容は公式ドキュメントが最強。数時間の末です。。。
これでgetStaticProps内、つまりビルド時のレンダリングでもユーザーごとにカスタマイズされたページを生成できます!
…と思っていたのですが、冷静に考えると「他の人がユーザーID知ってると自分の履歴とかお気に入り全部見れちゃうよね」ということに気づいたので、結局ボツになりました。他のサイト、ほんとどうやってnextjsでユーザー別のページ(プロフィール画面以外)を作成しているのか気になります。Zennもnextjsなのにめちゃくちゃ速い…!
決定的に違うのはHTMLの読み込みですね。この違いを見ると、やっぱり事前レンダリングめちゃくちゃいいと痛感します!
素直にブラウザ側でデータをフェッチしてくるしかないんでしょうか。一度それで試してみて、デプロイ後にパフォーマンスチェックしてみましょう。
const apiUrl = `${process.env.NEXT_PUBLIC_RESTAPI_URL}api/result-by-quiz-user?quizid=${quiz.id}&userid=${userId}`;
const { data: results_mutate, mutate } = useSWR(apiUrl, fetcher, { fallbackData: results, }, { refreshInterval: 1000 });
もうおなじみのuseSWRでフェッチし、無事表示までできました!
3,UUIDとエディタの修正
「重複しなさそうだし類推しにくいし」とuuidにしていたユーザーIDですが、結局1から始まる標準のPKに戻そうと思います。上記のようにURLが長くなってしまうのが好きじゃないのもそうですし、改めて調べてみるとパフォーマンスに影響が出てくるのだそうです。
長く議論されている内容ですし、それぞれなるほどと思うのですが、どちらでもいいなら標準の方にしよう、ということで今回は戻します。
さて、データを再登録しよう…と思ったら今度は新規登録ができず。多対多外部キーを追加したのでエラーになっていました。ここでは解決した方法を掲載。これでAPI経由でも多対多を含めて保存できます!
--- post.js ---
const data = { user: userId, title: title, desc: desc, status: status, tags: [{id:1,user:1,name:'はじめてのタグ'},{id:2,user:2,name:'秘密のタグ'}], updated_at: now_str, created_at: now_str };
await axios.post(`${SERVERURL}api/${url}/`, data)
.then(res => {
if (res.status != 201) console.log(`例外発生時の処理 : ${res.status}`);
})
.catch(err => {
console.log(err.response);
});
※ 多対多のtagsは、tags: [{id:1,user:'',name:'-'},{id:3,user:'',name:'-'}] のようにidと
必要な項目さえ揃っていれば、idに一致した外部キーが保存されました
--- serializer.py ---
class BookSerializer(serializers.ModelSerializer):
tags = BookTagSerializer(many=True)
class Meta:
model = Book
fields = ('id', 'user', 'title', 'desc', 'status', 'tags', 'updated_at', 'created_at')
def create(self, validated_data):
tags = []
for t in validated_data['tags']:
tags.append(t['id'])
validated_data.pop('tags')
book = Book.objects.create(**validated_data)
book.tags.set(tags)
book.save()
return book
ネスト状のデータを保存する場合
Djangoにおける多対多のドキュメント
▼
また、ずっと放置していたことですが、Editor.jsで作成・htmlに変換して保存してそれを表示する、もこの機会に修正しておきました。
ただ、はじめは<h1>test</h1>のようにタグごと表示されてしまっていた(正常動作)ので、以下のように処理を加えました。参考になれば。
export default function SectionQuizComp({}) {
const html2text = html => {return <div dangerouslySetInnerHTML={{__html: html}} />;}
return (
<div>{ html2text(quiz.front) }</div>
)
}
さて、おそらくデプロイ後もエラーが見つかると思うので、このあたりで一度デプロイしていきます!
4,デプロイ・修正
デプロイはいつもどおり、バックエンドはConoha、フロントエンドはvercelを使います。GitHubにpushすると自動で検知してサーバー上で更新してくれるため、非常に便利です。
AWSで18万飛ばしたり、沼にハマってConohaさんに非常に丁寧に付き合っていただいたり、進級試験期間の複数同時接続でなんとか運用できたりと色々あった一年間。今回のバックエンドは、今のレベルに一番合っているなと感じたConohaで構築しました。
で、フロントエンドをデプロイしたところ…
見事にエラーの嵐。主なミスをあげると
next-authとnext13の競合。→ package(/lock).jsonで「"next": "12.2.5",」のようにnextのバージョンを下げた
React Hook "useState" is called conditionally.~ →エラー文にある通り、フックが現れる前にreturnしうる場所があった。ので順番を修正
[id].jsでreturn前に「section.id」などを含んだuseSWRを入れると、「idは定義されていません」と怒られる。原因不明だが、子コンポーネントでuseSWRすることにして、このファイルではsection.id箇所を削除
.env.localに書いていた環境変数はデプロイ時の詳細設定で入力(先頭のprocess.env. は必要なし)(そもそも.gitignoreで除外していたので)
ということで、新たにVPSを借りてアクセスしてみると…
無事実機でも表示できました! ノート・セクション・問題の作成と履歴の保存、エディタの入力も問題なく行えそうです。(いくつか修正しましたが)
ただしAPIはhttpsじゃないとだめらしいので、APIのサーバー用のドメインも登録・SSL化すると接続成功!
さらにvercelで入力した環境変数が反映されずAPIでhttpのリクエストを送り続けていたので、以下のところから再ビルド(すでに製品版にしていても同じ表示になります)。
それでもNext-authでうまく認証されなかったため、Django APIでグーグル認証を試してみる(今までもこのaxiosの部分でエラーになることが多かった)と
原因を発見。どうやら、サーバーにインストールしたPyJWTのバーションが新しすぎたようで。
競合しているライブラリがあったので少し怪しい感じはしますが、これで無事APIは返ってきました!
また、今回のエラーに関係していたかは不明ですが、本番環境の場合、『「⑴ “secret“を記載」または「⑵ 環境変数に”NEXTAUTH_SECRET”を設定」のどちらかをしないとエラーになる』(https://craft-time.jp/nextauthjs-options/)とのことだったので、下記のようにsecretも追記しておきました。
export default NextAuth({
providers: [
GoogleProvider({
clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
clientSecret: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_SECRET,
}),
// ...add more providers here
],
secret: 'secret',
そしてついに…
無事表示されました!! Woo---foo-----!
最後に一番気になっていたページの表示速度ですが、ページ遷移しても速度は非常に快適でした! 押した瞬間に次の画面が表示されるくらい。一方でデータが更新されていないなどgetStaticProps特有の障害も見られたため、気付き次第修正していきます。
おわりに
今回、サーバーでの運用までたどり着きました! これで基盤が固まり、ここからはこれまで追加したかった機能の整備 + 実際の使用感を比べながら気になったところを修正していけるようになりますね。
次回から、実際にこれから国試対策で使いたい問題も入れながら、最後に何日前に演習したかを表示・通知が来るようにしていきます。
useSWRやget「ServerSide」Props、APIでの多対多に応じたcreate()の上書きなど、ドキュメントを漁りながらアプリを作成していく習慣もつきつつあるこの頃です。と言ってもまだまだ Document <<< Stackoverflowですが笑
メリークリスマス! ではでは!