웹 어플리케이션에서 성능 향상하기(클라이언트 편)

정인우·2022년 12월 1일
0

React Dev.

목록 보기
1/8
post-thumbnail

아주 이전에 웹은 템플릿을 활용하여 클라이언트를 구성하였고, 그 이후에 Angular가 등장, React가 등장, 발전하면서 현재는 Quick이나 Svelte까지 다양한 방법을 통해서 웹을 보여주고 있습니다.

그리고 이러한 발전을 거치면서 웹에서 제공 가능한 서비스의 범위와 한계 역시 확실히 넓어지고 있습니다. 간단한 정적 페이지에서 다양한 동적 페이지, 3D 애니메이션, 게임 등 다양한 분야로 침투했습니다.

그리고 이러한 발전은 더 대개 더 많은 리소스와 통신을 필요로 합니다. 사용자의 디바이스 컴퓨팅 성능은 나날히 증가했지만 이러한 것이 개발자로 하여금 최적화를 하지 않아도 됨을 의미하는 것이 아닙니다.

본 글은 다음과 같은 순서로 진행이 될 예정입니다.

  • 초기 상황
  • 최적화 적용 방법 및 그에 따른 개선 결과(최적화 방안은 아래와 같습니다)
  • 최종 상황

본 글에서 다룬 최적화 방법은 다음과 같습니다.

  • Dynamic import
  • 경량화 라이브러리로의 대체
  • 사용하지 않는 라이브러리, 파일 삭제 및 대체
  • 압축 방식 변경
  • CDN 사용
  • 이미지 파일 최적화

물론 이외에도 추가적으로 가능한 여러 가지 최적화 방법(image sprite, api call 최소화 등등) 해당 게시글의 방법을 적용하는 것만으로도 충분한 성능 목표치를 이끌어낼 수 있을 것이라고 생각합니다.


초기 상황

최적화를 전혀 진행하지 않은 초창기의 로컬 환경에서의 메인 페이지 로드 결과입니다.

메인 화면 초기 로딩

끔찍합니다. 3.19s라는 해당 시간은 throttling이 전혀 걸리지 않은 상태에서의 소요 시간으로 throttling이 있거나 네트워크 상태가 좋지 않은 곳이라면 해당 웹에서 컨텐츠를 보기까지 족히 6초 이상도 걸릴 수 있습니다. 이러한 로드 시간은 사용자 경험에 중대한 하자임이 명확합니다.

구글 리서치 자료에 따르면, 모바일 웹사이트의 로딩 시간이 3초 이상일 때 사용자의 32%, 5초 이상은 90%, 6초 이상은 106%, 마지막으로 10초가 넘으면 123%의 이탈률이 발생합니다.

123%라는 수치가 어떻게 나오는 지에 대해서는 잘 모르겠지만 어쨌든 심각한 상태임을 알 수 있습니다. 구글의 코어 웹 바이탈 보고서에 따르면 구글은 2.5초 이내의 로딩 시간을 권장한다고 합니다. 이러한 점에서 현재 메인에서 64.3MB를 불러오며 3.2s의 로드 시간을 요구하는 초기 상황은 개선의 여지가 많아 보입니다.


최적화 적용 방법 및 그에 따른 개선 결과

1. Dynamic import

Dynamic import란

대부분의 코드들은 사용자가 보는 첫 페이지에는 필요하지 않습니다. 첫 페이지 진입시에 필요한 최소한의 코드만 다운 받게 되고, 사용자가 특정 페이지나 위치에 도달할 때마다 코드를 로드 한다면, 첫 페이지의 초기 성능을 올릴 수 있습니다.

한 마디로 제가 어떠한 페이지에 들어갈 때, 그 사이트의 서로 다른 많은 페이지들에 대해서 원래대로라면 한 번에 불러오게 되는데 dynamic import를 사용하면 런타임에 필요할 때에 맞게 불러오게 됩니다.

