이마고웍스 클라우드 팀의 책장에는 개발관련 도서가 많이 있습니다. 출퇴근시간 + 점심시간을 활용해서 틈틈히 책을 읽고 있습니다. 입사 후 두달동안 벌써 6권은 읽은거 같은데 기억이 휘발되는 것이 느껴져 한권 읽을 때마다 간단하게라도 기록을 하려합니다.
유동균 저 | 인사이트(insight) | 2022년 11월 15일
프론트엔드 최적화는 웹사이트의 성능을 향상시키는데 중요한 역할을 합니다. 이는 사용자 경험(UX)을 개선하고, 사용자의 시간을 절약하며, 사용자의 만족도를 높이는 데 기여합니다. 프론트엔드 최적화를 통해 얻을 수 있는 이점은 아래와 같습니다:
사용자 경험 향상: 최적화된 프론트엔드는 웹페이지의 로딩 시간을 줄이고, 사용자 인터페이스의 반응성을 향상시킵니다. 이는 사용자에게 더 나은 경험을 제공하며, 사용자가 웹사이트를 더 오래 사용하고 더 자주 방문하도록 만들 수 있습니다.
검색 엔진 최적화: 웹페이지의 로딩 시간은 검색 엔진의 랭킹 알고리즘에 영향을 미칩니다. 즉, 웹사이트가 빠르게 로드되면 검색 엔진에서 더 높은 순위를 얻을 수 있습니다. 이는 웹사이트에 더 많은 트래픽을 유도하며, 결과적으로 더 많은 사용자를 끌어들일 수 있습니다.
모바일 사용자 대응: 모바일 사용자는 대개 데스크탑 사용자보다 인터넷 연결 속도가 느리고, 처리능력이 떨어지는 장치를 사용합니다. 프론트엔드를 최적화하면 이러한 사용자들에게 더 나은 사용자 경험을 제공할 수 있습니다.
데이터 사용량 감소: 프론트엔드 최적화는 불필요한 데이터 전송을 줄여 웹사이트의 데이터 사용량을 감소시킵니다. 이는 사용자의 데이터 비용을 절약하고, 특히 제한적인 데이터 플랜을 사용하는 모바일 사용자에게 유익합니다.
프론트엔드 최적화는 여러 가지 방법으로 이루어질 수 있습니다. 이미지 최적화, 자바스크립트 및 CSS 최소화 및 병합, 캐싱 전략 설정, 웹폰트 최적화, 비동기 로딩 등이 포함됩니다. 이러한 최적화 기법들은 개별적으로 적용될 수도 있고, 종합적인 성능 향상 전략의 일부로 함께 적용될 수도 있습니다.
아래에는 책을 통해 학습한 최적화 기법들의 요약입니다.
Code Splitting
코드 스플릿팅은 webpack
, rollup
, browserify
와 같은 모듈 번들러를 이용하여 만들어진 하나의 번들 파일을 여러 개의 번들 파일로 나누는 것을 의미합니다. 하나의 번들 파일을 여러 개의 번들 파일로 나누는 이유는 더 빠른 속도로 화면을 로드하기 위해서입니다.
모듈 번들러를 사용하게 된 이유는 브라우저에서 호출하는 파일의 개수를 줄여 부하가 발생되는 것을 방지하기 위함입니다. 하지만 프로젝트의 규모가 커짐과 동시에 번들링 되는 파일의 크기도 점점 커지게 되고 이것은 결국 url을 입력하여 처음 접근을 하게 될 때 크기가 커진 번들 파일을 로드하는 시간이 길어지는 결과를 만듭니다. 곧 사용자가 느끼는 서비스의 만족도에 영향을 미칠 수 있게 됩니다.
코드 스플리팅은 하나의 번들 파일을 여러 개의 번들 파일로 나눈 뒤 실제 로드될 화면에 필요한 번들 파일만 불러오고 나머지 번들 파일은 호출하지 않고 지연시킴으로 써 작업량을 줄여 더 빠른 속도로 화면이 보일 수 있게 도와줍니다.
공식 문서를 참고해보면 코드 스플릿팅을 적용할 최적의 위치로 라우트를 지칭하고 있습니다.
Text Compression
HTML, CSS, JS는 텍스트 기반의 파일이기에 텍스트 압축 기법 적용이 가능합니다. 파일을 압축하여 더 적은 크기로 빠르게 전송한 뒤, 사용하는 시점에 압축을 해제합니다. 파일 사이즈가 작아지며 리소스 전송 시간이 단축됩니다. 압축 여부는 HTTP의 헤더로 파악 가능합니다.
// Response Headers
Content-Encoding: gzip
Gzip
: 블록화, 휴리스틱 필터링, 헤더와 체그섬, 내부적으로 Deflate를 사용하는 압축 방식, Deflate 단독 사용보다 효과적Deflate
: LZ77 알고리즘 + 허프먼 코딩Image Size
화면에 그려지는 이미지의 사이즈 x2가 적정 크기입니다. 레티나 디스플레이는 같은 픽셀에 더 많은 픽셀을 그릴 수 있기 때문입니다. 이미지의 사이즈를 조정하는 것만으로 40fps에서 60fps 까지 성능 향상 시킨 경험이 있습니다.
Hardware Acceleration
하드웨어 가속은 컴퓨터의 특정 하드웨어 부분, 특히 그래픽 처리 장치(GPU)를 사용하여 애플리케이션의 성능을 향상시키는 것을 말합니다. 이것은 특히 그래픽 표현 및 애니메이션과 같은 고성능 작업에 유용하며, CSS 애니메이션의 경우에도 마찬가지입니다.
CSS 애니메이션의 최적화를 위해 하드웨어 가속을 사용하는 방법은 크게 다음과 같습니다:
Transforms 사용: translate3d
, scale3d
, rotate3d
등의 3D 변환을 사용하면 브라우저가 이러한 요소를 GPU로 보내려고 시도합니다. 이는 2D 변환과 비교해 더 효과적이라고 여겨집니다. 3D 변환을 사용하면 요소가 그래픽 카드에서 별도의 레이어로 처리되므로 복잡한 애니메이션에 대한 성능이 향상됩니다.
will-change Property 사용: will-change
속성은 브라우저에게 어떤 요소가 변할 것이라는 것을 미리 알려주는 것입니다. 예를 들어, will-change: transform;
이나 will-change: opacity;
를 사용하면 브라우저가 해당 요소에 대한 변경 사항을 미리 예상하고 이를 최적화할 수 있습니다.
Backface-visibility Property 사용: backface-visibility
속성을 hidden
으로 설정하면 3D 변환 중에 요소의 뒷면이 보이지 않도록 하여 성능을 향상시킬 수 있습니다.
Avoid Layout Thrashing: 레이아웃 스래싱은 스크립트가 실행되는 동안 레이아웃 계산이 여러 번 일어나는 현상을 의미합니다. 이를 피하려면 한 번에 모든 스타일 변경을 수행하고, 다음 프레임까지 기다린 후에 읽기 작업을 수행해야 합니다.
주의해야 할 것은, GPU를 오버로드하면 반대효과가 나타날 수 있으므로 모든 것에 하드웨어 가속을 적용하는 것은 좋지 않습니다. 필요한 곳에만 적절하게 사용해야 합니다.
Reflow
& Repaint
웹 페이지에서 reflow와 repaint는 브라우저 렌더링 과정 중 일어나는 중요한 이벤트들입니다. 이 두 가지 작업은 웹 페이지의 성능에 큰 영향을 미칠 수 있으므로, 이해하고 최적화하는 것이 중요합니다.
Reflow는 연산이 많기 때문에 상당히 비용이 많이 드는 작업으로 간주되며, 웹사이트의 성능에 부정적인 영향을 미칠 수 있습니다. 따라서 필요한 경우에만 reflow를 발생시키고, 불필요한 reflow를 피하는 것이 중요합니다.
Reflow와는 달리, Repaint는 레이아웃 변경이 없으므로 성능에 미치는 영향이 상대적으로 덜합니다. 그러나 이 작업도 고비용 작업이므로, 꼭 필요한 경우에만 발생시키는 것이 바람직합니다.
이 두 가지 작업은 최적화에서 중요한 역할을 하는데, 가능한 한 덜 발생시키는 것이 성능 향상에 도움이 됩니다. 예를 들어, CSS 애니메이션을 최적화하려면 transform과 opacity 변경만을 이용하면 됩니다. 이는 이들 변경 사항이 reflow나 repaint를 발생시키지 않기 때문입니다.
Component Lazy Loading
컴포넌트 지연 로딩(Lazy Loading)은 일반적으로 웹 페이지나 애플리케이션의 초기 로딩 시간을 줄이는 데 사용되는 기술입니다. 지연 로딩은 필요한 컴포넌트나 데이터를 바로 로딩하지 않고, 사용자가 해당 컴포넌트나 데이터를 필요로 할 때에만 로딩하는 방법을 의미합니다.
이 방법은 특히 웹 애플리케이션의 성능 최적화에 도움이 됩니다. 웹 페이지가 많은 양의 JavaScript 또는 복잡한 컴포넌트를 가지고 있을 때, 모든 컴포넌트를 한번에 로딩하려고 하면 사용자는 웹페이지가 완전히 로드될 때까지 오랜 시간을 기다려야 할 수 있습니다. 반면에 지연 로딩을 사용하면 필요한 부분만 로드하여 초기 로딩 시간을 크게 단축시킬 수 있습니다.
예를 들어, React에서는 React.lazy
와 Suspense
를 사용하여 컴포넌트 지연 로딩을 수행할 수 있습니다. 이를 사용하면 컴포넌트를 분할하고, 해당 컴포넌트가 실제로 렌더링될 필요가 있을 때에만 번들로부터 로드할 수 있습니다.
지연 로딩은 초기 로드 시간을 줄이는 데 뿐만 아니라, 불필요한 네트워크 대역폭 사용을 줄이고, 사용자의 데이터를 절약하는 등 여러가지 이점이 있습니다.
import React, { Suspense } from "react";
const OtherComponent = React.lazy(() => import("./OtherComponent"));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
Component Preloading
컴포넌트 사전 로딩(Preloading)은 사용자의 사용 패턴을 예측하거나 필요성을 미리 인지하고, 해당 컴포넌트를 미리 로딩하는 기술입니다. 이는 사용자가 실제로 해당 컴포넌트에 접근할 때 대기 시간을 줄이고, 애플리케이션의 반응성을 향상시킵니다.
예를 들어, 사용자가 특정 페이지의 버튼에 마우스를 올리는 순간, 그 버튼을 클릭하면 로드되는 다음 페이지의 컴포넌트를 미리 로딩해 놓을 수 있습니다. 그 결과, 사용자가 버튼을 실제로 클릭하면 즉시 다음 페이지가 표시되므로 좋은 사용자 경험을 제공할 수 있습니다.
웹팩(Webpack)과 같은 모듈 번들러를 사용하면, 특정 조건에 따라 컴포넌트를 사전 로딩하는 것이 가능합니다. 예를 들어, 웹팩의 require.ensure()
API를 사용하면 특정 코드 조각을 별도의 번들로 분리하고, 이를 필요한 시점에 동적으로 로드할 수 있습니다.
React에서는 React.lazy
를 사용하여 컴포넌트를 동적으로 가져오는 것을 지원하며, 이를 통해 사전 로딩 구현이 가능합니다. 그러나 별도의 라이브러리나 도구를 사용하여 더 세밀한 제어가 필요한 경우도 있습니다. 예를 들어, react-loadable
이나 loadable-components
와 같은 라이브러리는 더 많은 옵션과 기능을 제공합니다.
물론, 사전 로딩에는 주의할 점이 있습니다. 너무 많은 컴포넌트를 불필요하게 사전 로딩하면, 초기 로드 시간이 늘어나거나 네트워크 리소스를 낭비할 수 있습니다. 따라서 사전 로딩은 필요하고, 사용자가 곧 사용할 것으로 예상되는 컴포넌트에 대해서만 적절히 사용해야 합니다.
Image Preloading
이미지 사전 로딩(preloading)은 웹 페이지가 사용자에게 보여지기 전에 이미지를 미리 로딩하는 기술입니다. 이는 사용자가 해당 이미지를 실제로 보려고 할 때 로딩 지연을 방지하고, 페이지의 전체적인 사용자 경험을 향상시키는 데 도움이 됩니다.
사용자가 웹사이트를 탐색할 때 페이지에 있는 대부분의 이미지를 즉시 볼 수 있도록 이미지를 미리 로딩해 두면 사용자에게 더 빠른 반응성을 제공할 수 있습니다.
이미지 사전 로딩을 구현하는 방법은 여러 가지가 있습니다:
var img = new Image();
img.src = "path/to/image.jpg";
<link rel="preload" as="image" href="path/to/image.jpg" />
body::after {
content: "";
background: url(path/to/image.jpg) no-repeat -9999px -9999px;
display: none;
}
이러한 사전 로딩 방법들은 사용자 경험 향상을 위해 사용되지만, 불필요한 데이터 다운로드를 유발할 수 있으므로 신중하게 사용해야 합니다.
Image Lazy Loading
이미지 지연 로딩(Lazy Loading)은 웹 페이지의 초기 로딩 시간을 줄이기 위한 기법 중 하나로, 페이지에 있는 이미지들을 모두 한 번에 로딩하는 대신 필요할 때(일반적으로 이미지가 뷰포트에 들어올 때)에만 로딩하는 방법을 의미합니다.
이미지 지연 로딩은 특히 이미지가 많이 포함된 웹 페이지에서 유용합니다. 페이지 로딩 시 사용자에게 처음으로 보여지는 이미지들만 로딩하고, 나머지 이미지는 스크롤 등의 사용자 행동에 따라 로딩하면 초기 로딩 시간을 크게 줄일 수 있습니다.
HTML에는 이러한 지연 로딩을 쉽게 구현할 수 있는 loading 속성이 있습니다. 이 속성을 lazy로 설정하면 브라우저가 자동으로 이미지의 지연 로딩을 처리해줍니다.
<img src="image.jpg" loading="lazy" alt="My Image" />
이 외에도 JavaScript를 사용하여 더 세밀한 제어가 가능합니다. Intersection Observer API를 사용하면 웹 페이지의 특정 요소가 뷰포트에 들어오는 시점을 감지하고, 이 시점에 이미지를 로딩하는 코드를 실행할 수 있습니다.
document.addEventListener("DOMContentLoaded", function () {
let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
if ("IntersectionObserver" in window) {
let lazyImageObserver = new IntersectionObserver(function (
entries,
observer
) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
let lazyImage = entry.target;
lazyImage.src = lazyImage.dataset.src;
lazyImage.classList.remove("lazy");
lazyImageObserver.unobserve(lazyImage);
}
});
});
lazyImages.forEach(function (lazyImage) {
lazyImageObserver.observe(lazyImage);
});
} else {
// IntersectionObserver가 지원되지 않는 경우에 대한 fallback 코드
}
});
또한, React와 같은 프론트엔드 라이브러리나 프레임워크에서는 react-lazyload, react-loadable 등의 라이브러리를 이용해 이미지 또는 컴포넌트의 지연 로딩을 구현할 수 있습니다.
이미지 지연 로딩은 성능 최적화의 중요한 방법 중 하나이지만, 적절하게 사용되어야 합니다. 만약 모든 이미지에 지연 로딩을 적용하면 사용자가 스크롤할 때마다 이미지 로딩이 발생해 사용자 경험을 해칠 수 있습니다. 따라서, 주요 컨텐츠나 초기에 보여지는 이미지 등은 지연 로딩 대상에서 제외하는 것이 좋습니다.
Optimize Image/Video Size
WebP
WebP는 Google이 개발한 이미지 포맷입니다. JPEG, PNG, GIF 등 기존의 이미지 포맷에 비해 파일 크기를 크게 줄일 수 있으면서도 비슷하거나 더 좋은 품질을 유지할 수 있습니다. 이렇게 하면 웹 페이지의 로딩 속도를 높이고, 대역폭 사용을 줄일 수 있습니다.
WebP는 손실 압축(Lossy compression)과 무손실 압축(Lossless compression) 둘 다 지원합니다.
WebP
: 원본 이미지의 일부 데이터를 생략하여 파일 크기를 줄입니다. 이 때문에 원본 이미지와 정확히 동일하게 복원할 수는 없지만, 품질 손실이 눈에 띄지 않을 정도로 작습니다. 일반적으로 JPEG보다 약 25~34% 작은 파일 크기를 가집니다.WebP
: 원본 이미지의 모든 데이터를 유지하면서 파일 크기를 줄입니다. 압축 후에도 원본 이미지와 완전히 동일하게 복원할 수 있습니다. 일반적으로 PNG보다 약 26% 작은 파일 크기를 가집니다.또한, WebP는 애니메이션과 투명도(알파 채널)도 지원합니다. 이런 면에서는 GIF나 PNG와 같은 기능을 대체할 수 있습니다.
그러나 WebP는 모든 웹 브라우저에서 지원되지는 않습니다. 특히 Internet Explorer나 일부 구형 브라우저에서는 WebP를 지원하지 않습니다. 따라서 웹 사이트에서 WebP 이미지를 사용할 경우에는 브라우저의 지원 여부를 확인하고, 필요에 따라 JPEG나 PNG와 같은 다른 포맷의 이미지를 대체로 제공해야 할 수도 있습니다.
<picture>
<source data-srcset="{main_items_webp}" type="image/webp" />
<img data-src="{main_items}" ref="{imagEl1}" />
</picture>
WebM
WebM은 Google이 2010년에 개발한 오픈소스 동영상 파일 포맷입니다. VP8 또는 VP9 비디오 코덱과 Vorbis 또는 Opus 오디오 코덱을 사용하며, HTML5의 <video>
태그와 잘 호환됩니다.
WebM의 몇 가지 주요 특징은 다음과 같습니다:
높은 압축률: WebM은 고품질의 비디오를 상대적으로 작은 파일 크기로 압축할 수 있습니다. 이는 웹 상에서 비디오를 스트리밍하거나 다운로드할 때 대역폭을 절약하는 데 유용합니다.
로열티-프리: WebM 프로젝트는 모든 소프트웨어와 하드웨어 제조사가 자유롭게 WebM 포맷을 사용할 수 있도록 로열티-프리 라이선스를 제공합니다. 이로 인해 개발자는 추가 비용 없이 WebM을 사용할 수 있습니다.
브라우저 호환성: WebM은 Chrome, Firefox, Opera 등 대부분의 모던 웹 브라우저에서 지원됩니다. 하지만 Safari는 기본적으로 WebM을 지원하지 않으므로 주의가 필요합니다.
WebM은 특히 웹 환경에서 동영상을 효율적으로 제공하고자 할 때 유용합니다. YouTube 같은 동영상 플랫폼에서는 WebM 포맷을 널리 사용하고 있습니다.
<video autoplay loop muted>
<source src="{video_webm}" type="video/webm" />
<source src="{video}" type="video/mp4" />
</video>
웹페이지에서 웹 폰트를 로드할 때 발생할 수 있는 두 가지 현상이 있습니다.
FOUT
(Flash of Unstyled Text): 웹페이지가 로드되는 동안, 웹 폰트가 아직 로드되지 않았을 때 브라우저가 시스템 폰트를 임시로 보여주는 현상을 말합니다. 웹 폰트가 완전히 로드되면, 웹페이지는 웹 폰트로 업데이트됩니다. 이 현상은 사용자가 내용을 빠르게 볼 수 있게 하지만, 폰트가 바뀌는 과정에서 레이아웃의 변화가 발생할 수 있습니다. (edge)
FOIT
(Flash of Invisible Text): 이는 웹페이지가 로드되는 동안 웹 폰트가 로드되지 않았을 때 브라우저가 텍스트를 숨기는 현상을 말합니다. 웹 폰트가 완전히 로드되면 텍스트가 나타납니다. 이 현상은 레이아웃의 안정성을 유지하지만, 폰트 로딩 시간 동안 사용자가 텍스트를 볼 수 없게 됩니다. (chrome, safari, firefox, etc ..)
폰트 최적화 방법은 폰트 적용 시점을 제어하는 방법과 사이즈를 줄이는 방법이 있습니다.
CSS의 font-display
속성으로 폰트 적용 시점 제어가 가능합니다.
@font-face {
font-family: BMYEONSUNG;
src: url("./assets/fonts/BMYEONSUNG.woff2") format("woff2"), url("./assets/fonts/BMYEONSUNG.woff")
format("woff"), url("./assets/fonts/BMYEONSUNG.ttf") format("truetype");
font-display: block;
}
서브셋 폰트
사용 고려가능합니다.Layout Shift
Layout Shift는 웹 페이지의 사용자 인터페이스가 예기치 않게 움직이는 현상을 말합니다. 이는 주로 웹 페이지의 요소들이 로드되는 과정에서 순서가 바뀌거나, 새로운 요소가 삽입되면서 발생합니다.
예를 들어, 사용자가 웹 페이지의 버튼을 누를 려고 할 때, 페이지의 일부분이 아직 로드되지 않았다가 로드되면서 버튼의 위치가 바뀌게 될 수 있습니다. 이 때문에 사용자는 의도치 않게 다른 버튼을 누르게 될 수 있어, 사용자 경험을 해칠 수 있습니다.
Google은 이렇게 레이아웃이 변경되는 정도를 측정하는 지표로 Cumulative Layout Shift (CLS)를 제시했습니다. CLS는 웹페이지의 레이아웃이 얼마나 자주 변경되는지에 대한 척도로, 높은 값은 페이지가 많이 움직였음을 의미합니다. Google은 이 CLS 값을 최적화하여 웹사이트의 사용성을 높이는 것을 권장하고 있습니다.
CLS를 줄이는 방법 중 하나는 이미지나 동영상, 광고 등에 적절한 크기 (width와 height)를 사전에 지정해 놓는 것입니다. 이렇게 하면 브라우저가 요소의 크기를 미리 알 수 있어 레이아웃이 변경되는 것을 방지할 수 있습니다. 또한 웹폰트를 사용할 때는 폰트의 로드 상태에 따라 텍스트가 화면에서 움직이지 않도록 설정하는 것도 중요합니다.
<div class="wrapper">
<img class="image" src="..." />
</div>
/* Intrinsic aspect ratio 16:9 */
/* Method 1 */
.wrapper {
position: relative;
width: 160px;
paddign-top: 56.25%;
}
.image {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
/* Method 2 */
.wrapper {
width: 100%;
aspect-ratio: 16 / 9;
}
.image {
width: 100%;
height: 100%;
}
이전에 언급했듯이, 저는 이마고웍스의 클라우드팀에서 최적화 작업을 담당하게 되었습니다. 최적화를 위한 다양한 기법과 성능 측정 방법에 대해 책을 통해 공부할 수 있었습니다. 이러한 지식을 바탕으로, 병목 현상이 발생하는 코드의 개선부터 Text Compression을 통한 번들 크기 축소까지 여러 시도를 해 보았습니다.
그러나 책에서 제시된 기법들을 그대로 적용했다고 해서 항상 큰 효과를 보는 것은 아닙니다. 하지만 이러한 기법들이 어떻게 동작하는지 이해하고, 그 개념을 우리 팀의 프로젝트에 맞게 조정하며 적용하는 것이 가능했습니다.
현재까지도 다양한 시도를 계속하고 있으나, 실질적인 변화는 아래 사진에 적힌 이유때문에 배포 후에 확인할 수 있을 것 같습니다.
앞으로도 성능 개선 작업은 계속 이어질 예정이며, 그 과정을 계속 공유하겠습니다.
와 진짜 대단하십니다👍🏻