Page(Route)

장현욱(Artlogy)·2022년 12월 5일
0

Next.js

목록 보기
2/6
post-thumbnail

페이지

기본 적으로 Next.js는 모든 페이지를 사전랜더링 합니다.
생성된 각 HTML은 해당 페이지에 필요한 최소한의 JavaScript코드와 연결되고 페이지가 완전히 로드 되면 나머지 JavaScript코드가 실행되며 페이지가 완전히 상호작용 하도록합니다.

사전 랜더링의 형태는 크게 두가지로 정적랜더링/동적랜더링이 있습니다.

정적 랜더링(서버)

정적 랜더링은 우리가 웹을 처음 배울때 HTML을 작성하여 웹을 꾸며주었던 때로 돌아갑니다.
next build시 우리가 jsx(tsx)로 작성하였던 코드가 HTML 정적파일로 생성됩니다.
정적페이지는 우리가 next.js를 쓰는 큰 이유중 하나입니다.

Next에선 데이터가 없을 경우 간단하지만 데이터를 외부에서 가져오는 페이지를 작성 할 경우 일정한 디자인 패턴을 따릅니다.

🔔 정적랜더링은 사전랜더링시 보여지게 되는 기본 랜더링입니다. (HTML)

데이터가 없는 페이지

function About() {
  return <div>About</div>
}

export default About

외부에서 데이터를 가져올 필요가 없다면 위 처럼 평범하게 작성해주면 됩니다.

데이터가 있는 페이지

일부 페이지는 사전 랜더링을 위한 외부 데이터를 가져와야 할 때가 있다.
크게 다음과 같은 경우가 있거나 한 페이지의 여러 경우가 있을 수 있을 것입니다.

  1. 페이지 빌드시 필요한 외부 데이터가 있을 경우
  2. 페이지 경로가 외부데이터에 따라 달라지는 경우

🔔 ReactApp과 다른 점은 사전 랜더링시 데이터를 가져와 함께 보여줄 수 있다는 점입니다.

1. 페이지 빌드시 필요한 외부 데이터가 있을 경우

: 블로그 페이지는 서버에서 블로그 게시물 목록을 가져와야 할 수 있다.

import type { GetStaticProps, InferGetStaticPropsType } from "next";

type Post = {
  title: string;
}

export const getStaticProps: GetStaticProps = async (context) => {
  const response = await fetch("https://example.com");
  const post: Post = await response.json();

  return {
    props: {
      post
    }
  }
}

const function Blog = ({ post }: InferGetStaticPropsType<typeof getStaticProps>) => {
  // { post: Post }로 타입 지정됨
  return (
    <div>{post.title}</div>
  )
}

export default Blog;

위 사전 렌더링 컴포넌트에서 데이터를 가져오기 위해선 NextJS는 동일한 파일에서 getStaticProps함수를 export async로 호출합니다.

  • getStaticProps는 컴포넌트 빌드시 호출된 데이터를 props에 매핑하여 전달합니다.
  • build시 DOM생성전에 딱 한번만 호출 됨으로 속도가 빠르고 SEO에 유리합니다.

🔔 getStaticProps 다음과 같은 상황에서 사용 하는걸 추천합니다.

  • 페이지에 필요한 데이터가 빌드 시에 사용 가능할 때
  • 데이터를 headless CMS에서 가져올 때
  • 모든 사용자에게 같은 데이터를 보여줄 때
  • SEO를 위해서 속도 빠른 페이지가 필요할 때
  • Node api(path, fs 등)을 사용해야 할 때

2. 페이지 경로가 외부데이터에 따라 달라지는 경우

동적 라우팅을 사용할 때, 어떤 페이지를 미리 Static으로 빌드할 지 정하고 싶을 땐,
getStaticPath를 사용합니다.

import type { GetStaticPaths } from "next";

export const getStaticPaths: GetStaticPaths = async () => {
  return {
    paths: [
      { params: {} }
    ],
    // 빌드 시에 해당 페이지들을 static으로 생성
    fallback: true | false | 'blocking'
    // fallback을 리턴해야 함
  }

  // 예시
  return {
    paths: [
      { params: { id: "1" }},
      { params: { id: "2" }}
    ]
    // pages/posts/[id].tsx라고 가정
    // pages/posts/1과 pages/posts/2를 static으로 생성
  }

  // 예시 2
  return {
    paths: [
      {
        params: {
          id: "1",
          title: "first post"
        }
      },
      {
        params: {
          id: "2",
          title: "second post"
        }
      }
    ]
    // pages/posts/[id]/[title].tsx라고 가정
    // pages/posts/1/first post와 pages/posts/2/second post/를 static으로 생성
  }
}
fallback
  • fallback: false
    1. getStaticPaths에서 리턴하지 않은 페이지는 모두 404로 연결

  • fallback: true
    1. 먼저 사용자에게 fallback 페이지를 보여줌
    2. 서버에서 정적 페이지를 생성함
    3. 해당 페이지를 사용자에게 보여줌
    4. 다음부터 해당 페이지로 접속하는 사용자에게는 만들어진 정적 페이지를 보여줌

    ⭐ 많은 정적 페이지를 생성해야 하지만 빌드 시간이 너무 오래 걸릴 때 사용

  • fallback:block
    1. 사용자에게 SSR 한 정적 페이지를 보여줌
    2. 다음부터 해당 페이지로 접속하는 사용자에게는 SSR된 정적 페이지를 보여줌

