[Next.js] Link and Navigate

Haizelยท2024๋…„ 3์›” 19์ผ
post-thumbnail

๐Ÿ“ข Next.js ๊ณต์‹๋ฌธ์„œ๋ฅผ ๋ณด๊ณ  ์ •๋ฆฌํ•œ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค.



  • <Link>๋Š” HTML <a> ํƒœ๊ทธ๋ฅผ ํ™•์žฅํ•˜์—ฌ ๊ฒฝ๋กœ ๊ฐ„ ํ”„๋ฆฌํŒจ์นญ ๋ฐ ํด๋ผ์ด์–ธํŠธ ์ธก ํƒ์ƒ‰์„ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋ณธ ์ปดํฌ๋„ŒํŠธ์ด๋‹ค.
  • Next.js์—์„œ ๊ฒฝ๋กœ ๊ฐ„ ํƒ์ƒ‰ํ•˜๋Š” ๊ธฐ๋ณธ์ ์ด๊ณ  ๊ถŒ์žฅ๋˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.
import Link from 'next/link'
 
export default function Page() {
  return <Link href="/dashboard">Dashboard</Link>
}

๋‹ค์Œ๊ณผ ๊ฐ™์ด next/link๋ฅผ importํ•˜๊ณ  ์ปดํฌ๋„ŒํŠธ์—์„œ href ํ”„๋กœํผํ‹ฐ๋ฅผ ์ „๋‹ฌํ•ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.


PropsDescriptionTypeRequired
href์ด๋™ํ•  ๊ฒฝ๋กœ(URL)string or ObjectO
replace์ƒˆ URL์„ ํ˜„์žฌ ๊ธฐ๋ก์ƒํƒœ๋กœ ๋Œ€์ฒดBooleanX
scroll์Šคํฌ๋กค ์œ„์น˜ ์œ ์ง€BooleanX
prefetch๊ฒฝ๋กœ์™€ ํ•ด๋‹น๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ๊ฐ€์ ธ์™€ ๋กœ๋“œBoolean or nullX

โ‘  href

// string
<Link href="/dashboard">Dashboard</Link> 

// object - Navigate to /about?name=test
<Link
  href={{
    pathname: '/about',
    query: { name: 'test' },
  }}
>
  About
</Link>

โ‘ก replace

  • ๊ธฐ๋ณธ๊ฐ’ false
  • true์ธ ๊ฒฝ์šฐ ๋ธŒ๋ผ์šฐ์ €์˜ ๊ธฐ๋ก ์Šคํƒ์— ์ƒˆ URL์„ ์ถ”๊ฐ€ํ•˜๋Š” ๋Œ€์‹ , ํ˜„์žฌ ๊ธฐ๋ก ์ƒํƒœ๋ฅผ ๋Œ€์ฒดํ•œ๋‹ค.
import Link from 'next/link'
 
export default function Page() {
  return (
    <Link href="/dashboard" replace>
      Dashboard
    </Link>
  )
}

โ‘ข scroll

  • ๊ธฐ๋ณธ๊ฐ’์€ true๋กœ, ์ƒˆ ๊ฒฝ๋กœ์˜ ์ƒ๋‹จ์œผ๋กœ ์Šคํฌ๋กคํ•˜๊ฑฐ๋‚˜ ์•ž๋’ค๋กœ ํƒ์ƒ‰ํ•  ๋•Œ ์Šคํฌ๋กค ์œ„์น˜๋ฅผ ์œ ์ง€ํ•œ๋‹ค.
  • false๋กœ ์„ค์ •ํ•  ๊ฒฝ์šฐ, ๋งํฌ ์ด๋™ ์‹œ ํŽ˜์ด์ง€ ์ƒ๋‹จ์œผ๋กœ ์Šคํฌ๋กค ๋˜์ง€ ์•Š๋Š”๋‹ค.
import Link from 'next/link'
 
export default function Page() {
  return (
    <Link href="/dashboard" scroll={false}>
      Dashboard
    </Link>
  )
}

๐Ÿ’ก ํƒ์ƒ‰ ์‹œ ํŽ˜์ด์ง€๊ฐ€ ๋ทฐํฌํŠธ(vh)์— ํ‘œ์‹œ๋˜์ง€ ์•Š์œผ๋ฉด, Next.js๊ฐ€ ํŽ˜์ด์ง€๋กœ ์Šคํฌ๋กค๋œ๋‹ค.