이는 동적 Code Spliting인데 정적 Code Spliting을 위해 webpack chunk 관련 설정을 진행할 수 있지만 이는 다음에 다뤄보도록 하겠습니다.

이런 방식을 lazy-load 게으른 로딩이라고 합니다.

간단히 코드를 통해 보면 다음과 같습니다.

import {
  Main,
  Redirect,
  LectureStream,
  LectureSpace,
  LectureUpload,
  LectureSetting,
  Mypage,
} from '@/page';
const Main = React.lazy(() => import('@/page/MainPage'));
const Redirect = React.lazy(() => import('@/page/RedirectPage'));
const LectureStream = React.lazy(() => import('@/page/LectureStreamPage'));
const LectureSpace = React.lazy(() => import('@/page/LectureSpacePage'));
const LectureUpload = React.lazy(() => import('@/page/LectureUploadPage'));
const LectureSetting = React.lazy(() => import('@/page/LectureSettingPage'));
const Mypage = React.lazy(() => import('@/page/Mypage'));

해당 Code들은 Router에 속한 코드로, Router에서 각각의 페이지들을 Dynamic import하도록 함으로서 각 페이지를 필요한 시점에 불러올 수 있도록 했습니다. 페이지가 많은 웹사이트일 수록 라우터에서의 dynamic import가 더욱 유용하게 사용될 수 있습니다.

Dynamic import 적용 후

dynamic import를 적용한 것만으로 3.19s에서 2.05s로 35%의 로드 시간 절감을 불러올 수 있었고, resource 역시 64.3MB에서 39.3MB로 38% 적게 불러오는 것을 확인할 수 있습니다.

이로써 구글의 2.5s 안에는 들 수 있었지만, 여전히 39.3MB의 Resource들을 받아오고 있으며, throttling이 걸린 환경에서는 해당 규칙을 만족하지 못할 것으로 보입니다.

2. 경량화 라이브러리로의 대체

그렇다면 이제는 불러오는 Resource들에 대해서 살펴보도록 합시다. 과연 이 무거운 Resource들이 무엇으로 구성이 되어 있고 어떻게 이 크기를 줄일 수 있을 지 말입니다.

현재 프로젝트에는 webpack를 사용하고 있으므로 Webpack bundle analyzer와 실제 받아오는 resource를 통해 살펴보도록 하겠습니다.

현재 불러오고 있는 리소스들을 확인하여 시각화를 진행했더니 문제점이 드러났습니다. 현재 메인 어플리케이션보다 더 무거운 것이 있었습니다.

해당 프로젝트는 다양한 아이콘을 사용하고 react-icons를 활용하면 다양한 아이콘들을 간편하고 통일된 방식으로 사용할 수 있기에 사용하였는데 현재 메인 어플리케이션 js build의 두배에 가까운 resource를 차지하고 있습니다. 위 그림의 빨간색 부분이 react-icons가 차지하는 부분입니다.

이와 관련해서 github 내의 활발한 토론들을 확인할 수 있었고 issue를 통해 해결 방법 역시 해결할 수 있습니다.

https://github.com/react-icons/react-icons/issues/154

링크의 내용을 요약하자면, 현재 react-icons는 어떠한 아이콘을 사용하던지 모든 아이콘을 불러오도록 짜여져 있다는 것입니다. 사용한 아이콘은 20개 남짓이지만 그 수많은 아이콘들을 다 불러오고 있기에 크고 느려질 수 밖에 없던 것입니다.

그렇기에 이를 실제 사용하는 아이콘만 가져오는 해당 라이브러리의 개선 버전으로 대체하였습니다.

이전의 2.06s 역시 일전보다 충분히 발전한 수치였지만 해당 개선을 통해서 훨씬 빠른 반응속도를 가지게 되었습니다. 기존의 39.3MB의 resource 요청은 11.2MB로 71% 감소하였고, 2.06s의 로드 시간은 420ms로 79% 감소하였습니다.

