[Next.js] Next.js의 Route System

Jane·2023년 8월 23일
7

Next.js

목록 보기
6/12
post-thumbnail
post-custom-banner

🌟 Next.js에서 라우팅을 지원하는 방식

🛤️ Route

Routing

  • 네트워크상의 주소로 이동하여 해당 주소에 연결되어 있는 데이터를 사용하는 일련의 과정

Router

  • 라우팅을 수행할 수 있게 해주는 장치

React 🤼 Next.js

  • React.js
    • 라우터를 기본적으로 제공하지 않아 react-router-dom이라는 패키지를 설치해 라우팅을 진행한다.
    • 라우터 설정을 위해 직접 라우터 관련 코드를 작성하면 해당 코드가 호출되어 URL 분기 처리를 진행한다.
    • 동적 라우팅의 경우 :을 사용해주어야 한다.
  • Next.js
    • 라우터를 별도로 설치하지 않는다.
    • 파일 기반 라우팅을 지원한다.
      • 라우트와 페이지 구조를 연결할 때 JSX, JS 코드를 사용하지 않는다.
      • 설정된 폴더 구조로부터 폴더명, 파일명에 따라 자동으로 Path를 도출해 설정한다.
    • Next.js에서 정해둔 방식대로 페이지 파일 생성 시 해당 파일이 라우터 역할을 하는 제어의 역전이 발생한다.
    • 동적 라우팅의 경우 파일명, 폴더명을 []로 감싸주면 된다.

🤔 만약 pagessrc/pages 폴더가 모두 존재한다면?

  • pages 폴더에 작성한 코드가 우선적으로 적용된다.
  • src/pages 폴더 내부의 코드는 무시된다.

🌌 Routing in Next.js

👩‍🏫 Next.js에서 생성한 페이지 파일은 어떻게 라우팅 될까요?

🚩 Index Routes

  • 이름이 붙은 정적 라우트 파일을 사용한다.
    • 예: pages/about.js
  • 변환 예시
    • pages/index.js ➡️ /
    • pages/posts/index.js ➡️ /posts

🕸️ Nexted Routes

  • 중첩 경로를 활용해도 방법만 다를 뿐 결과는 동일하다.
  • pages 폴더 안에 하위 폴더를 생성해 구조화하여 여러 파일들을 그룹화하기 좋다.
  • 변환 예시
    • pages/posts/firstPost.js ➡️ /posts/firstPost
    • pages/board/user/userId.js ➡️ /board/user/userId

