head
<head>
메타데이터의 필요성 분석하기기존 프로젝트를 봤을 때, <head>
의 메타데이터가 부족하다는 것을 느낄 수 있다. 메타데이터가 있어야 탭에 표시될 제목과 같이 세부적인 요소를 반영함으로써 사용자의 경험의 질을 원하는 정도까지 끌어올릴 수 있다. 또한 검색 엔진에도 메타데이터는 필수적인 부분이다. Google 크롤러 같은 검색 엔진 크롤러가 확인하는 게 메타데이터에 설정된 제목과 설명이기 때문이다.
<head>
콘텐츠 구성하기<head>
섹션이라는 HTML 요소를 추가하고 나면 Next.js가 알아서 해당 요소를 <Head>
섹션에 추가할 것이다.<meta name="description">
: 검색엔진에 필요한 속성. 검색 결과를 출력할 때 같이 출력되는 설명문이 된다.// pages/index.js
import Head from "next/head";
import { getFeaturedEvents } from "../helper/api-util.js";
import EventList from "../components/events/event-list";
function HomePage({ featuredEvents }) {
return (
<div>
<Head>
<title>NextJS Events</title>
<meta
name="description"
content="Find a lot of great events that allow you to evolve..."
/>
</Head>
<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;
<head>
콘텐츠 추가하기// pages/events/[eventId].js
import Head from "next/head";
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>
{/* 동적 head */}
<Head>
<title>{event.title}</title>
<meta name="description" content={event.description} />
</Head>
<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 default EventDetailPage
// pages/events/[...slug].js
import Head from "next/head";
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>
{/* 동적 head */}
<Head>
<title>Filtered Events</title>
<meta
name="description"
content={`All events for ${filteredMonth}/${filteredYear}`}
/>
</Head>
<ResultsTitle date={date} />
<EventList items={filteredEvents} />
</Fragment>
);
}
export default FilteredEventsPage;
// pages/events/[...slug].js
import Head from "next/head";
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]);
let pageHeadData = (
<Head>
<title>Filtered Events</title>
<meta name="description" content={`A List of filtered events.`} />
</Head>
);
if (!loadedEvents) {
return (
<>
{pageHeadData}
<p className="center">Loading...</p>
</>
);
}
const filteredYear = +filterData[0];
const filteredMonth = +filterData[1];
pageHeadData = (
<Head>
<title>Filtered Events</title>
<meta
name="description"
content={`All events for ${filteredMonth}/${filteredYear}`}
/>
</Head>
);
if (
isNaN(filteredYear) ||
isNaN(filteredMonth) ||
filteredYear > 2030 ||
filteredYear < 2021 ||
filteredMonth < 1 ||
filteredMonth > 12 ||
error
) {
return (
<Fragment>
{pageHeadData}
<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>
{pageHeadData}
<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>
{pageHeadData}
<ResultsTitle date={date} />
<EventList items={filteredEvents} />
</Fragment>
);
}
export default FilteredEventsPage;
_app.js
파일 작업하기<meta name="viewport" content="initial-scale=1.0, width=device-width" />
: 반응형 페이지의 스케일을 적정값으로 만드는 데 자주 쓰이는 태그이다. 따라서 해당 태그는 일부 페이지가 아닌 모든 페이지에 적용이 되야한다.import Head from "next/head";
import Layout from "../components/layout/layout";
import "../styles/globals.css";
function MyApp({ Component, pageProps }) {
return (
<Layout>
<Head>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
</Head>
<Component {...pageProps} />
</Layout>
);
}
export default MyApp;
<head>
콘텐츠 병합하기<Head>
요소를 알아서 병합해준다.<Head>
섹션이 있더라도 Next.js가 전부 병합해준다.<Head>
작성 중 발생하는 충돌도 자동적으로 해결해준다. 만약 같은 요소가 여러 개 있다면 가장 최근의 요소만 반영하는 식으로 해결.페이지 컴포넌트는 어플리케이션 컴포넌트보다 나중에 렌더링되므로 뒤에 렌더링되는 페이지 컴포넌트의 <Head>
섹션이 우선 표시된다.
_document.js
파일_app.js는 어플리케이션 셸(Shell)이다. 따라서 _app.js는 HTML 문서의 body 섹션 속 루트 컴포넌트라고 생각하면 된다.
_document.js는 전체 HTML 문서를 커스터마이징할 수 있게 해준다. (HTML 문서를 구성하는 모든 요소)
// pages/_document.js
import Document, { Html, Head, Main, NextScript } from "next/document";
class MyDocument extends Document {
render() {
return (
<Html>
<Head></Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
next/document
를 확장하여 작성되어야한다.next/head
에서 임포트하는 Head 컴포넌트와는 다르다.next/head
의 Head는 렌더링된 페이지의 Head 콘텐츠를 조정하기 위해서 JSX 코드 어디서나 사용된다.next/document
의 Head는 _document에 구축할 특수한 문서 컴포넌트에만 사용된다.위의 클래스가 오버라이드하지 않는 이상, 문서가 가지는 기본 구조가 된다. 만약 오버라이드하려면 해당 구조를 다시 만들어야한다.
// pages/_document.js
import Document, { Html, Head, Main, NextScript } from "next/document";
class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head></Head>
<body>
<div id="overlays" />
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
<div id="overlays" />
: HTML 콘텐츠를 어플리케이션 컴포넌트 트리 외부에 추가할 수 있게 해준다. (ex. React의 portal)Next Image
컴포넌트 & 기능을 통해 이미지 최적화하기<Image>
컴포넌트를 사용하면 Next.js에서 여러버전의 이미지를 요청이 들어올 때마다 바로 생성해주는데 각 운영 체제와 장치 크기에 최적화되도록 한다.// components/events/event-item.js
<Image src={"/" + image} alt={title} width={240} height={160} />
width height
: 원본 크기의 이미지 크기가 아니라 페이지에 표시하고자 하는 크기를 말한다.
<Image>
컴포넌트를 사용하면 품질은 낮추되 이미지에는 영향을 미치지 않고 이미지의 용량도 줄어들게 되며 이미지의 유형도 Chrome에 최적화된 WebP로 변한다.
이렇게 생성된 이미지는 .next 폴더에 저장된다. 이러한 이미지들은 필요할 때마다 최적화되어 생성되는 것으로 미리 생성해 두는 것이 아니라 요청이 있을 때 생성하고 저장해 두었다가 나중에 유사한 기기에서 요청이 들어왔을 때 바로 이미지를 꺼내서 렌더링하는 방식이다.
next/image
문서 살펴보기🔗 Image