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

·2024년 6월 23일
0

NextJS

목록 보기
21/26
post-thumbnail

이전의 포스팅과 이어집니다.

📌 병렬 라우트 사용하기

📖 Throwing(라우트 관련) 오류

  • 유효하지 않은 경로 세그먼트를 입력 시, 실제로 오류 표시를 하는 것이 좋다.

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

  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()를 통해 유효하지 않은 경로 세그먼트에 접근 시 기본 개발 에러 페이지(?)가 나오게 된다.

  • 기본적인 개발 모드 오버레이를 통해 Throwing 에러가 표현되고 있다. 다음은 Throwing 에러가 발생 시, 자체 오류 폴백 페이지를 렌더링되기 위한 과정이다.

💎 /app/archive/@archive/[[...filter]]/error.js

  • error.js 파일 또는 여기로 반환하는 컴포넌트는 클라이언트 컴포넌트여야만 하므로 파일의 상단에 'use client'를 작성해야 한다. → 에러는 서버가 작동 중일 때 말고도 클라이언트 사이드에서도 발생할 수 있기 때문이다.
  • 에러 폴백은 클라이언트, 서버 양쪽 모두에서 작동해야한다. → 클라이언트 컴포넌트는 서버 및 클라이언트 사이드 양쪽에서 작동한다. (서버 컴포넌트는 서버에서만 작동 )
"use client";

export default function FilterError({ error }) {
  return (
    <div id="error">
      <h2>오류가 발생했습니다!</h2>
      <p>{error.message}</p>
    </div>
  );
}


➕ 서버/클라이언트 컴포넌트

💎 RSC (React 서버 컴포넌트)

  • 서버에서만 렌더링 → 컴포넌트의 전체 기능이 서버에서만 실행되고 클라이언트에서는 절대 실행되지 않는다.
  • 기본적으로 Next.js에서는 모든 컴포넌트를 서버 컴포넌트로 유지해야한다. 특별한 이유가 없다면 클라이언트 컴포넌트로 바꿀 필요가 없다.

💎 클라이언트 컴포넌트

  • 서버에서 먼저 렌더링이 되지만 클라이언트 사이드에서도 실행될 수 있다.
// 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>
  );
}

📌 인터셉팅 라우트(Intercepting Routes)

🔗 Next.js 공식문서 : intercepting routes

📖 동적 라우트 안에 중첩된 라우트

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

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

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

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. 새로고침)에 따라 표시되는 페이지가 달라진다. → 인터셉팅 라우트는 기본적으로 내부 네비게이션 요청을 가로챈다.

  1. /image를 가로채고 싶으므로 /app/news/[slug]/(.)image 폴더를 생성한다
    • 만약 같은 같은 폴더내에 있다면 (.)
    • 인터셉팅 라우트가 중첩된 경로 폴더 내에 설정된다면 (..)
  2. /app/news/[slug]/(.)image/page.js 파일을 생성한다.

💎 /app/news/[slug]/(.)image/page.js

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 문구가 보인다. → 보이는 콘텐츠가 달라진다.


📖 병렬 및 인터셉트 라우트 결합하기

💎 /app/news/[slug]/(.)image/page.js

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>를 추가하였다.

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

이미지를 클릭했을 때 모달이 나오긴 하지만 backdrop부분이 반투명하게 나오지 않는다. 이를 수정하기 위해서 병렬 라우트와 인터셉트 라우트를 결합하고자 한다.

export default function NewsDetailLayout({ children }) {
  return <>{children}</>;
}

💎 /app/news/[slug]/@modal/(.)image/page.js

기본 디테일 페이지 내용은 layout.js로도 충분히 렌더링 가능하다. 기존의 (.)image 의 경로 설정은 (..)image으로 바뀌지 않는다.
병렬 라우트 폴더는 무시하기 때문이다. → (.)image에서 경로는 폴더 시스템 내의 경로가 아니라 폴더 구조 때문에 렌더링 될 URL 경로에 있기 때문이다.

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

다시한번 레이아웃을 수정해준다.

export default function NewsDetailLayout({ children, modal }) {
  return (
    <>
      {modal}
      {children}
    </>
  );
}

💎 /app/news/[slug]/@modal/default.js

export default function ModalDefaultPage() {
  return null;
}

이제 모달의 백드롭에 반투명하게 디테일한 내용이 나오는 것을 확인할 수 있다.


➕ 프로그램 방식으로 탐색하기 - 백드롭 클릭 시 상세 페이지로 리턴

💎 /app/news/[slug]/@modal/(.)image/page.js

"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로 이동함을 볼 수 있다.


📌 라우트 그룹 사용 및 이해

🔗 Next.js 공식문서 : Route Groups

  1. /app/(content) 폴더 생성하여 라우트 그룹 생성 → URL 경로에는 아무것도 추가되지 않는다.
  2. /app/archive 폴더와 /app/news 폴더, /app/layout.js를 /app/(content)로 옮긴다.

라우트 그룹의 이점 : 라우트 그룹을 통해 전용 레이아웃을 설정할 수 있다. 해당 그룹에 포함되는 라우트에만 적용이 된다.

  1. /app/(marketing) 폴더를 생성하여 라우트 그룹을 생성한다.

  2. /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>
      );
    }
  3. /app/page.js도 /app/(marketing) 폴더로 옮긴다.

  4. /app/not-found.js도 옮겨야한다. 왜냐하면 라우트 그룹을 생성하면 not-found.js 페이지를 비롯해 다른 페이지는 라우트 그룹과 같은 수준에 둘 수 없기 때문이다. → /app/(content) 폴더로 옮긴다.

라우트 그룹에서 설정한 대로 root page로 접속했을 때, 네비게이션이 보이지 않는다. 대신 "Read the latest news" 버튼을 누르면 그제서야 네비게이션이 보이게 된다. → 다른 루트 레이아웃을 가지는 다른 라우트 그룹에 있기 때문이다.


📌 라우트 핸들러로 API 구축하기

라우트 핸들러 : 다양한 함수를 내보내는 파일로 GET, POST, PATCH, PUT, DELETE 등으로 HTTP 메서드 이름으로 함수를 작성해야한다.
라우트 핸들러의 핵심은 화면에 렌더링 되는 페이지를 반환하지 않는 라우트를 설정하는 것이다. 대신 라우트 핸들러에서는 JSON 데이터를 반환하거나 수신되는 JSON 데이터를 수락하고 JSON 응답을 반환한다.
따라서 라우트 핸들러의 목적은 API 같은 라우트를 설정하여 데이터를 생성, 저장하는 등 필요한 작업을 전부 수행하되 클라이언트에서 내부적으로 호출하는 것이다.

  1. /app/api/route.js 생성하기

  2. /app/api/route.js 작성하기

    export function GET(req) {
      console.log(req);
      return new Response("Hello");
      return new Response.json();
    }
    
    export function POST(req) {}
  3. /api에 접속하기


📌 미들웨어 사용하기

🔗 Next.js 공식문서 : 미들웨어

미들웨어를 사용하기 위해선 루트 프로젝트 폴더로 이동해서 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",
};

위와 같이 설정한다면, 뉴스 페이지에 대한 요청은 존재하지만 아이콘이나 이미지에 대한 요청은 콘솔에 보이지 않는다.

0개의 댓글

관련 채용 정보