"로딩이 왜 이렇게 느려요?"
최근 Next.js 15로 개발한 서비스 Poromy AI의 문의 페이지에서 이런 피드백이 들어왔습니다. 페이지 로딩에 5-7초가 걸리는 문제였죠. 개발 환경에서는 전혀 느끼지 못했던 문제였는데, 프로덕션 환경에서 사용자들이 겪는 경험은 완전히 달랐습니다.
처음엔 막막했습니다. Next.js 15의 새로운 기능들을 제대로 활용하지 못한 건가? Supabase 쿼리가 문제인가? 아니면 배포 환경의 문제일까?
이번 포스팅에서는 Chrome DevTools Performance 탭을 활용해 실제 병목 지점을 찾아내고, 성능을 57% 개선한 과정을 공유하려고 합니다.
문제 상황
Chrome DevTools Performance 분석 결과
제가 처음 Performance 탭을 열었을 때 마주한 결과는 충격적이었습니다. 초기 성능 측정 결과는 다음과 같았습니다:
- LCP (Largest Contentful Paint): 5.64초
- TTFB (Time to First Byte): 2.4초
- FCP (First Contentful Paint): 3.2초
- Speed Index: 4.8초
- 총 로딩 시간: 7.9초
- JavaScript 실행 시간: 1.8초
특히 LCP가 5.64초라는 것은 사용자가 의미 있는 콘텐츠를 보기까지 거의 6초를 기다려야 한다는 뜻이었습니다. Google의 권장 기준인 2.5초를 훨씬 초과하는 수치였죠.
주요 병목 지점 식별
Performance 분석을 통해 발견한 주요 병목 지점들은 크게 세 가지로 분류되었습니다.
첫째, 네트워크 타임라인 분석에서 발견한 워터폴 로딩 패턴이었습니다. 0-4초 동안 메인 HTML 문서가 로딩되고, 4-5초는 유휴 시간으로 Critical Rendering Path가 차단되었으며, 5-7초에 모든 리소스가 동시에 요청되기 시작했습니다. 이는 전형적인 워터폴 패턴으로, 리소스들이 병렬로 로딩되지 않고 순차적으로 처리되고 있었습니다.
둘째, 서버사이드 렌더링 과정에서 순차적 DB 쿼리로 인해 4초가 소요되고 있었습니다. 불필요한 데이터 페칭과 함께 병렬 처리가 전혀 이루어지지 않고 있었습니다.
셋째, 이미지 로딩 문제가 심각했습니다. 히어로 이미지가 우선순위 설정 없이 마지막에 로딩되고 있었고, 레이지 로딩이 적용되지 않아 불필요한 리소스가 즉시 로딩되고 있었습니다.
상세 병목 분석
Performance 탭의 결과를 세부적으로 분석하여 다음과 같이 해결해야 할 문제를 구체화할 수 있었습니다:
- 서버사이드 렌더링 지연:
- 순차적 DB 쿼리
- 클라이언트 사이드 이슈:
- 동적 임포트 사용하지 않음
- 캐싱 미활용
- 컴포넌트 리렌더링
- 리소스 로딩 문제:
- 이미지 레이지 로딩 미적용
- 우선순위 설정 부재
이제부터 위의 문제를 어떻게 코드적으로 해결했는지 경험을 공유하겠습니다.
최적화 전략 및 구현
1. 서버사이드 병렬 데이터 페칭
가장 큰 문제였던 서버사이드 데이터 페칭은 Promise.all을 이용한 병렬 처리로 로직을 변경하여 해결했습니다:
// Before: 순차적 실행 (총 4초)
export default async function InquiryPage() {
const supabase = await createClient()
const inquiries = await supabase.from('inquiries').select('*')
const user = await supabase.auth.getUser()
const profiles = await supabase.from('profiles').select('*')
const answers = await supabase.from('answers').select('*')
}
// After: 병렬 실행 (총 1초)
export default async function InquiryPage() {
const supabase = await createClient()
const [inquiries, user, profiles, answers] = await Promise.all([
supabase.from('inquiries').select('*'),
supabase.auth.getUser(),
supabase.from('profiles').select('*'),
supabase.from('answers').select('*')
])
}
이 단순한 변경만으로도 서버사이드 렌더링 시간이 4초에서 1초로 단축되었습니다.
2. Next.js 15 정적 생성 활용
Next.js 15의 새로운 캐싱 옵션을 활용하여 자주 접근하는 페이지들을 미리 생성했습니다:
// Next.js 15의 새로운 캐싱 옵션 활용
export const revalidate = 300 // 5분마다 재검증
export const dynamic = 'force-static' // 가능한 정적 생성
// 자주 접근하는 페이지 미리 생성
export async function generateStaticParams() {
const supabase = await createClient()
const { data: inquiries } = await supabase
.from('inquiries')
.select('id')
.order('created_at', { ascending: false })
.limit(100)
return inquiries.map(inquiry => ({
id: inquiry.id
}))
}
3. 포괄적인 이미지 최적화
Next.js 15의 이미지 최적화 기능을 최대한 활용하기 위해 다음과 같이 next.config.ts파일을 설정했습니다:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
images: {
domains: [],
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
minimumCacheTTL: 60 * 60 * 24 * 365,
dangerouslyAllowSVG: true,
contentDispositionType: 'attachment',
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
},
...
return config
},
}
export default nextConfig
Modern Format 지원을 통해 AVIF와 WebP 포맷을 우선 사용하여 이미지 크기를 최대 60-80% 감소시켰습니다. 반응형 디바이스 크기 설정으로 각 디바이스에 최적화된 이미지를 제공하고, 장기 캐싱을 통해 재방문 시 성능을 개선했습니다.
4. React 컴포넌트 최적화
불필요한 리렌더링을 방지하기 위해 메모이제이션과 동적 임포트를 적용했습니다:
const InquiryCard = memo(({ inquiry }: { inquiry: Inquiry }) => {
const formattedDate = useMemo(() => {
return formatDistanceToNow(new Date(inquiry.created_at))
}, [inquiry.created_at])
const handleClick = useCallback(() => {
router.push(`/inquiry/${inquiry.id}`)
}, [inquiry.id, router])
return (
<div onClick={handleClick}>
{/* 카드 내용 */}
</div>
)
})
// 무거운 컴포넌트는 필요할 때만 로드
const HeavyComponent = dynamic(
() => import('./HeavyComponent'),
{
loading: () => <Skeleton />,
ssr: false
}
)
최적화 결과 및 성과
체계적인 최적화 적용 후 Chrome DevTools Performance 측정 결과는 다음과 같이 개선되었습니다:
지표 | 최적화 전 | 최적화 후 | 개선율 |
LCP | 5.64초 | 2.43초 | 57% 개선 |
TTFB | 2.4초 | 1.2초 | 50% 개선 |
FCP | 3.2초 | 1.5초 | 53% 개선 |
Speed Index | 4.8초 | 2.1초 | 56% 개선 |
총 로딩 시간 | 7.9초 | 4.5초 | 43% 개선 |
JavaScript 실행 시간 | 1.8초 | 0.5초 | 72% 개선 |
Lighthouse 점수도 크게 향상되었습니다. 성능 점수는 42점에서 91점으로 49점 상승했으며, 접근성은 95점에서 98점으로, 모범 사례는 87점에서 95점으로, SEO는 90점에서 100점으로 개선되었습니다.
사용자 경험 개선 사항
정량적인 지표 개선 외에도 다음과 같은 사용자 경험 개선이 이루어졌습니다. 즉각적인 피드백으로 체감 속도가 향상되었고, 레이아웃 시프트 제거로 안정적인 UI를 제공할 수 있게 되었습니다.
핵심 인사이트 및 교훈
이번 최적화 작업을 통해 얻은 핵심 인사이트는 다음과 같습니다.
첫째, 측정 우선 접근이 필수적입니다. Chrome DevTools를 활용한 정확한 문제 진단이 성공적인 최적화의 시작점이었습니다.
둘째, 병렬 처리의 중요성을 재확인했습니다. Promise.all을 활용한 단순한 변경만으로도 극적인 성능 개선이 가능했습니다.
셋째, 캐싱 전략의 효과를 체감했습니다. 적절한 캐싱으로 서버 부하 감소와 사용자 경험 개선을 동시에 달성했습니다.
마치며
지금까지 Chrome DevTools Performance 탭을 활용해 실제 병목 지점을 찾아내고, 실제로 성능을 개선하는 과정을 알아보았습니다.
이번 최적화 작업을 통해 Next.js 15 애플리케이션에서의 성능을 극적으로 개선할 수 있었습니다. 특히 체계적인 접근 방법과 데이터 기반 분석 덕분에 문제를 빠르게 해결할 수 있었습니다. 최근에는 Cursor AI를 활용하여 대부분의 작업을 진행하고 있습니다. Chrome DevTools를 통해 정확하게 문제를 진단하고, Cursor를 이용하여 빠르게 문제를 할 수 있었습니다. 이후에는 제가 사용하고 있는 Cursor AI를 소개하고 MCP 설정, .cursorrules 파일을 통해 Cursor 경험을 극대화하는 방법에 대해서도 공유해보겠습니다.
읽어주셔서 감사합니다.
참고
Poromy - GPT/Claude AI 자소서 프롬프트 아카이브
ChatGPT, Claude 등 AI 모델을 활용한 자소서 작성, 기업 분석, 채용 공고 분석을 위한 최고의 AI 프롬프트 아카이브
poromy.ai.kr
'Troble Shooting' 카테고리의 다른 글
React Query의 Query Key 효율적으로 관리해보자(feat. @lukemorales/query-key-factory) (4) | 2025.06.01 |
---|---|
Next.js 15에서 TypeScript 파일과 next-sitemap 통합하기 (0) | 2025.06.01 |
Next.js 애플리케이션에서 이미지 프록시 API로 외부 도메인 제한 문제 해결하기 (1) | 2025.06.01 |