Next의 Intercepting Routes는 소셜 미디어와 궁합이 찰떡이다

avatar
kidow
@kidow
Next의 Intercepting Routes는 소셜 미디어와 궁합이 찰떡이다
태그
Nextjs
설명
UX를 쉽게 한 단계 상승시켜주는 비법, 지금 공개합니다.
배포
배포
수정일
Nov 15, 2023 04:13 PM
생성일
Nov 15, 2023 02:19 AM
소셜 미디어 웹을 서핑하다 보면 종종 이런 웹사이트들을 만난 적이 있을 겁니다. 피드를 클릭하면 모달이 뜨는 데, 동시에 url도 같이 변경되는 케이스를요.
인스타그램 예시
인스타그램 예시
비핸스 예시
비핸스 예시
드리블 예시
드리블 예시
프로덕트헌트 예시
프로덕트헌트 예시
여기서 새로고침을 하게 되면 바로 해당 상세 페이지로 이동하게 됩니다. 새로고침을 하지 않고 모달 바깥을 클릭하면 다시 원래 경로로 돌아오구요. 이렇게 하면 마치 리스트 페이지에서 상세 페이지를 갔다가 돌아오는 느낌의 UX를 연출할 수 있으며, 페이지를 이동하지 않고도 각 리스트의 상세 정보를 매끄럽게 읽어나갈 수 있도록 할 수 있습니다. 이런 식의 라우트 구성을 경로 가로채기(Intercepting Routes)라고 부릅니다.
Next.js 13.3에서 새로 공개된 Intercepting Routes는 이 로직을 아주 쉽게 구현할 수 있도록 해줍니다. 저는 과거에도 저런 UX가 참 좋다고 생각하고 구현해보고 싶었는데, 이번에 Next가 편리하게 기능을 직접 만들어 주니 정말 고맙지 뭡니까.
일간 ProductHunt 홈페이지
일간 ProductHunt 홈페이지
마침 제가 운영하는 프로젝트인 일간 ProductHunt는 리스트 페이지와 상세 페이지가 있습니다. 기존에는 유저가 아이템을 클릭하면 새 페이지를 열었는데, UX가 별로 좋아보이지는 않아서 딱 적용해 볼 적절한 타이밍이 온 것 같습니다.
 
다음과 같은 준비물이 필요합니다.
  • 라우팅 폴더
    • Parallel Routes 컨벤션에 대한 이해
    • Intercepting Routes 컨벤션에 대한 이해
  • 모달 컴포넌트
  • 공용 자식 컴포넌트
 

라우팅 폴더

저는 다음과 같이 폴더를 구성했습니다.
(product) ├── @modal │ └── (.)product/[id] │ └── page.tsx │ └── default.tsx ├── product/[id] │ └── page.tsx └── layout.tsx
먼저 @modal 에 대한 이해가 필요한데요, 폴더명 앞에 @가 붙는 것은 Parrllel Routes라고 불리는 Next의 또 다른 기능입니다. default.tsx는 아직 공식 문서가 작성 중이라 설명은 패스하겠습니다. 그냥 이렇게 적으면 됩니다.
// app/@modal/default.tsx export default function Default() { return null }
 

Parallel Routes 컨벤션

Parallel Routes는 말 그대로 평행 경로를 만드는 기술인데요, 쉽게 설명하자면 props.children 이 각 레이아웃마다 자식 컴포넌트를 렌더링하는 유일한 속성이라고 했을 때 이 것을 유일하지 않도록 해주는 기술입니다. 공식 문서에 이를 아주 잘 설명해준 사진이 있습니다.
notion image
 

Intercepting Routes 컨벤션

