Next.js 라우팅

hyemeeny·2024년 9월 23일

Next

목록 보기
2/9
post-thumbnail

Next.js에서는 링크를 연결하는데 a 태그 대신에 Link 컴포넌트를 사용한다.

  • a 태그 : 사용하면 페이지를 이동할 때 페이지 전체를 다시 로딩하기 때문에 속도가 느리고, 빈 화면이 잠깐 보이면서 깜빡거림이 생긴다.
  • Link 컴포넌트 : Next.js에서 내부적으로 여러 가지 최적화를 해주기 때문에 빠르고 부드러운 페이지 전환이 가능하다.
import Link from 'next/link';

//...

export default Page() {
  return <Link href="/">홈페이지로 이동</Link>;
}

📌 useRouter() Hook

📖 쿼리 사용하기

router.query 값을 사용하면 페이지 주소에서 Params 값이나 쿼리스트링 값을 참조할 수 있다.

import { useRouter } from 'next/router';

const router = useRouter();
const { id } = router.query;

예를 들면 pages/products/[id].js 페이지에서 router.query['id'] 값으로 Params id에 해당하는 값을 가져올 수 있다.

// [id].js 페이지
import { useState, useEffect } from "react";
import { useRouter } from "next/router";
import axios from "@/lib/axios";

export default function Product() {
  const [product, setProduct] = useState();
  const [sizeReviews, setSizeReviews] = useState([]);
  const router = useRouter();
  const id = router.query['id']; // { id } = router.query;
  
  // targetId를 파라미터로 리퀘스트를 보낸다
  async function getProduct(targetId) {
    const res = await axios.get(`/products/${targetId}`);
    const nextProduct = res.data;
    setProduct(nextProduct);
  }

  async function getSizeReviews(targetId) {
    const res = await axios.get(`/size_reviews/?product_id=${targetId}`);
    // 값이 없을 수도 있으니 빈 배열로 처리 []
    const nextSizeReviews = res.data.results ?? [];
    setSizeReviews(nextSizeReviews);
  }
  
  // 렌더링이 끝난 후에 비동기적으로 실행
  useEffect(() => {
    if (!id) return;

    getProduct(id);
    getSizeReviews(id);
  }, [id]); // id값이 바뀔때마다 실행

  // 맨 처음 product 값이 없으므로 렌더링하지 않는다
  if (!product) return null;

  return <>Product #{id} 페이지</>;
}

/search?q=티셔츠와 같은 주소로 들어왔을 때 router.query['q'] 값으로 쿼리스트링 q에 해당하는 값을 가져올 수 있다.

// search 컴포넌트
import { useState, useEffect } from "react";
import { useRouter } from "next/router";
import axios from "@/lib/axios";

export default function Search() {
  const router = useRouter();
  const q = router.query['q']; // { q } = router.query;
  
  async function getProducts(query) {
    const res = await axios.get(`/products?q=${query}`);
    const nextProducts = res.data.results;
    setProducts(nextProducts);
  }

  useEffect(() => {
    getProducts(q);
  }, [q]);

  return (
    <div>
      <SearchForm initialValue={q} />
      <h2 className={styles.title}>
        <span className={styles.keyword}>{q}</span> 검색 결과
      </h2>
      <ProductList className={styles.productList} products={products} />
    </div>
  );
}

📖 페이지 이동하기

router.push() 함수를 사용하면 코드로 페이지를 이동할 수 있다.

// SearchForm 컴포넌트
import { useState } from 'react';
import { useRouter } from 'next/router';

export default function SearchForm({ initialValue = "" }) {
  const [value, setValue] = useState(initialValue);
  const router = useRouter();

  function handleChange(e) {
    setValue(e.target.value);
  }

  function handleSubmit(e) {
    e.preventDefault();
    // 검색어가 없으면 홈페이지로 이동
    if (!value) {
      return router.push('/');
    }
    
    // 검색어가 있으면 해당 페이지로 이동
    return router.push(`/search?q=${value}`);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="q" value={value} onChange={handleChange} />
      <button>검색</button>
    </form>
  );
}

📌 리다이렉트

next.config.js 파일을 수정하면 특정 주소에 대해서 리다이렉트할 주소를 지정할 수 있다. 예를 들어서 /products/:id라는 주소로 들어오면 /items/:id라는 주소로 이동시켜 줄 수 있다.

이때 permanent라는 속성으로 상태 코드를 정할 수 있다.

  • permanent: false로 하면 307 Temporary Redirect를 하고,
  • permanent: true로 하면 브라우저에 리다이렉트 정보를 저장하는 308 Permanent Redirect를 할 수 있다.
/** @type {import('next').NextConfig} */
const nextConfig = {
  async redirects() {
    return [
      {
        // :id => param의 이름
        source: '/products/:id', // 리다이렉트 처리할 주소
        destination: '/items/:id', // 이동시킬 주소
        permanent: true,
      },
    ];
  },
}

module.exports = nextConfig;

💡 공식문서 참고
https://nextjs.org/docs/app/api-reference/next-config-js/redirects

📌 커스텀 404 페이지

