좋아, 이번 편은 지금까지 했던 걸 **“실전 트러블슈팅 관점”**에서 정리해보자.
즉, 실제 서비스에서 “갑자기 느려졌다 / 특정 페이지만 버벅인다 / 배포했더니 LCP 깨졌다” 같은 상황이 터졌을 때
어떤 순서로 성능 문제를 추적하고 해결할지, 단계별 플로우로 가져갈 거야.
1. “느려요”라는 말은 너무 추상적이다
실무에서 흔한 요청이 이런 거다.
- “유저들이 메인 페이지가 느리다고 함”
- “어드민 대시보드 들어가면 브라우저가 버벅임”
- “모바일에서만 페이지가 엄청 오래 걸린다”
이때 바로 코드부터 고치기 시작하면 거의 100% 돌아서 다시 뜯어야 한다.
성능 문제는 “느림의 종류를 먼저 좁히는 것” 이 핵심이다.
크게 나누면 이렇게 볼 수 있어 👇
- 로딩이 느린가? (네트워크/번들/렌더링 초기 단계)
- 인터랙션이 느린가? (클릭/스크롤/입력 시 버벅임)
- 시간이 지날수록 느려지는가? (메모리 누수, 이벤트 중복, 캐시 문제)
먼저 이 세 개 중 어디에 속하는지부터 명확히 잡고 들어가야 한다.
2. 1단계 — 재현 & 관찰: “느려진 순간”을 눈으로 본다
2-1. 재현 시나리오를 먼저 적어라
- 어떤 URL?
- 어떤 브라우저/OS/디바이스?
- 최초 진입? 새로고침 이후? 라우팅 이동 이후?
- 로그인 상태? 특정 데이터 조건?
이걸 텍스트로 적어두면,
나중에 “성능 개선 전/후 비교”에도 기준이 된다.
2-2. DevTools Performance / Network 켜놓고 재현
- Chrome DevTools → Performance 탭 → Record
- 문제 상황을 실제로 재현
- Stop 눌러서 Flame Chart/CPU 사용량 확인
여기서 먼저 보는 건 두 가지다.
- Main Thread가 어디서 오래 걸리는지
- Recalculate Style / Layout / Paint가 비정상적으로 많은지
Network 탭도 같이 본다.
- 특정 API가 오래 걸리나?
- JS/CSS 번들이 비정상적으로 크거나, 여러 번 로드되나?
- TTFB(서버 응답 시작시간)가 긴가?
이 단계에서 “느리다”를 “어디가 느리다”로 바꿔놓는 게 1차 목표다.
3. 2단계 — 로딩이 느릴 때 보는 것들
3-1. 초기 진입이 느리다 → 번들 + LCP 체크
- Lighthouse / Web Vitals 로 LCP 시간 확인
- Network 탭에서 JS/CSS/이미지 사이즈/로드 순서 체크
자주 나오는 원인 패턴:
- JS 번들이 1MB 이상인데 코드 스플리팅이 안 돼 있음
- Hero 이미지가 1~2MB짜리 JPEG로 바로 로딩
- 웹폰트 여러 개 로딩 + font-display 설정 없음
- CSS/JS를 <head>에서 동기 <script>로 막고 있음
3-2. 해결 방향
- 페이지별 코드 분할 (Dynamic Import, 라우트 기반 코드 스플리팅)
- LCP 요소(Hero 이미지, 제목, 주요 텍스트)를 Preload + 최적화된 포맷(WebP/AVIF) 로 전환
- 웹폰트는 font-display: swap + Preload
- Next.js라면 next/image, next/font 도입
즉, “처음 보여줄 것만 가볍게, 나머지는 나중에” 원칙으로 구조를 갈아야 한다.
4. 3단계 — 인터랙션(클릭/스크롤/입력)이 느릴 때
“페이지는 금방 뜨는데 스크롤만 하면 버벅인다 / 검색 조금만 해도 렉 걸린다”
이건 보통 렌더링/리렌더링/JS 연산이 문제다.
4-1. 체크 포인트
- 긴 리스트를 한 번에 렌더링 중인가?
- 수백~수천 개 DOM 노드를 그대로 뿌리는지
- 입력/검색 시마다 무거운 계산을 하고 있는지?
- 매 키 입력마다 filter/sort/map 돌리는지
- 불필요한 리렌더링이 발생하는지?
- 부모 state 바뀔 때 자식이 전부 다시 렌더되는 구조인지
4-2. React DevTools Profiler로 확인
- 어떤 컴포넌트가 몇 번이나 렌더되는지
- 한 번 렌더할 때 얼마나 시간이 걸리는지
자주 나오는 원인들:
- React.memo 없이 큰 컴포넌트 트리가 전부 리렌더
- useCallback/useMemo 없이 매 렌더마다 함수/객체 생성
- Context Provider value가 매번 새 객체로 생성되어 전체 구독자가 리렌더
4-3. 해결 패턴
- 자주 안 바뀌는 컴포넌트 → React.memo
- 자식에게 넘기는 함수/객체 → useCallback, useMemo 로 참조 고정
- 긴 리스트 → react-window, react-virtualized 로 virtualized list 적용
- 전역 상태 → selector 기반 구독(Zustand, Redux 등)으로 필요한 부분만 리렌더
핵심은 “불필요한 리렌더링을 얼마나 줄이느냐” 다.
5. 4단계 — 시간이 지날수록 느려질 때 (메모리/누수)
“처음엔 괜찮은데 탭을 오래 열어놓으면 점점 느려진다”
이건 대개 메모리 누수 혹은 이벤트/타이머 중복 등록 문제다.
5-1. 의심해야 할 것들
- setInterval, setTimeout 을 걸어놓고 clear 안 한 경우
- addEventListener만 하고 removeEventListener 안 하는 경우
- 클로저 안에서 큰 배열/객체를 잡아놓고 참조를 끊지 않는 경우
- SPA에서 페이지 이동 시 이전 화면 state나 DOM 참조를 안 끊은 경우
5-2. Memory 탭으로 확인
- Heap Snapshot을 여러 번 찍어보고,
- 특정 객체가 계속 사라지지 않고 축적되는지 확인
이런 패턴이 보인다면,
클린업 함수(useEffect cleanup, removeEventListener, clearInterval) 를 제대로 구현해야 한다.
6. 5단계 — 네트워크/API 병목
“화면 로딩은 괜찮은데 API 응답이 오래 걸린다”
이건 백엔드 이슈일 수도 있지만, 프론트 쪽에서도 할 수 있는 게 많다.
6-1. 체크 포인트
- 같은 데이터를 여러 컴포넌트에서 중복 요청하고 있지 않은가?
- 매 라우트 이동마다, 같은 리스트를 매번 새로 불러오고 있지는 않은가?
- 불필요하게 초당 여러 번 병렬로 호출하고 있지 않은가?
6-2. 해결 패턴
- React Query / SWR로 요청을 캐싱 + 중복 제거
- 스크롤/검색 입력에 Debounce / Throttle 적용
- “실시간처럼 보이면 되지만 굳이 진짜 실시간일 필요 없는” 요청 간격 조정
체감상 빠르면 됐지,
진짜 매 요청이 0.1초 빠를 필요는 없다.
7. 6단계 — Core Web Vitals 기준으로 다시 검증
마지막으로, 트러블슈팅이 끝났다면
Core Web Vitals(LCP, CLS, FID) 기준으로 결과를 검증하는 게 좋다.
- LCP:
- Hero 이미지/텍스트가 언제 뜨는지
- 이미지/폰트 최적화, Preload/Lazy Load 적용 여부
- CLS:
- 이미지에 width/height 설정했는지
- 동적 콘텐츠가 레이아웃을 밀어내지 않는지
- FID:
- 초기 JS 실행이 너무 무겁지 않은지
- 메인 스레드를 오랫동안 점유하는 연산이 없는지
Lighthouse, WebPageTest, Web Vitals 스크립트 등을 조합해서
개선 전/후 점수를 비교해보면 성능 작업이 “체감 + 수치” 둘 다에서 성공했는지 확인할 수 있다.
8. 실전 트러블슈팅 플로우 정리
한 장으로 정리하자면 👇
1️⃣ 문제 정의
- 어떤 페이지 / 어떤 상황 / 어떤 디바이스에서 느린가?
2️⃣ 재현 & 측정
- DevTools Performance/Network, Lighthouse로 수치화
3️⃣ 원인 분류
- 로딩 문제 (번들, 이미지, 폰트, SSR)
- 렌더링/리렌더링 문제
- 메모리/이벤트/타이머 누수 문제
- 네트워크/API 병목 문제
4️⃣ 수정 전략 적용
- 코드 구조/렌더링 최적화
- 이미지/폰트/애니메이션 튜닝
- 캐싱, 코드 스플리팅, Lazy Load, Suspense/Transition
- Web Worker, Debounce/Throttle 등
5️⃣ Core Web Vitals 기준 재검증
- LCP, FID, CLS 확인
- 개선 전/후 비교
이 플로우를 그대로 템플릿처럼 써먹으면,
새 프로젝트든 레거시든 성능 문제를 “감으로”가 아니라 체계적으로 해결할 수 있게 된다.
9. 마무리
성능 트러블슈팅은 “마법 같은 한 방 스킬”이 아니라,
관찰 → 분류 → 측정 → 수정 → 재검증 의 반복 루틴에 가깝다.
한 번 이 루틴이 몸에 익으면,
“느린데요?”라는 말이 들어와도
어디부터 봐야 할지, 어떤 도구를 켜야 할지, 어떤 순서로 손대야 할지가 자연스럽게 떠오른다.
결국 실력 차이는
“문제가 생겼을 때의 루틴”에서 드러난다.
'frontend > javascript' 카테고리의 다른 글
| 🟨 2-41. 프론트엔드 성능 아키텍처 완성 — ‘빠름’을 구조로 설계하기 (0) | 2025.11.07 |
|---|---|
| 🟨 2-39. 실전 성능 종합 튜닝 — React + Next.js 프로젝트 최적화 로드맵 (0) | 2025.11.07 |
| 🟨 2-38. 이미지·폰트·애니메이션 최적화 — 시각적 성능 튜닝의 모든 것 (0) | 2025.11.07 |
| 🟨 2-37. React 앱 구조 최적화 — Code Splitting, Dynamic Import, Bundle 분석 완전 가이드 (0) | 2025.11.07 |
| 🟨 2-36. React 성능 병목 진단법 — Re-render, Virtual DOM, Memoization 완전 정리 (0) | 2025.11.07 |