1. 개요

사용자가 운동 일지를 작성하기 위해 운동 부위(예: 가슴, 대퇴사두근, 이두근 등)를 시각적으로 선택하는 SVG 신체 구조도(Muscle Map) 기능이였습니다. 마우스로 특정 근육 영역을 클릭하면 예쁜 테마 색상(var(—primary))으로 하이라이트가 점등되고, 다시 한번 클릭하면 하이라이트가 해제되며 원래대로 되돌아가야 정상인 스펙이였습니다. 하지만 아이폰 사파리나 안드로이드 크롬 모바일 브라우저 런타임 환경에서, 근육 선택을 해제했음에도 하이라이트 색상이 꺼지지 않고 끈적하게 달라붙어 박제되는 치명적인 잔상 결함이 지속 접수되었습니다. 가상 DOM 패러다임과 명령형 시각화 라이브러리 간의 원천적인 충돌 원인을 파악하고 이를 순수 React 스펙으로 리액티브하게 리팩토링해 낸 여정을 꼼꼼히 정리합니다.


2. 핵심 내용

2-1. 🚨 주요 문제 현상 및 근본 원인 분석

  • 현상:
    • 하이라이트 해제 불능 및 잔상 박제: 특정 근육 조각을 마우스/터치로 켜고 난 뒤에, 다시 클릭하여 선택을 명백히 해제했는데도 하이라이트 색상이 꺼지지 않고 화면에 박제되는 현상이 확인되었습니다.
    • 호버 스타일 번짐 자국: 다른 부위로 터치를 옮겨가도 이전 터치 지점의 호버(Hover) 스타일 번짐 자국이 얼룩처럼 남아 사용자가 현재 어떤 부위가 실제로 필터링 적용된 것인지 혼란을 겪는 치명적인 UI 기현상이였습니다.
  • 원인:
    1. React 선언형 패러다임 vs D3.js 명령형 DOM 조작의 충돌: 기존 레거시 코드는 SVG 제어를 위해 시각화 엔진인 D3.js를 끌어와 사용하며 d3.select().classed("active", true/false)와 같이 명령형 체이닝 메서드로 실물 DOM 노드에 클래스를 임의로 주입/소거하고 있었습니다. React가 실물 DOM을 렌더링한 직후 D3가 실물 DOM을 강제로 변경하는데, 그 뒤 1프레임 이후 React의 가상 DOM 리렌더링 스레드가 기존 가상 노드 메모리 정보를 가지고 실물 DOM을 다시 원래대로 덮어써 버려 D3의 클래스 삭제 명령이 무력화되었고 잔상이 박제되었습니다.
    2. 모바일 PointerEvent의 호버(Hover) 보존 오차: 모바일 터치 스크린은 마우스 포인터가 없어 사용자가 터치를 떼어낸 뒤에도 모바일 브라우저 렌더러들은 이전 터치 좌표에 계속 마우스가 올라가 있는 것처럼 인지하여 CSS :hover 상태를 기이하게 보존하는 스펙적 결함이 결합되어 있었습니다.

2-2. 💡 우아한 해결책 및 구현 코드

  • 해결 방안: 웹 시각화 성능에 나쁜 영향을 주는 무거운 D3.js 의존성을 완전히 제거하고, 순수 100% React의 선언형 패러다임과 웹 표준 이벤트 스펙만을 조합했습니다. 수백 개의 path 조각마다 걸려 메모리 누수를 발생시키던 리스너를 단 하나의 최상위 부모 리스너로 단일화하는 ‘이벤트 위임(Event Delegation)’ 구조를 도입하고, 상태에 따라 JSX 최하단에 <style> 구문을 동적으로 마운트/언마운트 시키는 ‘동적 CSS 스타일 인젝션(Dynamic Style Injection)’ 설계를 도입하여 물리적인 잔상 잔류 통로를 영구 파괴했습니다.
  • 코드 명세:
// muscle-map-filter.tsx
import React, { useState, useMemo } from "react";
 
export function MuscleMapFilter() {
    const [selectedId, setSelectedId] = useState<string>("");
 
    // A. 이벤트 위임(Event Delegation)을 통해 단일 리스너로 전체 SVG 클릭 통제
    const handleSvgClick = (event: React.MouseEvent<SVGSVGElement>) => {
        const path = (event.target as HTMLElement).closest("[data-muscle-id]");
        if (!path) return;
 
        const muscleId = path.getAttribute("data-muscle-id") || "";
        setSelectedId((prev) => (prev === muscleId ? "" : muscleId));
    };
 
    // 모바일 터치 기기에서는 불필요한 호버 피드백을 바이패스하도록 포인터 타입 검증
    const handlePointerOver = (event: React.PointerEvent<SVGSVGElement>) => {
        if (event.pointerType !== "mouse") return;
        // 데스크탑 마우스 환경에서만 호버 피드백 활성화...
    };
 
    // B. 동적 스타일 인젝션: 상태가 비워지면 스타일 공급 태그가 즉시 가상 DOM 상에서 완벽히 소거됩니다.
    const dynamicStyleTag = useMemo(() => {
        if (!selectedId) return null;
        return (
          <style>{`
            [data-muscle-id="${selectedId}"] {
              fill: var(--primary) !important;
              filter: drop-shadow(0 0 6px var(--primary));
              transition: fill 0.2s ease, filter 0.2s ease;
            }
          `}</style>
        );
    }, [selectedId]);
 
    return (
        <div className="relative w-full overflow-hidden rounded-3xl border bg-card p-6">
            <svg 
              onClick={handleSvgClick} 
              onPointerOver={handlePointerOver}
              viewBox="0 0 600 1200" 
              className="h-auto w-full select-none"
            >
                <path data-muscle-id="biceps_left" d="..." className="muscle-segment transition-colors" />
                <path data-muscle-id="quads_right" d="..." className="muscle-segment transition-colors" />
            </svg>
            
            {/* 동적으로 마운트되는 스타일 태그. 잔상의 원천 차단 */}
            {dynamicStyleTag}
        </div>
    );
}

2-3. ✅ 결과 검증 및 모던 오픈소스 라이브러리 검토

  • 최종 검증:
    • 메모리 절감 및 진입 최적화: D3.js 라이브러리 번들 크기(약 260KB 이상)가 통째로 제거되며 컴포넌트 마운트 초기 오버헤드가 극적으로 감소했습니다.
    • 잔상 완벽 소거: selectedId가 빈 값("")으로 변하는 즉시, 가상 DOM에서 스타일 노드가 삭제되며 하이라이트 색상이 즉각 해제되어 잔상이 100% 깔끔하게 지워짐을 입증했습니다.
  • 추천 오픈소스 라이브러리 검토:
    1. SVGR (Vite Plugin SVG Transformer):
      • 평가: 무겁고 제어가 어려운 순수 .svg 그래픽 리소스를 완벽한 React 컴포넌트로 자동 변환 빌드해주는 도구로서, 개별 path들을 React state와 Props로 선언적 통제를 수행할 수 있게 돕는 극강의 유틸리티입니다.
    2. Event Delegation (JS Native Pattern):
      • 평가: 개별 다수의 하위 노드에 리스너를 루프 돌려 바인딩하는 안티패턴을 타파하고, 브라우저 표준 버블링 메커니즘을 사용해 최상위 단일 이벤트 통로로 성능 오버헤드를 극적으로 경량화시켜 주는 네이티브 설계 패턴으로 상시 적용을 권유합니다.