[리액트] 모던 리액트 딥다이브 12장

효효·2024년 12월 21일

리액트

목록 보기
5/5

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

12.1 웹사이트와 성능

사용자가 웹사이트에 접속했을 때 기대하는 사항

  1. 방문한 목적을 쉽게 달성할 수 있어야 함
  2. 방문한 목적을 달성하는데 시간이 짧아야 함
  3. 웹사이트에서 개인정보가 누출되는 등의 사고 없이 보안에 철저해야 함

12.2 핵심 웹 지표란?

구글에서 만든 지표로, 웹사이트에서 뛰어난 사용자 경험을 제공하는 데 필수적인 지표를 일컫는 용어이다

핵심 웹 지표 3가지

  1. 최대 콘텐츠 풀 페인트(LCP)
  2. 최초 입력 지연(FID)
  3. 누적 레이아웃 이동(CLS)

특정 문제를 진단하는 데 사용할 수 있는 2가지 지표

  1. 최초 바이트까지의 시간(TTFB)
  2. 최초 콘텐츠풀 시간(FCP)

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

12.3.1 정의

페이지가 처음으로 로드를 시작한 시점부터 뷰포트 내부에서 가장 큰 이미지 또는 텍스트를 렌더링하는 데 걸리는 시간을 말한다

뷰포트 내부에서 '큰 이미지와 텍스트'

  1. <img>
  2. <svb> 내부의 <img>
  3. poster 속성을 사용하는 <video>
  4. url()을 통해 불러온 배경 이미지가 있는 요소
  5. 텍스트와 같이 인라인 텍스트 요소를 포함하고 있는 블록 레벨 요소
    • 이 블록 레벨 요소에는 <p>, <div> 등이 포함된다

이미지와 텍스트가 각각 사용자의 시점에 언제 노출됐는지를 확인하는 정확한 시점은 W3C 문서에 나와 있다.

12.3.2 의미

  • 웹페이지가 로딩이 완료되어 사용자에게 노출되기까지 걸리는 시간의 기준을 DOMCotentLoaded 이벤트가 호출되는 시간으로 잡으면 안 된다
  • DOMCotentLoaded는 HTML 문서를 완전히 불러오고 파싱했을 때 발생하는 이벤트로, 페이지의 document를 대상으로 일어나며 단 한 번만 호출된다. 이 이벤트는 스타일 시트, 이미지, 하위 프레임의 로딩은 기다리지 않는다.
  • 사용자는 화면에 노출되는 부분만 로딩돼 있다면 사용자는 페이지 로딩이 완료되었다고 느끼기 때문에 뷰포트에 메인 콘텐츠가 화면에 완전히 전달되는 속도를 기준으로 한다.
  • 따라서 사용자에게 페이지의 정보를 화면에 전달하는 속도를 객관적으로 판단하기 위한 지표로 만들어진 것이 바로 최대 콘텐츠풀 페인트이다.

12.3.3 예제

  • 생략
  • LCP는 페이지 로딩에 따라 변화하는 지표이다. 사용자가 이용하는 디바이스의 크기에 따라 그리고 그것이 이미지와 같이 크기가 큰 리소스라면 실제로 로딩에 필요한 시간에 따라 LCP의 값이 달라질 수 있다

12.3.4 기준 점수

  • 기준 점수를 측정하는 방법은 자바스크립트 API를 호출하는 방법과 다른 도구를 활용하는 방법(더 많이 사용)이 있다
  • 해당 지표가 2.5초 내로 응답이 오는 것이 좋은 점수이다. 4초 이내로 응답이 온다면 보통, 그 이상이 걸리면 나쁨으로 판단한다

12.3.5 개선 방안

텍스트는 언제나 옳다

  • 가장 확실한 방법이다.
  • 이미지를 최적화해도 텍스트 노출이 훨씬 더 빠르다.

