Chapter1. Optimization
Chapter2. Optimization 기법
Chapter2-1. 최적화 기법
Chapter2-2. Tree Shaking
Chapter2-3. Lighthouse
최적화는 주어진 자원, 즉 실제로 웹 앱을 구동시키는 브라우저의 파워나 게임이라면 게임기의 CPU 및 GPU 등의 자원을 좀 더 효율적으로 사용하기 위해 개발자들이 고안한 방법이다.
최적화를 잘 해놓은 앱일수록 더 빠르게 함수를 호출하고, 빠르게 함수를 호출한다는 것은 결국 앱이 더 빠른 속도로 구동되면서 앱을 사용하는 유저에게 더 좋은 경험을 준다.
최적화, 最適化, optimization [한국정보통신기술협회의 정보통신 용어사전]
주어진 상황에서 원하는 가장 알맞은 결과를 얻을 수 있도록 처리하는 과정.
최적화는 허용된 자원의 한계 내에서 주어진 요구사항을 만족시키면서 최선의 결과를 얻는 과정이다. 수익과 관련되는 분야에서는 이익을 최대로 내는 과정을 말하기도 한다. 다양한 분야와 때에 따라 다르게 정의할 수 있고 물류(logistics), 설계(design) 문제 등에 응용된다.
분야에 따라서 의미가 조금씩 달라지긴 하지만, 최적화는 보통 주어진 조건으로 최대 효율을 낼 수 있도록 하는 것을 의미한다.
물류업을 예로 들어보자. 물류업에서는 상품의 특성, 출발지와 도착지의 위치, 운송 수단의 속도, 운송 비용, 도착해야 하는 기한 등 고려해야 하는 여러 조건이 존재한다. 이 조건들 아래에서 가능한 적은 비용으로 정해진 시간 내에 운송이 완료될 수 있는 최적의 답안을 찾아가는 것을 물류 최적화라고 할 수 있다. 같은 물건이라도 도착 기한이 길다면 느려도 비용이 저렴한 운송 수단을 선택하는 것이, 도착 기한이 짧다면 비싸더라도 기한 내에 도착할 수 있는 운송 수단을 선택하는 것이 주어진 상황에서 가장 효율적인, 최적의 답안일 것이다.
컴퓨터 공학에서의 최적화는 가능한 적은 리소스를 소모하면서 가능한 한 빠르게 원하는 결과를 얻을 수 있도록 하는 것을 의미한다. 알고리즘 문제를 푸는 것을 생각하면 이해하기 쉽다. 원하는 결과가 나온다면, 메모리를 조금이라도 덜 소모하거나 연산 횟수가 한 번이라도 더 적은 코드가 더 효율적이고 최적화된 코드이다. 더 적은 비용, 더 적은 시간을 소모하기 때문.
물론 컴퓨터 성능을 업그레이드하면 같은 코드를 사용하더라도 더 빠르게 결과를 얻을 수 있다. 하지만 알고리즘 문제의 결과를 빠르게 확인하기 위해 부품을 업그레이드하는 것은 비용도 많이 들고, 업그레이드한 상태에서도 최적화되지 않은 코드보다 최적화된 코드가 더 빠를 테니, 기본적으로 더 효율적인 코드를 작성하기 위해서 노력하는 것이 좋다.
💡 그렇다면 웹 개발에서의 최적화는 무엇을 의미할까? 바로 주어진 조건 하에서 최대한 빠르게 화면을 표시하도록 만드는 것이다.
웹 개발 중 프론트엔드 단에서 할 수 있는 최적화 방법을 알아보기 전, 웹 개발에서 최적화가 왜 필요한지, 그 필요성에 대해서 먼저 알아보자.
웹 개발에서의 최적화는 화면을 최대한 빠른 속도로 표시하게 하는 것이라고 했다. 이는 최적화가 잘되지 않은 웹 페이지는 화면 로딩에 시간이 걸린다는 뜻으로도 볼 수 있다. 화면을 불러오는 시간이 길어지면 사용자가 페이지를 이탈할 확률이 높아진다.
이탈: 방문자가 웹 사이트의 첫 페이지에서 아무런 상호작용도 하지 않고 종료하는 것
2016년도, 2017년도 구글의 조사 결과에 따르면 페이지 로드가 3초 이상 걸리면 53%의 사용자가 사이트를 이탈한다고 한다. 또한 페이지 로드 시간이 길어지면 사이트 방문자의 이탈률이 폭발적으로 증가한다고 한다. 페이지 로드 속도가 1초에서 3초로 늘어나면 이탈률은 32%, 5초로 늘어나면 90%, 6초로 늘어나면 106%, 10초로 늘어나면 123%까지 증가하는 것으로 나타났다.
현재는 인터넷 속도가 더 빨라졌을 테니, 사람들은 조사 결과보다 더 빠르게 이탈할 가능성이 높다. 거기다 속도를 중시하는 한국은 더 큰 폭으로 이탈률이 증가할 것으로 예상할 수 있다. 열심히 만든 웹 사이트가 로딩이 느리다는 이유로 사용자가 이탈하게 된다면 정말 속상한 일일것. 하지만 반대로, 웹 사이트의 성능 최적화를 통해 페이지 로딩 속도를 줄이면, 사용자의 이탈률을 효과적으로 줄일 수 있다는 의미이기도 하다. 여기서 최적화의 필요성을 엿볼 수 있다.
이탈률이 줄어들면, 전환율이 높아질 확률도 커진다. 여기서 전환율이란, 웹 사이트를 방문한 사용자 중 회원가입, 상품 구매, 게시글 조회, 다운로드 등의 행위를 한 방문자의 비율을 의미한다.
물론 화면이 제대로 표시된다고 해도 방문자를 실제 서비스 이용자로 전환하게 하는 일은 어려운 일이다.
하지만 이탈해버린 사용자의 전환율은 0%임. 전환율을 늘려 서비스 사용자를 늘리기 위해서는 이탈률을 줄여야 함.
빠른 웹 사이트 로딩 속도는 수익 증대까지 이어질 수 있다.
이탈률 감소, 전환율 증가는 트래픽 증대 및 회원 수 증가로 이어지고, 이는 곧 수익 증대를 의미한다. 실제로 로딩 속도가 1초 빨라졌을 때 아마존 판매량은 1%, 구글 검색량은 0.2%, 월마트의 전환율은 2% 증가했다고 한다. 퍼센티지로 보면 크지 않아 보이지만, 이 수치를 돈으로 환산하면 각각 68억 달러, 4억 5천만 달러, 2억 4천400만 달러의 매출 증가라고 한다. 1초 차이로 어마어마한 수익이 왔다 갔다 하는 것이다.
꼭 아마존이나 월마트처럼 규모가 크지 않은 서비스라도, 로딩 속도의 차이는 이처럼 유의미한 수익 차이를 낼 수 있다.
최적화는 효과적인 UX 개선 수단이다.
페이지 로딩이 빠를수록 UX는 향상되기 때문에 이미 페이지 로드 속도가 빠른 편이라고 해도 최적화를 통해 UX가 더욱 향상할 수 있다.
만약 로딩이 오래 걸릴 경우, 스피너, 프로그레스 바, 스켈레톤과 같이 로딩 중임을 알려주는 UI를 먼저 표시하여 방문자가 조금 더 인내심을 갖고 기다리게 하는 방법도 있다. 하지만 이러한 방법은 최적화를 통해 페이지 로드 속도 자체를 최대한 빠르게 하는 것보다 UX에 좋다고 볼 수는 없다. 또한 방문자의 체류 시간이 좀 더 늘어날 뿐, 페이지 로드 속도가 개선되지 않는다면 이탈률 개선까지 이어지기는 어렵다.
따라서 이탈률 감소와 UX 향상 효과를 동시에 보기 위해서는 웹 사이트 성능 최적화를 진행하는 것이 가장 좋다.
최적화의 방법에는 매우 다양한 방법이 있고, 개발할 때 어떤 방법을 사용할 것인지 판단하는 능력이 필요하다.
화면을 렌더링할 때는 HTML 파일과 CSS 파일이 필요하다. HTML 파일은 DOM 트리를, CSS 파일은 CSSOM 트리를 만들고 두 트리를 결합하여 렌더링할 때 사용하게 된다. 이 두 트리 중에서 하나라도 변경되면 리렌더링을 유발하는데, 이때 트리의 크기가 크고 복잡할수록 더 많은 계산이 필요하기 때문에 리렌더링에 소모되는 시간도 길어진다. 따라서 HTML, CSS 코드를 최적화함으로써 렌더링 성능을 향상시킬 수 있다.
DOM 트리가 깊을수록, 자식 요소가 많을수록 DOM 트리의 복잡도는 커진다. 복잡도가 클수록 DOM 트리가 변경되었을 때 계산해야 하는 것도 많아진다. HTML 요소들의 관계를 잘 살펴보고, 불필요하게 깊이를 증가시키는 요소가 있다면 삭제하자.
// 수정 전
<div>
<ol>
<li> 첫 번째 </li>
<li> 두 번째 </li>
<li> 세 번째 </li>
</ol>
</div>
// 수정 후 : 불필요한 div 요소 제거
<ol>
<li> 첫 번째 </li>
<li> 두 번째 </li>
<li> 세 번째 </li>
</ol>
인라인 스타일은 개별 요소에 스타일 속성을 작성해주는 것이기 때문에, 클래스로 묶어서 한 번에 작성해도 될 스타일 속성을 중복으로 작성하게 되는 경우가 생긴다.
이처럼 불필요한 코드 중복은 가독성을 떨어뜨릴 뿐 아니라 파일 크기를 증가시킨다. 또한 CSS 파일을 따로 작성하면 단 한 번의 리플로우만 발생하는 것과 달리, 인라인 스타일은 리플로우를 계속해서 발생시켜 렌더링 완료 시점을 늦춘다. 애초에 인라인 스타일은 웹 표준에 맞지 않으므로 지양해야 함.
//수정 전
<div style="margin: 10px;"> 마진 10px </div>
<div style="margin: 10px;"> 이것도 마진 10px </div>
//수정 후 : class와 CSS로 대체
<div class="margin10"> 마진 10px </div>
<div class="margin10"> 이것도 마진 10px </div>
.margin10 {
margin: 10px;
}
CSS 파일의 모든 코드의 분석이 끝난 후에 CSSOM 트리가 생성된다. 그만큼 불필요한 CSS 코드가 있다면 CSSOM 트리의 완성이 늦어짐. 따라서 사용하지 않는 CSS 코드가 있다면 제거하는 것이 좋다. 보통 CSS 코드를 사용하던 요소를 삭제하면서 해당 요소의 CSS 코드만 남게 되는 경우가 많다. 요소를 삭제할 일이 생기면, CSS 코드만 남지는 않는지 확인하고 함께 삭제하면 이런 상황을 방지할 수 있다.
셀렉터가 복잡할수록 스타일 계산과 레이아웃에 시간을 더 많이 소모하게 된다. 따라서 최대한 간결한 CSS 셀렉터를 사용하는 것이 좋다.
// 복잡한 CSS 셀렉터 예시
.cart_page .cart_item #firstItem { ... }
// 필요한 경우에는 어쩔 수 없지만, 가능한 한 간결하게 작성해줍니다.
.cart_item { ... }
HTML 파일에서 JavaScript 파일을 불러올 땐 <script>
요소를, CSS 파일을 불러올 땐 <link>
요소를 사용하게 된다. 이때 파일을 불러오는 위치가 어디인가에 따라서 렌더링 완료 시점이 달라질 수 있다.
화면을 렌더링할 때는 DOM 트리와 CSSOM 트리가 필요함. DOM 트리는 HTML 코드를 한 줄 한 줄 읽으면서 순차적으로 구성할 수 있지만, CSSOM 트리는 CSS 코드를 모두 해석해야 구성할 수 있다. 따라서 CSSOM 트리를 가능한 빠르게 구성할 수 있도록 HTML 문서 최상단에 배치하는 것이 좋다.
// CSS 파일은 HTML 파일 상단의 head 요소 안에서 불러오는 것이 좋습니다.
<head>
<link href="style.css" rel="stylesheet" />
</head>
JavaScript는 DOM 트리와 CSSOM 트리를 동적으로 변경할 수 있다. HTML 코드 파싱 중에 <script>
요소를 만나는 순간 해당 스크립트가 실행되며, <script>
요소 이전까지 생성된 DOM까지만 접근할 수 있다.
<script>
요소를 HTML 코드 중간에 넣는다면, 해당 요소 이후에 생성될 DOM을 수정하는 코드가 있는 경우에는 화면이 의도한 대로 표시되지 않게 된다.
또한 스크립트 실행이 완료되기 전까지 DOM 트리 생성이 중단된다.
JavaScript 파일을 다운받아와서 사용하는 경우에는 다운로드 및 스크립트 실행이 완료될 때까지 DOM 트리 생성이 중단된다. DOM 트리 생성이 중단된 시간만큼 렌더링 완료 시간은 늦춰지게 된다. 이러한 이유로 JavaScript 파일은 DOM 트리 생성이 완료되는 시점인 HTML 문서 최하단에 배치하는 것이 좋다.
<body>
<div>...</div>
...
// JavsScript 파일은 body 요소가 닫히기 직전에 작성하는 것이 가장 좋습니다.
<script src="script.js" type="text/javascript"></script>
</body>
페이지의 대부분의 용량은 HTML/CSS/JS와 같은 코드 데이터가 아닌 이미지 파일과 같은 미디어 파일이 차지한다. (전체 페이지 용량의 약 51% 차지) 그래서 이미지의 용량을 줄이거나 요청의 수를 줄이는 것을 우선적으로 고려할 시, 사용자 경험을 빠르게 개선할 수 있다.
클라이언트에서 서버 요청이 증가할수록 로딩 시간은 점점 늘어난다. 따라서 웹 페이지를 로드하는 데 필요한 서버 요청 수를 줄이기 위해 이미지 스프라이트 기법을 사용할 수 있다.
친숙한 네이버의 메인 화면의 일부다. 박스에 표시한 아이콘 이미지의 경우 각각의 이미지를 서버에 요청할 경우 웹사이트의 로딩 시간이 늘어나게 된다. 이를 해결하기 위해 사용하는 기법이 이미지 스프라이트이다.
이미지 스프라이트 기법은 여러 개의 이미지를 모아 하나의 스프라이트 이미지로 만들고 CSS의 background-position
속성을 사용해 이미지의 일정 부분만 클래스 등으로 구분하여 사용하는 방법.
실제로 네이버에 접속한 후 개발자 도구를 이용해 아이콘 컴포넌트를 살펴보면 다음과 같이 스프라이트 이미지가 적용되어 있는 것을 직접 확인할 수 있다.
하나의 이미지를 배경 이미지로 사용하되, 표시하고 싶은 부분에 맞춰 width, height, background-position 속성을 주어 아이콘을 만든다. 위 이미지를 보면서 CSS 속성값에따라 어떤 부분이 보이는지 확인하면서 이해해보자.
해당 기법을 이용하면 한번의 이미지 요청으로 대부분의 개별 이미지를 사용할 수 있기 때문에 네트워크 로딩 시간을 줄일 수 있다. 또한, 많은 이미지 파일을 개별로 관리할 필요없이 특정 스프라이트 이미지 파일만을 관리하면 되므로 관리가 용이하다는 장점이 있다. 네이버 예시만 해도 이미지 스프라이트를 통해 120여개의 아이콘을 하나의 이미지로 모두 사용할 수 있게 됨.
아이콘 사용이 많을 때에는, 모든 아이콘을 이미지로 사용하는 것이 아니라 아이콘 폰트를 사용하여 용량을 줄일 수 있다. 대표적인 아이콘 글꼴 서비스로는 Font Awesome이 있다. Font Awesome의 사용 방법에는 두 가지가 있다.
Font Awesome에 가입하면 키트를 발급해주는데, 이 키트를 HTML 파일에서 <head>
요소에 넣어주기만 하면 CDN으로 Font Awesome을 사용할 준비가 완료된다.
Font Awesome 사이트에서 사용하고 싶은 아이콘을 찾아서 사용할 환경(HTML, React, Vue)에 맞는 코드를 복사하고 붙여넣기만 하면 사용할 수 있
다.
Font Awesome을 다른 라이브러리처럼 설치해서 사용하는 방법도 있다. React 환경에서 사용할 경우에는 다음과 같은 패키지들을 설치하면 된다.
// 핵심 패키지 설치
npm i --save @fortawesome/fontawesome-svg-core
// 아이콘 패키지 설치 (해당 코드는 무료 아이콘들입니다. 유료 아이콘을 사용할 경우 추가로 설치가 필요합니다.)
npm i --save @fortawesome/free-solid-svg-icons
npm i --save @fortawesome/free-regular-svg-icons
npm i --save @fortawesome/free-brands-svg-icons
// Font Awesome React 구성 요소 설치
npm i --save @fortawesome/react-fontawesome@latest
설치 후에는 Font Awesome 사이트에서 사용하고 싶은 아이콘의 정보를 확인한 후에, 알맞게 불러와서 사용하면 됨. 이때 아이콘 이름은 camelCase로 작성해야 함.
이렇게 불러온 아이콘은 클래스명을 직접 붙이거나 Font Awesome이 정해준 방법을 사용하여 스타일링해줄 수 있다.
https://fontawesome.com/v5/docs/web/use-with/react#features
이미지 최적화를 위해 전통적으로 사용하는 JPEG 또는 PNG 형식이 아닌 새롭게 등장한 이미지 포맷인 WebP 또는 AVIF를 사용하여 용량을 더욱 감소시킬 수 있다.
WebP는 PNG와 비교해 26% 용량이 감소되며 JPEG와 비교했을 땐 25-35% 더 감소된다. AVIF는 JPEG와 비교했을 때 무려 용량의 50%가 감소되며 WebP와 비교했을 땐 20% 감소된다.
하지만 WebP와 AVIF 모두 비교적 최근에 등장한 이미지 포맷이기 때문에 JPEG 포맷처럼 모든 브라우저에서 호환되지 않는다는 단점이 있다. WebP의 경우 비교적 최근에 브라우저 지원이 되었으므로 구버전의 브라우저에서는 지원이 안될 수 있으며 Safari 브라우저에서도 지원하지 않는다. AVIF의 경우에는 Chrome, Opera 등 소수의 브라우저만 지원하고 있다. (참고 - Can I use WebP?, Can I use AVIF?)
그럼 개발자가 각 브라우저의 이미지 호환성을 파악해서 이미지 파일을 분기해야 할까? 다행히도 HTML의 <picture>
태그를 이용하면 각 브라우저의 호환에 맞도록 분기를 대체할 수 있다.
<picture>
: img 요소의 다중 이미지 리소스(multiple image resources)를 위한 컨테이너를 정의할 때 사용한다.
다음과 같이 HTML 태그를 작성할 시, 만약 접속한 브라우저에서 <source>
태그 내의 srcset에 정의한 WebP 포맷을 지원하지 않는다면 해당 <source>
태그는 무시된다. 이와 같은 속성을 이용하여 각 브라우저에 따라 이미지 포맷을 최적화할 수 있다.
<picture>
<source srcset="logo.webp" type="image/webp">
<img src="logo.png" alt="logo">
</picture>
캐시는 다운로드 받은 데이터나 값을 미리 복사해 놓는 임시 장소를 뜻하며, 데이터에 접근하는 시간이 오래 걸리는 경우나 값을 다시 계산하는 시간을 절약하고 싶은 경우에 사용한다. 캐시를 사용하면 리소스를 매번 다운로드받을 필요 없이 캐시에서 꺼내와 재사용하면 되기 때문에 네트워크 리소스는 물론 로딩 시간을 줄일 수 있다.
프론트엔드단에서 캐시를 사용하기 위해서는 HTTP 요청을 보낼 때 조건부 요청 헤더를 작성하여 캐시를 재사용해도 되는지 확인해주면 된다.
If-Modified-Since
: 캐시된 리소스의 Last-Modified 값 이후에 서버 리소스가 수정되었는지 확인하고, 수정되지 않았다면 캐시된 리소스를 사용한다.If-None-Match
: 캐시된 리소스의 ETag 값과 현재 서버 리소스의 ETag 값이 같은지 확인하고, 같으면 캐시된 리소스를 사용한다.보통 두 종류의 헤더를 동시에 사용한다. 둘 중 하나만 사용했다가 매칭되는 응답 헤더가 없는 경우에는 재사용할 수 있는 경우에도 리소스를 다시 받아와야 하는 경우가 생길 수 있기 때문이다.
CDN은 콘텐츠를 좀 더 빠르고 효율적으로 제공하기 위해 설계되었다. 네트워크 지연(latency)은 유저와 호스팅 서버간의 물리적 거리의 한계가 존재하기 때문에 발생할 수 밖에 없다. 유저와 서버의 거리가 멀다면 지연(latency) 또한 늘어난다. CDN은 이를 해결하고자 세계 곳곳에 분포한 분산된 서버에 콘텐츠를 저장한다.
간단히 말해, CDN은 유저가 가까운 곳에 위치한 데이터 센터(서버)의 데이터를 가져온다. 그러므로 데이터가 전달되기 위해 거쳐야하는 서버의 갯수가 크게 줄기 때문에 로딩 속도가 빨라진다.
추후에 CDN을 사용해보고싶다면, CloudFront, Cloudflare와 같은 CDN 서비스들에 대해서 알아보자.
'트리쉐이킹(Tree Shaking)은 말 그대로 나무를 흔들어 잔가지를 털어내듯 불필요한 코드를 제거하는 것을 의미한다.
웹 개발을 할 때, 애플리케이션의 규모가 커지면서 코드의 양이 방대해지고, 다양한 라이브러리를 가져다 사용하게 되면 불필요한 코드를 그대로 가져가는 경우가 생각보다 많이 생긴다. 이런 불필요한 코드들을 찾아내어 제거하면 웹 사이트 성능 최적화에 큰 도움이 된다. 특히 JavaScript는 다음과 같은 이유로 가능하면 트리쉐이킹을 해주는 것이 좋다.
요즘은 과거 HTML 위주의 단순한 웹 페이지와는 비교도 안 될 정도로 규모 있고 화려한 인터랙션을 자랑하는 웹 애플리케이션들이 많다. 웹 사이트에서 인터랙션이 많아졌다는 것은 그만큼 JavaScript의 비중이 높아졌다는 뜻이기도 함.
JavaScript 파일의 크기만 커진 것이 아님. JavaScript 파일을 요청하는 HTTP 요청 수 또한 크게 증가함. 크기가 훨씬 커진 JavaScript 파일이 늘어난 요청 횟수만큼 더 오가는 것이니, 네트워크 리소스 소모가 그만큼 커졌다는 것을 알 수 있다.
JavaScript 파일 크기의 증가, 요청 횟수의 증가는 그만큼 파일이 오고 가는 동안 화면 표시가 늦어진다는 것을 뜻하고, 네트워크 속도가 느린 환경에서는 더 큰 병목현상을 유발한다. 따라서 트리쉐이킹을 통해 파일 크기를 가능한 줄이는 것이 최적화에 도움이 된다.
JavaScript 파일이 실행되기 위해서는 여러 과정을 거치게 된다. 다운로드부터 필요한 경우에는 우선 요청을 보내어 파일을 다운받아 온 다음 압축을 해제해야 한다. 그다음에는 JavaScript 코드를 파싱하여 DOM 트리를 생성한다. 파싱이 끝나면 컴파일하여 컴퓨터가 이해할 수 있는 언어로 바꿔줘야 한다. 이 컴파일 과정까지 거쳐야지 비로소 코드를 실행할 수 있다.
이처럼 코드 실행까지 거쳐야 하는 과정이 많기 때문에 JavaScript는 다른 리소스에 비해서 실행까지 상대적으로 많은 시간을 소모하게 된다.
JavaScript 파일의 실행은 CPU에 크게 영향을 받는데, 그렇다 보니 사양이 천차만별인 모바일 환경에서 그 영향이 더욱 두드러진다. 실제로 휴대폰의 사양에 따라 소모 시간이 크게 차이 나는 것을 아래 도표를 통해 확인할 수 있다.
(출처 : The cost of JavaScript in 2019, V8 dev)
앞서 공부한 최적화의 개념에서, 페이지 로드 시간이 3초를 넘어가면 53%의 사용자가 이탈한다고 했다. JavaScript 파일을 요청하고 다운받아 오는 시간을 제외하고서 파일을 실행하는 데만 2.9초가 걸린다면 파일을 실행하는 동안에만 이미 50% 이상의 사용자가 이탈할 것이라고 예상할 수 있다. 기기 환경에 따라서 2.9초의 몇 배의 시간을 파일 실행에만 사용할 수도 있는 만큼 이탈률은 그만큼 커질 수 있다. 이러한 상황을 최대한 줄이기 위해서라도 트리쉐이킹을 통한 최적화가 필요하다.
웹팩 4버전 이상을 사용하는 경우에는 ES6 모듈(import
, export
를 사용하는 모듈)을 대상으로는 기본적인 트리쉐이킹을 제공한다.
Create React App을 통해 만든 React 애플리케이션도 웹팩을 사용하고 있기 때문에 트리쉐이킹이 가능하다. 웹팩을 사용하는 환경에서 효과적으로 트리쉐이킹을 수행하는 방법에 대해서 알아보자.
import 구문을 사용해서 라이브러리를 불러와서 사용할 때, 라이브러리 전체를 불러오는 것이 아니라 필요한 모듈만 불러오면 번들링 과정에서 사용하는 부분의 코드만 포함시키기 때문에 트리쉐이킹이 가능해진다.
import React from 'react'
위처럼 모든 코드를 불러오면, 아래와 같이 React의 모든 코드가 불려온다.
이 중에서 실제로 사용하는 코드는 얼마 되지 않더라도 번들링할때 이 모든 코드를 같이 빌드하게 된다. 불필요한 코드가 포함되는 것임. 이를 방지하기 위해서 import 해올 때 아래와 같이 실제로 사용할 코드만 불러와 주면 된다.
import { useState, useEffect } from 'react'
그러면 불러오지 않은 코드는 빌드할 때 제외되므로 코드의 크기를 줄일 수 있게 된다.
Babel은 자바스크립트 문법이 구형 브라우저에서도 호환이 가능하도록 ES5 문법으로 변환하는 라이브러리이다.
이 때 ES5문법은 import
를 지원하지 않기 때문에 commonJS 문법의 require
로 변경시키는데, 이 과정은 트리쉐이킹에 큰 걸림돌이 된다. require
는 export
되는 모든 모듈을 불러오기 때문이다.
1번에서 작성한 것처럼 필요한 모듈만 불러오기 위한 코드를 작성해도 소용이 없어짐.
이를 방지하기 위해서 Barbelrc 파일에 다음과 같은 코드를 작성해주면 ES5로 변환하는 것을 막을 수 있다
{
“presets”: [
[
“@babel/preset-env”,
{
"modules": false
}
]
]
}
반대로, modules 값을 true로 설정하면 항상 ES5 문법으로 변환하므로 주의해서 작성해야 한다.
웹팩은 사이드 이펙트를 일으킬 수 있는 코드의 경우, 사용하지 않는 코드라도 트리쉐이킹 대상에서 제외시킵니다.
const crews = ['kimcoding', 'parkhacker']
const addCrew = function (name) {
crews.push(name)
}
위 코드에서 addCrew
함수는 함수 외부에 있는 배열인 crews
를 변경시키는 함수이다. 해당 함수는 외부에 영향을 주지도 받지도 않는 함수, 순수 함수가 아니기 때문에 트리쉐이킹을 통해 제외하는 경우 문제가 생길 수도 있다고 판단해 웹팩은 이 코드를 트리쉐이킹 대상에서 제외시킨다.
이럴 때 package.json
파일에서 sideEffects
를 설정하여 사이드 이펙트가 생기지 않을 것이라고 웹팩에게 알려줄 수 있다. = 트리쉐이킹이 가능하도록
다음과 같이 작성하면 애플리케이션 전체에서 사이드 이펙트가 발생하지 않을 것이라고 알려준다.
{
"name": "tree-shaking",
"version": "1.0.0",
"sideEffects": false
}
혹은 아래와 같이 작성하여 특정 파일에서는 사이드 이펙트가 발생하지 않을 것임을 알려줄 수 있다
{
"name": "tree-shaking",
"version": "1.0.0",
"sideEffects": ["./src/components/NoSideEffect.js"]
}
보통 3번까지 작성하면 트리쉐이킹이 잘 작동한다.
그런데 트리쉐이킹이 적용되지 않는 라이브러리가 있다면, 해당 라이브러리가 어떤 문법을 사용하고 있는지 확인해볼 필요가 있다. 모듈에 따라서 ES5로 작성된 모듈이 있을 수도 있기 때문. ES5 문법을 사용하는 모듈을 통째로 사용하는 상황이라면 상관없지만, 일부만 사용하는 경우라면 해당 모듈을 대체할 수 있으면서 ES6를 지원하는 다른 모듈을 사용하는 것이 트리쉐이킹에 유리하다. ES6 문법을 사용하는 모듈을 사용하면 해당 모듈에서도 필요한 부분만 import 해서 사용하지 않는 코드는 빌드할 때 제외되기 때문.
Lighthouse는 사이트를 검사하여 성능 측정을 할 수 있는 도구이다.
Lighthouse는 다양한 지표를 이용하여 웹페이지의 성능 검사를 해줄 뿐만 아니라 그에 대한 개선책도 제공해준다.
Lighthouse는 구글에서 개발한 오픈소스로서 웹 페이지의 품질을 개선할 수 있는 자동화 툴이다. Lighthouse는 성능, 접근성, PWA, SEO 등을 검사하며 이를 이용해 사용자는 어떤 웹페이지든 품질 검사를 할 수 있다.
Lighthouse는 Chrome DevTools부터 CLI, 노드 모듈 등 다양한 경로를 통해 사용할 수 있다. 검사할 페이지의 url을 Lighthouse에 전달하면 Lighthouse는 해당 페이지에 대한 여러 검사를 실행한다.
그 후, 위 이미지처럼 검사 결과에 따른 리포트를 생성하고 개발자는 해당 리포트를 통해 점수가 낮은 지표에 대해 개선을 꾀할 수 있다. 또한 각각의 지표가 왜 중요한지, 어떻게 개선할 수 있는 지에 대한 레퍼런스도 리포트에서 참고할 수 있다.
크롬에서 검사하고 싶은 페이지의 url을 입력한다.
개발자 도구를 연다.
lighthouse 탭을 클릭한다.
Generate report를 클릭한다. Categories에서 특정한 지표만 선택하여 검사할 수도 있다.
대략 30-60초간 검사가 실행된다. 그 후 아래와 같이 리포트가 해당 페이지의 개발자 도구내에 생성된다.
npm install -g lighthouse
lighthouse <url>
lighthouse --help
Lighthouse 노드모듈을 이용해 동적으로 프로그래밍하여 페이지 검사 리포트를 생성할 수도 있다. 이를 이용해 성능 테스트를 자동화할 수 있다.
Performance 항목에서는 웹 성능을 측정한다. 화면에 콘텐츠가 표시되는데 시간이 얼마나 걸리는지, 표시된 후 사용자와 상호작용하기 까진 얼마나 걸리는지, 화면에 불안정한 요소는 없는지 등을 확인한다.
Accessibility 항목에서는 웹 페이지가 웹 접근성을 잘 갖추고 있는지 확인한다. 대체 텍스트를 잘 작성했는지, 배경색과 콘텐츠 색상의 대비가 충분한지, 적절한 WAI-ARIA 속성을 사용했는지 등을 확인한다.
Best Practices 항목에서는 웹 페이지가 웹 표준 모범 사례를 잘 따르고 있는지 확인한다. HTTPS 프로토콜을 사용하는지, 사용자가 확인할 확률은 높지 않지만 콘솔 창에 오류가 표시 되지는 않는지 등을 확인한다.
SEO 항목에서는 웹 페이지가 검색 엔진 최적화가 잘 되어있는지 확인한다. 애플리케이션의 robots.txt가 유효한지, <meta>
요소는 잘 작성되어 있는지, 텍스트 크기가 읽기에 무리가 없는지 등을 확인한다.
PWA 항목에서는 해당 웹 사이트가 모바일 애플리케이션으로서도 잘 작동하는지 확인한다. 앱 아이콘을 제공하는지, 스플래시 화면이 있는지, 화면 크기에 맞게 콘텐츠를 적절하게 배치했는지 등을 점수가 아닌 체크리스트로 확인한다.
Lighthouse가 생성한 리포트를 보면 위 이미지처럼 하단에 주요 메트릭이 안내되어 있는 것을 볼 수 있다. 각각의 메트릭이 어떤 것을 측정하는 지 알아보자.
First Contentful Paint, 줄여서 FCP는 성능(Performance) 지표를 추적하는 메트릭이다.
FCP는 사용자가 페이지에 접속했을 때 브라우저가 DOM 컨텐츠의 첫 번째 부분을 렌더링하는 데 걸리는 시간을 측정한다. 즉 사용자가 감지하는 페이지의 로딩속도를 측정할 수 있다. 우수한 사용자 경험을 제공하려면 FCP가 1.8초 이하여야 한다.
페이지의 이미지와 <canvas>
요소, SVG 등 모두 DOM 콘텐츠로 구분되며 <iframe>
요소의 경우 이에 포함되지 않는다.
위 타임라인에서 FCP는 첫 번째 텍스트와 이미지 요소가 화면에 렌더링되는 두 번째 프레임에서 측정된다.
이때 FCP처럼 일부 콘텐츠의 첫 번째 렌더링 시점을 측정하는 것이 아닌 주요 콘텐츠 로딩이 완료된 시점을 측정하는 것을 목표로 한다면 Large Contentful Paint, 줄여서 LCP 지표로 확인할 수 있다.
Largest Contentful Paint, 줄여서 LCP는 뷰포트를 차지하는 가장 큰 콘텐츠(이미지 또는 텍스트 블록)의 렌더 시간을 측정한다. 이를 이용해 주요 콘텐츠가 유저에게 보이는 시간까지를 가늠할 수 있다.
다음의 표를 기준으로 LCP 점수를 해석할 수 있다.
LCP time(in seconds) | Color-coding |
---|---|
0-2.5 | Green (fast) |
2.5-4 | Orange (moderate) |
Over 4 | Red (slow) |
Speed Index는 성능(Performance) 지표를 추적하는 메트릭. Speed Index는 페이지를 로드하는 동안 얼마나 빨리 컨텐츠가 시각적으로 표시되는 지를 측정한다.
Lighthouse는 먼저 브라우저의 페이지 로딩과정을 각 프레임마다 캡쳐한다. 그리고 프레임 간 화면에 보이는 요소들을 계산한다. 그 후 Speedline Node.js module을 이용하여 Speed Index 점수를 그래프의 형태로 나타낸다.
점수에 따라 다음의 기준으로 성능을 분류한다.
Speed Index(in seconds) | Color-coding |
---|---|
0–3.4 | Green (fast) |
3.4–5.8 | Orange (moderate) |
Over 5.8 | Red (slow) |
Time to interactive, 줄여서 TTI는 페이지가 로드되는 시점부터 사용자와의 상호작용이 가능한 시점까지의 시간을 측정한다.
TTI는 페이지가 완전히 상호 작용 가능하기까지의 시간을 측정한다. 그 기준은 다음과 같다.
TTI 점수는 아카이브된 HTTP 데이터를 기반으로 백분위 단위로 점수를 측정한다.
다음의 표를 기준으로 점수를 해석할 수 있다
TTI metric(in seconds) | Color-coding |
---|---|
0–3.8 | Green (fast) |
3.9–7.3 | Orange (moderate) |
Over 7.3 | Red (slow) |
대부분의 사용자는 0.05초가 넘는 작업에는 응답이 올때까지 계속 키보드를 두드리거나 마우스를 클릭하기 때문에 페이지가 느리다고 인식한다. 이를 개선하기 위한 지표가 TBT이다.
Total Blocking Time, 줄여서 TBT는 페이지가 유저와 상호작용하기까지의 막혀있는 시간을 측정한다. Lighthouse에서는 FCP와 TTI 사이에 긴 시간이 걸리는 작업들을 모두 기록하여 TBT를 측정한다.
다음의 예를 통하여 TBT의 측정 기준을 살펴보자.
위 타임라인에는 5개의 작업이 있고 그 중 50ms(0.05초)를 초과하는 3개는 긴 작업으로 간주된다.
따라서 메인스레드에서 작업을 실행하는 데 소요된 총 시간은 560ms(0.56)초이지만 TBT로 측정되는 것은 345ms(0.345초)이다.
아무런 경고 없이 텍스트가 움직이며 읽던 부분을 놓치게 되거나, 더 심한 경우 링크나 버튼을 탭하기 직전 갑작스레 링크가 움직이는 바람에 의도하지 않은 것을 클릭할 수도 있다.
대부분의 경우 이러한 경험은 짜증스러운 정도에서 그치지만, 경우에 따라 실제 피해를 겪게 될 수 있다. 이런 상황을 측정하기 위한 지표가 CLS.
Cumulative Layout Shift, 줄여서 CLS는 사용자에게 컨텐츠가 화면에서 얼마나 많이 움직이는지(불안정한 지)를 수치화한 지표이다. 이 지표를 통해 화면에서 이리저리 움직이는 요소(불안정한 요소)가 있는 지를 측정할 수 있다.
Lighthouse는 성능을 측정할 뿐 아니라 무엇이 시간을 많이 소모하는지, 어떻게 개선하여 최적화를 할 수 있을지 해결책도 제시해준다. Opportunities 항목을 확인하면 각 메트릭별 문제를 확인할 수 있다.
각 항목을 열어보면 해당 문제에 대한 상세 설명과 함께 어떤 코드, 어떤 화면에서 문제 상황을 발견했는지 확인할 수 있기 때문에 최적화의 방향을 잡기 좋다.
Lighthouse는 웹 성능 최적화 뿐만 아니라 웹 접근성, 웹 표준, SEO 관련 항목도 확인하고 해결책을 제시해준다.
1️⃣ 반응형 이미지 사용하기
각 이미지의 여러 버전을 생성한 다음 미디어 쿼리, 뷰포트 크기 등을 사용하여 HTML 또는 CSS에서 사용할 버전을 지정해주자
2️⃣ 이미지 CDN
이미지를 사용자에게 보내기전에 특정 형태로 가공(이미지 사이즈 변경 및 포맷등)을 한 이후에 사용자에게 전달하는 방식
즉, 이미지 변환을 위한 웹 서비스 API와 같은 이미지 CDN을 생각해볼 수 있다.
3️⃣ 벡터 기반 이미지 형식 사용하기
SVG 이미지는 한정된 양의 코드를 사용해 어떤 크기로든 확장할 수 있다.
JPEG 또는 PNG 형식이 아닌 WebP 또는 AVIF를 사용하여 용량을 더욱 감소시킬 수 있다.
하지만 WebP와 AVIF 모두 비교적 최근에 등장한 이미지 포맷이기 때문에 JPEG 포맷처럼 모든 브라우저에서 호환되지 않는다는 단점이 있다.
HTML의 <picture>
태그를 이용하면 각 브라우저의 호환에 맞도록 분기를 대체할 수 있다. 다음과 같이 HTML 태그를 작성할 시, 만약 접속한 브라우저에서 <source>
태그 내의 srcset에 정의한 WebP 포맷을 지원하지 않는다면 해당 <source>
태그는 무시된다. 이와 같은 속성을 이용하여 각 브라우저에 따라 이미지 포맷을 최적화할 수 있다.
<picture>
<source srcset="logo.webp" type="image/webp">
<img src="logo.png" alt="logo">
</picture>
1️⃣ Image Lazy Load
로드하는 이미지의 갯수를 줄여 이미지를 로드하는데 소요하는 시간을 줄일 수 있다.
Image Lazy Loading은 페이지 안에 있는 실제 이미지들이 실제로 화면에 보여질 필요가 있을 때 로딩을 할 수 있도록 하는 테크닉이다.
즉, 페이지 내에서 바로 필요하지 않은 이미지들을 로딩 시점을 뒤로 미루는 것이다. 페이지 로드 시점에 유저에게 보여지지 않는 이미지는 스크롤 등으로 실제로 이미지가 보여지는 시점이 올 때 로딩된다. 만약 사용자가 스크롤하는 행위가 없으면, 사용자에게 보여지지 않은 이미지는 절대 로딩되지 않는다.
HTML img 'loading' attribute
아래와 같이 <img>
엘리먼트에서 loading
속성을 사용하면, 해당 이미지에 대해 Lazy loading
을 적용할 수 있다.
<img src="image.png" loading="lazy" alt="…" width="250" height="150">
lazy: 브라우저에서 정의한 수치만큼 사용자가 특정 위치로 스크롤할 때까지 이미지의 로딩을 지연시킨다.
eager: 이미지가 뷰포트 내에 위치하는지 여부와 무관하게 이미지를 즉시 로드합니다. (default)
2️⃣ Image Compression
압축된 이미지를 제공하자.
이미지 압축은 이미지 픽셀 데이터를 압축시켜 품질을 낮출 수 있다. 그렇기 때문에 파일크기를 작게하면서, 품질이 낮은 이미지를 보여줘도 되는 경우 압축을 수행할 수 있다. 반대로 파일크기를 크게 유지하더라도 품질이 좋은 이미지 만을 보여주어야한다면 압축을 하는 것은 좋지 않을 수 있다.
그렇다고 하더라도 품질을 낮추는 것을 걱정하여 최적화 하지않는 것은 어플리케이션에 도움이 되지 않는다. 품질이 조금 낮더라도 이미지 압축 같은 최적화를 진행하게되면 상당한 바이트 절약을 유도할 수 있다.
Ref
https://velog.io/@rat8397/210826-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0-Image-Optimization