최적화(Optimization)
주어진 조건으로 최대 효율을 낼 수 있도록 하는 것.
웹 개발에서의 최적화는 주어진 조건 아래에서 최대한 빠르게 화면을 표시하도록 만드는 것이다.
최적화가 필요한 이유
1. 이탈률 감소
최적화가 잘 안되었을경우 화면을 불러오는 시간이 길어지고 사용자가 페이지를 이탈할 확률이 높아진다.
따라서 성능 최적화를 통해 페이지 로딩 속도를 줄이면 사용자의 이탈률을 효과적으로 줄일 수 있다.
2. 전환율 증가
이탈률이 줄어들면 방문자가 회원가입, 상품 구매, 게시글 조회 등의 행위를 하며 실제 서비스 이용자로 전환될 확률이 늘어난다. 전환율을 높이기 위해서는 최적화를 통해 이탈률을 줄여야한다.
3. 수익 증대
이탈률이 감소하고 전환율이 증가하면 트래픽 증대 및 회원 수의 증가로 이어지고, 수익 또한 증대된다.
4. 사용자 경험(UX) 향상
만약 로딩이 오래걸릴 경우 스피너, 프로그레스 바, 스켈레톤과 같은 로딩중임을 알리는 UI를 먼저 표시해 방문자가 조금 더 인내심을 갖고 기다리게 할 수있다. 하지만 가장 좋은 방법은 최적화를 통해 페이지 로드 속도 자체를 최대한 빠르게 하여 UX를 개선하는 것이다. 따라서 웹사이트 성능을 최적화하여 이탈률을 감소시키고 UX를 향상시켜야한다.
최적화 기법
HTML, CSS 최적화
화면 렌더링시 HTML은 DOM 트리를, CSS는 CSSOM 트리를 만들고 두 트리를 결합해 사용하는데, 두 트리중 하나라도 변경되면 리렌더링을 유발하게 된다. 트리의 크기가 크고 복잡할 수록 더 많은 계산이 필요하고 리렌더링에 소모되는 시간이 늘어나기때문에 HTML, CSS 코드를 최적화하면 렌더링 성능을 향상시킬 수 있다.
HTML 최적화
1. DOM 트리 가볍게 만들기
- 깊이가 깊고 자식요소가 많을수록 DOM 트리의 복잡도가 커지기 때문.
- 불필요하게 깊이를 증가시키는 요소들을 삭제한다.
// 수정 전
<div>
<ol>
<li> 첫 번째 </li>
<li> 두 번째 </li>
<li> 세 번째 </li>
</ol>
</div>
// 수정 후 : 불필요한 div 요소 제거
<ol>
<li> 첫 번째 </li>
<li> 두 번째 </li>
<li> 세 번째 </li>
</ol>
2. 인라인 스타일 사용 지양
- 개별 요소에 스타일 속성을 작성하기 때문에 클래스로 묶을 수 있는 것도 중복으로 작성하게 됨
- 불필요한 코드 중복으로 인해 가독성이 떨어지고 파일 크기가 증가함
- 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 최적화
1. 사용하지 않는 CSS 제거
- CSS 파일의 모든 코드 분석이 끝나고 CSSOM트리가 생성됨으로 불필요한 코드가 있을경우 CSSOM 트리의 완성이 늦어짐
- 요소를 삭제할 일이 생기면 CSS코드만 남지는 않았는지 확인하고 함께 삭제하는것이 좋음
2. 간결한 셀렉터 사용
- 셀렉터가 복잡할수록 스타일 계산과 레이아웃에 더 많은 시간 소모
/* 복잡한 CSS 셀렉터 예시 */
.cart_page .cart_item #firstItem { ... }
/* 필요하다면 어쩔수 없지만 가능한 간결하게 작성한다 */
.cart_item { ... }
리소스 로딩 최적화
1. CSS 파일 불러오는 위치는 문서 최상단
- 렌더링할때는 DOM트리, CSSOM 트리가 필요함
- DOM트리는 HTML 코드를 한줄씩 읽으면서 순차적으로 구성
- CSSOM트리는 CSS코드를 모두 해석해야 구성할 수 있음
=> 빠르게 구성해야함으로 HTML 문서 최상단에 배치해 불러오도록 함
<!-- HTML 파일 상단의 head 요소 안에서 불러오기 -->
<head>
<link href="style.css" rel="stylesheet" />
</head>
2. JavaScript 파일 불러오는 위치는 문서 최하단
- HTML 코드 파싱중 <script> 요소를 만나면
- 스크립트 요소가 실행되고 이 이전까지 생성된 DOM까지만 접근할 수 있음
- 만약 해당 요소 이후 생성될 DOM을 수정하는 코드가 있다면 의도한 대로 화면이 표시되지 않음
- 스크립트 실행이 완료되기 전까지 DOM 트리 생성이 중단됨 = >렌더링 완료시간 늦어짐
- => JavaScript 파일은 DOM트리 생성이 완료되는 시점인 HTML 문서 최하단에 배치
<body>
<div>...</div>
...
<!-- body 요소가 닫히기 직전에 작성 -->
<script src="script.js" type="text/javascript"></script>
</body>
브라우저 이미지 최적화
페이지 대부분의 용량은 이미지 파일과 같은 미디어 파일이 차지한다. 그래서 이미지의 용량을 줄이거나 요청의 수를 줄이면 사용자 경험을 빠르게 개선할 수 있다.
1. 이미지 스프라이트
- 여러개의 이미지를 모아 하나의 스프라이트 이미지로 만들어 CSS의 background-position 속성을 사용해 일정 부분만 클래스 등으로 구분하여 사용하는 방법
- 하나의 이미지를 배경 이미지로 사용하고 원하는 부분에 맞춰 width, height, background-position 속성을 주어 사용한다.
- 한번의 이미지 요청으로 대부분의 개별 이미지를 사용할 수있기때문에 네트워크 로딩 시간이 줄어듬
- 많은 이미지 파일을 개별로 관리하는 것이 아니라 특정 스프라이트 이미지 파일만을 관리하면 되기 때문에 관리가 용이함
2. 아이콘 폰트 사용
- 아이콘 사용이 많을 경우 이미지가 아닌 아이콘 폰트를 사용해 용량을 줄일 수있음
- Font Awesome 등의 서비스를 사용할 수 있음
1) Font Awesome 사용법 - CDN
Font Awesome에 가입하고 발급받은 키트를 <head> 요소에 넣어준다.
그리고 사이트에서 원하는 아이콘을 찾아 사용할 환경(HTML, React, Vue)에 맞는 코드를 복사해 붙여넣으면 된다.
2) Font Awesome 사용법 - Font Awesome 모듈
라이브러리처럼 설치해서 사용할 수 있다. 리액트의 경우 아래의 패키지들을 설치할 수 있다.
// 핵심 패키지 설치
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
사용하고싶은 아이콘을 찾아 불러와서 사용하면 된다. 이때 아이콘 이름은 camelCase로 작성한다.
3. WebP / AVIF 이미지 포맷 사용
- JPEG, PNG가 아닌 새로운 이미지 포맷 WebP / AVIF 사용하여 용량을 감소시킬 수 있음
- WebP : PNG에 비해 용량 26% 감소, JPEG와 비교했을때는 25-35% 더 감소함
- AVIF : JPEG에 비해 용량의 50% 감소, WebP와 비교시 20% 감소
- WebP, AVIF 모두 최근에 등장한 이미지 포맷이기때문에 일부 브라우저에서는 호환되지 않음
- WebP : 구버전의 브라우저에서는 지원하지 않을 수 있음. Safari에서는 지원 안함 (Can I use WebP? 참고)
- AVIF : Chrome, Opera 등 소수의 브라우저에서만 지원 (Can I use AVIF? 참고)
- HTML의 <picture> 태그를 이용해 브라우저의 호환에 맞도록 분기해줄 수 있음
- <picture> : img 요소의 다중 이미지 리소스(multiple image resources)를 위한 컨테이너를 정의할 때 사용
- 아래 코드의 경우, 접속한 브라우저에서 <source> 태그 내의 webp 파일을 지원하지 않는다면 해당 태그를 무시하고 아래의 <img> 태그의 png 파일을 읽는다.
<picture>
<source srcset="logo.webp" type="image/webp">
<img src="logo.png" alt="logo">
</picture>
CDN 사용하기
- 콘텐츠를 더 빠르고 효율적으로 제공하기 위해 설계됨
- 유저와 호스팅 서버와의 거리가 멀다면 네트워크 지연(latency)이 발생함
- CDN은 유저와 가까운 곳에 위치한 데이터 센터(서버)의 데이터를 가져옴 => 로딩속도 빨라짐
- CloudFront, Cloudflare 등의 CDN 서비스를 이용할 수 있음
캐시 관리
캐시(Cache) : 다운로드 받은 데이터, 값 등을 미리 복사해 놓는 임시 저장소. 데이터에 접근하는 시간이 오래 걸리는 경우, 또는 값을 다시 계산하는 시간을 절약하고 싶은 경우에 사용
캐시를 사용해야 하는 이유
- 캐시를 사용하지 않을 경우
- 특정 파일을 받아오는 요청시 첫번째 요청에서는 해당 파일을 받은 적이 없기 때문에 파일을 통째로 받아온다.
- 두번째 요청에서도 같은 파일을 또 다시 받아온다. 여러번 요청해도 마찬가지다.
- => 캐시를 사용하지 않는다면 같은 동작을 계속 수행하느라 네트워크 리소스를 낭비하게 된다.
- 캐시를 사용할 경우
- 특정 파일을 받아오는 요청시 첫번째 요청에서 이미지와 함께 헤더에 Cache-Control : max-age = 60를 작성해서 보내준다. 이것은 해당 파일이 60초 동안 유효하다는 의미이다.
- 두번째 요청부터 캐시를 우선적으로 조회한다. 캐시에 데이터가 존재하고, 아직 유효기간이 지나지 않았다면 다시 파일을 받아오는 것이 아니라 캐시에서 해당하는 데이터를 가져와서 사용하게 된다.
- 만약 유효기간이 지났을 경우에는 서버에서 다시 이미지를 받아온다.
=> 캐시 유효기간 동안은 똑같은 데이터를 다시 받아오지 않는다.
- 캐시 사용의 효과
- 캐시가 유효한 시간동안 네트워크 리소스를 아낄 수 있음
- 파일을 다시 받아올 필요가 없으므로 브라우저 로딩이 빨라짐
- 로딩이 빨라지고, 빠른 사용자 경험을 제공 가능
캐시 검증 헤더와 조건부 요청
- 캐시 검증 헤더
- 캐시에 저장된 데이터와 서버의 데이터가 동일한지 확인하기 위한 정보를 담은 응답 헤더
- Last-Modified : 데이터가 마지막으로 수정된 시점을 의미하는 응답 헤더. 조건부 요청헤더 If-Modified-Since와 함께 사용
- Etag : 데이터의 버전을 의미하는 응답 헤더. 조건부 요청헤더 If-None-Match와 묶어서 사용
- 조건부 요청 헤더
- 캐시의 데이터와 서버의 데이터가 동일하다면 재사용하게 해달라는 의미의 요청 헤더
- If-Modified-Since : 캐시된 리소스의 Last-Modified 값 이후에 서버 리소스가 수정되었는지 확인하고 수정되지 않았다면 캐시된 리소스를 사용
- If-None-Match : 캐시된 리소스의 Etag값과 현재 서버리소스의 Etag 값이 같은지 확인하고 같다면 캐시된 리소스 사용
사용방법
- Last-Midified & If-Modified-Since
- 첫번째 요청을 보내고 응답을 받을때 파일과 함께 서버의 파일이 마지막으로 수정된 시간이 담긴 Last-Modified가 캐시에 함께 저장됨
- 캐시 유효기간 이후 두번째 요청을 보낼때, 유효시간이 지났어도 해당 데이터를 재사용해도 되는지 확인하기 위해 요청 헤더 If-Modified-Since에 캐시에 함께 저장했던 Last-Modified 값을 담아 요청을 보낸다.
이 값을 이용해 서버 데이터의 최종수정일과 캐시에 저장된 데이터의 수정일을 비교하는데, 두 데이터가 동일하다면 최종 수정일이 같다. - 서버와 캐시의 데이터가 동일한 데이터임이 검증되었다면 서버는 304 Not Modified(데이터가 수정되지 않았음)를 응답으로 보내주고, 캐시 데이터의 유효 시간이 갱신되면서 해당 데이터를 재사용할 수 있다.
- Etag & If-None-Match
- 첫번째 요청을 보내고 응답을 받을때 파일과 함께 서버의 파일 버전이 담긴 Etag 헤더도 캐시에 함께 저장됨
- 캐시 유효기간 이후 두번재 요청을 보낼때, 유효시간이 지났어도 해당 데이터를 재사용해도 되는지 확인하기 위해 요청 헤더 If-None-Match에 캐시에 함께 저장했던 Etag 값을 담아 요청을 보낸다.
이 값을 이용해 서버데이터의 Etag와 캐시에 저장된 Etag를 비교한다. 두 데이터가 동일한 데이터라면 두개의 Etag 값이 같다. - 서버와 캐시의 데이터가 동일한 데이터임이 검증되었다면 서버는 304 Not Modified(데이터가 수정되지 않았음)를 응답으로 보내주고, 캐시 데이터의 유효 시간이 갱신되면서 해당 데이터를 재사용할 수 있다.
두 쌍중의 한 쌍만 사용할 수도 있지만, 보통 두 쌍을 동시에 사용한다. 둘 중 하나만 사용할 경우, 매칭되는 응답 헤더가 없을때 재사용할 수 있는 경우에도 리소스를 다시 받아오는 일이 발생할 수도 있기 때문이다.
Tree Shaking
나무를 흔들어 잔가지를 털어내듯 불필요한 코드를 제거하는것.
JavaScript를 트리쉐이킹 해야하는 이유
- 파일의 크기
- 웹사이트에서 인터렉션이 많아지고 JavaScript의 비중이 높아지면서 파일의 크기가 커졌다.
- JavaScript 파일을 요청하는 HTTP 요청 수 또한 증가했다. => 네트워크 리소스 소모가 커짐
- 파일 크기의 증가 + 요청 횟수의 증가 = 파일이 오가는동안 화면표시가 늦어짐
- 파일의 실행시간
- JavaScript 파일의 실행 과정
- 요청을 보내 파일 다운로드 → 압축 해제 → 코드 파싱하여 DOM 트리 생성 → 컴퓨터가 이해할 수 있게 컴파일 → 코드 실행
- 거쳐야 하는 과정이 많아서 JavaScript는 다른 리소스에 비해 실행까지 상대적으로 많은 시간을 소모한다.
- 파일의 크기가 커진만큼 실행시간 또한 증가했음
- 실행시간은 CPU에 크게 영향을 받고, 모바일 환경에서 차이가 뚜렷하게 드러남 (휴대폰 사양에 따라 소모시간의 차이가 있음)
- 실생시간이 길어지면 사용자의 이탈률이 늘어남 => 최적화 필요
- JavaScript 파일의 실행 과정
웹팩 사용환경에서의 JavaScript 트리쉐이킹
웹팩 4버전 이상을 사용하는 경우 ES6모듈(import / export 사용하는 모듈) 대상으로 기본적인 트리쉐이킹이 제공됨.
Create React App으로 만든 React 애플리케이션도 웹팩을 사용함으로 트리쉐이킹이 가능함
1. 필요한 모듈만 import 하기
- 모든 코드를 불러오게 될 경우 실제로 사용하는 코드가 아니어도 번들링시 모든 코드를 빌드하기때문에 불필요한 코드들까지 빌드하게 됨
- 필요한 모듈만 불러오면 번들링 과정에서 사용하는 부분의 코드만 포함시키므로 코드의 크기를 줄일 수 있고 트리쉐이킹이 가능해짐
2. Babelrc 파일 설정
Babel - 자바스크립트 문법을 구형 브라우저에서도 호환 가능하도록 ES5 문법으로 변환해주는 라이브러리
- ES5는 import / export를 지원하지 않기 때문에 commonJS 문법의 require로 변경시키는데, require는 export 되는 모든 모듈을 불러오기때문에 1번에서처럼 필요한 모듈만 불러오기 위한 코드를 작성해도 소용이 없음
- => Barbelrc 파일에 다음 코드 작성하여 ES5 변환을 막음
{
“presets”: [
[
“@babel/preset-env”,
{
"modules": false
// modules : true 인 경우 항상 ES5 문법으로 변환하므로 주의할것
}
]
]
}
3. sideEffects 설정
웹팩은 side Effect를 일으킬 수 있는 코드의 경우, 사용하지 않는 코드라도 트리쉐이킹 대상에서 제외시킨다.
이때 package.json 파일에서 sideEffects를 설정하여 코드를 제외시켜도 된다는 것을 설정해줄 수 있다.
// 애플리케이션 전체에서 사이드 이펙트가 발생하지 않음
{
"name": "tree-shaking",
"version": "1.0.0",
"sideEffects": false
}
// 특정파일(예시는 NoSideEffect.js)에서는 사이드 이펙트가 발생하지 않음
{
"name": "tree-shaking",
"version": "1.0.0",
"sideEffects": ["./src/components/NoSideEffect.js"]
}
4. ES6 문법을 사용하는 모듈 사용
3까지 설정해줄경우 트리쉐이킹이 잘 작동하지만, 적용되지 않는 라이브러리가 있을 경우 해당 라이브러리의 문법이 ES5로 작성되었는지 확인해봐야한다. ES5 문법을 사용하는 모듈을 통째로 사용하고 있다면 상관 없지만, 일부만 사용하는 경우 대체가능한 ES6 문법을 사용하는 다른 모듈을 쓰는것이 트리쉐이킹에 유리하다. ES6 문법을 쓰는 모듈을 사용하면 해당 모듈에서도 필요한 부분만 import해서 사용하지 않는 코드는 빌드할때 제외되기 때문이다.
Lighthouse
웹페이지의 품질을 개선할 수 있는 자동화 툴. 구글에서 개발한 오픈소스이고, 사이트를 검사해 성능을 측정하고 그에 대한 개선책도 제공해준다.
검사할 페이지의 URL을 Lighthouse에 전달하면 해당 페이지에 대해 여러 검사를 실행하고 검사 결과에 따른 리포트 생성 및 레퍼런스를 제공한다.
Chrome 개발자 도구에서 Lighthouse 사용하기
1. 검사할 페이지의 url 입력
2. 개발자도구 열기
3. Lighthouse 탭 클릭
4. Generate report 클릭. Categories에서 원하는 부분을 선택할수도 있음
5. 검사가 실행되고 나면 해당 페이지의 개발자 도구 내에 리포트가 생성됨
Node CLI에서 Lighthouse 사용하기
1. Lighthouse 설치. -g 옵션을 사용해 전역모듈로 설치하는 것이 좋음
npm install -g lighthouse
2. 검사 실행
lighthouse <url>
3. 옵션보기
lighthouse --help
Lighthouse 노드 모듈을 이용해 동적으로 프로그래밍하여 페이지 검사 리포트를 생성할 수도 있다.
https://github.com/GoogleChrome/lighthouse/blob/main/docs/readme.md#using-programmatically
GitHub - GoogleChrome/lighthouse: Automated auditing, performance metrics, and best practices for the web.
Automated auditing, performance metrics, and best practices for the web. - GitHub - GoogleChrome/lighthouse: Automated auditing, performance metrics, and best practices for the web.
github.com
Lighthouse 분석 결과 항목
- Performance
- 웹 성능 측정
- 화면에 콘텐츠가 표시되는데 시간이 얼마나 걸리는지, 표시되고 사용자와 상호작용하기까지 얼마나 걸리는지, 화변에 불안정한 요소는 없는지...
- Accessibility
- 웹 접근성 측정
- 대체 텍스트 작성, 배경색과 콘텐츠 색상의 대비, 적절한 WAI-ARIA 속성...
- Best Practices
- 웹 표준 모범 사례를 잘 따르고 있는지 측정
- HTTPS 프로토콜 사용, 콘솔창에 오류가 표시되는지...
- SEO
- 검색엔진 최적화 확인
- 애플리케이션의 robots.txt가 유효한지, <meta> 요소가 잘 작성되어 있는지, 텍스트 크기가 읽기에 무리가 없는지...
- PWA(Progressive Web App)
- 웹사이트가 모바일 애플리케이션으로서 잘 작동하는지
- 앱 아이콘 제공여부, 스플래시 화면 유무, 화면 크기에 맞게 콘텐츠가 적절하게 배치되었는지 등을 체크리스트로 확인
Lighthouse Performance 측정 메트릭
- First Contentful Paint(FCP)
- 성능(Performance) 지표를 추적하는 메트릭
- 사용자가 페이지에 접속했을때 브라우저가 DOM 컨텐츠의 첫번째 부분을 렌더링하는데 걸리는 시간을 측정
- = 사용자가 감지하는 페이지의 로딩 속도를 측정
- 우수한 사용자 경험을 제공하려면 FCP 1.8초 이하여야함
- 이미지, <canvas>, SVG 등 모두 DOM 콘텐츠로 구분되나 <iframe>의 경우 포함되지 않음
- Largest Contentful Paint(LCP)
- 뷰포트를 차지하는 가장 큰 콘텐츠(이미지 또는 텍스트 블록)의 렌더 시간 측정
- 콘텐츠가 유저에게 보이는 시간을 가늠
- 기준(초)
- 0 - 0.25 : Green(fast)
- 2.5-4 : Orange(moderate)
- Over 4 : Red(slow)
- Speed Index
- 성능(Performance) 지표를 추적하는 메트릭
- 페이지를 로드한 동안 얼마나 빨리 컨텐츠가 시각적으로 표시되는지를 측정
- Lighthouse가 브라우저의 페이지 로딩과정을 각 프레임마다 캡쳐하고, 프레임간 화면에 보이는 요소들을 계산함.
그리고 Speedline Node.js module을 이용하여 Speed Index 점수를 그래프의 형태로 나타냄 - 기준(초)
- 0-3.4 : Green(fast)
- 3.4-5.8 : Orange(moderate)
- Over 5.8 : Red(slow)
- Time to interactive(TTI)
- 페이지가 로드되는 시점부터 사용자와의 상호작용이 가능한 시점까지의 시간
- 측정 기준
- 페이지에 FCP로 측정된 컨텐츠가 표시되어야 함
- 이벤트 핸들러가 가장 잘 보이는 페이지의 엘리먼트에 등록되어야 함
- 페이지가 0.05초 안에 사용자의 상호작용에 응답함
- 아카이브된 HTTP 데이터를 기반으로 백분위 단위로 점수를 측정
- 기준(초)
- 0-3.8 : Green(fast)
- 3.9-7.3 : Orange(moderate)
- Over 7.3 : Red(slow)
- Total Blocking Time(TBT)
- 페이지가 유저와 상호작용하기까지 막혀있는 시간을 측정
- FCP와 TTI 사이의 긴 시간이 걸리는 작업들을 모두 기록하여 TBT를 측정함
- Cumulative Layout Shift(CLS)
- 사용자에게 컨텐츠가 화면에서 얼마나 많이 움직이는지(불안정한지)를 수치화한 지표
- https://www.youtube.com/watch?v=zIJuY-JCjqw
Lighthouse의 Oppertunities
성능을 측정한 후 Oppertunities 항목에서 각 메트릭별 문제를 확인할 수 있다.
항목 내에는 해당 문제에 대한 상세 설명과 함께 어떤 코드, 어떤 화면에서 문제 상황을 발견했는지 확인 할 수 있으므로 개선방향을 더 잘 잡을 수 있다.