概要
既製のAIチャットアプリについて、利用言語をPython(Streamlit)からNext.jsに置き換えようとした際に起こった事象についてまとめたもの
(現在も着手中のため、適宜追加します)
前提
Next.js(というよりフロントエンド全般)の勉強歴が2週間+AIにガンガン質問して実装をする形式のため、かなり初歩的な内容になっています
言いたいこと
作りこみ方法はAppルータ形式とPagesルータ形式の2種類があるが、そのうちの前者はまだ発展途上
GPTのバージョンが古いとしれっと後者の実装を回答してくる(しかも正しいかのように)ので、騙されないようにしよう!
前者についてChat-GPTに聞くならGPT-4oが良い
Web検索で見つかるサイトも今のところ(2024/5/17時点)は後者が多い
本文
1.npx実行時にフォルダなしエラー
事象
コマンドプロンプトでnpxを実行しプロジェクトを立ち上げようとした際に発生。「指定した場所にnpmフォルダが見つからない」エラーになる
対策
エラーで指定された場所にnpmという名前のフォルダを配置する(空フォルダでよい)
2.プログラム実行時にmoduleが見つからずエラー
事象
npm run devで作成したプログラムを実行した際に発生
ガイド通りに進めたがlocalhostアクセス時に「moduleが見つからない」エラーが発生する
対策
「#」がパスに含まれていると発生
今回のケースだとデスクトップ(C:\Users\#User0123\Desktop\Project)に配置したため
実行するプロジェクトの配置パスを「#」を含まない場所に配置すればよい
3.runした際に設定したファビコンが出ない
事象
Appルータ形式で作りこみをしたプログラムを実行したとき、appフォルダに配置したファビコンが出てこなかった
対策
Appフォルダ配下のみにおかないと反応しない
調査作業の時に「publicフォルダに配置することで出力される」との内容があったため取り込んだが、それはPagesルータの対策法だった
npxで作ったばかりのプログラムなら正しく動くので余計なことしなければOK
(PagesルータとAppルータという2種類の方法があり、対処法がそれぞれ違うので混同しないように)
4.「暗黙的にanyになります」となってコンパイルエラー
事象
TypeScriptを利用する場合において、JavaScriptのサンプルプログラムを持ってきた場合に、上記メッセージが出てコンパイルが通らない
対策
TypeScriptは型宣言が必要になるため、適切な型を宣言するか、anyとして宣言すればよい(後者の場合は型チェックが出来るというTypeScriptとしての強みがなくなるが、機能実行を優先する場合は検討する)
5.ChatGPT出力結果のストリーム表示をしたい
事象
ChatGPTを利用するためのAPIについて、デフォルトの出力結果はまとめて出力である
これをリアルタイムで出力するようにしたい
対策
API部品の呼び出しをする際に出力部分のstream設定をtrueとすればリアルタイムで情報が取得される
ただし受け取った情報は細切れになるので出力側が制御する必要あり
6.MarkdownをHTMLに変換する際にエラー
事象
ChatGPTからの返却がMarkdown形式で返ってくるため、これをHTMLで描写するためにmarkedパッケージのimportを実施した
プログラム実行時に、変換(parse)メソッドを使おうとした際にundefinedが出て読み取れないエラーが出る
対策
import文を以下の通り修正する
修正前:import marked from 'marked';
修正後:import {marked} from 'marked';
7.DB取得用のAPIを実行しようとすると404エラーが発生する
事象
Appルータ形式でAPI実行する内容を作成したが
実行されなかった。内容を追う限りだと404エラーが発生しているようだった
呼び出し元 app/page.tsx
// DBアクセス処理を追加
// resにはDBから取得したデータが入る
// 以下はサーバーから返されたデータをコンソールに表示する例(idを引数として渡す)
const response = await fetch('/api/query/DBAccess?id='+inputValue)
const data = await response.json();
呼び出し先 app/api/query/DBAccess.ts
// サンプルのためHello,worldを詰めて返却するだけになっている
export async function GET(request: any) {
return new Response(JSON.stringify({ message: 'Hello, world!' }), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
});
}
ChatGPTにも確認したが、フォルダ体系には問題がないような回答がなされていた
対策
Appルータ形式はAPIフォルダにおけるファイル名が決まっている
「route.js」または「route.ts」にしなければならなかった
呼び出し先を「route.ts」に修正したうえで
呼び出し元のパス指定を以下の通り修正すればよい。
// DBアクセス処理を追加
// resにはDBから取得したデータが入る
// 以下はサーバーから返されたデータをコンソールに表示する例(idを引数として渡す)
const response = await fetch('/api/query?id='+inputValue)
const data = await response.json();
8.DB取得用のAPIを実行すると結果をreturnする際にエラーになる【5/17 追加】
事象
SQLを発行してローカルのMySQLと連携するAPIを作成した際に発生。
「TypeError: res.status is not a function」というエラー文言が出力される。
'use server';
import { NextApiRequest, NextApiResponse } from 'next';
import mysql from 'mysql2/promise';
export async function GET(req: NextApiRequest ,res: NextApiResponse) {
//中略
const [results] = await connection.query('SELECT * FROM sample');
if (!results) {
console.log('No data found');
return res.status(404).json({ message: 'No data found' });
} else {
console.log('Fetched data:', results);
return res.status(200).json(results);
}
//中略
}
対策
こちらもルータ形式によって実装が異なるために発生した事象。
Next.jsのappディレクトリを使用している場合、APIルートのハンドラはNextApiRequestやNextApiResponseを直接受け取るのではなく、RequestとResponseオブジェクトを使用している。
Appルータ形式は「NextRequest(Response)」による利用を想定されているため、「NextAPIRequest(Response)」を使わずに実装する。
逆にPagesルータ形式の場合については「NextApiRequest(Response)」を利用して実装する。
'use server';
import { NextRequest, NextResponse } from 'next/server';
import mysql from 'mysql2/promise';
export async function GET(req: NextRequest) {
//中略
const [results] = await connection.query('SELECT * FROM sample');
if (!results) {
console.log('No data found');
return new NextResponse(JSON.stringify({ message: 'No data found '}),{status:404});
} else {
console.log('Fetched data:', results);
return new NextResponse(JSON.stringify(results),{status:200});
}
//中略
}
9.DBレコード取得後の件数判定でエラーになる【5/20 追加】
事象
SQLを発行してレコード取得を行い、件数判定を行う処理で発生。以下のようなエラーが発生する。
プロパティ 'length' は型 'QueryResult' に存在しません。
プロパティ 'length' は型 'OkPacket' に存在しません。
//中略
// sample_dbデータベースのsampleテーブルからIDが一致する要素を取得する
const [results] = await connection.query('SELECT * FROM sample WHERE userid = ?', userid);
// resultsが空の場合はエラーを返す
if (results.length === 0) {
console.log('No data found');
return new NextResponse(JSON.stringify({ message: 'No data found '}),{status:404});
} else {
console.log('Fetched data:', results.length);
return new NextResponse(JSON.stringify(results),{status:200});
}
//中略
対策
型推論のエラー。
javaとは異なり型宣言をしていないため、resultsの型が何かを読み取れる範囲で読み取る形(型推論)となっている。
現時点の条件ではlengthメソッドを使える型であるかが確定できない。
以下の通り、配列であるかどうかのチェックを追加することで抑止が可能。
//中略
// sample_dbデータベースのsampleテーブルからIDが一致する要素を取得する
const [results] = await connection.query('SELECT * FROM sample WHERE userid = ?', userid);
if (!Array.isArray(results) || results.length === 0){
console.log('No data found');
return new NextResponse(JSON.stringify({ message: 'No data found '}),{status:404});
} else {
console.log('Fetched data:', results.length);
return new NextResponse(JSON.stringify(results),{status:200});
}
//中略
10.一度設定したパラメータが意図せずに更新される【5/20 追加】
事象
ワンタイムパスワードとして6桁のランダムな数値を生成してメール送信し、ユーザーの入力値と比較させる処理を実装しようとしていた。
しかし、実際に検証してみたところ、alertとして出力された時点のotpの値と、入力値との比較で用いられるotpの値がずれていてログインできなかった。
import React,{ useState } from 'react';
const [otp, setOTP] = useState('');
// ワンタイムパスワードの生成処理
const handleGenerateOTP = () => {
// ここでサーバーにIDとPWを送信し、ワンタイムパスワードを取得する処理を行う
// ランダムな6桁の数字(100000から999999までのランダムな数字)を生成している
const randomOTP = Math.floor(100000 + Math.random() * 900000).toString();
// 以下はサーバーから返されたワンタイムパスワードをセットする例
setOTP(randomOTP); // 仮のワンタイムパスワード
alert('送信します:'+sampleData[0].EMAIL+'に'+otp+'を送信します。')
sendmail(sampleData[0].EMAIL , otp);
};
const handleLogin = () => {
// パスワード入力後の判定処理
// TODO:ワンタイムパスワードとの比較処理にする
if (inputValue === otp) {
// ログイン処理
}
};
対策
useStateの編集仕様によるもの。
useState(ここではsetOTP)が非同期的に状態を更新するため、otpが更新される前にalertやsendmailが動く。これにより古い値が使われることになる。
(inputValueと比較する時点ではotpはrandomOTPの値が格納されているたえ不一致になる)
解決策としては、randomOTPを直接使ってメール送信の処理を行う。
const [otp, setOTP] = useState('');
// ワンタイムパスワードの生成処理
const handleGenerateOTP = () => {
// ランダムな6桁の数字(100000から999999までのランダムな数字)を生成している
const randomOTP = Math.floor(100000 + Math.random() * 900000).toString();
// サーバーから返されたワンタイムパスワードをセットする
setOTP(randomOTP); // 仮のワンタイムパスワード
alert('送信します:'+sampleData[0].EMAIL+'に'+randomOTP+'を送信します。')
sendmail(sampleData[0].EMAIL , randomOTP);
};
const handleLogin = () => {
// パスワード入力後の判定処理
// TODO:ワンタイムパスワードとの比較処理にする
if (inputValue === otp) {
// ログイン処理
}
};
この例に関わらずuseStateの反映に時間差があるため、値を入れたはずなのに反映されていない事例が続出する。
確実に制御順を整えたい場合はuseEffectを使ったトリガー処理の作り込みが必要。
11.NextAuthのパス誤りでエラー【5/22 追加】
事象
セッション管理のためにNextAuthを利用しようとした際に発生。
以下のようなエラーメッセージが発生した。
「Appルータ形式だからかな?」と思ってパス回りを試行錯誤するも改善しない。
[next-auth][error][MISSING_NEXTAUTH_API_ROUTE_ERROR]
https://next-auth.js.org/errors#missing_nextauth_api_route_error Cannot find [...nextauth].{js,ts} in `/pages/api/auth`.
Make sure the filename is written correctly.
MissingAPIRoute [MissingAPIRouteError]:
Cannot find [...nextauth].{js,ts} in `/pages/api/auth`
対策
フォルダ名の指定誤り。app/api/auth配下のフォルダ名を以下の通りすべきだった。
誤:[…nextauth]
正:[...nextauth]
外見上だけだと全く違いが判らないが、誤は「…」1文字で、正は「.」3文字である。
また、nextauth以外の名前も許容していない(nextAuth、NextAuthなどだとダメ)。
12.画像生成時にURL情報が取得できずにエラー【5/29 追加】
事象
入力した文字列から画像生成するためにopenai.createImageを利用した際に発生。
responseから生成された画像が配置されたURL情報を取得しようとしたが、
AIが提案した方式のままだと「dataはない」とコンパイルエラーになってしまう。
response.urlという項目があったので使ってみたが、画像は出力されなかった。(ログを見る限り生成はされているように見えた)
busboy.on('finish', async () => {
console.log('Busboy finished processing');
try {
let imageGeneratedUrl = '';
// 画像生成のための API 呼び出し
if (text.includes('generate an image')) {
console.log('Generating image');
const response = await openai.createImage({
prompt: text,
n: 1,
size: '1024x1024',
});
imageGeneratedUrl = response.data[0].url; //ここでエラーになる
}
resolve(NextResponse.json({ text, files, imageGeneratedUrl }));
} catch (error) {
console.error('Error:', error);
reject(new NextResponse('Error generating image', { status: 500 }));
}
});
対策
responseのjson部を一度定義してから(値が完全に設定されることを担保するためにawaitを付与)、そのjsonの中身を抜き出すようにする。
busboy.on('finish', async () => {
console.log('Busboy finished processing');
try {
let imageGeneratedUrl = '';
// 画像生成のための API 呼び出し
if (text.includes('generate an image')) {
console.log('Generating image');
const response = await openai.createImage({
prompt: text,
n: 1,
size: '1024x1024',
});
const responseData = await response.json(); // 追加
imageGeneratedUrl = responseData.data[0].url; // コンパイルOK
}
resolve(NextResponse.json({ text, files, imageGeneratedUrl }));
} catch (error) {
console.error('Error:', error);
reject(new NextResponse('Error generating image', { status: 500 }));
}
});
13.ビルド時に「The punycode
module is deprecated.~」が出る【6/4 追加】
事象
ビルド時に以下のようなワーニングが出力される。
資材が増えるほど大量に発生する。実行には影響を及ぼしていない。
[DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
対策
nodeのバージョンによる不備?
バージョンが21以上になっているために発生している。
20以下にすることで回避できるようだが、動作に影響がないため対応は見送った。
14.EC2上で実行した際にプロセスが終了してしまう【6/14 追加】
事象
EC2上で作成したプログラムを実行させたところ、最初のうちはきちんと想定した動作が行われるが、途中で画面表示もされなくなった。
この場合、EC2から再度実行をかける必要がある。
対策
エラーハンドリングが十分に出来ていないと、アプリがクラッシュしてプロセスが終了してしまう。
また、いったん終了したプロセスを再起動する仕組みもない。
これらの問題に対応するため、以下の対応を実施
- エラーハンドリング:発生しうる例外についてキャッチを行い、安全側に実行する
- プロセス管理:管理用のアプリ「PM2」を使用して、アプリケーションがクラッシュした際に自動的に再起動するように設定する
15.ログ取得画面を表示した際、最新のログ情報が取得できない【6/19 追加】
事象
実行ログを取得するページを作成した際に発生した事象。
リアルタイムに発生したログが取得できる想定であったが、デプロイ実行以降に出力されたログが取得できなかった。
'use client';
import { useEffect, useState } from 'react';
const LogsPage = () => {
const [logs, setLogs] = useState<string[]>([]);
// 拡張子が.logのファイルの一覧を取得
useEffect(() => {
fetch('/api/logs')
.then((response) => response.json())
.then((data) => data.filter((filename: string) => filename.endsWith('.log')))
.then((data) => setLogs(data))
.catch((error) => console.error('Error fetching logs:', error));
}, []);
const downloadLog = (filename: string) => {
const link = document.createElement('a');
link.href = `/api/logs/${filename}`;
link.download = filename;
link.click();
};
return (
<div>
<h1>Logs</h1>
<ul>
{logs.map((log) => (
<li key={log}>
{log} <button onClick={() => downloadLog(log)}>Download</button>
</li>
))}
</ul>
</div>
);
};
export default LogsPage;
対策
ビルド実行時の画面を確認したところ、当画面がstatic contentになっていた。
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand
この場合、ビルド時点で事前に画面を構築するため、以降の情報が取得されない状態になっていた。
unstable_noStoreを利用することで、動的に読み込みを行うようになった。
'use client';
import { useEffect, useState } from 'react';
import { unstable_noStore as noStore } from 'next/cache'; // 追加
const LogsPage = () => {
noStore(); // 追加
const [logs, setLogs] = useState<string[]>([]);
// 拡張子が.logのファイルの一覧を取得
useEffect(() => {
fetch('/api/logs')
.then((response) => response.json())
.then((data) => data.filter((filename: string) => filename.endsWith('.log')))
.then((data) => setLogs(data))
.catch((error) => console.error('Error fetching logs:', error));
}, []);
const downloadLog = (filename: string) => {
const link = document.createElement('a');
link.href = `/api/logs/${filename}`;
link.download = filename;
link.click();
};
return (
<div>
<h1>Logs</h1>
<ul>
{logs.map((log) => (
<li key={log}>
{log} <button onClick={() => downloadLog(log)}>Download</button>
</li>
))}
</ul>
</div>
);
};
export default LogsPage;
16.画面内で生成されたファイルが取得できない【6/20 追加】
事象
ボタンを押下時にAPIを実行、サーバー内に格納した結果ファイルを画面上に出力して、それをダウンロードさせる処理を実装した際に発生。
サーバー内に結果ファイルは作成されたが、参照・ダウンロードすることが出来ない。
本番環境だけで発生した事象。
対策
GETとPOSTの違いがはっきり分かっていなかったがために発生した事象。
動的に生成された情報はGETメソッドで取得をする。(APIでの生成がPOSTだったのと経験不足のために苦戦した)
なぜローカルでは取得出来たのか……
17.セッション情報に「ユーザー権限」を持たせようとしてエラー【6/20 追加】
事象
next-authを使ったセッション管理をしようとした際に発生。
セッション情報に「ユーザー権限」を持たせて持ち回ろうとした際に、
「'admin' は型 'User | AdapterUser' に存在しません。」とエラーになってしまう。
import NextAuth, { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
id: { label: 'ID', type: 'text' },
admin: { label: 'Admin', type: 'text' },
},
authorize: async (credentials) => {
if (!credentials) {
return null;
}
// 認証を別にしているため、セッションの管理のみを行う
// 認証済みのユーザー情報を返す
return { id: credentials.id, admin: credentials.admin };
},
}),
],
session: {
strategy: 'jwt',
maxAge: 60 * 60,
updateAge: 15 * 60,
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
token.admin = user.admin; // ※ここでエラーになる
}
async session({ session, token }) {
session.user = {
...session.user,
id: token.id as string | undefined,
admin: token.admin as string | undefined,
};
return session;
},
},
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
対策
TypeScriptの型を拡張して、adminプロパティを含む User 型を定義すればよい。
import NextAuth, { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
declare module 'next-auth' {
interface User {
level?: string;
}
}
const authOptions: NextAuthOptions = {
//(以降は上記と同様)