성능 최적화 - 1. 필요성,측정 도구 및 성능 저하 해결 방법

badahertz52·2024년 11월 6일
0

💫우테코_6기

목록 보기
15/17
post-thumbnail

'우아한테크코스' 레벨4의 프론트엔드 미션과 '리뷰미' 프로젝트에서 성능 최적화를 진행하며 공부한 내용을 정리한 글이다. 이 글은 성능 최적화의 필요성과 Webpack과 React 환경에서의 구체적인 성능 최적화 방법에 관심이 있는 독자를 위해 작성되었다.

성능 최적화 필요성

UX와 고객 유치

성능을 개선하는 것은 고객 유치와 유지에 밀접한 관계가 있다. 사이트 속도가 빨라지면 운영 비용이 감소하고, 사용자 경험(UX)이 개선된다. 성능 개선 사례로 Tokopedia와 Pinterest의 성공적인 최적화 결과를 들 수 있다.

  • Tokopedia : 3G 연결에서 렌더링 시간을 14초에서 2초로 줄여 방문자 19% 증가
  • Pinterest : 페이지 성능을 개선해 대기 시간을 40% 줄이고 SEO 트래픽과 전환율이 각각 15% 증가

구글의 연구에서도 성능이 고객 유치에 얼마가 영향을 주는 지 알 수 있다. 코어 웹 바이탈(Core Web Vitals)을 충족한 페이지는 방문자가 사이트를 떠날 확률이 24% 낮다. 모바일 기기의 경우 모바일 사이트의 53%가 로드하는데 3초 이상 걸리면 사용자는 해당 사이트를 떠났다.

  • 코어 웹 바이탈(Core Web Vitals)?
    구글이 웹 콘텐츠 이용자의 사용자 경험에 영향을 미치는 다양한 값 중 중요시 여기는 여러 가지로, LCP (Largest Contentful Paint), FID(First Input Delay), CLS(Cumulative Layout Shift)가 있다.

SEO (검색 최적화)

성능은 UX뿐만아니라 검색 엔진의 순위에도 영향을 준다.
2010년 구글은 사이트 속도가 검색 순위에 반영된다고 발표했다. 발표 이후 검색 순위에서의 사이트 속도의 중요도는 점점 커졌다.

결국 성능 최적화를 진행하는 이유는 '사용자에게 불편함 없는 서비스를 제공해 비즈니스적 이점을 취하는 것'에 있다.

브라우저 렌더링 과정 설명

성능 측정

측정 항목

성능 최적화의 목표가 UX 증대인 것처럼, 성능 측정 항목도 사용자가 실직적으로 서비스를 사용하는 방식과 밀접한 관련이 있다.

  • 인식된 로드 속도 : 페이지가 로드되고 모든 시각적 요소를 화면에 렌더링할 수 있는 속도
  • 로드 응답성 : 구성요소가 사용자 상호작용에 빠르게 응답하기 위해 페이지가 필요한 자바스크립트 코드를 로드하고 실행할 수 있는 속도
  • 런타임 응답성 : 페이지 로드 후 페이지가 사용자 상호작용에 얼마나 빠르게 반응할 수 있는지
  • 시각적 안정성 : 페이지 요소가 사용자가 예상하지 못한 방식으로 전환되어 상호작용을 방해하는지
  • 부드러움 : 전환 및 애니메이션이 일관된 프레임 속도로 렌더링되고 한 상태에서 다음 상태로 부드럽게 이어지는지

측정 지표

웹 성능을 측정하는 중요한 지표에는 LCP, FCP, CLS 등이 있다. 이러한 지표를 사용하여 웹 페이지의 성능을 평가하고 개선할 수 있다.

지표설명
LCP(Largest Contentful Paint)페이지 로드 중 가장 큰 이미지나 텍스트 블록이 화면에 표시되는 시간
FCP(First Contentful Paint)페이지 로드 중 처음으로 화면에 텍스트나 이미지가 나타나는 시간
CLS(Cumulative Layout Shift)예기치 않은 레이아웃 변화가 발생하는 빈도
TTFB(Time to First Byte)사용자가 요청 보낸 후 서버가 첫 번째 바이트를 응답하는 데 걸리는 시간
INP(Interaction to Next Paint)사용자가 상호작용(클릭,탭등)을 하고 그에 따른 화면 갱신이 일어나는 지연 시간

PageSpeedTools의 품질 평가 기준

지표좋음개선 필요나쁨
FCP0 ~ 1800ms1800ms ~ 3000ms3000ms 초과
LCP0 ~ 2500ms2500ms ~ 4000ms4000ms 초과
CLS0 ~ 0.10.1 ~ 0.250.25 초과

