FE 성능 최적화 단권화 정리

Sheryl Yun·2023년 3월 27일
2
post-thumbnail

최근 웹 성능 최적화 기법 책을 보며 성능 최적화에 대해 공부하고 있다. 아직은 대략적으로 훑어보는 1회독을 마쳤고 이제 목차별로 찬찬히 다시 볼 예정.. (한번 레이어를 깔고 세부 항목을 보는 건 차이가 있는 것 같다)

자투리 시간에 성능 최적화에 대해 검색해보다가 좋은 자료들을 찾았다. 최적화는 파고 들어가면 한도 끝도 없는 것 같은데 대표적인 방법으로 몇 가지로 추려지는 것 같아서 여러 블로그 자료와 읽고 있는 책의 내용을 종합해서 단권화를 해볼까 한다.

웹 성능 최적화하기 서론

참고: 웹 성능 최적화 5분 정리

우선 대표적인 최적화 방법에는 다음과 같은 것들이 있다. 개인적으로 부연할 부분만 가져왔다.

  • style은 상단, js는 하단에서 불러오기
    이 항목은 웹 성능 최적화에서 거의 항상 나오는 얘기인데 리액트와 같은 프레임워크를 주로 쓰는 경우에는 어떻게 해야 하는지 늘 궁금했다. javascript는 index.html에서 body 직전에 불러올 수 있다고 하지만 style 태그는..? 보통 프레임워크에서는 CSS-in-JS나 Sass(SCSS)를 사용한다. 프레임워크에서도 태그의 위치로 최적화하는 방법이 있을까?

  • 웹팩(Webpack)을 활용한 최적화
    CRA로 리액트 앱을 만들 때는 웹팩에 거의 신경쓰지 않았다. (사실 잘 알면 웹팩 커스텀으로 최적화 가능하다고 알고 있지만..) 근데 최근에 CRA 대신 를 쓰게 되면서 'CRA가 편하지만 구동이 느렸던 이유가 모든 웹팩 셋팅이 되어 있어서인데 Vite는 이 부분을 어떻게 처리했을까?' 하는 궁금증이 들면서 다음을 정리했다.

  • js의 공백 줄이기
    이건 성능화 책에서도 봤는데 주석이나 불필요한 공백을 줄이면 js 파일 크기 자체가 줄어들어서 최적화에 영향을 미치는 것 같다. 하지만 요즘은 에디터의 힘을 빌리면 불필요한 공백은 쉽게 정리할 수 있고(emmit, prettier 등) 주석은 꼭 필요한 경우에는 유지 보수를 위해 달아둬야 한다고 배워서 이 부분은 넘어갔다.

  • html 작성 시 불필요한 div 제거
    예전에 비슷하게 면접을 보러 갔다가 새로 리모델링한 사이트 이전에 외주로 맡겼던 사이트를 보여주셨는데 내가 보기에도 div 천지여서(?) 브라우저가 문서를 잘 읽어갈 것 같지 않았다.
    리액트 공식 문서에서는 불필요한 태그 사용을 줄이는 방안으로 React.Fragment 태그를 제시했다.

📌 React.Fragment 태그
여러 자식 태그를 그룹화하는 것 외에 어떤 역할도 하지 않는 태그
<></>로 간략하게 사용 가능하다

  • 리플로우와 리페인트 고려
    리플로우는 layout 단계에서, 리페인트는 paint 단계에서 수정이 일어날 경우 브라우저에 리렌더링을 발생시킨다. 리플로우 발생을 줄이는 방법은 아래 내용과 함께 이 글에서도 정리했다.
  • 스프라이트 이미지 사용
    이미지들을 모아서 하나의 이미지 파일(스프라이트 이미지)로 만들고 css에서 background 좌표를 잡아서 필요한 이미지를 가져와 사용하는 방법이다. 인스타그램 클론을 할 때 실제 사용해봤는데, 이미지가 아무리 많아도 하나의 이미지 파일만 불러오기 때문에 용량 최적화에 큰 도움이 되었다.
  • SEO (검색엔진최적화)
    검색 시 해당 서비스가 포털에 잘 뜨게 하기 위해 검색 크롤러봇이 사이트의 정보를 더 잘 읽어갈 수 있게 처리해주는 것이다. 리액트에선 index.html의 meta 태그, robot.txt, manifest.json 등의 파일을 작성하고, Next.js에서는 자체 SSR을 제공하여 SEO 설정에 유리한 환경을 제공한다. 마크업을 할 때도 div 천국이 아닌 시맨틱 마크업을 하면 기본적으로 SEO가 가능하다.
  • 파일 개수 줄이기
    JS 파일의 갯수를 줄이면 그만큼 빠른 번들링과 렌더링이 가능하다.

  • 라이브러리 의존도 낮추기
    찔림 직접 구현이 가능한 부분은 라이브러리 의존도를 최대한 낮추자.


