반응형

개발을 진행하면서 가끔 Icon 추가나 변경이 이루어질때가 있는데

그럴때마다 아래 과정을 거쳤다.

1. 디자이너가 피그마에서 업데이트한 아이콘을 내려받은 후, 이를 압축합니다.
2. 사내 메신저에 업로드하고, 엔지니어에게 공유합니다.
3. 엔지니어는 이를 내려받아 압축을 풀고, 소스 코드에 적절히 추가합니다.
4. PR을 올려 코드 리뷰 후 머지합니다.
// (https://channel.io/ko/blog/figma-icon-plugin) 에서 발췌

 

그래서 피그마에서 아이콘 업데이트가 이뤄진다면 코드에서 바로 다운받을 수는 없을까? 생각했고,

찾아본 결과 피그마 plugin을 발견하여 직접 써본 후 사용 방식을 공유해본다.

 

사용방법

 

1. 피그마 access token 추가

 

 

figma setting > personal access token에서 토큰을 생성한다. 

피그마 web api를 사용하려면 access token을 같이 보내줘야 한다.

 

2. node-fetch 설치

pnpm add -D node-fetch //pnpm, yarn, npm 등 사용하는걸로 설치

https://www.npmjs.com/package/node-fetch

 

node-fetch 라이브러리를 devDependency에 추가한다.

프론트 환경이든 백엔드 환경이든 dev 환경에서만 돌릴꺼이므로(node를 사용하므로) 크게 상관은 없다.

 

설치 후 

   "icon": "node ./src/icons/generator.mjs"

 

packages.json에서 아래 3번의 파일을 실행하면 피그마 icon들이 자동생성되게 구현할 것이다.

 

3. Icon들을 다운받는 generator.mjs 파일 생성

 

여담으로  나는 import를 쓰고 싶은데 ESM 환경이 아니라서 mjs로 명시했는데

뭐 굳이 esm을 사용하고 싶지 않으면 .mjs 확장자를 쓰지 않아도 되긴 한다. 

 

코드는 https://medium.com/iadvize-engineering/using-figma-api-to-extract-illustrations-and-icons-34e0c7c230fa 에서

약간의 수정을 거친 코드이다.

 

전체 코드 보기

더보기
/*
 reference:
  https://medium.com/iadvize-engineering/using-figma-api-to-extract-illustrations-and-icons-34e0c7c230fa
*/


import dotenv from 'dotenv'
import { appendFileSync, writeFileSync } from 'fs'
import fetch from 'node-fetch'


dotenv.config()


const TOKEN = process.env.FIGMA_WEBHOOK
const FILE_KEY = 'v2LAIwRuECBSb24aIFDKwB'


const fetchFigmaFile = (key) => {
  return fetch(`https://api.figma.com/v1/files/${key}`, { headers: { 'X-Figma-Token': TOKEN } }).then((response) =>
    response.json(),
  )
}


const flatten = (acc, cur) => [...acc, ...cur]


const getComponentsFromNode = (node) => {
  if (node.type === 'COMPONENT') {
    return [node]
  }
  if ('children' in node) {
    return node.children.map(getComponentsFromNode).reduce(flatten, [])
  }
  return []
}


const formatIconsSVG = (svg) => svg.replace(/fill="(?:#[a-fA-F0-9]{6}|none)"/gm, 'fill="currentColor"')


const formatName = (name) => name?.toUpperCase().replace(/-/g, '_') // replaces '/' by '_'


