Next.js 캐럿마켓 클론코딩( MODALS)

짜스의 하루 ·2024년 8월 20일

Introduction

이번 챕터에서 우리가 할 것은 --> /product 페이지에서 상품을 하나 누르면, /product/1로 이동하면서, 상품 상세보기 모달은 띄우지만, 같은 페이지에 사용자가 새로고침을 하게 되면, 제품 상세 페이지로 가게 되는 것 을 구현할 것이다!

아자아자 화이팃부리!


Intercepting Routes

intercepting Routes 이란? 애플리케이션의 다른 부분에서 현재의 레이아웃으로 route를 불러올 수 있도록 해준다 이게 무슨 말일까?

사용자가 product/id로 가려고 product를 하나 클릭할 때, 사용자에게 실제로 product/id 페이지를 보여주는 대신에 사용자가 그 URL로 갈 때, 그 페이지 대신 다른 component를 보여줄 수 있다는 것을 의미한다.
---> 이건 사용자가 그 페이지로 가려고 제품을 클릭할 때에만 발생한다고 한다.
---> 즉 그 페이지로 이동하는 것을 intercept하는 것(가로채는것)이다.

하지만, 사용자가 URL를 타고 product/id로 바로 접속을 하게 되면 intercept는 일어나지 않고, product/id의 페이지가 보여지게 된다.

intercepting routes를 하기 위해 폴더 구조를 살펴보면, (..)products/[id]/page 폴더 구조로 이루어져있다.

기존의 products/[id](..)만 다르고 나머지 폴더 구조는 똑같은 것을 확인할 수 있다.

만약, 사용자가 products 페이지에 있는데, /product/id로 이동하는 링크를 클릭하게 되면, (..)product/[id]/page.tsx 이 컴포넌트를 보여주게 된다.

위와 같이 동일한 폴더 구조를 만들어야 한다. 폴더 구조를 그대로 작성하게 되면, Next JS가 우리가 그 route를 intercept하고 싶다는 것을 알게 된다. 정말 간단하게 (..)products/[id]/page.tsx** 에 모달을 만들어 보았다.

/products에서 "태국 국수" 제품을 클릭했을 때, url은 producs/11 로 이동했지만, 화면에는 Hi Im Modal이 보여지는 것을 확인할 수 있다. 즉, intercept이 이루어진 것을 확인할 수 있다.

그럼 (..)는 과연 무엇을 나타내는 것일까?
(..) 는 상위 디렉터리로의 상대 경로를 나타낸다. 이를 통해 현재 경로와 관계없이, 상위 경로로 이동할 수 있다.
--> 이 기능을 사용해서 특정 경로로 이동을 가로채고, 원하는 대체 경로를 제공하거나 중간에 다른 경로를 삽입할 수 있게 해주는 특징이 있다.


Intercepting Recap

/products에서
/products/[id]로 이동을 하려고 할 때,
/products에서 페이지를 벗어나야 한다.


app/(tabs)/products로 페이지가 있는데,(tabs)는 라우트 그룹이기 때문에 URL경로에 어떠한 영향을 끼치지 않는다.

--> /로 가야 한다
--> 어떻게 가냐? (..)products를 해주면 된다.

이렇게 하는건 Next JS에게 root URL을 intercept하고 싶다고 말하는 것과 같다.

(..)를 사용하여 상위 디렉토리인 app으로 이동한 후, 다시 products/[id] 경로를 찾아서 그 경로로 이동하는 과정을 통해 Interception을 수행하게 된다.

만약, 같은 폴더 구조 안에서 intercept하고 싶을 땐?

/products
   /recent 
   (.)recent

가 있는데 /products/recent로 이동할 때, intercept를 하고 싶을 때엔 ?
(.)recent를 해주면 된다.

왜? (.)같은 폴더 구조에 있는 녀석을 의미하기 때문이다.
만약, 여기서 (..)를 하게 되면, /products 밖으로 폴더를 벗어나 밖에서 recent폴더를 찾기 때문에 intercept가 이루어지지 않게 된다.

  • (.): 현재 디렉토리를 의미한다.
  • (..): 상위 디렉토리를 의미한다.

Parallel Routes

(..)products/[id]/page.tsx 의 모달창과, 해당 product를 같이 화면에 렌더링하려고 한다. (예를 들면, 같은 페이지 안에서 모달창을 맨 앞에 두고, product화면을 뒤에 둔다는 등)

두 route가 동시에 렌더링 되고 있으면 좋겠는데, 어떻게 하지 ? --> Parallel Routes를 사용하면 된다.

