πŸŽ‰ berenickt λΈ”λ‘œκ·Έμ— 온 κ±Έ ν™˜μ˜ν•©λ‹ˆλ‹€. πŸŽ‰
Front
NextJs
10-Parallel Routes

Parallel Routes

병렬 경둜(Parallel Routes)λ₯Ό μ‚¬μš©ν•˜λ©΄ λ™μΌν•œ λ ˆμ΄μ•„μ›ƒ λ‚΄μ—μ„œ ν•˜λ‚˜ μ΄μƒμ˜ νŽ˜μ΄μ§€λ₯Ό λ™μ‹œμ— λ˜λŠ” μ‘°κ±΄λΆ€λ‘œ λ Œλ”λ§ν•  수 μžˆμŠ΅λ‹ˆλ‹€. λŒ€μ‹œλ³΄λ“œλ‚˜ μ†Œμ…œ μ‚¬μ΄νŠΈμ˜ ν”Όλ“œμ™€ 같이 μ•±μ˜ 맀우 동적인 μ„Ήμ…˜μ— μœ μš©ν•©λ‹ˆλ‹€.

예λ₯Ό λ“€μ–΄, λŒ€μ‹œλ³΄λ“œλ₯Ό κ³ λ €ν•  λ•Œ, 병렬 경둜λ₯Ό μ‚¬μš©ν•˜μ—¬ team νŽ˜μ΄μ§€μ™€ analytics νŽ˜μ΄μ§€λ₯Ό λ™μ‹œμ— λ Œλ”λ§ν•  수 μžˆμŠ΅λ‹ˆλ‹€:

Parallel Routes


1. Slots(슬둯; 자리)

병렬 κ²½λ‘œλŠ” λͺ…λͺ…λœ Slots을 μ‚¬μš©ν•˜μ—¬ λ§Œλ“€μ–΄μ§‘λ‹ˆλ‹€. Slots은 @folder κ·œμΉ™μœΌλ‘œ μ •μ˜λ©λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, λ‹€μŒ 파일 κ΅¬μ‘°λŠ” 두 개의 Slots을 μ •μ˜ν•©λ‹ˆλ‹€: @analyticsκ³Ό @team:

Slots

Slots은 곡유 λΆ€λͺ¨ λ ˆμ΄μ•„μ›ƒμ— props으둜 μ „λ‹¬λ©λ‹ˆλ‹€. μœ„μ˜ μ˜ˆμ œμ—μ„œ, app/layout.js의 μ»΄ν¬λ„ŒνŠΈλŠ” 이제 @analytics 및 @team 슬둯 props을 ν—ˆμš©ν•˜λ©°, ν•˜μœ„ propsκ³Ό ν•¨κ»˜ λ³‘λ ¬λ‘œ λ Œλ”λ§ν•  수 μžˆμŠ΅λ‹ˆλ‹€:

app/layout.tsx
1
export default function Layout({
2
children,
3
team,
4
analytics,
5
}: {
6
children: React.ReactNode
7
analytics: React.ReactNode
8
team: React.ReactNode
9
}) {
10
return (
11
<>
12
{children}
13
{team}
14
{analytics}
15
</>
16
)
17
}

κ·ΈλŸ¬λ‚˜ Slots은 경둜 μ„Έκ·Έλ¨ΌνŠΈκ°€ μ•„λ‹ˆλ©° URL ꡬ쑰에 영ν–₯을 λ―ΈμΉ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, /dashboard/@analytics/views의 경우, @analyticsκ°€ Slotsμ΄λ―€λ‘œ URL은 /dashboard/viewsκ°€ λ©λ‹ˆλ‹€.

μ•Œμ•„λ‘λ©΄ μœ μš©ν•©λ‹ˆλ‹€:

μžμ‹ ν”„λ‘œνΌν‹°λŠ” 폴더에 λ§€ν•‘ν•  ν•„μš”κ°€ μ—†λŠ” μ•”μ‹œμ  Slotsμž…λ‹ˆλ‹€. 즉, app/page.jsλŠ” app/@children/page.js와 λ™μΌν•©λ‹ˆλ‹€.


2. Active state and navigation

