4. Next JS API Router와 Vercel Postgres로 TODO

jonyChoiGenius·2023년 12월 13일
1

NEXT JS 13 투두앱

목록 보기
4/4

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? ... @/

API Routes

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만으로도 거의 모든 요청을 처리할 수 있다. 하지만 클라이언트와 백엔드를 분리하는 것이 코드의 유지 보수성 측면에서 이점을 줄 수 있다.

Vercel Postgres

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 에서 배포된 프로젝트를 확인할 수 있다.

새로운 Store를 만들기

https://vercel.com/dashboard/stores

  • 해당 페이지에 접속하여 Postgres를 Create 한다.

  • Store가 생성되면, connect project를 클릭하여 프로젝트를 연결한다.

  • vercel env pull .env.development.local을 실행하여 vercel postgres에 필요한 환경 변수를 받아온다.

DB를 만들고 접근하기

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>
  );
}

Select

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;

Where (Next JS 다이나믹 라우팅)

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;

Insert (Next JS Revalidate Path)

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할 수 있다.

Update

만일 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;
profile
천재가 되어버린 박제를 아시오?

0개의 댓글

관련 채용 정보