6월 22일(목) Devocean에서 진행한 "웹 프론트엔드 성능 최적화 방법 및 적용 사례" 세미나의 발표 내용을 기반으로 작성된 글입니다.
오늘 포스팅에서는 크게 두 목차를 다루겠습니다.
- 왜 프론트엔드 성능이 중요한가?
- 어떻게 프론트엔드 성능을 개선하는가?
왜 프론트엔드 성능이 중요한가?
속도가 느리면 유저는 이탈하기 마련
누구라도 긴 로딩이 꺼려지는 법 입니다. 실제로 한국의 경우 로딩이 2초만 걸려도 사람들이 이탈하죠. 실제로 Tokopedia라는 서비스가 로딩 시간을 3.78s에서 1.72s로 개선하니, 유저 평균 체류 시간이 15%, 세션 유지 시간은 23%나 늘었습니다. 그래서 서비스 속도를 빠르게 하는 것이, 유저 이탈을 막는 지름길이라 말할 수 있습니다.
그렇다면, 속도의 정의가 무엇이지?
앞서, 서비스 속도를 빠르게 하는 것이, 유저 이탈을 막는 지름길이라 말씀 드렸습니다. 그렇다면 과연 서비스 속도가 무엇일까요? 이를 나타내는 지표가 있으면 편할 것 같습니다.
이전에는 Page 수, Network Request 등 여러 속도 지표가 있었습니다. 하지만 이 지표들은 개발자의 관점에서 만들어진 표죠. 사용자가 사이트를 방문할 때 개발자 도구를 보고 Network Request 수를 일일히 확인하지는 않죠.
CWV(Core Web Vitals)
그래서 Google에서 CWV(Core Web Vitals)라는 재밌는 지표를 정의했고, 현재까지 널리 사용되고 있습니다. 이 지표가 널리 사용되는 이유는 Google이 만들어서일 수도 있지만, 이 지표가 사용자 관점을 잘 대변해주기 때문이예요.
CWV에는 크게 세 가지 지표가 있습니다.
- LCP(Largest Contentful Paint)
사용자가 사이트 화면을 봤을 때, 가장 큰 컨텐츠가 얼마나 빨리 로드되는지?
(대부분 LCP 컨텐츠는 이미지, 동영상 등의 미디어 컨텐츠입니다) - FID(First Input Delay)
초기 화면의 첫 번째 Input을 클릭했을 때. 사이트가 얼마나 빨리 반응하는지? - CLS(Cumulative Layout Shift)
얼마나 사이트 내용이 안정적인지?
(광고가 사이트 내용을 가린다던지, 갑자기 내용의 위치가 바뀌어 버린다던지 하는 경우)
보시면 아시겠지만, 모두 사용자 입장에서 바라본 지표이죠. 이 지표가 높을수록 사용자는 사이트에서 좋은 경험을 얻을 가능성이 매우 높습니다. 그런데 FID는 맨 처음 Interaction만 나타내는 지표라는 지적이 많이 있습니다. 그래서 모든 Input에 대한 평균 응답 시간을 나타내는 INP(Interaction to Next Paint)로 대체될 예정입니다.
이처럼 CWV는 오직 사용자를 대변해 사이트를 바라보며, 지속적으로 기준을 수정 및 개선하고 있습니다. 그래서 CWV는 많은 관심과 사람을 받으며, 현재까지도 많이 사용되고 있죠.
CWV가 중요한 이유
이제 CWV는 SEO(Search Engine Optimization)에도 큰 영향을 끼칠 것입니다. Google Search의 발표에 따르면, 검색 엔진이 기존 HTTPS, 모바일 접근성 등과 더불어, 이 CWV도 평가에 고려하겠다고 합니다. 즉, 사용자 경험이 좋은 사이트를 우선적으로 보여주겠다는 것이죠.
대부분의 웹 서비스들은 SEO가 서비스 유입에 매우 중요한데, 왜 CWV 지표를 개선해야 하는지 분명이 보여주고 있네요.
그렇다면, 속도는 어떻게 측정할까?
앞서 CWV에 대해 간단하게 살펴 봤습니다. 그렇다면 어떻게 CWV 지표를 확인할 수 있을까요? 다행히 Google이나 여러 Third-Party 서비스들이 존재합니다. 서비스를 알아보기 전에, 간단하게 LAB Data와 RUM Data에 대해 알아봅시다.
- LAB Data: 사이트 개발자가 Tool을 활용해 시뮬레이션해서 얻은 데이터
- RUM Data: Real-User-Monitoring이란 뜻으로, 실제 사용자의 여러 지표를 수집해 측정한 데이터
- PageSpeed Insights
RUM Data와 Lab Data를 둘 다 제공합니다. - Chorme UX Report
Chrome 사용자들의 CWV를 집계해 Google DB에 저장한 뒤, 이를 기반으로 RUM Data를 보여줍니다.
특별한 점은 Origin Level 별로 Data를 보여준다는 점 입니다.
(map.naver.com, blog.naver.com 등을 모두 naver.com으로 간주합니다.) - Lighthouse
Github 오픈소스로 제공하는 Tool로, LAB 데이터를 보여줍니다.
특별한 점은 on-premise로 Network 없이 측정할 수 있어요. - Web-Vitals Extension
Chorme Extension 입니다. 켜두면 사이트를 이동할 때마다 해당 사이트의 CWV를 보여줍니다.
추가로, 발표자 분께서 좋은 Tool을 소개해 주셨습니다. Google Engineer의 선택이니 믿음이 가네요 :)
- WebPageTest.org
third-party 분석 Tool 입니다. 신뢰도도 높고, 설정할 수 있는 항목이 많습니다. PageSpeed Insights와 병행해 활용하면 좋습니다. - Web-Vitals.js
위 Extenstion과 다르게, 사이트에 설치하는 JS 라이브러리입니다. 대부분의 Tool은 Chrome 사용자만 집계해 RUM Data를 제공하는데, 이것은 여러 Browser 사용자들의 Data를 집계할 수 있어 RUM Data 수집에 좋습니다.
어떻게 프론트엔드 성능을 개선하는가?
LCP 최적화
LCP 자원, 즉 페이지에서 가장 큰 영역을 차지하는 컨텐츠가 HTML 문서에서 빨리 찾아져야 합니다. 아래는 이를 위한 테크닉들입니다.
- 미디어 Source를 꼭 HTML src 태그에 추가해 사용합시다.
- 브라우저가 사이트를 렌더링할 때, CSS보다 HTML 파일을 먼저 읽습니다. 그러므로 CSS 속성 보다, HTML 태그에 직접 명시하는 것이 좋습니다.
- LCP를 먼저 다운받을 수 있도록 우선 순위를 설정합시다.
- LCP 컨텐츠에 fetchpriorty="high" 속성을, LCP가 아닌 컨텐츠에는 loading=”lazy” 속성을 추가해 LCP 컨텐츠를 먼저 Web Server에서 가져오도록 합니다.
- LCP 컨텐츠가 외부 서버에서 제공되는 경우, HTML Head에 <link rel=”preload”>을 추가합니다.
- CDN을 사용해 먼 거리에서도 빨리 다운받을 수 있도록 합니다.
- 외국에서 접속하는 경우, Web Server와의 거리가 멀어 가져오는데 시간이 걸릴 수밖에 없습니다.
- CDN은 전 세계 곳곳에 Edge Server를 둬 Content를 제공하므로, 외국에서 접속하는 유저의 전송 속도를 비약적으로 올릴 수 있습니다.
대부분의 웹 사이트는 이 방식으로 LCP 지표를 개선합니다. 물론 SSR(Server-Side-Rendering)인 경우, Client에서 할 작업을 Server에서 처리하는 경우도 있고, 비동기 프로그래밍을 활용해 추가로 개선할 수 있습니다.
CLS 최적화
위 사진의 Before 부분을 봐 주세요. Promo Banner와 Image가 로드되면서 내용이 밑으로 밀리고 있습니다. 이것 또한 CLS 감점 요소입니다. 이런 경우가 없도록, After처럼 Content의 영역을 미리 확보해놔야 합니다.
- 컨텐츠의 높이를 먼저 확보합시다.
- 정적 크기를 가진 요소의 경우, 고정 height 값을 설정하면 됩니다.
- 반응형 Layout 등, 동적 크기를 가진 요소는 aspect-ratio을 사용합시다. 요소의 가로, 세로 비율을 기반으로 브라우저가 알아서 맞춰 줍니다.
- 과도한 애니메이션을 지양합시다. (특히 Layout이 흔들리거나, Blur 효과를 주는 등)
- 캐시를 활용합시다.
- 브라우저는 BFCache(Back/Forward Cache)가 있습니다. 사이트 내 페이지를 이동할 떄, Network Request를 보내는 대신, Cache에서 요소를 찾아 보여줍니다.
- 그런데 간혹 사이트나 서버에서 BFCahce를 막는 경우가 있으니, 잘 확인할 필요가 있습니다/
복잡한 반응형 Layout을 가진 사이트 등, 아무리 고민해도 방법이 없으면 min-height 속성을 써 봅시다. 요소의 최소 사이즈라도 미리 영역을 확보한다면, 그나마 컨텐츠가 덜 밀리겠죠.
FID 최적화
가장 어렵습니다. 곧 모든 Input의 반응 시간을 반영하는 INP 지표로 대체되기 때문에 모든 Interaction에 대해 깊게 고민해봐야 하죠.
가장 중요한 점은 긴 Task를 짧은 단위로 쪼개야 한다는 점 입니다.
위 사진의 Before와 After를 봐 주세요. Before는 다른 Interaction을 수행하려면 긴 Task가 끝나야만 실행 가능합니다. 반면 After는 Task 중간마다 Term이 있어 다른 Interaction이 실행될 수 있죠. 반응 시간이 훨씬 빨라질 것 입니다.
또한 Javascript는 대표적인 싱글쓰레드 언어로, 멀티쓰레딩(Multi-Threading)으로 Task를 병렬로도 수행할 수 없습니다. 최선의 방법은 Code Splitting이나 Webpack Bundling을 활용해 긴 Script를 작은 단위로 분할하고, 공통 Logic을 파악하는 등 불필요한 Script의 사용을 줄일 수밖에 없습니다.
- 지속적으로 분석 Tool을 보면서 개발합시다.
- Chorme DevTool 등을 보면서, 얼마나 Script를 사용하는지, 메모리 사용량은 몇인지 보면서 개발합시다.
- 큰 Rendering Update를 지양합시다.
- 예를 들어, Infinity Scroll은 전체 페이지 Update를 야기합니다. 보기에는 깔끔하고 좋지만, CWV에 매우 좋지 않습니다.
- 또 다른 예로, 애니메이션을 위한 reqeustAnimationFrame() 함수가 있겠습니다. 1초에 60번씩 동작하면서, 전체 페이지를 Update해 큰 성능 하락을 야기합니다.
- 꼭 필요한 데이터만 수집합시다.
- 마케팅을 위한 각 데이터들(페이지 이동, 결제 등) 중, 꼭 필요한 데이터만 수집합시다. 데이터를 수집하는 것도 Overhead를 야기하며, 사이트 속도를 느려지게 하는 큰 요소입니다.
- DOM Size를 작게 유지합시다.
- 브라우저는 Dom-Tree를 그리고, HTML 요소를 불러옵니다. DOM Size가 크면 HTML 요소를 가져오는데 시간을 다 뻇겨서, Interaction을 위한 시간이 줄어듭니다.