프로젝트 : 페이지 사전 렌더링 & 데이터 페칭

·2024년 3월 29일
0

NextJS

목록 보기
10/26
post-thumbnail

🔗 레파지토리에서 보기

📌 스스로 작업해보기

📖 fetch를 이용하여 firebase로부터 데이터 가져오기

export async function getData() {
  const response = await fetch(
    "https://nextjs-course-demo-846e7-default-rtdb.firebaseio.com/events.json"
  );
  if (!response.ok) {
    console.log("fetch error");
  }

  const resData = await response.json();
  const transformedDatas = [];
  for (const key in resData) {
    transformedDatas.push({
      id: key,
      date: resData[key].date,
      description: resData[key].description,
      image: resData[key].image,
      isFeatured: resData[key].isFeatured,
      location: resData[key].location,
      title: resData[key].title,
    });
  }
  return transformedDatas;
}

📖 HomePage 페이지 사전 렌더링

// pages/index.js
import { getData } from "../dummy-data";
import EventList from "../components/events/event-list";

function HomePage({ featuredEvents }) {
  return (
    <div>
      <EventList items={featuredEvents} />
    </div>
  );
}

export async function getStaticProps() {
  const events = await getData();
  const featuredEvents = events.filter((event) => event.isFeatured);

  if (!featuredEvents) {
    return { notFound: true };
  }

  return {
    props: {
      featuredEvents: featuredEvents,
    },
    revalidate: 10,
  };
}

export default HomePage;

📖 AllEventsPage 페이지 사전 렌더링

// pages/events/index.js
import { Fragment } from "react";
import { useRouter } from "next/router";

import { getData } from "../../dummy-data";
import EventList from "../../components/events/event-list";
import EventsSearch from "../../components/events/events-search";

function AllEventsPage({ events }) {
  const router = useRouter();

  function findEventsHandler(year, month) {
    const fullPath = `/events/${year}/${month}`;

    router.push(fullPath);
  }

  return (
    <Fragment>
      <EventsSearch onSearch={findEventsHandler} />
      <EventList items={events} />
    </Fragment>
  );
}

export async function getStaticProps() {
  const events = await getData();

  if (!events) {
    return { notFound: true };
  }

  return {
    props: {
      events: events,
    },
  };
}

export default AllEventsPage;

📖 EventDetailPage 페이지 사전 렌더링

// pages/events/[eventId].js
import { Fragment } from "react";

import { getData } from "../../dummy-data";
import EventSummary from "../../components/event-detail/event-summary";
import EventLogistics from "../../components/event-detail/event-logistics";
import EventContent from "../../components/event-detail/event-content";
import ErrorAlert from "../../components/ui/error-alert";

function EventDetailPage({ event }) {
  if (!event) {
    return (
      <ErrorAlert>
        <p>No event found!</p>
      </ErrorAlert>
    );
  }

  return (
    <Fragment>
      <EventSummary title={event.title} />
      <EventLogistics
        date={event.date}
        address={event.location}
        image={event.image}
        imageAlt={event.title}
      />
      <EventContent>
        <p>{event.description}</p>
      </EventContent>
    </Fragment>
  );
}

export async function getStaticProps(context) {
  const { params } = context;
  const eventId = params.eventId;

  const events = await getData();
  const event = events.find((event) => event.id === eventId);

  if (!event) {
    return {
      props: {
        event: null,
      },
    };
  }

  return {
    props: {
      event,
    },
  };
}

export async function getStaticPaths() {
  const events = await getData();
  const ids = events.map((event) => event.id);
  //[{params: {eventId: 'id'}}]
  const pathWithParams = ids.map((id) => ({ params: { eventId: id } }));
  return {
    paths: pathWithParams,
    fallback: true,
  };
}
export default EventDetailPage;

📖 FilteredEventPage 사전 렌더링

  • 해당 페이지는 그동안 사용했던 방법인 getStaticProps, getStaticPaths와는 다른 방법인 getServerSideProps를 사용하였다.
// pages/events/[...slug].js
import { Fragment } from "react";