const hash = (path) => path.replace(/^.*\/img\//g, '').replace(/\//g, '_')


const generateFiles = (ele) => {
  if (!ele) return ''


  const { name, fileName, svg } = ele
  const component = `
  import * as React from "react";


  const ${name} = (props: React.SVGProps<SVGSVGElement>) => {
    return (${svg.replace(/<svg /, '<svg {...props} ')});
  }


  export default ${name};
  `


  writeFileSync(`./src/icons/${name}.tsx`, component)
  return `${name}`
}


const getSVGsFromComponents = (components) => {
  const key = FILE_KEY
  const filteredComponent = components.filter(({ name }) => name?.toUpperCase().startsWith('ICON'))
  const ids = filteredComponent.map(({ id }) => id)


  return fetch(`https://api.figma.com/v1/images/${key}?ids=${ids.join()}&format=svg`, {
    headers: { 'X-Figma-Token': TOKEN },
  })
    .then((response) => response.json())
    .then(({ images }) =>
      Promise.all(
        filteredComponent.map(
          ({ id, name, type }) =>
            images[id] &&
            fetch(images[id])
              .then((response) => response.text())
              .then((svg) => ({
                name: formatName(name),
                fileName: hash(images[id]),
                svg: formatIconsSVG(svg),
              })),
        ),
      ),
    )
}


async function run() {
  if (!TOKEN) {
    console.error(
      'The Figma API token is not defined, you need to set an environment variable `FIGMA_API_TOKEN` to run the script',
    )
    return
  }
  fetchFigmaFile(FILE_KEY)
    .then((data) => getComponentsFromNode(data.document))
    .then(getSVGsFromComponents)
    .then((dataArray) => dataArray.map(generateFiles))
    .then((ele) => Array.from(new Set(ele)))
    .then((texts) => {
      writeFileSync(
        './src/icons/index.ts',
        texts.reduce((t, v) => `${t}\n import ${v} from './${v}'`, ''),
      )


      appendFileSync('./src/icons/index.ts', texts.reduce((t, v) => `${t} ${v},`, '\n\n export {').slice(0, -1))


      appendFileSync('./src/icons/index.ts', '}')
    })
}


run()

 

혹시 copy가 필요한 경우는 아래 gist에서 전체 코드를 볼 수 있다.

https://gist.github.com/lodado/24da180db5042ff1bb5b20b1527d5e33

 

icon 자동 설치

icon 자동 설치. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

 

전체 코드는 아래와 같은데

async function run() {
  if (!TOKEN) {
    console.error(
      'The Figma API token is not defined, you need to set an environment variable `FIGMA_API_TOKEN` to run the script',
    )
    return
  }
  fetchFigmaFile(FILE_KEY)
    .then((data) => getComponentsFromNode(data.document))
    .then(getSVGsFromComponents)
    .then((dataArray) => dataArray.map(generateFiles))
    .then((ele) => Array.from(new Set(ele)))
    .then((texts) => {
      writeFileSync(
        './src/icons/index.ts',
        texts.reduce((t, v) => `${t}\n import ${v} from './${v}'`, ''),
      )

      appendFileSync('./src/icons/index.ts', texts.reduce((t, v) => `${t} ${v},`, '\n\n export {').slice(0, -1))

      appendFileSync('./src/icons/index.ts', '}')
    })
}

run()

 

진행 과정을 한줄 한줄 차근차근 보자.

 

 

1. 피그마 api 접근

import dotenv from 'dotenv'
import { appendFileSync, writeFileSync } from 'fs'
import fetch from 'node-fetch'

dotenv.config()

const TOKEN = process.env.FIGMA_WEBHOOK
const FILE_KEY = 'v2LAIwRuECBSb24aIFDKwB'

const fetchFigmaFile = (key) => {
  return fetch(`https://api.figma.com/v1/files/${key}`, { headers: { 'X-Figma-Token': TOKEN } }).then((response) =>
    response.json(),
  )
}

 

나는 dotenv를 써서 아까 받은 피그마 access token을 .env에 저장하고, api 요청을 할때 token을 담아 보내게 작성했다.

FILE_KEY는 피그마 프로젝트에 들어가서 내가 받아오고 싶은 프로젝트의 file key를 쓰면 된다. 

 

figma.com/file/${file_key}&nbsp; <- 이부분!

 

2. 받아온 api 전처리

const flatten = (acc, cur) => [...acc, ...cur]

const getComponentsFromNode = (node) => {
  if (node.type === 'COMPONENT') {
    return [node]
  }
  if ('children' in node) {
    return node.children.map(getComponentsFromNode).reduce(flatten, [])
  }
  return []
}

피그마엔 Component와 frame?등 컴포넌트를 분류하는 기준이 있는데,

개발과는 크게 상관이 없는 부분이니 사용하기 쉽게 모두 전처리해서 다음 부분으로 넘긴다. 

 

3. SVG 파일(Icon들만) 받아오기

const formatIconsSVG = (svg) => svg.replace(/fill="(?:#[a-fA-F0-9]{6}|none)"/gm, 'fill="currentColor"')

const formatName = (name) => name?.toUpperCase().replace(/-/g, '_') // replaces '/' by '_'

const hash = (path) => path.replace(/^.*\/img\//g, '').replace(/\//g, '_')

const getSVGsFromComponents = (components) => {
  const key = FILE_KEY
  const filteredComponent = components.filter(({ name }) => name?.toUpperCase().startsWith('ICON'))
  const ids = filteredComponent.map(({ id }) => id)

  return fetch(`https://api.figma.com/v1/images/${key}?ids=${ids.join()}&format=svg`, {
    headers: { 'X-Figma-Token': TOKEN },
  })
    .then((response) => response.json())
    .then(({ images }) =>
      Promise.all(
        filteredComponent.map(
          ({ id, name, type }) =>
            images[id] &&
            fetch(images[id])
              .then((response) => response.text())
              .then((svg) => ({
                name: formatName(name),
                fileName: hash(images[id]),
                svg: formatIconsSVG(svg),
              })),
        ),
      ),
    )
}

 

피그마 프로젝트의 img 중에서 format이 svg인 이미지들만 전부 불러온다.

(당연히 필요한 icon이 다른 포멧이라면 해당 포멧도 추가해야한다.)

 

이때 모든 svg를 불러오게 되는데, Icon만 선별하기 위해서 디자이너 분에게 Icon은 모두 이름앞에 Icon prefix를 붙여달라고 요청했다.

아마 피그마에서 분류하는 다른 좋은방법이 있을거 같은데.. 

일정이 급해서 시간상 해당 방법으로 구현해달라고 요청했다.

 

여담으로 피그마에서 component 지정을 하지 않으면 위 api에서 나타나지 않는듯 싶다.

 

 

4. React Component로 변환

const generateFiles = (ele) => {
  if (!ele) return ''

  const { name, fileName, svg } = ele
  const component = `
  import * as React from "react";

  const ${name} = (props: React.SVGProps<SVGSVGElement>) => {
    return (${svg.replace(/<svg /, '<svg {...props} ')});
  }

  export default ${name};
  `

  writeFileSync(`./src/icons/${name}.tsx`, component)
  return `${name}`
}

그냥 ${name}.svg로 저장하는 방법도 있긴 한데 어차피 react component로 mapping 하는 과정이 필요해서 

아예 react component로 아이콘을 저장하게 생성한다. 

 

생성된 아이콘들 예시

5. 자동 export 

 

// run function의 마지막 부분)

.then((texts) => {
      writeFileSync(
        './src/icons/index.ts',
        texts.reduce((t, v) => `${t}\n import ${v} from './${v}'`, ''),
      )

      appendFileSync('./src/icons/index.ts', texts.reduce((t, v) => `${t} ${v},`, '\n\n export {').slice(0, -1))

      appendFileSync('./src/icons/index.ts', '}')
 })

 

import & export 하기 쉽도록 index.tsx에 import 후 export하는 파일을 작성한다.

자동생성 index.tsx 파일

해당 과정을 거치면 위와 같은 파일이 생성된다.

 

6. 끝!

 

혹시 storybook에서 보고 싶다면 추가 icon도 자동으로 import하게 

import * as Icons from 'icon 폴더 주소' 로 구현한다.

 

import type { Meta, StoryObj } from '@storybook/react'
import { FooterNav } from 'myll-ui'
import Image from 'next/image'
import * as Icons from 'shared'

// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta<typeof FooterNav> = {
  title: 'Example/Icon',
  argTypes: {},
}

export default meta

const ICONS = Object.entries(Icons)
  .filter(([key, value]) => key.startsWith('ICON'))
  .map(([key, value]) => {
    return { key, IconComponent: value }
  })

export const IconExamples = () => {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
      색깔은 fill, color를 통해 맞추세요
      {ICONS.map(({ key, IconComponent }) => {
        return (
          <div style={{ display: 'flex', flexDirection: 'row', width: '100%' }}>
            <div style={{ width: '280px' }}>{key} :</div> <IconComponent />
          </div>
        )
      })}
    </div>
  )
}

 

