Next.js에서는 링크를 연결하는데 a 태그 대신에 Link 컴포넌트를 사용한다.
import Link from 'next/link';
//...
export default Page() {
return <Link href="/">홈페이지로 이동</Link>;
}
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라는 속성으로 상태 코드를 정할 수 있다.
/** @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
존재하지 않는 주소로 들어올 경우에 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>
</>
);
}
모든 페이지에 공통적으로 코드를 적용하고 싶다면 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>
);
}
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>
)
}
: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>
</>
);
}