Parallel Routes는 Next.js 13에서 도입된 기능 중 하나로, 동시에 여러 경로(라우트)를 렌더링할 수 있는 기능을 제공한다.
이 기능을 통해 동일한 레이아웃 안에서 여러 경로의 내용을 병렬로 렌더링하여 페이지의 구성 요소를 분할하고, 다양한 콘텐츠를 동시다발적으로 보여줄 수 있게 된다.

Parallel Routes를 사용하려면, 일반적으로 해당 폴더 안에서 레이아웃을 설정하고, 병렬로 렌더링할 콘텐츠를 정의한다. 예를 들면, app/(tabs) 폴더 내에서 병렬로 렌더링할 콘텐츠를 설정하려면, app/(tabs)/@sidebar/page.tsx 이런식으로 정의하기도 한다.

(마치 component를 만든 뒤, layout에 추가해서 layout으로 추가하는 것과 비슷하다고 생각이 든다)

Parallel Routes@ 를 사용해서 폴더를 생성하고, page.tsx에 여러 경로로 렌더링 할 코드를 작성해두면 된다.

RootLayout 파일은 여러 @ 폴더에서 병렬로 렌더링되는 콘텐츠를 감싸는 역할을 한다.
RootLayout에서 children으로 하위 레이아웃이나 페이지들을 렌더링하면, Next.js는 자동으로 @로 시작하는 폴더들을 찾아 병렬로 콘텐츠를 렌더링하게 된다.


Default Routes

Rootlayout.tsx 폴더에 우선 @potato/page.tsx를 적용시키기 위해서 {children} 상단 위에 작성해 두었다.

여기서 생각해봐야할 것은 Rootlayout.tsx는 모든 폴더에 영항을 미친다. 전역 layout이기 때문이다.


여기서 살펴보면, (auth)안에는 layout.tsx가 없고, (tabs)에는 layout.tsx가 있다.

RootLayout.tsx와 전역 레이아웃:

  • RootLayout.tsx 는 Next.js 애플리케이션의 모든 페이지와 라우트에 영향을 미치는 전역 레이아웃이다.
  • 이 파일에서 {children} 상단에 @potato를 적용하면, 기본적으로 모든 페이지와 경로에서 @potato가 렌더링되도록 설정할 수 있다.

라우트 그룹의 레이아웃:

  • (auth) 그룹: 이 그룹에는 layout.tsx 파일이 없으므로, RootLayout.tsx가 기본 레이아웃으로 작동한다.

  • (tabs) 그룹: : 이 그룹에는 별도의 layout.tsx 파일이 존재하고 있다.
    이 파일은 RootLayout.tsx에서 설정한 전역 레이아웃을 덮어쓰거나 추가적인 레이아웃을 설정할 수 있다.

  • (tabs)/layout.tsx 파일이 @potato를 명시적으로 포함하고 있지 않다면, (tabs) 그룹 내 모든 페이지는 @potato를 렌더링하지 않게 된다.

이렇게 오류가 뜨게 된다.

여기서 app/@potato/default.tsx 파일을 생성하면, @potato 폴더와 그 하위 경로에 대해 정의된 레이아웃이 적용이 된다. default.tsx는 특정 폴더의 기본 레이아웃을 설정하는 데 유용하게 적용된다.

default.tsx 폴더를 설정해두었더니,
(tabs)에 속한 Page들의 {potato}가 제대로 적용된 것을 확인할 수 있다.


이제 해야할 것은 -> intercepting routes를 사용해서 modal를 만들고, 그 모달이 products와 함께 위치하도록 (Parallel Routes를 사용)해보면 될 것이다.

Intercepting Routes:

  • 페이지 내에서 특정 경로를 가로채거나 중첩된 레이아웃을 추가하는 기능이다. 주로 URL 경로를 가로채서 모달 같은 UI 요소를 렌더링할 때 사용한다.

Parallel Routes:
병렬로 렌더링되는 여러 콘텐츠 영역을 정의할 수 있는 기능이다. 모달과 페이지 콘텐츠를 병렬로 렌더링할 때 유용하다.

Interception Routes를 어디서 사용할 것인가?

  • 사용 시점: 사용자가 products 페이지에서 특정 상품을 선택했을 때.
  • 동작 방식: 사용자가 특정 상품을 선택하여 /products/[id] 경로로 이동할 때, 이 경로를 가로채서 모달을 표시한다.