⛪ 성능 측정 도구

간단한 성능 측정 도구를 먼저 소개한다.

  1. Lighthouse 검사
  2. PageSpeed Insights 사이트

Lighthouse는 몇 번 써봤는데 PageSpeed Insights는 처음이었다. 바로 토이 프로젝트의 URL을 넣어 돌려보니 Lighthouse와 비슷한 UI인데 좀 더 시각적으로 보기 편하고, 실제 배포되어 링크를 가진 사이트만 확인이 가능한 것 같다.


성능 최적화는 크게 렌더링 최적화와 로딩 최적화 두 가지로 나뉜다.

성능 최적화 = 렌더링 최적화 + 로딩 최적화

즉, 렌더링과 로딩 과정 이 두 가지를 최적화하면 웹 성능을 유의미하게 개선할 수 있다.

먼저 렌더링 최적화에 대해 알아보자.

🛸 렌더링 최적화

렌더링 최적화의 목표:
브라우저의 리플로우를 최대한 적게 발생시키면서 빠르게 화면을 그리는 것

리플로우는 렌더링 차단 리소스에 의해 발생한다.
렌더링 차단 리소스란 브라우저의 렌더링을 막는 소스로 일반적으로 css와 js 파일을 말한다.

📍 css 파일 최적화

1) 리플로우, 리페인트(Reflow/Repaint) 고려한 스타일 작성

레이아웃의 넓이, 높이, 위치 등에 영향을 주는 css 속성을 변경하면 Layout 단계부터 다시 그려진다. 이를 리플로우(Reflow)라고 한다.

반면 레이아웃에 영향을 주지 않는 속성을 변경하면 레이아웃을 건너뛰고 Paint 단계부터 리렌더링하게 되는데 이를 리페인트(Repaint)라고 한다.

결론
레이아웃 단계부터 변경하는 Reflow가 일어나면 브라우저가 전체 픽셀을 다시 계산하기 때문에, 되도록이면 Repaint인 속성을 사용해서 css를 작성하는 것이 좋다.

🍑 리플로우(Reflow) 속성
너비, 높이, 배치 등에 영향을 주는 속성
width / height / margin / padding / display /
position / float / top / left / right / bottom /
box-sizing / border / border-color / border-width /
font-size / font-weight / font-family / text-align /
line-height / vertical-align /
white-space / word-wrap / text-overflow / text-shadow ...

🍎 리페인트(Repaint) 속성
너비, 높이, 배치 등에 영향을 주지 않는 속성
color / text-decoration / visibility /
background / background-color / background-image / background-position / background-repeat / background-size / box-shadow /
border-radius / border-style / text-decoration /
outline / outline-style / outline-color / outline-width ...

리플로우와 리페인트를 둘 다 발생시키지 않는 속성
opacity / transform / cursor / z-index /
display: none ...

2) 사용하지 않는 css 제거

앞서 말했듯 css는 렌더링 차단 리소스이기 때문에 사용하지 않는 css는 제거하는 것이 좋다. Unused css는 크롬 Lighthouse를 통해 확인할 수 있다.

  • Lighthouse는 2KB 이상 사용되지 않은 css가 있을 시 오류로 표기한다.

