붉은거위 노트 (redgoose note)

Lucide 아이콘 팩

Nest
Development
Category
Javascript
Hit
14
Star
0

꽤 오랫동안 아이콘 팩들을 찾아해매고 있었다.

기능적이면 하나도 이쁘지도 않았고, 이쁘면 기능적으로 부족하기 때문에 적절한 아름다움과 기능성을 갖춘 아이콘 아이콘팩을 오랫동안 비용을 들이면서 찾아 해매고 있었다.
마침내 가장 적절한것을 찾았는데 Feather 아이콘팩이며 여태까지 사용해오고 있었다.

종류에서 한계를 느끼긴 했지만 다른 대안이 없었기 때문에 계속 사용해오고 있었다.
Feather 아이콘의 특징은 취향을 크게 타지않는점과 선으로 만들어져 있어서 굵기를 자유롭게 변경 가능했다. 색 변경은 기본이고 말이다.

lucide-logo.webp

최근에 Feather 아이콘 베이스로 만들어진 오픈소스 아이콘팩 프로젝트인 Lucide를 알게 되었다.

https://lucide.dev/

종류가 대단히 많고, 기원과 관리가 대단히 잘되어 있다는것을 느낀다.

많은 흥미가 생기고 꼭 사용해보자는 마음에 현재 사이드 프로젝트에서 사용하고 있던 Feather 아이콘팩에서 교체해보기로 했다.

적용해보기

최근에 사이드로 오픈소스 프로젝트를 만들고 있는 바구니에다 먼저 적용해보게 되었다.

막상 사용하려고 문서들을 읽어보고 패키지를 설치해보고 적용해보니 평소에 선호하는 방식대로 사용하는것이 쉽지 않았다.
기본적으로 사용하는 방식이 컴포넌트를 import로 직접 불러와서 바로 사용하는 것인데 여러가지 불편함과 유연함의 한계의 이유로 아이콘을 감싸는 래퍼 컴포넌트를 별도로 만들어 사용하는 편이다.

아이콘 팩은 갯수가 대단히 많기 때문에 성능에 대단히 민감하게 느끼기 때문에 성능적으로 부담없는 선택을 꼭 우선해야하는 옵션이라서 여러가지로 까다롭다.

현재까지 가장 선호하는 옵션은 래핑 컴포넌트에서 <svg/>엘리먼트를 감싸고 알맹이가 되는 svg 코드들을 아이콘을 집어넣는 방식을 쓴다. 코드를 직접 집어넣기도 하고 # 링크를 걸어 연결시키기도 한다.

<svg xmlns="http://www.w3.org/2000/svg">
  {ICON_CODE}
</svg>

이런 구성을 고집하는 이유는 스타일의 유연함 때문이다.

css varialble로 아이콘의 옵션을 컴포넌트 props를 통하지 않고 조절할 수 있다.
겉으로 보면 컴포넌트 props로 사이즈나 색같은 스타일 값을 조절하는것이 편리해 보이지만 값 조절에 가장 큰 한계는 css media query다.
UI라는것은 대단히 유기적인 장르라고 볼 수 있는데 css에서 네이티브하게 대응할 수 있는데 자바스크립트까지 써가며 대응하는것은 굉장한 손실만 불러일으킨다.

좀 고민을 하고 삽질끝에 래핑 컴포넌트를 만들게 되었다.

vue 래핑 컴포넌트

lucide 프로젝트에서 딱 원하는 형태로 리소스가 제공되어있지 않아서 lucide 프로젝트에서 사용하는 소스코드들을 뜯어보면서 원하는 형태로 개조해보니 상당히 원하는 형태로 나왔다.

<template>
<svg
  v-if="icon"
  v-html="icon"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
  :class="[
    'icon',
    `icon--${props.name}`,
    props.animation && `icon--animation-${props.animation}`,
  ]"
  v-bind="wrapProps"/>
</template>

<script setup>
import { computed } from 'vue'
import { icons } from 'lucide'

const props = defineProps({
  name: String,
  color: String,
  size: String,
  stroke: Number,
  animation: String,
  animationSpeed: String,
})

const icon = computed(() => {
  let src = icons[toPascalCase(props.name)]
  if (!src) return null
  const [ tag, attrs, children ] = src
  const element = createSvgElement(tag, attrs, children)
  return element.innerHTML
})

/**
 * 카멜케이스 문자로 변경
 * @param {string} str
 * @return {string}
 */
export function toPascalCase(str)
{
  return str.replace(/(\w)(\w*)(_|-|\s*)/g, (g0, g1, g2) => g1.toUpperCase() + g2.toLowerCase())
}

function createSvgElement(tag, attrs, children = [])
{
  const element = document.createElementNS("http://www.w3.org/2000/svg", tag)
  Object.keys(attrs).forEach((name) => {
    element.setAttribute(name, String(attrs[name]))
  })
  if (children.length)
  {
    children.forEach((child) => {
      const childElement = createSvgElement(...child)
      element.appendChild(childElement)
    })
  }
  return element
}

const wrapProps = computed(() => {
  let attr = {
    style: {},
  }
  if (props.color) attr.style['--icon-color'] = props.color
  if (props.size) attr.style['--icon-size'] = props.size
  if (props.stroke) attr.style['--icon-stroke'] = props.stroke
  if (props.animationSpeed) attr.style['--icon-animation-speed'] = props.animationSpeed
  return attr
})
</script>

<style lang="scss" scoped>
.icon {
  display: block;
  margin: var(--icon-margin, 0);
  color: var(--icon-color, var(--color-base));
  width: var(--icon-size, 24px);
  height: var(--icon-size, 24px);
  stroke-width: var(--icon-stroke, 2);
  animation: var(--icon-animation, none);
  transition: var(--icon-transition, none);
  &--animation-rotate {
    --icon-animation: spin var(--icon-animation-speed, 2000ms) linear infinite;
  }
}
</style>

출처: https://github.com/redgoose-dev/baguni/blob/main/src/components/icons/index.vue

구현의 핵심은 createSvgElement() 함수와 스타일시트 부분들인데 요령껏 변형시킬 수 있도록 참하하기 위하여 이렇게 노트로 기록하는 것이다.
아마도 svelte 같은것도 얼추 비슷하게 만들 수 있을것이다.