이전 Chapter에서 대시보드 개요 페이지에 대한 데이터 fetch를 구현했습니다. 이번에는 정적 렌더링과 관련된 제한 사항을 해결해보겠습니다.
현재 설정에서 존재하는 두 가지 주요 문제점:
오늘은 두 번째 문제인 정적 렌더링에 집중하여 해결 방법을 알아보겠습니다.
정적 렌더링은 데이터 fetch 및 렌더링이 서버에서 미리 수행되는 렌더링 방식입니다.
렌더링 결과물은 콘텐츠 전달 네트워크(CDN)에 분배되고 캐시되어 성능이 향상됩니다.
사용자가 페이지 방문 시마다 캐시된 결과가 제공되는 정적 렌더링의 주요 이점:
동적 렌더링은 각 사용자의 요청 시점에서 서버에서 콘텐츠를 렌더링하는 방식입니다.
💡 정적 vs 동적: 정적 렌더링은 빌드타임/revalidation 시, 동적 렌더링은 요청 시점에 발생
기본적으로 @vercel/postgres
는 자체 캐싱 의미론을 설정하지 않습니다. 이는 프레임워크가 자체 정적 및 동적 동작을 설정할 수 있도록 하기 위함입니다.
Next.js API인 unstable_noStore
를 사용하여 서버 컴포넌트나 데이터 Fetch 함수에서 정적 렌더링을 사용하지 않도록 설정할 수 있습니다.
/app/lib/data.ts
파일에서 next/cache
라이브러리의 unstable_noStore()
함수를 import하여 사용:
import { sql } from "@vercel/postgres";
import {
CustomerField,
CustomersTable,
InvoiceForm,
InvoicesTable,
LatestInvoiceRaw,
User,
Revenue,
} from "./definitions";
import { formatCurrency } from "./utils";
import { unstable_noStore as noStore } from "next/cache";
export async function fetchRevenue() {
// 🔥 응답이 캐시되지 않도록 설정
// fetch(..., {cache: 'no-store'})와 동일한 효과
noStore();
try {
const data = await sql<Revenue>`SELECT * FROM revenue`;
return data.rows;
} catch (error) {
console.error("Database Error:", error);
throw new Error("Failed to fetch revenue data.");
}
}
export async function fetchLatestInvoices() {
noStore();
try {
const data = await sql<LatestInvoiceRaw>`
SELECT invoices.amount, customers.name, customers.image_url, customers.email, invoices.id
FROM invoices
JOIN customers ON invoices.customer_id = customers.id
ORDER BY invoices.date DESC
LIMIT 5`;
const latestInvoices = data.rows.map((invoice) => ({
...invoice,
amount: formatCurrency(invoice.amount),
}));
return latestInvoices;
} catch (error) {
console.error("Database Error:", error);
throw new Error("Failed to fetch the latest invoices.");
}
}
export async function fetchCardData() {
noStore();
try {
const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;
const invoiceStatusPromise = sql`SELECT
SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid",
SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS "pending"
FROM invoices`;
const data = await Promise.all([
invoiceCountPromise,
customerCountPromise,
invoiceStatusPromise,
]);
const numberOfInvoices = Number(data[0].rows[0].count ?? "0");
const numberOfCustomers = Number(data[1].rows[0].count ?? "0");
const totalPaidInvoices = formatCurrency(data[2].rows[0].paid ?? "0");
const totalPendingInvoices = formatCurrency(data[2].rows[0].pending ?? "0");
return {
numberOfCustomers,
numberOfInvoices,
totalPaidInvoices,
totalPendingInvoices,
};
} catch (error) {
console.error("Database Error:", error);
throw new Error("Failed to card data.");
}
}
// 기타 함수들에도 noStore() 적용
const ITEMS_PER_PAGE = 6;
export async function fetchFilteredInvoices(
query: string,
currentPage: number
) {
noStore();
const offset = (currentPage - 1) * ITEMS_PER_PAGE;
try {
const invoices = await sql<InvoicesTable>`
SELECT
invoices.id,
invoices.amount,
invoices.date,
invoices.status,
customers.name,
customers.email,
customers.image_url
FROM invoices
JOIN customers ON invoices.customer_id = customers.id
WHERE
customers.name ILIKE ${`%${query}%`} OR
customers.email ILIKE ${`%${query}%`} OR
invoices.amount::text ILIKE ${`%${query}%`} OR
invoices.date::text ILIKE ${`%${query}%`} OR
invoices.status ILIKE ${`%${query}%`}
ORDER BY invoices.date DESC
LIMIT ${ITEMS_PER_PAGE} OFFSET ${offset}
`;
return invoices.rows;
} catch (error) {
console.error("Database Error:", error);
throw new Error("Failed to fetch invoices.");
}
}
export async function fetchInvoicesPages(query: string) {
noStore();
try {
const count = await sql`SELECT COUNT(*)
FROM invoices
JOIN customers ON invoices.customer_id = customers.id
WHERE
customers.name ILIKE ${`%${query}%`} OR
customers.email ILIKE ${`%${query}%`} OR
invoices.amount::text ILIKE ${`%${query}%`} OR
invoices.date::text ILIKE ${`%${query}%`} OR
invoices.status ILIKE ${`%${query}%`}
`;
const totalPages = Math.ceil(Number(count.rows[0].count) / ITEMS_PER_PAGE);
return totalPages;
} catch (error) {
console.error("Database Error:", error);
throw new Error("Failed to fetch total number of invoices.");
}
}
export async function fetchInvoiceById(id: string) {
noStore();
try {
const data = await sql<InvoiceForm>`
SELECT
invoices.id,
invoices.customer_id,
invoices.amount,
invoices.status
FROM invoices
WHERE invoices.id = ${id};
`;
const invoice = data.rows.map((invoice) => ({
...invoice,
amount: invoice.amount / 100,
}));
return invoice[0];
} catch (error) {
console.error("Database Error:", error);
}
}
export async function fetchCustomers() {
noStore();
try {
const data = await sql<CustomerField>`
SELECT
id,
name
FROM customers
ORDER BY name ASC
`;
const customers = data.rows;
return customers;
} catch (err) {
console.error("Database Error:", err);
throw new Error("Failed to fetch all customers.");
}
}
export async function fetchFilteredCustomers(query: string) {
noStore();
try {
const data = await sql<CustomersTable>`
SELECT
customers.id,
customers.name,
customers.email,
customers.image_url,
COUNT(invoices.id) AS total_invoices,
SUM(CASE WHEN invoices.status = 'pending' THEN invoices.amount ELSE 0 END) AS total_pending,
SUM(CASE WHEN invoices.status = 'paid' THEN invoices.amount ELSE 0 END) AS total_paid
FROM customers
LEFT JOIN invoices ON customers.id = invoices.customer_id
WHERE
customers.name ILIKE ${`%${query}%`} OR
customers.email ILIKE ${`%${query}%`}
GROUP BY customers.id, customers.name, customers.email, customers.image_url
ORDER BY customers.name ASC
`;
const customers = data.rows.map((customer) => ({
...customer,
total_pending: formatCurrency(customer.total_pending),
total_paid: formatCurrency(customer.total_paid),
}));
return customers;
} catch (err) {
console.error("Database Error:", err);
throw new Error("Failed to fetch customer table.");
}
}
export async function getUser(email: string) {
noStore();
try {
const user = await sql`SELECT * from USERS where email=${email}`;
return user.rows[0] as User;
} catch (error) {
console.error("Failed to fetch user:", error);
throw new Error("Failed to fetch user.");
}
}
unstable_noStore()
함수를 사용함으로써:
대시보드를 동적으로 만드는 것은 좋은 선택이지만, 여전히 해결되지 않은 문제가 있습니다:
❓ 핵심 문제: 다른 모든 데이터 요청보다 하나의 데이터 요청이 느리게 처리된다면?
데이터를 느리게 가져오는 것을 시뮬레이션하기 위해 /app/lib/data.ts
파일에서 fetchRevenue()
함수를 수정합니다:
export async function fetchRevenue() {
try {
// 🚨 데모 목적으로 인위적인 지연 추가
// 실제 프로덕션에서는 사용하지 마세요!
console.log("Fetching revenue data...");
await new Promise((resolve) => setTimeout(resolve, 3000));
const data = await sql<Revenue>`SELECT * FROM revenue`;
console.log("Data fetch complete after 3 seconds.");
return data.rows;
} catch (error) {
console.error("Database Error:", error);
throw new Error("Failed to fetch revenue data.");
}
}
코드 변경 후 http://localhost:3000/dashboard
페이지를 새 탭에서 열면:
인위적인 3초 지연을 추가한 결과:
개발자로서 해결해야 할 공통적인 문제들:
이러한 문제들을 해결하기 위한 방법들:
다음 Chapter에서는 이러한 해결책들을 구체적으로 구현해보겠습니다! 🚀