12장. 모든 웹 개발자가 관심을 가져야 할 핵심 웹 지표

핵심 웹 지표(Core Web Vital)란?

  • 웹사이트에서 뛰어난 사용자 경험을 제공하는 데 필수적인 지표를 일컫는 용어
  • 구글에서 핵심 웹 지표로 꼽는 지표는
    • 최대 콘텐츠풀 페인트(LCP: Largest Contentful Part)
    • 최초 입력 지연(FID: First Input Delay)
    • 누적 레이아웃 이동(CLS: Cumulative Layout Shit)
  • 핵심까지는 아니지만, 특정 문제를 진단하는 데 사용될 수 있는 지표는
    • 최초 바이트까지의 시간(TTFB: Time To First Byte)
    • 최초 콘텐츠풀 페인트(FCP: First Contentful Paint)

최대 콘텐츠풀 페인트(LCP)

  • 페이지가 처음으로 로드를 시작한 시점부터 뷰포트 내부에서 가장 큰 이미지 또는 텍스트를 렌더링하는 데 걸리는 시간
  • 뷰포트 : 사용자에게 현재 노출되는 화면
  • 뷰포트 내부에서 ‘큰 이미지와 텍스트’
    • <img>
    • <svg> 내부의 <image>
    • poster 속성을 사용하는 <video>
    • url()을 통해 불러온 배경 이미지가 있는 요소
    • 텍스트와 같이 인라인 텍스트 요소를 포함하고 있는 블록 레벨 요소 (<p>, <div> 등이 포함됨)
  • 즉, 최대 콘텐츠풀 페인트란 사용자의 기기가 노출하는 뷰포트 내부에서 가장 큰 영역을 차지하는 요소가 렌더링되는 데 얼마나 걸리는지를 측정하는 지표
  • 좋은 점수란 해당 지표가 2.5초 내로 응답이 오는 것이다. 4초 이내로 응답이 온다면 보통, 그 이상이 걸리면 나쁨으로 판단됨

개선 방안

  • 텍스트는 언제나 옳다. 가능한 한 해당 영역은 텍스트로 채워라.
  • 이미지는 어떻게 불러오는 것이 좋을까?
    • <img> : 내부의 리소스는 HTML 파싱이 미처 완료되지 않더라도 프리로드 스캐너가 병렬적으로 리소스를 다운로드하므로 최대 콘텐츠풀 페인트 요소를 불러오기에 적절한 방법이다.
    • <svg> 내부의 <image> : <svg> 내부의 <image>가 로딩되는 순서는 자바스크립트 리소스의 다운로드가 완료된 후에야 이미지를 다운로드한다. 즉, 프리로드 스캐너에 의해 발견되지 않아 병렬적으로 다운로드가 일어나지 않는다. 이는 결국 최대 콘텐츠풀 페인트 점수에도 악영향을 미치므로 이러한 방식은 삼가는 것이 좋다.
    • <video>의 poster : 프리로드 스캐너에 의해 조기에 발견되어 <img>와 같은 성능을 나타낸다.
    • background-image : CSS에 있는 리소스는 항상 느리다. 브라우저가 해당 리소스를 필요로 하는 DOM을 그릴 준비가 될 때까지 리소스 요청을 뒤로 미루기 때문이다.

그 밖에 조심해야 할 사항

  • 이미지 무손실 압축
  • loading=lazy 주의 : 리소스를 중요하지 않음으로 표시하고 필요할 때만 로드하는 전략. 그저 로딩 속도만 늦출 뿐 지표 점수에는 도움이 되지 않는다.
  • fadein과 같은 각종 애니메이션
  • 클라이언트에서 빌드하지 말 것
  • 최대 콘텐츠풀 리소스는 직접 호스팅 : 다른 출처(origin)에서 이렇게 정제한 이미지를 가져오는 것은 최적화에 별로 좋은 영향을 미치지 않는다. 왜냐하면 이미 연결이 맺어진 현재 출처와는 다르게, 완전히 새로운 출처의 경우에는 네트워크 커넥션부터 다시 수행해야 하기 때문이다.