측정 도구

Goggle Lighthouse

웹 페이지의 종합적인 성능을 평가한다. 접근성, SEO, PWA 성능까지 측정 가능 및 성능 최적화 방법도 제시한다. 모바일과 데스크탑 성능을 평가할 수 있다.

  • Lighthouse 성능 측정 결과

    lighthouse에서 성능 측정 결과

PageSpeed Insights

실사용자 데이터를 바탕으로 웹 페이지의 속도와 성능을 분석한다. 모바일과 데스크탑 성능을 평가할 수 있다.

  • PageSpeed 측정 결과

    설명

WebPageTest

다양한 국가, 브라우저, 기기에서 테스트 가능해 다양한 환경에서 성능을 테스트할 수 있다. 또한 세부적인 성능 병목을 분석하는 데 유용하다.

  • WebPageTest에서 프랑스 3G 환경에서 테스트한 결과

    WebapageTest에서 프랑스에서 3G 환경에서 테스트한 모습

Chrome DevTools

크롬의 개발자 도구를 사용해 개발자 도구를 통해 페이지 로딩 중 발생하는 다양한 작업의 타이밍, 리소스 크기, 캐시 여부등을 분석할 수 있다. Network, Performance에서 네트워크나 CPU 감속에 따른 테스트도 진행할 수 있다. 크롬에서 제공하는 다양한 성능 측정 플러그인(ex : React Developer Tools, LCP&CLS Monitor)을 통해 확장된 여러 성능 측정이 가능하다.

  • 개발자도구에서 CPU와 네트워크의 성능을 낮춰서 성능을 측정한 모습

    개발자도구에서 cpu,네트워크 감속해서 성능 측정한 모습

GTmetrix

웹 페이지의 로딩 성능을 분석하고, 최적화 방안을 제시한다. 성능 이력 추적 및 세부적인 성능 분석 가능하다.

  • GTmetrix 성능 측정한 모습

     GTmetrix 성능 측정한 모습

  • 측정 도구별 주요 성능 측정 지표

    도구주요 성능 측정 지표
    Google LighthouseLCP, FCP, CLS, TBT, INP, PWA 성능
    PageSpeed InsightsLCP, FCP, CLS, TBT, TTI
    WebPageTestLCP, TTFB, CLS, Speed Index, First Byte, Time to Interactive
    Chrome DevTools Performance PanelJavaScript 실행 시간, 리플로우, 리페인트, 네트워크 요청 시간
    GTmetrixLCP, FCP, CLS, Page Load Time, Total Page Size, HTTP Requests

webpack-bundle-analyzer

Webpack을 사용 중이라면, webpack-bundle-analyzer를 사용해 번들링 된 리소스의 크기를 확인할 수 있다.

브라우저 렌더링 과정

성능 저하의 원인과 개선 방법을 이야기에 앞서, 브라우저 렌더링 과정에 대한 이해가 필요하다.

브라우저 렌더링 과정 설명

브라우저에는 여러 엔진이 있다. 그 중 브라우저 렌더링 과정과 직간접으로 영향이 있는 엔진은 렌더링 엔진과 자바스크립트 엔진이다. 렌더링 엔진은 리소스(HTML, CSS, 이미지등)를 파싱하여 웹 페이지에 그린다. 자바스크립트 엔진은 자바스크립트 코드를 파싱하여 페이지 상호작용 및 동적 변화를 처리한다.

렌더링 엔진에서 일어나는 브라우저 렌더링 과정은 다음과 같다.

렌더링 엔진의 브라우저 렌더링 과정

과정

1. 파싱 단계
HTML 파일을 파싱해 DOM을 생성하고, CSS 파일을 파싱해 CSSOM을 생성한다.

2. 레이아웃
DOM과 CSSOM을 결합하여 렌더 트리를 구성하고, 각 요소의 위치와 크기를 계산한다.

3. 페인팅
요소들의 스타일을 적용하여 화면에 출력할 내용을 결정한다.

4. 컴포지팅
이전 단계에서 만든 여러 레이어를 결합나다.

5. 화면에 표시
컴포지팅 단계에서 끝낸 결과물은 GPU 프로세서가 화면에 그린다.

리플로우,리페인팅,리컴포지팅

  • 리플로우 : DOM 구조나 레이아웃이 변경될 때 브라우저가 레이아웃을 다시 계산하는 과정

  • 리페인팅: 레이아웃 변화 없이 시각적 스타일이 변경될 때 화면을 다시 그리는 과정

  • 리컴포지팅 : 레이어가 변경되거나 애니메이션을 처리할 때 레이어를 GPU가 결합하는 과정