나의 경우는 프로젝트에서 사용되지 않는 global 폰트가 있어서 제거했더니 Performance에서 5점이 올랐다.

  • Potential Savings 항목을 통해 잠재적으로 어느 정도 절약 가능한지 보여준다.

3) css 선택자 간결화

셀렉터(선택자)가 복잡할수록 부모 선택자에 자식 선택자 포함 여부를 확인하려고 DOM을 거슬러 올라가는 데 시간이 소요되어 성능에 좋지 않다.

.mypage .mypage_item{...} /* 🔺 */
.mypage_item{...} /* ✅ */ 

📮 HTML 최적화

1) 인라인 스타일 사용하지 않기

인라인 스타일은 웹 페이지가 그려질 때 레이아웃에 영향을 미치고 유지보수 측면에도 좋지 않으므로 지양한다.

<div style="margin-top:20px;"></div> <!-- ❌ -->

2) DOM 태그 지나친 중첩 피하기

DOM 트리가 커지고 깊어질수록 DOM 변경 시 계산해야 하는 것들이 많아진다. 불필요한 div wrapper 등을 제거하자.

🎢 애니메이션 최적화

면접 때 애니메이션 최적화의 중요성에 대해 설명을 들은 적 있다.
애니메이션은 자바스크립트나 라이브러리보다 css를 통해 구현하는 것이 성능적으로 이득이다.

1) transform

transform은 리플로우와 리페인트를 모두 발생시키지 않기 때문에 애니메이션에서 사용하면 렌더링 속도를 향상시킬 수 있다.

2) position

position을 absolute나 fixed로 설정해도 주변 요소에 영향을 주지 않는다.

🎭 Javascript 최적화

1) 강제 동기 레이아웃 피하기

원래 레이아웃은 비동기이나 특정 속성을 읽을 때 최신 값을 계산하기 위해 레이아웃이 동기적으로 발생하는 경우이다.

예를 들어 style 변경 이후에 offsetHeight, offsetTop과 같은 계산된 값을 속성으로 읽을 때에 강제 동기 레이아웃이 발생하게 된다.

const tabBtn = document.getElementById('tab_btn');

tabBtn.style.fontSize = '24px';
console.log(testBlock.offsetTop); // offsetTop 호출 직전 브라우저 내부에서는 동기 레이아웃이 발생한다.
tabBtn.style.margin = '10px';
// 레이아웃 변경

계산된 값을 반환하기 전에 변경된 style이 계산 결과에 적용되어 있지 않으면 style 변경 이전의 계산 값을 반환한다. 최신 브라우저에서도 발생하는 부분이므로 강제 동기 레이아웃을 발생할 수 있는 코드는 최대한 사용하지 않도록 주의해야 한다.

2) 레이아웃 스레싱(thrashing) 피하기

한 프레임 내에서 강제 동기 레이아웃이 연속적으로 발생하는 경우이다. 다음 코드에서 반복문 안에서 style.width를 설정하고 box.offsetWidth를 읽어와서 for문이 실행될 때마다 레이아웃 변경이 반복 발생한다. 반복문 밖에서 box 엘리먼트의 너비를 읽어오면 레이아웃 스레싱을 막을 수 있다.

3) 가능한 한 하위 노드의 DOM 조작하기

DOM의 상위 노드의 스타일을 변경하면 하위 노드에 모두 영향을 미치기 때문에 변경 범위를 최소화하기 위해서 가능한 하위 노드의 DOM을 조작한다.

4. display: none 속성으로 숨겨진 요소 수정하기

display: noneDOM이 조작되어도 레이아웃과 리페인트가 발생하지 않는다. 따라서 많은 수의 요소를 변경해야 할 때 display: none으로 숨겨놓고 요소를 변경한 뒤 다시 보이게 하면 레이아웃 발생을 최대한 줄일 수 있다.

참고로 visibility: hidden의 경우, 리페인트는 발생하지 않지만 공간을 차지하기 때문에 리플로우가 발생하게 된다.

