Skip to Content
Frontend · 심화

프론트엔드 성능 최적화

성능 최적화는 감이 아니라 측정에서 시작한다. 무엇이 느린지 숫자로 확인하고, 가장 큰 병목부터 손대야 한다. 리렌더·번들·이미지·네트워크 — 흔한 병목 네 가지를 측정 가능한 지표 위에서 다룬다.
1

측정이 먼저: Core Web Vitals

최적화의 첫걸음은 ‘무엇을 좋게 만들 것인가’를 숫자로 정하는 일이다. 구글의 Core Web Vitals는 사용자 체감 성능을 세 지표로 요약한다. 추측 대신 이 값을 먼저 잰다.

지표측정하는 것주요 원인
LCP가장 큰 콘텐츠가 그려지는 시점느린 서버 응답, 큰 이미지, 렌더 차단 리소스
CLS예기치 않은 레이아웃 이동량크기 미지정 이미지, 늦게 삽입되는 배너·폰트
INP상호작용에 대한 응답 지연메인 스레드를 막는 긴 자바스크립트

도구로 재고, 원리로 이해한다

Lighthouse·DevTools Performance·실사용자 데이터(RUM)로 측정한다. 이 지표들이 왜 나빠지는지는 결국 브라우저 렌더링 원리 — 렌더 차단, 리플로우, 메인 스레드 점유 — 로 설명된다.

2

불필요한 리렌더 줄이기

React에서 INP·버벅임의 흔한 원인은 ‘바뀌지 않아도 될 컴포넌트가 다시 그려지는’ 것이다. 리렌더 자체가 죄는 아니지만, 비싼 트리가 자주 리렌더되면 문제가 된다. 먼저 원인을 찾고, 그다음 메모이제이션을 쓴다.

리렌더를 부르는 패턴과 처방
// 문제: 부모가 리렌더될 때마다 새 객체/함수가 생겨 자식도 리렌더
function Parent() {
  const [count, setCount] = useState(0)
  const style = { color: 'red' }              // 매번 새 참조
  return <Child style={style} onClick={() => {}} />  // 매번 새 함수
}

// 처방: 참조를 안정화하고, 자식을 memo로 감싼다
const style = { color: 'red' }                 // 컴포넌트 밖으로
const Child = React.memo(function Child(props) { /* ... */ })
const onClick = useCallback(() => { /* ... */ }, [])

원인부터 찾기

React DevTools Profiler로 ‘무엇이 왜 리렌더됐는지’ 먼저 본다. 상태를 쓰는 곳 가까이로 내리거나(상태 내리기), 자주 바뀌는 부분만 분리하면 메모이제이션 없이도 해결되는 경우가 많다.

과한 메모이제이션

useMemo·useCallback도 비교 비용이 든다. 모든 것을 감싸면 오히려 복잡해지고 느려질 수 있다. 측정으로 ‘비싼 곳’에만 선별 적용한다.

상태의 출처를 구분하고 렌더링을 최적화하는 더 구체적인 패턴은 React 핵심 패턴 글에서 이어서 본다.

3

번들 크기와 코드 스플리팅

내려보내는 자바스크립트가 많을수록 다운로드·파싱·실행이 모두 느려진다. 첫 화면에 필요 없는 코드는 나눠서(코드 스플리팅) 나중에 불러오는 것이 핵심이다.

동적 import로 필요할 때 로드
// 무거운 차트 라이브러리를 첫 로드에 포함하지 않는다
const Chart = lazy(() => import('./HeavyChart'))

function Dashboard() {
  return (
    <Suspense fallback={<Spinner />}>
      <Chart />          {/* 이 컴포넌트가 필요할 때 비로소 청크를 받는다 */}
    </Suspense>
  )
}
  • 경로 기반 분할: 라우트 단위로 청크를 나누면 방문한 페이지의 코드만 받는다. 메타프레임워크는 이를 기본 제공한다.
  • 트리 셰이킹: 쓰지 않는 export는 번들에서 빠진다. 라이브러리를 통째로 import하지 말고 필요한 것만 가져온다.
  • 번들 분석: 번들 애널라이저로 ‘무엇이 큰지’ 눈으로 확인하고, 무거운 의존성은 더 가벼운 대안으로 교체한다.

덜 보내는 게 가장 빠르다

최적화 중 가장 효과가 큰 건 ‘아예 보내지 않는 것’이다. 정적인 부분은 자바스크립트 없이 보내는 접근이 메타프레임워크 의 아일랜드 아키텍처다.

4

이미지와 폰트

이미지는 보통 페이지에서 가장 무거운 자원이고, 폰트는 가장 흔한 레이아웃 이동(CLS) 원인이다. 둘 다 작은 습관으로 큰 개선을 얻는다.

이미지

현대 포맷(WebP·AVIF)과 반응형 크기(srcset)를 쓰고, 화면 밖 이미지는 지연 로딩(loading="lazy")한다. width/height를 지정해 자리를 미리 잡아 CLS를 막는다.

폰트

웹폰트 로딩 중 글자가 사라지거나 튀지 않게 font-display: swap을 쓰고, 핵심 폰트는 preload한다. 시스템 폰트를 대체 폰트로 잘 맞추면 전환이 자연스럽다.

CLS는 ‘자리 잡기’다

레이아웃 이동은 대부분 ‘크기를 모른 채 늦게 들어오는 요소’ 때문이다. 이미지·광고·임베드의 공간을 미리 예약하면 콘텐츠가 밀리지 않는다.

5

네트워크와 캐싱

코드를 줄였다면, 이제 ‘얼마나 빨리·얼마나 적게 받느냐’다. 캐싱과 사전 로딩을 활용하면 같은 자원을 반복해서 받지 않고, 필요한 것을 미리 준비할 수 있다.

캐싱과 CDN

정적 자산에 긴 캐시 수명과 콘텐츠 해시 파일명을 주면, 바뀌지 않는 한 다시 받지 않는다. CDN으로 사용자 가까이에서 내려주면 지연도 준다.

미리 준비하기

곧 필요할 자원은 preload, 곧 방문할 페이지는 prefetch로 미리 받아 둔다. 메타프레임워크는 링크가 보이면 다음 페이지를 자동 prefetch하기도 한다.

순서대로 정리하면

측정으로 병목을 찾고 → 불필요한 리렌더와 자바스크립트를 줄이고 → 이미지·폰트로 자리를 잡고 → 캐싱으로 반복 비용을 없앤다. 큰 병목부터 하나씩, 매번 다시 재면서 진행한다.

성능은 기능이 아니라 습관이다.
추측하지 말고 측정하고, 가장 큰 병목부터, 덜 보내는 쪽으로.