import { getData } from "../../dummy-data";
import EventList from "../../components/events/event-list";
import ResultsTitle from "../../components/events/results-title";
import Button from "../../components/ui/button";
import ErrorAlert from "../../components/ui/error-alert";

function FilteredEventsPage({ filteredEvents, filteredYear, filteredMonth }) {
  console.log(filteredEvents, filteredYear, filteredMonth);
  const date = new Date(filteredYear, filteredMonth - 1);

  if (
    isNaN(filteredYear) ||
    isNaN(filteredMonth) ||
    filteredYear > 2030 ||
    filteredYear < 2021 ||
    filteredMonth < 1 ||
    filteredMonth > 12
  ) {
    return (
      <Fragment>
        <ErrorAlert>
          <p>Invalid filter. Please adjust your values!</p>
        </ErrorAlert>
        <div className="center">
          <Button link="/events">Show All Events</Button>
        </div>
      </Fragment>
    );
  }

  if (!filteredEvents || filteredEvents.length === 0) {
    return (
      <Fragment>
        <ErrorAlert>
          <p>No events found for the chosen filter!</p>
        </ErrorAlert>
        <div className="center">
          <Button link="/events">Show All Events</Button>
        </div>
      </Fragment>
    );
  }

  return (
    <Fragment>
      <ResultsTitle date={date} />
      <EventList items={filteredEvents} />
    </Fragment>
  );
}

export async function getServerSideProps(context) {
  const { params } = context;
  const eventSlug = params.slug;

  const events = await getData();

  const filteredYear = +eventSlug[0];
  const filteredMonth = +eventSlug[1];

  const filteredEvents = events.filter((event) => {
    const eventDate = new Date(event.date);
    return (
      eventDate.getFullYear() === filteredYear &&
      eventDate.getMonth() === filteredMonth - 1
    );
  });

  if (!filteredEvents || filteredEvents.length === 0) {
    return {
      props: {
        filteredEvents: null,
        filteredYear,
        filteredMonth,
      },
    };
  }

  return {
    props: {
      filteredEvents,
      filteredYear,
      filteredMonth,
    },
  };
}

export default FilteredEventsPage;

📌 프로젝트 강의

📖 홈페이지에 정적 사이트 생성(SSG) 추가하기

  • 시작 페이지는 FeaturedEvents를 표시할 것.
  • 시작 페이지는 검색 엔진 크롤러가 이해해야 하는 페이지이다. 크롤러는 사이트를 이해하고 이곳으로 트래픽을 유도해야한다.
  • 사용자 입장에서는 페이지가 바로 보이길 원하고 또한 이 데이터가 짧은 시간에 여러 번 바뀔 가능성도 없다.
  • 따라서 데이터로 페이지를 사전 렌더링을 하는 것이 좋다.

🔗 Firebase Retrieing Data

// helper/api-util.js
export async function getAllEvents() {
  const response = await fetch(
    "https://nextjs-course-demo-846e7-default-rtdb.firebaseio.com/events.json"
  );
  const data = await response.json();

  const events = [];

  for (const key in data) {
    events.push({
      id: key,
      ...data[key],
    });
  }

  return events;
}

export async function getFeaturedEvents() {
  const allEvents = await getAllEvents();
  return allEvents.filter((event) => event.isFeatured);
}

// pages/index.js
import { getFeaturedEvents } from "../helper/api-util.js";
import EventList from "../components/events/event-list";

function HomePage({ featuredEvents }) {
  return (
    <div>
      <EventList items={featuredEvents} />
    </div>
  );
}

export async function getStaticProps() {
  const featuredEvents = await getFeaturedEvents();

  if (!featuredEvents) {
    return { notFound: true };
  }

  return {
    props: {
      featuredEvents: featuredEvents,
    },
  };
}

export default HomePage;
  • 나는 일일히 data[key]로 백엔드의 데이터를 가져왔는데, ...data[key]를 이용해서 더 쉽게 가져올 수 있는 것을 잊고있었다..!

