라우팅이란, 어떤 주소에 어떤 페이지를 보여줄지 정하는 것을 의미한다. 파일시스템 기반 라우팅은 파일의 경로가 주소에 매칭되는 라우팅 방식이다.
pages 폴더 안의 파일들이 곧 페이지가 된다. pages 폴더명은 Next.js에서 지정한 폴더명이기 때문에 변경해서는 안된다. 다이나믹 라우팅을 사용하고 싶을 때는 파일명(폴더명)에 [id].js 형태로 작성하면 된다.
import Link from 'next/link';
export default function Home() {
return (
<>
<ul>
<li>
<Link href="/products/1">첫 번째 상품</Link>
</li>
<li>
<Link href="/products/2">두 번째 상품</Link>
</li>
<li>
<Link href="/products/3">세 번째 상품</Link>
</li>
<li>
<Link href="https://naver.com">네이버</Link>
</li>
</ul>
</>
)
}
// [id].js
import { useRouter } from 'next/router';
export default function Product() {
const router = useRouter();
// 파일명의 id
const { id } = router.query;
return <div>Product 페이지</div>;
}
// search.js
import { useRouter } from 'next/router';
export default function Search() {
const router = useRouter();
// /q?=값
const { q } = router.query;
return (
<div>
<h1>Search 페이지</h1>
<h2>{q} 검색 결과</h2>
</div>
);
}
// pages 폴더 밖에 폴더 생성
// components/SearchForm.js
import ( useRouter } from 'next/router';
import { useState } from 'react';
export default function SearchForm({ initialValue = '' }) {
const router = useRouter();
const [value, setValue] = useState('');
function handleChange(e) {
setValue(e.target.value);
};
function handleSubmit(e) {
e.preventDefault();
if (!value) {
router.push('/');
return;
}
router.push(`/search?q=${value}`);
};
return (
<form onSubmit={handleSubmit}>
<input name='q' value={value} onChange={handleChange} />
<button>검색</button>
</form>
);
}
import { useEffect, useState } from 'react';
import MovieList from '@/components/MovieList';
import SearchForm from '@/components/SearchForm';
import styles from '@/styles/Home.module.css';
import Header from '@/components/Header';
import Container from '@/components/Container';
import axios from '@/lib/axios';
export default function Home() {
const [movies, setMovies] = useState([]);
async function getMovies() {
const res = await axios.get('/movies/');
const movies = res.data.results ?? [];
setMovies(movies);
}
useEffect(() => {
getMovies();
}, []);
return (
<>
<Header />
<Container page>
<SearchForm />
<MovieList className={styles.movieList} movies={movies} />
</Container>
</>
);
}
next.config.js 파일은 Next.js 서버를 설정하는 파일이다.
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
async redirects() {
return [
{
source: "/products/:id", // 바꾸기 전 주소
destination: "/items/:id", // 바뀐 주소
permanent: true, // 리다이렉트를 저장할 지 결정
},
]
},
}
module.exports = nextConfig;
import ButtonLink from "@/components/ButtonLink";
import Container from "@/components/Container";
import Header from "@/components/Header";
import styles from "@/styles/NotFound.module.css";
export default function NotFound() {
return (
<>
<Header />
<Container>
<div className={styles.notFound}>
<div className={styles.content}>
찾을 수 없는 페이지입니다.
<br />
요청하신 페이지가 사라졌거나, 잘못된 경로를 이용하셨어요 :)
</div>
<ButtonLink className={styles.button} href="/">
홈으로 이동
</ButtonLink>
</div>
</Container>
</>
);
}
// _app.js 파일은 공통된 레이아웃을 구현할 때 작성하면 된다.
import Container from "@/components/Container";
import Header from "@/components/Header";
import "@/styles/global.css";
export default function App({ Component, pageProps }) {
return (
<>
<Header />
<Container>
<Component {...pageProps} />
</Container>
</>
);
}
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;
}
Next.js에서는 페이지를 만드는 것처럼 간단하게 백엔드 API를 만들 수 있다. 사실상 작은 Node.js 서버를 구현할 수 있는 것이다. 우선 /pages 폴더 아래에 /api라는 폴더를 만들고, 특별한 형태의 자바스크립트 파일을 추가하면 된다.
// /pages/api/cart/js
let cart = [];
export default function handler(req, res) {
if (req.method === 'GET') {
return res.status(200).json(cart);
} else if (req.method === 'PUT') {
cart = req.body;
return res.status(200).json(cart);
} else {
return res.sendStatus(404);
}
}
default export로 request 객체(req)와 response 객체(res)를 파라미터로 받는 함수를 만들면 된다. request 객체와 response 객체는 Node.js의 request 객체와 response 객체이다.
GET request를 보냈을 때 cart 배열을 response로 보내 주고, PUT request를 보냈을 때 cart 배열을 수정하는 코드이다. 이 API의 주소는 Next.js에서 페이지를 만들었을 때의 주소와 마찬가지이다. /api/cart.js라는 경로이니까 /api/cart라는 주소로 request를 보내면 파일에 있는 핸들러 함수를 실행해서 response를 보내주는 형태이다.
웹 브라우저에서 http://localhost:3000/api/cart라는 주소로 접속하거나, API 테스트를 해보면 아래와 같은 JSON 데이터가 response로 전달될 것이다.
GET http://localhost:3000/api/cart
Content-Type: application/json
GET request를 보내고 받은 response 예시
[]
PUT request 보낼 때
PUT http://localhost:3000/api/cart
Content-Type: application/json
[1, 2, 3]
PUT request를 보내고 받은 response 예시
[1, 2, 3]
requset 객체를 활용하면 requset의 헤더나 쿠키 같은 값을 사용해서 다양한 동작을 하도록 만들 수 있다. 더 궁금한 점이 있다면 Next.js - API Routes를 참고해 보면 좋다.
<Link> 컴포넌트Next.js에서는 링크를 연결하는데 <a> 태그 대신에 <Link> 컴포넌트를 사용한다. <a> 태그를 사용하면 페이지를 이동할 때 페이지 전체를 다시 로딩하기 때문에 속도가 느리고, 빈 화면이 잠깐 보이면서 깜빡거림이 생기지만, <Link> 컴포넌트는 Next.js에서 내부적으로 여러 가지 최적화를 해주기 때문에 빠르고 부드러운 페이지 전환이 가능하다.
import Link from 'next/link';
export default Page() {
return <Link href="/">홈페이지로 이동</Link>;
}
router.query 값을 사용하면 페이지 주소에서 Params 값이나 쿼리스트링 값을 참조할 수 있다. 예를 들면 pages/products/[id].js 페이지에서 router.query['id'] 값으로 Params id에 해당하는 값을 가져올 수 있다.
import { useRouter } from 'next/router';
export default function Product() {
const router = useRouter();
const id = router.query['id'];
return <>Product #{id} 페이지</>;
}
/search?q=티셔츠와 같은 주소로 들어왔을 때, router.query['q'] 값으로 쿼리스트링 q에 해당하는 값을 가져올 수도 있다.
import { useRouter } from 'next/router';
export default function Search() {
const router = useRouter();
const q = router.query['q'];
return <>{q} 검색 결과</>;
}
router.push() 함수를 사용하면 코드로 페이지를 이동할 수 있다.
import { useState } from 'react';
import { useRouter } from 'next/router';
export default function SearchForm() {
const [value, setValue] = useState();
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 [
{
source: '/products/:id',
destination: '/items/:id',
permanent: true,
},
];
},
}
module.exports = nextConfig;
존재하지 않는 주소로 들어올 경우에 Next.js에서는 기본적으로 404 페이지를 보여준다. 원하는 404 페이지를 보여주려면, pages.NotFound.js 파일을 만들고 일반적인 페이지처럼 구현하면 된다.
모든 페이지에 공통적으로 코드를 적용하고 싶다면 커스텀 App 컴포넌트를 수정하면 된다. pages/_app.js 파일에 있는 컴포넌트이다. 이 컴포넌트에 사이트 전체에서 보여줄 컴포넌트나 전체적으로 적용할 리액트 컨텍스트를 적용할 수 있다. 그리고 사이트 전체에 적용할 CSS 파일도 여기서 임포트할 수 있다.
커스텀 App 컴포넌트의 Props는 Component와 pageProps가 있다. 우리가 만든 페이지들이 Component Prop으로 전달되고 여기에 내부적으로 필요한 Props는 pageProps라는 값으로 전달된다.
import Header from '@/components/Header';
import { ThemeProvider } from '@/lib/ThemeContext';
import '@/styles/globals.css';
export default function App({ Component, pageProps }) {
return (
<ThemeProvider>
<Header />
<Component {...pageProps} />
</ThemeProvider>
);
}
pages/_document.js 파일에 있는 Document 컴포넌트는 HTML 코드의 뼈대를 수정하는 용도로 사용한다. 코드는 React 컴포넌트이지만 일반적인 컴포넌트처럼 동작하지 않기 때문에 useState나 useEffect처럼 브라우저에서 실행이 필요한 기능들은 사용할 수 없다.
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html lang="ko">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}