최초 입력 지연(FID)

  • 사용자가 페이지와 처음 상호 작용할 때부터 해당 상호 작용에 대한 응답으로 브라우저가 실제로 이벤트 핸들러 처리를 시작하기까지의 시작을 측정
  • 사용자가 얼마나 빠르게 웹페이지와의 상호작용에 대한 응답을 받을 수 있는지 측정하는 지표
  • 최초의 입력 하나에 대해서만 그 응답 지연이 얼마나 걸리는지 판단
  • 메인 스레드가 바쁜 경우, 자바스크립트 실행 환경은 ‘싱글 스레드’이기 때문에 자바스크립트가 이벤트 리스너와 같은 다른 작업을 실행할 수 없어 지연이 발생한다. 즉, 이벤트가 발생하는 시점에 최대한 메인 스레드가 다른 작업을 처리할 수 있도록 여유를 만들어 둬야 사용자에게 빠른 반응성을 보장할 수 있다.
  • 구글은 사용자 경험을 크게 4가지로 분류해 정의하는데, 이를 RAIL이라고 한다.
    • Response : 사용자의 입력에 대한 반응 속도. 50ms 미만으로 이벤트를 처리할 것
    • Animation : 애니메이션의 각 프레임을 10ms 이하로 생성할 것
    • Idle : 유휴 시간을 극대화해 페이지가 50ms 이내에 사용자 입력에 응답하도록 할 것
    • Load : 5초 이내에 콘텐츠를 전달하고 인터랙션을 준비할 것
  • 정리하자면, 최초 입력 지연이란 화면이 최초에 그려지고 난 뒤, 사용자가 웹페이지에서 클릭 등 상호작용을 수행했을 때 메인 스레드가 이 이벤트에 대한 반응을 할 수 있을 때까지 걸리는 시간을 의미한다.
  • 메인 스레드가 처리해야 하는 다른 작업이 많을수록 느려진다.
  • 이벤트 핸들러가 완료되는 데 걸리는 시간은 측정하지 않는다. 만약 이벤트 핸들러의 실행 시간을 측정하고 싶다면 Event Timing API를 사용하는 것이 좋다.
  • 좋은 점수를 얻기 위해서는 100ms 이내로 응답이 와야 하며, 300ms 이내인 경우 보통, 그 이후의 경우에는 나쁨으로 처리된다.

개선 방안

  • 실행에 오래 걸리는 긴 작업을 분리 : 몇 가지 대안을 연구해야 한다.
    • 꼭 웹페이지에서 해야 하는 작업인가
    • 긴 작업을 여러 개로 분리하기 : 단순히 실행이 오래 걸릴 것 같은 작업을 분리하는 것뿐만 아니라 웹페이지 최초 로딩에 필요하지 않은 내용을 나중에 불러오는 것도 포함된다. 이러한 리소스는 리액트의 Suspense와 lazy를, 혹은 Next.js의 dynamic을 이용해 나중에 불러오게 할 수 있다.
  • 자바스크립트 코드 최소화
    • 크롬 개발자 도구로 들어가서 커버리지를 클릭한다. 현재까지 웹 페이지에서 사용되지 않은 코드가 얼마나 있는지 확인할 수 있다.
    • 또 한가지 살펴볼 만한 것은 폴리필(polyfill)이다. 폴리필이란 브라우저에서 지원하지 않는 기능을 사용하기 위해 웹페이지에서 직접 구현하고 집어넣는 코드를 의미한다.
    • 폴리필의 크기는 제법 크기 때문에, 폴리필을 집어넣기 전에는 반드시 두가지를 먼저 확인해봐야 한다.
    • 폴리필이 필요한 환경인가?
    • 꼭 필요한 폴리필인가?
  • 타사 자바스크립트 코드 실행의 지연 : 타사 스크립트는 대부분 웹페이지 로드에 중요한 자원이 아니므로 <script>의 async와 defer를 이용해 지연 불러오기를 하는 것이 좋다.
    • defer : 해당 스크립트를 다른 리소스와 함께 병렬로 다운로드한다. 이 스크립트의 실행은 페이지가 완전히 로딩된 이후에 맨 마지막에 실행된다.
    • async : 해당 스크립트를 다른 리소스와 함께 병렬로 다운로드한다. async 리소스의 다운로드가 완료되어 버리면 다른 리소스의 다운로드가 완료되는 것을 기다리지 않고 바로 실행한다.
    • 둘 다 없는 경우 : script를 만나는 순간 다운로드가 우선되며, 다운로드가 완료되면 코드 실행이 우선된다. 다른 작업은 다운로드와 실행이 끝날 때까지 미뤄진다.
    • 만약 실제 사용자의 뷰포트 위치에 따라 불러와야 하는 컴포넌트라면 Intersection Observer를 이용해 뷰포트에 들어오는 시점에 불러오는 것이 좋다.

누적 레이아웃 이동(CLS)

  • 페이지의 생명주기 동안에 발생하는 모든 예기치 않은 이동에 대한 지표를 계산하는 것
  • 이 지표가 낮을수록, 즉 사용자가 겪는 예상치 못한 레이아웃 이동이 적을수록 더 좋은 웹사이트다.
  • 사용자의 가시적인 콘텐츠에 영향을 미쳐야 하기 때문에 뷰포트 내부의 요소에 대해서만 측정하며, 뷰포트 밖의 요소에 대해서는 측정하지 않는다.
  • 이 점수를 계산할 때 포함되는 내용은
    • 영향분율 : 레이아웃 이동이 발생한 요소의 전체 높이와 뷰포트 높이의 비율
    • 거리분율 : 레이아웃 이동이 발생한 요소가 뷰포트 대비 얼마나 이동했는지 를 곱해서 최종 점수를 계산한다.
  • 클라이언트에서 미치 노출이 예상되는 부분을 HTML로 자리 잡아두는 것이 누적 레이아웃 지표에 큰 도움이 된다는 점을 알 수 있다.
  • 0.1 이하인 경우 좋음, 0.25 이하인 경우 보통이며 그 외에는 개선이 필요한 나쁜 점수로 보고된다.

