학습 자료

페이지를 옮기는 세 가지 방법


Article

로그인 버튼을 누르면 대시보드로 보내고 싶다. <Link>로는 안 된다. 로그인이 성공했을 때만 옮겨야 하기 때문이다.

이렇게 쓰고 싶어진다.

async function handleSubmit(e) {
  e.preventDefault();
  const ok = await login(e.target);

  if (ok) window.location.href = "/dashboard";
}

동작은 한다. 그런데 브라우저가 페이지를 통째로 다시 받는다. 2강에서 <a> 태그를 쓰지 말라던 이유가 그대로 재현된다.

이동을 시작하는 주체는 셋이다. 사용자가 누르거나, 브라우저의 코드가 부르거나, 서버가 결정한다.

누가 이동을 결정하는가

사용자가 누른다

  • 메뉴 · 목록 · 버튼
  • 미리 받아둔다
  • 대부분 이걸 쓴다

useRouter()

브라우저의 코드가 부른다

  • 제출 성공 후 이동
  • "use client" 필요
  • 누를 곳이 없을 때

redirect()

서버가 결정한다

  • 권한 없음 · 주소 변경
  • 화면을 그리기 전에
  • 자바스크립트가 꺼져도 동작

<Link><a> 태그를 그린다. 다만 클릭을 가로챈다.

클릭했을 때
  1. 클릭브라우저 기본 동작을 막는다
  2. 바뀐 부분만 요청레이아웃은 그대로
  3. history.pushState주소창만 바꾼다
  4. 화면 교체새로고침 없음

주소창은 바뀌었는데 페이지는 다시 받지 않았다. 레이아웃도, 그 안의 상태도 살아 있다. 4강에서 "레이아웃은 다시 렌더링되지 않는다"고 한 것이 이 흐름 위에서 벌어지는 일이다.

넘길 수 있는 값이 몇 개 있다.

prop기본값무엇을 바꾸나
href필수문자열 또는 { pathname, query }
replacefalsetrue 면 뒤로가기 기록을 남기지 않는다
scrolltruefalse 면 스크롤 위치를 유지한다
prefetch자동false 면 미리 받지 않는다

replace는 로그인 페이지에 쓴다. 로그인 후 대시보드에서 뒤로가기를 눌렀는데 다시 로그인 화면이 나오면 이상하기 때문이다.

외부 사이트로 가는 링크는 <Link>를 쓸 이유가 없다. 미리 받아둘 것도, 유지할 상태도 없다. 그냥 <a href="https://…">를 쓴다.

지금 어디 있는지 안다

메뉴에서 현재 페이지를 표시하려면 지금 주소를 알아야 한다. usePathname이 알려준다.

"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";

export default function Nav() {
  const pathname = usePathname();

  return (
    <nav>
      <Link href="/" className={pathname === "/" ? "on" : ""}></Link>
      <Link href="/about" className={pathname === "/about" ? "on" : ""}>
        소개
      </Link>
    </nav>
  );
}

맨 위의 "use client"가 눈에 걸릴 것이다. 훅은 브라우저에서만 돌기 때문이다. 3강에서 서버 컴포넌트가 터미널에 console.log를 찍는 걸 봤다. 그 컴포넌트에는 훅이 없다.

이 경계를 8강과 9강에서 제대로 긋는다. 지금은 use로 시작하는 것을 쓰면 "use client"가 필요하다는 정도로 넘어간다.

돌려주는 것쓰는 곳
usePathname()"/posts/1"활성 메뉴 표시
useParams(){ id: "1" }클라이언트에서 [id] 읽기
useSearchParams()?page=2 읽기필터 · 검색어
useRouter()이동 함수 묶음코드로 이동시킬 때

usePathname()은 물음표 뒤를 빼고 준다. /posts?page=2에서 "/posts"만 나온다.

코드로 이동시킨다

useRouter는 이동 함수를 묶어서 준다.

"use client";

import { useRouter } from "next/navigation";

export default function LoginForm() {
  const router = useRouter();

  async function handleSubmit(e) {
    e.preventDefault();
    const ok = await login(e.target);

    if (ok) router.push("/dashboard");
    else alert("로그인 실패");
  }

  return <form onSubmit={handleSubmit}></form>;
}

window.location.href와 달리 페이지를 다시 받지 않는다. <Link>를 클릭한 것과 똑같이 동작한다.

함수하는 일
router.push("/a")이동하고 기록을 남긴다
router.replace("/a")이동하되 기록을 남기지 않는다
router.back()뒤로
router.forward()앞으로
router.refresh()주소는 그대로, 서버에서 다시 받아온다
router.prefetch("/a")미리 받아둔다

refresh()가 특이하다. 주소를 바꾸지 않고 서버 컴포넌트만 다시 실행해 새 데이터를 받아온다. 클라이언트 컴포넌트의 상태는 살아 있다. 입력하던 폼이 날아가지 않는다.

댓글을 달고 목록을 갱신할 때 쓰던 방법이다. 다만 지금은 더 나은 길이 있다. 11강의 Server Actions와 14강의 캐시 무효화에서 다시 만난다.

잘못 임포트하면 이렇게 터진다. 코드는 멀쩡해 보이는데 에러 메시지가 라우터 얘기를 한다.

콘솔
  • Error: NextRouter was not mounted.
  • https://nextjs.org/docs/messages/next-router-not-mounted

두 라우터의 함수 이름도 미묘하게 다르다. next/router에는 router.pathnamerouter.query가 있었지만, next/navigationrouter에는 없다. 대신 usePathname()useSearchParams()를 따로 부른다. 이동시키는 것과 읽는 것을 분리했다.

