본문 바로가기
Next

Next.js - Route 로 관리하는 모달 만들기 (URL 히스토리)

by 새발개발JA 2023. 12. 29.
반응형

 

updated 01/13/24

Next.js 로 포트폴리오 사이트를 만들면서

Route 로 관리되는 모달 (Route as Modal) 을 구현하고자 하였다

 

리액트로 같은 기능을 구현할 때 useHistory 로 구현하였던 기억이 있어 비슷하게 헤매일 줄(?) 알았는데

생각보다 훨씬 간단하였다 (머 - 쓱 😅)

 

Next js 는 그만큼 라우터에 진심이라는 것을 다시한번 느끼며 그때의 감동을 정리해보았다

 


Next.js -  Route 로 관리하는 모달 만들기 (URL 히스토리)

 

일단 처음 살펴봐야할 개념이 있다. Intercepting Routes 라는 개념이다.

이 친구에 대해 간단히 정리를 하면 원래의 주소의 화면과 다른 라우트를 가진 컴포넌트를 동시에 보여줄 수 있는 기능이다

This allows you to load a route from another part of your application within the current layout.
For example, when clicking on a photo in a feed, you can display the photo in a modal, overlaying the feed. In this case, Next.js intercepts the /photo/123 route, masks the URL, and overlays it over /feed.
→ 딱 나에게 맞는 예시가 아닐까 싶다
 

Routing: Intercepting Routes | Next.js

Use intercepting routes to load a new route within the current layout while masking the browser URL, useful for advanced routing patterns such as modals.

nextjs.org

 

 

방법은 너 - 무 간단하다. 디렉터리 구조를 아래와 이란성 쌍둥이처럼 꾸려주는 것이다

자료 이미지를 보면 빠른 이해가 가능할 것이다

- (괄호로 감싸진 상대 경로) 로 표현한 디렉터리 구조
- 일반적으로 표현한 디렉터리 구조

 

 

 


App Directory

 

 

디렉터리 자체를 모달일 때와 페이지일 때의 경로로 구분해주었다

- @modal 디렉터리 내에 모달로 된 page 를 셋업해주었고

- portfolio 디렉터리 내에 페이지로 된 page 를 셋업해주었다

(page.tsx 파일명은 컨벤션이다)

 

 

components/common/modal

모달 자체의 기능을 정의한 컴포넌트이다

"use client";
import React, {
  HTMLAttributes,
  ReactNode,
  useCallback,
  useEffect,
  useRef,
} from "react";
import { useRouter } from "next/navigation";

export interface ModalProps {
  children: React.ReactNode;
}

const Modal = ({
  children,
}: ModalProps) => {
  const router = useRouter();
  const clickedRef = useRef<EventTarget>();

  // 닫기 기능
  const onClose = useCallback(() => {
    router.back(); // 라우터 뒤로가기 기능을 이용
  }, [router]);

  function handleClickClose(e: React.MouseEvent<HTMLElement>) {
    if (clickedRef.current) {
      clickedRef.current = undefined;
      return;
    }

    e.stopPropagation();
    onClose();
  }

  return (
    // 모달 외부
    <div
      className={`modal_outer`}
      onMouseUp={handleClickClose}
    >
    	// 모달 내부
        <div
          className={`modal_inner`}
          onMouseDown={(e) => clickedRef.current = e.target}
          onMouseUp={(e) => clickedRef.current = e.target}
        >
          {children}
          <button onClick={handleClickClose}> 닫기 </Button>
        </div>
    </div>
  );
};

export default Modal;

 

@modal/(.)portfoio/[portfolioID]/page

일단 모달일 때 뜨는 컴포넌트는 여기서 정의한다!

// 모달일 때
const PortfolioModal = async ({ params }: { params: { id: string };}) => {
  const { id } = params;
  const portfolio = await getPostData(id); // get 메소드

  return (
      <Modal> // 모달 공통 컴포넌트를 만들고
          <PortfolioMain portfolio={portfolio} /> // 자식 컴포넌트1
          <PortfolioInfo portfolio={portfolio} /> // 자식 컴포넌트2
      </Modal>
  );
};

export default PortfolioModal;

 

portfolio/[portfolioID]/page

이친구는 해당 페이지로 정직하게 주소창(?)를 치고 들어갔을 때 우리를 반기는 페이지 컴포넌트이다 (비록 모달에게 밀렸지만)

// 페이지 일 때
const PortfolioDetail = async ({ params }: { params: { id: string }}) => {
  const { id } = params;
  const portfolio = await getPostData(id);

  return (
    <div>
      <h1>{portfolio.title}</h1>
      <p>{portfolio.date}</p>
		.
        .
		.
    </div>
  );
};

export default PortfolioDetail;

 

 

app/layout.tsx

레아이웃 파일은 최상단 디렉터리인 page(/)와 하위 page(/portfolio)에  공통으로 적용되는 레이아웃이다.

import { Noto_Sans_KR } from "next/font/google";

const notoSansKr = Noto_Sans_KR({ // implement google font! 
  subsets: ["latin"],
  weight: ["400", "500", "700"],
});

export const metadata: Metadata = {
  title: "SBJA's Portfolio",
  description: "Welcome to SBJA's Portfolio",
};

export default function RootLayout({
  children,
  modal, // @으로 시작하는 디렉터리명
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <html lang="en" className={notoSansKr.className}>
      <body>
        <Header />
        {children}
        <Footer />
        {modal} // 모달 컴포넌트는 여기에 위치
      </body>
    </html>
  );
}

 

 

app/page.tsx

여기는 app 라우터의 최상단! 메인 페이지의 컴포넌트들이 위치하는 홈이다

export default function Home() {
  const portfolios = getPortfolioList();
  
  return (
    <main>
    {portfolios.map(item => 
      <Link href={`/portfolio/${item.id}`}> // url 이동을 하게 되면 로직을 타고 모달이 뜬다!
        포트폴리오 {item.id} 페이지로 이동
      </Link>
     }
    </main>
  );
}

 

 

 

 

결과화면 미리보기 

아주 만족 스럽다! 성 - 공 :)

 

반응형

댓글