C
Next.js/라우팅/Lesson 10

Layout vs Template — 상태 유지 vs 매번 새로 마운트

25분·theory
이 챕터
2/3
TypeScript

Layout vs Template — 상태 유지 vs 매번 새로 마운트

💡 왜 배워야 할까요? — 상태가 살아남느냐 죽느냐

🎯 App Router 에서 라우트 이동 시 layout.tsx 는 다시 마운트되지 않습니다 — 즉 그 안의 useState·useRef·스크롤 위치·재생 중인 비디오가 그대로 유지.
💼 이게 좋을 때도 있고 (사이드바·헤더), 나쁠 때도 있습니다 (페이지마다 입장 애니메이션을 다시 보여주고 싶을 때).
template.tsx 는 layout.tsx 와 모양은 같지만 **매 이동마다 새로 마운트** — useEffect 가 다시 실행됨.
🔗 보통 90% 는 layout 으로 충분. template 은 특수 케이스 (입장 애니메이션·페이지별 추적·강제 리셋) 에서만.
🏢 실무에서는
codemaster40 의 사이드바는 카테고리 사이를 이동해도 같은 사이드바를 유지합니다 — layout.tsx 의 효과. 만약 카테고리 이동마다 사이드바의 펼침/접힘 상태가 리셋된다면, 그건 template.tsx 로 잘못 짠 경우. 반대로 페이지마다 새 fade-in 애니메이션을 보여주고 싶다면 template.tsx 가 적합.

Layout · Template · 상태 유지 모델

1. layout.tsx — 자식 사이 이동 시 마운트 유지

tsx
// app/dashboard/layout.tsx
import { useState } from 'react';

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div>
      <Sidebar />
      <main>{children}</main>
    </div>
  );
}
  • /dashboard/posts/dashboard/users 이동 시: layout 은 그대로, children 만 새 페이지로 교체.
  • layout 안의 Client Component 가 useState 를 갖고 있다면 그 state 는 유지됨.

2. template.tsx — 매번 새로 마운트

tsx
// app/dashboard/template.tsx
'use client';
import { motion } from 'framer-motion';

export default function DashboardTemplate({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      transition={{ duration: 0.3 }}
    >
      {children}
    </motion.div>
  );
}
  • /dashboard/posts/dashboard/users 이동 시: template 이 새로 마운트 → fade-in 애니메이션 다시 실행.
  • 페이지별 입장 애니메이션·페이지뷰 추적(useEffect 가 매번 실행돼야 함) 에 적합.

3. 둘이 함께 있을 수도 있음

code
app/dashboard/
├── layout.tsx     ← 사이드바 (상태 유지)
├── template.tsx   ← fade-in (매번 마운트)
├── posts/page.tsx
└── users/page.tsx

렌더 순서: layout > template > page. 이동 시 layout 은 살아남고, template 과 page 가 매번 새로.

4. 시각화 — 상태 유지 모델

code
사용자: /dashboard/posts → /dashboard/users

[layout] -------- 같은 인스턴스 유지 (state 살아남)
  └─ [template] -------- 새 인스턴스 (state 리셋, useEffect 재실행)
       └─ [page] -------- 새 인스턴스 (당연)

5. 언제 무엇을?

사용 사례권장
헤더·사이드바·푸터 (전역 셸)layout
펼침/접힘 상태가 있는 사이드바layout (state 유지가 자연스러움)
페이지 단위 fade-in / slide-in 애니메이션template
useEffect 가 매 페이지 진입마다 실행되어야 함 (분석·페이지뷰)template
폼 자동 초기화 (페이지 떠났다 와도 깨끗)template
90% 의 경우layout

template 은 정말 필요할 때만. 무지성 template = 매번 마운트 비용 + 깜빡임.

💻 🅰️ template.tsx 만 쓴 안티패턴 — 상태가 매번 리셋
// ❌ template.tsx 에 사이드바를 둔 안티패턴

// 📁 app/study/template.tsx
'use client';
import { useState } from 'react';

export default function StudyTemplate({
  children,
}: {
  children: React.ReactNode;
}) {
  // ★ 사용자가 /study/javascript → /study/typescript 이동하면
  //   template 이 새로 마운트 → expanded 가 다시 false 로 리셋
  const [expanded, setExpanded] = useState(false);

  return (
    <div>
      <aside>
        <button onClick={() => setExpanded(!expanded)}>
          {expanded ? '접기' : '펼치기'}
        </button>
        {expanded && <NavList />}
      </aside>
      <main>{children}</main>
    </div>
  );
}

