🎯 학습 목표
- Next.js의 두 라우팅 시스템 차이점 완전 이해
- React useState의 배치 업데이트 메커니즘 마스터
- 실무에서 어떤 라우터를 선택해야 하는지 판단 능력 습득
파일 구조:
pages/
├── index.js // '/' 경로
├── about.js // '/about' 경로
├── blog/
│ ├── index.js // '/blog' 경로
│ └── [slug].js // '/blog/동적경로' 경로
├── _app.js // 전역 App 컴포넌트
├── _document.js // HTML 구조 커스터마이징
└── api/
└── hello.js // API 라우트
특징:
// pages/blog/[slug].js
export default function BlogPost({ post }) {
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}
export async function getServerSideProps({ params }) {
const post = await fetchPost(params.slug)
return {
props: { post }
}
}
파일 구조:
app/
├── layout.js // 루트 레이아웃
├── page.js // '/' 경로
├── loading.js // 로딩 UI
├── error.js // 에러 UI
├── not-found.js // 404 페이지
├── about/
│ └── page.js // '/about' 경로
├── blog/
│ ├── layout.js // 블로그 섹션 레이아웃
│ ├── page.js // '/blog' 경로
│ └── [slug]/
│ ├── page.js // '/blog/동적경로' 경로
│ └── loading.js // 해당 페이지 로딩 UI
└── api/
└── hello/
└── route.js // API 라우트
혁신적인 특징:
// app/blog/page.js - 서버 컴포넌트 (기본)
async function BlogPage() {
// 서버에서 직접 데이터 페칭
const posts = await fetch('https://api.example.com/posts')
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
</article>
))}
</div>
)
}
export default BlogPage
// app/layout.js (루트 레이아웃)
export default function RootLayout({ children }) {
return (
<html lang="ko">
<body>
<nav>전역 네비게이션</nav>
{children}
<footer>전역 푸터</footer>
</body>
</html>
)
}
// app/blog/layout.js (블로그 섹션 레이아웃)
export default function BlogLayout({ children }) {
return (
<div className="blog-container">
<aside>블로그 사이드바</aside>
<main>{children}</main>
</div>
)
}
// app/dashboard/page.js
import { Suspense } from 'react'
async function SlowComponent() {
await new Promise(resolve => setTimeout(resolve, 3000))
return <div>느린 컴포넌트 로드 완료!</div>
}
export default function Dashboard() {
return (
<div>
<h1>대시보드</h1>
<Suspense fallback={<div>로딩 중...</div>}>
<SlowComponent />
</Suspense>
</div>
)
}
| 상황 | 권장 라우터 | 이유 |
|---|---|---|
| 새 프로젝트 | App Router | 최신 기술, 더 나은 성능, React 18 기능 활용 |
| 기존 프로젝트 마이그레이션 | 단계적 접근 | 점진적으로 App Router로 이전 |
| 단순한 정적 사이트 | Pages Router | 러닝 커브 낮음, 충분한 기능 |
| 복잡한 대화형 앱 | App Router | 서버 컴포넌트, 스트리밍 등 고급 기능 |
배치 아님: 편지 1장씩 개별 배송 📮📮📮
배치: 편지 3장을 한 번에 묶어서 배송 📦
React에서 배치 업데이트:
배치 아님: setState 3번 → 렌더링 3번 😵
배치: setState 3번 → 렌더링 1번 😊
React 18 이전에는 이벤트 핸들러 내에서만 상태 업데이트를 배치 처리했습니다.(setTimeout, Promise 안에서는 한번에 처리가 안됌.)
React 18부터는 모든 상황에서 자동 배치 처리됩니다.
function Counter() {
const [count, setCount] = useState(0)
const [flag, setFlag] = useState(false)
console.log('렌더링 발생:', count, flag)
// React 17: 이벤트 핸들러에서만 배치 처리
const handleClick = () => {
setCount(c => c + 1) // 배치 처리 ✅
setFlag(f => !f) // 배치 처리 ✅
// 한 번만 리렌더링
}
// React 17: setTimeout에서는 배치 처리 안됨
const handleTimeout = () => {
setTimeout(() => {
setCount(c => c + 1) // 리렌더링 1회
setFlag(f => !f) // 리렌더링 2회 (총 2번 렌더링)
}, 1000)
}
// React 18: 모든 곳에서 배치 처리!
const handleTimeoutV18 = () => {
setTimeout(() => {
setCount(c => c + 1) // 배치 처리 ✅
setFlag(f => !f) // 배치 처리 ✅
// 한 번만 리렌더링
}, 1000)
}
return (
<div>
<p>Count: {count}</p>
<p>Flag: {flag.toString()}</p>
<button onClick={handleClick}>동기 업데이트</button>
<button onClick={handleTimeout}>비동기 업데이트 (17)</button>
<button onClick={handleTimeoutV18}>비동기 업데이트 (18)</button>
</div>
)
}
function UserForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
age: ''
})
const [errors, setErrors] = useState({})
const [isSubmitting, setIsSubmitting] = useState(false)
const handleSubmit = async (e) => {
e.preventDefault()
// 이 모든 상태 업데이트가 배치 처리됨 ✅
setErrors({}) // 에러 초기화
setIsSubmitting(true) // 로딩 상태
try {
await submitForm(formData)
// 성공 처리도 배치됨
setFormData({ name: '', email: '', age: '' }) // 폼 초기화
setIsSubmitting(false) // 로딩 완료
} catch (error) {
// 에러 처리도 배치됨
setErrors({ general: '제출 중 오류가 발생했습니다' })
setIsSubmitting(false)
}
}
return (
<form onSubmit={handleSubmit}>
{/* 폼 내용 */}
</form>
)
}
function ProductList() {
const [products, setProducts] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [page, setPage] = useState(1)
const fetchProducts = async () => {
// React 18: 모든 상태 업데이트가 배치됨
setLoading(true)
setError(null)
try {
const response = await api.getProducts(page)
// 이것들도 배치됨
setProducts(response.data)
setLoading(false)
} catch (err) {
// 에러 상태도 배치됨
setError(err.message)
setLoading(false)
}
}
return (
<div>
{loading && <div>로딩 중...</div>}
{error && <div>에러: {error}</div>}
{products.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
)
}
import { flushSync } from 'react-dom'
function UrgentUpdate() {
const [count, setCount] = useState(0)
const [message, setMessage] = useState('')
const handleUrgentUpdate = () => {
flushSync(() => {
setCount(c => c + 1) // 즉시 리렌더링
})
// 이 시점에서 이미 count가 업데이트된 상태
console.log('업데이트된 count 사용 가능')
setMessage('업데이트 완료') // 별도 배치로 처리
}
return (
<button onClick={handleUrgentUpdate}>
긴급 업데이트 (flushSync)
</button>
)
}
1. 현재 프로젝트 라우터 확인:
2. 라우터 분석 보고서 작성:
## 프로젝트 라우터 분석 보고서
### 현재 상태
- 사용 중인 라우터: [Pages/App/혼재]
- Next.js 버전: [버전]
- 주요 페이지 수: [개수]
### 장단점 분석
- 현재 라우터의 장점: [분석]
- 현재 라우터의 한계: [분석]
### 개선 방안
- App Router 마이그레이션 필요성: [Y/N]
- 우선순위 페이지: [목록]
- 예상 작업 기간: [기간]
3. useState 배치 업데이트 실험:
💡 핵심 포인트:
1. App Router는 미래, 새 프로젝트라면 무조건 App Router
2. 서버 컴포넌트로 성능과 SEO 크게 개선 가능
3. 배치 업데이트는 React 18에서 더욱 강력해짐
4. 실무에서는 점진적 마이그레이션 전략이 중요
이해가 안 되는 부분이 있거나 더 자세한 설명이 필요한 부분이 있다면 언제든 질문해주세요! 🚀
🎯 특정 페이지에서 최상단 레이아웃 제외하기
🚀 Next.js App Router에서 루트 레이아웃 건너뛰는 방법들
방법 1: Route Groups 사용하기 (가장 깔끔한 방법)
app/
├── layout.js // 기본 루트 레이아웃
├── page.js // 홈페이지 (루트 레이아웃 적용)
├── dashboard/
│ ├── layout.js // 대시보드 레이아웃 (루트 레이아웃 적용)
│ └── page.js
└── (standalone)/ // 🔥 Route Group - 독립된 레이아웃
├── layout.js // 완전히 새로운 루트 레이아웃!
├── admin/
│ ├── layout.js // 관리자 전용 레이아웃
│ └── page.js // 루트 레이아웃 없이 관리자 레이아웃만!
└── login/
└── page.js // 완전 독립된 로그인 페이지
jsx// ❌ 잘못된 사용
function BadExample() {
return (
<Suspense fallback={<div>로딩...</div>}>
{/* 여러 컴포넌트가 각각 다른 데이터를 로딩 */}
<UserProfile /> {/* 1초 */}
<UserPosts /> {/* 3초 */}
<UserFollowers /> {/* 2초 */}
</Suspense>
)
// 결과: 가장 느린 것(3초)까지 모두 기다림
}
// ✅ 올바른 사용
function GoodExample() {
return (
<div>
{/* 각각 독립적으로 로딩 */}
<Suspense fallback={<ProfileSkeleton />}>
<UserProfile />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<UserPosts />
</Suspense>
<Suspense fallback={<FollowersSkeleton />}>
<UserFollowers />
</Suspense>
</div>
)
// 결과: 각각 준비되는 대로 표시
}
// 실수 2: 불필요한 의존성
useEffect(() => {
if (user) {
fetchUserPosts(user.id).then(setPosts)
}
}, [user, setPosts]) // ❌ setPosts는 항상 동일한 함수
// 실수 3: 객체를 의존성으로 사용
const searchParams = { query: 'react', limit: 10 }
useEffect(() => {
searchPosts(searchParams).then(setPosts)
}, [searchParams]) // ❌ 매 렌더링마다 새 객체 생성
// 수정
useEffect(() => {
if (user) {
fetchUserPosts(user.id).then(setPosts)
}
}, [user]) // setPosts 제외
// ✅ 객체는 useMemo로 안정화
const searchParams = useMemo(() => ({
query: 'react',
limit: 10
}), [])
useEffect(() => {
searchPosts(searchParams).then(setPosts)
}, [searchParams])
const searchParams = useMemo(() => ({
query: 'react',
limit: 10
}), [])

✅ 써야 하는 경우:
// 1. React.memo된 자식에게 props로 전달할 때
const MemoizedChild = React.memo(({ onClick }) => { ... })
const onClick = useCallback(() => { ... }, [])
// 2. useEffect 의존성 배열에 함수가 들어갈 때
useEffect(() => {
expensiveFunction()
}, [expensiveFunction])
// 3. 커스텀 훅에서 함수를 반환할 때
function useAPI() {
const fetchData = useCallback(async () => { ... }, [])
return { fetchData }
}
// 4. 복잡하고 비싼 계산이 포함된 함수
const complexCalculation = useCallback(() => {
// 수백 줄의 복잡한 로직
}, [dependency])
❌ 쓰지 않아도 되는 경우:
// 1. 단순한 이벤트 핸들러
const handleClick = () => setCount(count + 1) // useCallback 불필요
// 2. 자식 컴포넌트가 React.memo가 아닐 때
<RegularChild onClick={() => doSomething()} /> // 괜찮음
// 3. 의존성이 항상 바뀌는 함수
const fn = useCallback(() => {
doSomething(alwaysChanging)
}, [alwaysChanging]) // 의미없음, 매번 재생성됨
🤔 Next.js에서 React.memo 사용 실태
📊 실무 현실: React.memo 사용 빈도
Next.js에서 React.memo 사용 현황
답: 생각보다 많이 안 씁니다!
그 이유들:
1️⃣ Server Components가 기본
jsx// Next.js 13+ App Router - 서버 컴포넌트 (기본)
async function BlogPage() {
const posts = await fetchPosts() // 서버에서 실행
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} /> // 서버에서 렌더링
))}
</div>
)
}
// React.memo가 필요 없음!
// 서버에서 한 번 렌더링되고 HTML로 전송되기 때문
2️⃣ Static Generation (SSG/ISR)
jsx// 빌드 타임 또는 요청 시 한 번만 렌더링
export async function generateStaticParams() {
return [{ slug: 'post-1' }, { slug: 'post-2' }]
}
async function BlogPost({ params }) {
const post = await getPost(params.slug)
return (
<article>
<h1>{post.title}</h1>
<PostContent content={post.content} /> // memo 불필요
</article>
)
}
🎯 Next.js에서 React.memo가 필요한 실제 상황들
1️⃣ Client Components에서 복잡한 상호작용
jsx'use client'
import { useState, memo, useCallback } from 'react'
export default function ProductPage() {
const [selectedOptions, setSelectedOptions] = useState({})
const [quantity, setQuantity] = useState(1)
const [reviewFilter, setReviewFilter] = useState('all')
// 이 함수들이 바뀔 때마다 모든 자식이 리렌더링될 수 있음
const handleOptionChange = useCallback((option, value) => {
setSelectedOptions(prev => ({ ...prev, [option]: value }))
}, [])
return (
<div className="product-page">
<ProductImages /> {/* 정적이라 memo 불필요 */}
<ProductOptions
options={selectedOptions}
onChange={handleOptionChange} // memo 필요할 수 있음
/>
<ProductReviews
filter={reviewFilter}
onFilterChange={setReviewFilter} // memo 고려
/>
{/* 매우 복잡한 컴포넌트라면 memo 적용 */}
<ExpensiveProductCalculator
options={selectedOptions}
quantity={quantity}
/>
</div>
)
}
// ✅ 복잡한 계산이 있는 컴포넌트는 memo 적용
const ExpensiveProductCalculator = memo(function Calculator({ options, quantity }) {
// 수백 줄의 복잡한 가격 계산 로직...
const totalPrice = useMemo(() => {
// 매우 복잡한 계산
return calculateComplexPrice(options, quantity)
}, [options, quantity])
return <div>총 가격: {totalPrice}</div>
})
2️⃣ 무한 스크롤, 대용량 리스트
jsx'use client'
import { memo } from 'react'
// ✅ 리스트 아이템은 memo 적용하는 것이 좋음
const ProductCard = memo(function ProductCard({ product, onAddToCart }) {
return (
<div <className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>{product.price}</p>
<button onClick={() => onAddToCart(product)}>
장바구니 추가
</button>
</div>
)
})
export default function ProductList() {
const [products, setProducts] = useState([])
const [cart, setCart] = useState([])
const handleAddToCart = useCallback((product) => {
setCart(prev => [...prev, product])
}, [])
return (
<div className="grid grid-cols-3 gap-4">
{products.map(product => (
<ProductCard // memo 덕분에 cart 변경시 다른 카드들은 리렌더링 안됨
key={product.id}
product={product}
onAddToCart={handleAddToCart}
/>
))}
</div>
)
}
3️⃣ 실시간 데이터가 있는 대시보드
jsx'use client'
import { useState, useEffect, memo } from 'react'
// ✅ 실시간으로 업데이트되지 않는 위젯은 memo 적용
const UserInfoWidget = memo(function UserInfoWidget({ user }) {
return (
<div className="widget">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
)
})
const StaticChartWidget = memo(function StaticChartWidget({ data }) {
return (
<div className="chart-widget">
{/* 복잡한 차트 렌더링 */}
<ComplexChart data={data} />
</div>
)
})
export default function Dashboard() {
const [user] = useState({ name: '김개발', email: 'kim@dev.com' })
const [chartData] = useState(expensiveChartData)
const [realTimeData, setRealTimeData] = useState(0)
// 1초마다 실시간 데이터 업데이트
useEffect(() => {
const interval = setInterval(() => {
setRealTimeData(prev => prev + Math.random())
}, 1000)
return () => clearInterval(interval)
}, [])
return (
<div className="dashboard">
{/* memo 덕분에 realTimeData가 바뀌어도 리렌더링 안됨 */}
<UserInfoWidget user={user} />
<StaticChartWidget data={chartData} />
{/* 실시간 데이터 (memo 불필요) */}
<div>실시간 값: {realTimeData}</div>
</div>
)
}
📈 실무에서의 React.memo 사용 가이드라인
✅ React.memo를 써야 하는 경우
대용량 리스트의 아이템 컴포넌트
jsx const ListItem = memo(({ item, onAction }) => { ... })
복잡한 계산이나 렌더링을 하는 컴포넌트
jsx const ComplexChart = memo(({ data }) => { /* 복잡한 차트 로직 */ })
자주 바뀌는 상태를 가진 부모의 자식들
jsx // 실시간 타이머가 있는 페이지에서
const StaticSidebar = memo(() => { ... })
Third-party 라이브러리 래퍼
jsx const ExpensiveMapComponent = memo(({ markers }) => {
return
})
❌ React.memo가 불필요한 경우
1. Next.js Server Components (기본적으로 서버에서 한 번만 렌더링)
2. 작고 간단한 컴포넌트
3. props가 자주 바뀌는 컴포넌트
4. 최적화 측정 없이 추측으로 적용


고정 값 ≠ memo 불필요
결론: 고정 값이어도 컴포넌트가 복잡하고 부모가 자주 리렌더링된다면 memo를 쓰는 것이 좋습니다! 🎯