📖 동적 페이지에 대한 데이터 & 경로 로딩하기

  • 개별 이벤트 페이지는 이벤트에 대한 세부 사항을 갖고 있는 단일 페이지이다.
  • 따라서 해당 페이지는 처음부터 데이터를 가져야 하므로 사전 렌더링이 필요하다.
  • 해당 페이지는 늘 변경되는 사용자 특정 데이터를 필요로하는 페이지가 아니므로 getStaticProps를 사용한다.
// helper/api-util.js
export async function getEventById(id) {
  const allEvents = await getAllEvents();
  return allEvents.find((event) => event.id === id);
}

// pages/events/[eventId].js
import { Fragment } from "react";

import { getAllEvents, getEventById } from "../../helper/api-util";
import EventSummary from "../../components/event-detail/event-summary";
import EventLogistics from "../../components/event-detail/event-logistics";
import EventContent from "../../components/event-detail/event-content";
import ErrorAlert from "../../components/ui/error-alert";

function EventDetailPage({ event }) {
  if (!event) {
    return (
      <ErrorAlert>
        <p>No event found!</p>
      </ErrorAlert>
    );
  }

  return (
    <Fragment>
      <EventSummary title={event.title} />
      <EventLogistics
        date={event.date}
        address={event.location}
        image={event.image}
        imageAlt={event.title}
      />
      <EventContent>
        <p>{event.description}</p>
      </EventContent>
    </Fragment>
  );
}

export async function getStaticProps(context) {
  const eventId = context.params.eventId;

  const event = await getEventById(eventId);

  return {
    props: {
      event,
    },
  };
}

export async function getStaticPaths() {
  const events = await getAllEvents();

  const pathWithParams = events.map((event) => ({
    params: { eventId: event.id },
  }));

  return {
    paths: pathWithParams,
    fallback: false,
  };
}
export default EventDetailPage;

강사는 fallback:false로 존재하지 않는 event는 404 페이지로 리디렉션 되도록 설정했다. 하지만 나는 기존에 <ErrorAlert>를 사용하고 싶어서 fallback:true로 설정했다. 다만, 폴백에 대한 키를 참으로 설정해도 잠깐 <ErrorAlert> 내용이 나올 뿐, 금방 오류가 발생했다. 해당 오류를 해결하기 위해서 아래의 코드를 추가했더니 정상적으로 작동되었다.

// pages/events/[eventId].js
export async function getStaticProps(context) {
  const eventId = context.params.eventId;

  const event = await getEventById(eventId);

  // event가 없을 때 폴백을 해결하기 위해 props.event를 null로 설정
  if (!event) {
    return {
      props: {
        event: null,
      },
    };
  }

  return {
    props: {
      event,
    },
  };
}

📖 데이터 페칭 최적화하기

  • revalidate를 설정함으로써 혹여나 데이터가 변경됐을 때 적용할 수 있도록 함.
// pages/index.js
import { getFeaturedEvents } from "../helper/api-util.js";
import EventList from "../components/events/event-list";

function HomePage({ featuredEvents }) {
  return (
    <div>
      <EventList items={featuredEvents} />
    </div>
  );
}

export async function getStaticProps() {
  const featuredEvents = await getFeaturedEvents();

  if (!featuredEvents) {
    return { notFound: true };
  }

  return {
    props: {
      featuredEvents: featuredEvents,
    },
    revalidate: 1800, // 30분에 한번씩 페이지 재생성.
  };
}

export default HomePage;


// pages/events/[eventId].js
import { Fragment } from "react";

import { getEventById, getFeaturedEvents } from "../../helper/api-util";
import EventSummary from "../../components/event-detail/event-summary";
import EventLogistics from "../../components/event-detail/event-logistics";
import EventContent from "../../components/event-detail/event-content";
import ErrorAlert from "../../components/ui/error-alert";

function EventDetailPage({ event }) {
  if (!event) {
    return (
      <div className="center">
        <p>Loading...</p>
      </div>
    );
  }

  return (
    <Fragment>
      <EventSummary title={event.title} />
      <EventLogistics
        date={event.date}
        address={event.location}
        image={event.image}
        imageAlt={event.title}
      />
      <EventContent>
        <p>{event.description}</p>
      </EventContent>
    </Fragment>
  );
}