존재하지 않는 주소로 들어올 경우에 Next.js에서는 기본적으로 404 페이지를 보여준다. 내가 원하는 404 페이지를 보여주려면 pages/404.js 파일을 만들고 일반적인 페이지처럼 구현하면 된다.

// 404.js 페이지
import ButtonLink from "@/components/ButtonLink";
import styles from "@/styles/NotFound.module.css";

export default function NotFound() {
  return (
    <>
      <div className={styles.notFound}>
        <div className={styles.content}>
          찾을 수 없는 페이지입니다.
          <br />
          요청하신 페이지가 사라졌거나, 잘못된 경로를 이용하셨어요 :)
        </div>
        <ButtonLink className={styles.button} href="/">
          홈으로 이동
        </ButtonLink>
      </div>
    </>
  );
}

📌 커스텀 App

모든 페이지에 공통적으로 코드를 적용하고 싶다면 pages/_app.js 파일에 있는 커스텀 App 컴포넌트를 수정하면 된다. 이 컴포넌트에 사이트 전체에서 보여 줄 컴포넌트나 전체적으로 적용할 리액트 컨텍스트를 적용할 수 있다. 그리고 Theme와 같이 사이트 전체에 적용할 CSS 파일도 여기서 import할 수 있다.

커스텀 App 컴포넌트의 Props는 Component와 pageProps가 있는데, 우리가 만든 페이지들이 Component Prop으로 전달되고 여기에 내부적으로 필요한 Props는 pageProps라는 값으로 전달된다.

// _app 컴포넌트 
import Header from '@/components/Header';
import { ThemeProvider } from '@/lib/ThemeContext';
import '@/styles/globals.css';

// Component Prop은 Next.js 페이지
// 컴포넌트로 렌더링하고 pageProps를 내려준다
export default function App({ Component, pageProps }) {
  return (
    <ThemeProvider>
      {/* 공통된 컴포넌트 구현 Header, Container */}
      <Header />
      <Component {...pageProps} />
    </ThemeProvider>
  );
}

📌 커스텀 Document

pages/_document.js 파일에 있는 Document 컴포넌트는 HTML 코드의 뼈대를 수정하는 용도로 사용한다. 코드는 React 컴포넌트이지만 일반적인 컴포넌트처럼 동작하지는 않기 때문에 useState나 useEffect처럼 브라우저에서 실행이 필요한 기능들은 사용할 수 없다.

import { Html, Head, Main, NextScript } from 'next/document'

// Next.js에서는 서버에서 렌더링할 때만 Document 컴포넌트 실행하고 
// 클라이언트 사이드에서는 이 컴포넌트를 실행하지 않는다
export default function Document() {
  return (
    <Html lang="ko">
      <Head />
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}

📌 Context 활용

:global(...) 이라는 CSS Modules 라이브러리의 문법을 사용해서 global.css에서 정의한 클래스를 사용한다.

:global(.light) .header {
  border-color: #e1e1e1;
}

ThemeContext.js 파일에는 테마값('light' 또는 'dark')과 이 값을 수정하는 함수인 setTheme() 함수를 객체 형태({ theme, setTheme })로 내려 주는 ThemeProvider 컴포넌트가 있다. 그리고 useTheme()라는 커스텀 훅을 사용하여 ThemeContext에서 내려 주는 theme 값과 setTheme() 함수를 사용한다.

// ThemeContext.js 파일
import { createContext, useContext, useEffect, useState } from "react";

export const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("dark");

  useEffect(() => {
    document.body.classList.add(theme);

    return () => {
      document.body.classList.remove(theme);
    };
  }, [theme]);

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const themeContext = useContext(ThemeContext);
  if (!themeContext) {
    throw new Error("ThemeContext 안에서 써야 합니다");
  }

  return themeContext;
}

ThemeProvider 컴포넌트를 사이트 전체에 컨텍스트를 적용하려면 커스텀 App 컴포넌트로 적용한다.

// _app 컴포넌트
import Container from '@/components/Container';
import Header from '@/components/Header';
import { ThemeProvider } from '@/lib/ThemeContext';
import '@/styles/globals.css';

export default function App({ Component, pageProps }) {
  return (
    <ThemeProvider>
      <Header />
      <Container page>
        <Component {...pageProps} />
      </Container>
    </ThemeProvider>
  );
}

pages/settings.js 파일에서 useTheme() 훅을 사용해서 theme 값과 setTheme() 함수를 사용한다.

// Setting 컴포넌트
import Dropdown from '@/components/Dropdown';
import { useTheme } from '@/lib/ThemeContext';
import styles from '@/styles/Setting.module.css';

export default function Setting() {
  const { theme, setTheme } = useTheme();
  
  function handleDropdownChange(name, value) {
    const nextTheme = value;
    setTheme(nextTheme);
  }

  return (
    <>
      <h1 className={styles.title}>설정</h1>
      <section className={styles.section}>
        <h2 className={styles.sectionTitle}>테마 설정</h2>
        <Dropdown
          className={styles.dropdown}
          name="theme"
          value={theme}
          options={[
            { label: '다크', value: 'dark' },
            { label: '라이트', value: 'light' },
          ]}
          onChange={handleDropdownChange}
        />
      </section>
    </>
  );
}

0개의 댓글