- Published on
프론트엔드에서 URL이 부여되는 모달(Routable modal) 구현하기 (2) - 실전편
이제 이전 글에 이어 갤러리 예제를 통해 Next.js의 pages 라우터에서 Routable 모달을 실제 구현하는 과정을 살펴보겠습니다.
Next.js의 app 라우터에서 useRouter
는 클라이언트 컴포넌트에서만 사용할 수 있는 훅입니다. 또한,
쿼리 스트링이나 pathname을 읽기 위해서는 usePathname
, useSearchParams
와 같은 훅을 사용해야
합니다. 따라서 본문의 코드가 제대로 작동되지 않을 수 있습니다.
기본 아이디어
Routable 모달은 유저가 모달을 여는 동작을 수행할 때 동시에 브라우저의 히스토리 스택에 새로운 URL을 추가하여 실제 페이지를 내비게이션하지 않고 브라우저 주소창에 표시되는 URL만을 변경합니다.
따라서 위 사진처럼 갤러리 페이지(/gallery
)에서 썸네일을 클릭하면 다음 두 가지 동작이 수행되어야 합니다.
- 모달을 연다.
- 브라우저의 히스토리 스택에 새로운 URL을 추가한다(여기서 URL은
/gallery/{사진의 고유 ID}
의 형태가 될 것입니다).
이 작업에 추가로, 모달에 부여한 URL로 사용자가 직접 접근했을 때의 처리를 해주어야 합니다. 이전 글에서 설명한 두 가지 세부 유형으로 나누어 구현해보겠습니다.
- URL로 직접 접속했을 때 모달이 열리는 방식
- URL로 직접 접속했을 때 모달이 아닌 별도의 페이지로 이동하는 방식
1. URL로 직접 접속했을 때 모달이 열리는 방식
이 방식에서는 모달에 부여한 URL(/gallery/123
)에 접근했을 때 실제로는 갤러리 페이지(/gallery
)가 열리고, 그 위에 123번 사진이 모달로 열려야 합니다.
따라서, 라우터에서는 /gallery/123
으로 직접 접속한 경우 실제로는 /gallery
로 이동되어야 하고, 동시에 123번 사진 모달을 열 것이라는 정보도 함께 전달해야 합니다. 이를 위해 쿼리 스트링을 이용할 수 있습니다. 정리하자면 아래와 같습니다.
이를 코드로 간략하게 나타내면 아래와 같습니다.
/* pages/gallery.tsx */
...
<Link
href={{
query: {
...router.query,
photoId: 123,
},
}}
as="/gallery/123"
>
<img src="..." />
</Link>
...
갤러리 페이지에서는 썸네일을 클릭했을 때 Next 라우터의 Link
의 as
prop을 이용하여 히스토리 스택에는 /gallery/123
을 넣고, 실제로는 현재 페이지에 photoId
라는 쿼리 스트링을 덧붙이도록(/gallery?photoId=123
) 설정합니다.
이와 같이 설정하면 링크를 눌렀을 때 브라우저 주소창에는 /gallery/123
으로의 내비게이션이 일어난 것처럼 보이게 됩니다.
또한, 갤러리 페이지에서는 모달의 열림/닫힘 여부를 관리하기 위해 별도의 state가 아닌 쿼리 스트링의 photoId
값을 이용하게 됩니다.
/* pages/gallery.tsx */
...
const { photoId } = router.query;
...
{
photoId &&
<Modal
photoId={photoId}
onClose={() => {
router.push({
query: {
...router.query,
photoId: undefined,
},
})
}}
/>
}
위와 같이 photoId
가 존재할 때만 모달을 엽니다. 또 하나 중요한 것은, 모달 내부에서 "닫기" 동작이 일어날 때 router.push
를 이용하여 쿼리 스트링의 photoId
를 undefined
로 설정해야 한다는 점입니다.
그러면 이 작업에 의해 photoId
값이 없어지므로 모달은 닫히게 될 것입니다.
이제 /gallery/123
으로 직접 접속했을 때의 처리를 해 주어야 합니다. 본 예제에서는 Next를 사용하고 있으므로 미들웨어 등을 활용해 페이지가 클라이언트에서 렌더링 되기 전 서버단에서 리다이렉트를 처리할 수도 있습니다.
하지만 여기서는 클라이언트 사이드에서 useEffect
를 통해 리다이렉트를 처리하도록 하겠습니다.
/* /pages/gallery/[photoId].tsx */
...
const { photoId } = router.query;
useEffect(() => {
router.replace(
{
pathname: '/gallery',
query: {
...router.query,
photoId,
},
},
`/gallery/${photoId}`
)
}, [router])
...
위와 같이 구현하면, /gallery/123
으로 직접 접속했을 때 실제로는 /gallery?photoId=123
으로 리다이렉트 되고, as
속성에 의해 브라우저 주소창에는 쿼리 스트링이 보여지지 않은 채로 /gallery/123
이 노출되게 됩니다
(router.push
나 router.replace
메서드에 전달하는 두 번째 parameter가 as
가 됩니다).
하지만, 갤러리 페이지로 replace될 때에 실제로는 쿼리 스트링에 photoId
가 존재하므로 페이지에 입장하자마자 모달이 열리게 됩니다.
2. URL로 직접 접속했을 때, 모달이 아닌 별도의 페이지로 이동하는 방식
이 방식에서는 모달에 부여한 URL(/gallery/123
)에 접근했을 때, 갤러리 페이지(/gallery
)가 아닌 123번 사진이 나와 있는 별도의 페이지(/gallery/123
)로 이동해야 합니다.
1번 유형과 다른 점은, 사진의 상세 페이지(/gallery/123
)이 실제로 존재하는 페이지라는 것입니다.
/* pages/gallery.tsx */
<Link
href={{
query: {
...router.query,
photoId: 123,
},
}}
as="/gallery/123"
>
<img src="..." />
</Link>
...
{
photoId &&
<Modal
onClose={() => {
router.push({
query: {
...router.query,
photoId: undefined,
},
})
}}
>
<PhotoDetailModalContent photoId={photoId} />
</Modal>
}
1번 유형과 전체적인 구현은 같지만 모달 내부의 컨텐츠를 재사용하기 위해 별도의 컴포넌트로 분리했습니다.
한편, /gallery/123
페이지에서는 아래와 같이 모달이 아닌 전체 페이지로 열리는 형태가 되어야 합니다.
/* pages/gallery/[photoId].tsx */
...
const { photoId } = router.query;
...
return () => (
<PhotoDetailModalContent photoId={photoId} />
)
위와 같이 구현하면 /gallery
페이지에서 썸네일을 클릭했을 때는 주소가 부여된 모달이 열리고, 이 주소(/gallery/123
)로 직접 접속했을 때는 모달이 아닌 전체 페이지로 열리게 됩니다.