최근에 Sprite SVG라는 기술을 이용해서 웹 성능을 최적화해봤어요.
왜 Sprite SVG를 선택했는지, 그리고 어떻게 Sprite SVG를 만들었는지 이야기해볼게요.
Sprite SVG는 여러 개의 SVG 이미지를 하나의 파일로 합친 거예요.
이름에서 알 수 있듯이, 여러 개의 작은 이미지(스프라이트)를 하나로 모아놓은 거죠.
이 기술은 보통 웹 성능 최적화와 효율적인 리소스 관리를 위해 사용해요
제가 프로젝트에 이걸 적용하게 된 가장 큰 이유는 네트워크 비용이었어요.
Sprite를 사용하지 않으면 이미지를 개별적으로 불러와야 하지만, Sprite SVG를 이용하면 여러 이미지를 한 번에 불러올 수 있거든요.
덕분에 HTTP 요청이 줄어들고, 대역폭도 절약할 수 있어요.
여러 개의 작은 파일 대신 하나의 큰 파일을 다운로드하는 게 일반적으로 더 효율적이에요.
물론 단점도 있어요.
이런 장단점을 고려해봤을 때, 제 상황에는 Sprite SVG가 꽤 적합한 방법이라고 판단했어요.
FCP를 고려하지 않아도 되는 부분이었고, 한 페이지에 모든 SVG를 보여줄 예정이었거든요.
그래서 초기 로딩 시간이나 사용하지 않는 아이콘 로드 같은 단점은 크게 문제가 되지 않았어요.
Sprite SVG를 만드는 과정은 생각보다 간단해요.
기본적으로 개별 SVG 파일들을 읽어와서 하나의 큰 SVG 파일로 합치는 거예요.
전체 과정은 아래와 같아요.
1. SVG 파일 읽기
2. 각 SVG를 <symbol>
태그로 변환
3. 모든 <symbol>
을 하나의 <svg>
태그 안에 모으기
먼저, 외부에 있는 SVG 파일들을 읽어와야 해요. (내부에 있는 SVG를 읽어올 수도 있지만 제 경우엔 외부에서 읽어와야 했어요.)
const getSvgFiles = async (spriteInfo: SpriteInfoValue): Promise<SvgFile[]> => {
const svgFiles = await Promise.allSettled(
spriteInfo.list.map(async (item) => {
try {
const res = await axios.get(`${spriteInfo.svgBaseUrl}/${item}.svg`);
return {
name: item,
data: res.data,
};
} catch (error) {
console.error(`❌ - Failed to fetch SVG for ${item}`);
}
})
);
return svgFiles
.filter((result): result is PromiseFulfilledResult<SvgFile> => result.status === 'fulfilled')
.map((result) => result.value);
};
이 코드는 주어진 URL에서 SVG 파일들을 가져와요. 파일을 가져오는 동안 병렬 처리를 위해 Promise.allSettled
를 사용했어요.
Promise.allSettled
를 이용하면 일부 파일에 문제가 있어도 전체 프로세스가 멈추지 않아요. 특히 많은 SVG 파일을 처리할 때 유용하죠. 하나의 파일 로딩이 실패해도 나머지 파일들은 계속 처리할 수 있거든요. 😌
이제 읽어온 SVG 파일들을 하나의 Sprite SVG로 만들어 볼게요.
const getSpriteSvg = async (svgFiles: SvgFile[]) => {
const symbols: string[] = [];
if (svgFiles.length === 0) {
throw new Error('❌ - No SVG files fetched');
}
svgFiles.forEach((file) => {
const svgElement = parse(file.data).querySelector('svg') as HTMLElement;
const symbolElement = parse('<symbol/>').querySelector('symbol') as HTMLElement;
svgElement.childNodes.forEach((child) => symbolElement.appendChild(child));
symbolElement.setAttribute('id', file.name);
if (svgElement.attributes.viewBox) {
symbolElement.setAttribute('viewBox', svgElement.attributes.viewBox);
}
symbolElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
symbolElement.setAttribute('fill', 'none');
symbols.push(symbolElement.toString());
});
const svgSprite = `<svg width="0" height="0" class="hidden">${symbols.join('')}</svg>`;
return svgSprite;
};
이 코드는 각 SVG 파일을 <symbol>
태그로 변환하고, 모든 symbol을 하나의 <svg>
태그 안에 모아요. 이렇게 하면 각 SVG를 id로 구분할 수 있게 돼요.
여기서 중요한 점은 viewBox
속성을 유지한다는 거예요. 이 속성은 SVG의 뷰포트를 정의하는데, 이를 유지함으로써 각 아이콘의 원래 비율과 크기를 보존할 수 있어요. 또한 fill
속성을 'none'
으로 설정해 기본 색상을 제거하고, 나중에 CSS로 쉽게 색상을 변경할 수 있게 했어요.
Sprite SVG를 만들었다면, 이제 사용할 차례예요! 아래와 같이 간단하게 사용할 수 있어요.
<svg width={size} height={size} fill="currentColor">
<use href={`/assets/sprite/countryCode.svg#${id}`} />
</svg>
이 코드는 <use>
태그를 사용해 sprite SVG 파일 내의 특정 symbol을 참조해요. href
속성에서 #
뒤에 오는 값이 바로 우리가 앞서 설정한 id예요.
좀 더 편리하게 사용하기 위해 컴포넌트로 만들어봤어요.
type CountryCodeSpriteSvgProps = {
id: string;
size?: number;
};
function CountryCodeSpriteSvg({ id, size = 28 }: CountryCodeSpriteSvgProps) {
return (
<svg width={size} height={size} fill="currentColor">
<use href={`/assets/sprite/countryCode.svg#${id}`} />
</svg>
);
}
이렇게 컴포넌트로 만들면 여러 가지 장점이 있어요. (지극히 개인적인 생각입니다 🤔)
Sprite SVG는 처음에는 좀 복잡해 보였지만, 익숙해지니까 정말 유용한 기술이라고 생각해요. 특히 아이콘을 많이 사용하는 프로젝트에서 성능 향상에 큰 도움이 될 것 같아요.
이 기술을 사용하면서 느낀 점은, 개발자로서 항상 성능과 사용성 사이의 균형을 고민해야 한다는 거예요. Sprite SVG는 그 균형을 잘 맞춘 기술 중 하나라고 생각해요.
다음에는 script를 이용해 자동으로 sprite SVG를 생성하는 방법에 대해 알아볼게요.
저는 script를 이용해서 복잡해 보일 수 있는 sprite SVG 생성 과정을 간단하고 효율적으로 만들 수 있었어요. 🚀
다음 글: [[sprite 2, 스크립트로 Sprite SVG 자동 생성하기]]