이미지는 어떻게 불러올 것인가?

  • 이미지 노출하는 방법으론 아래와 같이 여러 가지가 있다

  • <img>

    <img src="lcp.jpg" ... />
    • 브라우저의 프리로드 스캐너에 의해서 먼저 발견되어 빠르게 요청이 일어난다.
    • 프리로드 스캐너란 HTML을 파싱하는 단계를 차단하지 않고 이미지와 같이 빠르게 미리 로딩하면 좋은 리소스를 먼저 찾아 로딩하는 브라우저의 기능이다
    • HTML 파싱이 미처 완료되지 않더라도 프리로드 스캐너가 병렬적으로 리소스를 다운로드하므로 LCP 요소를 불러오기에 적절한 방법이다
  • <svg> 내부의 <img>

    <svg xmls="http://www.w3.org/1000/svg">
      <image href="lcp.jpg" />
    </svg>
    • 모든 리소스를 다 불러온 이후에 이미지를 불러온다
    • 따라서 프리로드 스캐너에 의해 발견되지 않아 병렬적으로 다운로드가 일어나지 않는다. 이는 결국 최대 LCP 점수에도 악영향을 미쳐 권장하지 않는다
  • <video>의 poster

    <video poster="lcp.jpg"></video>
    • poster는 사용자가 video 요소를 재생하거나 탐색하기 전까지 노출되는 요소다
    • 프리로드 스캐너에 의해 조기에 발견되어 적절한 방법이다
    • 향후 poster가 없는 video의 경우 video를 실제로 로딩해 첫 번째 프레임을 해당 poster 리소스로 대체할 예정이기 때문에 poster를 반드시 넣어주는 것이 좋다
  • background-image

    <div style="backgroud-image: url(lcp.jpg)">...</div>
    • CSS에 있는 리소스는 항상 느리다. 이러한 리소스는 브라우저가 해당 리소스를 필요로 하는 DOM을 그릴 준비가 될 때까지 리소스 요청을 뒤로 미루기 때문에 사용하지 않는 것이 좋다.

그 밖에 조심해야 할 사항

  • 이미지 무손실 압축 : 이미지는 가능한 한 무손실 형식으로 압축해 최소한의 용량으로 서비스하는 것이 좋다
  • loading=lazy 주의 : loading=lazy는 리소스를 중요하지 않음으로 표시하고 필요할 때만 로드하는 전략이기 때문에 LCP의 이미지에 사용하면 로딩 속도를 늦춰서 사용하지 않는 것이 좋다
  • fadein과 같은 각종 애니메이션 : 이미지가 그냥 뜨는 것보다 fadein을 쓰면 LCP도 늦어진다
  • 클라이언트에서 빌드하지 말 것 : 서버에서 빌드해온 HTML을 프리로드 스캐너가 바로 읽어서 최대 콘텐츠풀 페인트로 빠르게 가져가는 것이 최덕의 시나리오이다
  • 최대 콘텐츠풀 리소스는 직접 호스팅 : 이미 연결이 맺어진 현재 출처가 아니라 완전 새로운 출처의 경우에는 네트워크 커넥션부터 다시 수행하기 때문에 권장하지 않는다

12.4 최초 입력 지연(FID)

12.4.1 정의

  • 사용자가 페이지와 처음 상호 작용할 때(예: 링크를 클릭하거나 버튼을 탭하거나 사용자 지정 JavaScript 기반 컨트롤을 사용할 때)부터 해당 상호 작용에 대한 응답으로 브라우저가 실제로 이벤트 핸들러 처리를 시작하기까지의 시간을 측정한다
  • 쉽게 말해 사용자가 최초의 입력을 할 때 얼마나 빠르게 웹페이지와의 상호작용에 대한 응답을 받을 수 있는지를 측정하는 지표이다

12.4.2 의미

  • 대부분 웹사이트 내부의 이벤트가 반응이 늦어지는 이유는 해당 입력을 처리해야 하는 브라우저의 메인 스레드가 바쁘기 때문이다.
  • 메인 스레드가 바쁜 이유는 대규모 렌더링이 일어나고 있거나 다른 작업을 처리하는 데 리소스를 할애하고 있기 때문이다.
  • 즉, 이벤트가 발생하는 시점에 최대한 메인 스레드가 다른 작업을 처리할 수 있도록 여유를 만들어 둬야 사용자에게 빠른 반응성을 보장할 수 있다.

구글에서 정의한 사용자 경험 4가지(RAIL)

  • Response: 사용자의 입력에 대한 반응 속도를 50ms 미만으로 이벤트를 처리할 것 (FID에 해당당)
  • Animation: 애니메이션의 각 프레임을 10ms 이하로 생성할 것
  • Idle: 유휴 시간을 극대화해 페이지가 50ms 이내에 사용자 입력에 응답하도록 할 것
  • Load: 5초 이내에 콘텐츠를 전달하고 인터렉션을 준비할 것

