Next JS App Router로 TODO와 같은 방식으로 next-js-postgres-todo
폴더에서 아래와 같이 입력한다.
$ yarn create next-app .
success Installed "create-next-app@14.0.4" with binaries:
- create-next-app
√ Would you like to use TypeScript? ... No / Yes
√ Would you like to use ESLint? ... No / Yes
√ Would you like to use Tailwind CSS? ... No / Yes
√ Would you like to use src/
directory? ... No / Yes
√ Would you like to use App Router? (recommended) ... No / Yes
√ Would you like to customize the default import alias (@/)? ... No / Yes
√ What import alias would you like configured? ... @/
https://nextjs.org/docs/app/building-your-application/routing/route-handlers
App Router 기반에서 Next JS의 API Router는 "route-handlers"라 부른다.
기존 Pages Router의 API Routes공식문서와는 개념적으로 다소 다른데, route handlers는 fetch api의 response와 request api를 사용하여 응답을 주고 받는다.
기본적으로 app/api 폴더 안에서 route.ts 파일을 통해 route handlers를 만든다.
app\api\user\route.ts
import { NextResponse } from "next/server";
export async function GET(request: Request) {
try {
const result = { username: "홍길동", email: "gildong@naver.com" };
return NextResponse.json({ result }, { status: 200 });
} catch (error) {
return NextResponse.json({ error }, { status: 500 });
}
}
해당 route handler를 아래와 같이 호출해 사용하게 된다.
export default async function Home() {
const res = await fetch("http://localhost:3000/api/user");
const data = await res.json();
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
{JSON.stringify(data)}
</div>
</main>
);
}
next js의 App Router는 그 기본 패러다임이 매우 서버 주도적이기에 route handler 없이 server action만으로도 거의 모든 요청을 처리할 수 있다. 하지만 클라이언트와 백엔드를 분리하는 것이 코드의 유지 보수성 측면에서 이점을 줄 수 있다.
https://vercel.com/docs/storage/vercel-postgres/quickstart
yarn add @vercel/postgres
vercel postgres를 사용하기 위한 모듈이다.
npm install -g vercel
vercel로부터 정보를 주고 받아 추가하지 위한 vercel cli이다.
vercel link
를 실행하여 프로젝트를 배포할 수 있다.
https://vercel.com/dashboard 에서 배포된 프로젝트를 확인할 수 있다.
https://vercel.com/dashboard/stores
Store가 생성되면, connect project를 클릭하여 프로젝트를 연결한다.
vercel env pull .env.development.local
을 실행하여 vercel postgres에 필요한 환경 변수를 받아온다.
https://github.com/vercel/examples/blob/main/storage/postgres-starter/components/table.tsx
위의 vercel 예제를 참조하여 DB를 만드는 코드를 아래와 같이 추가하였다.
lib\seed.ts
import { sql } from "@vercel/postgres";
export async function seed() {
const createTable = await sql`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
`;
console.log(`Created "users" table`);
const users = await Promise.all([
sql`
INSERT INTO users (username, email)
VALUES ('Guillermo Rauch', 'rauchg@vercel.com')
ON CONFLICT (email) DO NOTHING;
`,
]);
console.log(`Seeded ${users.length} users`);
return {
createTable,
users,
};
}
이렇게 만든 createTable 문은 아래와 같이 실행이 가능하다.
app/page.tsx
import { seed } from "@/lib/seed";
import { sql } from "@vercel/postgres";
export default async function Home() {
let data;
try {
data = await sql`SELECT * FROM users`;
} catch (e: any) {
if (e.message.includes('relation "users" does not exist')) {
await seed();
data = await sql`SELECT * FROM users`;
} else {
throw e;
}
}
const { rows: users } = data;
return (
<div>
{users.map((user) => (
<div key={user.id}>{user.username}</div>
))}
</div>
);
}
app\user\page.tsx
import { sql } from "@vercel/postgres";
const UserListPage = async () => {
const data = await sql`SELECT * FROM users ORDER BY id;`;
const { rows } = data;
return <div>{JSON.stringify(rows)}</div>;
};
export default UserListPage;
https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes
next js에서는 [param명]
으로 params를 받을 수 있으며,
하위 컴포넌트에서는 {params}
객체로 해당 params를 받을 수 있다.
가령 app\blog\[id]\[slug]
인 경우, params 객체는 { id: string, slug: string }
이 된다.
app\user[id]\page.tsx
import { sql } from "@vercel/postgres";
const UserPage = async ({ params }: { params: { id: string } }) => {
const data = await sql`SELECT * FROM users WHERE id=${params.id}`;
const { rows } = data;
return <div>{JSON.stringify(rows)}</div>;
};
export default UserPage;
https://nextjs.org/docs/app/api-reference/functions/revalidatePath
서버 액션을 활용하여 아래와 같이 코드를 작성하였다.
app\user\create\page.tsx
import { sql } from "@vercel/postgres";
import { revalidatePath } from "next/cache";
const insertUser = async (formData: FormData) => {
"use server";
try {
await sql`INSERT INTO users (username, email)
VALUES (${String(formData.get("username"))}, ${String(
formData.get("email")
)})
ON CONFLICT (email) DO NOTHING;`;
revalidatePath("/user");
} catch (e) {
console.log(e);
}
};
const UserCreatingPage = () => {
return (
<form action={insertUser}>
<input name="username" placeholder="이름을 입력하세요" />
<input name="email" type="email" placeholder="이메일을 입력하세요" />
<button type="submit">전송하기</button>
</form>
);
};
export default UserCreatingPage;
이때 revalidatePath를 통해 특정 경로의 데이터를 revalidate하여 SQL로 받아온 데이터를 revalidate할 수 있다.
만일 revalidatePath의 두번째 파라미터로 문자열 "layout"을 주면 해당 페이지의 layout을 공유하는 모든 페이지의 데이터가 revalidate 된다.
이때 겉으로 보여지지 않는 값을 formData에 추가하기 위해 <input type="hidden" />
을 사용할 수 있다.
또한 리액트의 input 태그에서 value와 defaultValue는 다르다. 기본값을 추가해주기 위해서는 defaultValue를 사용한다.
app\user[id]\update\page.tsx
import { sql } from "@vercel/postgres";
import { revalidatePath } from "next/cache";
const updateUser = async (formData: FormData) => {
"use server";
try {
await sql`UPDATE users
SET username = ${String(formData.get("username"))}, email = ${String(
formData.get("email")
)}
WHERE id = ${String(formData.get("id"))};`;
revalidatePath(`/`, "layout");
} catch (e) {
console.log(e);
}
};
const UserUpdatingPage = async ({ params }: { params: { id: string } }) => {
const data = await sql`SELECT * FROM users WHERE id=${params.id}`;
const { rows } = data;
const user = rows[0];
return (
<form action={updateUser}>
<input type="hidden" name="id" defaultValue={params.id} />
<input
name="username"
defaultValue={user.username}
placeholder="이름을 입력하세요"
/>
<input
name="email"
type="email"
defaultValue={user.email}
placeholder="이메일을 입력하세요"
/>
<button type="submit">전송하기</button>
</form>
);
};
export default UserUpdatingPage;