import Link from "next/link";
export default function NewsPage() {
return (
<>
<h1>News Page</h1>
<ul>
<li>
<Link href="/news/first-news">First News Item</Link>
</li>
<li>
<Link href="/news/second-news">Second News Item</Link>
</li>
<li>
<Link href="/news/third-news">Third News Item</Link>
</li>
</ul>
</>
);
}
export default function DetailNewsPage({ params }) {
return <h1>news detail : {params.slug}</h1>;
}
import Link from "next/link";
export default function MainHeader() {
return (
<header>
<ul>
<li>
<Link href="/">Home</Link>
</li>
<li>
<Link href="/news">News</Link>
</li>
</ul>
</header>
);
}
// /app/layout.js
import MainHeader from "@/components/main-header";
import "./globals.css";
export const metadata = {
title: "Next.js Page Routing & Rendering",
description: "Learn how to route to different pages.",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<MainHeader /> {/* MainHeader를 추가 -> 헤더가 항상 보이도록 함*/}
{children}
</body>
</html>
);
}
앱 스타일링은 /app/globals.css를 참고하여 그에 맞는 클래스, 아이디를 부여하여 스타일링을 완성하였다.
전체 뉴스 데이터들을 표현하는 페이지. 더미데이터를 불러와 map
함수를 이용해 표현하였다.
import Link from "next/link";
import Image from "next/image";
import { DUMMY_NEWS } from "@/dummy-news";
export default function NewsPage() {
return (
<>
<h1>News Page</h1>
<ul className="news-list">
{DUMMY_NEWS.map((newsItem) => (
<li key={newsItem.id}>
<Link href={`/news/${newsItem.slug}`}>
<img
src={`/images/news/${newsItem.image}`}
alt={newsItem.title}
/>
<span>{newsItem.title}</span>
</Link>
</li>
))}
</ul>
</>
);
}
디테일한 뉴스를 표현하는 페이지이다. 더미 데이터에서 find
메서드를 통해 현재 페이지의 slug와 더미데이터의 slug에서 일치하는 값이 있는지 확인 후 표현.
import { DUMMY_NEWS } from "@/dummy-news";
export default function DetailNewsPage({ params }) {
const newsSlug = params.slug;
const newsItem = DUMMY_NEWS.find((newsItem) => newsItem.slug === newsSlug);
return (
<article className="news-article">
<header>
<img src={`/images/news/${newsItem.image}`} alt={newsItem.title} />
<h1>{newsItem.title}</h1>
<time dateTime={newsItem.date}>{newsItem.date}</time>
</header>
<p>{newsItem.content}</p>
</article>
);
}
페이지나 리소스를 찾을 수 없을 때 화면에 표시될 콘텐츠를 담당한다.
export default function NotFoundPage() {
return (
<div id="error">
<h1>Not Found</h1>
<p>요청한 리소스를 찾을 수 없습니다.</p>
</div>
);
}
이 not-found 파일은 조금 더 중첩된 폴더 내에서도 설정할 수 있다.
export default function NewsNotFoundPage() {
return (
<div id="error">
<h1>Not Found</h1>
<p>요청하신 기사를 찾을 수 없습니다.</p>
</div>
);
}
만약 존재하지 않는 /news/abc로 접근하게 된다면 위에서 설정한 not-found 파일의 내용이 리턴되는 것이 아니라 런타임 에러가 발생하여 해당 에러를 표현된다. 따라서 의도한대로 not-found 페이지가 표현되기 위해선 /app/news/[slug]/page.js
의 내용이 일부 수정되야한다.
import { DUMMY_NEWS } from "@/dummy-news";
import { notFound } from "next/navigation";
export default function DetailNewsPage({ params }) {
const newsSlug = params.slug;
const newsItem = DUMMY_NEWS.find((newsItem) => newsItem.slug === newsSlug);
if (!newsItem) {
notFound();
}
return (
<article className="news-article">
<header>
<img src={`/images/news/${newsItem.image}`} alt={newsItem.title} />
<h1>{newsItem.title}</h1>
<time dateTime={newsItem.date}>{newsItem.date}</time>
</header>
<p>{newsItem.content}</p>
</article>
);
}
notFound()
를 통해 요청한 기사를 찾을 수 없다는 오류를 트리거할 것이고 NextJS가 이를 처리할 것이다. → fallback 콘텐츠 출력
우선 /app/archive 폴더를 생성한다. 이 폴더는 병렬 페이지 두 개로 구성되어야 한다.
🔗 NextJS 공식문서 : Parallel Routes
병렬 라우트는 별도의 경로를 가지는 라우트 두 개의 콘텐츠를 동일한 페이지에서 렌더링하는 기능이다. 병렬 라우트를 사용하기 위해선 병렬 라우트를 포함하려는 경로에
layout.js
를 작성해야한다.
// /app/archive/@archive/page.js
export default function ArchievePage() {
return <h1>Archieve Page</h1>;
}
// /app/archive/@latest/page.js
export default function LatestNewsPage() {
return <h1>Latest News Page</h1>;
}
일반적으로 레이아웃 컴포넌트 함수는 children
프로퍼티를 받는다.
children
프로퍼티를 통해 엑세스할 수 있게 된 콘텐츠는 페이지의 콘텐츠가 될 것이고 그것이 화면에 표시가 될 것이다.
layout.js 파일에서 작업할 때 병렬 라우트 폴더가 있는 경우 자식 프로퍼티 뿐만 아니라 '@' 옆에 작성한 이름(예를 들어, archive, latest)과 같은 프로퍼티를 통해 해당 콘텐츠에 접근이 가능하다.
NextJS는 병렬 라우트 폴더와 인접해 있다면 해당 레이아웃 컴포넌트에 프로퍼티를 자동으로 추가할 것이다.
export default function ArchiveLayout({ archive, latest }) {
return (
<div>
<h1>News Archive</h1>
<section id="archive-filter">{archive}</section>
<section id="archive-latest">{latest}</section>
</div>
);
}
같은 라우트 '/archive'에 접근했는데 archive page와 latest news page가 동시에 보이게 된다. 병렬 라우트를 사용했기 때문에 두 페이지의 콘텐츠가 한꺼번에 보인다.
import NewsList from "@/components/news-list";
import { getNewsForYear } from "@/lib/news";
export default function FilteredNewsPage({ params }) {
const newsYear = params.year;
const filteredNews = getNewsForYear(newsYear);
return <NewsList news={filteredNews} />;
}
import Link from "next/link";
export default function NewsList({ news }) {
return (
<ul className="news-list">
{news.map((newsItem) => (
<li key={newsItem.id}>
<Link href={`/news/${newsItem.slug}`}>
<img src={`/images/news/${newsItem.image}`} alt={newsItem.title} />
<span>{newsItem.title}</span>
</Link>
</li>
))}
</ul>
);
}
default.js
파일을 추가할 수 있다.default.js
파일을 추가할 수 있다. → 기본 폴백 콘텐츠를 정의하기 위한 파일이기 때문이다.// default.js
export default function LatestNewsPage() {
return <h1>Latest News</h1>;
}
// page.js
export default function LatestNewsPage() {
return <h1>Latest News</h1>;
}
import NewsList from "@/components/news-list";
import { getLatestNews } from "@/lib/news";
export default function LatestNewsPage() {
const latestNews = getLatestNews();
return (
<>
<h1>Latest News</h1>
<NewsList news={latestNews} />
</>
);
}
위의 이미지를 통해 2024년 혹은 2023년 등 연도를 선택할 때마다, 연도 선택 네비게이션이 사라지는 것을 알 수 있다. 이를 고정하여 사용하고 싶은 경우 병렬 라우트에 속하도록 중첩된 레이아웃을 추가하면 된다.
그러나 위의 방식 NextJS의 또다른 기능을 사용하여 문제를 해결할 수 있다.
이때 기존에 /@archive/page.js가 존재하고 /@archive/[[...filter]]/page.js가 /@archive/page.js의 라우트까지 잡아내므로, 기존의 /@archive/page.js를 삭제한다.
// /app/archive/@archive/[[...filter]]/page.js
import NewsList from "@/components/news-list";
import { getAvailableNewsYears, getNewsForYear } from "@/lib/news";
import Link from "next/link";
export default function FilteredNewsPage({ params }) {
const filter = params.filter;
console.log(filter); // ['2024']
const selectedYear = filter?.[0]; // filter ? filter[0] : undefined
const selectedMonth = filter?.[1];
let news;
if (selectedYear && !selectedMonth) {
news = getNewsForYear(selectedYear);
}
let newsContent = <p>선택된 기간에 대한 뉴스를 찾지 못했습니다.</p>;
if (news && news.length > 0) {
newsContent = <NewsList news={news} />;
}
const links = getAvailableNewsYears();
return (
<>
<header id="archive-header">
<nav>
<ul>
{links.map((link) => (
<li key={link}>
<Link href={`/archive/${link}`}>{link}</Link>
</li>
))}
</ul>
</nav>
</header>
{newsContent}
</>
);
}
let links
로 하여 links를 변경가능하도록 한다.selectedYear
가 있고 selectedMonth
가 없는 경우, links를 업데이트한다.selectedYear
과 selectedMonth
가 있는지 조건문을 통해 확인 후, links를 빈 배열로 업데이트한다.<ul>
에서 <Link>
의 href
를 동적으로 변경해 줄 필요가 있다. 따라서 삼항연산자를 이용하여 href
를 업데이트한다.// /app/archive/@archive/[[...filter]]/page.js
import NewsList from "@/components/news-list";
import {
getAvailableNewsMonths,
getAvailableNewsYears,
getNewsForYear,
getNewsForYearAndMonth,
} from "@/lib/news";
import Link from "next/link";
export default function FilteredNewsPage({ params }) {
const filter = params.filter;
console.log(filter); // ['2024']
const selectedYear = filter?.[0]; // filter ? filter[0] : undefined
const selectedMonth = filter?.[1];
let news;
let links = getAvailableNewsYears();
if (selectedYear && !selectedMonth) {
news = getNewsForYear(selectedYear);
links = getAvailableNewsMonths(selectedYear);
}
if (selectedYear && selectedMonth) {
news = getNewsForYearAndMonth(selectedYear, selectedMonth);
links = [];
}
let newsContent = <p>선택된 기간에 대한 뉴스를 찾지 못했습니다.</p>;
if (news && news.length > 0) {
newsContent = <NewsList news={news} />;
}
return (
<>
<header id="archive-header">
<nav>
<ul>
{links.map((link) => {
const href = selectedYear
? `/archive/${selectedYear}/${link}`
: `/archive/${link}`;
return (
<li key={link}>
<Link href={href}>{link}</Link>
</li>
);
})}
</ul>
</nav>
</header>
{newsContent}
</>
);
}