8. Next.js14 학습코스 따라하기 (Chapter 8)

이상범·2024년 10월 7일
0
  • 이전 Chapter에서는, 대시보드 개요 페이지에 대하여 데이터를 fetch해 보았다.
  • 그리고, 현재 설정에는 아래와 같은 2가지 제한 사항이 존재한다고 언급하였다. 오늘은 2번째, 정적 렌더링과 관련된 내용에 대해 알아볼 것이다.
    • Request Waterfalls 패턴 문제
    • 경로를 사전에 렌더링하는 정적 렌더링이 발생하여, 데이터가 변경되어도 대시보드에 반영되지 않는 문제

 

1. What is Static Rendering? (정적 렌더링이란?)

  • 정적 렌더링은 데이터 fetch 및 렌더링이 다음과 같은 상황에서 서버에서 발생하는 것을 의미한다.
    1. 빌드(배포) 시간
    2. revalidation (데이터를 새로고침하거나 다시 로드하여 최신 데이터를 가져오는 프로세스) 발생 시
  • 해당 결과물은 그 후에 콘텐츠 전달 네트워크(CDN)에 분배되고 캐시되어, 성능이 향상될수 있다.
    • (사용자와 자리적으로 가까운 CDN에서 제공될 수 있으므로).
  • 사용자가 페이지 방문 시마다, 캐시된 결과가 제공되는 정적 렌더링에는 몇 가지 장점이 있다.
    1. 더 빠른 웹사이트: 미리 렌더링된 콘텐츠는 캐시되어, 전 세계의 사용자가 웹사이트 콘텐츠에 더 빠르게 및 신뢰성 있게 접근할 수 있다.
    2. 서버 부하 감소: 콘텐츠가 캐시되기 때문에 서버가 각 사용자 요청에 동적으로 콘텐츠를 생성할 필요가 없다.
    3. SEO: 미리 렌더링된 콘텐츠는 검색 엔진 크롤러가 페이지 로드 시 이미 사용 가능한 콘텐츠로 인덱싱하기 쉽다. 이는 검색 엔진 순위를 향상시키는 데 유의미한 기여를 한다.

 

2. What is Dynamic Rendering? (동적 렌더링이란?)

  • 동적 렌더링은 각 사용자의 콘텐츠가 다음과 같은 시점에서, 서버에서 렌더링되는 것을 의미한다.
    • 페이지를 방문할 때와 같은 사용자의 요청이 발생할 때 (정적 렌더링은 빌드타임, revalidation 발생 시)
  • 동적 렌더링의 장점은 아래와 같다.
    • 실시간 데이터(Real-time Data) - 동적 렌더링을 통해, 애플리케이션이 실시간 혹은 자주 업데이트되는 데이터를 표시할 수 있다. 이는 데이터가 자주 변경되는 애플리케이션에 이상적이다.
    • 사용자별 콘텐츠: 동적 렌더링을 사용하면, 대시보드나 사용자 프로필과 같은 개인화된 콘텐츠를 제공하고, 사용자 상호작용에 따라 데이터를 업데이트하는 것이 정적 렌더링보다 더 유리하다.
    • 요청 시간 정보: 동적 렌더링을 통해 요청 시간에만 알 수 있는 쿠키나 URL 검색 매개변수와 같은 정보에 액세스할 수 있다.

 

3. Making the dashboad dynamic (대시보드를 동적으로 만들기)

  • 기본적으로 @vercel/postgres는 자체 캐싱 의미론을 설정하지 않는데, 이는 프레임워크가 자체 정적 및 동적 동작을 설정할 수 있도록 한다.
  • Next.js API인 unstable_noStore를 사용하여, 서버 컴포넌트나 데이터 Fetch 함수에서 정적 렌더링을 사용하지 않도록 선택할 수 있다.
    • /app/lib/data.ts파일 내에서 next/cache 라이브러리의 unstable_noStore() 함수를 import하여, 데이터 Fetch 함수의 맨 위에서 호출하면 된다.
    • unstable_noStore함수를 사용함으로써, 응답이 캐시되지 않도록 하여, 항상 최신 데이터를 가져올 수 있게 된다.
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() {
  // Add noStore() here prevent the response from being cached.
  // This is equivalent to in 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.");
  }
}

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,
      // Convert amount from cents to dollars
      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.");
  }
}

 

4. Simulating a Slow Data Fetch (데이터 느리게 가져오기 시뮬레이션)

  • 대시보드를 동적으로 만드는 것은 좋은 선택이다. 하지만, 지난 챕터에서 해결하지 못한 문제가 남아있다.
    • 다른 모든 데이터 요청보다 하나의 데이터 요청이 느리게 처리된다면?
  • 일단 데이터를 느리게 가져오는 것을 시뮬레이션해 보기 위해, /app/lib/data.ts 파일에서 fetchRevenue() 안의 console.logsetTimeout 주석을 해제해보자.
export async function fetchRevenue() {
  try {
    // Artificially delay a reponse for demo purposes.
    // Don't do this in real life :)
    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초 지연을 추가했기 때문에, 그 결과 데이터를 가져오는 3초의 시간동안 페이지가 차단된 것이다.
  • 이것은 개발자로서 해결해야 할 공통적인 다음의 문제로 이어진다.
    • 페이지를 동적으로 만들었지만, 느린 데이터 Fetch가 결과적으로 애플리케이션 성능(현재 상황에서는 페이지 로드)에 큰 영향을 미쳤다.
    • 데이터 요청이 느린 경우 사용자 경험을 개선할 수 있는 방법이 없을까?
profile
프론트엔드 입문 개발자입니다.

0개의 댓글