정리

  • 화면이 최초에 그려지고 난 뒤, 사용자가 웹페이지에서 클릭 등 상호작용을 수행했을 때 메인 스레드가 이 이벤트에 대한 반응을 할 수 있을 때까지 걸리는 시간을 의미한다.
  • 이 사간은 메인 스레드가 처리해야 하는 다른 작업이 많을수록 느리다

12.4.3 예제

  • 크롬으로 디버깅하는 것은 이벤트가 처리되는 것이 얼마나 지연되는지만 알 수 있다
  • 즉, 최초 이벤트 발생으로부터 해당 이벤트의 핸들러가 실행되는 순간까지 사이의 기간만 측정한다
  • 만약 이벤트 핸들러의 실행 시간을 측정하고 싶다면 Event Timing API를 사용하는 것이 좋다

12.4.4 기준 점수

  • 좋은 점수의 기준은 100ms 이내로 응답이 와야 하며, 300ms 이내인 경우 보통, 그 이후에는 나쁨으로 처리된다

12.4.5 개선 방안

실행에 오래 걸리는 긴 작업을 분리

  • 만약 실행에 오래 걸리는 작업이 있다면 몇 가지 대안을 연구해야 한다

  • 꼭 웹페이지에서 해야 하는 작업인가?
    꼭 웹페이지에서 해야 되는 작업이 아니라면 서버로 옮겨서 처리하는 것이 좋다. 서버에서 처리하면 상대적으로 빠르고 쾌적한 환경에서 처리해 브라우저의 메인 스레드를 오래 점유하지 않게 할 수 있다.

  • 긴 작업을 여러 개로 분리하기
    하나의 긴 작업이 메인 스레드를 계속 점유할수록 사용자는 페이지에서 응답을 받지 못하고 있을 가능성이 크다. 웹페이지 최초 로딩에 필요하지 않은 내용을 나중에 불러오는 것도 포함해서 분리하는 것이 좋다

자바스크립트 코드 최소화

  • 현재 사용하지 않는 코드 중에서는 특정 이벤트에 따라 실행되는 코드, 예기치 못한 상황에서 실행될 코드 등 다양한 것들이 존재할 수 있다
  • 이러한 코드들은 당장에 급하지 않은 코드로 간주해서 앞서 언급한 지연 로딩 기법, 사용자가 필요로 하는 순간에 불러오거나 우선순위를 낮춰서 불러오는 것이 좋다
  • 폴리필 확인하기
    폴리필이란 브라우저에서 지원하지 않는 기능을 사용하기 위해 웹페이지에서 직접 구현하고 집어넣는 코드를 의미한다
    한 메서드에 들어가는 폴리필의 크기가 제법 크기 때문에 폴리필을 넣기 전에 반드시 2가지를 확인해 봐야 한다
    1. 폴리필이 필요한 환경인가? : 만약 구현 브라우저 환경을 지원하지 않기로 결심했다면 폴리필을 집어 넣을 필요가 없다
    2. 꼭 필요한 폴리필인가? : 애플리케이션의 여러 군데에서 자주 사용하는 코드인지 반드시 확인해 봐야 한다. 만약 사용하는 곳이 몇 군데 되지 않는다면 직접 저수준 자바스크립트 코드를 작성해 구현하는 편이 도움이 될 수 있다

타사 자바스크립트 코드 실행의 지연

  • Google Analytics나 Firebase와 같이 웹페이지의 통계 집계를 위해 제3자가 만든 타사 스크립트를 넣는 경우도 있다. 이러한 코드의 실행으로 메인 스레드가 점유될 수 있다.
  • 이러한 타사 스크립트는 대부분 웹 페이지 로드에 중요한 자원이 아니므로 <script>의 async와 defer를 이용해 지연 불러오기를 하는 것이 좋다.
    • defer
      먼저 해당 스크립트를 다른 리소스와 함께 병렬로 다운로드한다.
      다운로드 중에도 HTML 파싱 등의 메인 스레드 작업은 멈추지 않는다.
      다운로드가 완료됐다 하더라도 이 스크립트의 실행은 페이지가 완전히 로딩된 이후에 맨 마지막에 실행된다
    • async
      해당 스크립트를 다른 리소스와 함께 병렬로 다운로드한다.
      리소스의 다운로드가 완료되어 버리면 다른 리소스의 다운로드가 완료되는 것을 기다리지 않고 바로 실행한다.
      따라서 async 리소스의 실행 순서는 다운로드가 완료된 순서대로 실행된다
    • 둘 다 없는 경우
      script를 만나는 순간 다운로드가 우선되며, 다운로드가 완료되면 코드 실행이 우선된다. 다른 작업은 다운로드와 실행이 끝날 때까지 미뤄진다.
  • 따라서 타사 스크립트는 가능하면 async를, 더 가능하면 defer로 지연하는 것이 성능에 도움이 된다

