이전에 작성했던 리팩토링 / 최적화를 마저 이어가보려고한다.
먼저, 저번 포스팅에서도 말했듯이 이정도의 규모에서는 크게 코드의가독성, 유지보수에 대한 리팩토링 외에는 최적화에 대한 의미가 크게 와닿지도 않고, 오히려 메모리공간만 차지할 수 있지만 그냥 이런 방법도 있구나 라는걸 경험해보기 위해 이어가본다.
저번 포스팅에서 개선방법3까지 진행했으니, 개선방법4부터 가보자
개선방법4
react-icons 번들사이즈 줄이기
항상 react-icons 라이브러리를 사용할 때마다 react-icons 문서에 들어가서 icon들의 개수를 보면 와... 이거 사용하지도 않는데 너무많이 받아오는데? 라는 생각을 계속 하긴했었다. 이 생각을 나만하진 않았을것이기 때문에 방법을 찾아보았다. 당연히(?) 방법이 있었다. 빌드할 때 모든 파일이 포함되어 chunk 사이즈가 커지는 react-icons 라이브러리 대신 아이콘 별로 자바스크립트 파일을 각자 가지고 있는@react-icons/all-files라는 라이브러리를 통해 빌드시 트리쉐이킹 방식으로 chunk의 사이즈를 줄일 수 있었다. 적용방법은 간단했다.
npm remove react-icons. // 원래 사용했던 react-icons 라이브러리를 쳐내고
npm i @react-icons/all-files // @react-icons/all-files라는 라이브러리를 설치했다.
그다음 기존코드에서 react-icons에서 import한 코드를 아래처럼 바꿔주었다.
// GameListCard.tsx. 리팩토링 전
import {
AiFillWindows,
AiFillAndroid
} from "react-icons/ai";
import { RiCheckLine, RiXboxFill } from "react-icons/ri";
import { SiLinux, SiNintendo3Ds } from "react-icons/si";
// GameListCard.tsx 리팩토링 후
import { AiFillWindows } from "@react-icons/all-files/ai/AiFillWindows";
import { AiFillAndroid } from "@react-icons/all-files/ai/AiFillAndroid";
import { RiPlaystationFill } from "@react-icons/all-files/ri/RiPlaystationFill";
import { RiXboxFill } from "@react-icons/all-files/ri/RiXboxFill";
import { RiCheckLine } from "@react-icons/all-files/ri/RiCheckLine";
import { SiLinux } from "@react-icons/all-files/si/SiLinux";
모든 react-icons를 활용한 아이콘들이 들어가는 부분을 위처럼 바꾸어주니, TBT(Total Blocking Time)과 Javascript 로딩시간이 감소하니 LCP 또한 감소하게 되었다. 사실 LCP 최적화는 이미지 자체크기, 차세대 이미지형식을 제공하는 것이 제일 크게 와닿겠지만 imageUrl 자체가 jpg로 오기때문에 어떻게 손쓸 방법을 아직 찾지 못하였다. 일단 아주 좋은 라이브러리를 알게된 것에 만족하자!!
개선방법5
웹폰트 최적화
라이트하우스에서 웹폰트가 다운로드 되는동안 텍스트가 계속 표시되는지 확인하라는 진단도 나왔다. 이 부분은 포트폴리오를 만들 때도 똑같이 겪었던 문제였다. 간단하게 다시 말하면, 브라우저가 렌더링할 때
- 브라우저가 HTML 파일 요청
- HTML응답파일로 DOM구성
- CSS컨텐츠파일로 CSSOM 구성
- 이 둘을 결합시켜 렌더트리를 구성 (이 때 폰트 리소스를 요청하게 된다.)
- 렌더트리를 토대로 화면에 그림
이때 5번 과정에서 폰트 리소스응답이 늦어지면 FOIT 혹은 FOUT이 발생하게 되는 것이다.
즉, 폰트 리소스응답이 늦어져서 먼저 콘텐츠가 화면에 그려질 때, 텍스트가 없다가 늦게 뜨거나, 텍스트는 있지만 폰트가 뒤늦게 적용된다. 이러한 현상을 각각 (FOIT, FOUT)이라고 부른다.
서론이 길어졌는데 결국 하고싶었던 개선방법은 사용자의 네트워크 속도는 내가 바꿀수 없으니, 웹 폰트의 용량, 폰트가 로드되고 있을 때도 텍스트가 계속 표시되게 하는 방향으로 개선을 하였다.
@font-face {
font-family: "Pretendard";
src: local("Pretendard-SemiBold"),
url("Pretendard-SemiBold.subset.woff2") format("woff2");
font-weight: 600;
font-display: swap;
}
나는 폰트를 4가지의 weight를 사용하였고, 그 중 한가지를 가져왔다. 먼저, 웹폰트의 용량을 줄이기 위해 처음 폰트를 받아올 때부터 subset이 적용된 폰트를 가져왔다. 서브셋 폰트는 간단하게 말해서 불필요한 글자를 제거하고 사용할 글자만 남겨둔 폰트라고 생각하면된다. 또한, 폰트형식도 woff2형식을 사용하여 타 형식보다 압축이 더 많이 된 형식을 선택하였다.
그리고 나는 폰트가 로드되고 있을 때에도 텍스트를 계속 표시하게 할 것이기 때문에 FOUT 방식을 사용했다. FOUT방식으로 작동하게 하기위해 font-display 속성을 주어 웹폰트의 로딩상태에 따른 동작을 설정할 수 있었다. 나는 FOUT방식과 동일하게 작동하는 옵션인 swap옵션을 주어서 텍스트를 먼저 렌더링하고, 웹 폰트 로딩이 완료되면 웹 폰트를 적용한 텍스트를 적용해줌으로서 웹 폰트 로딩 여부와 관계없이 항상 텍스트를 보이게 하였다.
다른 auto, block, fallback, optional 등의 옵션들도 있는데 이것들에 알고 싶으면 아래 문서를 참고하면 될 것 같다.
개선방법6
totalPrice 메모이제이션
장바구니에 등록한 게임들의 가격의 총합을 계산해주고 리턴해준 값을 저장하는 totalPrice가 있다. totalPrice는 cartItems의 값만 바뀔 때만 가격을 더해주거나 빼주면되는데, Cart 컴포넌트가 렌더링이 될 때마다 불필요하게 계속 계산을 해주고있다. 따라서 useMemo훅으로 첫번째 파라미터에는 어떻게 가격계산을 할지 정의하는 함수를 넣어주고, 두번째 파라미터에는 배열이 들어가는데, 배열안에 cartItems를 넣어줌으로써 cartItems의 변화가 있을때만 계산을 새로 해주고, 변화가 없을때는 재렌더링이 되더라도 이전에 메모이징한 값을 가져다가 재사용하게 하였다.
// Cart.tsx 리팩토링 전
const totalPrice = cartItems
.reduce((acc, item) => acc + item.price, 0)
.toFixed(2);
// Cart.tsx 리팩토링 후
const totalPrice = useMemo(() => {
return cartItems.reduce((acc, item) => acc + item.price, 0).toFixed(2);
}, [cartItems]);
물론 함수의 연산비용보다 메모이징에 들어가는 리소스의 비용이 더 클 수도 있다. 하지만 useMemo훅을 이렇게 사용할 수 있구나 라고 실제로 여러프로젝트, 코드에서 사용해보고 싶었다. useMemo를 사용하지 말라는 해외 아티클을 본적이 있다. 거기서는 효과적이고 의도적으로 최적화 통합을 시작하려면 먼저 문제식별을 하라고 하는데, 아직 이 부분이 정확히 와닿지 않는다. 또한 아래의 상황에서는 useMemo를 사용하지 말라고 하였다.
- 최적화하려는 계산이 저렴할 때
- 메모가 필요한지 확실하지 않을 때
- 메모하고 있는 값이 JSX에서만 사용될 때
- 종속성 배열이 너무 자주 변경될 때
사실 이번 리팩토링한게 위의 시나리오에 해당한다. 그렇지만 React.memo, 코드 분할, 지연 로딩 등의 다른 기술과 함께 리팩토링에 사용해보고 싶었다. 이제는 아키텍쳐를 리팩토링을 해볼생각이다. 지금의 아키텍쳐에서 코드베이스를 이해하기 쉽고, 유지보수를 좋게 하기위한 리팩토링을 하고, 포스팅해보겠다. 잘될지는.... 모름 :)