자바스크립트에 의한 DOM, CSSOM 변경

자바스크립트의 파싱 작업이 실행되는 시점은 자바스크립트를 실행하는 코드에 따라 다르다.

head태그나 body태그 시작점에 script 태그가 있다면, HTML 파싱 시 해당 코드를 만다면 HTML 파싱을 중단하고 자바스크립트 엔진이 자바스크립트 파일을 파싱힌다. 자바스크립트 파싱 작업이 끝난 후에 중단된 HTML 파싱을 재개한다.

body태그 끝에 있거나 script에서 defer을 사용했다면 HTML 파싱이 끝나 DOM이 생겨난 후에 자바스크립트 파싱 작업이 이루어진다.

scriptasync를 사용했다면 HTML 파싱 작업에서 해당 태그를 만나면 자바스크립트 파일을 로드하다가 로드가 끝나면 HTML 파싱 작업을 중단하고 자바스크립트 파싱작업을 실행한다.

이렇게 파싱된 자바스크립트 코드에 DOM, CSSOM에 변경을 주는 코드가 있다면, 리플로우와 리페인팅이 발생한다.

성능 저하 원인 및 해결 방법

로딩 성능

원인

로딩하는 리소스 파일이 크거나 HTTP 요청 건수가 많거나 서버의 지연으로 인해 로딩 속도가 느려진다. 로딩 속도가 느리면 페이지 렌더링이 지연되어 성능 저하를 일으킨다.

해결 방법

1. 요청 리소스 크기를 줄이기

💡자세한 성능 최적화 방법은 'Webpack에서 성능 최적화 하기'파트에서 다루고 있다.

  • JS 최적화 : 번들 도구를 사용한 압축화 및 난독화, Code Spliting, Lazy Loading(동적 import), Tree Shaking

  • CSS 최적화 : 번들 도구를 사용한 압축화 및 불필요한 코드 삭제

  • 이미지 최적화 : 이미지 포맷, 반응형 이미지, 이미지 압축

  • 폰트 최적화 : 필요한 폰트만 로드,WOFF2,Font Display ,서브셋 사용

2. 필요한 리소스 미리 요청하기
preconnect, preload, prefetch, prerender등을 사용해 필요한 리소스를 미리 요청힐 수 있도록 한다.

속성목적사용 시점리소스 유형특징
preconnect외부 리소스 서버와의 네트워크 연결을 미리 설정리소스를 가져오기 전에모든 리소스 (이미지, 스크립트 등)DNS 조회, TCP 핸드셰이크, TLS 협상까지 미리 수행해 첫 요청 시간을 단축
preload중요한 리소스를 우선적으로 미리 불러오기페이지 로딩 중JS, CSS, 글꼴, 이미지, 비디오 등중요한 리소스의 우선순위를 설정하고 빠르게 가져오도록 브라우저에 지시
prefetch미래에 사용할 리소스를 미리 불러오기현재 페이지에서 나중에 필요할 때모든 리소스나중에 사용할 가능성이 높은 리소스를 브라우저가 여유 있는 시간에 미리 다운로드하여 캐시에 저장.
dns-prefetch외부 도메인에 대한 DNS 조회를 미리 수행외부 리소스를 불러오기 전에모든 리소스DNS 조회만 미리 처리하여 첫 연결 시간을 단축, preconnect에 비해 범위가 제한됨
prerender미래에 방문할 가능성이 높은 페이지를 미리 렌더링페이지 이동 전에HTML 페이지해당 페이지 전체를 미리 로드하고 렌더링 해두고 페이지 전환을 빠르게 만듦, 많은 리소스가 소모될 수 있음

3. 가까운 곳에 필요한 리소스를 가져오기
CDN을 이용하면 리소스를 보다 가까운 곳에서 가져올 수 있다.
CDN은 콘텐츠를 사용자의 가까운 서버에서 제공하며 서버 분산을 통한 병목 현상 최소화등으로 지연 시간을 줄인다.

CDN 캐시와 API 요청에 대한 브라우저 캐시를 사용하면 origin 서버로부터 데이터를 받지 않고 캐시된 데이터를 즉시 사용할 수 있다.

3. 비동기 데이터 처리
동기 방식으로 데이터를 처리하면, 데이터를 받아오는 동안 브라우저의 메인 스레드가 차단돼 페이지 렌더링이 지연된다.

이를 방지하기 위해 비동기 방식으로 데이터를 요청해야한다. 비동기 데이터 처리를 사용하면, 데이터 요청과 동시에 렌더링이 수행된다.

