
프론트엔드에서 성능 최적화는 어떤 의미를 가질까? 우선 웹 사이트의 성능은 사용자의 경험이랑 직결되는 부분이라고 할 수 있다. 어떤 웹 서비스를 사용했을 때 너무 느린 로딩 속도 때문에 답답함을 느꼈던 경험,한 번쯤은 해봤을 것이다. 이처럼 페이지의 로딩 속도가 늦어지면, 사용자는 불편함을 느끼고 이는 이탈로 이어지게 된다.BBC는 사이트가 로드되는 데 1초가 걸릴 때마다 사용자의 10%가 이탈한다는 사실을 발표한 적도 있다.
즉, 웹 페이지의 성능 최적화는 UX를 위해 개발 이후에 하는 중요한 단계라고 할 수 있다. 성능 최적화에 대한 기준이 궁금하다면, web.dev 를 참고해보자. 이 페이지에는 다음과 같은 성능 최적화에 대한 핵심 기준들이 소개되어 있다.
페이지에서 가장 큰 콘텐츠를 로드하는 데 걸리는 시간. 페이지의 로드 성능을 측정하는 지표이다. 2.5 초 이내를 기준으로 한다.
레이아웃 변경 횟수이다. 레이아웃의 변경이 많이 일어나면, 엔진이 그만큼 일을 더 많이 하고 시간이 오래 걸리게 된다.
상호작용을 할 때 다음 페이지가 로드되는 시간을 의미한다. 전체적인 사용자 경험을 반영하는 지표이다.
첫 번째 페이지가 로드되는데 걸리는 시간이다.
웹 페이지가 사용자와 상호작용을 할 수 없는 시간을 말한다. First Contentful Paint (FCP)와 Time to Interactive (TTI) 사이의 총 차단 시간을 의미한다. 사용자가 상호작용을 시도했을 때, 기본 스레드가 차단된 총 시간을 측정하는 것이다.
성능을 측정하는 가장 기본적인 방법은 크롬의 개발자 도구에 있는 lighthouse 를 사용하는 것이다. 위 측정지표들을 알아서 계산해서 알려주는 편리한 도구이다. 한번 lighthouse 를 사용해서 우리 프로젝트인 방끗을 측정해보았다.

정말 처참한 점수가 나왔는데..(목표는 90점 이상) 덕분에 다양한 성능 최적화 기법의 효과를 제대로 볼 수 있을 것 같다는 생각이 들었다.😇
우선 프로젝트의 빌드 파일을 줄이기로 했다. 빌드 파일을 줄이면, 페이지 로딩 시간이 줄어들 뿐만 아니라 FCP, TBT, CLS 등을 줄여주는 효과가 있다.
빌드 파일을 줄이려면, 빌드의 내용을 자세히 까볼 필요가 있다. BundleAnalyzerPlugin는 현재 번들에 어떤 항목들이 들어있는지를 세세하게 알려주는 플러그인이다. 이것을 실행해서 빌드 내용을 보면 어떤 파일이 빌드 용량을 많이 차지하고 있는지, 어떤 것이 불필요하게 들어가 있는지를 알 수 있다. 웹팩의 prod 설정에서 밑과 같이 플러그인 설정을 해주었다.
//webpack.prod
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
config.plugins.push(new BundleAnalyzerPlugin());
우리의 최적화 전 빌드 내용은 다음과 같았다. (개발에만 쓰는 스토리북이 있네..?딱 걸렸다 네 이놈)
스토리북이 빌드 파일의 가장 큰 용량을 차지하고 있는 것을 발견했고, 이것은 스토리북을 devDependencies가 아닌 dependencies로 설치했기 때문이라는 것을 알 수 있었다. dependencies 는, 프로젝트의 실행시 필요한 의존성을 모아놓는 곳이기에, 개발 시에만 필요한 스토리북은 필요가 없다.
빌드에서 라이브러리가 차지하는 용량이 꽤나 크다는 것을 알게 되고, 밑과 같이 dependencies에는 실행시 필요한 라이브러리만 들어가도록 라이브러리 정리를 시작했다.