🚁 로딩 최적화

🏒 렌더 블로킹 최적화하기

1) css는 head 안에서, js는 body 직전에 불러온다.

html과 css는 바로 눈에 보여지는 부분이다.
빠르게 그려질 수 있도록 css는 head 내에서 임포트한다.

파싱 중 script 태그를 만나면 브라우저는 html 파싱을 멈춘다. 따라서 js 외에 나머지 소스들이 먼저 로딩이 끝나고 js가 실행되도록 body 태그를 닫기 직전에 script 태그를 임포트 한다.

2) media 속성으로 css에 반응형을 명시한다.

media 속성을 사용하면 브라우저에 css를 반응형이라고 인식시킬 수 있다.
크롬 Lighthouse는 media 속성이 없는 <link rel=”stylesheet” />태그를 렌더 블로킹 리소스로 판단한다.
media 속성이 없으면 브라우저가 stylesheet를 해석할 때 화면에 style을 불러오지 못한다.

<link href="style.css" rel="stylesheet"> <!-- ❌ -->
<!-- 화면이 특정 너비일 때 브라우저가 stylesheet 해석 -->
<link href="style.css" rel="stylesheet" media="(min-width: 320px) and (max-width: 768px)"> 

3) async / defer를 적절히 사용한다.

두 속성은 html 파싱을 멈추지 않고 스크립트 파일을 병렬적으로 다운로드하며 실행에서 시간차가 있다.

async는 다운로드 후 즉시 실행해서 잠깐 파싱이 끊기지만, (즉시 실행)

<script async src="test.js"></script>

defer는 다운로드 및 파싱이 모두 끝난 후에 script를 실행한다. (지연 실행)

<script defer src="test.js"></script>

그림으로 확인해보자.

초록색: HTML 파싱
회색: HTML 파싱 멈춤
파랑색: script 다운로드
빨강색: script 실행

일반적 동작

파싱이 도중에 끊기고 script를 다운로드해서 실행까지 한 뒤에 다시 파싱을 시작한다.

async

파싱을 끊지 않고 script를 병렬 다운로드하지만, 실행을 위해서 잠시 파싱을 끊었다가 실행이 끝나면 다시 파싱을 진행한다.

defer

파싱을 끊지 않고 script를 병렬 다운로드하고, 이후에 파싱이 모두 끝나면 그때서야 script를 실행한다. (도중에 파싱이 끊기지 않음)

사용 예시

  • async - script를 병렬 다운로드한 후 (파싱을 끊고서라도) 실행을 먼저 해야 할 때
  • defer - 파싱이 끊기지 않아야 할 때, script를 마지막에 실행해도 될 때
  • 일반 동작 - 무조건 원래 순서대로 진행되어야 할 때

출처: growing with the web

🎨 이미지 최적화

1) picture 태그 사용하기

picture 태그는 다음 2가지가 가능하다.

  • source 태그의 type으로 다양한 형식의 이미지 제공 가능
    avif가 webp보다 용량 면에서 더 유리하며, avif를 지원하지 않는 브라우저에 대비해 webp를 제공, webp를 지원하지 않는 브라우저에 대비해 jpg(img 태그)를 초기 값으로 넣어둔다.

avif - 저용량 & 고품질, 가장 좋음
webp - jpg와 png 대비해서 30%-70% 용량

<picture>
    <source srcset="aaa.avif" type="image/avif" /> /* avif */
    <source srcset="aaa.webp" type="image/webp" /> /* webp */
    <img src="aaa.jpg" alt> /* jpg */
</picture>
  • sourcemedia반응형 이미지 제공 가능
    img 태그로 반응형이 없을 때의 초기값 이미지를 지정할 수 있다.
<!-- 보통은 pc.webp 이미지, 브라우저의 넓이가 760px 이하일 때는 mob.webp 이미지 출력-->
<picture>
    <source srcset="mob.webp" media="(max-width: 760px)"> 
    <img src="pc.webp" alt>
</picture>

