이전의 포스팅과 이어집니다.
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);
const selectedYear = filter?.[0];
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} />;
}
// throwing 오류
if (
(selectedYear && !getAvailableNewsYears().includes(+selectedYear)) ||
(selectedMonth &&
!getAvailableNewsMonths(selectedYear).includes(+selectedMonth))
) {
// selectedYear가 있지만 사용가능한 연도에 포함되지 않는다면
// 혹은 selectedMonth가 있지만 사용가능한 월에 포함되지 않는다면
throw new Error("유효하지 않는 필터입니다.");
}
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}
</>
);
}
if
문을 이용해서 해당 조건을 만족 시 throw new Error()
를 통해 유효하지 않은 경로 세그먼트에 접근 시 기본 개발 에러 페이지(?)가 나오게 된다.'use client'
를 작성해야 한다. → 에러는 서버가 작동 중일 때 말고도 클라이언트 사이드에서도 발생할 수 있기 때문이다."use client";
export default function FilterError({ error }) {
return (
<div id="error">
<h2>오류가 발생했습니다!</h2>
<p>{error.message}</p>
</div>
);
}
// components/main-header.js
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
export default function MainHeader() {
const path = usePathname();
return (
<header id="main-header">
<div id="logo">
<Link href="/">NextNews</Link>
</div>
<nav>
<ul>
<li>
<Link
href="/news"
className={path.startsWith("/news") ? "active" : undefined}
>
News
</Link>
</li>
<li>
<Link
href="/archive"
className={path.startsWith("/archive") ? "active" : undefined}
>
Archive
</Link>
</li>
</ul>
</nav>
</header>
);
}
usePathname
을 이용하여 현재 경로를 탐색하는데 이는 'use client'
를 상단에 작성해줘야한다.usePathname
으로 인해 클라이언트 컴포넌트로 사용하는 것인데, 그 외의 다른 <header>, <nav>
같은 태그들이 많이 존재하므로 최적화할 필요가 있다. → 클라이언트 컴포넌트를 최소화하여 아웃소싱하는 방식이 좋다.// /components/nav-link.js
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
export default function NavLink({ href, children }) {
const path = usePathname();
return (
<Link href={href} className={path.startsWith(href) ? "active" : undefined}>
{children}
</Link>
);
}
// /components/main-header.js
import Link from "next/link";
import NavLink from "./nav-link";
export default function MainHeader() {
return (
<header id="main-header">
<div id="logo">
<Link href="/">NextNews</Link>
</div>
<nav>
<ul>
<li>
<NavLink href="/news">News</NavLink>
</li>
<li>
<NavLink href="/archive">Archive</NavLink>
</li>
</ul>
</nav>
</header>
);
}
🔗 Next.js 공식문서 : intercepting routes
import { DUMMY_NEWS } from "@/dummy-news";
import { notFound } from "next/navigation";
export default function ImagePage({ params }) {
const newsItemlug = params.slug;
const newsItem = DUMMY_NEWS.find((newsItem) => newsItem.slug === newsItemlug);
if (!newsItem) {
notFound();
}
return (
<div id="fullscreen-image">
<img src={`/images/news/${newsItem.image}`} alt={newsItem.title} />
</div>
);
}
import { DUMMY_NEWS } from "@/dummy-news";
import Link from "next/link";
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>
{/* Link를 이용해서 해당 이미지를 클릭 -> 전체 풀스크린으로 이미지를 볼 수 있게 함. */}
<Link href={`/news/${newsItem.slug}/image`}>
<img src={`/images/news/${newsItem.image}`} alt={newsItem.title} />
</Link>
<h1>{newsItem.title}</h1>
<time dateTime={newsItem.date}>{newsItem.date}</time>
</header>
<p>{newsItem.content}</p>
</article>
);
}
Intercepting Routes : 인터셉팅 라우트는 대체 라우트로 페이지 내부 링크를 통한 탐색 여부에 따라 때때로 활성화 된다. 같은 경로라 하더라도 접근 방식(ex. 새로고침)에 따라 표시되는 페이지가 달라진다. → 인터셉팅 라우트는 기본적으로 내부 네비게이션 요청을 가로챈다.
import { DUMMY_NEWS } from "@/dummy-news";
import { notFound } from "next/navigation";
export default function InterceptedImagePage({ params }) {
const newsItemlug = params.slug;
const newsItem = DUMMY_NEWS.find((newsItem) => newsItem.slug === newsItemlug);
if (!newsItem) {
notFound();
}
return (
<>
<h2>Intercepted</h2>
<div id="fullscreen-image">
<img src={`/images/news/${newsItem.image}`} alt={newsItem.title} />
</div>
</>
);
}
위의 이미지에서 볼 수 있듯이, 새로 고침하면 Intercepted라는 문구가 보이지 않는다. 대신 /news에서부터 접근을하면 Intercepted 문구가 보인다. → 보이는 콘텐츠가 달라진다.
import { DUMMY_NEWS } from "@/dummy-news";
import { notFound } from "next/navigation";
export default function InterceptedImagePage({ params }) {
const newsItemlug = params.slug;
const newsItem = DUMMY_NEWS.find((newsItem) => newsItem.slug === newsItemlug);
if (!newsItem) {
notFound();
}
return (
<>
<div className="modal-backdrop" />
<dialog className="modal" open>
<div className="fullscreen-image">
<img src={`/images/news/${newsItem.image}`} alt={newsItem.title} />
</div>
</dialog>
</>
);
}
이미지를 클릭했을 때 모달로 나오도록 <div className="modal-backdrop" />
과 <dialog>
를 추가하였다.
이미지를 클릭했을 때 모달이 나오긴 하지만 backdrop부분이 반투명하게 나오지 않는다. 이를 수정하기 위해서 병렬 라우트와 인터셉트 라우트를 결합하고자 한다.
export default function NewsDetailLayout({ children }) {
return <>{children}</>;
}
기본 디테일 페이지 내용은 layout.js로도 충분히 렌더링 가능하다. 기존의 (.)image 의 경로 설정은 (..)image으로 바뀌지 않는다.
병렬 라우트 폴더는 무시하기 때문이다. → (.)image에서 경로는 폴더 시스템 내의 경로가 아니라 폴더 구조 때문에 렌더링 될 URL 경로에 있기 때문이다.
다시한번 레이아웃을 수정해준다.
export default function NewsDetailLayout({ children, modal }) {
return (
<>
{modal}
{children}
</>
);
}
export default function ModalDefaultPage() {
return null;
}
이제 모달의 백드롭에 반투명하게 디테일한 내용이 나오는 것을 확인할 수 있다.
"use client";
import { DUMMY_NEWS } from "@/dummy-news";
import { notFound } from "next/navigation";
import { useRouter } from "next/navigation";
export default function InterceptedImagePage({ params }) {
const router = useRouter();
const newsItemlug = params.slug;
const newsItem = DUMMY_NEWS.find((newsItem) => newsItem.slug === newsItemlug);
if (!newsItem) {
notFound();
}
return (
<>
<div className="modal-backdrop" onClick={router.back} />
<dialog className="modal" open>
<div className="fullscreen-image">
<img src={`/images/news/${newsItem.image}`} alt={newsItem.title} />
</div>
</dialog>
</>
);
}
백드롭을 클릭하면 router.back
이 실행되어 이전 URL로 이동함을 볼 수 있다.
라우트 그룹의 이점 : 라우트 그룹을 통해 전용 레이아웃을 설정할 수 있다. 해당 그룹에 포함되는 라우트에만 적용이 된다.
/app/(marketing) 폴더를 생성하여 라우트 그룹을 생성한다.
/app/(marketing)/layout.js를 생성하여 작성한다.
// /app/(marketing)/layout.js
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>{children}</body>
</html>
);
}
/app/page.js도 /app/(marketing) 폴더로 옮긴다.
/app/not-found.js도 옮겨야한다. 왜냐하면 라우트 그룹을 생성하면 not-found.js 페이지를 비롯해 다른 페이지는 라우트 그룹과 같은 수준에 둘 수 없기 때문이다. → /app/(content) 폴더로 옮긴다.
라우트 그룹에서 설정한 대로 root page로 접속했을 때, 네비게이션이 보이지 않는다. 대신 "Read the latest news" 버튼을 누르면 그제서야 네비게이션이 보이게 된다. → 다른 루트 레이아웃을 가지는 다른 라우트 그룹에 있기 때문이다.
라우트 핸들러 : 다양한 함수를 내보내는 파일로
GET, POST, PATCH, PUT, DELETE
등으로 HTTP 메서드 이름으로 함수를 작성해야한다.
라우트 핸들러의 핵심은 화면에 렌더링 되는 페이지를 반환하지 않는 라우트를 설정하는 것이다. 대신 라우트 핸들러에서는 JSON 데이터를 반환하거나 수신되는 JSON 데이터를 수락하고 JSON 응답을 반환한다.
따라서 라우트 핸들러의 목적은 API 같은 라우트를 설정하여 데이터를 생성, 저장하는 등 필요한 작업을 전부 수행하되 클라이언트에서 내부적으로 호출하는 것이다.
/app/api/route.js 생성하기
/app/api/route.js 작성하기
export function GET(req) {
console.log(req);
return new Response("Hello");
return new Response.json();
}
export function POST(req) {}
/api에 접속하기
미들웨어를 사용하기 위해선 루트 프로젝트 폴더로 이동해서 middleware.js
라는 이름으로 파일을 생성해야한다.
// /middleware.js
import { NextResponse } from "next/server";
export function middleware(req) {
return new NextResponse(); // 새로운 Response 객체 생성 가능
return NextResponse.next(); // 들어오는 요청을 실제 대상으로 전달
}
이 미들웨어 함수는 수신하는 요청을 차단하거나 처리할 수 있으나 사실 이 함수의 목적은 수신하는 요청을 살펴보고 변경하거나 차단해서 인증을 구현하고 다른 페이지로 리디렉션하는 것이다.
미들웨어는 페이지, 라우트 등 전체 웹사이트로 전송된 요청에서 실행할 코드를 설정하도록 허용한다. 따라서 해당 요청 블록을 검사하거나 리디렉션할 수 있다.
// /middleware.js
import { NextResponse } from "next/server";
export function middleware(req) {
console.log(req);
// return new NextResponse() // 새로운 Response 객체 생성 가능
return NextResponse.next(); // 들어오는 요청을 실제 대상으로 전달
}
/news 로 접속하면 화면에 보이는 이미지마다 요청이 있을 것이다. 서버에서 로딩이 되기 때문에 별도의 요청을 통해 이루어진다. → 모든 요청에 대해 middleware
함수를 실행하므로 원하는 작업이 뭐든 수행할 수 있다.
config
객체를 내보낼 수도 있다. config
라는 이름의 변수 또는 상수이자 객체여야 한다. 여기서 matcher
프로퍼티를 설정할 수 있다.
matcher
는 미들웨어를 트리거하는 요청을 필터링하도록 한다.
// /middleware.js
export const config = {
matcher: "/news",
};
위와 같이 설정한다면, 뉴스 페이지에 대한 요청은 존재하지만 아이콘이나 이미지에 대한 요청은 콘솔에 보이지 않는다.