하지만 이러한 변화는 완벽히 긍정적이지는 않았습니다. 일단 코드의 변화양상은 다음과 같습니다.

import { GrRotateLeft, GrRotateRight } from 'react-icons/gr';
import { GrRotateLeft } from '@react-icons/all-files/gr/GrRotateLeft'
import { GrRotateRight } from '@react-icons/all-files/gr/GrRotateRight'

react-icons에서 @react-icons/all-files로 교체함에 따라 위의 코드처럼 한 번에 import를 하던 것을 아래처럼 하나하나 따 쪼개야했습니다. 한 번에 6개씩 import를 하는 경우 코드가 6줄로 나뉘어졌습니다. 이는 감내할 만하지만 critical한 것은 두 번째였습니다. 두 라이브러리가 가진 아이콘이 같지 않았습니다. 정확히 말하면 react-icons가 가진 최신 버전의 아이콘들이 @react-icons/all-files에서는 아직 업데이트가 되지 않은 부분이 많았고 이로 인해 대체할 아이콘을 찾아야만 했습니다.

관련된 이슈의 링크와 이미지를 첨부합니다.
https://github.com/react-icons/react-icons/issues/154

그러함에도 불구하고 성능의 상승이 극적이었기에 보람이 있는 듯합니다.

개선 이후의 analyze입니다.

analyze의 용량 역시 대폭 감소한 것을 확인할 수 있었습니다.

3. 사용하지 않는 라이브러리, 파일 삭제 및 대체

다음으로 최적화할 수 있는 방안은 사용하지 않는 라이브러리를 정리하는 것이었습니다.

현재 react-query를 도입을 중단하여 react-query를 사용하지 않으므로 삭제하였습니다.

시간과 resource가 소폭 감소하였습니다.

추가적으로 recoil을 context API로 교체하며 recoil을 삭제하였고 이를 바탕으로 소폭의 리소스 크기 감소를 이끌어냈습니다. 해당 글 링크는 ()

4. 압축 방식 변경 (CDN 사용)

마지막으로 기존의 gzip 압축 방식을 brotli로 변경하고자 하였고 관련된 webpack plugin을 확인하였습니다.

https://www.npmjs.com/package/brotli-webpack-plugin

하지만 현재 AWS의 CloudFront를 통해 배포하고 있고, CloudFront의 Caching Optimized option을 선택하면 brotli를 사용할 수 있으며, 기본적으로 CloudFront는 gzip과 brotli가 둘 다 가능한 브라우저 환경이라면 brotli를 통해 보내주는 것으로 확인했고 실제로도 그러하여 이 부분에서는 추가적인 작업을 하지 않았습니다.

64.3MB의 resource 요청에서 10.3MB까지 감소하였고, Load에 걸리는 시간 역시 3.19s에서 0.4s까지 감소하였습니다. 여기에 적절한 webp를 사용한다면 현재 png를 주로 사용하고 있기에 5MB 정도에 0.2~0.3s까지 감소시킬 수 있으리라 생각합니다.

5. 이미지 최적화

메인화면 같은 경우 현재 이미지들을 많이 사용하고 있는데 전부 png 형식으로 되어 있습니다. 그렇기에 해당 파일들을 webp 형식으로 변경해주려고 했다. 일일이 손으로 말입니다. 하지만 개발자는 귀찮은 사람들이고 자동화를 사랑하기에 이를 해결해줄 방법을 webpack plugin에서 찾았습니다.

imagemin-webp-webpack-plugin

이를 적용하고자 했지만, 아래의 압축 방식 brotli를 사용하였을 때 기존의 작은 이미지 용량이 더욱 작아지기에 해당 부분을 적용하지는 않았습니다.

최종 상황(현재)

마지막으로 위의 사항을 전부 적용하고 실제 CDN(cloud-front의 brotli encoding, 빠른 반응속도 향상)을 통해 접근한 메인 페이지의 성능 및 분석을 확인하면서 끝내겠습니다.

0개의 댓글