Next.js의 Routing을 알아보자! 💌

라우팅이란?

라우팅은 네트워크 용어로서 네트워크상의 주소로 이동하여 해당 주소에 연결되어있는 데이터를 사용하는 일련의 과정을 의미한다.
라우터는 라우팅을 수행할 수 있는 장치이다.

리액트에서는 라우터를 제공하지 않기 때문에 react-router-dom이라는 패키지를 설치하여 사용한다.

import React from 'react';
import { Switch, Route } from 'react-router-dom';

import Home from './pages/Home';
import HeaderContainer from './views/shared/containers/HeaderContainer';
import { GlobalStyle } from './styled/GlobalStyle';
import Watch from './pages/Watch';
import Search from './pages/Search';
import SidebarContainer from './views/shared/containers/SidebarContainer';
import SidebarGuide from './views/shared/components/Sidebar/components/SidebarGuide';

const App = () => (
  <Container>
    <GlobalStyle />
    <HeaderContainer />
    <SidebarContainer />
    <Template>
      <SidebarGuide />
      <Main>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route exact path="/watch/:id" component={Watch} />
          <Route exact path="/results/:query" component={Search} />
        </Switch>
      </Main>
    </Template>
  </Container>
)

const Template = styled.div`
  display: flex;
`;

const Main = styled.div`
  flex: 1;
`;
export default App;

리액트로 유튜브 웹 사이트를 클론 코딩해본 적이 있는데 (react-router-dom v6 이전에 작성한 코드입니다)
위와 같이 각 컴포넌트를 Route라는 태그로 감싸서 path를 지정해주었다.

Next.js가 pages (혹은 src/pages) 폴더 아래에 파일을 추가하는 방식으로 각 컴포넌트에 URL을 부여하는 것과 크게 다르다 👀
이러한 차이점이 리액트는 라이브러리, Next.js는 프레임워크라고 하는 근거라고 생각한다.

리액트는 라우터를 설정하기 위해 개발자가 직접 라우터와 관련한 코드를 작성하고 해당 코드가 호출되어 URL을 분기처리해준다.
반면에 Next.js는 해당 프레임워크에서 정해둔 방식대로 페이지 파일을 생성하면 해당 파일이 라우터 역할을 한다. (제어의 역전이 발생한다.)

참고! pages vs src/pages pages 폴더가 src/pages 폴더보다 우선권을 갖는다.
즉, 두 폴더가 모두 존재하는 경우 pages 폴더에서 작성한 코드가 우선적으로 적용되고, src/pages 폴더 내부는 무시된다.





Next.js 에서 생성한 페이지 파일은 아래와 같이 라우팅된다.

  1. Index Routes
  • pages/index.js/
  • pages/blog/index.js/blog
  1. Nested Routes
    • pages/blog/first-post.js/blog/first-post
    • pages/dashboard/settings/username.js/dashboard/settings/username
  2. Dynamic Routes
    • 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)

router 객체에 접근하기 위해서는 useRouter라는 훅을 사용하면 된다.
페이지 컴포넌트간의 연결은 Link 컴포넌트를 사용할 수 있다.





아래와 같은 코드가 있다.

import Link from 'next/link';

export default function Home() {
  return (
    <>
     <div>
        <Link href={'/posts/first-post'}>첫번째  읽기 with Link 컴포넌트</Link>    
     </div>
     <div>
        <a href={'/posts/first-post'}>첫번째  읽기 with a 태그</a>    
    </div>
    </>
  )
}
  • Link 컴포넌트로 감싼 링크를 클릭했을때

image

  • first-post.js 을 받아온다.
  • 자바스크립트가 로드된 상태에서 해당 페이지에 필요한 내용만 추가적으로 가져온다.



  • a 태그로 감싼 링크를 클릭했을때

image

  • first-post.js 이외에도 다양한 js 파일을 새로 받아온다.
  • Link 컴포넌트가 제공하는 최적화를 이용할 수 없다. 브라우저 주소창에 새로운 URL을 입력해서 이동한 것과 같은 동작을 한다.





Next.js는 이를 Client-Side Navigation 라는 개념으로 설명한다.

Client-Side Navation은 페이지간 전환이 자바스크립트를 기반으로 페이지 컴포넌트를 교체해주는 형식이므로 브라우저 주소창에 직접 URL을 입력하여 이동하는 것보다 훨씬 빠르다는 장점이 있다.


개발자 도구에서 body 태그의 background-color 속성을 변경해보자.
image



Link 컴포넌트를 클릭하면 해당 background-color은 유지되는 것을 볼 수 있다.
이는 브라우저 전체가 다시 로드되지 않고, 클라이언트 사이드에서만 navigation이 발생했음을 알 수 있다.
image



반면에 a 태그를 클릭할때는 background-color가 유지되지 않는다. 문서를 처음부터 다시 불러오기 때문이다.
image



