카테고리 없음

Next template 과 layout 파일

노엠디엔 2024. 9. 13. 15:45

layout.js 

layout 은 라우트 간에 공유되는 UI이다 즉  하위 경로로 공유되는 파일로써

여러 경로에서 공유되기 때문에 한 번 마운트 되고 나면 해당 레이아웃이
정의된 경로를 벗어나지 않는 한 UnMount 되지 않는다.

그렇기 때문에 Layout 컴포넌트는 상태를 저장하여
하위 컴포넌트들에게 전달하는 역할도 한다.

children (필수)
레이아웃 컴포넌트는 children prop을 받아서 사용해야 합니다.
렌더링 시 children에는 레이아웃이 감싸고 있는 라우트 세그먼트들이 채워지며,
주로 자식 레이아웃 이나 페이지의 컴포넌트가 들어가지만, 
상황에 따라 Loading 또는 Error와 같은 다른 특수 파일들이 포함될 수 있다.

 

params (선택 사항)
루트 세그먼트부터 해당 레이아웃까지의 동적 라우트 매개변수 객체이다.

params 객체에 현재 동적 라우트 url을 가져올 수 있다.

예시) [tag ]: shoes , item: nike-air-max-97

app/shop/[tag]/[item]/layout.tsx 해당 경로 예시 파일!

export default function ShopLayout({
  children,
  params,
}: {
  children: React.ReactNode
  params: {
    tag: string
    item: string
  }
}) {
  // URL -> /shop/shoes/nike-air-max-97
  // `params` -> { tag: 'shoes', item: 'nike-air-max-97' }
  return <section>{children}</section>
}
 

Root Layouts (상위 레이아웃)

  • app 디렉토리에는 app/layout.js라는 루트 레이아웃 파일이 포함되어야 한다.
  • 루트 레이아웃은 <html> 및 <body> 태그를 정의해야 한다.
  • <title> 및 <meta>와 같은 <head> 태그를 루트 레이아웃에 작성해서는 안되며.
    대신, 스트리밍 및 중복 제거와 같은 고급 요구 사항을 자동으로 처리하는
    Metadata API를 사용해야 한다.
  • 여러 루트 레이아웃을 생성하려면 라우트 그룹을 사용할 수 있다.
  • 여러 루트 레이아웃 간의 네비게이션은 전체 페이지 로드를 발생시킨다.
    (클라이언트 측 네비게이션과는 다름)
    예시), app/(shop)/layout.js를 사용하는 /cart에서 app/(marketing)/layout.js를 사용하는
    /blog로 이동하면 전체 페이지 로드가 발생한다.

nested layout(중첩 레이아웃)

각 레이아웃들은 중첩하여 사용 가능하며 라우팅 구조에 맞게 분배된다.

 

Layouts는 searchParams를 받지 않음
페이지와 달리, 레이아웃 컴포넌트는 searchParams prop을 받지 않는다.
이는 공유된 레이아웃이 네비게이션 중 다시 렌더링 되지 않기 때문에,
네비게이션 간에 searchParams가 오래된 상태(stale state)로
남을 수 있기 때문이다.

 

예시)

 

 

네비게이션 시 동작
/dashboard/settings에서 /dashboard/analytics로 이동할 때,
/dashboard/analytics의 page.tsx는 서버에서 다시 렌더링 됩니다.
하지만 dashboard/layout.tsx는 두 라우트 간에 공유되는 공통 UI이기 때문에
다시 렌더링되지 않습니다.

전체 라우트가 아니라 페이지에 대한 데이터 가져오기 및 렌더링만 수행되기 때문에
레이아웃을 공유하는 페이지 간 네비게이션이 빠르게 이루어진다.

 

searchParams의 상태
dashboard/layout.tsx는 다시 렌더링 되지 않기 때문에,

레이아웃 서버 컴포넌트의 searchParams prop이
네비게이션 후에 오래된 상태가 될 수 있지만,
Page의 searchParams prop이나 클라이언트 컴포넌트에서
useSearchParams 훅을 사용하여 최신 searchParams를 

렌더링 된 컴포넌트에서 사용할 수 있다.

 

Layouts는 pathname에 접근할 수 없다
레이아웃은 서버 컴포넌트이기 때문에, 클라이언트 측
네비게이션 중에 다시 렌더링 되지 않아

pathname이 네비게이션 간에 오래된 상태가 될 수 있기 때문입니다.