// 사용자 경험:
// 1. /study 진입 → 사이드바 접힘 상태
// 2. 사용자가 "펼치기" 클릭 → 열림
// 3. /study/javascript 진입 → template 재마운트 → 다시 접힘 😡
// 4. 다시 펼치기 → 또 다른 카테고리 이동 → 또 리셋
//
// 이런 상태 리셋은 사용자에게 매우 짜증나는 UX
💻 🅱️ layout.tsx 로 옮긴 정답 — 상태 유지, 페이지만 교체
// ✅ layout 으로 상태 유지 + 필요시 template 로 진입 애니메이션

// 📁 app/study/layout.tsx — 사이드바, 상태 유지
import { Sidebar } from './Sidebar';

export default function StudyLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex">
      <Sidebar />
      <main className="flex-1">{children}</main>
    </div>
  );
}

// 📁 app/study/Sidebar.tsx — Client (상태 보유)
'use client';
import { useState } from 'react';

export function Sidebar() {
  // ★ 이 state 는 카테고리 이동에도 살아남
  //   layout 이 재마운트되지 않으므로
  const [expanded, setExpanded] = useState(false);

  return (
    <aside>
      <button onClick={() => setExpanded(!expanded)}>
        {expanded ? '접기' : '펼치기'}
      </button>
      {expanded && <NavList />}
    </aside>
  );
}

// 📁 app/study/template.tsx — 페이지별 fade-in (선택)
'use client';
import { motion } from 'framer-motion';

export default function StudyTemplate({
  children,
}: {
  children: React.ReactNode;
}) {
  // ★ 페이지 이동마다 fade-in 다시 — template 이라 매번 마운트
  return (
    <motion.div
      initial={{ opacity: 0, y: 10 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.2 }}
    >
      {children}
    </motion.div>
  );
}

// 사용자 경험:
// 1. /study 진입 → 사이드바 접힘
// 2. 사용자가 "펼치기" 클릭 → 열림
// 3. /study/javascript 진입 → 사이드바 그대로 열림 유지 😊
//                              page 본문은 fade-in (template)
// 4. /study/typescript 이동 → 사이드바 그대로, 본문만 fade-in
//
// 상태가 자연스럽게 살아남고, 본문 전환은 부드러움

// 폴더 구조:
// app/study/
// ├── layout.tsx        ← 사이드바 (상태 유지)
// ├── template.tsx      ← 페이지 진입 애니메이션 (매번 마운트)
// ├── Sidebar.tsx       ← Client
// ├── page.tsx          ← /study
// ├── [category]/page.tsx
// └── [category]/[slug]/page.tsx

💡 💡 Layout vs Template 결정 가이드

1. 기본은 항상 layout. template 은 '왜 필요한가' 답할 수 있을 때만
무지성 template 사용 = 매번 마운트 비용 + state 리셋으로 인한 사용자 짜증.

2. 'state 가 페이지 이동에도 살아남으면 좋은가?'

  • 예 → layout (사이드바·전역 셸·재생 중인 비디오 플레이어)
  • 아니오 → template (페이지마다 깨끗하게 시작)

3. template 이 필요한 명확한 신호 3가지

  • 페이지 전환 애니메이션 (fade-in·slide-in)
  • 페이지뷰 분석 — useEffect 가 매 페이지 진입마다 실행되어야 함
  • 폼 자동 초기화 — 페이지 떠났다 와도 빈 상태

4. layout.tsx 도 'use client' 면 그 안의 state 가 유지
layout 이 Server 든 Client 든 인스턴스가 유지된다는 사실은 같음. Client layout 의 useState 는 정상 유지.

5. 둘 다 children 만 받는다 — 페이지 props 직접 못 받음
layout·template 모두 params 를 받을 수 있지만 (동적 라우트), 페이지의 데이터 props 를 받을 수는 없음. 데이터는 페이지가 직접 fetch.

⚡ 직접 실행해보기 — layout vs template 인스턴스 추적

라우트 이동마다 layout 과 template 이 어떻게 다르게 동작하는지 시뮬레이션합니다.
✏️ JS 코드
📟 콘솔 출력
▶ 실행 버튼을 눌러보세요
⚠️ 브라우저 샌드박스에서 실행 — console.log()만 지원, alert/fetch 불가

확인 퀴즈

사이드바의 '메뉴 열림/접힘' 상태가 카테고리 이동 시에도 그대로 유지되어야 한다면, 그 사이드바는 어디에 두어야 하나요?
💡 layout.tsx 는 그 폴더 하위 라우트 사이를 이동해도 **재마운트되지 않습니다**. 그 안의 Client Component 가 `useState` 로 가진 상태(예: 펼침/접힘)는 그대로 살아남습니다. 만약 template.tsx 에 두면 매 페이지 이동마다 컴포넌트가 새로 마운트되어 상태가 초기값으로 리셋됩니다 — '펼쳤는데 이동하니 또 접힘' UX 가 됩니다.
Layout vs Template — 상태 유지 vs 매번 마운트 - Next.js