YEONSUI 버전 2의 세 번째 작업은 아이콘을 정의하는 것이다.
버전 1에서는 피그마에서 아이콘을 SVG로 저장하고 path 속성을 객체로 정리했다.
export const filledIcons = {
StepBackward: {
...defaultStyle,
svgdata: {
paths: [
{
d: 'M12.2203 18.5959L25.6922 29.1786C26.1932 29.5724 26.9297 29.2173 26.9297 28.5827V7.41691C26.9297 6.78234 26.1932 6.42762 25.6922 6.82137L12.2203 17.4041C12.1299 17.4749 12.0568 17.5654 12.0065 17.6686C11.9562 17.7718 11.9301 17.8852 11.9301 18C11.9301 18.1148 11.9562 18.2282 12.0065 18.3314C12.0568 18.4346 12.1299 18.5251 12.2203 18.5959ZM11.6016 30.375H9.35156C9.27697 30.375 9.20543 30.3454 9.15269 30.2926C9.09994 30.2399 9.07031 30.1683 9.07031 30.0938V5.90625C9.07031 5.83166 9.09994 5.76012 9.15269 5.70738C9.20543 5.65463 9.27697 5.625 9.35156 5.625H11.6016C11.6762 5.625 11.7477 5.65463 11.8004 5.70738C11.8532 5.76012 11.8828 5.83166 11.8828 5.90625V30.0938C11.8828 30.1683 11.8532 30.2399 11.8004 30.2926C11.7477 30.3454 11.6762 30.375 11.6016 30.375Z',
fill: 'black',
fillOpacity: '0.85',
},
],
},
},
StepForward: {
...
},
...
}
아이콘 이름을 Icon
컴포넌트의 props로 받아 해당 아이콘의 객체 값을 다시 path 요소에 넣는다.
const Icon = ({ icon, size, color, className = '' }: IconProps) => {
const { width: defWidth, height: defHeight, viewBox, fill, svgdata } = { ...filledIcons, ...outlinedIcons }[icon]
const [width, setWidth] = useState<string>(defWidth)
const [height, setHeight] = useState<string>(defHeight)
const contents = svgdata.paths.map((path: SvgPathType) => {
return <path key={path.d} d={path.d} fill={color ? color : path.fill} fillOpacity={path.fillOpacity} />
})
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
viewBox={viewBox}
fill={fill}
className={className}
>
{contents}
</svg>
)
}
export default Icon
<Icon icon="Heart" size="small" />
와 같이 아이콘 컴포넌트를 사용한다.
이렇게 아이콘 컴포넌트를 정의하면서 여러 번의 현타를 맞았다. 100개 이상의 아이콘에 해당하는 path 속성을 복사 붙여넣기를 반복하는 작업이 생각보다 오래 걸리고 손과 눈도 아팠다. 😵
그래서 버전 2에서는 아이콘 파일을 컴포넌트로 변환할 수 있도록 자동화를 도입하였다. 우선 작업 과정을 알아보자.
1. 아이콘 컴포넌트의 템플릿을 설정한다.
// src/assets/icon/utils/svgr-cli.template.js
function defaultTemplate({ template }, _, { componentName, jsx }) {
const typeScriptTpl = template.smart({ plugins: ['jsx', 'typescript'] })
const IconComponentName = componentName.name.slice(3)
return typeScriptTpl.ast`
import { SVGAttributes } from 'react';
interface IconProps extends SVGAttributes<SVGElement> {
size?: 12 | 16 | 20 | 24 | 32 | 40
fillColor?: string
}
export const ${IconComponentName} = ({ size = 20, fillColor = '#000', ...props }: IconProps) => {
return ${jsx}
}
`
}
module.exports = defaultTemplate
typeScriptTpl
: 템플릿을 TypeScript와 JSX를 지원하도록 설정한다. SVGR이 생성하는 컴포넌트가 TypeScript 문법을 준수하도록 한다.IconComponentName
:componentName.name
은 SVG 이름 앞에 Svg 접두사가 붙은 상태로 만들어진 컴포넌트 명이다. 접두사를 지우는 작업을 한다.typeScriptTpl.ast
: TypeScript 코드가 다음과 같이 적용된다.
2. 설정 파일을 만든다.
// src/assets/icon/utils/.svgrrc
{
"expandProps": "start",
"dimensions": true,
"replaceAttrValues": {
"#343330": "{fillColor}"
},
"svgProps": {
"width": "{size}",
"height": "{size}",
"viewBox": "0 0 24 24"
}
}
.svgrrc
파일은 SVGR CLI에서 SVG 파일을 변환할 때 사용하는 JSON 파일이다.
expandProps
: svg 속성을 확장하는 방식을 정한다. start는 기본 props보다...props
를 앞서 추가한다.dimensions
: SVG 파일의 width와 height 속성을 유지한다.replaceAttrValues
: 특정 값을 속성 값으로 대체하여 동적으로 값을 설정한다. SVG 파일에서 path 색상이 #343330으로 고정되어 있으므로fillColor
로 대체할 수 있도록 한다.svgProps
: 생성된 SVG 컴포넌트에 적용될 속성을 정의한다.
3. 생성된 컴포넌트를 내보내는 파일을 만든다.
// src/assets/icon/utils/generate-export.js
const fs = require('fs')
const path = require('path')
const defaultPath = path.join(__dirname, '../')
const iconFiles = fs.readdirSync(path.join(defaultPath, 'generated')).filter((file) => file.includes('tsx'))
const generatedCodes = [
iconFiles
.map((iconFile) => {
const fileName = iconFile.split('.')[0]
return `export { ${fileName} as ${fileName}Icon } from './generated/${fileName}';`
})
.join('\n'),
]
fs.writeFile(path.join(defaultPath, 'index.tsx'), generatedCodes.join('\n'), (error) => {
if (error) {
throw error
}
console.log('Saved successfully🎉')
})
generate-export.js
파일은 아이콘 컴포넌트를 자동으로 export하는 작업을 수행한다.
defaultPath
: 파일 시스템 모듈fs
와 경로 관련 모듈path
를 import한 후,__dirname
을 기반으로 상위 디렉터리로 이동하고generated
디렉터리 내의 생성된 컴포넌트들을 대상으로 작업한다.iconFiles
:generated
폴더에서.tsx
확장자를 가진 파일들을 목록화한다.generatedCodes
: 각 아이콘에 대해 export 코드 형식을 만든다.export 코드를
index.tsx
파일에 저장한 후, 성공하면 메세지를 출력하고 에러가 발생하면 에러를 던진다.
4. 스크립트가 수행할 작업을 작성한다.
cd svgs
npx @svgr/cli@5.5.0 --template ../utils/svgr-cli.template.js --config-file ../utils/.svgrrc *.svg --out-dir ../generated --ext tsx
cd ../utils
node generate-export.js
cd ../ && prettier --write .
터미널에 명령문을 입력하면 수행할 작업들을 순서대로 작성한다.
svgs
디렉터리로 이동하여 SVG 파일을 TypeScript React 컴포넌트로 변환한다.utils
디렉터리로 이동하여 생성된 컴포넌트를 index.tsx 파일로 export한다.프로젝트 전체에 prettier을 적용한다.
5. package.json에 스크립트 명령문을 추가한다.
"scripts": {
...
"icon": "cd src/assets/icon && sh generate-icon.sh",
},
npm run icon
을 실행하면 src/assets/icon
디렉터리로 이동한 후 generate-icon.sh
에 작성한 작업들을 수행한다.
이제 아이콘 SVG 파일을 svgs
디렉터리에 저장하고 npm run icon
실행만 하면 아이콘 컴포넌트가 만들어진다.
300+개의 아이콘을 저장했다. generated
디렉터리와 index.tsx
파일은 비어있다.
명령문을 실행하니 Saved successfully🎉 메세지가 뜨고 디렉터리에 수많은 컴포넌트가 저장되었다.
export const IconExample = () => {
return (
<>
<ArrowCounterClockwiseIcon fillColor="skyblue" size={12} />
<AirplaneTiltIcon fillColor="pink" size={16} />
<ArrowFatLeftIcon fillColor="green" />
<DropSimpleIcon fillColor="red" size={24} />
<ListStarIcon fillColor="orange" size={32} />
<WindIcon className="icon" size={40} />
</>
)
}
시각적인 테스트를 위해 스토리북으로 실행해보았다. 컴포넌트가 제대로 실행하는 걸 볼 수 있다!
앞으로 아이콘을 추가할 일이 생겨도 짜증내지 않고 2단계(파일 저장, 명령어 실행)만 거치면 되기 때문에 많은 시간과 에너지를 단축할 수 있게 되었다. 😎