기본적으둜, Next.jsλŠ” 각 slot의 ν™œμ„± μƒνƒœ(λ˜λŠ” ν•˜μœ„ νŽ˜μ΄μ§€)λ₯Ό μΆ”μ ν•©λ‹ˆλ‹€. κ·ΈλŸ¬λ‚˜, 슬둯 λ‚΄μ—μ„œ λ Œλ”λ§λ˜λŠ” μ½˜ν…μΈ λŠ” 탐색 μœ ν˜•μ— 따라 λ‹¬λΌμ§‘λ‹ˆλ‹€:

  • μ†Œν”„νŠΈ 탐색(soft navigation):
    • ν΄λΌμ΄μ–ΈνŠΈ μΈ‘ 탐색 쀑에 Next.jsλŠ” λΆ€λΆ„ λ Œλ”λ§μ„ μˆ˜ν–‰ν•˜μ—¬ 슬둯 λ‚΄μ˜ ν•˜μœ„ νŽ˜μ΄μ§€λ₯Ό λ³€κ²½ν•˜λŠ” λ™μ‹œμ—,
    • λ‹€λ₯Έ 슬둯의 ν™œμ„± ν•˜μœ„ νŽ˜μ΄μ§€λŠ” ν˜„μž¬ URLκ³Ό μΌμΉ˜ν•˜μ§€ μ•Šλ”λΌλ„ μœ μ§€ν•©λ‹ˆλ‹€.
  • ν•˜λ“œ 탐색(hard navigation):
    • 전체 νŽ˜μ΄μ§€ λ‘œλ“œ(λΈŒλΌμš°μ € μƒˆλ‘œ κ³ μΉ¨) ν›„ Next.jsλŠ” ν˜„μž¬ URLκ³Ό μΌμΉ˜ν•˜μ§€ μ•ŠλŠ” 슬둯의 ν™œμ„± μƒνƒœλ₯Ό 확인할 수 μ—†μŠ΅λ‹ˆλ‹€.
    • λŒ€μ‹  μΌμΉ˜ν•˜μ§€ μ•ŠλŠ” μŠ¬λ‘―μ— λŒ€ν•΄ default.js νŒŒμΌμ„ λ Œλ”λ§ν•˜κ±°λ‚˜, default.jsκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우, 404λ₯Ό λ Œλ”λ§ν•©λ‹ˆλ‹€.

μ•Œμ•„λ‘λ©΄ μœ μš©ν•©λ‹ˆλ‹€:

μΌμΉ˜ν•˜μ§€ μ•ŠλŠ” κ²½λ‘œμ— λŒ€ν•œ 404λŠ” μ˜λ„ν•˜μ§€ μ•Šμ€ νŽ˜μ΄μ§€μ— μ‹€μˆ˜λ‘œ 병렬 κ²½λ‘œκ°€ λ Œλ”λ§λ˜λŠ” 것을 λ°©μ§€ν•˜λŠ” 데 도움이 λ©λ‹ˆλ‹€.


2.1 default.js

초기 λ‘œλ“œ λ˜λŠ” 전체 νŽ˜μ΄μ§€ λ‹€μ‹œ λ‘œλ“œ 쀑에 μΌμΉ˜ν•˜μ§€ μ•ŠλŠ” slot에 λŒ€ν•œ 폴백으둜 λ Œλ”λ§ν•  default.js νŒŒμΌμ„ μ •μ˜ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

λ‹€μŒ 폴더 ꡬ쑰λ₯Ό κ³ λ €ν•˜μ„Έμš”. @team μŠ¬λ‘―μ—λŠ” /settings νŽ˜μ΄μ§€κ°€ μžˆμ§€λ§Œ, @analyticsμ—λŠ” μ„€μ • νŽ˜μ΄μ§€κ°€ μ—†μŠ΅λ‹ˆλ‹€.

default.js

/dashboard/settings으둜 이동할 λ•Œ, @team slot은 @analytics slot의 ν˜„μž¬ ν™œμ„± νŽ˜μ΄μ§€λ₯Ό μœ μ§€ν•˜λ©΄μ„œ, /settings νŽ˜μ΄μ§€λ₯Ό λ Œλ”λ§ν•©λ‹ˆλ‹€.

