프로젝트를 진행하며 svg 파일을 쓸 일이 되게 많은데 나는 svg 파일을 사용할 때마다 svgr lib를 사용해왔다. 그리고 대부분의 경우 icon은 사이즈나 색상이 정해져서 재사용되는 경우가 많았었다. 그런데 svgr을 사용하면 svg파일 자체를 component로 import하기 때문에 재사용 가능한 component로 만드는 것이 쉽지 않았었다.
예를 들어서 menu.svg
라는 파일이 있으면,
import MenuIcon from '/public/svgs/menu.svg'
...
const App = () => {
return <MenuIcon/>
}
export default App;
위와 같이 바로 컴포넌트로 import해서 사용할 수 있다. 기존보다는 사용하기 쉽지만, 바로 component로 import되기 때문에 어떻게 재사용 가능한 컴포넌트로 만들지 처음에는 잘 감이 잡히지 않았었다.
나는 Next JS의 TS 환경에서 작업을 진행했다.
먼저 package를 설치해준다!
# yarn
yarn add -D @svgr/webpack
# npm
npm install -D @svgr/webpack
개발 환경에 따라서 config 파일을 작성한다.
나는 https://www.timegambit.com/blog/solve/svg-usage-fix-bug 를 참고해서 작성했다.
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.svg$/,
use: ['@svgr/webpack'],
},
],
},
};
// next.config.js
module.exports = {
webpack: (config) => {
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack'],
});
return config;
},
};
public 폴더에 svgs 폴더를 만들어서 svg 파일을 모아준다.
icons라는 파일을 만들어서 svgr을 활용해서 svg 파일들을 component 형태로 import 해준다.
// icons/icons.ts
import IC_Calendar from "/public/svgs/ic_calendar.svg";
import IC_Cancel from "/public/svgs/ic_cancel.svg";
import IC_Delete from "/public/svgs/ic_delete.svg";
import IC_Edit from "/public/svgs/ic_edit.svg";
import IC_ExpiryDate from "/public/svgs/ic_expiry_date.svg";
import IC_Eye from "/public/svgs/ic_eye.svg";
import IC_LogIn from "/public/svgs/ic_log_in.svg";
import IC_LogOut from "/public/svgs/ic_log_out.svg";
import IC_Menu from "/public/svgs/ic_menu.svg";
import IC_MoreOptions from "/public/svgs/ic_more_options.svg";
import IC_Paste from "/public/svgs/ic_paste.svg";
import IC_Plus from "/public/svgs/ic_plus.svg";
import IC_Send from "/public/svgs/ic_send.svg";
import IC_Success from "/public/svgs/ic_success.svg";
그다음에 IconMap
이라는 객체를 만들어서 import한 svgr component들을 모두 담아준다.
export const IconMap = {
IC_Calendar,
IC_Cancel,
IC_Delete,
IC_Edit,
IC_ExpiryDate,
IC_LogIn,
IC_LogOut,
IC_MoreOptions,
IC_Paste,
IC_Plus,
IC_Send,
IC_Success,
IC_Menu,
IC_Eye,
} as const;
이렇게 되면 IconMap['IC_Calendar']
에는 <IC_Calendar/> 컴포넌트
가 담겨있게 된다.
그 다음에 IconMap
의 type
을 추출한다.
export type IconMapTypes = keyof typeof IconMap;
이렇게 하면 실제로는,
export type IconMapTypes = "IC_Calendar" | "IC_Cancel" | "IC_Delete" | ...;
처럼, IconMap
객체의 Key들을 type으로 가지게 된다.
또한 Icon의 크기도 미리 정의해서 동적으로 받을 수 있도록 객체를 정의했다.
export const IconSizes = {
lg: 24,
md: 20,
sm: 16,
};
export type IconSizeTypes = keyof typeof IconSizes;
// icons/icons.ts
import IC_Calendar from "/public/svgs/ic_calendar.svg";
import IC_Cancel from "/public/svgs/ic_cancel.svg";
import IC_Delete from "/public/svgs/ic_delete.svg";
import IC_Edit from "/public/svgs/ic_edit.svg";
import IC_ExpiryDate from "/public/svgs/ic_expiry_date.svg";
import IC_Eye from "/public/svgs/ic_eye.svg";
import IC_LogIn from "/public/svgs/ic_log_in.svg";
import IC_LogOut from "/public/svgs/ic_log_out.svg";
import IC_Menu from "/public/svgs/ic_menu.svg";
import IC_MoreOptions from "/public/svgs/ic_more_options.svg";
import IC_Paste from "/public/svgs/ic_paste.svg";
import IC_Plus from "/public/svgs/ic_plus.svg";
import IC_Send from "/public/svgs/ic_send.svg";
import IC_Success from "/public/svgs/ic_success.svg";
export const IconMap = {
IC_Calendar,
IC_Cancel,
IC_Delete,
IC_Edit,
IC_ExpiryDate,
IC_LogIn,
IC_LogOut,
IC_MoreOptions,
IC_Paste,
IC_Plus,
IC_Send,
IC_Success,
IC_Menu,
IC_Eye,
} as const;
export type IconMapTypes = keyof typeof IconMap;
export const IconSizes = {
lg: 24,
md: 20,
sm: 16,
};
export type IconSizeTypes = keyof typeof IconSizes;
// components/SVGIcon.tsx
interface SVGIconProps {
icon: IconMapTypes;
size?: IconSizeTypes;
className?: string;
}
const SVGIcon: React.FC<SVGIconProps> = ({
icon,
size = "lg",
className,
}: SVGIconProps) => {
const Icon = IconMap[icon as IconMapTypes];
return (
<Icon
className={className}
width={IconSizes[size]}
height={IconSizes[size]}
/>
);
};
export default SVGIcon;
SVGIcon component는 icon
, size
, className
이라는 props를 받는다.
icon
에는 "IC_Calendar"
와 같은 string 타입이 들어오는데,
const Icon = IconMap[icon as IconMapTypes];
여기서 IconMap[icon]
은 IconMap["IC_Calendar"]
와 같다.
즉 Icon
변수에는 아까 IconMap{}
객체에 넣었던 <IC_Calendar/>
컴포넌트가 들어가는 것이다.
그래서 현재 이 코드는,
return (
<Icon
className={className}
width={IconSizes[size]}
height={IconSizes[size]}
/>
);
Icon
에 <IC_Calendar/>
가 할당되어 있기 때문에,
return (
<IC_Calendar
className={className}
width={IconSizes[size]}
height={IconSizes[size]}
/>
);
위와 같은 형태로 동적으로 할당된다고 볼 수 있다.
또한 svgr component는 width
와 height
로 숫자를 받기 때문에 아까 정의해놓은 size 객체로 크기를 동적으로 할당해준다.
<Button onClick={() => setIsAuthorized(true)}>
<IC_Eye className="fill-white" width={24} height={24}/>
</Button>
<Button onClick={() => setIsAuthorized(true)}>
<SVGIcon icon={"IC_Eye"}/>
</Button>
이렇게 svg 파일을 component로 만들면 위와 같이 간단하게 사용할 수 있다. 지금 당장은 큰 차이가 없어보이지만, 관리적인 측면에서 좋은 것 같고 cva와 같이 사용하거나, 속성이 많아지면 많아질수록 컴포넌트화의 장점이 더 크다고 생각한다.
<Button onClick={() => setIsAuthorized(true)}>
<IC_Eye className="fill-white" width={24} height={24}/>
</Button>
주의할 점이 있다. svgr 컴포넌트에 className을 통해서 색상을 설정하려면 기본적으로 svg 파일 자체에 속성 변경이 필요하다.
svg 파일에 보면 width, height, fill
와 같은 속성들이 #000000
과 같은 헥스코드나 white
와 같은 색상 이름, 혹은 none
으로 설정되어 있다. 이 친구들을 current
로 바꿔주어야 우리가 svgr 컴포넌트에서 직접 속성을 주어 사용할 수 있다. 안그러면 백날 속성을 적용해도 바뀌지 않는다.
이렇게 바꿔주어야 작동한다.
svg 파일이 너무 많아서 모두 바꾸기 힘들 경우에는, 에디터에 있는 replace
기능을 적절하게 활용하면 쉽게 모든 값을 바꿀 수 있으니 참고하길 바란다!
그리고 모든 svg 파일을 import하는 일이 불필요한 자원을 소모한다고 생각해서 필요한 파일만 동적으로 import하는 Dynamic Import
- Lazy Loading
방법도 시도해봤는데, 동적으로 import하니까 component가 최초 렌더링시에 계속 한 박자 늦게 나오는 문제가 있어서 나는 포기했다.
끝!