โ‘ฃ prefetch

  • ํ”„๋ฆฌํŒจ์นญ์€ Link ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์‚ฌ์šฉ์ž์˜ ๋ทฐํฌํŠธ์— ๋“ค์–ด์˜ฌ ๋•Œ(์ฒ˜์Œ ๋˜๋Š” ์Šคํฌ๋กค์„ ํ†ตํ•ด) ๋ฐœ์ƒํ•œ๋‹ค.

  • Next.js๋Š” ํด๋ผ์ด์–ธํŠธ ์ธก ํƒ์ƒ‰์˜ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ค๊ธฐ ์œ„ํ•ด ๋งํฌ๋œ ๊ฒฝ๋กœ(href)์™€ ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋ฏธ๋ฆฌ ๊ฐ€์ ธ์™€ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค.

    ๐Ÿ’ก ์ฃผ์˜ํ•  ์ ์€, ํ”„๋ฆฌํŒจ์นญ์€ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋งŒ ํ™œ์„ฑํ™”๋œ๋‹ค.

  • ๊ธฐ๋ณธ๊ฐ’์€ null์ด๋‹ค.

  • ํ”„๋ฆฌํŒจ์น˜ ๋™์ž‘์€ ๊ฒฝ๋กœ๊ฐ€ ์ •์ ์ธ์ง€, ๋™์ ์ธ์ง€์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง„๋‹ค.
    - ์ •์ ๊ฒฝ๋กœ์ธ ๊ฒฝ์šฐ : ์ „์ฒด ๊ฒฝ๋กœ๊ฐ€ ํ”„๋ฆฌํŒจ์น˜๋œ๋‹ค (๋ชจ๋“  ๋ฐ์ดํ„ฐ ํฌํ•จ)
    - ๋™์ ๊ฒฝ๋กœ์ธ ๊ฒฝ์šฐ : loading.js ๊ฒฝ๊ณ„๊ฐ€ ์žˆ๋Š” ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ์„ธ๊ทธ๋จผํŠธ(ํด๋”)๊นŒ์ง€์˜ ๋ถ€๋ถ„ ๊ฒฝ๋กœ๊ฐ€ ํ”„๋ฆฌํŒจ์น˜๋œ๋‹ค.

import Link from 'next/link'
 
export default function Page() {
  return (
    <Link href="/dashboard" prefetch={false}>
      Dashboard
    </Link>
  )
}
๊ฐ’Description
null์ •์  ๊ฒฝ๋กœ์ผ ๊ฒฝ์šฐ, ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•ด ์ „์ฒด ๊ฒฝ๋กœ๊ฐ€ ํ”„๋ฆฌํŒจ์น˜๋œ๋‹ค.
๋™์  ๊ฒฝ๋กœ์ธ ๊ฒฝ์šฐ, loading.js ์— ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ์„ธ๊ทธ๋จผํŠธ๊นŒ์ง€ ๋ถ€๋ถ„ ๊ฒฝ๋กœ๊ฐ€ ํ”„๋ฆฌํŒจ์น˜๋œ๋‹ค.
true์ •์  ๊ฒฝ๋กœ์™€ ๋™์  ๊ฒฝ๋กœ ๋ชจ๋‘์— ๋Œ€ํ•ด ์ „์ฒด ๊ฒฝ๋กœ๊ฐ€ ํ”„๋ฆฌํŒจ์น˜๋œ๋‹ค.
false๋ทฐํฌํŠธ์— ๋“ค์–ด๊ฐˆ ๋•Œ์™€ ๋งˆ์šฐ์Šค์˜ค๋ฒ„ํ•  ๋•Œ ๋ชจ๋‘ ํ”„๋ฆฌํŒจ์นญ์ด ์ˆ˜ํ–‰๋˜์ง€ ์•Š๋Š”๋‹ค

02. useRouter()


  • useRouter hook์„ ์‚ฌ์šฉํ•˜๋ฉด ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ฒฝ๋กœ๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค.
'use client'
 
import { useRouter } from 'next/navigation'
 
export default function Page() {
  const router = useRouter()
 
  return (
    <button type="button" onClick={() => router.push('/dashboard')}>
      Dashboard
    </button>
  )
}

๐Ÿ“ข Next.js
"์‚ฌ์šฉ ๋ผ์šฐํ„ฐ ์‚ฌ์šฉ์— ๋Œ€ํ•œ ํŠน๋ณ„ํ•œ ์š”๊ตฌ ์‚ฌํ•ญ์ด ์—†๋Š”ํ•œ, useRouter๋ณด๋‹จ Link ์š”์†Œ๋ฅผ ์‚ฌ์šฉํ•ด ๊ฒฝ๋กœ ์ด๋™์„ ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•œ๋‹ค."
โ†’ useRouter์˜ ๊ฒฝ์šฐ use๊ฐ€ ๋ถ™์€ ๋งŒํผ, ์‚ฌ์šฉ์ด ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋กœ ์ œํ•œ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.