2) 스프라이트 이미지

여러 개의 이미지를 하나로 만든 이미지이다.
다음은 인스타그램 스프라이트 이미지 예시이다.

css의 background-position 속성으로 부분적으로 이미지를 가져와 사용한다. 리소스 요청 자체가 줄기 때문에 의미 있는 최적화 방법이다.

background-position으로 가져온 모습

.icon_rotate { width:15px; height:15px; background-position:-23px -50px; }
.icon_del { width:10px; height:13px; background-position:-65px -15px; }
...

3) img 지연 로딩 활용하기

img 태그의 loading 속성을 사용해서 브라우저 화면에 이미지를 지연/병렬 로딩할 수 있다.

  • 사용 가능한 값: auto, lazy, eager
  • auto: loading을 안 쓴 것과 동일 (default)
  • lazy: 화면에 보이는 부분만 먼저 출력하고 화면 바깥쪽 이미지들은 로딩하지 않음. 사용자가 화면을 위로 올리면 아래쪽에 있던 이미지가 올라오면서 로딩된다.
  • eager: 화면 위치에 상관없이 페이지가 로딩되자마자 이미지 동시 로딩
// 사용된 코드
<img src="item.jpg" loading="lazy" alt>

🔩 웹팩 활용

웹팩으로 css 파일과 js 파일들을 각각 하나의 파일로 번들링(묶기)한다.

⬇️ css/js 번들링 이전 ⬇️

<html>
  <head>
    /* css 파일 3개 */
    <link href="main.css" rel="stylesheet">
    <link href="sub.css" rel="stylesheet">
    <link href="sub2.css" rel="stylesheet">
  </head>
  <body>
    <div id="content">
      ...
    </div>
    /* js 파일 2개 */
    <script async src="sample1.js" type="text/javascript"></script>
    <script async src="sample2.js" type="text/javascript"></script>
  </body>
</html>

⬇️ 웹팩을 통한 css/js 번들링 이후 ⬇️

<html>
  <head>
    /* css 파일이 bundle.css 1개로 묶임 */
    <link href="bundle.css" rel="stylesheet">
  </head>
  <body>
    <div id="content">
      ...
    </div>
    /* js 파일이 bundle.js 1개로 묶임 */
    <script async src="bundle.js" type="text/javascript"></script>
  </body>
</html>

🎫 Gzip 활용

모든 브라우저에서 지원하며, 가장 유명한 데이터 압축 및 압축 해제 알고리즘이다.

사용 방법

  1. 클라이언트가 요청 헤더에 Accept-Encoding: gzip으로 서버에 요청을 보낸다.
  2. 서버가 이를 지원한다면 해당 알고리즘으로 데이터를 압축하여 클라이언트에게 전송한다.
  3. 클라이언트는 응답 헤더인 Content-Encoding: gzip을 통해 데이터가 압축된 방식을 확인하고, 해당 방식으로 데이터 압축을 해제한다.

🎑 js 파일 압축

UglifyJS 등의 라이브러리를 사용해서 js 파일을 압축한다. 불필요한 공백이나 줄바꿈을 제거해서 파일의 용량이 감소하며, 난독화를 하면 민감한 코드를 알아보기 어려워진다.

🎡 CDN 사용

Content Delivery Network로, 유저에게 많은 콘텐츠를 손실 없이 빠르게 전달하는 서비스이다.
대용량 콘텐츠 다운 또는 스트리밍 등에 사용하며 사용량 만큼 비용을 지불한다.

🎿 캐싱

html, css, js, 이미지 등의 리소스를 첫 요청 시 다운받고 특정한 위치에 복사본을 저장해둔 뒤 이후에 동일한 URl의 리소스 요청이 오면 이전에 저장해둔 리소스를 사용하는 방법이다.
프로그램 전반에 해당하는 공통의 개념인데, 프론트엔드에서의 캐싱은 구체적으로 웹 캐시를 말한다.

웹 캐시 정의