이를 방지하려면 Next.js는 라우트의 모든 세그먼트를 다시 가져와야 하며,

이는 캐싱의 이점을 잃고 네비게이션 중에
RSC(payload size) 크기가 증가할 수 있다.

pathname에 의존하는 로직을 클라이언트 컴포넌트로 추출하고

이를 레이아웃에 임포트 할 수 있습니다.

클라이언트 컴포넌트는 네비게이션 중에 다시 렌더링 되지만

다시 가져오지는 않으므로, Next.js 훅인 usePathname을 사용하여

현재 pathname에 접근하고 오래된 상태를 방지할 수 있습니다.

import { ClientComponent } from '@/app/ui/ClientComponent'
 
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <ClientComponent />
      {/* Other Layout UI */}
      <main>{children}</main>
    <>
  )
}

 

template.js

https://nextjs.org/docs/app/api-reference/file-conventions/template

 

File Conventions: template.js | Next.js

API Reference for the template.js file.

nextjs.org

 

Next에서의 template.js은  layout.js와 유사한 특수파일로

레이아웃이나 페이지를 감싸는 역할을 한다.
하지만 layout과 달리 template은 고유한 키를 가지며,
이는 자식 클라이언트 컴포넌트들이 네비게이션 시

상태를 초기화한다는 것을 의미한다.

layout은 여러 라우트 간에 지속되며 상태를 유지하는 반면,

template은 유지하지 않는다.

layout 대신에 teamplate 파일로도 다음과 같은 경우에는 사용할 수 있다고 하는데 

  • useEffect나 useState에 의존하는 기능이 필요한 경우
  • layout 내부의 Suspense 경계는 레이아웃이 처음 로드될 때만
    대체 UI(fallback)를 보여주고,
    페이지 전환 시에는 대체 UI가 나타나지 않는데 이때사용 가능

template은 서버 컴포넌트이지만, 'use client' 지시어를 통해
클라이언트 컴포넌트로도 사용할 수 있으며

사용자가 동일한 템플릿을 공유하는 라우트 간을 이동할 때,
해당 컴포넌트의 새로운 인스턴스가 마운트되며,
DOM 요소들이 다시 생성되고,
클라이언트 컴포넌트에서는 상태가 유지되지 않으며, 이펙트가 다시 동기화된다고 함!

 

이 template 파일을 이용해서 나는 Next에서 페이지 간 이동 시
애니메이션을 적용해보았다.

좀 더 수정과 최적화 부분에 있어서 코드 수정이 필요한데
나중에 정리해볼려고한다.

// src/app/template.tsx
"use client";

import { usePathname } from "next/navigation";
import { motion } from "framer-motion";
import {
  NavigationDirection,
  useRouterWrapper,
} from "@/provider/RouterWrapperProvider";
import {
  CLASS_PATHNAME,
  EVENT_PATHNAME,
  MAIN_PATHNAME,
  SOCIAL_PATHNAME,
} from "@/constants/path";
import Main from "@/components/main/Main";
import Class from "@/components/Class";
import Social from "@/components/Social";
import Event from "@/components/Event";

export default function Template({ children }: { children: React.ReactNode }) {
  const pathname = usePathname();
  const { direction, prevPath } = useRouterWrapper();

  return (
    <main className="relative justify-center flex-col gap-10 w-[1280px] m-auto pt-[96px]">
      <motion.div
        key={pathname}
        custom={direction}
        variants={{
          enter: (direction: NavigationDirection) => ({
            x: direction === "forward" ? "100vw" : "-100vw",
          }),
          center: {
            x: 0,
          },
        }}
        initial={"enter"}
        animate={"center"}
        transition={{ duration: 1, ease: "easeInOut" }}
      >
        {children}
      </motion.div>

      <motion.div
        key={`cache-${pathname}`}
        custom={direction}
        variants={{
          center: {
            x: 0,
          },
          exit: (direction: NavigationDirection) => ({
            x: direction === "forward" ? "-100vw" : "100vw",
          }),
        }}
        initial={"center"}
        animate={"exit"}
        transition={{ duration: 1, ease: "easeInOut" }}
        style={{
          position: "absolute",
          width: "100vw",
        }}
      >
        {prevPath === MAIN_PATHNAME && <Main />}
        {prevPath === CLASS_PATHNAME && <Class />}
        {prevPath === SOCIAL_PATHNAME && <Social />}
        {prevPath === EVENT_PATHNAME && <Event />}
      </motion.div>
    </main>
  );
}