AI가 발전되어
이제 canvas나 svg tag를 직접 조작할수 있는 레벨까지 온거 같네요.
이 글에서는 svg에 대해 간단히 설명하고 "Figma"에서 svg를 직접 추출해내어 react로 수작업으로 progressbar를 만들며 간단한 인터렉션을 넣어봅시다.
SVG 간단 설명
SVG는 Scalable Vector Graphics의 약자로, 크기를 늘리거나 줄여도 화질이 깨지지 않는 벡터 이미지 포맷이에요
쉽게 말해, 그림을 수학적인 좌표와 도형으로 표현하는 언어이지요. 그래서 그림을 태그(tag)와 속성으로 작성하게 되는데, HTML이 <div>나 <p> 같은 태그로 문서를 구조화하듯, SVG도 여러 태그를 사용해요. 겁먹을 필요 없어요. 하나씩 알아볼까요?
- <svg>: 모든 SVG 코드의 뿌리, 도화지 역할을 하는 태그예요. 이 태그 안에 우리가 그리고 싶은 도형들을 넣으면 돼요.
- <g>: 그룹(group)을 만드는 태그예요. 여러 도형을 <g>로 묶으면 한꺼번에 이동하거나 스타일을 같이 적용하기 편해요. 폴더처럼 묶어준다고 생각하면 쉬워요.
- <rect>: 사각형(rectangle)을 그리는 태그예요. x, y 좌표와 width, height 속성을 주면 해당 위치에 네모를 그려줘요. 색을 채우고 싶다면 fill 속성으로 색상을 지정하면 돼요.
- <path>: 가장 만능 도형 그리기 태그예요. 직선, 곡선 등 복잡한 모양은 <path>의 d 속성에 특별한 문자열(그림 그리는 명령어들)을 넣어서 표현해요. 마치 점 잇기 놀이로 그림을 그린다고 상상해보세요.
- <linearGradient>: 예쁘게 색을 섞어주는 그라디언트(gradient) 효과를 정의하는 태그예요. 시작 색부터 끝 색까지 점차 변하는 색깔띠를 만들 수 있어요. 이 태그는 주로 <defs> 안에 넣어서 정의하고, 나중에 도형에서 fill="url(#그라디언트ID)"처럼 불러내 써요.
- <stop>: 방금 말한 그라디언트에서 색이 바뀌는 지점을 지정하는 태그예요. offset 속성으로 위치(0%~100%)를 정하고 stop-color로 색을 정해요. 여러 개의 <stop>을 넣으면 색이 여러 번 변할 수도 있어요.
- <clipPath>: 그림을 자르는 액자 같은 역할이에요. <clipPath> 안에 특정 모양을 그려놓고 다른 도형에 적용하면, 그 모양 안쪽 부분만 보이고 바깥 부분은 잘려 보이게 돼요. 꼭 종이를 오려서 겹쳐놓은 것처럼요.
- <mask>: 마스크는 투명한 필름지처럼 부분 투명하거나 가려주는 효과를 줄 때 써요. 예를 들어, <mask> 안에 검정~흰색 그라디언트를 넣고 어떤 이미지에 씌우면, 천천히 사라지는 페이드 효과를 낼 수 있어요.
- <filter>: 블러(흐리게)나 그림자 같은 특수 효과를 줄 때 사용해요. 포토샵의 필터를 떠올리시면 돼요. <filter> 안에 종류와 강도를 지정하고 도형에 적용하면, 그림자가 생기거나 반짝거리게 만들 수도 있어요.
어때요, 하나하나 보니까 생각보다 할 만하지요? 😀 이 밖에도 <circle>로 원 그리기, <text>로 글자 쓰기 등 많은 태그가 있지만, 위에 소개한 태그들만 알아도 SVG의 핵심은 대부분 배운 셈이에요. 이제 실제로 SVG를 어떻게 활용하는지 알아볼까요?
Figma에서 SVG 추출하기