보통 사이트를 새로 들어갈 때 첫 접속 때는 로딩 속도가 느리지만 이후에는 속도가 현저히 빨라지는 것이 예이다.

캐싱은 브라우저가 새로 다운로드할 파일의 개수 자체가 줄어서 리소스 요청 수를 줄인다.

웹 캐싱 방법

브라우저는 서버에게 받은 응답 헤더의 내용에 따라 캐쉬 정책을 수행한다.
응답 헤더에 Last-Modified, Etag, Expires, Cache-Control:max-age 항목이 존재하면 복사본을 생성하고 값을 저장하게 된다.

1. Last-Modified

  1. 브라우저는 최초 응답 시 받은 Last-Modified 값을If-Modified-Since라 는 헤더에 포함시켜 페이지 요청

  2. 서버는 요청 파일의 수정 시간을 If-Modified-Since 값과 비교하여 동일하 면 '수정되지 않음(304 Not Modified)'로 응답, 다르면 '수정됨(200 OK)'과 함께 새로운 Last-Modified 값을 응답 헤더에 넣어 전송

  3. 브라우저는 응답 코드가 304(수정 없음)이면 이미 캐쉬된 리소스로 페이지를 로드하고, 200(수정됨)이면 새로운 리소스를 다운 받은 후 서버에서 받은 새로운 Last-Modified 값으로 갱신

2. ETag (Entity Tag)

  1. 브라우저는 최초 응답 시 받은 Etag 값을 If-None-Match 헤더에 포함시켜 페이지 요청

  2. 서버는 요청된 파일의 Etag 값을 If-None-Match 값과 비교하여 동일하 면 '수정되지 않음(304 Not Modified)'로 응답, 다르면 '수정됨(200 OK)'과 함께 새로운 ETag 값을 응답 헤더에 넣어 전송

  1. 브라우저는 응답 코드가 304(수정 없음)이면 이미 캐쉬된 리소스로 페이지를 로드하고, 200(수정됨)이면 새로운 리소스를 다운 받은 후 새로운 ETag 값으로 갱신

Last-Modified(1.0) 와 ETag(1.1)는 validation을 체크하기 위해 서버와 통신이 한번 발생한다. 이때 통신의 요청과 응답에서 header와 cookie 등에 의한 1KB의 데이터 전송이 발생하게 된다.

Etag는 서버마다 생성하는 값이 다르고, 파일마다 고유한 값을 가진다.

3. Cache-Control

  1. 브라우저는 최초 응답 시 받은 Cache-Controlmax-age 값을 GMT와 비교하여 기간이 지나지 않았다면 바로 캐쉬에서 페이지를 로드하고, 기간이 만료되었으면 validation 체크를 한 뒤 새로운 리소스를 다운 받는다.

Expires(1.0)와 Cache-Control의 max-age(1.1)는 freshness를 기준으로 캐시 사용 여부를 결정한다. (기간이 지나기 전에는 캐시 사용, 지나면 서버에서 새로 다운)

  • Expiresmax-age에 사용되는 시간은 HTTP date 형태이며 로컬 타임이 아닌 GMT를 사용한다.

  • 서버는 Last Modified Time(마지막 수정 시간) 또는 Last Access Time(마지막 접속 시간)을 기준으로 하여 일정 시간 이후에 Expiresmax-age를 설정한다.

4. Expires

  1. 브라우저는 최초 응답 시 받은 Expires를 비교하여 기간이 지나지 않으면 서버에 통신 요청 없이 바로 캐쉬에서 페이지를 로드하고, 만약 기간이 만료되었다면 validation 작업 이후 새로운 리소스를 다운 받는다.

캐쉬 설정법
캐쉬는 서버에서 설정된다. Expires나 Etag는 서버 설정에 의하여 사용하지 않을 수도 있다. Expires의 경우, 설정되면 max-age와 함께 설정된다.

참고

profile
영어강사, 프론트엔드 개발자를 거쳐 데이터 분석가를 준비하고 있습니다 ─ 데이터분석 블로그: https://cherylog.tistory.com/

0개의 댓글