물음표 뒤를 읽는 두 가지 방법

5강에서 page.jssearchParams를 props로 받는 걸 봤다. 클라이언트에서는 훅으로 읽는다. 같은 값을 두 경로로 읽는 셈이다.

?page=2 를 읽는 법

await searchParams

서버 컴포넌트

  • page.js 만 받는다
  • 값이 바뀌면 서버가 다시 그린다
  • 데이터를 가져올 때

useSearchParams()

클라이언트 컴포넌트

  • 어느 컴포넌트든 부른다
  • get("page") 로 꺼낸다
  • 입력값을 주소에 반영할 때

검색창처럼 사용자가 타이핑한 값을 주소에 넣고 싶을 때는 훅 쪽을 쓴다.

"use client";

import { useRouter, useSearchParams, usePathname } from "next/navigation";

export default function SearchBox() {
  const router = useRouter();
  const pathname = usePathname();
  const searchParams = useSearchParams();

  function handleChange(e) {
    const params = new URLSearchParams(searchParams);
    params.set("q", e.target.value);

    router.replace(`${pathname}?${params}`);
  }

  return <input onChange={handleChange} defaultValue={searchParams.get("q") ?? ""} />;
}

replace를 쓴 것이 중요하다. push로 하면 글자 하나 칠 때마다 뒤로가기 기록이 쌓인다. 뒤로가기를 열 번 눌러야 원래 페이지로 돌아간다.

주소를 상태 저장소로 쓰는 셈이다. 검색 결과를 그대로 링크로 공유할 수 있고, 새로고침해도 검색어가 남는다. React 편 18강에서 Context로 끌어올리던 상태 중 일부는 이렇게 주소에 두는 게 낫다.

콘솔
  • Error: useSearchParams() should be wrapped in a suspense boundary at page "/search".
import { Suspense } from "react";

export default function Page() {
  return (
    <Suspense fallback={<input disabled />}>
      <SearchBox />
    </Suspense>
  );
}

경계 바깥은 미리 만들어두고, 안쪽만 요청을 기다린다. 15강에서 이 경계를 직접 그으며 스트리밍을 배운다.

서버가 이동시킨다

권한이 없는 사용자를 로그인 페이지로 보내야 한다. 화면을 그려서 보낸 다음 브라우저에서 옮기면 늦다. 그리기 전에 결정한다.

import { redirect } from "next/navigation";

export default async function Dashboard() {
  const user = await getUser();

  if (!user) redirect("/login");

  return <h1>{user.name}</h1>;
}

redirect()를 부르면 그 아래 코드는 실행되지 않는다. 4강의 notFound()와 똑같다. 내부적으로 예외를 던져 렌더링을 중단시키기 때문이다.

let user;

try {
  user = await getUser();
} catch (e) {
  console.error(e);
}

if (!user) redirect("/login");

서버 컴포넌트 · 라우트 핸들러 · Server Actions 어디서든 부를 수 있다. 클라이언트 컴포넌트에서 부르면 안 된다. 거기서는 useRouter()를 쓴다.

함수상태 코드쓰는 곳
redirect("/login")307 임시권한 없음 · 조건부 이동
permanentRedirect("/new")308 영구주소가 아예 바뀌었을 때

차이는 검색엔진이 본다. 307은 원래 주소를 계속 색인하고, 308은 새 주소로 옮긴다. 옛 글의 주소 체계를 바꿨다면 permanentRedirect를 쓴다.

프리페치 — 누르기 전에 이미 받아뒀다

2강에서 던져둔 이야기를 회수한다. <Link>화면에 보이기만 해도 Next.js는 그 페이지를 미리 내려받는다.

클릭하기 전에 벌어지는 일
프리페치 없음
대기클릭 → 요청 → 응답
프리페치
미리 받아둔다클릭 → 즉시
링크가 보임클릭
받아두는 시간은 사용자가 화면을 읽는 동안 흘러간다

개발 서버에서는 이게 꺼져 있다. 매번 컴파일해야 해서 느려지기 때문이다. 프리페치를 확인하려면 npm run build && npm start로 확인한다. 개발 모드에서 링크가 느리다고 뭘 고칠 필요는 없다.

정리하면

  • 이동의 주체는 셋이다. 사용자(<Link>), 브라우저의 코드(useRouter), 서버(redirect).
  • <Link><a>를 그리고 클릭을 가로챈다. 안에 <a>를 또 넣지 않는다.
  • 훅은 전부 **next/navigation**에서 가져온다. next/router는 Pages Router의 것이다.
  • 훅을 쓰는 컴포넌트에는 "use client"가 필요하다. 이 경계는 8·9강에서 긋는다.
  • router.replace()는 뒤로가기 기록을 남기지 않는다. 검색창처럼 자주 바뀌는 주소에 쓴다.
  • useSearchParams()를 쓰면 <Suspense>로 감싸야 빌드가 통과한다.
  • redirect()는 예외를 던진다. try 블록 안에서 부르면 catch가 삼킨다.
  • 영구 이전은 permanentRedirect()다. 307과 308의 차이를 검색엔진이 본다.
  • 프리페치는 자동이고, 개발 모드에서는 꺼져 있다.

다음 강의에서는 주소를 만들지 않는 폴더와, 한 화면에 페이지 둘을 동시에 띄우는 법을 본다. 폴더 이름에 괄호를 치면 규칙이 달라진다.

페이지를 옮기는 세 가지 방법 — 디코드랩(DCODELAB)