12.5 누적 레이아웃 이동(CLS)

12.5.1 정의

  • 페이지의 생명주기 동안 발생하는 모든 예기치 않은 이동에 대한 지표를 개선하는 것이다.
  • 다른 지표와 마찬가지로 이 지표가 낮을수록, 즉 사용자가 겪는 예상치 못한 레이아웃 이동이 적을수록 더 좋은 웹사이트이다.

12.5.2 의미

  • 누적 레이아웃 이동은 사용자의 가시적인 콘텐츠에 영향을 미쳐야 하기 때문에 뷰포트 내부의 요소에 대해서만 측정한다.
  • 최초 렌더링이 시작된 위치에서 만약 레이아웃의 이동이 발생한다면 누적 레이아웃 이동 점수로 기록하게 된다.
  • 단순히 요소가 추가된다고 해서 무조건 누적 레이아웃 이동으로 간주되는 것은 아니다. 요소가 추가됐다 하더라도 다른 요소의 시작 위치에 영향을 미치지 않았다면 레이아웃 이동으로 간주되지 않는다.
  • 사용자 액션으로 인해 발생한 레이아웃 이동은 점수에 포함되지 않는다. - 이 점수를 계산할 때 포함되는 내용은 다음과 같다
    • 영향분율: 레이아웃 이동이 발생한 요소의 전체 높이와 뷰포트 높이의 비율을 의미한다
    • 거리분율: 레이아웃 이동이 발생한 요소가 뷰포트 대비 얼마나 이동했는지를 의미한다

12.5.3 예제

  • 생략

12.5.4 기준 점수

  • 좋은 점수는 0.1 이하인 경우, 0.25 이하인 경우 보통이며 그 외에는 개선이 필요한 나쁜 점수로 보고된다

12.5.5 개선 방안

삽입이 예상되는 요소를 위한 추가적인 공간 확보

  • useEffect의 내부에서 요소에 영향을 미치는 작업, 특히 뷰포트 내부에서 노출될 확률이 높은 작업은 최소화하는 것이 좋다.
  • 스켈레톤 UI처럼 미리 무언가 동적으로 뜰 것으로 예상되는 공간을 미리 확보해 두는 것도 좋은 방법이다. 레이아웃 이동을 막으면서 클라이언트 시점에 정해지는 콘텐츠를 안정적으로 보여줄 수 있다
  • 가장 좋은 방법은 서버 사이드 렌더링이다. 서버에서 이러한 동적인 요소의 유무를 사전에 판단해 클라이언트에 HTML을 미리 제공해 준다면 클라이언트에서는 이러한 고민을 할 필요 없이 깔끔하게 처리할 수 있다.

폰트 로딩 최적화

  • 폰트로 인해 레이아웃 이동이 발생할 수 있는 문제는 아래 2가지이다

    • FOUT: HTML 문서에서 지정한 폰트가 보이지 않고 대체 기본 폰트로 보이다가 뒤늦게 폰트가 적용되는 현상
    • FOIT: HTML 문서에서 지정한 폰트가 보이지 않고, 기본 폰트도 없어서 텍스트가 없는 채로 있다가 뒤늦게 폰트가 로딩되면서 페이지에 렌더링되는 현상
  • 사용자 기기의 기본 폰트 이외에 다른 폰트로 웹페이지를 보여주고 싶다면 아래와 같은 점을 유념해야 한다

    • <link>의 preload 사용
      • <link> 요소의 rel=preload는 페이지에서 즉시 필요로 하는 리소스를 명시하는 기능이다.
      • preload로 지정된 요소는 웹페이지의 생명주기에서 초기에 불러와야 하는 중요한 리소르로 간주되므로 브라우저는 리소를 더 빠르게 사용할 수 있도록 준비해 준다.
      • 따라서 rel=preload로 스타일이나 폰트를 지정하면 페이지의 렌더링을 가로막거나 레이아웃을 방해할 가능성이 줄어든다.
    • font-family
      • 폰트를 불러올 수 있는 방법은 크게 5개로 나뉜다
      • auto(기본값): 브라우저가 폰트를 불러오는 방법을 결정한다.
      • block: 폰트가 로딩되기 전까지 렌더링 중단(최대 3초), 웹 폰트의 로딩이 완료되면 비로스 폰트 적용
      • swap: 앞서 언급한 FOUT 방식, 폴백 폰트로 렌더링한 다음, 웹 폰트의 로딩이 완료되면 웹 폰트 적용
      • fallback: 100ms간 텍스트가 보이지 않고 그 이후에 폴백 폰트로 렌더링, 3초 안에 폰트가 로딩되면 해당 웹 폰트로 전환하고, 그렇지 않으면 폴백 폰트 유지
      • optional: fallback과 유사, 100ms 동안 보이지 않고 폴백 폰트로 렌더링한다는 점은 동일하나 네트워크 상태를 파악해 0.1초 이내로 폰트가 다운로드되지 않으면 해당 폰트를 보여주지 않음
  • 요약하자면 최대한 중요한 폰트의 다운로드를 우선순위에 밀어넣고, 이 우선순위를 활용했음에도 빠르게 로딩하는데 실패했다면 다음을 기약하고 기본 폰트를 노출하는 것이다.