Code Splitting

Next.js는 Automatic Code Splitting을 제공한다.
위와 같이 특정 페이지에 접근할때는 필요한 chunk만을 로드한다.
즉, 다른 페이지에 대한 코드는 처음에 제공되지 않는다.
따라서, 수백 개의 페이지가 있는 웹 사이트일지라도 필요한 페이지만을 빠르게 로드할 수 있다.

프로덕션 환경에서 Link 컴포넌트가 브라우저의 뷰포트에 나타날 때마다 Next.js는 백그라운드에서 링크된 페이지에 대한 코드를 자동으로 미리 가져온다.

뷰포트에 Link 컴포넌트가 있기 때문에 해당 페이지의 js 파일을 미리 로드한 것을 알 수 있다. (프리패칭한다.)
image



스크롤을 내려서 두번째 Link 컴포넌트도 뷰포트 안에 들어오면 그제서야 second-post.js 파일을 로드한다. (프리패칭한다.)
image

링크를 클릭하는 시점에 해당 페이지의 코드가 이미 백그라운드에 로드되어있기 때문에 페이지가 거의 즉각적으로 빠르게 전환된다! ✨

따라서, Link 컴포넌트를 사용하면 code splitting으로 인한 prefetching 효과를 톡톡히 볼 수 있겠다.



동적 라우팅 (Dynamic Routing)

항상 미리 정의된 경로를 사용할 수 있는 것은 아니다.
예를 들어, 블로그 게시물의 경로는 /blog/1, /blog/2 … 와 같을텐데 모든 경로에 대해 미리 정의하거나 페이지 컴포넌트를 생성하는 일은 굉장히 비효율적이고, 정확하지 않을 수 있다.

이때 사용하는 것이 동적 라우팅이다.
페이지 파일을 생성할때 대괄호를 추가하면 해당 페이지 컴포넌트는 동적으로 라우팅된다.

  • pages/post/[pid].js
import { useRouter } from 'next/router'

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

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

export default Post

이때 query 객체는 아래와 같은 값을 가진다.

{ "pid": "abc" }
  • /post/abc?foo=bar
{"pid": "abc", "foo": "bar"}
  • pages/post/[pid]/[comment].js에 대한 경로가 /post/abc/a-comment인 경우
{ "pid": "abc", "comment": "a-comment" }

클라이언트 사이드에서 동적 경로에 대해 접근하기 위해서는 next/link가 사용된다.

import Link from 'next/link'

function Home() {
  return (
    <ul>
      <li>
        <Link href="/post/abc">Go to pages/post/[pid].js</Link>
      </li>
      <li>
        <Link href="/post/abc?foo=bar">Also goes to pages/post/[pid].js</Link>
      </li>
      <li>
        <Link href="/post/abc/a-comment">
          Go to pages/post/[pid]/[comment].js
        </Link>
      </li>
    </ul>
  )
}

export default Home

이외에도 router.push 같은 메소드를 사용할 수도 있다!





모든 경로 캐치하기 (Catch All Routes)

대괄호 안에 ...를 추가하면 모든 경로를 캐치할 수 있다.

예를 들어, pages/post/[...slug].jspost/a 뿐만 아니라, post/a/b, post/a/b/c, post/a/b/c/d 등 모든 경로를 캐치할 수 있다.
query 객체 안에 slug라는 프로퍼티는 배열이며, 아래와 같은 값을 가진다.

import { useRouter } from "next/router";

const Post = () => {
    const router = useRouter();
    const {slug} = router.query;
    console.log(slug)
    return <h1>Post</h1>
}

export default Post;



image



image





선택적으로 모든 경로 캐치하기 (Optional Catch All Routes)

[[...slug]] 이렇게 더블 브라켓을 사용하면 Catch All 경로를 선택적으로 만들 수 있다.
Catch All과의 다른 점은 매개변수가 없는 경로도 일치하는 점이다.

  • Optional Catch All Routes image



  • Catch All Routes image





Shallow Routing

Shallow Routing은 getServerSideProps, getStaticProps 등의 메소드를 다시 실행해서 새롭게 데이터 패칭을 하지 않으면서 URL을 변경할 수 있도록 한다.
즉, 현재 상태를 잃지 않고 URL만 변경할 수 있다.

URL을 변경하는 방식 몇개를 살펴보자.

  • location.replace('url')

    -pages/my-page/info.js 에 아래와 같이 코드를 작성했다.

import { useRouter } from "next/router";
import { useState } from "react";

const Info = () => {
    const router = useRouter()
    const [clicked, setClicked] = useState(false);
    const {status = 'initial'} = router.query;
    
    return <>
    <h1>Info</h1>
    <h1>Clicked: {String(clicked)}</h1>
    <h1>Status: {status}</h1>
    <button onClick={
        ()=> {
            alert('clicked');
            setClicked(true);
            location.replace('/my-page/info?status=editing')
        }
    
    }>edit(location.replace)</button>
    </>
}