โœ๏ธ UseRouter Native History API

  • Next.js์—์„œ๋Š” window.history.pushState ๋ฐ window.history.replaceState ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด ํŽ˜์ด์ง€๋ฅผ ๋‹ค์‹œ ๋กœ๋“œํ•˜์ง€ ์•Š๊ณ ๋„ ๋ธŒ๋ผ์šฐ์ €์˜ ๊ธฐ๋ก ์Šคํƒ์„ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋˜ํ•œ pushState ๋ฐ replaceState ํ˜ธ์ถœ์€ Next.js ๋ผ์šฐํ„ฐ์— ํ†ตํ•ฉ๋˜์–ด ์‚ฌ์šฉ ๊ฒฝ๋กœ์˜ ์ด๋ฆ„ ๋ฐ ์‚ฌ์šฉ ๊ฒ€์ƒ‰ ๋งค๊ฐœ ๋ณ€์ˆ˜์™€ ๋™๊ธฐํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค.

โถ window.history.pushState

  • window.history.pushState๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ธŒ๋ผ์šฐ์ €์˜ ๊ธฐ๋ก ์Šคํƒ์— ์ƒˆ ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ถ”๊ฐ€ action์ด๊ธฐ ๋•Œ๋ฌธ์— ์‚ฌ์šฉ์ž๊ฐ€ ๋’ค๋กœ๊ฐ€๊ธฐ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์ด์ „ ์ƒํƒœ๋กœ ๋Œ์•„๊ฐ„๋‹ค.

ex) ์ƒํ’ˆ ์ •๋ ฌํ•˜๊ธฐ(์˜ค๋ฆ„์ฐจ์ˆœ/๋‚ด๋ฆผ์ฐจ์ˆœ)

'use client'
 
import { useSearchParams } from 'next/navigation'
 
export default function SortProducts() {
  const searchParams = useSearchParams()
 
  function updateSorting(sortOrder: string) {
    const params = new URLSearchParams(searchParams.toString())
    params.set('sort', sortOrder)
    window.history.pushState(null, '', `?${params.toString()}`)
  }
 
  return (
    <>
      <button onClick={() => updateSorting('asc')}>Sort Ascending</button>
      <button onClick={() => updateSorting('desc')}>Sort Descending</button>
    </>
  )
}

โท window.history.replaceState

  • ๋ธŒ๋ผ์šฐ์ €์˜ ๊ธฐ๋ก ์Šคํƒ์—์„œ ํ˜„์žฌ URL๋กœ ์ถ”๊ฐ€๊ฐ€ ์•„๋‹Œ ๋ฐ”๊พธ๊ณ  ์‹ถ์„ ๋•Œ์‚ฌ์šฉํ•œ๋‹ค. ๊ทธ๋Ÿผ ์‚ฌ์šฉ์ž๋Š” ๋’ค๋กœ๊ฐ€๊ธฐ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ๋„ ์ด์ „ ์ƒํƒœ๋กœ ๋Œ์•„๊ฐˆ ์ˆ˜ ์—†๋‹ค.

ex) app์˜ locale ์ „ํ™˜ํ•˜๊ธฐ

'use client'
 
import { usePathname } from 'next/navigation'
 
export function LocaleSwitcher() {
  const pathname = usePathname()
 
  function switchLocale(locale: string) {
    // e.g. '/en/about' or '/fr/contact'
    const newPath = `/${locale}${pathname}`
    window.history.replaceState(null, '', newPath)
  }
 
  return (
    <>
      <button onClick={() => switchLocale('en')}>English</button>
      <button onClick={() => switchLocale('fr')}>French</button>
    </>
  )
}

03. redirect


  • ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„  useRouter๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— redirect์„ ์ด์šฉํ•ด router.pushํ˜น์€ router.replace๋ฅผ ๋Œ€์‹ ํ•ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
import { redirect } from 'next/navigation'
 
async function fetchTeam(id: string) {
  const res = await fetch('https://...')
  if (!res.ok) return undefined
  return res.json()
}
 
export default async function Profile({ params }: { params: { id: string } }) {
  const team = await fetchTeam(params.id)
  if (!team) {
    redirect('/login')
  }
 
  // ...
}

