
Next.js에서 서버 컴포넌트에서 클라이언트 컴포넌트로의 하이드레이션 과정에서 발생할 수 있는 지연 문제와 그 해결 방법을 정리했다.
// 컴포넌트 지연 로딩
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <div>로딩 중...</div>,
ssr: false // 필요한 경우 SSR 비활성화
});
// 라이브러리 지연 로딩
const Chart = dynamic(() => import('react-chartjs-2'), {
ssr: false
});
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
webpack: (config, { isServer }) => {
// 불필요한 모듈 제거
if (!isServer) {
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
net: false,
tls: false,
};
}
return config;
},
});
// ❌ 전체 라이브러리 임포트
import _ from 'lodash';
// ✅ 필요한 함수만 임포트
import { debounce } from 'lodash';
// 또는
import debounce from 'lodash/debounce';
// app/page.tsx (서버 컴포넌트)
import { Suspense } from 'react';
import StaticContent from './StaticContent';
import InteractiveSection from './InteractiveSection';
export default function Page() {
return (
<main>
<StaticContent /> {/* 서버 컴포넌트 */}
<Suspense fallback={<div>인터랙션 로딩 중...</div>}>
<InteractiveSection /> {/* 클라이언트 컴포넌트 */}
</Suspense>
</main>
);
}
// InteractiveSection.tsx
'use client';
import { useState, useEffect } from 'react';
interface UserData {
id: number;
name: string;
email: string;
}
async function fetchUsers(): Promise<UserData[]> {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Failed to fetch users');
}
return response.json();
}
export default function InteractiveSection() {
const [data, setData] = useState<UserData[] | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetchUsers()
.then(setData)
.finally(() => setLoading(false));
}, []);
if (loading) return <div>데이터 로딩 중...</div>;
return (
<section>
<h2>사용자 목록</h2>
{data?.map(user => (
<div key={user.id}>
<p>{user.name} ({user.email})</p>
</div>
))}
</section>
);
}
// utils/scheduler.ts
export function scheduleWork<T>(
work: () => T,
priority: 'high' | 'normal' | 'low' = 'normal'
): Promise<T> {
return new Promise((resolve) => {
const timeSlice = priority === 'high' ? 0 : priority === 'normal' ? 5 : 16;
setTimeout(() => {
resolve(work());
}, timeSlice);
});
}
// 사용 예시
useEffect(() => {
scheduleWork(() => {
// 무거운 계산 작업
processLargeDataset();
}, 'low');
}, []);
// workers/dataProcessor.ts
self.onmessage = function(e) {
const { data, type } = e.data;
switch (type) {
case 'PROCESS_DATA':
const result = heavyDataProcessing(data);
self.postMessage({ type: 'PROCESSED', result });
break;
}
};
// hooks/useWorker.ts
import { useEffect, useRef, useState } from 'react';
export function useWorker(workerUrl: string) {
const workerRef = useRef<Worker>();
const [isReady, setIsReady] = useState(false);
useEffect(() => {
workerRef.current = new Worker(workerUrl);
setIsReady(true);
return () => workerRef.current?.terminate();
}, [workerUrl]);
const postMessage = (data: any) => {
if (workerRef.current) {
workerRef.current.postMessage(data);
}
};
return { worker: workerRef.current, postMessage, isReady };
}
// hooks/useIntersectionObserver.ts
import { useEffect, useRef, useState } from 'react';
export function useIntersectionObserver(options?: IntersectionObserverInit) {
const [isIntersecting, setIsIntersecting] = useState(false);
const targetRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
setIsIntersecting(entry.isIntersecting);
}, options);
if (targetRef.current) {
observer.observe(targetRef.current);
}
return () => observer.disconnect();
}, [options]);
return { targetRef, isIntersecting };
}
// 컴포넌트에서 사용
function LazySection() {
const { targetRef, isIntersecting } = useIntersectionObserver();
return (
<div ref={targetRef}>
{isIntersecting ? <HeavyComponent /> : <Placeholder />}
</div>
);
}
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ko">
<head>
{/* 중요 폰트 미리 로딩 */}
<link
rel="preload"
href="/fonts/main.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
{/* 중요 CSS 미리 로딩 */}
<link rel="preload" href="/styles/critical.css" as="style" />
{/* 다음 페이지 미리 로딩 */}
<link rel="prefetch" href="/about" />
</head>
<body>{children}</body>
</html>
);
}
// components/OptimizedImage.tsx
import Image from 'next/image';
interface Props {
src: string;
alt: string;
priority?: boolean;
}
export default function OptimizedImage({ src, alt, priority = false }: Props) {
return (
<Image
src={src}
alt={alt}
width={800}
height={600}
priority={priority}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ..."
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
);
}