はじめに
React学習の集大成として、AIから返信がもらえる感謝日記アプリを個人開発しました。
0から1を生み出す過程は挑戦の連続でしたが、同時に多くの気づきと学びをもたらしてくれました。今回の開発経験で得た苦労や喜び、そして学びを共有したいと思います。
どうしてこのアプリを作成しようと思ったかの理由はこちらの記事で詳しく説明をしています。
よかったら見てみてください!
完成したアプリ
デモ
概要
その日に感謝できることを3つ入力するとAIから返信をもらえて、続けていくうちに幸福度を上げていくアプリ
使用している技術スタック
- 環境
vite: 5.3.4
firebase: 10.12.5
- 言語
typescript: 5.2.2
- ライブラリ
react: 18.3.1
react-hook-form: 7.52.2
react-router-dom: 6.26.0
axios: 1.7.3
- DB
supabase/supabase-js: 2.45.0
- CSS
tailwindcss: 3.4.8
daisyui: 4.12.10
- Test
testing-library/jest-dom: 6.4.8
testing-library/react: 16.0.0
testing-library/user-event: 14.5.2
types/jest: 29.5.12
実装機能
新規ユーザー登録
ユーザーの登録を行えます。
ログイン機能
ジャーナル登録機能
その日に感謝したいことを3つ入力していきます。
1つ入力をしたらプラスボタンを押します。
3つの入力が完了したら、submitボタンが表示されます。
submitボタンを押すことでAIからの返信を受け取ることができます。
ジャーナルは1日1回の投稿としています。
AIからのレスポンス受信機能
入力したジャーナルに対してAIからポジティブなレスポンスを受け取ることができます。
カレンダー機能
ハイライトされた日付を押すとカレンダー下にジャーナルの内容と受け取ったAIからの返信を見ることができます。
大変だったこと
AIからのレスポンスを得るところ
このアプリではGemini APIを利用してレスポンスを画面に表示させています。
普通にAPIを使用してレスポンスを受けとりデータを取り出すだけなのですが、データのある階層が深いのかログを出力しても表示されずどこにあるのか特定するのに何度かトライする必要がありました。
最終的にはresult.data.candidates[0].content
でAIからのレスポンスを得ることができました。
fetched日付のカレンダーテスト
アプリの中にカレンダーをおいてジャーナルを書いた日をハイライトしています。
カレンダー実装詳細は以下に説明をしましたので興味があれば見てみてください。
今回はこの部分のテストをしようとしたら沼ってしまいました。
最初のテストコード
test("ハイライト部分の日付をクリックすると下にジャーナル内容が表示される", async () => {
const mockHighlightedDates = [new Date("2024-08-01")];
const mockJournalContent = ["Journal Entry 1", "Journal Entry 2"];
(fetchHighlightedDates as jest.Mock).mockResolvedValue(
mockHighlightedDates.map((date) => ({ created_at: date.toISOString() }))
);
(handleDateSelect as jest.Mock).mockResolvedValue({
id: 1,
content: mockJournalContent,
});
render(
<MemoryRouter>
<UserProvider>
<CalendarPage />
</UserProvider>
</MemoryRouter>
);
const calendar = await screen.findByTestId("calendar");
expect(calendar).toBeInTheDocument();
const dayElement = screen.getByText(mockHighlightedDates[0].getDate());
fireEvent.click(dayElement);
await waitFor(() => {
+ const journalContainer = screen.queryByTestId("journal-container");
expect(journalContainer).toBeInTheDocument();
});
mockJournalContent.forEach((content) => {
const element = screen.queryByText((_, element) => {
return element
? element.textContent?.includes(content) ?? false
: false;
});
console.log(element);
expect(element).toBeInTheDocument();
});
});
ハイライト部分でエラーが出てしまっています。実際のコード部分は以下のようになっておりうまくtest-idを見つけることができないようでした。
{selectedContent && (
<div
className="mt-4 p-4 bg-gray-100/60 rounded-md"
+ data-testid="journal-container"
>
<h3 className="font-bold">選択された日付のジャーナル:</h3>
<ul className="list-disc pl-3 space-y-1">
{selectedContent.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
)}
このエラーを解消させるために観点を変えてテストコードを書き換えました。
test("カレンダーが表示され、日付選択をするとジャーナル内容と削除ボタンが表示される", async () => {
const mockHighlightedDates = [new Date("2024-08-01")];
const mockJournalContent = ["Journal Entry 1"];
const mockAiResponse = "AI Response";
(supabaseFunction.fetchHighlightedDates as jest.Mock).mockResolvedValue(
mockHighlightedDates.map((date) => ({ created_at: date.toISOString() }))
);
(supabaseFunction.handleDateSelect as jest.Mock).mockResolvedValue({
id: 1,
content: mockJournalContent,
});
(supabaseFunction.getAiResponse as jest.Mock).mockResolvedValue([
{ response: mockAiResponse },
]);
render(<CalendarPage />);
expect(screen.getByTestId("calendar")).toBeInTheDocument();
await waitFor(() => {
+ const highlightedDate = screen.getByText("1");
+ expect(highlightedDate).toHaveStyle(
+ "background-color: rgb(185, 123, 252)"
);
});
+ const dayElement = screen.getByText("1");
fireEvent.click(dayElement);
await waitFor(() => {
+ expect(screen.getByText("Journal Entry 1")).toBeInTheDocument();
+ expect(screen.getByText("AI Response")).toBeInTheDocument();
});
expect(screen.getByText("投稿を削除する")).toBeInTheDocument();
const deleteButton = screen.getByText("投稿を削除する");
fireEvent.click(deleteButton);
await waitFor(() => {
expect(screen.queryByText("Journal Entry 1")).not.toBeInTheDocument();
expect(screen.queryByText("AI Response")).not.toBeInTheDocument();
const deleteButton = screen.queryByText("投稿を削除する");
expect(deleteButton).toHaveClass("hidden");
});
});
このテストコードではコードの内部実装に依存するのではなく外部からアプリを見た時の動作に焦点を当てています。エラーが出ていたidを探しに行くのではなく、代わりに画面上に表示されている要素から確認を進めるようにしました。
反省点
事前にAPIをきちんと設計したほうがよかった
DBのテーブル設計はあまり問題がなかったのですが、データベースとのやり取りを行う関数が各コンポーネントで個別に作成することが多くなり予想以上に数が増えてしまいました。
こういう部分を事前に考えておいてコードの重複を減らし、共通化ができるとより最適化されたアプリが作れるのかなと思いました。
設計についてはインプットもしつつ今後のアプリ作りに活かせるようにしたいです。
UIはまだまだ学ぶことが多い
今回初めてTailwindを使用してUIを作成しました。
今まではChakra UIで細かい調整をすることがあまりなかったのですが、Tailwindだと自由度が高いのとライブラリコンポーネントが多いので細かい部分までこだわることができます。
そのためユーザーが利用しやすいUIにするためにはデザインについても学び実践を重ねる必要があるなと思いました。
総括
個人でゼロからアプリを作りきったのはこれが初めてでした。(今まではJISOUのカリキュラムがあったためアプリのアイデアや仕様をゼロから考えることはありませんでした)
2ヶ月前の6月はアプリをつくり始めることさえできなかったのに、このアプリを2週間くらいで作りきることができるようになったことは、7月から今まで手を動かし続けていたことが少し成長につながったのかなと思います。
特に嬉しかったことは、事前に思い描いていた通りのアプリをある程度作り上げることができたことです。これは私にとって一つの自信となりました。
とはいえ、手を動かせば動かすほど足りていない知識やスキルが見えてきます。
スキルを向上させるには、実践を続けることが最も効果的だと考えています。そのため、今後もアプリ開発に取り組み続ける予定です。
1ヶ月後、再び振り返ったときに、さらに成長を実感できることを目標に頑張ります!
JISOUのメンバー募集中!
プログラミングコーチングJISOUでは、新たなメンバーを募集しています。
実践的なカリキュラムで、あなたのエンジニアとしてのキャリアを最短で飛躍させましょう!
興味のある方は、ぜひホームページからお気軽にカウンセリングをお申し込みください!
▼▼▼