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;
}
// 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;
// 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;
// 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;
// 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;
// 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;
getStaticPaths
에서 모든 데이터를 가져오는 것은 차후에 낭비일 수 있다.(너무 많은 데이터가 있을 경우..) 따라서 주요 이벤트만 사전 렌더링하도록 한 다음 fallback:true
로 설정해 주요 데이터가 아닌 경우에도 잠깐의 폴백 메시지(Loading...)를 띄운 뒤에 상세 데이터가 나오도록 한다.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;
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 코드를 반환하도록 설정하여 전체 코드를 더 가볍게 만들었다.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
를 제거했기 때문에 데이터 사전 렌더링은 되지 않는다.