최근에 익명고민함이라는 Next.js 프로젝트를 새로 시작했다.
이 프로젝트는 “익명으로 고민을 나누는 커뮤니티”를 만들고 싶어서 시작한 팀 프로젝트인데,
Next.js는 아직 익숙하지 않아 하나씩 공부하면서 구조를 잡고 있다.
이번 글에서는 프로젝트를 세팅하면서 받았던 피드백 중
가장 도움이 되었던 부분인 “루트에서 바로 페이지를 만들지 말고, 한 뎁스를 더 파서 관리하라”
이 부분을 실제로 어떻게 적용했는지를 정리해봤다.
처음엔 단순하게 이렇게 시작했다.
app/
├── layout.tsx
├── page.tsx
└── globals.css
app/page.tsx에서 바로 메인 페이지를 구성하고,
Header나 SearchFilter, TagBadge 같은 컴포넌트를 전부 여기에 넣었다.
// app/page.tsx
import Header from '@/components/common/Header'
import SearchFilter from '@/components/common/SearchFilter'
import TagBadge, { DEFAULT_TAGS } from '@/components/common/TagBadge'
export default function HomePage() {
return (
<><Header ... />
<SearchFilter ... />
<div className="flex flex-wrap gap-2 mt-4">
{DEFAULT_TAGS.map(tag => (
<TagBadge key={tag.value}>{tag.label}</TagBadge>
))}
</div>
</>
)
}
그런데 코드가 점점 길어지고,
“이 Header를 다른 페이지에서도 쓸 거면 layout에 두는 게 낫지 않을까?”
“root layout이랑 page 역할이 좀 섞여 있는 것 같다”
라는 피드백을 받았다.
Next.js의 App Router는 app/layout.tsx와 app/page.tsx 구조를 기본으로 쓰는데,
layout은 고정된 외피, page는 실제 콘텐츠 역할을 한다.
문제는 루트에 전부 넣으면
그래서 구조를 아래처럼 바꿨다 👇
app/
├── layout.tsx ← 전역 (html/body/provider)
├── (main)/ ← 메인 섹션 전용
│ ├── layout.tsx ← Header 포함
│ └── page.tsx ← 메인 페이지 내용
└── globals.css
app/layout.tsx전역 레이아웃은 html, body, Provider 등 최소한만 유지했다.
// app/layout.tsx
import './globals.css'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<body className="min-h-screen bg-background text-foreground antialiased">
{children}
</body>
</html>
)
}
app/(main)/layout.tsx메인 전용 레이아웃.
Header를 이쪽으로 옮겨서 /, /post/... 같은 페이지 전환 시에도
Header가 유지되도록 했다.
// app/(main)/layout.tsx
import Header from '@/components/common/Header'
export default function MainLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen bg-background text-foreground">
<HeaderclassName="glass-card border-b border-border/50"
containerClassName="flex items-center justify-between"
>
<div className="flex items-center gap-2">
<h1 className="text-xl sm:text-2xl font-extrabold gradient-text">익명고민함</h1>
</div>
</Header>
<main className="mx-auto max-w-4xl px-4 sm:px-6 py-6">{children}</main>
</div>
)
}
app/(main)/page.tsx이제 메인 페이지는 콘텐츠만 담당하게 깔끔해졌다.
// app/(main)/page.tsx
import SearchFilter from '@/components/common/SearchFilter'
import TagBadge, { DEFAULT_TAGS } from '@/components/common/TagBadge'
export default function HomePage() {
return (
<><SearchFilterplaceholder="내용이나 태그로 검색..."
className="text-base"
containerClassName="h-12 w-full rounded-[16px] bg-background border border-border/60 focus-within:ring-2 ring-ring/40"
/>
<div className="flex flex-wrap gap-2 mt-4">
{DEFAULT_TAGS.map((t) => (
<TagBadge key={t.value} size="md">
{t.label}
</TagBadge>
))}
</div>
</>
)
}
Header가 페이지 전환 시 유지된다.
/ → /post/1로 이동해도 Header가 새로 렌더링되지 않는다.
layout의 “지속 렌더링(persistent)” 개념을 직접 체감했다.
역할이 명확해졌다.
RootLayout → 전역 구조 (HTML, body, Provider)(main)/layout → Header + Container(main)/page → 실제 페이지 콘텐츠확장하기 쉬운 구조가 되었다.
나중에 /auth나 /mypage 같은 다른 섹션이 생겨도
(auth)/layout.tsx, (mypage)/layout.tsx를 만들어서 독립적으로 관리 가능하다.
Next.js의 “중첩 레이아웃” 개념을 이번에 처음 제대로 이해했다.
React로만 개발할 때는 컴포넌트를 공통으로 import해서 쓰면 된다고 생각했는데,
Next.js는 아예 폴더 구조 단위로 UI의 계층과 지속성을 관리한다는 게 새로웠다.
특히 “URL은 바뀌지 않지만 page만 교체될 때 layout은 그대로 유지된다”는
렌더링 구조가 진짜 강력했다.
처음엔 괜히 복잡해 보였는데,
이제는 “(main), (auth), (mypage)”처럼 폴더로 섹션을 구분하는 게
진짜 유지보수하기 좋은 구조라는 걸 몸으로 느꼈다.