
SNS 프로젝트에서 다음 메인페이지의 레이아웃처럼 구현하려고 한다. Next.js에서는 Parallel Routes 기능을 사용해서 여러 페이지를 병렬 렌더링할 수 있다. 이 부분은 소셜 사이트에서 대시보드나 피드처럼 여러 콘텐츠를 보여줄 때 유용하다.

프로젝트는 다음과 같은 레이아웃으로 잡는다.
Navbar 컴포넌트Search 관련 컴포넌트이 레이아웃을 Next.js의 Parallel Routes 기능을 통해 구현할 예정이며, 프로젝트의 폴더 구조는 아래와 같이 구성한다.
/app
/dashboard
@sidebar
page.tsx // 사이드바 컴포넌트
@search
page.tsx // 검색 컴포넌트
page.tsx // 메인 콘텐츠
layout.tsx // 레이아웃 설정
Paralle Routes는 특정 경로에 여러 페이지를 병렬로 렌더링할 수 있게 해준다. 예를 들어, @team과 @analytics 경로를 동시에 렌더링하여 대시보드의 다양한 정보를 한 화면에 보여줄 수 있다.

폴더 구조에서 @sidebar, @search와 같은 이름을 사용하여 슬롯(Slots)를 정의할 수 있다. 이 슬롯들은 부모 레이아웃 컴포넌트로 전달되며, 병렬로 다른 콘텐츠와 함께 렌더링된다.
병렬로 렌더링되는 각 섹션을 관리하기 위해 layout.tsx 파일을 설정합니다. 이 파일은 children, sidebar, search와 같은 props를 받아서 적절한 위치에 콘텐츠를 배치한다.
// app/dashboard/layout.tsx
import { ReactNode } from 'react';
export default function DashboardLayout({
children,
sidebar,
search,
}: {
children: ReactNode;
sidebar: ReactNode;
search: ReactNode;
}) {
return (
<div className="dashboard-layout">
<aside>{sidebar}</aside> {/* 사이드바 */}
<main>{children}</main> {/* 메인 콘텐츠 */}
<div className="search-area">{search}</div> {/* 검색 영역 */}
</div>
);
}
여기서 @sidebar, @search와 같은 슬롯은 라우트 세그먼트가 아니기 때문에 URL 구조에 영향을 미치지 않는다. 예를 들어, @search 밑에 content 폴더가 있을 경우 @search 제외한 /content URL 경로로 렌더링할 수 있다.
따라서, { children } 같은 경우도 이 prop은 암묵적 slot이기 때문에 app/@children/page.ts로 처리되어 app/page.ts로 동작하는 것이다.
다음은 각 병렬로 렌더링되는 섹션에 해당하는 컴포넌트이다.
// app/dashboard/@sidebar/page.tsx
export default function Sidebar() {
return <div>@sidebar</div>;
}
// app/dashboard/@search/page.tsx
export default function SearchBar() {
return <div>@search</div>;
}
// app/dashboard/page.tsx
export default function MainPage() {
return <div>content</div>;
}
이와 같이 각 슬롯의 내용을 정의한 후, 브라우저에서 실행하면 각 섹션이 잘 렌더링되는 것을 확인할 수 있다.

만약에 페이지들이 잘 보이지 않는다면 /.next 파일을 삭제한 후에 다시 npm run dev를 해본다.

실제 프로젝트에 적용하기 위해서 폴더 구조를 변경한다. 프로젝트는 sidebar, main, search 세 부분으로 나뉘며, 로그인한 사용자만 접근할 수 있는 (authorized) 폴더 구조로 구성된다. 이 구조는 / 경로에서는 메인 페이지가, /profile 경로에서는 프로필 페이지가 보이도록 설정되어 있다.

메인 레이아웃은 children과 search 슬롯을 받아 화면에 렌더링한다.
// app/(authorized)/(main)/layout.tsx
export default async function MainLayout({
children,
search,
}: Readonly<{
children: React.ReactNode;
search: React.ReactNode;
}>) {
return (
<>
<div className="w-full lg:w-[70%]">
<div className="flex flex-col gap-6">{children}</div>
</div>
<div className="hidden lg:block w-[30%]">{search}</div>
</>
);
}
/profile 페이지에서 search 없이도 프로필만 렌더링할 수 있도록, /(authorized)/layout.tsx에서 sidebar와 children을 관리한다.
// app/(authorized)/layout.tsx
export default async function Layout({
sidebar,
children,
}: Readonly<{
sidebar: React.ReactNode;
children: React.ReactNode;
}>) {
return (
<div className="h-screen flex">
<aside className="w-[14%] md:w-[8%] lg:w-[16%] xl:w-[14%] p-6">
{sidebar}
</aside>
<main className="w-[86%] md:w-[92%] lg:w-[84%] xl:w-[86%] overflow-scroll flex">
{children}
</main>
</div>
);
}
/ 의 메인 페이지

/profile의 프로필 페이지

/profile로 url을 이동하면(새로고침하면) 404 오류가 발생한다. slot이 포함된 URL이 일치하지 않으면 404 오류가 발생하는데, @sidebar slot과 같은 경우 / url에서 보여지는 slot이기 때문에 /profile url에서는 페이지를 찾을 수 없다.
URL이 일치하지 않을 경우, 404 오류를 피하기 위해 @sidebar에 default.jsx를 추가한다. 이는 /profile 페이지에서 사이드바가 없을 때 404 대신 기본적인 사이드바를 렌더링하게 한다.
// app/(authorized)/@sidebar/default.jsx
export default function Default() {
return <div>@sidebar/default</div>;
}
/profile의 프로필 페이지
동일한 Sidebar 컴포넌트를 보여주기 위해 default.jsx에서도 page.tsx를 불러와 재사용한다.
// app/dashboard/@sidebar/default.jsx
import Sidebar from './page';
export default function DefaultSidebar() {
return <Sidebar />;
}
/profile의 프로필 페이지
Next.js는 클라이언트 측 탐색(Soft Navigation)과 전체 페이지 새로고침(Hard Navigation)에 따라 병렬 슬롯(parallel slots)의 동작을 다르게 처리한다.
Soft Navigation
사용자가 링크 클릭하면(sidebar에서 My Profile 버튼을 누르면) /profile로 이동해서 클라이언트 측에서만 필요한 부분을 업데이트하기 때문에 @sidebar가 보이면서 children 내용만 바뀐다. 이 부분은 클라이언트 측에서 탐색이 발생하는 것으로 현재 URL과 정확히 일치하지 않더라도 다른 슬롯(slot)의 활성 상태를 유지한 채로 부분적으로 페이지를 렌더링한다. 즉, 필요한 부분만 업데이트되고 다른 슬롯의 콘텐츠는 그대로 남아있다.
Hard Navigation
반면, 전체 페이지를 새로 고침하면 페이지 전체가 다시 로드되기 때문에 슬롯 상태가 초기화된다. URL과 일치하지 않는 슬롯에 대해 활성 상태를 잃어버려서 404 에러가 뜬다. 따라서 URL과 맞지 않는 슬롯에 대해서는 default.js를 렌더링해야 한다.
Reference.
Next.js Docs : Parallel Routes