여기에 Suspense를 적용할 수 있다. 데이터 요청 중에는 Suspense가 실행되다가 데이터 요청이 만료되면 해당 부분만 업데이트만 되어, FCP나 FMP가 빨라진다.

  • 비동기 처리 및 Suspense 예시 코드
// App.jsx
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Suspense fallback={<LoadingSpinner />}>
        <DataComponentWrapper />
      </Suspense>
    </QueryClientProvider>
  );
}

// DataComponentWrapper.jsx
function DataComponentWrapper() {
  const { data } = useQuery("fetchData", fetchData);
  return <DataComponent data={data} />;
}

4.Lazy Loading (지연 로딩)
Lazy Loading은 모든 리소스를 한 번에 로드하지 않고 시용자가 필요할 때 로드하는 방식이다. Lazy Loading을 사용하면 해당 화면에서 필요한 리소스만 부르기 때문에 초기 로딩 시간이 단축된다.

이미지 지연 로딩, React.lazy, 옵저버를 사용한 방법등을 통해 Lazy Loading를 구현할 수 있다.

  • 이미지 지연 로딩 예시 코드
<img src="image.jpeg" loading="lazy" alt="Lazy Loaded Image">
  • React.lazy로 특정 컴포넌트가 필요할 때 로드하는 예시 코드
const LazyComponent = React.lazy(() => import("./MyComponent"));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  );
}
  • 옵저버를 사용해 뷰포트에 옵저버 관찰 대상이 들어 올 때 이미지 로드 예시 코드
const imgObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src; // 실제 이미지 소스를 로드
      observer.unobserve(img); // 로드 후 관찰 중단
    }
  });
});

document.querySelectorAll("img[data-src]").forEach(img => {
  imgObserver.observe(img);
});

렌더링 성능

원인

렌더링 성능을 저하시키는 주요한 원인은 CPU을 사용하는 레이아웃과 페인팅 작업이다.

방법 1: GPU 애니메이션 사용

렌더링 성능을 개선하기 위해서는 CPU를 사용하는 리플로우와 리페인트보다 GPU를 사용하는 리컴포지팅을 활용해 스타일을 변경해야한다.

  • CPU 애니메이션 사용 시 Frame drop 일어난 모습

CPU 애니메이션 frame drop

  • GPU 애니메이션을 사용해 Frame drop 해결한 모습

    GPU 애니메이션 frame drop

리플로우, 리페인트과 리컴포지팅을 유발하는 css 속성

과정css 속성
리플로우(Reflow)크기(width, height), 위치(top, left), 마진(margin), 패딩(padding), 디스플레이(display), 플로트(float), 폰트 크기(font-size) 등
리페인트(Repaint)배경색(background-color), 글꼴 색상(color), 테두리 색상(border-color), 그림자(box-shadow) 등
리컴포지팅(Recompositing)변환(transform), 투명도(opacity), 애니메이션(animation), 고정 위치 요소(position: fixed)

CPU보다 GPU를 사용한 작업의 성능이 더 좋은 이유

1. GPU의 병렬 처리 능력

GPU는 수천 개의 코어를 갖춘 병렬 처리에 특화되었다. GPU는 한 프레임의 각 픽셀을 동시에 계산할 수 있기 때문에 애니메이션이나 복잡한 화면 전환에서 성능이 훨씬 뛰어나다. 반면 CPU는 소수의 강력한 코어로 설계되어 직렬 처리에 특화되었다.

2. GPU의 전용 하드웨어 최적화

CPU가 범용 처리 장치인 반면에 GPU는 그래픽 렌더링과 관련된 작업을 처리하도록 설계된 전용 하드웨어다. 3D 모델링, 이미지 렌더링, 텍스처 매핑과 같은 작업에서 뛰어난 성능을 발휘한다.

3. 리소스 분배 및 병목 현상 방지

GPU를 사용하면 CPU와 GPU 간의 작업 분할이 이루어지기 때문에, CPU가 다른 작업(ex : 자바스크립트 실행, 이벤트 처리)에 집중할 수 있다. 이는 병목 현상을 방지하고 전체적인 시스템 성능을 향상시킨다. 가령 CPU가 레이아웃 계산을 하는 동안 GPU는 컴포지팅 작업을 처리해 더 빠른 렌더링이 가능하다.

4. 애니메이션과 인터랙션에서의 부드러움

GPU 가속을 사용하면 60fps(초당 프레임 수)와 같은 고주사율 애니메이션을 쉽게 처리할 수 있다. GPU는 각 프레임을 빠르게 계산하여 사용자에게 부드러운 애니메이션을 제공한다. 반면에 CPU기반 애니메이션은 계산이 시간이 더 소요되어 Frame drop이나 jank(끊김 현상)이 발생할 가능성이 크다.