정적 페이지경로 동적생성

위의 예제는 수동으로 정적페이지가 될 경로를 지정해줬습니다.
허나 데이터가 1억개가 있고 더 늘어날 가능성도 있으며 각 데이터의 디테일 페이지를 정적으로 만들어야 된다고 생각해 봅시다. 위 방식대로라면 정말 끔찍한 작업이 될 것입니다.
NextJS에서 다음처럼 동적 경로를 가지는 정적페이지를 간편하게 만들어 줄 수 있습니다.

export const getStaticPaths: GetStaticPaths = async () => {
  const test: any[] = (
    await (
      await fetch(
        `some_api`
      )
    ).json()
  ).results;

  const paths = test.map((item) => ({ params: { id: item.id } }));
  return { paths, fallback: "blocking" };
};

export const getStaticProps: GetStaticProps = async (context) => {
  const movies: any[] = (
    await (
      await fetch(
        `some_api`
      )
    ).json()
  ).results;

  return { 
    props: { test: movies }, 
    //핵심은 이녀석 입니다.
    //빌드 후 10초 마다 페이지에 요청이 들어오면 페이지를 재생성하여 업데이트 해줍니다.
    //페이지 업데이트가 실패하더라도 이전 정적페이지는 유지 됩니다
    revalidate: 10 
  };
};

💥 정적 랜더링에서 외부 데이터는 화면과 함께 보여줘야 할 때 사용합니다.
그 외의 외부 데이터는 CSR방식으로 구성하는게 좋을 수 있습니다.


동적 랜더링(클라이언트)

동적랜더링은 React app에서 자주 사용하던 형태입니다.
useEffect를 통해 다음과 같이 많이 사용 하셨을 겁니다.

  useEffect(() => {
    (async () => {
      const { results } = await (await fetch(`/example.com`)).json();
    })();
  }, []);

특정 이벤트가 있을 때 외부 데이터를 가져오는게 아닌 위 처럼 컴포넌트가 did mount되는
즉 페이지가 요청 될 때 서버사이드로 외부 데이터를 가져 올 수 있습니다.

페이지 요청 마다 필요한 외부 데이터가 있을 경우

페이지가 요청 될 때마다 필요한 외부 데이터가 있을 경우 다음과 같이 getServerSideProps를 이용합니다. 페이지가 요청 될 때 마다 (새로고침등..) props 매핑합니다.

