트위터 로그인 모달창을 만들어보며 넥스트의 parallel routes 와 intercepting routes 을 학습한 내용을 정리해보았습니다.
트위터 로그인 창을 확인해봅시다.

루트 디렉토리 화면을 배경으로 i/flow/login 페이지가 동시에 표시되고 있습니다.
저는 app router 를 학습하기 전까지는 createPortal 을 사용하여 포탈 영역에 로그인 컴포넌트를 띄우는 방식을 사용했었습니다.
const NoLogin =()=>{
let [loginModal, setLoginModal] = useState<boolean>(false);
let [signupModal, setsignupModal] = useState<boolean>(false);
const loginModalHandler = useCallback((sign:boolean)=>{
setLoginModal(sign);
}, [])
const SignupModalHandler = useCallback((sign:boolean)=>{
setsignupModal(sign);
}, [])
return(
{/* 생략 */}
<div className="pt-6">
<Link href='/' as='/i/flow/login' scroll={false}>
<button className="rounded-2xl border-2 p-2" onClick={()=>{loginModalHandler(true)}}>
로그인
</button>
</Link>
{loginModal && createPortal(<LoginModal loginModalHandler={()=>{loginModalHandler(false)}}/>, document.body)}
</div>
{/* 생략 */}
)
href 경로와 as 경로를 달리해 뒷배경으로 메인 화면을 띄우고 그 위에 로그인 모달을 띄우는 데 성공했습니다. 하지만 이 방식은 loginModal state 를 정의해서 modal 의 상태를 계속 감시해야하는 번거로움이 있습니다.
Next.js app router 를 학습하며, 모달창을 쉽게 구현하도록 해주는 기능이 내재되어있다는 것을 알게되었고, 위의 createPortal 방식을 넥스트의 parallel routes, intercepting routes 를 활용한 모달창 방식으로 바꿔보고자 하였습니다.
하나의 레이아웃에서 여러 페이지를 동시에 보여줄 수 있는 기능입니다. slots(@folderName)에 의해 생성이 되며, 이렇게 생성된 slot 속 페이지는 slot과 같은 레벨의 레이아웃에 props 로 전달됩니다. (이 때, slot 폴더는 url 에 영향을 주지 않고 무시됩니다.)
넥스트 앱라우터 공식 문서를 참고하며 parallel routes 에 대해 자세히 알아봅시다.

@analytics 와 @team 이라는 slot 이 생성되었습니다. 이들은 모두 같은 레벨의 레이아웃에 props 로 다음과 같이 전달되어 렌더링됩니다.
export default function Layout({
children,
team,
analytics,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<>
{children}
{team}
{analytics}
</>
)
}
자 이제 다시 트위터로 돌아가겠습니다. app router 의 파일 컨벤션에 따라서 app 디렉토리에 다음과 같은 폴더 구조를 생성했습니다.

폴더구조를 잘 살펴봅시다. 같은 레이아웃을 공유해야한다는 것, 그것에 따라 폴더 구조를 정렬해야한다는 것을 이해해야 합니다.
@modal 은 props 의 modal로 전달이 되었고, 그 외 요소들은 children로 전달이 되었습니다. 그 후 매칭되는 라우트에 네비게이션할 때 해당 라우팅의 컴포넌트가 렌더링됩니다.
slot 을 따라 레이아웃을 다음과 같이 구성했습니다.
import { ReactNode } from "react"
export default function Layout({children, modal}:{children: ReactNode, modal:ReactNode}){
return(
<div>
{children}
{modal}
</div>
)
}
이제 baseURL/i/flow/login 으로 접속 시 children 부분에는 (beforeLogin)/i/flow/login 이, modal 부분에는 (beforeLogin)/@modal/i/flow/login 이 매핑됩니다.
그렇다면 baseURL/ 로 접속한다면?
children 부분에는 page.tsx 가 매핑되고, modal 부분에는 default.tsx 가 매핑됩니다. (@modal/page.tsx 부재를 default.tsx 가 대체)
default.tsx→ parallel route 의 default page. parallel route 가 이용되지 않을 때 이 페이지를 디폴트값으로 띄워줍니다. 즉, 라우팅이 unmatched 할 때 이 페이지를 렌더링합니다. (따라서 위와 같은 상황에 default.tsx 페이지가 없다면 에러가 발생하겠지요.)

하지만 우리가 원하는 것은 i/flow/login/page.tsx 가 동시에 렌더링 되는 것이 아닙니다.. 우리가 원하는 것은 main page(라우팅 주소가 다름)가 뒷배경으로 렌더링 되는 것! 페이지를 동시에 띄우는 것까지 했으니, 주소가 다른 페이지를 렌더링하는 작업을 해보도록 하겠습니다.
intercepting routes 는 현재 페이지 컨텍스트를 유지한 채로 새로운 라우트를 렌더링합니다. 따라서 주소가 다른 페이지들을 동시에 렌더링 하고자 할 때 유용합니다.
원하는 화면을 구성하기 위해 layout props 로 children 을 page.tsx 로, modal 을 @modal/i/flow/login/page.tsx 로 전달해봅시다.
interception routes 는 (..) 와 같은 컨벤션으로 정의됩니다.
(.) 동일한 라우팅 레벨 세그먼트에 매칭(..) 부모 라우팅 레벨 세그먼트에 매칭(..)(..) 2단계 윗 레벨(…) app 디렉토리 루트 요소에 매칭💡 주의할 것은, 기준이 브라우저 주소라는 것입니다! (라우트 세그먼트 기준)

구조를 위와 같이 바꿔 intercepting routes 를 사용해보았습니다.
다시 구조를 살펴보면, @modal 은 라우팅에 영향을 미치지 않는 slot이지, 라우트 세그먼트가 아닙니다. 그렇기 때문에 @modal 요소 컨텍스트는 유지한 채 (브라우저 주소 상)같은 레벨의 i 요소의 라우팅을 인터셉트할 수 있습니다. ((..)i 로 폴더 이름을 바꾸는 실수를 저지르면 안됩니다.)
이제 layout.tsx 에 children prop 에 page.tsx 가 렌더링되고, modal prop에 i/flow/login 으로 인터셉트한 기존 모달 부분이 렌더링되어 원하던 결과를 얻을 수 있게 됩니다.

(localhost:3000/i/flow/login 화면)
이렇게 모달을 만들면 좋은 점은 다음과 같습니다. (공식문서 참조)
즉, 페이지를 모달로 띄우기가 가능해 모달 자체가 url 로 관리될 수 있다는 것입니다. createPortal 보다 방법이 직관적이고 간단해서 좋은 것 같습니다.
왜 인터셉트 후, page.tsx가 children 으로, @modal/i/flow/login/page.tsx 가 modal 로 렌더링되는가?
저는 위 사항이 이해가 가지 않았습니다. parallel routes 는 매칭되는 라우팅에 해당하는 여러 페이지를 동시에 보여준다고 이해했는데, 둘은 매칭되는 라우팅 주소가 다른데 어떻게 동시에 렌더링이 되는 것일까요.
공식문서에 의하면 기본적으로 슬롯에서 렌더링되는 컨텐츠는 현재 url 과 매치됩니다. 그런데 매치되지 않는 슬롯이 있는 경우는 다음과 같이 페이지를 렌더링한다고 합니다.

위는 라우팅이 통일되지 않았습니다. 여기서 /settings 로 이동하는 상황을 가정해보면, @analytics 는 어떤 페이지를 보내줘야할까요?
@analytics/default.js 가 존재하는 경우 | @analytics/default.js 가 존재하지 않는 경우 | |
|---|---|---|
| 소프트 네비게이션 | @team/settings/page.js 그리고 @analytics/page.js | @team/settings/page.js 그리고 @analytics/page.js |
| 하드 네비게이션 | @team/settings/page.js 그리고 @analytics/default.js | 404 |
Softe navigation: 변화된 segments들의 캐시 사용
Hard Navigation: 세그먼트 리렌더링, 데이터 refetch
(출처: https://velog.io/@glassfrog8256/번역-Next.js13-App-Router-Routing-Parallel-Routes)
기본적으로 브라우저는 하드 네비게이션으로 동작하지만, 넥스트 앱라우터는 소프트 네비게이션을 제공합니다. 위에서 제가 느꼈던 의문은 기존 하드 네비게이션 방식이 아닌 소프트 네비게이션 방식으로 페이지 이동이 이루어졌기 때문인 것 같습니다. 아직 정확히 넥스트에서 어떤 기준으로 soft navigation 을 사용하고, hard navigation 을 사용하는지는 모르겠습니다. 추후에 관련 내용을 추가해보고자 합니다.
오류 / 번외 관련 내용은 댓글로 알려주시면 감사하겠습니다.
https://rocketengine.tistory.com/entry/NextJS-13-Routing-Intercepting-Routes라우트-가로채기
https://nextjs.org/docs/app/building-your-application/routing/parallel-routes
https://nextjs.org/docs/app/building-your-application/routing/intercepting-routes