μƒˆλ‘œ κ³ μΉ¨ μ‹œ, Next.jsλŠ” @analytics에 λŒ€ν•œ default.jsλ₯Ό λ Œλ”λ§ν•©λ‹ˆλ‹€. default.jsκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμœΌλ©΄ 404κ°€ λŒ€μ‹  λ Œλ”λ§λ©λ‹ˆλ‹€.

λ˜ν•œ childrenλŠ” μ•”μ‹œμ  slotμ΄λ―€λ‘œ, Next.jsκ°€ μƒμœ„ νŽ˜μ΄μ§€μ˜ ν™œμ„± μƒνƒœλ₯Ό 볡ꡬ할 수 μ—†λŠ” 경우, ν•˜μœ„ νŽ˜μ΄μ§€μ— λŒ€ν•œ 폴백을 λ Œλ”λ§ν•˜λŠ” default.js νŒŒμΌλ„ λ§Œλ“€μ–΄μ•Ό ν•©λ‹ˆλ‹€.


2.2 useSelectedLayoutSegment(s)

useSelectedLayoutSegment 와 useSelectedLayoutSegments λŠ” λͺ¨λ‘ 슬둯 λ‚΄μ—μ„œ ν™œμ„± 경둜 μ„Έκ·Έλ¨ΌνŠΈλ₯Ό 읽을 수 μžˆλŠ” parallelRoutesKey λ§€κ°œλ³€μˆ˜λ₯Ό ν—ˆμš©ν•©λ‹ˆλ‹€.

app/layout.tsx
1
'use client'
2
3
import { useSelectedLayoutSegment } from 'next/navigation'
4
5
export default function Layout({ auth }: { auth: React.ReactNode }) {
6
const loginSegments = useSelectedLayoutSegment('auth')
7
// ...
8
}

μ‚¬μš©μžκ°€ app/@auth/login(λ˜λŠ” URL ν‘œμ‹œμ€„μ˜ /login)으둜 μ΄λ™ν•˜λ©΄ loginSegmentsλŠ” "login" λ¬Έμžμ—΄κ³Ό λ™μΌν•©λ‹ˆλ‹€.


3. μ˜ˆμ‹œ

3.1 Conditional(쑰건뢀) Routes

병렬 경둜λ₯Ό μ‚¬μš©ν•˜μ—¬ μ‚¬μš©μž μ—­ν• κ³Ό 같은 νŠΉμ • 쑰건에 따라 μ‘°κ±΄λΆ€λ‘œ 경둜λ₯Ό λ Œλ”λ§ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, /admin λ˜λŠ” /user 역할에 λŒ€ν•΄ λ‹€λ₯Έ λŒ€μ‹œλ³΄λ“œ νŽ˜μ΄μ§€λ₯Ό λ Œλ”λ§ν•  수 μžˆμŠ΅λ‹ˆλ‹€:

Conditional Routes

app/dashboard/layout.tsx
1
import { checkUserRole } from '@/lib/auth'
2
3
export default function Layout({ user, admin }: { user: React.ReactNode; admin: React.ReactNode }) {
4
const role = checkUserRole()
5
return <>{role === 'admin' ? admin : user}</>
6
}

3.2 Tab Groups

슬둯 μ•ˆμ— layout을 μΆ”κ°€ν•˜μ—¬, μ‚¬μš©μžκ°€ μŠ¬λ‘―μ„ λ…λ¦½μ μœΌλ‘œ 탐색할 수 μžˆλ„λ‘ ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이 κΈ°λŠ₯은 탭을 λ§Œλ“€ λ•Œ μœ μš©ν•©λ‹ˆλ‹€.

예λ₯Ό λ“€μ–΄, @analytics μŠ¬λ‘―μ—λŠ” 두 개의 ν•˜μœ„ νŽ˜μ΄μ§€κ°€ μžˆμŠ΅λ‹ˆλ‹€: /page-views 및 /visitors.

Tab Groups

@analytics λ‚΄μ—μ„œ, layout νŒŒμΌμ„ λ§Œλ“€μ–΄ 두 νŽ˜μ΄μ§€ 간에 탭을 κ³΅μœ ν•©λ‹ˆλ‹€:

app/dashboard/@analytics/layout.tsx
1
import Link from 'next/link'
2
3
export default function Layout({ children }: { children: React.ReactNode }) {
4
return (
5
<>
6
<nav>
7
<Link href="/dashboard/page-views">Page Views</Link>
8
<Link href="/dashboard/visitors">Visitors</Link>
9
</nav>
10
<div>{children}</div>
11
</>
12
)
13
}

3.3 Modal

병렬 경둜λ₯Ό μΈν„°μ…‰νŒ… κ²½λ‘œμ™€ ν•¨κ»˜ μ‚¬μš©ν•˜μ—¬ λͺ¨λ‹¬μ„ λ§Œλ“€ 수 μžˆμŠ΅λ‹ˆλ‹€. 이λ₯Ό 톡해 λͺ¨λ‹¬μ„ λ§Œλ“€ λ•Œ λ‹€μŒκ³Ό 같은 일반적인 문제λ₯Ό ν•΄κ²°ν•  수 μžˆμŠ΅λ‹ˆλ‹€:

  • URL을 톡해 λͺ¨λ‹¬ μ½˜ν…μΈ λ₯Ό κ³΅μœ ν•  수 μžˆλ„λ‘ μ„€μ •ν•©λ‹ˆλ‹€.
  • νŽ˜μ΄μ§€λ₯Ό μƒˆλ‘œ κ³ μΉ  λ•Œ λͺ¨λ‹¬μ„ λ‹«λŠ” λŒ€μ‹  μ»¨ν…μŠ€νŠΈλ₯Ό μœ μ§€ν•©λ‹ˆλ‹€.
  • 이전 경둜둜 μ΄λ™ν•˜μ§€ μ•Šκ³  λ’€λ‘œ 탐색할 λ•Œ λͺ¨λ‹¬μ„ λ‹«μŠ΅λ‹ˆλ‹€.
  • μ•žμœΌλ‘œ νƒμƒ‰μ—μ„œ λͺ¨λ‹¬μ„ λ‹€μ‹œ μ—½λ‹ˆλ‹€.

μ‚¬μš©μžκ°€ ν΄λΌμ΄μ–ΈνŠΈ μΈ‘ 탐색을 μ‚¬μš©ν•˜μ—¬ λ ˆμ΄μ•„μ›ƒμ—μ„œ 둜그인 λͺ¨λ‹¬μ„ μ—΄κ±°λ‚˜ λ³„λ„μ˜ /login νŽ˜μ΄μ§€μ— μ•‘μ„ΈμŠ€ν•  수 μžˆλŠ”, λ‹€μŒ UI νŒ¨ν„΄μ„ κ³ λ €ν•˜μ„Έμš”:

Modal

이 νŒ¨ν„΄μ„ κ΅¬ν˜„ν•˜λ €λ©΄, λ¨Όμ € κΈ°λ³Έ 둜그인 νŽ˜μ΄μ§€λ₯Ό λ Œλ”λ§ν•˜λŠ” /login 경둜λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.

Modal-2

app/login/page.tsx
1
import { Login } from '@/app/ui/login'
2
3
export default function Page() {
4
return <Login />
5
}

그런 λ‹€μŒ @auth μŠ¬λ‘―μ—, null을 λ°˜ν™˜ν•˜λŠ” default.js νŒŒμΌμ„ μΆ”κ°€ν•©λ‹ˆλ‹€. μ΄λ ‡κ²Œ ν•˜λ©΄ λͺ¨λ‹¬μ΄ ν™œμ„±ν™”λ˜μ–΄ μžˆμ§€ μ•Šμ„ λ•Œ λ Œλ”λ§λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

app/@auth/default.tsx
1
export default function Default() {
2
return null
3
}

@auth 슬둯 λ‚΄μ—μ„œ /(.)login 폴더λ₯Ό μ—…λ°μ΄νŠΈν•˜μ—¬ /login 경둜λ₯Ό κ°€λ‘œμ±„μ„Έμš”. <Modal> μ»΄ν¬λ„ŒνŠΈμ™€ κ·Έ ν•˜μœ„ μ»΄ν¬λ„ŒνŠΈλ₯Ό /(.)login/page.tsx 파일둜 κ°€μ Έμ˜΅λ‹ˆλ‹€:

app/@auth/(.)login/page.tsx
1
import { Modal } from '@/app/ui/modal'
2
import { Login } from '@/app/ui/login'
3
4
export default function Page() {
5
return (
6
<Modal>
7
<Login />
8
</Modal>
9
)
10
}

μ•Œμ•„λ‘λ©΄ μœ μš©ν•©λ‹ˆλ‹€:

  • 경둜λ₯Ό κ°€λ‘œμ±„λŠ” 데 μ‚¬μš©λ˜λŠ” κ·œμΉ™(e.g.: (.))은 파일 μ‹œμŠ€ν…œ ꡬ쑰에 따라 λ‹€λ¦…λ‹ˆλ‹€. 경둜 μΈν„°μ…‰νŠΈ κ·œμΉ™μ„ μ°Έμ‘°ν•˜μ‹­μ‹œμ˜€.
  • <Modal> κΈ°λŠ₯을 λͺ¨λ‹¬ μ½˜ν…μΈ (<Login>)와 λΆ„λ¦¬ν•˜λ©΄ λͺ¨λ‹¬ λ‚΄λΆ€μ˜ λͺ¨λ“  μ½˜ν…μΈ (예: forms)κ°€ μ„œλ²„ μ»΄ν¬λ„ŒνŠΈμΈμ§€ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.
    • μžμ„Έν•œ λ‚΄μš©μ€ ν΄λΌμ΄μ–ΈνŠΈ 및 μ„œλ²„ μ»΄ν¬λ„ŒνŠΈ 인터리빙을 μ°Έμ‘°ν•˜μ„Έμš”.

3.3.1 λͺ¨λ‹¬ μ—΄κΈ°

이제 Next.js λΌμš°ν„°λ₯Ό ν™œμš©ν•˜μ—¬ λͺ¨λ‹¬μ„ μ—΄κ³  닫을 수 μžˆμŠ΅λ‹ˆλ‹€. μ΄λ ‡κ²Œ ν•˜λ©΄ λͺ¨λ‹¬μ΄ μ—΄λ € μžˆμ„ λ•Œμ™€ μ•žλ’€λ‘œ 탐색할 λ•Œ URL이 μ˜¬λ°”λ₯΄κ²Œ μ—…λ°μ΄νŠΈλ©λ‹ˆλ‹€.

λͺ¨λ‹¬μ„ μ—΄λ €λ©΄, @auth μŠ¬λ‘―μ„ λΆ€λͺ¨ λ ˆμ΄μ•„μ›ƒμ— ν”„λ‘œνΌν‹°λ‘œ μ „λ‹¬ν•˜κ³ , children ν”„λ‘œνΌν‹°μ™€ ν•¨κ»˜ λ Œλ”λ§ν•©λ‹ˆλ‹€.

app/layout.tsx
1
import Link from 'next/link'
2
3
export default function Layout({ auth, children }: { auth: React.ReactNode; children: React.ReactNode }) {
4
return (
5
<>
6
<nav>
7
<Link href="/login">Open modal</Link>
8
</nav>
9
<div>{auth}</div>
10
<div>{children}</div>
11
</>
12
)
13
}

μ‚¬μš©μžκ°€ <Link>λ₯Ό ν΄λ¦­ν•˜λ©΄, /login νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•˜λŠ” λŒ€μ‹  λͺ¨λ‹¬μ΄ μ—΄λ¦½λ‹ˆλ‹€. κ·ΈλŸ¬λ‚˜, μƒˆλ‘œ κ³ μΉ¨ λ˜λŠ” 졜초 λ‘œλ“œ μ‹œμ—λŠ” /login으둜 μ΄λ™ν•˜λ©΄, κΈ°λ³Έ 둜그인 νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•©λ‹ˆλ‹€.


3.3.2 λͺ¨λ‹¬ λ‹«κΈ°

router.back()을 ν˜ΈμΆœν•˜κ±°λ‚˜, Link μ»΄ν¬λ„ŒνŠΈλ₯Ό μ‚¬μš©ν•˜μ—¬ λͺ¨λ‹¬μ„ 닫을 수 μžˆμŠ΅λ‹ˆλ‹€.