โ‘  redirect ์‚ฌ์šฉ ์‹œ ์ฃผ์˜ํ•  ์ 

  1. redirect๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋ฏ€๋กœ, try-catch ๋ฌธ ์™ธ๋ถ€์—์„œ ํ˜ธ์ถœํ•ด์•ผ ํ•œ๋‹ค.
  2. redirect๋Š” ๋ Œ๋”๋ง ํ”„๋กœ์„ธ์Šค ์ค‘ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์—์„œ๋Š” ํ˜ธ์ถœํ•  ์ˆ˜ ์—†๋‹ค. ์ด๋•Œ์—” useRouter hook์„ ์‚ฌ์šฉํ•œ๋‹ค.
  3. redirect๋Š” ์ ˆ๋Œ€ URL๋„ ํ—ˆ์šฉํ•˜๋ฉฐ ์™ธ๋ถ€ ๋งํฌ๋„ ๋ฆฌ๋””๋ ‰์…˜ํ•˜๋Š”๋ฐ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
  4. ๋ Œ๋”๋ง ํ”„๋กœ์„ธ์Šค ์ „์— ๋ฆฌ๋””๋ ‰์…˜์„ ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, next.config.js ๋˜๋Š” ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

โ‘ก redirect์˜ Type

๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฆฌ๋””๋ ‰์…˜์€ ์„œ๋ฒ„ ์ž‘์—…์—์„œ๋Š” push(๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋ก ์Šคํƒ์— ์ƒˆ ํ•ญ๋ชฉ ์ถ”๊ฐ€)๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ๊ทธ ์™ธ์—๋Š” replace(ํ˜„์žฌ URL์„ ๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋ก ์Šคํƒ์œผ๋กœ ๋ฐ”๊พธ๊ธฐ)๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
์ด๋•Œ type ์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์ง€์ •ํ•ด push/replace๋กœ ์ •์˜ํ•  ์ˆ˜ ์žˆ๋‹ค.

parameterdefault valuedescription
pushserver action์˜ ๊ธฐ๋ณธ๊ฐ’๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋ก ์Šคํƒ์— ์ƒˆ ํ•ญ๋ชฉ ์ถ”๊ฐ€
replace๊ทธ ์™ธ ๊ธฐ๋ณธ ๊ฐ’ํ˜„์žฌ URL์„ ๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋ก ์Šคํƒ์œผ๋กœ ๋ฐ”๊พธ๊ธฐ

โถ Server Component

  • redirect() ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด NEXT_REDIREACT ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๊ณ , ํ•ด๋‹น ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ์„ธ๊ทธ๋จผํŠธ์˜ ๋ Œ๋”๋ง์ด ์ข…๋ฃŒ๋œ๋‹ค.
  • ์ด๋•Œ redirect() ํ•จ์ˆ˜๋Š” ํƒ€์ž„์Šคํฌ๋ฆฝํŠธ์˜ neverํƒ€์ž…์œผ๋กœ ๋ฐ˜ํ™˜ ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค.
import { redirect } from 'next/navigation'
 
async function fetchTeam(id) {
  const res = await fetch('https://...')
  if (!res.ok) return undefined
  return res.json()
}
 
export default async function Profile({ params }) {
  const team = await fetchTeam(params.id)
  if (!team) {
    redirect('/login')
  }
 
  // ...
}

โท Client Component

  • redirect() ํ•จ์ˆ˜๋Š” Server action์„ ํ†ตํ•ด ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋Œ€ํ‘œ์ ์œผ๋กœ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์‚ฌ์šฉํ•ด ์‚ฌ์šฉ์ž๋ฅผ ๋ฆฌ๋””๋ ‰์…˜ํ•ด์•ผ ๊ฒฝ์šฐ, useRouter hook์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
    app/client-redirect.tsx

'use client'
 
import { navigate } from './actions'
 
export function ClientRedirect() {
  return (
    <form action={navigate}>
      <input type="text" name="id" />
      <button>Submit</button>
    </form>
  )
}

app/actions.ts

'use server'
 
import { redirect } from 'next/navigation'
 
export async function navigate(data: FormData) {
  redirect(`/posts/${data.get('id')}`)
}
profile
ํ•œ์ž… ํฌ๊ธฐ๋กœ ๋ฒ ์–ด๋จน๋Š” ๊ฐœ๋ฐœ์ง€์‹ ๐Ÿฐ

0๊ฐœ์˜ ๋Œ“๊ธ€