useLine 로직 통합하기
SVG 에디터에서 점, 선, 사각형, 원의 중복된 도형 로직을 하나의 모델로 통합한 리팩터링 과정을 정리합니다.
1. 문제 상황
기존에는 점(Dot), 선(Line), 사각형(Rect), 원(Circle)을 각각 별도의 Class로 만들어 관리했습니다.
내부 동작은 useDot.ts 와 useLine.ts 로 나누어 작성했습니다.
이 방식은 동작에는 문제가 없지만, 비슷한 동작을 각기 다른 모듈에서 관리하다 보니
문제가 발생하면 4개의 파일을 모두 수정해야 하고, 유지보수가 어려워집니다.
이에 따라 동일한 구조를 가진 도형은 하나의 Class로 통합하여 관리하기로 했습니다.
특히 문제가 된 부분은 렌더링이 아니라 상태 관리였습니다. 도형마다 시작점과 끝점을 저장하고, 드래그 중 좌표를 갱신하고, 선택 핸들을 다시 계산하는 흐름이 거의 같았습니다. 그런데 파일이 분리되어 있다 보니 작은 계산 규칙 하나를 바꿔도 Line, Rect, Circle, Dot 쪽을 모두 확인해야 했습니다.
예를 들어 최소 크기 제한이나 핸들 위치 계산은 모든 도형에서 비슷한 목적을 갖습니다. 하지만 각 도형이 자체 구현을 갖고 있으면 동일한 버그가 반복되거나, 한 도형만 수정되고 다른 도형은 이전 동작을 유지하는 문제가 생깁니다. SVG 에디터처럼 사용자의 드래그 입력에 즉시 반응해야 하는 화면에서는 이런 차이가 곧 선택 영역 오차, 저장 데이터 불일치, undo/redo 재현 오류로 이어질 수 있습니다.
이번 리팩터링의 목표는 도형의 종류를 없애는 것이 아니었습니다. 외부 API는 기존처럼 "line", "rect", "circle", "dot"을 구분하되, 내부에서는 공통 좌표 모델과 계산 함수를 사용하게 만드는 것이었습니다. 이렇게 하면 UI 컴포넌트는 도형 타입만 전달하고, 실제 SVG 속성 변환은 한곳에서 처리할 수 있습니다.
2. 설계 방향
도형을 통합할 때 가장 먼저 정한 기준은 "저장 데이터는 단순하게, 렌더링 변환은 명시적으로"였습니다.
- 저장 데이터는 도형 타입과 기준 좌표만 가진다.
- SVG 렌더링에 필요한
path,circle, 핸들 좌표는 렌더링 직전에 계산한다. - 도형별 예외는 분기 처리하되, 좌표 보정과 범위 제한은 공통 유틸리티로 둔다.
이 기준을 둔 이유는 저장 포맷과 화면 표현을 섞지 않기 위해서입니다. rect는 화면에서는 닫힌 path로 그릴 수 있지만, 편집 데이터에서는 시작점과 끝점만 있어도 충분합니다. circle도 중심점과 반지름을 직접 저장할 수 있지만, 드래그 기반 편집에서는 중심점과 외곽점을 저장하는 편이 입력 흐름을 다루기 쉽습니다.
반대로 dot은 예외로 남겼습니다. 점을 path로 그릴 수도 있지만, 내부가 비거나 fill/stroke 규칙이 다른 도형과 충돌하기 쉽습니다. 그래서 데이터 모델은 같은 계열로 두되 렌더링은 circle 요소를 사용하도록 분리했습니다.
3. getPathAttribute 함수
type 값에 따라 두 점의 좌표로 SVG Path를 다르게 만들어 반환합니다.
export const getPathAttribute = (
type: "line" | "rect" | "circle",
points: number[][]
): string => {
switch (type) {
case "line": {
const [[x1, y1], [x2, y2]] = points;
return `M${x1},${y1} L${x2},${y2}`;
}
case "rect": {
const [[x1, y1], [x2, y2]] = points;
const minX = Math.min(x1, x2);
const minY = Math.min(y1, y2);
const maxX = Math.max(x1, x2);
const maxY = Math.max(y1, y2);
return `M${minX},${minY} L${maxX},${minY} L${maxX},${maxY} L${minX},${maxY} Z`;
}
case "circle": {
const [center, edge] = points;
const [cx, cy] = center;
const [ex, ey] = edge;
const r = Math.hypot(ex - cx, ey - cy);
return `M${cx - r},${cy} A${r},${r} 0 1,0 ${
cx + r
},${cy} A${r},${r} 0 1,0 ${cx - r},${cy}`;
}
default:
throw new Error(`Unsupported type: ${type}`);
}
};
dot은path로 그리면 내부가 비게 되므로, 별도로circle요소를 사용하여 처리합니다.
fill속성을 공유하면 충돌이 생기기 때문입니다.
이 함수의 장점은 도형 타입별 SVG 문자열 생성 규칙이 한곳에 모인다는 점입니다. 이전 구조에서는 각 도형 클래스가 자기 렌더링 방식을 알고 있었기 때문에, 도형을 추가하거나 수정할 때 상태 관리 코드와 렌더링 코드를 함께 건드려야 했습니다. 지금은 도형 상태가 바뀌더라도 getPathAttribute의 입력과 출력 계약만 유지하면 됩니다.
또 하나의 장점은 테스트하기 쉽다는 점입니다. 이 함수는 DOM이나 React 상태에 의존하지 않고, type과 좌표 배열만 입력으로 받습니다. 그래서 특정 좌표가 들어왔을 때 어떤 path 문자열이 만들어지는지 순수 함수처럼 검증할 수 있습니다.
4. 로직 리팩토링 예시
4-1. 핸들러 위치 계산 개선
기존 코드:
const biggerX = Math.max(x1, x2);
const smallerX = Math.min(x1, x2);
const handlerFlg = (x2 - x1 > 0 && y2 - y1 > 0) || (x2 - x1 < 0 && y2 - y1 < 0);
const targetX = handlerFlg ? biggerX - radius : smallerX + radius;
리팩토링 후:
const [x1, y1] = circleCoords[0];
const [x2, y2] = circleCoords[1];
const isSameDirection = (x2 - x1) * (y2 - y1) > 0;
const targetX = isSameDirection
? Math.max(x1, x2) - radius
: Math.min(x1, x2) + radius;
기존 코드는 조건 자체가 나쁜 것은 아니지만, 좌표 방향을 읽는 사람이 직접 해석해야 했습니다. 리팩터링 후에는 isSameDirection이라는 이름으로 의도를 먼저 드러내고, 실제 좌표 선택은 Math.max, Math.min 조합으로 단순화했습니다. 이렇게 바꾸면 이후 y축 계산이나 다른 핸들 계산에도 같은 규칙을 재사용하기 쉽습니다.
4-2. 범위 제한 간소화
기존:
if (r < 0) {
this.r = 0;
} else if (r < minLength) {
this.r = r;
} else {
this.r = minLength;
}
리팩토링:
this.r = Math.max(0, Math.min(r, minLength));
여기서 중요한 점은 코드 줄 수를 줄이는 것보다 경계값 처리를 한 줄의 명확한 규칙으로 만든다는 점입니다. 반지름은 0보다 작을 수 없고, 특정 최소 길이를 넘어가면 제한되어야 합니다. 이 규칙은 if/else보다 clamp 형태로 표현하는 편이 실수 가능성이 낮았습니다.
5. 적용 결과
리팩터링 후 도형 처리 흐름은 다음처럼 단순해졌습니다.
- 사용자가 캔버스에서 드래그를 시작한다.
- 도형 타입과 기준 좌표를 공통 모델에 저장한다.
- 드래그 중에는 두 번째 좌표만 갱신한다.
- 렌더링 단계에서 타입에 맞는 SVG 속성을 계산한다.
- 선택 핸들, 크기 제한, 저장 데이터는 같은 좌표 모델을 기준으로 처리한다.
이 구조로 바꾸면서 도형별 코드가 크게 줄었고, 새 도형을 추가할 때 확인해야 하는 위치도 줄었습니다. 특히 undo/redo나 선택 상태 같은 기능을 붙일 때 도형마다 별도 분기를 늘리지 않아도 되는 점이 가장 큰 이점이었습니다.
단점도 있습니다. 모든 도형을 하나의 모델로 다루면 개별 도형만의 예외가 생길 때 공통 모델이 복잡해질 수 있습니다. 그래서 이번 작업에서는 공통화 범위를 좌표와 path 변환까지만 제한했습니다. 텍스트, 자유곡선, 그룹처럼 편집 규칙이 다른 객체까지 같은 구조에 억지로 넣지는 않는 편이 안전합니다.
6. 요약 정리
| 구분 | 설명 |
|---|---|
| 관리 | 점, 선, 사각형, 원을 하나의 Class로 통합 |
| 렌더링 | type 에 따라 getPathAttribute 로 Path 반환 |
| dot 처리 | path 대신 circle 요소 사용 |
| 개선 | 핸들러 위치, 값 범위 계산 로직 단순화 |
| 성과 | 약 1100줄 -> 600줄로 간소화 |
이번 리팩터링은 큰 구조를 새로 만든 작업이라기보다, 이미 반복되고 있던 도형 편집 규칙을 한곳으로 모은 작업에 가깝습니다. SVG 에디터처럼 작은 인터랙션이 많은 도구에서는 이런 정리가 이후 기능 추가 속도보다 버그 재현성과 수정 범위를 줄이는 데 더 큰 효과를 냅니다.