이번주는 icon 자동화를 해보는 시간을 가졌다.
블로그에서 소개한 icona를 이용하면 피그마에서 icon을 추출하여 깃허브에 있는 레포지토리로 가져올 수 있다.
사용 방법에 대해 간단히 알아보고자 한다.
우선 피그마 플러그인에서 icona를 실행시켜준다.

위와 같이 icon을 추출할 깃허브 레포지토리 주소와 깃허브 토큰을 넣어주어야 한다.
깃허브 토큰 생성 방법
우선 프로필 → Settings에 들어간다.

왼쪽 메뉴 하단에 Developer Settings에 들어간다.

Personal access tokens에 있는 Tokens (classic)에 들어간다.

Generate new token (classic)을 이용하여 새로운 토큰을 생성한다.

이름을 설정해 주고 아래와 같이 체크를 해준다.

생성이 완료되면 토큰이 나오는데 토큰은 한 번만 나오니 꼭! 어딘가에 저장해 두어야한다.
생성된 토큰을 위 icona에 넣어준다.
피그마에서 frame을 생성(아무거나 해도 상관 없음!) 후 frame 이름을 icona-frame로 바꿔준다.
그런 다음 추출하고자 하는 icon들을 frame 안에 넣어준다.

플러그인에서 Deploy를 보면 아래 사진과 같이 나온다.

Deploy를 누르게 되면 레포지토리에 자동으로 pull request가 생성된다.

해당 pull request를 합치고 나면 아래와 같이 파일이 생성된다.

이런식으로 피그마에서 svg를 추출할 수 있다.
해당 레포지토리는 터보레포를 이용한 모노레포로 구성되어 있다.
// index.js
import fsPromise from "node:fs/promises";
import fs from "node:fs";
import path from "node:path";
import generateComponent from "./utils/generate-component.js";
async function main() {
try {
// 1️⃣ icons.json 파일 읽기
const result = await fsPromise.readFile("../../.icona/icons.json", "utf8");
// 2️⃣ 읽어들인 결과 JSON 형태로 변환
const svgs = JSON.parse(result);
// 3️⃣ 만들어진 icon 컴포넌트가 쓰여질 디렉토리 경로
const icondirPath = path.resolve("./src/icons");
// 디렉토리가 존재하지 않는다면 생성
if (!fs.existsSync(icondirPath)) {
await fsPromise.mkdir(icondirPath);
}
// 4️⃣ 모든 svg 파일을 컴포넌트로 생성
await Promise.all(Object.entries(svgs).map(([name, value]) => generateComponent(name, value.svg)));
} catch (err) {
console.error(err);
}
}
main();
// generate-component.js
import fs from "node:fs";
import prettier from "prettier";
/**
* 컴포넌트를 생성하여 파일로 저장합니다.
* @param {string} name - SVG 이름
* @param {string} svg - SVG 내용
*/
const generateComponent = async (name, svg) => {
const pascalCase = name[0].toUpperCase() + name.split("").splice(1).join("");
// size와 fill 변수를 이용하기 위해
// 고정되어있는 width, height, fill을 변수화 시키는 과정
// 단, icons.json에 있는 svg의 크기가 24이고, 색상이 black이라는 가정하에 적용됨
const replacedSvg = svg.replace(/"24"/g, "{size}").replace(/fill="black"/g, "fill={fill}");
// 함수 내용
const componentContent = `interface ${pascalCase}Props {
size?: number;
fill?: string;
}
function ${pascalCase}({ size = 24, fill = "black" }: ${pascalCase}Props): React.ReactElement {
return (
${replacedSvg});
}
export default ${pascalCase};
`;
// 함수 내용을 포멧팅하는 작업
const formattedComponent = await prettier.format(componentContent, {
parser: "typescript",
});
fs.writeFile(`./src/icons/${name}.tsx`, formattedComponent, "utf-8", (err) => {
if (err) throw err;
});
};
export default generateComponent;
// fire.tsx
interface FireProps {
size?: number;
fill?: string;
}
function Fire({ size = 24, fill = "black" }: FireProps): React.ReactElement {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g id="fire">
<path
id="Vector"
d="M17.66 11.2C17.43 10.9 17.15 10.64 16.89 10.38C16.22 9.78 15.46 9.35 14.82 8.72C13.33 7.26 13 4.85 13.95 3C13 3.23 12.17 3.75 11.46 4.32C8.87 6.4 7.85 10.07 9.07 13.22C9.11 13.32 9.15 13.42 9.15 13.55C9.15 13.77 9 13.97 8.8 14.05C8.57 14.15 8.33 14.09 8.14 13.93C8.08325 13.8825 8.03578 13.8248 8 13.76C6.87 12.33 6.69 10.28 7.45 8.64C5.78 10 4.87 12.3 5 14.47C5.06 14.97 5.12 15.47 5.29 15.97C5.43 16.57 5.7 17.17 6 17.7C7.08 19.43 8.95 20.67 10.96 20.92C13.1 21.19 15.39 20.8 17.03 19.32C18.86 17.66 19.5 15 18.56 12.72L18.43 12.46C18.22 12 17.66 11.2 17.66 11.2ZM14.5 17.5C14.22 17.74 13.76 18 13.4 18.1C12.28 18.5 11.16 17.94 10.5 17.28C11.69 17 12.4 16.12 12.61 15.23C12.78 14.43 12.46 13.77 12.33 13C12.21 12.26 12.23 11.63 12.5 10.94C12.69 11.32 12.89 11.7 13.13 12C13.9 13 15.11 13.44 15.37 14.8C15.41 14.94 15.43 15.08 15.43 15.23C15.46 16.05 15.1 16.95 14.5 17.5Z"
fill={fill}
/>
</g>
</svg>
);
}
export default Fire;
성공적으로 만들어진 것을 볼 수 있다.
현재 storybook은 apps/docs에 위치해 있고, icons를 컴포넌트로 추출하여 담아두는 곳은 packages/ui에 있다.