개선 방안

  • 삽입이 예상되는 요소를 위한 추가적인 공간 확보
  • 폰트 로딩 최적화
    • 폰트로 인해 발생할 수 있는 문제는 크게 두 가지다.
    • FOUT(flash of unstyled text) : HTML 문서에서 지정한 폰트가 보이지 않고 대체 기본 폰트로 보이고 있다가 뒤늦게 폰트가 적용되는 현상
    • FOIT(flash of invisible text) : HTML 문서에서 지정한 폰트가 보이지 않고, 기본 폰트도 없어서 텍스트가 없는 채로 있다가 뒤늦게 폰트가 로딩되면서 페이지에 렌더링되는 현상
    • 사용자 기기의 기본 폰트 이외에 다른 폰트로 웹페이지를 보여주고 싶다면 다음과 같은 점을 유념해야 한다.
    • <link>의 preload 사용 : rel=preload는 페이지에서 즉시 필요로 하는 리소스를 명시하는 기능이다. preload로 지정된 요소는 웹페이지의 생명주기에서 초기에 불러와야 하는 중요한 리소스로 간주되므로 브라우저는 리소스를 더 빠르게 사용할 수 있도록 준비해준다. 따라서 rel=preload로 스타일이나 폰트를 지정하면 페이지의 렌더링을 가로먹거나 레이아웃을 방해할 가능성이 줄어든다.
    • font-family
    • fallback : 이 옵션을 사용하면 100ms간 텍스트가 보이지 않고, 그 이후에 폴백 폰트로 렌더링한다. 그리고 3초 안으로 폰트가 로딩된다면 해당 웹 폰트로 전환하고, 그렇지 않다면 폴백 폰트를 계속 사용한다.
    • optional : 100ms간 텍스트가 보이지 않고, 그 이후에 폴백 폰트로 렌더링한다. 그러나 0.1초 이내로 폰트가 다운로드돼 있거나 캐시돼 있지 않다면 폴백 폰트를 사용한다. 브라우저가 네트워크 상태를 파악해 일정 기간 폰트를 다운로드하지 못한다면 연결을 취소한다.
  • 적절한 이미지 크기 설정
    • width, height 지정
    • 만약 사용자 뷰포트 너비에 맞춰 다른 이미지를 제공하고 있는 경우, 즉 반응형 이미지를 사용하고 싶다면 srcset 속성을 사용하는 것이 좋다.

최초 바이트까지의 시간(TTFB)

  • 브라우저가 웹페이지의 첫 번째 바이트를 수신하는데 걸리는 시간
  • 최초의 응답이 오는 바이트까지가 얼마나 걸리는지 측정하는 지표
  • 600ms 이상이 걸릴 경우 개선이 필요한 것으로 간주된다.
  • 서버 사이드 렌더링을 하고 있는 애플리케이션에서 주의 깊게 봐야 할 지표다.
  • 최초 바이트까지의 시간을 개선하려면
    • 서버 사이드 렌더링을 수행하고 있다면 로직을 최적화해 페이지를 최대한 빨리 준비시켜야 한다. 서버 사이드 렌더링 시에 API 호출이 필요하다면 이 호출 또한 최적화할 필요가 있다.
    • 웹페이지의 주된 방문객의 국적을 파악해 최대한 해당 국적과 가깝게 서버를 위치시키는 것이 좋다.
    • 리액트 서버 사이드 렌더링이라면 스트리밍 API를 사용하는 것이 좋다. 스트리밍을 사용할 경우 완성된 영역부터 조각조각 받을 수 있어 최초 바이트까지의 시간을 단축할 수 있다.

최초 콘텐츠풀 페인트(FCP)

  • 페이지가 로드되기 시작한 시점부터 페이지 콘텐츠의 일부가 화면에 렌더링될 때까지의 시간을 측정한다.
  • 1.8초 이내에 이뤄진다면 좋음, 3.0초 이내는 보통, 그 이후는 개선이 필요한 것으로 보고된다.
  • 개선하려면 다음 사항을 고려해야 한다.
    • 최초 바이트까지의 시간(TTFB)을 개선
    • 렌더링을 가로막는 리소스 최소화
    • Above the Fold에 대한 최적화 : 웹에서는 최초에 스크롤을 굳이 하지 않아도 보이는 영역을 말한다. 이 영역은 최대한 사용자에게 빠르게 무언가를 보여줘야 하는 영역이므로 게으른 로딩을 하거나 스크립트(리액트의 useEffect와 같이)에 의존해 요소가 렌더링되는 것을 피해야 한다.
    • 페이지의 리다이렉트 최소화
    • DOM 크기 최소화