storybook 예시

 

위 icon들은 storybook이 연동된 사이드 프로젝트의 github-pages에서 볼 수 있다.

https://myll-github.github.io/myll-frontend/?path=/story/example-icon--icon-examples 

 

@storybook/cli - Storybook

 

myll-github.github.io

 

자동 업데이트를 통해서 끔찍한 파일 다운 & 저장 노가다에서 해방된 듯 싶다..?

webhook을 통하여 아이콘 업데이트마다 자동 PR을 날리거나 다른 설정도 더 해줄 수 있는것 같지만

토의 결과 아이콘이 한번 업데이트 된 이후에는 수정이 없을것으로 생각되어 시도하진 않았다. 

 

reference 

 

https://channel.io/ko/blog/figma-icon-plugin

 

피그마 플러그인으로 아이콘 업데이트 자동화하기

안녕하세요 👋, 채널톡 웹팀의 에드입니다. 채널톡엔 베지어(Bezier)라는 디자인 시스템이 있습니다. 저희 웹팀에서는 이 디자인 시스템의 React 구현체인 bezier-react 라는 오픈소스 라이브러리를

channel.io

위글에 영감을 받아서 제작했는데 webhook을 통한 자동 PR까진 만들지는 않았다.

 

https://medium.com/iadvize-engineering/using-figma-api-to-extract-illustrations-and-icons-34e0c7c230fa

 

반응형

+ Recent posts