export default function Home({
  test,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
  return (
    <div className={styles.container}>
      {test?.map((item: any) => (
        <div key={item.id}>{item.title}</div>
      ))}
    </div>
  );
}

export const getServerSideProps: GetServerSideProps = async (context) => {
  const test: any = (
    await (
      await fetch(
        `some_api.com`
      )
    ).json()
  ).results;

  return { props: { test } };
};

getServerSideProps로 가져온 데이터도 SEO가 적용되는 정적데이터로써 기능을 합니다.
외부데이터가 랜더링 되는 시점 때문에 동적랜더링에 넣은 것이니 헷갈리지 마세요!

⭐ 정적랜더링과 동적랜더링을 적재적소에 쓰는 것이 next.js의 핵심입니다.


Route

리액트에서는 대부분 react-router-dom을 이용해 라우트를 구현 했을 텐데
NextJS도 똑같이 react-router-dom을 사용합니다. 허나 구현 하는 방식이 정해져 있습니다.

  • 색인 경로
    index이름을 가진 파일은 루트경로로써 자동 라우팅됩니다.

    pages/index.tsx→/
    pages/blog/index.tsx→/blog
  • 중첩 경로
    중첩할 경로 형태로 폴더 구조를 만들면 같은 방식으로 자동 라우팅됩니다.

    pages/blog/first-post.js→/blog/first-post
    pages/dashboard/settings/username.js→/dashboard/settings/username
  • 동적 경로
    동적 경로를 사용 할려면, 대괄호 구문을 사용할 수 있습니다. 이를 통해 정의된 매개변수를 일치시킬 수 있습니다.

    pages/blog/[slug].js→ /blog/:slug( /blog/hello-world)
    pages/[username]/settings.js→ /:username/settings( /foo/settings)
    pages/post/[...all].js→ /post/*( /post/2020/id/title)

⭐ 페이지 경로 우선순위
1. 정적경로 ex) pages/home.tsx
2. 동적경로 ex) pages/[id].tsx
3. 동적배열경로 ex) pages/[...id].tsx

정적 경로

Link컴포넌트를 이용해 정적 경로간 연결을 구현 할 수 있습니다.

import Link from 'next/link'

function Home() {
  return (
    <ul>
      <li>
        <Link href="/">Home</Link>
      </li>
      <li>
        <Link href="/about">About Us</Link>
      </li>
      <li>
        <Link href="/blog/hello-world">Blog Post</Link>
      </li>
    </ul>
  )
}

export default Home

각각은 경로는 pages 폴더의 구조를 매핑합니다.

  • / → pages/index.js
  • /about → pages/about.js
  • /blog/hello-world →pages/blog/[slug].js

동적 경로

동적 경로 또한 Link컴포넌트로 구현이 가능합니다.

import Link from 'next/link'

function Posts({ posts }) {
  return (
    //방법1
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          //문자 호환성을 위해 encodeURIComponent를 사용함.
          <Link href={`/blog/${encodeURIComponent(post.slug)}`}>
            {post.title}
          </Link>
        </li>
      ))}
    </ul>
    //방법2 (추천)
     <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link
            href={{
              pathname: '/blog/[slug]',
              query: { slug: post.slug },
            }}
          >
            {post.title}
          </Link>
        </li>
      ))}
    </ul>
  )
}

export default Posts

useRouter()

쿼리 (query)

NextJS에선 useRouter를 이용하여 동적 경로의 params에 접근 할 수 있습니다.

pages/post/[pid].tsx

import { useRouter } from 'next/router'

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

  return <p>Post: {pid}</p>
}

export default Post

위와 같은 형태로 접근 해서 사용이 가능합니다.
prams는 물론 query개체도 useRouter()query객체로 접근이 가능합니다.

ex) /post/abc
query객체엔 다음과 같이 존재 할 것입니다.

{ "pid": "abc" }

ex) /post/abc?foo=bar
query객체엔 다음과 같이 존재 할 것입니다.

{ "foo": "bar", "pid": "abc" }

전환 (push | replace)

push는 클라이언트 측 페이지 이동을 담당하며, 히스토리 스택에 기록이 쌓입니다.
replace는 클라이언트 측 페이지 이동을 담당하며, 히스토리 스택에 기록이 쌓이지 않습니다.

router.push(url, as, options) | router.replace(url, as, option)
  • url: 이동 할 URL객체 또는 string입니다.
  • as : URL바에서 보여질 path
  • options:
    - scroll : 전환 후 스크롤을 맨위로 둘 것인지 선택여부입니다. (default:true)
    - shallow : getStaticProps,getServerSideProps를 실행하지 않고 페이지 업데이트 (default:false)
    -locale: 새로운 페이지 로케일 표시

빠른 전환 (prefetch)

빠른 페이지 전환을 위해 정적 페이지를 미리 가져와 전환하는 것

router.prefetch(url, as, options)
  • options :
    - locale : 활성 로케일과 다른 로케일을 제공할 수 있습니다.

💥 정적페이지를 미리 가져오기 때문에 배포 전용 기능입니다.

인터셉터 (beforePopState)

라우터가 동작하기전 특정 작업을 실행하고 싶을때 사용합니다.

router.beforePopState({url, as, options})

사용 예

import { useEffect } from 'react'
import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  useEffect(() => {
    router.beforePopState(({ url, as, options }) => {
      // 다음 두 경로만 허용 하고 싶을 때
      if (as !== '/' && as !== '/other') {
        // Have SSR render bad routes as a 404.
        window.location.href = as
        return false
      }

      return true
    })
  }, [])

  return <p>Welcome to the page</p>
}

뒤로가기 (back)

뒤로가기 이벤트를 발생시킵니다. window.history.back()과 같습니다.

router.back();

앞으로 (forward)

앞으로 가기 이벤트를 발생시킵니다. window.history.forward()와 같습니다.

router.forward()

새로고침 (reload)

현재 URL을 다시 로드합니다. window.history.reload()와 같습니다.

router.reload();

이벤트 (events)

next/router로 이벤트를 감지해서 특정 이벤트가 발생하면 함수를 실행합니다.

1. routeChangeStart

routeChangeStart(url, { shallow })
경로가 변경되기 시작할때 발생

2. routeChangeComplete

routeChangeComplete(url, { shallow })
경로가 완전히 변경되면 발생

3. routeChangeError

routeChangeError(url, { shallow })
경로 변경시 오류가 발생하거나 경로 전환 취소시 발생 (err.cancelled - 탐색이 취소되었는지 여부)

4. beforeHistoryChange

beforeHistoryChange(url, { shallow })
브라우저의 history를 변경하기 전에 발생

5. hashChangeStart

hashChangeStart(url, { shallow })
해시는 변경되지만 페이지는 변경되지 않을때 발생

6. hashChangeComplete

hashChangeComplete(url, { shallow })
해시가 변경되었지만 페이지는 변경되지 않을때 발생

사용 예

 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)
    }
  }, [])

0개의 댓글