또 우리 팀이 택한 방법은 용량이 큰 라이브러리를 작은 용량의 라이브러리로 바꾸는 것이다. 우리 팀의 아티클 페이지에서는 react-markdown-preview 라는 라이브러리를 쓰는데,BundleAnalyzerPlugin에서 이 라이브러리의 용량이 큰 것을 발견했다. react-markdown-preview/nohighlight 라는 같은 기능을 하지만 용량이 적은 라이브러리로 교체해주었더니, 322KB -> 110KB로 용량이 감소하는 효과를 볼 수 있었다!
HTTP 압축을 하면 웹 서버에서 리소스들을 압축 알고리즘을 통해 사이즈를 줄여준다. 즉, 절대적인 리소스의 크기를 줄여 최적화를 할 수 있는 방법이다.
자주 사용하는 방식은 gzip, brotli 두 가지가 있는데, gzip은 리소스를 70~80% 정도 줄이는 효과가 있고, brotli는 gzip보다 20% 정도 압축률이 더 뛰어난 최신 기술이다. 하지만 brotli는 모든 브라우저에서 지원되는 방식은 아니기 때문에, 이 점은 유의해서 사용해야 한다.
방끗 프로젝트는 cloudfront 에 배포를 했는데, 이 cloudfront 에서 압축 기능을 자동으로 지원한다. cloudfront 의 정책 탭에 가면, 이 압축 지원을 찾을 수 있으며, 기본으로는 Gzip으로 설정되어 있고 Brotli 를 선택할 수도 있다. 우리는 brotli 를 선택했다.

실제 리소스가 우리가 선택한 방식으로 압축이 되었는지는 네트워크 탭에 가면 확인할 수 있다. 다음과 같이 요청 헤더에는 브라우저에서 지원하는 압축 방식을 알 수 있다.

응답 헤더에는 이 리소스가 어떤 방식으로 오는지를 확인할 수 있다. 현재 방끗의 리소스는 br(brotli) 압축 방식이 잘 적용된 것을 확인할 수 있다.