export async function getStaticProps(context) {
  const eventId = context.params.eventId;

  const event = await getEventById(eventId);

  if (!event) {
    return {
      props: {
        event: null,
      },
    };
  }

  return {
    props: {
      event,
    },
    revalidate: 30,
  };
}

export async function getStaticPaths() {
  // 모든 이벤트를 페칭하는 것은 낭비이므로 주요 이벤트만 사전 렌더링
  const events = await getFeaturedEvents();

  const pathWithParams = events.map((event) => ({
    params: { eventId: event.id },
  }));

  return {
    paths: pathWithParams,
    fallback: true,
  };
}
export default EventDetailPage;
  • EventDetailPage의 경우, getStaticPaths에서 모든 데이터를 가져오는 것은 차후에 낭비일 수 있다.(너무 많은 데이터가 있을 경우..) 따라서 주요 이벤트만 사전 렌더링하도록 한 다음 fallback:true로 설정해 주요 데이터가 아닌 경우에도 잠깐의 폴백 메시지(Loading...)를 띄운 뒤에 상세 데이터가 나오도록 한다.

📖 AllEventsPage 작업하기

import { Fragment } from "react";
import { useRouter } from "next/router";

import { getAllEvents } from "../../helper/api-util";
import EventList from "../../components/events/event-list";
import EventsSearch from "../../components/events/events-search";

function AllEventsPage({ events }) {
  const router = useRouter();

  function findEventsHandler(year, month) {
    const fullPath = `/events/${year}/${month}`;

    router.push(fullPath);
  }

  return (
    <Fragment>
      <EventsSearch onSearch={findEventsHandler} />
      <EventList items={events} />
    </Fragment>
  );
}

export async function getStaticProps() {
  const events = await getAllEvents();

  if (!events) {
    return { notFound: true };
  }

  return {
    props: {
      events,
    },
    revalidate: 60,
  };
}

export default AllEventsPage;

📖 FilteredEventsPage(slug) 작업하기 - 서버 사이드 렌더링(SSR) 사용

  • 사전 생성하는 getStaticProps를 사용하는 것도 좋은 방법이긴 하지만 getServerSideProps를 사용하는 것이 훨씬 좋다. 가능한 필터링 조합을 미리 다 정할 수 없기 때문이다.
  • getServerSideProps를 통해 들어오는 모든 요청에 대해 즉시 데이터를 페칭해서 해당 요청에 대한 페이지를 반환하도록 한다.
// helper/api-util.js
export async function getFilteredEvents(dateFilter) {
  const { year, month } = dateFilter;

  const allEvents = await getAllEvents();

  let filteredEvents = allEvents.filter((event) => {
    const eventDate = new Date(event.date);
    return (
      eventDate.getFullYear() === year && eventDate.getMonth() === month - 1
    );
  });

  return filteredEvents;
}

// pages/events/[...slug].js
import { Fragment } from "react";

import { getFilteredEvents } from "../../helper/api-util";
import EventList from "../../components/events/event-list";
import ResultsTitle from "../../components/events/results-title";
import Button from "../../components/ui/button";
import ErrorAlert from "../../components/ui/error-alert";

function FilteredEventsPage({ hasError, events, eventDate }) {
  if (hasError) {
    return (
      <Fragment>
        <ErrorAlert>
          <p>Invalid filter. Please adjust your values!</p>
        </ErrorAlert>
        <div className="center">
          <Button link="/events">Show All Events</Button>
        </div>
      </Fragment>
    );
  }

  const date = new Date(eventDate.year, eventDate.month - 1);

  if (!events || events.length === 0) {
    return (
      <Fragment>
        <ErrorAlert>
          <p>No events found for the chosen filter!</p>
        </ErrorAlert>
        <div className="center">
          <Button link="/events">Show All Events</Button>
        </div>
      </Fragment>
    );
  }

  return (
    <Fragment>
      <ResultsTitle date={date} />
      <EventList items={events} />
    </Fragment>
  );
}

