이번 장에서는
- 이미지 지연 로딩
- 이미지 사이즈 최적화
- 폰트 최적화
- 캐시 최적화
- 불필요한 css 제거
에 대해서 학습하자.
- 이미지 지연로딩
- 이미지 지연 로딩은 말 그래도 첫 화면에 당장 필요하지 않은 이미지가 먼저 로드되지 않도록 지연시키는 기법이다. 이렇게 함으로써 사용자에게 가장 먼저 보이는 콘텐츠를 더 빠르게 로드할 수 있다.
- 이미지 사이즈 최적화
- 1장에서는 CDN에서 로드되었기 때문에 url만 수정하면 쉽게 이미지 사이즈를 조절할 수 있다. 이번 장에서는 CDN 이미지가 아닌 서버에 저장되어 있는 정적 이미지를 최적화해 볼 것이다.
- 폰트 최적화
- 폰트는 FE 개발에서 빠질 수 없는 주제이다. 커스텀 폰트를 적용하려고 할때 몇가지 성능 문제를 야기할 수 있는데 커스텀 폰트를 적용할 때 발생할 수 있는 문제를 알아보고 최적화해 보자.
- 캐시 최적화
- 아마 캐시라는 개념은 많이 들어봤을 것인데, 자주 사용되는 리소스를 브라우저에 저장해 두고, 다음번에 사용하려고 할때 새로 다운로드하지 않고 저장되어 있는 것을 사용하는 기술이다. 캐시를 어떻게 적용하고 활용하면 좋을지 살펴보자.
- 불필요한 CSS 제거
- 웹 서비스를 개발하다 보면 불 필요한 코드가 함께 빌드되는 경우가 있다. 그 코드가 css, js 코드일 수도 있는데, 여기서는 사용하지 않는 css 코드가 서비스 코드에 포함되어 있을 경우 해당 코드를 제거하여 파일 사이즈를 줄이는 방법에 대해 알아보자.
사용해볼 툴
- 크롬 개발자 도구의 Coverage 패널
- squoosh (이미지 압축 도구 made by 구글)
- PurgeCSS (사용하지 않는 CSS 제거해주는 툴)
이미지 지연로딩
네트워크 분석
다운로드되는 리소스 파일을 보면 폰트, 이미지, bundle.js, video등 많은 파일을 다운로드하는데 video가 용량이 커서 가장 늦게 로드된다. 우선순위를 두기 위해 이미지 등은 나중에 다운로드하고 동영상이 최우선적으로 다운로드 되도록 변경하기 위해 지연로딩을 할 예정인데, 지연로드할 이미지를 정해보자.
우리는 이미지가 화면에 보이는 순간 또는 그 직전에 이미지를 로드해야한다. 다시 말해 뷰 포트에 이미지가 표시될 위치까지 스크롤 되었을 때 이미지를 로드할지 판단할 수 있다.
Intersection Observer
만약 이미지 지연 로딩 작업을 위해 스크롤이 이동했을 때 해당 뷰포트에 이미지를 보이는 지 판단할 때 스크롤 이벤트에 이 로직을 넣으면 스크롤할 때마다 아주 많이 실행된다는 단점이 있다. 왜냐면 스크롤 이동하는 중에 이 이벤트가 계속 발생하기 때문이다. 여기에 조금이라도 무거운 로직이 들어간다면 브라우저의 메인 스레드에 무리가 간다. lodash의 throttle 방식으로 처리는 할 수 있으나 근본적인 해결방법은 아니다.
이를 해결하기 위해 많이 사용하는 방법으로 Intersection Observer를 사용하는데, 이는 브라우저에서 제공하는 API이다. 이를 통해 웹 페이지의 특정 요소를 관찰하면 페이지 스크롤 시, 해당 요소가 화면에 들어왔는지 아닌지를 알려준다. 즉 스크롤 이벤트처럼 스크롤할 때마다 함수를 호출하는 것이 아니라 요소가 화면에 들어왔을 때만 함수를 호출하는 것이다. 따라서 성능적인 면에서 scroll 이벤트 방식보다 훨씬 효율적이다.
const options = {
root: null,
rootMargin: '0px',
threshold: 1.0
};
const callback = (entries, observer) => {
console.log('Entries', entries);
}
const observer = new IntersectionObserver(callback, options);
observer.observe(document.querySelector('#target-element1'))
observer.observe(document.querySelector('#target-element2'))
options 는 Intersection Observer의 옵션이다. 여기서 root는 대상 객체의 가시성을 확인할 때 사용되는 뷰 포트 요소이다. 기본 값은 null이며 null 설정 시 브라우저의 뷰포트로 설정된다. rootMargin은 root 요소의여백이다. 쉽게 얘기해서 root의 가시 범위를 가상으로 확장하거나 축소할 수 있다. threshold는 가시성 퍼센티지로 대상 요소가 어느정도 보일 때 콜백을 실행할지 결정한다 1.0이면 대상 요소가 모두 보일 때 실행되며, 0으로 설정하면 1px이라도 보이는 경우 콜백이 실행된다. callback은 가시성이 변경될 때마다 실행되는 함수이다.
그 다음 IntersectionObserver를 생성하면 인스턴스가 나오는데 이 인스턴스를 이용하여 원하는 요소를 관찰할 수 있다.
entries 살펴보면 배열 형태로 다양한 정보(boundingClientRect, intersectionRatio 등)를 가지고 있다. 그 중에서도 가장 중요한 값은 isIntersecting이다. 이 값은 해당 요소가 뷰포트 내에 들어왔는지를 나타내는 값이다. 이 값을 통해 해당 요소가 화면에 보이는 것인지, 화면에서 나가는 것인지를 알 수 있다.
다음 할일은 화면에 이미지가 보이는 순간, 즉 콜백이 실행되는 순간에 이미지를 로드하는 일이다. 이미지 로딩은 img 태그에 src가 할당되는 순간에 일어난다. 이를 위해 src가 아닌 data-src에 넣으면 src에 값이 할당되지 않고 나중에 이미지가 뷰 포트에 들어왔을 때, data-src에 있는 값을 src로 옮겨 이미지를 로드한다.
이미지 사이즈 최적화
이미지 포멧 종류
이미지 사이즈 최적화는 간단히 말하면 이미지의 세로, 가로 사이즈를 줄여 이미지 용량을 줄이고 그만큼 더 빠르게 다운로드하는 기법이다. 이미지 사이즈를 줄이기 전에 이미지 포멧에 대해 정리를 해보면
- PNG
- JPG(JPEG)
- WebP
이다.
PNG는 무손실 압축 방식으로 원본을 훼손없이 압축하며 알파 채널을 지원하는 이미지 포멧이다. 알파 채널이란 투명도를 의미한다. PNG 포맷으로 배경 색을 투명하게 하여 뒤에 있는 요소가 보이는 이미지를 만들 수 있다.
JPG는 PNG와 다르게 압축 과정에서 정보 손실이 발생한다. 하지만 그 만큼 이미지를 더 작은 사이즈로 줄일 수 있다. 일반적으로 웹에서 이미지를 사용할 때는 고화질이어야 하거나 투명도 정보가 필요한게 아니라면 JPG를 활용한다.
WebP는 무손실 압축과 손실 압축을 모두 제공하는 최신 이미지 포맷으로 기존의 PNG나 JPG에 비해서 대단히 효율적으로 이미지를 압축할 수 있다. WebP방식은 PNG대비 26%, JPG 대비 25~34% 더 나은 효율을 가진다고 한다.
단순히 봤을 때 WebP를 이용하는게 가장 좋아보이나 최신 이미지 파일 포맷이라 아직 지원하지 않는 브라우저 호환성 때문에 사용이 쉽지 않다.
구분 PNG JPG WebP
사이즈 | 큼 | 중간 | 작음 |
화질 | 높음 | 중간 | 높음 |
호환성 | 높음 | 높음 | 낮음 |
Squoosh를 사용한 이미지 변환
JPG, PNG 포맷의 이미지를 WebP 포맷으로 변환하여 고화질, 저용량 이미지로 최적화 해보려고 한다. 이때 사용할 컨버터가 Squoosh 이다. Squoosh 는 구글에서 만든 이미지 컨버터 웹 어플리케이션으로 별도의 프로그램 설치 없이 웹에서 이미지를 손쉽게 여러가지 포맷으로 변환할 수 있다.
사이즈를 조정하고 webp로 변환하는 과정을 거친 후 이미지를 교체하면 이미지 사이즈가 상당히 줄어들어 다운로드 시간이 굉장히 짧아졌다.
하지만 WebP는 효율이 좋지만 호환성 문제가 있는데 WebP로만 렌더링을 할 경우 특정 브라우저에서는 제대로 렌더링되지 않을 수 있다. 이런 문제를 해결하려면 단순 img 태그로만 다양한 타입의 이미지를 렌더링하면 안되며, picture 태그를 사용해야한다. picture 태그는 다양한 타입의 이미지를 렌더링하는 컨테이너로 사용된다.
# 뷰포트에 따라 구분
<picture>
<source media="(min-width:650px)" srcset="img_pink_flowers.jpg">
<source media="(min-width:465px)" srcset="img_white_flowers.jpg">
<img src="img_ornage_flowers.jpg" alt="Flowers" style="width:auto;">
</picture>
# 이미지 포맷에 따라 구분
<picture>
<source srcset="photo.avif" type="image/avif">
<source srcset="photo.webp" type="image/webp">
<img src="photo.jpg" alt="photo">
</picture>
동영상 최적화
동영상 파일은 이미지처럼 하나의 요청으로 모든 영상을 다운로드하지 않는다. 동영상 콘텐츠의 특성상 파일 크기가 크기 때문에 당장 재생이 필요한 앞부분을 먼저 다운로드한 뒤 순차적으로 나머지 내용을 다운로드한다.
동영상 최적화는 이미지 최적화와 비슷한데, 가로와 세로 사이즈를 줄이고, 압축 방식을 변경하여 용량을 줄인다. 프레임 레이트를 줄이는 등 이미지보다 조금 더 복잡한 설정도 있지만, 영상 특화가 아닌경우 이정도까지 알 피룡는 없다.
주의해야할 점은 지금하려는 최적화 방법은 영상을 더 작은 사이즈로 압축하는 작업으로 화질을 낮추는 작업이다. 그렇기에 영상이 메인인 콘텐츠라면 적합하지 않는 작업이다. 이 장에서는 media.io 라는 서비스를 이용해보자.
우선 확장자를 WebM으로 선택하고 Bitrate와 Audio 체크를 해제한다. 그럼 사이즈가 1/5인 12mb로 줄어든다.
영상 파일을 교체하는데 picture 태그처럼 영상은 video 태그를 사용하면 된다.
import video from '../assets/banner-video.mp4';
import video_webm from '../assets/_banner-video.webm';
<video className="" autoPlay loop muted>
<source src={video_webm} type="video/webm">
<source src={video} type="video/mp4">
</video>
전보다 빠르게 로드되지만 화질이 많이 저하되었다. 책에서는 패턴과 필터를 씌우는 우회방법을 추천한다. (blur)
폰트 최적화
해당 페이지에서 폰트 파일 크기가 750kb이고 다운로드하는데 4.82초가 걸린다. 즉 페이지가 로드되고 대략 5초후에야 폰트가 제대로 적용된 모습을 볼 수 있다. 폰트가 깜박이면서 바뀌는 모습은 페이지가 느리다는 느낌을 줄 수 있고 또 다른 요소를 밀어낸 수도 있다.
FOUT, FOIT
폰트의 변화로 발생하는 이 현상을 FOUT(Flash of Unstyled Text), FOIT(Flash of Invisible Text) 라고 한다. FOUT는 Edge 브라우저에서 폰트를 로드하는 방식으로 폰트 다운로드 여부와는 상관없이 먼저 텍스트를 보여주고 폰트가 다운로드되면, 그때 폰트를 적용하는 방식이다.
FOIT는 폰트가 Chrome, Safari, Firefox 브라우저에서 폰트를 로드하는 방식으로, 폰트가 완전히 다운로드되기 전까지 텍스트 자체를 보여주지 않는다. 그리고 폰트 다운로드가 완료되면 폰트가 적용된 텍스트를 보여준다.
하지만 Chrome에서는 폰트가 제대로 다운로드되지 않았는데도 텍스트가 보인다. 그 이유는 완전하 FOIT가 아니라 3초만 기다리는 FOIT이기 때문이다. 즉 3초동안 폰트가 다운로드 되기를 기다리다가 3초가 지나도 폰트가 다운로드되지 않으면 기본 폰트로 텍스트를 보여준다. 그 다음 다운로드가 끝나면 해당 폰트를 적용한다.
어떤 방식이 더 낫다고 할 순 없지만 상황에 따른 적절한 방법이 있다. 우리가 집중해야할 점은 폰트를 최대한 최적화해서 폰트 적용시 발생한느 깜박임 현상을 최소화 하는 것이다.
폰트 최적화 방법으로는 크게 2가지가 있는데
- 폰트 적용 시점을 제어
- 폰트 사이즈를 줄이기
상황에 따라 좋은 표기 방식이 있는데 중요한 텍스트 같은 경우는 FOIT 방식으로 폰트를 적용하면 텍스트 내용이 사용자에게 빠르게 전달되지 않을 것이다. 반면 꼭 전달하지 않아도 되는 텍스트의 경우 FOUT 방식으로 인한 폰트 변화는 사용자의 시선을 분산시킬 수 있다. 따라서 서비스 또는 콘텐츠의 특성에 맞게 적절한 방식을 적용해야 한다. CSS의 font-display 속성을 이용하면 폰트가 적용되는 시점을 제어할 수 있다.
font-display는 @font-face에서 설정할 수 있고 다음과 같은 값을 갖는다.
- auto: 브라우저 기본동작
- block: FOIT (timeout = 3s)
- swap: FOUT
- fallback: FOIT (timeout = 0.1s) / 3초 후에도 불러오지 못한경우 기본포트 유지, 이후 캐시
- optional: FOIT (timeout = 0.1s) / 이후 네트워크 상태에 따라 기본 폰트로 유지할지 결정, 이후캐시
이 속성을 이용하면 FOUT 방식으로 폰트를 렌더링하는 Edge에 FOIT 방식을 적용하거나, FOIT 방식으로 폰트를 렌더링하는 크롬에 FOUT 방식을 적용할 수 있다. fallback과 optional은 FOIT 방식이지만 텍스트를 보여 주지 않는 시간이 3초가 아닌 0.1초이다. 차이점은 fallback의 경우 3초 후에도 폰트를 다운로드하지 못한 경우, 이후에 폰트가 다운로드되지 못하더라도 폰트를 적용하지 않고 캐시해둡니다. 결국 최초 페이지 로드에서 폰트가 늦게 다운로드되면 폰트가 적용되지 않는 모습이 계속 보인다. 하지만 새로고침 시 폰트가 캐시되어 바로 적용된 폰트를 볼 수 있다. optional의 경우 3초가 아니라 사용자의 네트워크 상태를 기준으로 폰트를 적용할 지 기본 폰트로 유지할 지 결정한다.
이처럼 font-display 속성을 이용해서 폰트가 적용되는 시점을 제어할 수 있다. 콘텐츠에 맞게 적절한 구현을 하자. 이 책에서는 fade-in 방식을 통해 애니메이션을 적용하여 어색함을 줄인다.
두번째로 폰트 사이즈 줄이기를 알아보면 하나는 압축률이 좋은 폰트 포맷을 사용하는 것이고 다른 하나는 필요한 문자의 폰트만 로드하는 것이다.
우리가 흔히 알고 있는 폰트 포맷은 운영체제에서 사용하는 TTF 및 OTF 포맷이다. TTF 포맷은 파일 크기가 크므로 매번 다운로드해야 하는 웹 환경에서는 적절하지 않다. 이래서 나온 것이 WOFF이다. Web Open Font Format의 약자로 웹을 위한 폰트이다. 이 포맷은 TTF 폰트를 압축하여 웹에서 빠르게 로드할 수 있도록 만들어졌고 더 나아가 WOFF2 라는 향상된 압축 방식을 적용한 포맷도 있다.
파일크기
EOT > TTF/OTF > WOFF >WOFF2
하지만 WOFF, WOFF2는 브라우저 호환성 문제가 있다. 버전이 낮은 일부 브라우저에서는 해당 포맷을 지원하지 않을 수 있다. 그래서 WOFF2 > WOFF > TTF를 적용하는 방식을 적용해보자.
@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;
}
서브셋 폰트 사용
모든 문자가 아닌 일부 문자의 폰트 정보만 가지고 있는 것을 서브셋 폰트라고 한다. 이는 transfonter 서비스에서 생성할 수 있다.
더 나아가 폰트를 파일 형태가 아닌 Data-URI 형태로 CSS 파일에 포함할 수도 있다. Data-URI란 data 스킴이 접두어로 붙은 문자열 형태의 데이터인데, 쉽게 말해서 파일을 문자열 형태로 변환하여 문서(HTML, CSS, JS 등)에 인라인으로 삽입하는 것이다. App.css 파일에 넣어두면 별도의 네트워크 로드 없이 App.css 파일에서 폰트를 사용할 수 있다.
src: url('data:font/woff2;charset=utf-8~~')
이렇게 경로가 아닌 문자열이 들어가게 된다. 실제로 로드 소요시간이 매우 짧다. 하지만 이 방식이 무조건 좋은건 아니다. App.css의 다운로드가 느려질 수 있기 때문이다. (App.css는 main.chunk.js로 포함되어 빌드됨)
그리고 여기서는 서브셋을 통해 폰트 파일크기가 작아 Data-URI형태로 큰 문제가 없었지만 매우 큰 파일이라면 Data-URI 형태로 포함한 파일 크기가 그만큼 커져 또 다른 병목을 발생시킬 수 있다.
캐시 최적화
lighthouse에 Serve static assets with an effcient cache policy 라는 항목이 있는데 이 항목은 네트워크를 통해 다운로드하는 리소스에 캐시를 적용하라는 의미이다.
캐시란 자주 사용하는 데이터나 값을 미리 복사해 둔 임시 저장 공간 또는 저장하는 동작이다.
캐시의 종류
웹에서 사용하는 캐시는 크게 2가지 인데
- 메모리 캐시
- 메모리에 저장하는 방식, 여기서 메모리는 RAM을 의미
- 디스크 캐시
- 파일 형태로 디스크에 저장하는 방식
어떤 캐시를 사용할지는 직접 제어할 수 없다. 브라우저가 사용 빈도나 파일 크기에 따라 특정 알고리즘에 의해 알아서 처리한다. 네트워크 패널에서 memory cache나 disk cache인지 확인할 수 있다.
새로고침하면 memory cache가 많고 브라우저를 껏다키면 디스크 캐시가 많다.
Cache-Control
Cache-Control은 리소스의 응답 헤더에 설정되는 헤더이다. 브라우저는 서버에서 이 헤더를 통해 캐시를 얼마나 어떻게 적용해야 하는지 판단한다.
- no-cache : 캐시를 사용하기 전 서버에 검사 후 사용
- no-store : 캐시 사용 않음
- public: 모든 환경에서 캐시 사용가능
- private: 브라우저 환경에서만 캐시 사용, 외부 캐시 서버에서는 사용 불가
- max-age: 캐시의 유효 시간
public과 private의 차이는 캐시 환경에 있는데, 웹 리소스는 브라우저뿐만 아니라 웹 서버와 브라우저 사이를 연결하는 중간 캐시 서버에서도 캐시될 수 있다. 만약 중간 서버에서 캐시를 적용하고 싶지 않다면 private 옵션을 사용한다.
캐시시간이 지나기 전에는 캐시 값을 가져오는 것을 네트워크 탭에서 확인할 수 있으나 그 이후에는 304를 통해 서버에 해당 리소스를 계속 사용할지 확인을 한다. 해당 값이 현재 서버의 값과 같은지 확인은 Etag로 확인한다.
불필요한 CSS 제거
coverage 탭에서 살펴보면 사용되지 않는 css가 많고 많은 유틸 클래스가 사용되지 않았음을 알 수 있다. 이를 위해 PurgeCSS를 사용한다.
'개발 > Front-end' 카테고리의 다른 글
e.preventDefault();의 역할과 언제 사용해야 할까? (0) | 2025.02.13 |
---|---|
웹 개발 스킬을 한단계 높여주는 프론트엔드 성능 최적화 - 2장 올림픽 통계 서비스 최적화 (0) | 2025.01.20 |
웹 개발 스킬을 한단계 높여주는 프론트엔드 성능 최적화 - 1장 블로그 서비스 최적화 (0) | 2025.01.16 |
ARIA 속성에 대하여 - 웹 접근성 (1) | 2024.10.09 |
웹 개발 스킬을 한단계 높여주는 프론트엔드 성능 최적화 - 0 (1) | 2024.09.24 |