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>
);
}