🎢 Dynamic Routes

  • 를 사용하여 동적 라우팅을 해줄 수 있다.
    • 예: /posts/[postId].js
    • 이때 Next.js는 주소를 /posts/postId로 설정하는 대신 postId를 placeholder로 인식하고, 해당 컴포넌트에 접근하여 여러 데이터를 불러올 수 있다.
  • placeholder에 고유값으로 인식되는 값이 이미 정적 파일에 존재하지 않도록 주의해야 한다.
    • 예를 들어, 이미 posts/list.js가 존재하는 상황에서 위의 동적 라우팅의 postId 값에 list를 넣을 경우 원래 존재하던 posts/list.js 파일로 이동한다.
    • Next.js는 이미 존재하는 정적 파일을 동적 파일보다 우선시하게 되기 때문이다.
  • 변환 예시
    • pages/[userId]/modifyInfo.js ➡️ /:userId/modifyInfo (/foo/modifyInfo)
  • pages/board/[slug].js ➡️ /board/:slug (board/firstPost)
  • Catch-All 라우트
    • JS의 전개 연산자를 사용하여 하위 파일들을 한 번에 동적으로 처리할 수 있다.
    • 예: pages/posts/[...all].js ➡️ /posts/* (/posts/2023/title/user)
👩‍🏫 위와 같은 라우팅을 구현하기 위해서는 어떤 방식을 사용해야 할까요?

✈️ next/link

a 태그

  • 순수 HTML 요소로, 사용자가 새 페이지의 URL로 이동할 수 있는 하이퍼링크를 생성한다.
  • URL을 통해 새 페이지로 이동할 때에, 이 페이지는 완전히 새로고침된다.
  • 이 때문에 필요하지 않은 JS 파일까지도 모두 받아오게 되어 Link 컴포넌트가 제공하는 최적화가 적용되지 않는다
  • 따라서 브라우저 주소창에 아예 새로운 URL을 입력해 이동한 것과 동일하게 동작한다.
👩‍🏫 페이지를 완전히 새로고침하므로 아래의 <Link> 태그를 사용하는 것이 좋습니다!
  • 페이지 컴포넌트 간의 연결을 위해 사용한다.
  • a 태그를 생성하여 웹 사이트가 크롤링될 수 있고 따라서 SEO에 적합하다.
  • 페이지를 다시 로드하지 않고 SPA가 동작하는 것처럼 보이게 만든다.
    • JS가 로드된 상태에서 선택된 페이지에 필요한 내용만 추가적으로 가져온다.
👩‍🏫 Next.js는 Link 컴포넌트를 "Client-Side Navigation"이라고 설명합니다.

Client-Side Navigation

  • JS를 기반으로 페이지 간 전환을 수행하여 페이지 컴포넌트를 교체해주는 형식
  • 주소 이동 시 브라우저 전체가 다시 로드되지 않고 Client-Side에서만 navigation이 발생한다.
  • 브라우저 주소창에 직접 URL을 입력하여 이동하는 것보다 속도가 훨씬 빠르다.

Code Splitting

  • Next.js는 자동 코드 분할을 통해 특정 페이지에 접근하는 경우 필요한 Chunk만을 로드한다.
  • 따라서 다른 페이지에 대한 코드는 처음에는 제공되지 않는다.
  • 이 덕분에 웹 사이트의 페이지가 수십, 수백 개에 달하더라도 필요한 페이지만 빠르게 로드할 수 있다.
  • Link 컴포넌트를 사용하면, Next.js는 브라우저의 뷰포트에 나타나는 Link 컴포넌트에 링크된 페이지에 대해서만 백그라운드에서 코드를 자동으로 미리 가져온다.
  • 만약 게시글 페이지에 대한 Link 컴포넌트가 첫 화면에 보이고, 스크롤을 내려야 사용자 페이지가 보이는 상황이라고 가정해보자.
    • 처음에는 뷰포트에 게시글 페이지 Link만 보이기 때문에 Next.js는 게시글 페이지의 JS 파일만을 pre-fetching 한다.
    • 스크롤을 내려 사용자 페이지 Link도 뷰포트 안에 들어오면, Next.js는 그때 사용자 페이지에 대한 JS 파일도 pre-fetching한다.
    • 그럼에도 링크를 클릭하는 시점에는 해당 페이지의 코드가 이미 백그라운드에 로드되어 있으므로 페이지는 즉각적인 전환이 가능하다.
  • Link 컴포넌트 사용 시 이처럼 code splitting을 통한 pre-fetching의 효과를 적극 활용할 수 있다.

href (필수)

  • 이동할 경로 또는 URL을 설정한다.
  • URL 객체도 받을 수 있다.
// 사전 정의된 경로: /about?name=test
<Link
          href={{
            pathname: '/about',
            query: { name: 'test' },
          }}
        >

// 동적 경로: /blog/my-post
<Link
    href={{
        pathname: '/blog/[slug]',
        query: { slug: 'my-post' },
    }}
>

as

  • 브라우저 URL 표시 부분에 표시될 경로를 설정한다.

passHref

  • Link가 href 속성을 자식에게 보낼 수 있게 한다.
  • 기본값: false

prefetch

  • 정적 페이지 생성을 사용하는 페이지는 더 빠른 페이지 전환을 위해 데이터와 JSON 파일을 미리 로드한다.
  • prefetch 속성은 백그라운드에서 페이지를 미리 가져와 뷰포트에 있는 모든 항목을 미리 로드한다.
  • 기본값: true
  • prefetch={false} 전달 시 비활성화시킬 수 있다.
    • 하지만 이 때에도 마우스 hover 시 pre-fetch는 계속 발생한다.

replace

  • history 스택에 새 URL을 추가하지 않고 현재의 상태만 교체한다.
  • 기본값: false

scroll

  • 페이지 이동 후 스크롤을 상단으로 조정한다.
  • 기본값: true

shallow

  • getStaticProps, getServerSideProps, getInitialProps를 실행하지 않고 현재 페이지의 경로만을 업데이트한다.
  • 기본값: false

locale

  • 활성 locale을 자동으로 앞에 추가한다.

🌏 router object

  • 아래의 useRouterwithRouter가 반환하는 값이다.

pathname (string)

  • 현재 경로
  • /pages 뒤에 오는 파일명을 의미한다.
  • basePath, locale은 포함하지 않는다.

query (object)

  • 동적 라우트의 매개 변수를 포함하는 객체
  • 구문 분석된 쿼리 문자열이다.

asPath (string)

  • basePath, locale 없이 브라우저에게 표시되는 경로
  • query를 포함한다.

isFallback (boolean)

  • 현재 페이지의 fallback 모드 여부

basePath (string)

  • 활성화된 basePath
  • basePath란 앱 내에서 설정되는 경로 접두사로 next.config.js에서 설정할 수 있다.
module.exports = {
  basePath: "/docs",
};
  • 위와 같이 설정하면 빌드 시 next/router에서 사용되는 경로에 접두사 /docs가 붙게 된다.
    • 예: /user ➡️ /docs/user

locale (string)

  • 활성화된 locale

locales (string[])

  • 지원되는 모든 locale의 목록

isReady (boolean)

  • 라우터 필드가 Client-Side에서 업데이트되고, 사용할 준비가 되었는지 여부
  • useEffect 메소드 내부에서만 사용해야 하며, 서버에서 조건부로 렌더링해서는 안 된다.

isPreview (boolean)

  • 애플리케이션의 미리보기 모드 여부

🗺️ next/router : useRouter

import { useRouter } from "next/router";

export default function Home() {
  const router = useRouter();
}
  • 애플리케이션 함수 구성 요소 내부의 router 객체에 접근할 수 있게 해주는 훅 함수
  • react-router-dom의 useLocation, useHistory의 기능을 떠올리면 이해하기 쉽다.

🌟 router.push

  • Client-Side의 라우팅 전환 처리를 할 수 있는 기능
  • react-router-dom의 useHistory (useNavigate)의 기능과 유사하다.
router.push(url, as, options);

url (필수!)

  • 이동할 경로
<button
  type="button"
  onClick={() => {
    router.push({
      pathname: "/post/[pid]",
      query: { pid: post.id },
    });
  }}
>
  Click me
</button>
  • Link 컴포넌트처럼 url에 객체도 받을 수 있다.

as

  • 브라우저에서 표시될 경로

options

Optiontype기본값설명
scrollbooleantrue페이지 이동 후 상단 스크롤
shallowbooleanfalsegetStaticProps, getServerSideProps, getInitialProps 실행 없이 현재 페이지 경로 업데이트
localestring새 페이지의 locale
⚠️ 외부 URL로의 이동 시 router.push 보다 window.location을 사용하는 것이 더 적합하다!

🌟 router.replace

  • 새 URL 항목을 history 스택에 추가하는 것을 방지한다.
  • 현재 페이지를 코드의 페이지로 대체하기 때문에 페이지 이동 후 되돌아갈 수 없다.
router.replace(url, as, options)
  • router.push의 파라미터와 동일하게 사용한다.
<button type="button" onClick={() => router.replace("/home")}>
  Click me
</button>

🌟 router.prefetch

  • 빠른 클라이언트 전환을 위해 페이지를 미리 로드한다.
  • 자동으로 페이지를 미리 가져오는 next/link가 없는 탐색에서 유용한 기능이다.
  • production에서 수행되는 기능으로 개발 시에는 페이지를 미리 가져오지 않는다.
router.prefetch(url, as);
  • url
    • prefetch할 URL
  • as
    • url의 optional decorator
    • Next.js 9 이전 버전에서 동적 경로를 미리 가져올 때 사용되었다.
const handleSubmit = useCallback((e) => {
  e.preventDefault();

  fetch("/api/login", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({}),
  }).then((res) => {
    if (res.ok) router.push("/dashboard");
  });
}, []);

useEffect(() => {
  // Prefetch the dashboard page
  router.prefetch("/dashboard");
}, []);
  • 위와 같이 로그인 페이지에서 로그인 후 사용자를 dashboard 페이지로 리다이렉션 하는 경우 dashboard 페이지를 미리 로드하여 페이지 전환을 빠르게 할 수 있다.

🌟 router.beforePopState

  • popstate 시점에 맞추어 라우터가 동작하기 전 수행하고자하는 작업이 있을 때 사용한다.
router.beforePopState(callbackFunc);
  • callbackFunc
    • popstate 이벤트에서 실행할 함수
    • callback 함수에서는 url, as, options를 수신한다.
  • callbackFunc가 false를 반환할 경우 Next.js 라우터는 popstate를 처리하지 않는다.
useEffect(() => {
  router.beforePopState(({ url, as, options }) => {
    // 오직 아래의 두 경로만 허용하고 싶을 때
    if (as !== "/" && as !== "/other") {
      // 다른 주소인 경우 SSR 렌더링이 404 상태를 갖게 한다.
      window.location.href = as;
      return false;
    }

    return true;
  });
}, []);
  • 위의 코드처럼 라우팅 요청을 조작하거나 SSR 새로고침을 강제 실행하게 하기 위해 사용된다.

🌟 router.back

<button type="button" onClick={() => router.back()}>
  Click here to go back
</button>
  • 브라우저에서 뒤로가기 버튼을 클릭하는 것과 동일하게 동작한다.
  • 호출 시 window.history.back()을 실행한다.

🌟 router.reload

<button type="button" onClick={() => router.reload()}>
  Click here to reload the page
</button>
  • 브라우저에서 새로고침 버튼을 클릭하는 것과 동일하게 동작한다.
  • 호출 시 window.history.reload()을 실행한다.

🌟 router.events

  • Next.js 라우터 내부에서 발생하는 이벤트를 수신할 수 있다.
  • 모든 애플리케이션 컴포넌트에서 이벤트를 구독할 수 있다.
  • 라우터 이벤트는 컴포넌트가 마운트 될 때 등록되어야 한다.

reouteChangeStart(url, {shallow})

  • 경로가 변경되기 시작할 때 발생
useEffect(() => {
  // 이벤트 구독
  const handleRouteChange = (url, { shallow }) => {
    console.log(
      `App is changing to ${url} ${
        shallow ? "with" : "without"
      } shallow routing`
    );
  };

  router.events.on("routeChangeStart", handleRouteChange);

  // 컴포넌트가 마운트되지 않은 경우 off 메서드로 구독을 취소한다.
  return () => {
    router.events.off("routeChangeStart", handleRouteChange);
  };
}, []);

routeChangeComplente(url, {shallow})

  • 경로가 완전히 변경되면 발생

routeChangeError(err, url, {shallow})

  • 경로 변경 시 오류가 발생하거나 경로 전환을 취소하는 경우 발생
  • err.cancelled
    • 탐색이 취소되었는지 여부를 나타낸다.
    • 경로 전환 취소 시 발생하는 routeChangeError를 전달받아 true 속성을 포함시킨다.
useEffect(() => {
  const handleRouteChangeError = (err, url) => {
    if (err.cancelled) {
      console.log(`Route to ${url} was cancelled!`);
    }
  };

  router.events.on("routeChangeError", handleRouteChangeError);

  // 컴포넌트가 언마운트 되면 구독을 취소한다.
  return () => {
    router.events.off("routeChangeError", handleRouteChangeError);
  };
}, []);

beforeHistoryChange(url, {shallow})

  • 브라우저의 history를 변경하기 전에 발생

hashChangeStart(url, {shallow})

  • 해시는 변경되기 시작하지만 페이지는 변경되지 않았을 때 발생

hashChangeComplete(url, {shallow})

  • 해시는 변경되었지만 페이지는 변경되지 않았을 때 발생한다.
👩‍🏫
useRouter은 React Hook이므로 클래스 컴포넌트에서는 사용이 불가능합니다.
그렇다면 클래스 컴포넌트에서는 어떻게 router 객체에 접근할 수 있을까요?

🗺️ next/router : withRouter

  • useRouter을 사용할 수 없는 클래스 컴포넌트의 경우 withRouter을 사용하여 컴포넌트에 동일한 router 객체를 추가할 수 있다.
import { withRouter } from "next/router";

function Page({ router }) {
  return <p>{router.pathname}</p>;
}

export default withRouter(Page);

🧐 params와 query-string 사용하기

  • href의 query의 value에 query-string을 담아 보내줄 수 있다.
  • 동적 라우팅의 [slug] 부분에 query 값을 담아 보내면 params로 전달된다.
  • 클릭 시 별도의 동작 없이 페이지 전환만 필요한 경우 사용하면 좋다.

⚠️ 주의할 점

  • 그냥 query에 값을 담아 보내기만 할 경우 사용자 URL에 데이터가 전부 노출될 위험이 있다.
  • 또한 보내고 싶은 데이터가 객체 형태일 경우 JSON.stringfy를 사용해 string으로 변환해주어야 하는데, 이 경우 URL의 길이가 과도하게 길어지는 문제가 발생한다.

🤠 as props를 추가해서 문제를 방지해줍시다!

  • as: 마스킹되어 브라우저의 URL 바에 보여질 주소를 의미한다.
import Link from "next/link";

function Home() {
  return (
    <ul>
      <li>
        <Link
          // /about?name=test
          href={{
            pathname: "/about",
            query: { name: "test" },
          }}
          as={`/about/question/${post.name}`}
        >
          <a>About us</a>
        </Link>
      </li>
      <li>
        <Link
          // /blog/my-post
          href={{
            pathname: "/blog/[slug]",
            query: { slug: "my-post" },
          }}
        >
          <a>Blog Post</a>
        </Link>
      </li>
    </ul>
  );
}

export default Home;

🗺️ useRouter에서 데이터 보내기

  • Link 컴포넌트에서처럼 URL 객체를 사용하여 query-string을 보내줄 수 있다.
  • 또한 동적 라우팅의 [slug] 부분에 query 값을 담아 보내면 params로 전달된다.
  • 두 번째 인자에 마스킹 라우트 값을 보내 사용자 URL에 query 데이터가 노출되지 않게 막을 수 있다.
  • 클릭 시 부가적인 함수나 이벤트와 함께 페이지를 이동할 경우 router.push를 사용하면 좋다.
import { useRouter } from "next/router";

export default function ReadMore({ post }) {
  const router = useRouter();

  return (
    <button
      type="button"
      onClick={() => {
        router.push(
          {
            pathname: "/post/[pid]",
            query: { pid: post.id },
          },
          "/order"
        );
      }}
    >
      Click here to read more
    </button>
  );
}

💫 useRouter로 query 데이터 받아오기

  • router 객체의 query 키 값을 사용하면 URL으로 전송된 query 데이터를 받아올 수 있다.
import { useRouter } from 'next/router'

export default () => {
    const router = useRouter()
    const {pid} = router.query

    ...
}

🧭 useSearchParams로 query-string 받아오기

  • useSearchParams는 현재 URL의 query-string을 받아올 수 있게 하는 Client-Component 훅 함수이다.
  • URLSearchParams의 읽기 전용 버전 interce를 반환한다.
// client-component에서 사용할 수 있다.
"use client";

import { useSearchParams } from "next/navigation";

export default function SearchBar() {
  const searchParams = useSearchParams();

  const search = searchParams.get("search");

  // URL -> `/dashboard?search=my-project`
  // `search` -> 'my-project'
  return <>Search: {search}</>;
}

URLSearchParams.get(searchParameter)

  • searchParameter과 일치하는 첫 번째 값을 반환한다.

URLSearchParams.has(searchParameter)

  • searchParameter과 일치하는 값이 있는지 여부를 boolean 타입의 값으로 반환한다.

🧭 page 컴포넌트에서 params, query-string 받아오기

// app/blog/[slug]/page.tsx
export default function Page({
  params,
  searchParams,
}: {
  params: { slug: string }
  searchParams: { [key: string]: string | string[] | undefined }
}) {
  return <h1>My Page</h1>
}
  • route에 특화된 UI인 page의 props를 사용하여 params와 query-string을 받아올 수 있다.

params (optional)

  • 동적 라우팅을 통해 전달된 params의 값을 포함하는 객체이다.
  • 사용 예시
예시URLparams
app/post-list/[slug]/page.js/post-list/1{ slug: '1' }
app/post-list/[user]/[date]/post.js/post-list/Jane/0822/1{ user: "Jane", date: "0822" }
app/post-list/[...slug]/page.js/post-list/1/2/3{ slug: ['1', '2', '3'] }

searchParams (optional)

  • 현재 URL의 search parameters의 값을 포함하는 객체이다.
  • useSearchParams를 사용할 수 없는 Server-Side 컴포넌트에서 query-string을 받아올 때 사용할 수 있다.
URLsearchParams
/post?user=Jane{user: "Jane"}
/post?user=Jane&date=0822{user: "Jane", date: "0822"}
/post?id=1&id=2{id: ["1", "2"]}

🔎 References

참고 자료 모음
profile
An investment in knowledge pays the best interest🙃
post-custom-banner

0개의 댓글