export default Info;

브라우저에서 확인해보자! 👀

  • 버튼을 클릭하면 alert이 뜬다.
  • Clicked: true 로 변경이 된다.
  • URL이 '/my-page/info?status=editing' 로 변경되면서 clicked 값이 false 로 다시 변경된다.

즉, URL을 변경하면서 리렌더가 발생했고, 기존의 state를 유지하지 못했다.


  • router.push(url)
    • pages/my-page/info.js 에 아래와 같이 코드를 작성했다.
import { useRouter } from "next/router";
import { useState } from "react";

const Info = () => {
    const router = useRouter()
    const [clicked, setClicked] = useState(false);
    const {status = 'initial'} = router.query;
    
    return <>
    <h1>Info</h1>
    <h1>Clicked: {String(clicked)}</h1>
    <h1>Status: {status}</h1>
    <button onClick={
        ()=> {
            alert('clicked');
            setClicked(true);
            router.push('/my-page/info?status=editing')
        }
    
    }>edit(router.push)</button>
    </>
}

export default Info;

브라우저에서 확인해보자! 👀

  • 버튼을 클릭하면 alert이 뜬다.
  • Clicked: true 로 변경이 된다.
  • URL이 '/my-page/info?status=editing' 로 변경되어도 clicked true 로 유지된다.

location.replace() 메소드와, router.push() 메소드를 사용하면서 getServerSideProps 함수도 호출해보자!

import { useRouter } from "next/router";
import { useState } from "react";

export async function getServerSideProps() {
    console.log('server-side');
    return {
        props: {}
    }
}

const Info = () => {
    const router = useRouter()
    const [clicked, setClicked] = useState(false);
    const {status = 'initial'} = router.query;
    
    return <>
    <h1>Info</h1>
    <h1>Clicked: {String(clicked)}</h1>
    <h1>Status: {status}</h1>
    

    <button onClick={
        ()=> {
            alert('clicked');
            setClicked(true);
            location.replace('/my-page/info?status=editing')
        }
    
    }>edit(location.replace)</button>

    <button onClick={
        ()=> {
            alert('clicked');
            setClicked(true);
            router.push('/my-page/info?status=editing')
        }
    
    }>edit(router.push)</button>
    </>
}

export default Info;

브라우저에서 확인해보자! 👀

  • 버튼을 클릭하면 alert이 뜬다.
  • Clicked: true 로 변경이 된다.
  • URL이 '/my-page/info?status=editing' 로 변경되어도 clicked true 로 유지된다.
  • IDE의 터미널에 ‘server-side’가 찍혔다.

이는 URL이 변경되면서 getServerSideProps 함수가 실행되면서 새로 데이터를 패칭했다는 것을 의미한다.


  • router.push(url, as, {shallow: true})
    • pages/my-page/info.js 에 아래와 같이 코드를 작성했다.
import { useRouter } from "next/router";
import { useState } from "react";

export async function getServerSideProps() {
    console.log('server-side');
    return {
        props: {}
    }
}

const Info = () => {
    const router = useRouter()
    const [clicked, setClicked] = useState(false);
    const {status = 'initial'} = router.query;
    
    return <>
    <h1>Info</h1>
    <h1>Clicked: {String(clicked)}</h1>
    <h1>Status: {status}</h1>
    

    <button onClick={
        ()=> {
            alert('clicked');
            setClicked(true);
            location.replace('/my-page/info?status=editing')
        }
    
    }>edit(location.replace)</button>

    <button onClick={
        ()=> {
            alert('clicked');
            setClicked(true);
            router.push('/my-page/info?status=editing')
        }
    
    }>edit(router.push)</button>



<button onClick={
        ()=> {
            alert('clicked');
            setClicked(true);
            router.push('/my-page/info?status=editing', undefined, {shallow:true})
        }
    
    }>edit(router.push)</button>
    </>



}

export default Info;

브라우저에서 확인해보자! 👀

  • 버튼을 클릭하면 alert이 뜬다.
  • Clicked: true 로 변경이 된다.
  • URL이 '/my-page/info?status=editing' 로 변경되어도 clicked true 로 유지된다.
  • IDE의 터미널에 아무것도 기록되지 않았다.

즉, 로컬 state와 데이터 패칭이 유지되었다.
따라서, 로컬 state을 유지하고, 데이터 패칭이 새로 발생하지 않으면서, 같은 페이지 내에서 query만 변경하고 싶다면 Shallow Routing을 사용할 수 있겠다.




Next.js 완전 정복 : 확장성 높은 커머스 서비스 구축하기, 최지민 님의 강의를 참고하여 작성한 글입니다.

댓글남기기