[Next.js v14] 라우팅 및 페이지 렌더링 - 1

·2024년 6월 20일
0

NextJS

목록 보기
20/26
post-thumbnail

📌 과제

  • /news → 더미 뉴스 항목
  • /news/ → 특정 뉴스 데이터
  • 컴포넌트 추가 : 링크(시작 페이지, 뉴스 페이지) 추가

📖 /news 페이지 생성하기

  • /app/news/page.js 생성
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>
    </>
  );
}

📖 /news/ 페이지 생성하기

  • /app/news/[slug]/page.js
export default function DetailNewsPage({ params }) {
  return <h1>news detail : {params.slug}</h1>;
}

📖 MainHeader 컴포넌트 생성하기

  • /components/main-header.js
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를 참고하여 그에 맞는 클래스, 아이디를 부여하여 스타일링을 완성하였다.

💎 /app/news/page.js

전체 뉴스 데이터들을 표현하는 페이지. 더미데이터를 불러와 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>
    </>
  );
}

💎 /app/news/[slug]/page.js

디테일한 뉴스를 표현하는 페이지이다. 더미 데이터에서 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>
  );
}


📖 'Not Found' 오류 처리 및 'Not Found' 페이지 표시하기

페이지나 리소스를 찾을 수 없을 때 화면에 표시될 콘텐츠를 담당한다.

💎 /app/not-found.js

export default function NotFoundPage() {
  return (
    <div id="error">
      <h1>Not Found</h1>
      <p>요청한 리소스를 찾을 수 없습니다.</p>
    </div>
  );
}

이 not-found 파일은 조금 더 중첩된 폴더 내에서도 설정할 수 있다.

💎 /app/news/[slug]/not-found.js

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, /app/archive/@latest/page.js

  • @ 문자를 이용하여 병렬라우트를 추가하기 위한 폴더를 생성 → NextJS의 병렬라우트를 생성하기 위한 명명 규칙
// /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>;
}

💎 /app/archive/layout.js

일반적으로 레이아웃 컴포넌트 함수는 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가 동시에 보이게 된다. 병렬 라우트를 사용했기 때문에 두 페이지의 콘텐츠가 한꺼번에 보인다.


📖 병렬 라우트 및 중첩 라우트로 작업하기

💎 /app/archive/@archive/[year]/page.js

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} />;
}

💎 /components/news-list.js

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>
  );
}

  • 동일한 페이지에 표시되는 병렬 라우트는 원하는 경로를 모두 지원해야 한다. → @archive/[year]가 있으니 @latest/[year]도 존재해야한다.
  • 그러나 @latest/[year]/page.js는 개발자가 원하는 방식이 아니다. 이런 경우 NextJS는 default.js 파일을 추가할 수 있다.
  • 병렬 라우트를 사용한다면 언제든 default.js 파일을 추가할 수 있다. → 기본 폴백 콘텐츠를 정의하기 위한 파일이기 때문이다.

💎 /app/archive/@latest

// default.js
export default function LatestNewsPage() {
  return <h1>Latest News</h1>;
}

// page.js
export default function LatestNewsPage() {
  return <h1>Latest News</h1>;
}
  • page.js(표준 콘텐츠)와 default.js(폴백 콘텐츠)가 동일한 내용을 가진다면 page.js 파일을 없애고 default.js만 남겨도 가능하다.

💎 /app/archive/@latest/default.js

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} />
    </>
  );
}


📖 Catch-All 라우트 구성

위의 이미지를 통해 2024년 혹은 2023년 등 연도를 선택할 때마다, 연도 선택 네비게이션이 사라지는 것을 알 수 있다. 이를 고정하여 사용하고 싶은 경우 병렬 라우트에 속하도록 중첩된 레이아웃을 추가하면 된다.

그러나 위의 방식 NextJS의 또다른 기능을 사용하여 문제를 해결할 수 있다.

  • [year]을 catch-all 라우트로 구성한다. → [[...filter]]로 변경
  • 내부의 page.js 파일이 archive 이후 모든 경로 세그먼트에 대해 활성화되도록 보장한다. 세그먼트가 몇 개이든 이름이 몇개이든!

이때 기존에 /@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를 업데이트한다.
  • selectedYearselectedMonth가 있는지 조건문을 통해 확인 후, 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}
    </>
  );
}

0개의 댓글

관련 채용 정보