소스맵(source map) 은 디버깅을 위해서, 압축되고 난독화된 코드와(사용되는 코드)와 원본 코드를 맵핑한 정보가 선언된 파일이다. 이 소스맵이 있기에 우리는 개발자 도구에서 문제 있는 코드가 원본의 몇 번째 코드인지 알 수 있다. 빌드 파일을 보면 .map 이라고 써져 있는 파일이 이 소스맵 파일이다.
개발 모드에서는 유용한 이 소스맵은 prod 배포 모드에서는 불필요한 파일이다. 오히려 빌드 파일의 크기만 증가시키고 성능 저하를 일으킨다. 또한 원본 코드를 찾을 수 있기 때문에, 보안 위험도 증가한다.
그래서 우리 프로젝트에서는 dev모드에서는 소스맵을 생성하지만, prod 모드에서는 생성하지 않기로 했다. 하지만 우리 프로젝트에서는 sentry 라는 에러 트래킹 도구를 사용하는데, 이걸 사용하려면 소스맵이 필수적이다.
따라서 우리가 택한 방식은, sentryWebpackPlugin을 사용해서 빌드 과정에서 소스맵을 제거하는 것이다.
//webpack.prod
sentryWebpackPlugin({
sourcemaps: {
filesToDeleteAfterUpload: ['**/*.js.map', '**/*.css.map', '**/*.LICENSE.txt'],
},
)
webpack.prod에 위와 같은 코드를 추가해주었다. 덕분에 배포 환경에서는 소스맵이 빌드에서 제외되었다!
만약 프로젝트에서 이미지를 많이 쓰고 있다면 가장 먼저 실행할 최적화 기법은 이미지를 경량화하는 것이다. 생각보다 빌드 파일에서 이미지가 차지하는 용량이 꽤나 크기 때문이다!
우리 방끗 또한 랜딩 페이지에서 스크린 샷 이미지, 아이콘 이미지의 용량이 큰 편이었다. png 를 경량화된 최신 확장자인 webp로 바꿔주었다.20kb정도 였던 이미지가 7kb로 거의 1/3 로 줄어든 것을 확인할 수 있었다. 이미지들은 확장자들만 바꾸어줘도 최적화가 잘 된다!

이미지를 처음 넣을 경우 디바이스 크기에 비해 훨씬 큰 편이다. 가장 큰 디스플레이(1920px) 에 최대 2배의 크기로 이미지를 변경하면 이미지 크기를 딱 맞게 최적화 할 수 있다.
sprite 기법은 여러개의 이미지 파일을 하나의 이미지로 합치는 기법을 의미한다.
이 기법을 쓰는 이유는 이미지를 하나의 단일 파일로 합쳐 하나만 로딩하게 함으로써, 로딩 속도를 줄이기 위해서이다.우리도 카테고리에서 쓰는 아이콘을 하나의 파일로 합쳐주었다.

또한 해당 파일을 png => webp 로 바꿔 용량을 줄였다.

앞서 설명한 빌드 파일의 용량을 줄이는 것도 로딩 성능을 개선해주지만, 이 방법외에도 폰트 파일 최적화, 외부 스크립트 로딩 줄이기, preload 사용 등을 통해서 로딩 성능을 개선할 수 있다.
방끗은 첫 페이지가 랜딩 페이지인데, 이 페이지에서는 방끗을 소개하는 다양한 스크린 샷 이미지 & 로고 이미지들이 들어간다.
특히 첫 페이지의 LCP를 차지하는 요소일 경우, 해당 이미지는 preload로 미리 불러오면 로딩 시간을 절약할 수 있다. lighthouse도 밑의 이미지를 preload 하라고 알려주었다.

index.html에 미리 preload 하고 싶은 이미지를 다음과 같이 불러와주었다.원래는 밑과 같이 파란선(DOMContentLoaded가 완료된 시점) 뒤에 이미지들을 불러와 이미지가 렌더링이 늦게 됐지만,

이미지들에 preload 들을 적용하니 밑과 같이 이미지들이 DOMContentLoaded가 되기 전에 로드되는 것을 확인할 수 있었다.

수치상으로, 이미지 로드까지 1.5초 이상 소요됐던 것이 0.5초 개선이 되었다!
원래 우리가 사용하는 SUITE 폰트는 파일을 외부 링크에서 다운받고 있는 상황이었다. 더군다나 2번이나 이 폰트를 불러와서 첫 페이지의 렌더링을 블로킹하고 있었다. 또한 폰트 파일을 외부 사이트에 의존하고 있는 것이 걸려서, 내부 파일에 포함시키기고 preload 를 하여 로드를 최적화하였다.
index.html에 다음과 같이 폰트 파일을 preload 해주었다.

청크 정리까지 더했더니, 다음과 같은 개선이 이루어졌다.
청크 정리 및 폰트 포함 전:
- Load: 1.8초
- Finish: 3초
폰트 포함 후:
- Load: 0.7초
- Finish: 1.8초 (페이지 로드 0.7초 + Sentry 1초)
사실 폰트는 외부에서 불러오더라도 preload 만 잘 해준다면 최적화가 된다.
기존에는 CDN에 저장된 리소스들의 캐시 기간이 짧아, 반복적인 요청 시에도 자주 네트워크에서 리소스를 다시 가져와야 했다. 100% 확실하지는 않지만, 초기에는 헤더에 캐시 정책이 없어서 캐시가 아예 되지 않았던 것 같다.

이로 인해, 동일한 리소스를 반복적으로 서버에서 가져오는 빈도가 높아져 네트워크 비용 증가와 페이지 로딩 속도 저하가 발생했다.
다음과 같이 cloudfront에 직접 사용자 지정 헤더를 추가하여, 캐시 기간을 1년으로 늘려주었다.
캐시 설정은 다음과 같이 cloudfront 에서 동작 편집 탭에 가서 응답 헤더 정책을 설정해주면 된다. 그러면 cloudfront에서 리소스들을 불러올 때 설정한 대로 캐시가 되어 불러와진다.

이 외에도 다음과 같은 최적화 작업들을 수행했다.
다음과 같이 라우터 파일에 모든 페이지를 필요할 때 로드하도록 해주었다. 참고로 lazy 는 suspense 와 같이 써야 에러가 안난다는 점을 기억해두자!

체크리스트 작성 컴포넌트들에 memo 기능을 사용해서 렌더링을 최적화했다.
위와 같은 방식으로 최적화를 진행해주었더니 방끗의 lighthouse 점수는 모든 페이지에서 90점 이상으로 업그레이드 되었다!!(성능 최적화 전 51점)
여기까지 우리 프로젝트 <방끗> 에서 사용했던 최적화 방법을 정리해봤다. 프론트엔드에서 최적화를 하는 방법은 너무나 많지만, 중요한 것은 '어떠한 문제' 를 해결하기 위해 '어떤 방법' 을 쓸 수 있는지 키워드 정도만 생각해내는 능력같다. 키워드만 기억해내면 적용은 쉽다!😄
https://helloinyong.tistory.com/336
https://yozm.wishket.com/magazine/detail/2082/
아휴 진짜 꿀정보네요. 저희 플젝도 빛집 정보가 박살이 났는데 한번 써먹어보겠습니다 😅