최적화(Optimization)
최적화, 最適化, optimization주어진 상황에서 원하는 가장 알맞은 결과를 얻을 수 있도록 처리하는 과정. 최적화는 허용된 자원의 한계 내에서 주어진 요구사항을 만족시키면서 최선의 결과를 얻는 과정이다. 수익과 관련되는 분야에서는 이익을 최대로 내는 과정을 말하기도 한다. 다양한 분야와 때에 따라 다르게 정의할 수 있고 물류(logistics), 설계(design) 문제 등에 응용된다. - 한국정보통신기술협회 정보통신 용어사전
분야에 따라 의미가 조금씩 다르지만 최적화는 보통 주어진 조건으로 최대 효율을 내는 것을 의미한다. 컴퓨터 공학에서의 최적화는 가능한 적은 리소스를 소모하면서 가능한 빠르게 원하는 결과를 얻을 수 있도록 하는 것을 의미한다. 원하는 결과가 나온다면 메모리를 조금이라도 덜 소모하거나 연산 횟수가 한 번이라도 더 적은 코드가 더 효율적이고 최적화된 코드이며, 웹 개발에서의 최적화는 주어진 조건 아래에서 최대한 빠르게 화면을 표시하도록 만드는 것이다.
최적화의 필요성과 효과
- 이탈률 감소
웹 개발에서의 최적화는 화면을 최대한 빠른 속도로 표시하는 것이고, 최적화가 잘 되지 않은 웹페이지는 화면 로딩에 시간이 걸린다는 뜻으로 볼 수 있다. 화면을 불러오는 시간이 길어지만 사용자가 페이지를 이탈할 확률이 높아지기 때문에, 웹 사이트의 성능 최적화를 통해 페이지 로딩 속도를 줄이면 사용자의 이탈률을 효과적으로 줄일 수 있다.
구글의 조사 결과에 따르면 페이지 로드가 3초 이상 걸리면 53%의 사용자가 사이트를 이탈한다. 또한 페이지 로드 시간이 길어지면 사이트 방문자가 이탈률이 폭발적으로 증가하는데, 페이지 로드 속도가 1초에서 3초로 늘어나면 이탈률은 32%, 5초로 늘어나면 90%, 6초로 늘어나면 106%, 10초로 늘어나면 123%까지 증가하는 것으로 나타났다.
- 전환율 증가
전환율이란 웹 사이트를 방문한 사용자 중 회원가입, 상품 구매, 게시글 조회, 다운로드 등의 행위를 한 방문자의 비율을 의미한다. 이탈률이 줄어들면 전환율이 높아질 확률이 커진다. 이탈해버린 사용자의 전환율은 0%이기 때문이다.
- 수익 증대
이탈율 감소, 전환율 증가는 트래픽 증대 및 회원 수 증가로 이어지고 이는 수익 증대를 의미한다. 로딩 속도의 차이는 유의미한 수익 차이를 만들 수 있다.
실제로 로딩 속도가 1초 빨라졌을 때 아마존 판매량은 1%, 구글 검색량은 0.2%, 월마트의 전환율은 2% 증가했다. 퍼센티지로 보면 크지 않아 보이지만, 이 수치를 돈으로 환산하면 각각 68억 달러, 4억 5천만 달러, 2억 4천400만 달러의 매출 증가라고 한다.
- 사용자 경험(UX) 향상
최적화는 효과적인 UX 개선 수단이다. 페이지 로딩이 빠를 수록 사용자 경험은 향상되기 때문에 페이지 로드 속도가 빠른 편이라고 해도 최적화를 통해 UX의 추가적인 향상을 기대할 수 있다. 로딩에 시간이 걸린다면 로딩중임을 표시하는 인디케이터를 표시할 수도 있지만, 페이지 로드 속도 자체를 개선하는 것 보다는 UX에 좋다고 할 수 없다.
Optimization 기법
최적화 기법
웹 개발에서의 최적화를 할 수 있는 방법은 다양하다. 코드의 최적화는 물론이고, 여러 리소스(이미지 등의 멀티미디어 등)들을 불러오는 것도 최적화에 영향을 주기 때문에 이러한 부분들을 고려할 필요성이 있다.
HTML 최적화
- DOM 트리 가볍게 만들기
DOM 트리가 깊을 수록, 자식 요소가 많을 수록 DOM 트리의 복잡도는 커진다. 복잡도가 클 수록 DOM 트리가 변경되었을 때 계산해야 할 것도 많아지기 때문에, 불필요하게 깊이를 증가시키는 요소는 삭제하는 것이 좋다.
// 수정 전 <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 최적화
- 사용하지 않는 CSS 제거
CSS 파일의 모든 코드 분석이 끝나면 CSSOM 트리가 생성되기 때문에, 불필요한 CSS 코드가 있다면 CSSOM 트리의 완성이 늦어진다. 사용하지 않는 CSS 코드가 있다면 제거하는 것이 좋다.
- 간결한 셀렉터 사용
셀렉터가 복잡할 수록 스타일 계산과 레이아웃에 시간을 더 많이 소모하므로, 최대한 간결한 CSS 셀렉터를 사용하는 것이 좋다.
// 복잡한 CSS 셀렉터 예시 .cart_page .cart_item #firstItem { ... } // 필요한 경우에는 어쩔 수 없지만 가능한 한 간결하게 작성한다. .cart_item { ... }
CSS 파일 불러오기
CSS 파일 연결은 HTML 문서 최상단에 배치해야 한다. 화면을 렌더링 할 때는 DOM 트리와 CSSOM 트리가 필요한데, DOM 트리는 HTML 코드를 한 줄씩 읽으면서 순차적 구성이 가능하지만 CSSOM 트리는 CSS 코드를 모두 해석해야 구성할 수 있기 때문에 CSSOM 트리를 가능한 빠르게 구성할 수 있도록 하는 것이다.
// CSS 파일은 HTML 파일 상단의 head 요소 안에서 불러오는 것이 좋다.
<head>
<link href="style.css" rel="stylesheet" />
</head>
JavaScript 파일 불러오기
HTML 코드 파싱 중에 <script>
요소를 만나는 순간 해당 스크립트가 실행되는데, <script>
요소 이전까지 생성된 DOM까지만 접근할 수 있기 때문에, HTML 코드 중간에 스크립트가 들어가면 화면이 의도한 대로 표시되지 않는다.
또한 스크립트가 실행이 완료되기 전까지 DOM 트리 생성이 중단되기 때문에, 중단된 시간만큼 렌더링 완료 시간은 늦춰진다. 따라서 JavaScript 파일은 DOM 트리 생성이 완료되는 시점인 HTML 문서 최하단에 배치하는 것이 좋다.
<body>
<div>...</div>
...
// JavsScript 파일은 body 요소가 닫히기 직전에 작성하는 것이 가장 좋다.
<script src="script.js" type="text/javascript"></script>
</body>
브라우저 이미지 최적화
- 이미지 스프라이트
클라이언트에서 서버 요청이 증가하면 로딩 시간은 점점 늘어난다. 간단한 아이콘이나 작은 이미지 요청을 위해 수십번의 요청을 보내는 것은 비효율적이기 때문에, 서버 요청 수를 줄이기 위해 사용하는 것이 이미지 스프라이트 기법이다.
이미지 스프라이트는 작은 아이콘이나 버튼 등을 한 이미지 파일에 모아서 저장하고, 실제 출력은 특정 요소에
width
,height
,background-image
,background-position
등의 스타일 속성을 사용하여 이미지를 삽입할 수 있다. 해당 기법을 이용하면 한 번의 이미지 요청으로 대부분의 개별 이미지를 사용할 수 있어 네트워크 로딩 시간을 줄일 수 있고, 많은 이미지 파일을 개별로 관리할 필요가 없기 때문에 관리가 용이하다는 장점이 있다.
- 아이콘 폰트 사용하기
아이콘 사용이 많을 때는 모든 아이콘을 이미지로 사용하기 보다는 아이콘 폰트를 사용해 용량을 줄일 수 있다. 대표적인 아이콘 글꼴 서비스로는 Font Awesome이 있다. Font Awesome은 CDN 방식 또는 npm 모듈을 설치해 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
- WebP 또는 AVIF 이미지 포맷 사용하기
이미지 최적화를 위해 전통적인 포맷(JPGE, PNG 등)이 아닌 새로운 이미지 포맷인 WebP 또는 AVIF를 사용해 용량을 더욱 감소시킬 수 있다. 하지만 비교적 최근에 등장한 이미지 포맷이기 때문에 전통적인 포맷처럼 모든 브라우저에서 호환되지 않는다는 단점이 있다. WebP의 경우 구버전의 브라우저나 Safari 브라우저에서는 지원되지 않으며 AVIF의 경우 Chrome, Opera 등 소수의 브라우저만 지원하고 있다.
이런 이미지 호환성을 지원하기 위해 실제로 사용할 경우 HTML의
<picture>
태그를 이용해 각 브라우저의 호환에 맞게 분기를 대체할 수 있다.<picture> <source srcset="logo.webp" type="image/webp"> <img src="logo.png" alt="logo"> </picture>
CDN 사용
CDN은 컨텐츠를 좀 더 빠르교 효율적으로 제공하기 위해 설계되었다. 네트워크 지연(latency)는 유저와 호스팅 서버간의 물리적 거리의 한계에 따라 발생할 수 밖에 없으며, 거리가 멀다면 지연 또한 늘어난다. CDN은 이를 해결하기 위해 세계 곳곳에 분포한 분산된 서버에 컨텐츠를 저장한다.
간단히 말해 CDN은 유저가 가까운 곳에 위치한 데이터 센터(서버)의 데이터를 가져온다. 그러므로 데이터가 전달되기 위해 거쳐야 하는 서버의 갯수가 크게 줄어들어 로딩 속도가 빨라진다.
캐시 사용하기
캐시(Cache)는 다운로드 받은 데이터나 값을 미리 복사해 놓는 임시 장소를 뜻하며, 데이터에 접근하는 시간이 오래 걸리거나 값을 다시 계산하는 시간을 절약하고 싶은 경우에 사용한다.
예를 들어, 모든 페이지에서 사용하는 logo.jpg
파일이 있다고 가정해보자. 다른 페이지로 이동을 할 때 새로운 리소스를 요청하게 되는데, 완전하게 동일한 파일을 똑같이 요청해 받아온다면 이는 리소스의 낭비로 이어지게 된다.
이런 불필요한 요청을 막기 위해 캐시를 활용한다. 서버에서 보낸 응답에 헤더에 Cache-Control
을 작성하면 해당 리소스의 유효기간을 설정할 수 있다. 이 방법은 서버측에서 리소스에 유효기간을 둠으로서 요청의 빈도를 줄일 수 있는 방법이다.
캐시 검증 헤더와 조건부 요청
서버에서 유효기간을 지정하지 않고, 서버의 파일과 캐시의 파일이 동일한지 확인해서 재사용하는 방법도 있다. 캐시 검증 헤더와 조건부 요청 헤더이다.
- 캐시 검증 헤더
캐시에 저장된 데이터와 서버의 데이터가 동일한지 확인하기 위한 정보를 담은 응답 헤더이다.
Last-Modified
: 데이터가 마지막으로 수정된 시점을 의미하는 응답 헤더로, 조건부 요청 헤더인If-Modified-Since
와 묶어서 사용한다.
Etag
: 데이터의 버전을 의미하는 응답 헤더로, 조건부 요청 헤더인If-None-Match
와 묶어서 사용한다.
- 조건부 요청 헤더
캐시의 데이터와 서버의 데이터가 동일하다면 재사용을 요구하는 의미의 요청 헤더
If-Modified-Since
: 캐시된 리소스의Last-Modified
값 이후에 서버 리소스가 수정되었는지 확인하고, 수정되지 않았다면 캐시된 리소스를 사용한다.
If-None-Match
: 캐시된 리소스의ETag
값과 현재 서버 리소스의ETag
값이 같은지 확인하고, 같으면 캐시된 리소스를 사용한다.
예제를 통해 알아보는 캐시 검증 헤더와 조건부 요청
- 첫 번째 요청을 보내고 응답을 받으면서 캐시 유효 시간이 60초인 이미지 파일을 같이 받아온다. 이 때,
서버의 파일이 수정된 시간/버전
을 의미하는 캐시 검증 헤더(Last-Modified
/Etag
)에 담긴 내용도 캐시에 함께 저장한다.
- 캐시 유효 시간인 60초를 초과한 후에 두 번째 요청을 보낼 때, 캐시의 데이터 사용을 위해 데이터 수정이 있었는지 확인하는 요청 헤더 (
If-Modified-Since
/If-None-Match
)와 캐시에 함께 저장해뒀던 캐시 검증 헤더(Last-Modified
/Etag
)값을 담아 요청을 보낸다. 이 값을 이용해 서버 데이터와 캐시 데이터를 비교하게 된다. 동일한 데이터라면 캐시 검증 헤더의 값은 같을 것이다.
- 서버와 캐시의 데이터가 동일하다는 것이 검증되었다면 서버는 데이터가 수정되지 않았음을 의미하는
304 Not Modified
라는 응답을 보내고, 캐시 데이터의 유효 시간이 갱신되어 해당 데이터를 재사용할 수 있게 된다.
트리 쉐이킹(Tree Shaking)
트리쉐이킹(Tree Shaking)은 말 그대로 나무를 흔들어 잔가지를 털어내듯 불필요한 코드를 제거하는 것을 의미한다. 웹 개발 시 어플리케이션의 규모가 커지면서 코드의 양이 방대해짐에 따라 다양한 라이브러리 사용시 불필요한 코드를 그대로 가져가는 경우가 많이 발생한다. 이런 불필요한 코드들을 찾아내어 제거하면 웹 사이트 성능 최적화에 큰 도움이 된다.
JavaScript에서 트리 쉐이킹을 해야하는 이유
- JavaScript 파일의 크기
과거의 HTML 위주의 단순한 웹 페이지와 달리 현재는 규모있고 화려한 인터랙션을 가진 웹 어플리케이션이 많다. 웹 사이트에서 인터랙션이 많아졌다는 것은 그만큼 JavaScript의 비중이 높아졌다는 뜻이기도 하다.
JavaScript의 파일 크기 뿐만 아니라, 파일을 요청하는 HTTP 요청 수 또한 폭발적으로 증가했으며, 이는 그만큼 네트워크 리소스 소모가 커졌다는 것을 의미하기도 한다. 이런 리소스 소모를 줄이기 위해 필요한 것이 트리 쉐이킹이다.
- JavaScript 파일의 실행 시간
JavaScript는 파일의 압축 해제, 코드 파싱 후 DOM 트리 생성, 컴파일 과정까지 거쳐야 하기 때문에 다른 리소스에 비해 실행까지 상대적으로 많은 시간이 소요된다. JavaScript 파일의 실행은 CPU에 크게 영향을 받는데, 그렇다 보니 사양이 천차만별인 모바일 환경에서 그 영향이 더욱 두드러진다. 만약 저사양 환경에서 코드를 실행한다면 그 시간을 기하급수적으로 늘어나기 때문에, 이탈률과 직결될 수 있다. 이를 막기 위해서도 트리 쉐이킹을 통한 최적화가 필요하다.
JavaScript 트리 쉐이킹
Webpack 4버전 이상을 사용하는 경우 ES6 모듈을 대상으로는 기본적으로 트리 쉐이킹을 제공한다. 웹팩을 사용하는 환경에서 효과적으로 트리쉐이킹을 수행하는 방법은 아래와 같다.
- 필요한 모듈만 import 하기
라이브러리 사용 시, 해당 라이브러리의 전체를 불러와서 사용하는 것 보단 필요한 모듈만 불러오면 번들링 과정에서 사용하는 부분의 코드만 포함시키기 때문에 트리쉐이킹이 가능해진다.
import { useState, useEffect } from 'react'
- Babelrc 파일 설정하기
Babel로 ES5 문법 변환 시 ES5문법은
import
를 지원하지 않기 때문에 commonJS 문법의require
로 변경시키는데,require
는export
되는 모든 모듈을 불러오기 때문에 트리 쉐이킹의 걸림돌이 된다. 이를 방지하기 위래 Bbelrc 파일에 다음과 같은 코드를 작성하면 ES5로 변환하는 것을 막을 수 있다.{ “presets”: [ [ “@babel/preset-env”, { "modules": false } ] ] }
- sideEffects 설정하기
웹팩은 사이드 이펙트를 일으킬 수 있는 코드의 경우엔 사용하지 않는 코드라도 트리쉐이킹 대상에서 제외시킨다. 이럴 때 package.json 파일에서
sideEffects
를 설정해 사이드 이펙트가 생기지 않을 것이므로 코드를 제외시켜도 됨을 웹팩에게 알려줄 수 있다.{ "name": "tree-shaking", "version": "1.0.0", "sideEffects": false } // 특정 파일만 지정하려면 아래와 같이 배열에 경로를 작성한다. { "name": "tree-shaking", "version": "1.0.0", "sideEffects": ["./src/components/NoSideEffect.js"] }
- ES6 문법을 사용하는 모듈 사용하기
트리쉐이킹이 적용되지 않는 라이브러리가 있다면 해당 라이브러리가 어떤 문법을 사용하고 있는지 확인해볼 필요가 있다. 모듈에 따라 ES5로 작성된 모듈이 있을 수 있기 때문이다. 해당 모듈을 통째로 사용하는 상황이라면 상관없지만, 일부만 사용하는 경우라면 해당 모듈을 대체하면서 ES6를 지원하는 다른 모듈을 사용하는 것을 고려할 수 있다.
Uploaded by N2T