방법2: window.requestAnimationFrame

window.requestAnimationFrame을 애니메이션 최적화를 할 수 있다.

브라우저는 초당 60번 화면을 업데이트한다. window.requestAnimationFrame은 브라우저의 화면 업데이트 주기에 맞춰 콜백 함수를 실행해 애니메이션이 부드럽게 보이도록 한다.

  • 버튼을 클릭해 box위치를 오른쪽으로 50px 이동하는 리액트 코드
import React, { useState, useRef } from "react";

const MovingBox = () => {
  const [isAnimating, setIsAnimating] = useState(false);
  const positionXRef = useRef(0); // 현재 X 좌표
  const boxRef = useRef(null);

  const targetX = 50; // 이동할 거리 50px

  const animate = () => {
    if (positionXRef.current < targetX) {
      positionXRef.current += 1; // 1px씩 이동
      boxRef.current.style.transform = `translate3d(${positionXRef.current}px, 0, 0)`;
      requestAnimationFrame(animate); // 다음 프레임 요청
    } else {
      setIsAnimating(false); // 애니메이션 종료
    }
  };

  const handleClick = () => {
    if (!isAnimating) {
      setIsAnimating(true); // 애니메이션 시작
      positionXRef.current = 0; // 초기화
      requestAnimationFrame(animate); // 애니메이션 실행
    }
  };

  return (
    <div>
      <div
        ref={boxRef}
        style={{
          width: "50px",
          height: "50px",
          backgroundColor: "red",
          position: "absolute",
          top: "100px",
          transform: "translate3d(0, 0, 0)", // 초기 위치
        }}
      ></div>
      <button onClick={handleClick} style={{ marginTop: "200px" }}>
        Move Box
      </button>
    </div>
  );
};

export default MovingBox;

방법3: 불필요한 렌더링 막는 메모이제이션

sns에서 새로운 피드를 보기 위해, 새로 고침을 했다고 가정해보자. 새로운 피드가 화면에 보일 것이고 이전에 있던 피드들도 여전히 화면에 있다. 이전 피드와 새로운 피드가 모두 재렌더링되는 것이 아닌, 새로운 피드만 새로 렌더링 되어는 것이 렌더링 측면에서 효율적이다.

이를 위해서 메모이제이션을 사용할 수 있다.
메모이제이션은 이전에 계산한 결과를 캐시해두고, 동일한 입력값이 호출되면 캐시된 값을 반환하는 방법이다.

  • React.memo (메모이제이션)를 사용해 재렌더링 막은 후 React Developer Tools Profiler 실행 모습

    React.memo를 사용해 재렌더링 막은 화면

React에서의 메모이제이션

리액트는 컴포넌트 기반 구조와 가상DOM을 활용한 렌더링 파이프라인을 통해서 이전 연산 결과를 기억(=캐싱)하는 메모이제이션 기법을 제공한다.

  • 메모이제이션 방법
방법설명
React.memo컴포넌트 자체를 메모이제이션
useMemo값의 메모이제이션
useCallback함수의 메모이제이션

리액트 렌더링 파이프라인

리액트 렌더링 파이프라인 단계

  1. 렌더 단계: 상태,props 변경되면 새로운 가상 DOM을 생성하고 이를 기존 가상 DOM과 비교
  2. 조정 단계: 가상 DOM 비교로 실제로 비교된 부분을 찾아 실제 DOM에 적용할지 결정
  3. 커밋 단계: 변경 사항을 실제 DOM에 반영

상태,props가 변경되지 않으면 가상 DOM에서 캐싱된 값을 사용한다. 변경이 있다면 파이프라인을 통해 변경으로 인한 사항을 화면에 반영한다.

인터랙션 성능

너무 많은 자바스크립트가 메인 스레드를 차지하면 사용자 인터랙션이 지연될 수 있다. 비동기 처리를 통해 메인 스레드 블로킹 문제를 해결하고, debouncethrottle을 사용해 이벤트 핸들러를 최적화할 수 있다.

  • 디바운스 적용 전 Layout shift, Frame drop 측정 결과

    디바운스 적용 전 Layout shift, Frame drop 발생

  • 디바운스 적용 후 Layout shift, Frame drop 측정 결과

    디바운스 적용 후 Layout shift, Frame drop 개선

🖱️ 다음 편: Webpack에서 성능 최적화 하기

profile
세상과 사람을 잇는 개발을 꿈꾸는 프론트엔드 개발자

0개의 댓글