notion image
(..)처럼 이름을 짓는 경우 상대 경로를 지정할 때와 유사한 컨벤션이 사용됩니다. 위 feed와 photo를 예시로 들자면 (..)photo의 실제 경로는 ./feed/photo에 있는 것이고, 경로를 가로챌 실제 photo 폴더는 /photo에 있기 때문에 (..)가 사용되는 것입니다. 여기서 Parallel Routes는 무시됩니다.
제 프로젝트 구조의 경우는 ./product/[id]와 @modal/product/[id]는 동일한 상대 경로에 위치하기 때문에 (.)가 사용되었습니다. 이 부분은 생소해서 처음엔 이해가 안 갈수도 있지만 터미널로 파일 시스템을 제어해보셨다면 쉽게 이해하실 수 있을 것 같습니다.
@modal을 생성하고 page를 만들었다면, 해당 폴더가 속한 경로에서 만들어진 layout.tsx에는 props.modal 이 자동으로 생기게 됩니다. layout에서 props.modal을 선언해야 해당 평행 경로가 렌더링될 수 있으니 이 점 꼭 참고합니다.
// app/(product)/layout.tsx import type { ReactNode } from 'react' interface Props { children: ReactNode modal: ReactNode } export default function Layout({ children, modal }: Props) { return ( <> <Header /> <main>{children}</main> {modal} </> ) }
 

모달 컴포넌트

위 공식 문서 사진에서도 보셨겠지만 @modal에서는 Photo 컴포넌트를 감싸기 위한 Modal 컴포넌트가 따로 필요합니다. 저는 특별히 headless ui라는 라이브러리를 사용해보겠습니다.
npm install @headlessui/react
다음과 같이 모달 페이지를 구현해 보겠습니다. 참고로 경로 가로채기는 서버 컴포넌트를 만들 수 없습니다.
// app/(product)/@modal/(.)product/[id]/page.tsx 'use client' import { Dialog, Transition } from '@headlessui/react' import { useRouter } from 'next/navigation' import { Fragment, useEffect } from 'react' interface Props { params: { id: string } } export default function Page({ params }: Props): JSX.Element { const [data, setData] = useState<any>(null) const { back } = useRouter() const getData = async () => { ... } useEffect(() => { getData() }, []) return ( <Transition show> <Dialog onClose={back}> <Transition.Child as={Fragment}> // 오버레이 <div className="fixed inset-0 bg-black/30" aria-hidden="true" /> </Transition.Child> <Transition.Child as={Fragment}> <Dialog.Panel> // 공용 자식 컴포넌트 </Dialog.Panel> </Transition.Child> </Dialog> </Transition> ) }
 

공용 자식 컴포넌트

이제 공용 자식 컴포넌트를 만들 차례입니다. 모달에서 보여줄 UI와 상세에서 보여줄 UI를 같게 만들면 좀 더 자연스러운 UI를 연출할 수 있겠죠? 만약 여러분들이 직접 만들 경우 모달 UI와 상세 UI를 다르게 할 계획이라면 저처럼 공용 자식 컴포넌트를 만드실 필요는 없습니다.
공용 자식 컴포넌트를 만드는 부분은 단순 UI 작업이라 Intercepting Routes를 다루는 과정에서 중요한 부분은 아닌 것 같아서 생략합니다. 이제 기존 상세 페이지에도 공용 자식 컴포넌트를 적용해주면 끝입니다.
// app/(product)/product/[id]/page.tsx interface Props { params: { id: string } } export default async function Page(): Promise<JSX.Element> { const data = await getData(params.id) return ( // 공용 자식 컴포넌트 ) }
 

완성

어떤가요? 경로 가로채기. 새로고침하면 꽤 자연스럽지 않나요?
어떤가요? 경로 가로채기. 새로고침하면 꽤 자연스럽지 않나요?
경로 가로채기는 여러분들이 만드는 대부분의 프로덕트에서 유용하게 쓰일 수 있을 겁니다. 이커머스라던지, 큐레이션 서비스라던지, 여하튼 리스트와 상세 페이지가 중요한 서비스이면 일수록 이 기능이 UX를 강화하는 데 큰 힘이 되어 줄 것입니다. 읽어 주셔서 감사합니다.
 

참고

  • Nextjs