[Next.js] parallel routes & intercepting routes

·

4 min read

트위터 로그인 모달창을 만들어보며 넥스트의 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 를 활용한 모달창 방식으로 바꿔보고자 하였습니다.


parallel 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

intercepting routes 는 현재 페이지 컨텍스트를 유지한 채로 새로운 라우트를 렌더링합니다. 따라서 주소가 다른 페이지들을 동시에 렌더링 하고자 할 때 유용합니다.

원하는 화면을 구성하기 위해 layout props 로 childrenpage.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 통해 모달 내용 공유 가능

  • 페이지 새로고침 시 모달이 닫히는 대신 컨텍스트가 유지됨

즉, 페이지를 모달로 띄우기가 가능해 모달 자체가 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.js404

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