개인적으로 공부하고 직접 프로젝트에 적용해보고 싶은 기능이 있습니다. 바로 'nextjs의 경로 가로채기'였습니다. 대부분의 모달의 경우 URL이 부여되지 않고 있고, 현재 페이지위에서 모달창을 띄우는 방식을 사용합니다. 인기있는 웹사이트의 모달만 봐도 그렇습니다. 하지만 이러한 모달에 경로 가로채기 개념을 적용한다면 이는 사용자 경험(UX)를 크게 향상하는 방법이라고 생각이 들었습니다.. 그래서 본인의 프로젝트 링킷에도 경로 가로채기 개념을 도입해보고자 생각이 들었습니다. 이에 대한 기대효과로는 추후에 사용자가 선택한 값이 새로고침을 해도 풀리지 않고, 링크를 복사해서 다른 사용자에게 공유까지 할 수 있다는 장점이 있습니다.
NextJS에서 경로 가로채기(Intercepting Routes)에 대한 개념을 공식문서에서 정의하고 있는 것을 알았습니다. 아래 링크를 통해서 오리지널 문서를 읽어보고 와도 좋을 것 같습니다.
Routing: Intercepting Routes | Next.js
Use intercepting routes to load a new route within the current layout while masking the browser URL, useful for advanced routing patterns such as modals.
nextjs.org
경로 가로채기(Intercepting Routes)란?
기본적인 라우팅 동작을 중단시키고 모달이나 대체뷰로 컴포넌트를 표시할 수 있는 기법. 즉 페이지가 리로드되어도 URL은 유지하면서 기존 모달은 닫히지 않고 표시되게 하는 등 기존 페이지의 context를 유지 할 수 있는 개념입니다..
예를 들어 페이지에서 어떤 요소를 클릭하면 URL은 바뀌게 되지만 다른 페이지로 이동하는 것이 아닌 모달창 형태로 정보를 보여줄 수 있습니다. 이는 페이지 로딩 시간을 줄일 수 있고, 사용자의 자연스러운 흐름을 제공합니다.
경로 가로채기 `(..)` 개념
NextJS의 경로 가로채기에서 필수로 사용되는 개념입니다. 이는 파일 시스템에서 상대 경로를 작성하는 방식과 유사하게 작동합니다. 여기서 중요한 점은 이것이 폴더가 아니라 세그먼트에 적용된다는 것입니다.
- `(.)` : 현재 수준의 경로
- `(..)` : 한 수준 위의 경로
- `(..)(..)` : 두 수준 위의 경로
- `(...)` : 루트 app 폴더또는 app 라우터로부터의 경로
같은 레벨의 세그먼트를 매치하기 위해서 중괄호, 괄호 또는 대괄호안에 사용할 수 있습니다.
app/
|--feeds/
| |--layout.tsx
| |__(..)photo/
| |__[id]/
| |__page.tsx
|__photo/
| |__[id]/
| |__page.tsx
|--layout.tsx
|__page.tsx
예를 들면 photo/[id]/page.tsx의 페에지를 모달로 나타내고 싶다면, '(..)photo' 폴더를 생성하여 photo 경로를 가로채어 fees페이지의 화면위의 모달로 나타낼 수 있습니다.
예시
경로 : app/photos/@modal/photos/[id]/page.tsx
// app/photos/@modal/photos/[id]/page.tsx
import Modal from '@/components/Modal'
import PhotoCard from '@/components/PhotoCard'
import photos, { Photo } from '@/lib/photos'
export default function PhotoModal({
params: { id }
}: {
params: { id: string }
}) {
const photo: Photo = photos.find(p => p.id === id)!
return (
<Modal>
<PhotoCard photo={photo} />
</Modal>
)
}
photos/[id]/page.tsx의 경로를 가로채어 모달로 표시합니다. /photos 페이지가 시작점이고, 여기서 모든 사진을 볼 수 있습니다. 각 사진을 볼 수 있는 동적 라우트를 포함하고 있고, 사용자가 모든 사진 페이지에서 특정 사진으로 이동할 때 라우트를 가로채는 방식으로 작동되어 집니다.
import Link from 'next/link'
import photos, { Photo } from '@/lib/photos'
import PhotoCard from '@/components/PhotoCard'
export default function PhotoPage({
params: { id }
}: {
params: { id: string }
}) {
const photo: Photo = photos.find(p => p.id === id)!
return (
<section className='py-24'>
<div className='container'>
<div>
<Link
href='/photos'
className='font-semibold italic text-sky-600 underline'
>
Back to photos
</Link>
</div>
<div className='mt-10 w-1/3'>
<PhotoCard photo={photo} />
</div>
</div>
</section>
)
}
모달에 들어가는 내용입니다. NextJS의 params를 가져와 ID에 대해서 필터링하여 특정 사진을 가져와 보여주며, 렌더링합니다.
이러한 구조와 코드는 photos 경로에서 사진 항목을 클릭하였을 때, 해당 사진 크게 모달로 띄우는 방식을 구현합니다. 이러함으로써 사용자는 페이지를 이동하지 않고도 이미지 클릭 시 개별페이지 대신 모달이 나타나도록 요청을 가로챕니다. 여기서 새로고침을 하거나 링크를 복사하여 접속하게 되면 개별 페이지로 확인할 수 있습니다.
이는 효과적으로 네트워크에 한 요청을 최소화하여 사용자 경험(UX)을 향상하는 기법 중 하나라고 생각합니다. 추가로 URL 공유 가능성, Context의 보존까지 가능합니다.
Headless UI 모달 적용
'use client'
import { Fragment } from 'react'
import { Dialog, Transition } from '@headlessui/react'
import { useRouter } from 'next/navigation'
export default function Modal({ children }: { children: React.ReactNode }) {
const router = useRouter()
const handleClose = () => router.back()
return (
<Transition.Root show={true} as={Fragment}>
<Dialog as='div' className='relative z-10' onClose={handleClose}>
<Transition.Child
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0'
enterTo='opacity-100'
leave='ease-in duration-200'
leaveFrom='opacity-100'
leaveTo='opacity-0'
>
<div className='fixed inset-0 bg-gray-500/80 transition-opacity' />
</Transition.Child>
<div className='fixed inset-0 z-10 w-screen overflow-y-auto'>
<div className='flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0'>
<Transition.Child
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
enterTo='opacity-100 translate-y-0 sm:scale-100'
leave='ease-in duration-200'
leaveFrom='opacity-100 translate-y-0 sm:scale-100'
leaveTo='opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
>
<Dialog.Panel className='relative transform overflow-hidden rounded-2xl bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg'>
{children}
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}
추가적으로 모달을 구현할때 헤드리스 UI를 이용하면 엄청난 편리함을 느낄 수 있었습니다. 기존에는 모달 외부를 클릭할때 모달이 닫히는 것을 구현할때에는 일일히 JS코드를 입혀줬어야 했지만 이를 이용하면 간단히 해결할 수 있었습니다.
헤드리스(Headless) UI는 Tailwind Labs에서 개발한 컴포넌트 라이브러리입니다. Headless는 스타일이 없다는 의미로, 개발자가 자신의 스타일링과 디자인 시스템을 완전히 제어할 수 있다는 점을 강조합니다.
주요 컴포넌트로는 아래와 같습니다.
- Dialog (모달)
- Menu (드롭다운 메뉴)
- Listbox(선택 상자)
- Popover
- Switch(토글 스위치)
- Tabs (탭 UI)
Transition Root/Child
Transition.Root는 전체 애니메이션 상태를 관리하는 컴포넌트입니다. show={true}로 설정되면 모달이 항상 표시된다는 것을 의미합니다. as={Fragment}로 설정되어 추가적인 DOM 노드를 생성하지 않습니다.
Transition.Child는 모달의 열림과 닫힘 애니메이션을 정의하는 컴포넌트입니다. 이는 자식 요소에 애니메이션을 적용합니다.
결론
UX를 향상시키는 방법을 항상 생각해오고 있었는데 Next.js의 경로 가로채기 기능은 그에 대한 정말 좋은 방안인 것 같습니다. 이는 모달을 평소에도 많이 활용하는 모든 프론트엔드 개발자가 사용했으면 좋을 것 같은 개념인 것 같다. NextJS의 경로 가로채기 기능은 이미 많은 사례에서 유효성이 입증되었고, 앞으로는 당분간은 이 개념을 대체할 기능이 있을까 싶은 생각이 듭니다.
현재 본인의 프로젝트에 구현된 모달 컴포넌트는 URL을 부여하지 않고, 그 페이지위에 띄우는 방식을 이용했었는데 '경로 가로채기' 기능으로 리팩토링을 하면서 좀 더 알아보고 고안해야겠다는 생각이 들었습니다.
'프론트엔드 기록 > 리액트' 카테고리의 다른 글
빈 화면 없이 자연스럽게 데이터 갱신하여 SSR UX 개선하기 (0) | 2025.03.03 |
---|---|
리액트 Flux 패턴이란?( vs MVC 모델 ) (2) | 2024.10.29 |
[Next.js 14] FSD 기능 관점 폴더 아키텍처에 대한 생각 (2) | 2024.10.04 |
리액트의 동작 원리, Virtual DOM (1) | 2024.09.12 |
NextJS 서버 클라이언트 컴포넌트의 대한 고찰 및 전략 (0) | 2024.08.25 |