Figma로 멋진 이미지를 만들었다고 가정해봐요. 그 이미지를 그대로 우리 React 앱에 가져오려면 SVG 파일로 추출하면 편해요. 방법은 정말 간단해요. Figma에서 원하는 객체를 선택하고 오른쪽 Export 패널에서 포맷을 SVG로 선택 후 Export 버튼을 눌러보세요.
또는 더 쉬운 방법으로, 객체를 오른쪽 클릭한 뒤 Copy as SVG를 선택하면 SVG 코드를 바로 복사할 수도 있어요 이렇게 얻은 SVG 코드나 파일을 이제 React에서 사용해보겠습니다.
React와 SVG로 세로 프로그레스 바 만들기
이제 Figma에서 가져온 SVG를 이용해, 세로로 된 프로그레스 바(슬라이더)를 만들어 볼 거예요.
이 프로그레스 바는 최소값(min)과 최대값(max) 두 가지 값을 가지며, 둘 다 사용자가 드래그해서 조절할 수 있는 인터랙티브한 컴포넌트예요. React와 TypeScript로 구현하면서, SVG로 도형을 그리고 pointer 이벤트로 드래그 기능까지 넣어볼게요!
1. 컴포넌트 상태 정의하기
먼저 React 함수 컴포넌트를 만들고, 프로그레스 바의 현재 최소값과 최대값을 상태(state)로 관리하겠습니다. React의 useState 훅을 사용해서 두 개의 숫자 상태 (minVal과 maxVal)를 만들어요. 초기값은 예를 들어 20과 80으로 설정할게요 (전체 범위를 0~100으로 생각). 그리고 드래그 중인 상태를 추적하기 위해 dragging이라는 상태도 하나 만들어요. dragging은 현재 드래그 중인 손잡이가 "min", "max" 또는 없음을 나타내도록 할 것입니다.
import React, { useState } from 'react';
function VerticalRangeBar() {
const [minVal, setMinVal] = useState<number>(20); // 최소값 (%)
const [maxVal, setMaxVal] = useState<number>(80); // 최대값 (%)
const [activeThumb, setActiveThumb] = useState<'min' | 'max' | null>(null);
...
}
이렇게 상태를 정의해두면, 나중에 SVG에서 이 값들에 따라 손잡이 위치나 색깔 변화를 줄 수 있어요.
2. SVG로 모양 그리기
이제 컴포넌트의 JSX 반환값으로 SVG 요소를 작성해 볼게요.
우선 progress bar의 "몸통"을 그려볼게요.
<defs>
{/* Gradient & clip */}
<linearGradient
id="rangeGradient"
gradientUnits="objectBoundingBox"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop offset="0%" stopColor="var(--ChartColor-ChartColor97, #FFFFE0)" />
<stop offset="48.56%" stopColor="var(--ChartColor-ChartColor99, #89C0C4)" />
<stop offset="100%" stopColor="var(--ChartColor-ChartColor100, #579EB9)" />
</linearGradient>
<clipPath id="rangeClip">
<rect x={trackX} y={maxPos} width={trackThickness} height={minPos - maxPos} />
</clipPath>
{/* Button filter & clip */}
<filter
id="filter0_dd_1314_121150"
x="0.186035"
y="0.25"
width="50"
height="38"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="1.1" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0.0117647 0 0 0 0 0.0156863 0 0 0 0 0.0156863 0 0 0 0.56 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_1314_121150"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="0.6" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0.0117647 0 0 0 0 0.0156863 0 0 0 0 0.0156863 0 0 0 0.5 0"
/>
<feBlend
mode="normal"
in2="effect1_dropShadow_1314_121150"
result="effect2_dropShadow_1314_121150"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect2_dropShadow_1314_121150"
result="shape"
/>
</filter>
<clipPath id="clip0_1314_121150">
<rect x="15.186" y="12.25" width="20" height="14" rx="5" fill="white" />
</clipPath>
</defs>
{/* Track background */}
<rect
x={trackX}
y={margin}
width={trackThickness}
height={height}
fill="#ddd"
/>
{/* Gradient clipped to range */}
<rect
x={trackX}
y={margin}
width={trackThickness}
height={height}
fill="url(#rangeGradient)"
clipPath="url(#rangeClip)"
/>
위 코드에서 <linearGradient id="rangeGradient">를 정의하고, 트랙용 <rect>의 fill에 url(#"rangeGradient")을 지정했어요.
이렇게 하면 트랙 사각형에 gradient 효과를 지정할 수 있습니다.
def 안에 각종 효과(gradient, filter, clip path 등)을 미리 선언해놓고, 다른 컴포넌트에서 해당 효과를 id를 참조해서 사용이 가능합니다.

"track"을 그려놓고 지금 min, max 값만큼 clip해서 그라데이션 효과를 적용합니다.
이제 손잡이를 그려볼게요. 손잡이는 min과 max 값을 조절하는 버튼이라고 생각하면 됩니다. 손잡이는 아까 따로 다운로드해두었던 scroll.svg 를 활용합니다.
...(위 코드 생략)
<g
transform={`translate(${btnOffsetX}, ${btnOffsetY(minPos)})`}
>
<SliderButton
onMouseDown={e => { e.preventDefault(); setActiveThumb('min'); }}
/>
</g>
{/* Max thumb */}
<g
transform={`translate(${btnOffsetX}, ${btnOffsetY(maxPos)})`}
>
<SliderButton
onMouseDown={e => { e.preventDefault(); setActiveThumb('max'); }}
/>
</g>
</svg>
interface SliderButtonProps {
onMouseDown: (e: React.MouseEvent<SVGRectElement, MouseEvent>) => void;
}
const SliderButton: React.FC<SliderButtonProps> = ({ onMouseDown }) => (
<g filter="url(#filter0_dd_1314_121150)">
<path
d="M12.186 17.25C12.186 14.4886 14.4246 12.25 17.186 12.25H33.186C35.9475 12.25 38.186 14.4886 38.186 17.25V21.25C38.186 24.0114 35.9475 26.25 33.186 26.25H17.186C14.4246 26.25 12.186 24.0114 12.186 21.25V17.25Z"
fill="#F4F4F4"
/>
<g clipPath="url(#clip0_1314_121150)">
<rect
x="15.186"
y="12.25"
width="20"
height="14"
rx="5"
fill="#D9D9FF"
fillOpacity="0.11"
/>
<path
d="M19.8527 23.25H30.5194C30.886 23.25 31.186 22.95 31.186 22.5833C31.186 22.2167 30.886 21.9167 30.5194 21.9167H19.8527C19.486 21.9167 19.186 22.2167 19.186 22.5833C19.186 22.95 19.486 23.25 19.8527 23.25ZM19.8527 19.9167H30.5194C30.886 19.9167 31.186 19.6167 31.186 19.25C31.186 18.8833 30.886 18.5833 30.5194 18.5833H19.8527C19.486 18.5833 19.186 18.8833 19.186 19.25C19.186 19.6167 19.486 19.9167 19.8527 19.9167ZM19.186 15.9167C19.186 16.2833 19.486 16.5833 19.8527 16.5833H30.5194C30.886 16.5833 31.186 16.2833 31.186 15.9167C31.186 15.55 30.886 15.25 30.5194 15.25H19.8527C19.486 15.25 19.186 15.55 19.186 15.9167Z"
fill="#C6C6C6"
/>
</g>
{/* Transparent rect to capture events */}
<rect
width="51"
height="39"
fill="transparent"
onMouseDown={onMouseDown}
/>
</g>
);
버튼 좌표 배치 원리는 간단한데, transform 애니메이션을 활용해서 지금 "y 값 좌표" 에 버튼을 위치시킵니다.

3. 포인터 이벤트로 드래그 기능 추가하기
이제 가장 재미있는 부분입니다. 손잡이를 드래그해서 위아래로 움직이면 값이 변하도록 만들어볼 거예요.
웹에서 드래그 동작을 구현하려면 마우스나 터치 이벤트를 다뤄야 하는데, React에선 Pointer Events를 쓰면 아주 편리합니다. Pointer Event는 마우스, 터치, 펜 등 다양한 입력을 하나의 통일된 방식으로 처리할 수 있는 이벤트예요 예전에는 PC와 모바일을 모두 지원하려면 onMouseDown과 onTouchStart 등을 각각 코딩해야 했지만, 이제는 onPointerDown 하나로 모두 처리할 수 있답니다.
우리는 이미 JSX에서 <circle> 손잡이에 onPointerDown을 설정해 두었죠? 사용자가 손잡이를 누르는 순간 어떤 손잡이를 움직이는지 상태를 업데이트하고 (dragging을 "min" 또는 "max"로 set), 이후 움직이는 동안(pointermove)과 놓는 순간(pointerup)을 포착해서 값을 갱신하면 돼요.
// Handles the pointer down event when a thumb is pressed
// Sets the active thumb ('min' or 'max') and captures the pointer
const handlePointerDown = (thumb: 'min' | 'max') => (e: React.PointerEvent) => {
...
};
// Handles the pointer move event when the thumb is dragged
// Updates the value of the active thumb based on the pointer's position
const handlePointerMove = (e: React.PointerEvent) => {
...
};
// Handles the pointer up event when the thumb is released
// Releases the pointer capture and clears the active thumb
const handlePointerUp = (e: React.PointerEvent) => {
...
};

최종 코드는 아래에 있습니다.
https://codesandbox.io/p/devbox/kll755
결론: SVG 활용 꿀팁 ✨
이번 예제로 SVG의 힘을 조금 느끼셨나요? 정리하자면, SVG는 화면 크기에 관계없이 선명하고 코드로 그리는 그림이라 자유롭게 수정하거나 상호작용할 수 있다는 게 장점이에요. 마무리로, 실무에서 SVG를 유용하게 활용하는 몇 가지 팁을 알려드릴게요:
- 반응형 디자인: SVG는 벡터 방식이라 화면 크기에 따라 자유롭게 확대/축소해도 깨지지 않아요. 따라서 아이콘이나 일러스트를 SVG로 사용하면 레티나 디스플레이에서도 언제나 또렷하게 보입니다. CSS로 너비나 높이를 %로 주거나 viewBox를 적절히 설정하면 부모 컨테이너 크기에 맞춰 유연하게 대응할 수도 있어요.
- 아이콘 최적화: Figma나 일러스트레이터에서 SVG를 내보낼 때 불필요한 데이터가 붙거나 좌표가 복잡하게 저장될 수 있어요. 이런 경우 SVG 압축 도구를 사용해보세요. 예를 들어 SVGO라는 오픈 소스 툴을 쓰면 SVG 파일 크기를 많이 줄일 수 있어요복잡한 경로는 단순화하고, 쓰이지 않는 요소는 제거해서 성능을 높이는 거죠. 최적화된 SVG는 파일 크기가 작아져 웹페이지 로딩도 빨라진답니다.
- 인터랙티브 컴포넌트: SVG 요소들은 DOM 요소이기도 해서, JavaScript나 CSS로 동적으로 제어하기가 쉬워요. 이번에 만든 프로그레스 바처럼 드래그 이벤트를 처리하는 것은 물론이고, <circle>에 마우스오버하면 색을 바꾸거나, 클릭하면 애니메이션을 주는 것도 가능합니다. 예를 들어, SVG에 CSS 클래스를 적용해서 .active일 때 특정 부분의 색깔을 변경하거나, 간단한 트랜지션 효과를 줄 수도 있어요. 또한 D3.js나 GSAP 같은 라이브러리와 결합하면 복잡한 데이터 시각화나 모션 그래픽도 구현할 수 있어요. 상상한 대로 자유롭게 SVG를 가지고 놀아보세요!
SVG를 배우기 시작하면 웹 개발에서 디자인과 상호작용을 다루는 새로운 무기가 생긴 셈이에요. 이번 튜토리얼이 따뜻한 입문 가이드가 되었길 바랍니다. 앞으로도 SVG를 활용해 멋지고 유용한 컴포넌트들을 많이 만들어보세요! 😃
P.S) 엄청 복잡한 인터렉션은 가내수공업이 아니고 lottieFiles같은 라이브러리의 힘을 빌리는게 좋을듯..?
'Front-end > 애니메이션 & 인터렉션' 카테고리의 다른 글
Canvas 충돌(collision) 애니메이션 구현 (0) | 2025.04.22 |
---|---|
브라우저 부드러운 인터렉션 구현(easing) (0) | 2025.04.22 |