이 글은 눈물나는 삽질이 많은 포스팅이니 유의해서 읽어주세요. 🥲
프로젝트에서 간단한 아이콘들을 다 찾아보는 것은 귀찮은 일이다. 그러던 중 팀원이 react-icons
라이브러리를 사용하자 제안해서 사용하게 되었고, 기존에 찾아서 사용할 때보다는 생각보다 많은 종류가 있던 건 아니였지만 그럼에도 대부분의 아이콘이 존재하고 편리했기 때문에 해당 라이브러리를 사용하기로 했다.
편리하게 사용했고 실제 데스크탑을 위한 서비스라 성능에 대해 크게 신경쓰지 않고 개발을 마쳤었다. 최근 리팩토링을 진행하며 lighthouse 성능 측정을 했었는데(desktop 기준), Performance가 55점이 나왔다.
네트워크 탭을 봐도 사이즈가 가장 컸다.
lighthouse에서 mobile로 측정을 하면 더 낮게 나오는데, 현재 데스크탑 기준으로 측정해도 55점이라면 개선할 필요가 있었다.
Performance는 웹 페이지 로딩 성능을 0에서 100까지의 숫자로 평가하는 종합 지표다.
보통 FCP, LCP, TTI, Speed Index, TBT 등을 분석하게 되는데, 점수에 영향을 주는 요인에는 자바스크립트 번들 크기, 이미지 최적화, 리소스 로딩 방식, 서버 응답 시간, 캐싱 전략에 따라 달라진다.
즉, 55점인 이 프로젝트는 중간 성능의 수치지만 89점도 중간 성능이기에 저성능 정도였다.
그리고 Treemap
을 확인해보니 대부분이 react-icons
였다.
Treemap
은 자바스크립트 번들을 시각화하여 어떤 모듈이 가장 많은 공간을 차지하는지 보여준다.
처음 쓰는 라이브러리인데 해당 라이브러리를 포기하고 그냥 icon으로 바꿔야하나 고민이 생겼다.
물론 하나의 그룹에 대해서만 import해서 사용한다면 나쁘지 않을지 모른다. 그러나 지금 현재 프로젝트에서 적용된 아이콘 그룹은 10개 정도는 되었다.
1번 방법은 굉장히 번거로울거 같았다. 아이콘을 사용하는 모든 곳을 찾아 바꿔줘야하고 아이콘 이미지를 찾아서 다 저장해줘야하기 때문이다. 아니면 비슷한 아이콘 링크를 가져와 넣는 방법도 있었겠지만 모든 곳을 찾아오는게 복잡하다 생각했다.
최대한 라이브러리를 사용하면서도 성능에 영향을 미치지 않게 하는 방법을 찾는게 좋을거 같다 생각했다.
이전에 찾아본 방법으로는 react-icons/all-files
이라는 라이브러리가 있었고, 이를 사용하면 아이콘 별로 자바스크립트 파일을 별도로 가지고 있어서 빌드 시에 트리쉐이킹 방식으로 더 적은 크기의 chunk를 만들 수 있게 된다.
트리쉐이킹이란?
트리쉐이킹은 사용하지 않는 코드를 번들에서 제거하는 최적화 기법이다. 모듈 의존성 그래프를 분석하거나 실제로 사용되는 코드를 식별해서 사용되지 않는 코드를 제거하는 방식이다.
그러나 이 방법은 적용하지 않았다. 왜냐하면 일부 아이콘 그룹은 지원하지 않았다. 그렇기 때문에 이 방법을 적용하는 경우에는 내가 사용하는 아이콘 그룹이 모두 있는지 확인하고, 만약 일부 그룹이 없다면 해당 그룹은 따로 다운 받아 사용하면 될 거 같다.
위와 같이 해결해도 되지만 일관되지 않은 방법이라 사실 적용하고 싶지 않았다.
Lazy Loading은 웹 애플리케이션에서 모든 코드를 한 번에 로드하는게 아닌, 실제로 필요한 시점에서 코드를 불러오는 기법이다. 리액트에서는 React.lazy()
와 Suspense
컴포넌트를 통해 이 기능을 구현할 수 있다.
위와 같이 구현하게 되면 초기 번들 크기를 줄여 애플리케이션 초기 로딩 시간을 단축할 수 있다. 특정 기능이나 특정 페이지에 접근할 때만 해당 코드를 로드해 첫 페이지 로딩 시간을 최적화할 수 있다.
react-icons는 다양한 아이콘 라이브러리를 한 곳에서 제공하는 패키지다. 그러나 라이브러리에 수천 개의 아이콘이 포함되어있으며, 일반적으로 애플리케이션에서는 그 중 일부만 사용한다. 이를 한 번에 로드하면 불필요한 코드가 포함되어 번들 크기가 커지고 성능이 저하되는 것이다.
React.lazy는 컴포넌트를 동적으로 불러올 수 있게 해주는 함수다. 일반적인 import 방식은 정적으로 애플리케이션이 시작될 때 모든 컴포넌트를 불러오지만, React.lazy를 사용하면 컴포넌트가 실제로 필요한 시점에 불러오는게 가능해진다.
해당 기능을 사용하기 위해서는 Suspense와 함께 사용해야한다. Suspense는 컴포넌트가 로딩될 때까지 기다리는 동안 대체 UI를 보여주는 래퍼 컴포넌트이다.
React.lazy를 사용한 컴포넌트는 즉시 로드되지 않고, 해당 컴포넌트가 렌더링될 때까지 로드가 지연된다.
React.lazy
로 아이콘을 import하고, 아이콘을 사용하는 컴포넌트 내에서 아이콘을 Suspense로 감싸서 사용하면 된다. 그런데 고민이 생겼다.
전부 다 위와 같은 방법을 적용해주게 되면 코드가 복잡해진다는 생각이 들었다.
import { Suspense, lazy } from 'react';
// 일반 import 대신 lazy import 사용
const FaHome = lazy(() => import('react-icons/fa').then(module => ({
default: module.FaHome
})));
function MyComponent() {
return (
<Suspense fallback={<div>아이콘 로딩 중</div>}>
<FaHome size={24} />
</Suspense>
);
}
위 코드는 아이콘을 하나만 사용해서 저렇지만, 만약 한 컴포넌트에서 불러오는 아이콘이 많다면 이야기는 달라진다. 좀 더 효율적으로 사용할 방법을 고민했다.
참고로 이 방법은 중간에 도입했다가 다시 사용하지 않게 된 방법입니다. 이 방법은 react-icons에서 이미 많은 그룹의 아이콘을 사용하고 있는데 번들 사이즈를 줄이려고 시도하는 분에게는 적합하지 않은 방법임을 알려드립니다.
import { ComponentType, lazy, LazyExoticComponent } from "react";
import { IconBaseProps } from "react-icons";
type IconCache = {
[key: string]: LazyExoticComponent<ComponentType<IconBaseProps>>;
};
const iconCache: IconCache = {};
const createIconComponent = (
iconModule: any,
iconName: string
): { default: ComponentType<IconBaseProps> } => {
const IconComponent = iconModule[iconName];
if (!IconComponent) {
console.warn(`Icon ${iconName} not found, using fallback`);
return { default: iconModule.FaQuestionCircle || (() => null) };
}
return { default: IconComponent as ComponentType<IconBaseProps> };
};
export const loadIcon = (iconName: string, library: string): LazyExoticComponent<ComponentType<IconBaseProps>> => {
const cacheKey = `${library}-${iconName}`;
if (iconCache[cacheKey]) return iconCache[cacheKey];
const LazyIcon = lazy(async () => {
switch (library) {
case 'fa':
return import('react-icons/fa').then(module =>
createIconComponent(module, iconName)
);
case 'fa6':
return import('react-icons/fa6').then(module =>
createIconComponent(module, iconName)
);
case 'io':
return import('react-icons/io').then(module =>
createIconComponent(module, iconName)
);
case 'io5':
return import('react-icons/io5').then(module =>
createIconComponent(module, iconName)
);
case 'md':
return import('react-icons/md').then(module =>
createIconComponent(module, iconName)
);
case 'bi':
return import('react-icons/bi').then(module =>
createIconComponent(module, iconName)
);
case 'tb':
return import('react-icons/tb').then(module =>
createIconComponent(module, iconName)
);
case 'ri':
return import('react-icons/ri').then(module =>
createIconComponent(module, iconName)
);
case 'gr':
return import('react-icons/gr').then(module =>
createIconComponent(module, iconName)
);
case 'bs':
return import('react-icons/bs').then(module =>
createIconComponent(module, iconName)
);
default:
console.error(`Unknown icon library: ${library}`);
return import('react-icons/fa').then(module =>
createIconComponent(module, 'QuestionCircle')
);
}
});
iconCache[cacheKey] = LazyIcon;
return LazyIcon;
};
이는 아이콘 그룹마다 import를 하는건데 동적으로 로딩하기 위해 switch 문을 이용해 경로를 적어줬다. 원래는 이렇게 하지 않고 변수를 전달해
react-icons/${변수}
와 같이 사용해서 코드를 줄였는데 이렇게 하면 적용되지 않고 문제가 발생했다.Vite 모듈 번들러가 코드를 정적으로 분석하고 필요한 모듈을 미리 찾아내 번들링하는 작업을 수행하는데, 이렇게 내가 변수로 전달하면 코드가 실행되기 전에 미리 모듈을 포함시키지 못하게된다. 그래서 모듈 경로가 변수가 할당되는 시점이 번들러가 모듈을 불러오는 시점보다 나중이라 발생하는 문제였다.
import { ComponentType, LazyExoticComponent, Suspense } from "react";
import { IconBaseProps } from "react-icons";
interface IconWrapperProps extends Omit<IconBaseProps, "size"> {
component: LazyExoticComponent<ComponentType<IconBaseProps>> | null;
size: number | string
}
function convertToRem(size: number | string) {
if (typeof size === "string") return size;
size = Number(size);
if (size % 4 === 0) return `${size / 4}rem`;
return `${size * 0.25}rem`;
}
const IconPlaceholder: React.FC<{ size?: number | string }> = ({ size = "1rem" }) => (
<div
className={`icon-placeholder animate-pulse bg-gray-200 rounded-md`}
style={{ width: `${convertToRem(size)}`, height: `${convertToRem(size)}` }}
/>
);
const IconWrapper: React.FC<IconWrapperProps> = ({
component: IconComponent,
className = "",
size = "1rem"
}) => {
if (!IconComponent) return <IconPlaceholder size={size} />;
return (
<Suspense fallback={<IconPlaceholder size={size} />}>
<IconComponent className={className} size={convertToRem(size)} />
</Suspense>
);
}
export default IconWrapper;
Wrapper 컴포넌트에서 Suspense로 감싸주고 IconPlaceHolder 컴포넌트를 만들어 컴포넌트 사이즈에 따라 적당한 크기의 회색 로딩 화면을 보여주게 설정했다.
여기서 size를 tailwind 사이즈와 동일하게 만들었는데 기존에 내가 아이콘 사이즈를 이렇게 설정했기 때문이다. 보통 tailwind에서는 w-4로 설정하면 4는 1rem이므로, 4를 전달하면 1rem으로 환산될 수 있게 설정했다. (만약 문자열 "1px"로 전달하면 이 부분이 바로 적용될 수 있게 설정했다.)
import { loadIcon } from "./IconLoader";
import IconWrapper from "./IconWrapper";
interface IconProps {
name: string;
library: string;
className?: string;
size?: number | string;
}
const Icon = ({ name, library, className = "", size = "1rem" }: IconProps) => {
const IconComponent = loadIcon(name, library);
return (
<IconWrapper
component={IconComponent}
className={className}
size={size}
/>
)
}
export default Icon;
사용자가 Icon을 사용할 때 이 컴포넌트를 불러오면 된다. react-icons에서 예를 들어 md 그룹에 MdClose 아이콘을 사용한다 가정하면,
<Icon name="MdClose" library="md" size={4} className="원하는 속성" />
와 같이 사용해주면 된다. name, library는 필수 항목이고(어떤 아이콘 그룹의 아이콘을 사용할 것인지 결정하기 때문에), 나머지 속성은 선택이다.
위와 같이 구성해서 lazy loading을 도입했고, 성능 측정을 했는데 놀랍게도 달라진게 없었다.
여전히 번들 사이즈가 컸다. 즉, 지연 로딩은 의미가 없었다.
예를 들어 내가 md 그룹과 io 그룹 아이콘을 하나씩만 사용해도 두 그룹의 모든 아이콘이 다 불러와지는 상태였기 때문에 지연 로딩을 도입해도 특정 페이지에 아이콘이 2~3개만 있어도 3개의 그룹 정도를 불러오기 때문에 의미가 없었다. 사실 저것보다 더 많았다.
FCP, LCP란?
FCP(First Contentful Paint)
는 브라우저가 DOM에서 첫번째 콘텐츠를 렌더링하는 시점을 측정한다. 사용자가 페이지에 접속한 후 텍스트, 이미지와 같은 의미 있는 첫번째 콘텐츠가 보이기까지 걸리는 시간을 의미한다.
보통 0~1.8초 사이면 성능이 좋다고 한다.
FCP를 최적화하려면 서버 응답 시간 개선, 렌더링 차단 리소스 제거, 리다이렉트 최소화, 중요 CSS 인라인 처리 등이 있다.
LCP(Largest Contentful Paint)
는 뷰포트 내에서 가장 큰 콘텐츠가 화면에 렌더링되는 시점을 측정한다. 페이지의 주요 콘텐츠가 사용자에게 얼마나 빨리 표시되는지를 나타내는 더 정확한 지표다.
LCP는 img, 배경 이미지, 텍스트 중 문단이나 헤딩, 비디오의 포스터 이미지 등을 측정한다.
보통 0~2.5초 사이면 성능이 좋다고 한다.
LCP를 최적화하려면 서버 응답 개선, 렌더링 차단 리소스 최소화, 리소스 로드 시간 개선(이미지 압축, 사전 로드), 클라이언트 렌더링 최적화(중요한 자바스크립트 코드 먼저 로드), 이미지 지연 로딩(뷰포트 밖의 이미지 나중에 로드) 등이 있다.
위와 같이 지연 로딩을 적용하고 FCP, LCP 값은 조금 개선된거 같았다. 그런데 이 변화가 크게 좋아졌다고 느낄 정도가 아니라 값 자체가 아직까지도 심각한 수준이었다.
이전의 FCP 값은 8.3s였고 현재는 6.0s가 되었다. 마찬가지로 LCP는 16.9s였는데, 11.7s가 되었다. 이렇게 보면 좋아진 것 같지만 점수는 똑같았다. 그리고 향상된 값도 좋은 값이 아니었기 때문에 개선되었다고 말할 수도 없었다.
다시 고민이 되었다. all-files를 사용하면 특정 아이콘 하나만 불러와진다고 했다. 그러면 지금 내 문제인 모든 아이콘 그룹이 불러와지는 문제는 해결할 수 있었다. 그래서 일단 all-files를 적용해보고 안되는 애만 불러오기로 결정했다. 안되는 애는 특정 그룹 아이콘이 지원 안된다 했으니, 내가 아이콘 그룹을 10개 정도 사용한다면 1~2개의 아이콘 그룹을 불러오니 많이 좋아질거라 생각했기 때문이다.
그런데 내 예상과 다르게 같은 그룹 내에서도 지원이 안되는 아이콘이 있었다.
내 생각은 bs 그룹 아이콘을 지원하면 다 있는 줄 알았는데, 그 그룹 중에서도 없는 아이콘이 있었다. 그러면 이 아이콘을 사용하기 위해 react-icons 라이브러리에서 불러오게 처리해야했다. 그렇다면 all-files를 차선책으로 선택한게 의미가 없어졌다고 생각했다. 이러한 이유로 all-files를 최종적으로 사용하지 않기로 했다.
lazy loading, all-files를 도입하는 삽질을 하고 나니 막막했다. 그냥 새로 아이콘을 다 찾아서 넣는게 빨랐겠다는 생각이 들었다.
그러나 원래 있던 디자인을 유지하면서 성능을 최적화할 방법을 고민해보고 싶었다. 그래서 node_modules 폴더로 가서 react-icons의 그룹별 아이콘 파일을 확인했다. 정말 파일이 너무 길었고 아이콘도 많았다.
결국 실패하더라도 어떻게든 여기서 내가 사용할 아이콘을 svg 컴포넌트로 만들자는 목표를 세우게 됐다.
이 일은 번거로웠지만 react-icons의 아이콘을 다 하나씩 만드는 것보다 내가 사용하는 아이콘을 리스트로 만드는게 더 빠르다 생각해 정리했다.
import * as md from "react-icons/md";
import * as fa from "react-icons/fa";
import * as fa6 from "react-icons/fa6";
import * as io from "react-icons/io";
import * as io5 from "react-icons/io5";
import * as bs from "react-icons/bs";
import * as ri from "react-icons/ri";
import * as bi from "react-icons/bi";
import * as gr from "react-icons/gr";
import * as im from "react-icons/im";
import * as tb from "react-icons/tb";
export const iconList = [
{ name: "MdLogout", component: md.MdLogout },
{ name: "MdLogin", component: md.MdLogin },
...
{ name: "IoHomeSharp", component: io5.IoHomeSharp },
{ name: "IoPersonSharp", component: io5.IoPersonSharp }
];
위에서 정의한 아이콘 리스트에서 목록을 가져와 각 아이콘에 대해 SVG 내용을 추출하고 리액트 컴포넌트를 생성하여 파일로 저장하는 코드를 작성했다.
const currentDir = process.cwd();
const outputDir = path.resolve(currentDir, "변환된 아이콘을 저장할 폴더 위치");
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
iconList.forEach(({ name, component }) => {
// 컴포넌트를 렌더링해서 SVG 데이터 추출
const rendered = component({});
const { attr } = rendered.props;
const viewBox = attr?.viewBox || "0 0 24 24";
// SVG 내용 추출 함수 (SVG 컴포넌트와 자식 요소를 재귀적으로 처리해서 문자열로 변환)
// 중첩된 구조가 있을 수 있다는 것도 중간에 만들면서 알게되어 수정했다.
const extractSvgContent = (children) => {
if (!children) return "";
// 배열인 경우
if (Array.isArray(children)) {
return children
.map(child => {
if (!child) return "";
// SVG 요소 타입 확인 (path, circle, rect)
const elementType = child.type;
if (!elementType || !child.props) return "";
// 일반적인 SVG 요소인 경우
if (typeof elementType === "string") {
const props = { ...child.props };
if (props.fill && props.fill !== 'none') { // fill 속성이 있고, "none"이 아닌 경우 currentColor
props.fill = 'currentColor';
}
if ((props['stroke-width'] || props.strokeWidth) && !props.stroke) { // stroke 속성이 있는 경우 currentColor로 변경
props.stroke = 'currentColor';
}
const attrs = Object.entries(props) // 모든 props를 속성으로 변환
.filter(([key, value]) => key !== 'children' && value !== undefined)
.map(([key, value]) => `${key}="${value}"`)
.join(" ");
if (child.props.children) { // 자식 요소가 있는 경우 재귀적으로 처리
const innerContent = extractSvgContent(child.props.children);
return `<${elementType} ${attrs}>${innerContent}</${elementType}>`;
}
return `<${elementType} ${attrs} />`; // 자식 요소가 없는 경우
}
if (child.props.children) { // 중첩된 요소인 경우는 재귀 처리
return extractSvgContent(child.props.children);
}
return "";
})
.filter(content => content.trim() !== "")
.join("\n ");
}
// 객체인 경우
if (children && typeof children === "object") {
const elementType = children.type;
if (!elementType || !children.props) return "";
if (typeof elementType === "string") {
const props = { ...children.props };
if (props.fill && props.fill !== "none") {
props.fill = "currentColor";
}
if ((props["stroke-width"] || props.strokeWidth) && !props.stroke) {
props.stroke = "currentColor";
}
const attrs = Object.entries(props)
.filter(([key, value]) => key !== "children" && value !== undefined)
.map(([key, value]) => `${key}="${value}"`)
.join(" ");
if (children.props.children) {
const innerContent = extractSvgContent(children.props.children);
return `<${elementType} ${attrs}>${innerContent}</${elementType}>`;
}
return `<${elementType} ${attrs} />`;
}
if (children.props.children) {
return extractSvgContent(children.props.children);
}
}
return "";
};
let svgContent = extractSvgContent(rendered.props.children);
// 컴포넌트 템플릿
const componentString = `import React from "react";
import { convertToRem } from "./convertToRem.ts";
interface IconProps extends React.SVGProps<SVGSVGElement> {
size?: number | string;
className?: string;
}
export const ${name} = ({ size = 4, className = "", ...props }: IconProps) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={convertToRem(size)}
height={convertToRem(size)}
viewBox="${viewBox}"
className={className}
fill="currentColor"
{...props}
>
${svgContent}
</svg>
);
};
`;
fs.writeFileSync(path.join(outputDir, `${name}.tsx`), componentString);
console.log(`🎉 아이콘 컴포넌트 생성 🎉: ${name}.tsx`);
});
여러 번 수정했다. 잘 만들어졌다 생각했는데 currentColor를 안해줘서 색상이 적용이 안되거나 stroke 속성을 고려하지 못해 원래의 아이콘과 다른 모양이 나오는 경우도 있었다. 이런 오류를 여러 번 마주하고 여러 번 수정해서 위와 같이 만들어졌다.
react-icons 라이브러리는 삭제해주었다. 그리고 내가 만든 컴포넌트로 경로를 바꿔줬다. 이름을 똑같이 만들었기 때문에 경로만 수정해주면 된다.
위와 같이 common/Icons 폴더에 컴포넌트를 생성해주어서 가져왔다.
그런데 아이콘이 많아질 수록 import가 늘어갔다. 이를 해결해주기 위해서 Barrel 파일
을 만들었다.
Barrel 파일이란?
여러 모듈이나 컴포넌트를 한 곳에서 내보내고 가져올 수 있도록 만든 인덱스 파일이다. Barrel 파일은 폴더 내 여러 파일들의 내용을 모아서 다시 내보내는 중간자 역할을 한다.
Icons 폴더에 index.ts를 만들어 모든 아이콘을 export하도록 했다.
이렇게 한 번에 아이콘을 가져올 수 있게 수정되었다.
Treemap을 보면 큰 부분을 차지하는게 거의 react-icons였는데 사라졌다.
점수도 55점에서 66점으로 개선되었다.
66점으로 개선되었지만 이 점수 또한 높은 점수는 아니다.
그래서 어떤 부분에서 문제가 되는지 더 살펴보았다.
렌더링을 하기 전 어떤 부분에서 오래 걸려서 느려지는지 파악할 필요가 있었다. performace를 측정해서 어느 부분이 오래 걸리는지 확인을 해보았다.
preconnect
를 설정해서 브라우저에게 곧 특정 도메인에 연결할거라고 미리 알려주도록 했다. DNS 조회 - TCP 연결 - TLS 협상 - 실제 리소스 요청 및 다운로드
단계로 이루어진다.DNS 조회 - TCP 연결 - TLS 협상
을 미리 완료해두어서 폰트 로딩 시간이 단축된다.최근엔 avif 형식의 파일을 많이 도입한다. webp보다 압축률이 높은 걸로 아는데 아직 도입하지 않았다. 나중에 시도해볼 생각이다.
또 이미지 파일은 화면 사이즈에 따라 로드하는 크기를 다르게 설정할 수도 있다.
const ErrorPage = lazy(() => import("./pages/error"));
const IntroPage = lazy(() => import("./pages/intro"));
const CreateChannelPage = lazy(() => import("./pages/channels/create"));
const ChannelListPage = lazy(() => import("./pages/channels"));
const ChannelPage = lazy(() => import("./pages/channel"));
const QuestionListPage = lazy(() => import("./pages/questions"));
const CreateQuestionPage = lazy(() => import("./pages/questions/create"));
const QuestionDetailPage = lazy(() => import("./pages/questions/detail"));
const LoginPage = lazy(() => import("./pages/login"));
const AuthCallbackPage = lazy(() => import("./pages/login/callback"));
const MyPage = lazy(() => import("./pages/mypage"));
SPA 방식
으로 작동한다. SPA는 초기에 필요한 모든 자바스크립트 번들을 로드한다.특정 페이지에서 필요한 데이터만 로딩하기 위해 lazy loading으로 초기에 모든 데이터를 받아오지 않도록 설정했다. 그렇지만 추후 사이드 바에 hover하면 미리 해당 페이지 데이터를 가져올 수 있게 도입해보면 좋을거 같다는 생각이 들었다.
lodash
는 자바스크립트 개발에서 널리 사용되는 유틸리티 라이브러리다.
npx vite-bundler-visualizer
로 번들 크기와 구성을 분석하는 도구를 이용할 수 있다.lodash를 사용하는 곳이 거의 없음에도 왜 큰 부분을 차지하는지 의문이 들었다.
import { throttle } from "lodash";
로 사용해주었는데, throttle 함수만 가져오는 것처럼 보이지만 전체 lodash 라이브러리가 번들에 포함되게 된다.찾아보니 lodash는 CommonJS 모듈 시스템을 사용하고 객체 구조 분해를 사용하게 되면, 브라우저가 전체 lodash 패키지를 로드하고 객체 구조 분해를 통해 throttle만 추출하는 과정을 거치게 된다. 그렇기 때문에 위와 같이 import해도 전체 라이브러리를 가져오기 때문에 번들러 도구로 확인했을 때 큰 부분을 차지하고 있었다.
91점으로 55점에서 많이 개선되었다. 더 나은 성능을 위해 노력할 수 있지만, 삽질이 꽤 오래 걸렸고 조금 지쳤다. 원래 계획은 다른 것을 리팩토링하기로 세워뒀는데 생각치도 못한 것들을 해결하느라 시간이 많이 걸렸다. 그렇지만 느낀 것도 굉장히 많고 여러 도구를 사용해보며 원인을 찾는 것도 꽤 즐거웠다. 조금 쉬다가 성능 개선에 시간을 좀 더 투자해봐야겠다.
프로젝트를 하면 편리하다는 이유로 라이브러리
를 많이 사용하게 된다.
라이브러리의 선택 기준은 편해서 혹은 팀원이 제안해서인 경우가 많았는데, 이번 삽질을 통해 라이브러리 선택 시 고려해야할 부분이 많다는 것을 느꼈다.
직접 분석해가며 라이브러리가 성능에 미치는 큰 영향에 대해 알 수 있었고, 편하다는 이유로 도입했다가 추후 성능 문제가 생기면 결코 편한게 아닌 더 많은 일을 하게 된다는 것도 알았다.
최소한 라이브러리를 사용하게 되면 사용한 사람들의 단점도 찾아보고, 내부가 어떤지는 한 번 들여다 볼 필요가 있다고 느꼈다. 또한 간단한 작업은 라이브러리가 아닌 직접 구현하는게 성능에 더 좋겠다는 생각도 들었다.
편하다는 이유로 라이브러리를 택했다가 나처럼 아이콘 컴포넌트 생성 코드를 작성해야하는 슬픈 일을 겪지 않기를 바라며 글을 마무리해야겠다.
사실 lodash가 대부분 잡아먹고잇던거시엿나여?