Parallel Routes를 활용하여 기존의 products와 모달을 병렬적으로 보여줄 것이다.

  • 목적: 사용자가 상품을 선택했을 때 모달이 나타나면서도, 기존의 products 페이지는 그대로 유지되도록 설정.
  • 구현 방식: interception routes를 사용하여 모달을 가로채어 표시하고,
  • parallel routes를 통해 모달과 products 페이지를 병렬로 렌더링하여 사용자 경험을 최적화한다.


현재 우리의 폴더 구조를 살펴보면,

/products/
├── @modal/
│   └── (..)products/[id]/page.tsx
└── page.tsx

이렇게 이루어져 있다.

사용자가 products/[id] 로 접근할 때

  • interception : products/[id] 경로로 접근이 될 때, /products/@modal/(..)products/[id]/page.tsx를 통해 라우트를 가로채게 된다.
  • Parallel Routes : products/page.tsx 는 유지되면서, 병렬적으로 @modal/(..)products/[id]/page.tsx에 있는 모달이 렌더링되게 된다.

결과적으로는

  • products/[id] 경로로 이동하면서, 메인페이지인 products/page.tsx와 선택한 상품에 대한 상세 정보가 포함된 모달이 동시에 렌더링 되게 된다.
    이런식으로products/[id]로 url은 이동했지만, 화면은 products와 modal이 병렬적으로 나타난 것을 확인할 수 있다!

여기서 중요한 점은, 병렬적으로 적용시키기 위해서 layout.tsx를 설정해주어야 한다.
* children: 이곳에는 HomeLayout 하위에 렌더링될 기본 콘텐츠가 들어간다. 예를 들어, 메인 페이지 콘텐츠(products/page.tsx 등)가 여기에 렌더링된다.

  • modal: 이곳에는 모달 콘텐츠가 렌더링된다. 이는 Interception Routes를 통해 특정 경로에 접근할 때 표시되는 모달이다.

여기서 중요한 점은, @modal 즉, slot의 이름을 layout.tsx에서 props로 받게 된다


이렇게 좀 밝은 (?) 인스타 스러운 모달창을 만들어 보고 싶어서 디자인을 이렇게 만들어 보았다.

먼저, product의 대한 db를 가져와야 한다. db.product.findUnique를 이용해서,
parameter로 넘어온 id(상품의 id)를 가지고, 데이터를 먼저 불러온다!

parmas로 받아온 product의 id를 가지고 있는 db를 찾은 뒤, user의 정보도 포함시켜서 데이터를 가져왔다.
modal이 화면에 나타났을 때, url은 products/id로 나타나게 된다. 여기서, 알아야 할 것은 modal도 params를 가져올 수 있다는 점이다!

여기서 가져온 id는 string으로 받아오게 되기 때문에 Number로 형변환을 시켜주어야 한다.

return (
    <div className="max-w-screen-sm flex justify-center items-center bg-black bg-opacity-60">
      <div className="relative bg-white rounded-lg shadow-lg max-w-4xl w-full h-auto p-6">
        <CloseBtn />
        <h1 className="text-2xl font-bold text-gray-900 mb-4 border-b pb-2">
          상품 더보기
        </h1>
        <div className="flex flex-col md:flex-row gap-4">
          <div className="w-full md:w-1/2 h-60 md:h-80 relative rounded-lg overflow-hidden bg-gray-100">
            <Image
              fill
              src={`${product.photo}/public`}
              alt={product.title}
              className="object-cover"
            />
          </div>
          <div className="flex flex-col justify-between">
            <div className="flex flex-col gap-2 mb-4">
              <h2 className="text-xl font-semibold text-gray-900">
                {product.title}
              </h2>
              <p className="text-sm font-medium text-gray-700">
                {formatToWon(product.price)}원
              </p>
              <p className="text-neutral-500">{product.description}</p>
            </div>
            <div className="flex items-center gap-4 mt-2">
              <div className="w-10 h-10 relative rounded-full overflow-hidden bg-gray-200">
                <Image
                  fill
                  src={`${product.user.avatar}/avatar`}
                  alt={product.user.username}
                  className="object-cover"
                />
              </div>
              <h3 className="text-lg font-medium text-gray-800">
                {product.user.username}
              </h3>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

나머지는 불러온 데이터로 화면을 구성한거라 설명은 딱히 안하겠다
여기서 살펴봐야할 것은 <CloseBtn /> 이다.
모달창을 닫는 역할을 하는 버튼인데, "use client" 로 코드를 만들어야 하기 때문에, 따로 컴포넌트를 만들어서 가져와 사용해 주었다.

짠! 이렇게 만들면 된다

profile
2024. 01. 02 ~ 백앤드 공부 시작, 2024. 04.01 ~ 프론트 공부 시작

0개의 댓글