app/ui/modal.tsx
1
'use client'
2
3
import { useRouter } from 'next/navigation'
4
5
export function Modal({ children }: { children: React.ReactNode }) {
6
const router = useRouter()
7
8
return (
9
<>
10
<button
11
onClick={() => {
12
router.back()
13
}}
14
>
15
Close modal
16
</button>
17
<div>{children}</div>
18
</>
19
)
20
}

Link μ»΄ν¬λ„ŒνŠΈλ₯Ό μ‚¬μš©ν•˜μ—¬ 더 이상 @auth μŠ¬λ‘―μ„ λ Œλ”λ§ν•΄μ„œλŠ” μ•ˆ λ˜λŠ” νŽ˜μ΄μ§€μ—μ„œ λ‹€λ₯Έ νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•˜λŠ” 경우, nullλ₯Ό λ°˜ν™˜ν•˜λŠ” catch-all 경둜λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.

app/ui/modal.tsx
1
import Link from 'next/link'
2
3
export function Modal({ children }: { children: React.ReactNode }) {
4
return (
5
<>
6
<Link href="/">Close modal</Link>
7
<div>{children}</div>
8
</>
9
)
10
}
app/@auth/[...catchAll]/page.tsx
1
export default function CatchAll() {
2
return null
3
}

μ•Œμ•„λ‘λ©΄ μœ μš©ν•©λ‹ˆλ‹€:

  • ν™œμ„± μƒνƒœ 및 탐색에 μ„€λͺ…λœ λ™μž‘ λ•Œλ¬Έμ— λͺ¨λ‹¬μ„ 닫을 λ•Œ, @auth μŠ¬λ‘―μ— 포괄적인 경둜λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.
    • 더 이상 슬둯과 μΌμΉ˜ν•˜μ§€ μ•ŠλŠ” κ²½λ‘œμ— λŒ€ν•œ ν΄λΌμ΄μ–ΈνŠΈ μΈ‘ 탐색이 계속 ν‘œμ‹œλ˜λ―€λ‘œ,
    • λͺ¨λ‹¬μ„ λ‹«μœΌλ €λ©΄ μŠ¬λ‘―μ„ null을 λ°˜ν™˜ν•˜λŠ” κ²½λ‘œμ— μΌμΉ˜μ‹œμΌœμ•Ό ν•©λ‹ˆλ‹€.
  • λ‹€λ₯Έ μ˜ˆλ‘œλŠ” κ°€λŸ¬λ¦¬μ—μ„œ 사진 λͺ¨λ‹¬μ„ μ—΄λ©΄μ„œ μ „μš© /photo/[id] νŽ˜μ΄μ§€λ„ ν•¨κ»˜ μ—΄κ±°λ‚˜, μ‚¬μ΄λ“œ λͺ¨λ‹¬μ—μ„œ μ‡Όν•‘ 카트λ₯Ό μ—¬λŠ” 것 등이 μžˆμŠ΅λ‹ˆλ‹€.
  • κ°€λ‘œμ±„κΈ° 및 병렬 κ²½λ‘œκ°€ μžˆλŠ” λͺ¨λ‹¬μ˜ μ˜ˆμ‹œλ₯Ό ν™•μΈν•˜μ„Έμš”.

3.4 λ‘œλ”© 및 였λ₯˜ UI

병렬 경둜(Parallel Routes)λ₯Ό λ…λ¦½μ μœΌλ‘œ μŠ€νŠΈλ¦¬λ°ν•  수 μžˆμœΌλ―€λ‘œ, 각 κ²½λ‘œμ— λŒ€ν•΄ 독립적인 였λ₯˜ 및 λ‘œλ”© μƒνƒœλ₯Ό μ •μ˜ν•  수 μžˆμŠ΅λ‹ˆλ‹€:

Loading and Error UI

μžμ„Έν•œ λ‚΄μš©μ€ λ‘œλ”© UI 및 였λ₯˜ 처리 λ¬Έμ„œλ₯Ό μ°Έμ‘°ν•˜μ„Έμš”.