
2025.7.10 목요일의 공부기록
Next.js와 Prisma를 활용하여 데이터베이스의 데이터를 페이징(Pagination) 처리하는 방법을 알아보고, 타입 안정성(Type-safety)을 유지하기 위해 Prisma의 PromiseReturnType과 TypeScript의 ReturnType 유틸리티 타입을 사용하는 방법을 함께 알아보자.
데이터가 많을 때 한 번에 모든 데이터를 불러오지 않고, 일정 수량씩 나누어 가져오는 것을 Pagination이라고 한다. Prisma는 이러한 페이징 처리를 간단하게 구현할 수 있도록 편리한 옵션들을 제공한다.
Pagination을 위해 Prisma에서 제공하는 주요 옵션은 아래와 같다:
skip: 결과를 가져오기 전에 건너뛸 데이터의 개수를 지정한다.take: 가져올 데이터의 개수를 제한한다.orderBy: 데이터를 특정 조건으로 정렬한다.const products = await db.product.findMany({
select: {
title: true,
price: true,
created_at: true,
photo: true,
id: true,
},
skip: 10, // 앞의 10개를 생략
take: 10, // 10개씩 가져옴
orderBy: {
created_at: "asc", // 오름차순 정렬 (최근순: "desc")
},
});
ReturnType과 Prisma의 타입 유틸리티 활용ReturnTypeTypeScript의 유틸리티 타입 중 하나인 ReturnType<Type>은 특정 함수의 반환 타입을 쉽게 추론할 수 있도록 도와준다.
type T0 = ReturnType<() => string>; // string
type T1 = ReturnType<(s: string) => void>; // void
PromiseReturnTypePrisma는 비동기 함수의 반환 타입을 추론할 수 있는 유틸리티인 PromiseReturnType을 제공한다. 이를 통해 데이터 조회 함수의 반환값에 대한 타입을 간편하게 지정할 수 있다.
actions.ts// app/(tabs)/products/actions.ts
"use server";
import db from "@/lib/db";
export async function getMoreProducts(page: number) {
const products = await db.product.findMany({
select: {
title: true,
price: true,
created_at: true,
photo: true,
id: true,
},
skip: page, // 페이지 번호에 따라 달라질 수 있음
take: 10, // 10개씩 가져오기
orderBy: {
created_at: "asc",
},
});
return products;
}
page.tsx타입 안정성을 위해 PromiseReturnType을 사용하여 초기 데이터 타입을 정의한다.
// app/(tabs)/products/page.tsx
import ProductsList from "@/components/product-list";
import db from "@/lib/db";
import { Prisma } from "@prisma/client";
async function getInitialProducts() {
const products = await db.product.findMany({
select: {
title: true,
price: true,
created_at: true,
photo: true,
id: true,
},
take: 10,
orderBy: {
created_at: "asc",
},
});
return products;
}
// Prisma의 유틸리티 타입으로 반환 타입 추론
export type InitialProducts = Prisma.PromiseReturnType<typeof getInitialProducts>;
export default async function Products() {
const initialProducts = await getInitialProducts();
return (
<div>
<ProductsList initialProducts={initialProducts} />
</div>
);
}
product-list.tsxuseState로 제품 데이터를 관리하고, 서버 액션을 통해 데이터를 추가적으로 가져온다.
// components/product-list.tsx
"use client";
import { InitialProducts } from "@/app/(tabs)/products/page";
import ListProducts from "./list-product";
import { useState } from "react";
import { getMoreProducts } from "@/app/(tabs)/products/actions";
interface ProductsListProps {
initialProducts: InitialProducts;
}
export default function ProductsList({ initialProducts }: ProductsListProps) {
const [products, setProducts] = useState(initialProducts);
const [isLoading, setIsLoading] = useState(false);
const [page, setPage] = useState(10); // 페이지 상태 추가 (skip 기준)
const onLoadMoreClick = async () => {
setIsLoading(true);
const newProducts = await getMoreProducts(page);
setProducts((prev) => [...prev, ...newProducts]);
setPage((prevPage) => prevPage + 10); // 다음 페이지로 이동
setIsLoading(false);
};
return (
<div className="flex flex-col gap-5 p-5">
{products.map((product) => (
<ListProducts key={product.id} {...product} />
))}
<button
disabled={isLoading}
className="text-sm font-semibold bg-orange-500 w-fit mx-auto px-3 py-2 rounded-md hover:opacity-90 active:scale-95"
onClick={onLoadMoreClick}
>
{isLoading ? "로딩중..." : "더보기"}
</button>
</div>
);
}