export async function getServerSideProps(context) {
  const filterData = context.params.slug;

  const filteredYear = +filterData[0];
  const filteredMonth = +filterData[1];

  if (
    isNaN(filteredYear) ||
    isNaN(filteredMonth) ||
    filteredYear > 2030 ||
    filteredYear < 2021 ||
    filteredMonth < 1 ||
    filteredMonth > 12
  ) {
    return {
      props: { hasError: true },
      // notFound:true,
      // redirect: {
      //   destination:'/error'
      // }
    };
  }

  const filteredEvents = await getFilteredEvents({
    year: filteredYear,
    month: filteredMonth,
  });

  return {
    props: {
      events: filteredEvents,
      eventDate: {
        year: filteredYear,
        month: filteredMonth,
      },
    },
  };
}

export default FilteredEventsPage;
  • 강사는 hasError 키를 이용해서 invalid 필터 값을 입력했을 때 특정 JSX 코드를 반환하도록 설정하여 전체 코드를 더 가볍게 만들었다.

📖 클라이언트 사이드 데이터 페칭 추가하기

  • 해당 어플리케이션에서는 클라이언트 사이드 데이터 페칭이 더이상 필요하지는 않다.
  • 대신, FilteredEventsPage의 경우 클라이언트 사이드 데이터 페칭을 사용하기에 좋은 예시가 될 수 있다.
  • 게다가 해당 사이트는 검색 엔진에 노출될 필요가 별로 없기 때문에 굳이 사전 렌더링을 할 필요가 없다.
import { Fragment, useEffect, useState } from "react";

import useSWR from "swr";
import { useRouter } from "next/router";

import EventList from "../../components/events/event-list";
import ResultsTitle from "../../components/events/results-title";
import Button from "../../components/ui/button";
import ErrorAlert from "../../components/ui/error-alert";

function FilteredEventsPage() {
  const router = useRouter();
  const [loadedEvents, setLoadedEvents] = useState();

  const filterData = router.query.slug;

  const { data, error } = useSWR(
    "https://nextjs-course-demo-846e7-default-rtdb.firebaseio.com/events.json",
    (url) => fetch(url).then((res) => res.json())
  );

  useEffect(() => {
    if (data) {
      const events = [];

      for (const key in data) {
        events.push({
          id: key,
          ...data[key],
        });
      }

      setLoadedEvents(events);
    }
  }, [data]);

  if (!loadedEvents) {
    return <p className="center">Loading...</p>;
  }

  const filteredYear = +filterData[0];
  const filteredMonth = +filterData[1];

  if (
    isNaN(filteredYear) ||
    isNaN(filteredMonth) ||
    filteredYear > 2030 ||
    filteredYear < 2021 ||
    filteredMonth < 1 ||
    filteredMonth > 12 ||
    error
  ) {
    return (
      <Fragment>
        <ErrorAlert>
          <p>Invalid filter. Please adjust your values!</p>
        </ErrorAlert>
        <div className="center">
          <Button link="/events">Show All Events</Button>
        </div>
      </Fragment>
    );
  }

  let filteredEvents = loadedEvents.filter((event) => {
    const eventDate = new Date(event.date);
    return (
      eventDate.getFullYear() === filteredYear &&
      eventDate.getMonth() === filteredMonth - 1
    );
  });

  if (!filteredEvents || filteredEvents.length === 0) {
    return (
      <Fragment>
        <ErrorAlert>
          <p>No events found for the chosen filter!</p>
        </ErrorAlert>
        <div className="center">
          <Button link="/events">Show All Events</Button>
        </div>
      </Fragment>
    );
  }

  const date = new Date(filteredYear, filteredMonth - 1);

  return (
    <Fragment>
      <ResultsTitle date={date} />
      <EventList items={filteredEvents} />
    </Fragment>
  );
}

export default FilteredEventsPage;
  • getServerSideProps를 제거했기 때문에 데이터 사전 렌더링은 되지 않는다.
  • 오로지 Loading만이 사전 렌더링이 된다.

0개의 댓글

관련 채용 정보