브라우저만으로 완성한 SVG 에디터 구현과 설계 노트
이 글의 핵심 요약
- ChatGPT와 상호작용하며 SVG 에디터 /tools/svgedit/를 만들려 했는데, 삽질의 연속이었다. 그 과정에서 겪은 난관을 정리했다.
- 혼자서는 절대 완성하지 못했을 것이다. 생성 AI의 코딩 역량은 역시 강력하다.
- 동시에 “AI에게 다 맡기면 끝”도 아니었다. 전체를 조율하고 요구 사항의 오류나 착각을 바로잡으며, 장애 대응의 방향을 잡아 주는 플레잉 매니저 역할이 필수였다.
- 그런 의미에서 AI 시대를 어떻게 버틸 것인가에 대한 힌트가 되었다.
브라우저만으로 완성한 SVG 에디터 구현과 설계 노트
SVG란 무엇인가
SVG(Scalable Vector Graphics)는 W3C가 표준화한 XML 기반 벡터 이미지 포맷이다. 래스터 이미지(PNG·JPEG 등)와 달리 확대·축소해도 화질이 떨어지지 않는 것이 특징이다. 또한 DOM의 일부로 HTML 문서에 직접 삽입할 수 있으므로, JavaScript와 CSS로 동적 조작·스타일 변경이 가능하다. 규격은 W3C SVG 사양을 따르며 도형, 텍스트, 경로, 그라데이션, 필터 등 폭넓은 표현력을 지원한다.
SVG는 다음과 같은 용도로 널리 쓰인다.
- 웹 페이지의 아이콘과 도표 표시
- 다이어그램이나 차트의 동적 생성
- 사용자 상호작용을 포함한 도형 그리기 애플리케이션
- 인쇄물·UI 디자인 소재 제작
이번 SVG 에디터 개요
이 에디터는 브라우저에서만 동작하는 100% 클라이언트 사이드 도구다. 사용자는 사각형·타원 같은 기본 도형뿐 아니라 별, 말풍선, 구름, 하트, 자유곡선을 그릴 수 있는 펜 모드까지 활용해 다양한 도형을 만들 수 있다. 작성한 도형은 이동·확대·회전이 가능하며, 정렬·스냅 기능, 그룹화, 레이어 목록도 지원한다. 완성된 그림은 SVG와 PNG 양쪽으로 내보낼 수 있다.
아래에서는 SVG 사양을 어떻게 JavaScript 구현으로 녹여 냈는지를 구체적으로 살펴본다.
구현 시 사양 반영과 난관
도형 추가와 속성 처리
SVG 사양에서는 도형마다 고유한 요소·속성이 정의되어 있다. 사각형은 <rect>
, 타원은 <ellipse>
, 다각형은 <polygon>
을 사용하며 각각 x,y,width,height
, cx,cy,rx,ry
, points
등의 속성을 정확히 설정해야 한다. 별이나 하트처럼 복잡한 형태는 <path>
요소로 베지어 곡선과 호 명령을 조합해 생성했다.
구현 예시(사각형 추가)
function addRectangle(x, y, width, height) {
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
rect.setAttribute("x", x);
rect.setAttribute("y", y);
rect.setAttribute("width", width);
rect.setAttribute("height", height);
rect.setAttribute("fill", "transparent");
rect.setAttribute("stroke", "#000");
svg.appendChild(rect);
}
사각형·타원은 단순하지만, 다각형이나 커스텀 도형은 좌표 계산이 필요하다. 예를 들어 별을 그릴 때는 중심각을 등분해 꼭짓점을 구한 뒤 points
속성에 모아 넣는다.
function createStar(cx, cy, spikes, outerR, innerR) {
let points = "";
const step = Math.PI / spikes;
for (let i = 0; i < 2 * spikes; i++) {
const r = i % 2 === 0 ? outerR : innerR;
const x = cx + r * Math.cos(i * step);
const y = cy + r * Math.sin(i * step);
points += `${x},${y} `;
}
const polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
polygon.setAttribute("points", points.trim());
polygon.setAttribute("stroke", "#000");
polygon.setAttribute("fill", "transparent");
svg.appendChild(polygon);
}
변형과 좌표계 처리
SVG에서는 transform
속성으로 translate
, rotate
, scale
등을 조합해 도형을 조작할 수 있다. 문제는 선택한 도형의 바운딩 박스를 얻을 때 로컬 좌표와 전역 좌표의 차이를 흡수해야 했다는 점이다. 이를 위해 getScreenCTM()
을 활용해 전역 좌표계로 변환한 뒤 사각형을 계산했다.
구현 예시(좌표 변환 적용)
function getTransformedBBox(element) {
const bbox = element.getBBox();
const matrix = element.getScreenCTM();
const points = [
svg.createSVGPoint(), svg.createSVGPoint(),
svg.createSVGPoint(), svg.createSVGPoint()
];
points[0].x = bbox.x; points[0].y = bbox.y;
points[1].x = bbox.x + bbox.width; points[1].y = bbox.y;
points[2].x = bbox.x; points[2].y = bbox.y + bbox.height;
points[3].x = bbox.x + bbox.width; points[3].y = bbox.y + bbox.height;
return points.map(p => p.matrixTransform(matrix));
}
이렇게 하면 회전이나 확대·축소 후에도 정확한 선택 사각형을 그릴 수 있다.
스냅 기능과 그리드
그리드는 SVG의 <pattern>
요소를 이용해 배경에 그렸다. 스냅 처리는 JavaScript에서 좌표를 계산해 지정 간격으로 반올림하는 방식이다.
function snapToGrid(value, gridSize) {
return Math.round(value / gridSize) * gridSize;
}
오브젝트 스냅은 다른 도형의 중심점이나 끝점을 스캔해 최근접을 탐색하고, 보조선을 표시해 흡착하도록 만들었다. 이로써 UI 정밀도가 크게 향상됐다.
텍스트 편집
SVG의 <text>
요소는 직접 편집할 수 없으므로, 인라인 편집용으로 HTML <input>
을 임시로 겹치는 방식을 채택했다.
function editTextElement(textElem) {
const input = document.createElement("input");
input.type = "text";
input.value = textElem.textContent;
document.body.appendChild(input);
const bbox = textElem.getBoundingClientRect();
input.style.position = "absolute";
input.style.left = bbox.x + "px";
input.style.top = bbox.y + "px";
input.focus();
input.addEventListener("blur", () => {
textElem.textContent = input.value;
document.body.removeChild(input);
});
}
패스와 자유곡선
SVG <path>
는 M, L, C, A 같은 명령을 조합해 그린다. 먼저 직선 기반 편집부터 시작해, 클릭할 때마다 좌표를 추가하는 구조로 구현했다.
let pathData = "M100,100";
function addPathPoint(x, y) {
pathData += ` L${x},${y}`;
path.setAttribute("d", pathData);
}
출력(SVG/PNG 내보내기)
저장 시에는 보조 요소를 제거한 뒤 내보낸다. PNG 변환은 Canvas를 거쳐 처리했다.
function exportToPNG(svgElement) {
const serializer = new XMLSerializer();
const source = serializer.serializeToString(svgElement);
const img = new Image();
const svgBlob = new Blob([source], { type: "image/svg+xml;charset=utf-8" });
const url = URL.createObjectURL(svgBlob);
img.onload = () => {
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
canvas.toBlob(blob => {
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "drawing.png";
a.click();
});
};
img.src = url;
}
캔버스 스크롤 대응
큰 SVG를 다룰 때는 <div style="overflow:auto">
로 감싸 필요 시 스크롤바를 표시해 해결했다.
정리
SVG는 강력한 사양을 갖고 있지만, 구현에서는 좌표 변환이나 패스 정의 등 만만치 않은 난관이 많다. 이번 에디터에서는 다음을 정리하며 대응했다.
- 도형 요소의 속성 제어(rect, ellipse, polygon, path)
transform
과 좌표계의 정확한 처리- 그리드·스냅 구현
- 인라인 텍스트 편집
- 패스 편집(직선 기반)
- PNG/SVG 양방향 내보내기
- 스크롤 가능한 대형 캔버스 대응
SVG의 표현력을 최대한 활용해, 브라우저만으로 동작하는 고기능 에디터를 구축할 수 있었다.