적절한 이미지 크기 설정

  • 많은 웹사이트가 반응형 웹사이트를 구현하면서 누적 레이아웃 이동이 커지는 결과를 낳게 되었다

  • width:100% height:auto로 지정하면 이미지의 높이에 자동으로 비례해서 auto를 설정해준다. 하지만 이미지가 완전히 다운로드되기 전까지는 높이를 알 수 없기 때문에 이미지의 높이를 높게 잡아 뒀다가 레이아웃 이동이 크게 발생한다

  • 적절한 이미지 크기를 설정하기 위한 방법은 아래와 같다

    • width, height 지정

      • width:100% height:auto; 와 함께 width, height를 원하는 비율로 지정하면 브라우저가 이미지를 로딩하기 전에 적절한 가로세로 비율을 계산해 이미지가 표시되는 만큼 면적을 할당해 둔다.
      • 이는 aspect-ratio 속성 덕분인데, 이 속성은 브라우저의 유저 에이전트 스타일시트에 포함돼 있으며, 이미지의 가로세로 비율을 자동으로 맞춰주는 역할을 한다
      import "./style.css"; // width: 100%, height: auto;
      
      export default function App() {
        return (
          <div className="App">
            <img src="/image.jpg" alt="이미지" width="1600" height="900" />
          </div>
        );
      }
      • 만약 뷰포트 너비에 맞춰서 다른 이미지를 제공하고 싶은 경우 srcset 속성을 사용하는 것이 좋다
      <img
        width="1000"
        height="1000"
        src="image-1000.jpg"
        srcset="image-1000.jpg 1000w, image-2000.jpg 2000w, image-3000.jpg 3000w"
        alt="이미지"
      />

12.5.6 핵심 웹 지표는 아니지만 성능 확인에 중요한 지표들

최초 바이트까지의 시간(Time To First Byte, TTFB)

  • 페이지를 요청했을 때 요청이 완전히 완료되는 데 걸리는 시간을 측정하는 것이 아니라 최초의 응답이 오는 바이트까지가 얼마나 걸리는지를 측정하는 지표이다.
  • 이 지표는 600ms 이상 걸릴 경우 개선이 필요한 것으로 간주된다
  • 최초 바이트까지의 시간을 개선하기 위한 고려사항
    • 서버 사이드 렌더링을 수행하고 있다면 로직와 API 호출을 최적화해서 페이지를 빨리 준비 시켜야 한다
    • 웹페이지의 주된 방문객의 국적을 파악해 해당 국적과 가깝게 서버를 위치시키는 것이 좋다
    • 리액트 서버 사이드 렌더링이라면 renderToNodeStream, renderToStaticNodeStream과 같은 스트리밍 API를 사용하는 것이 좋다

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

  • 웹사이트에 접속한 순간부터 페이지에 뭐라도 뜨기 시작한 시점까지의 시간이다
  • 일반적으로 좋은 점수는 1.8초 이내이며, 3.0초 이내는 보통, 그 이후는 개선이 필요하다
  • FCP 개선방법
    • TTFB를 개선한다. 렌더링을 최대한 빠르게 하기 위해 최초 바이트까지의 시간을 단축해야 한다
    • 렌더링을 가로막는 리소스를 최적화한다
    • 페이지 리다이렉트를 최소화한다.
    • DOM이 복잡하고 크다면 렌더링되는데 시간이 오래 걸리기 때문에 크기를 최적화한다
profile
효효 개발공부로그

0개의 댓글