Dandy Now!
  • [CSS] 로딩 스피너 뒤에서도 드래그 이벤트 활성화하기
    2025년 10월 22일 16시 37분 13초에 업로드 된 글입니다.
    작성자: DandyNow
    728x90
    반응형

    로딩 스피너 뒤에서도 드래그 이벤트 활성화하기

    애플리케이션에서 로딩 스피너를 전체 화면에 표시할 때, 사용자가 뒤쪽 요소들과 상호작용 가능해야 하는 경우가 있다. 이 글에서는 로딩 중에도 드래그 이벤트 등을 가능하게 하는 방법을 다룬다.

    먼저 CSS 스피너 애니메이션을 위한 스타일을 추가해야 한다.

    @keyframes spin {
      0% {
        transform: rotate(0deg);
      }
      100% {
        transform: rotate(360deg);
      }
    }

    1. 문제 상황

    일반적인 로딩 컴포넌트는 다음과 같이 구현된다.

    function Loading() {
      return (
        <div
          style={{
            position: "fixed",
            top: 0,
            left: 0,
            right: 0,
            bottom: 0,
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            zIndex: 1000,
          }}
        >
          <div
            style={{
              width: "40px",
              height: "40px",
              border: "4px solid rgba(0, 0, 0, 0.1)",
              borderTop: "4px solid rgba(0, 0, 0, 0.3)",
              borderRadius: "50%",
              animation: "spin 1s linear infinite",
            }}
          />
        </div>
      );
    }

    이 코드의 문제점은 position: fixed로 전체 화면을 덮고 있어서 마우스 이벤트가 뒤쪽 요소로 전달되지 않는다는 것이다.

    2. 해결 방법

    2-1. pointer-events: none 사용 (권장)

    가장 간단하고 효과적인 방법은 CSS의 pointer-events: none 속성을 사용하는 것이다.

    function Loading() {
      return (
        <div
          style={{
            position: "fixed",
            top: 0,
            left: 0,
            right: 0,
            bottom: 0,
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            zIndex: 1000,
            pointerEvents: "none", // 핵심!
          }}
        >
          <div
            style={{
              width: "40px",
              height: "40px",
              border: "4px solid rgba(0, 0, 0, 0.1)",
              borderTop: "4px solid rgba(0, 0, 0, 0.3)",
              borderRadius: "50%",
              animation: "spin 1s linear infinite",
            }}
          />
        </div>
      );
    }

    pointer-events: none을 추가하면:

    • 마우스 클릭, 드래그, 호버 등의 이벤트가 뒤쪽 요소로 전달된다.
    • 로딩 스피너는 여전히 화면에 표시된다.
    • 사용자는 로딩 중에도 뒤쪽 요소들과 상호작용할 수 있다.

    2-2. 스피너 위치 조정

    전체 오버레이 대신 스피너만 특정 위치에 표시하는 방법이다.

    function Loading() {
      return (
        <div
          style={{
            position: "fixed",
            top: "20px",
            right: "20px",
            zIndex: 1000,
          }}
        >
          <div
            style={{
              width: "30px",
              height: "30px",
              border: "3px solid rgba(0, 0, 0, 0.1)",
              borderTop: "3px solid rgba(0, 0, 0, 0.3)",
              borderRadius: "50%",
              animation: "spin 1s linear infinite",
            }}
          />
        </div>
      );
    }

    2-3. 반투명 배경과 함께 사용

    반투명 배경이 필요한 경우, 배경과 스피너를 분리하여 구현한다.

    function Loading() {
      return (
        <>
          {/* 반투명 배경 */}
          <div
            style={{
              position: "fixed",
              top: 0,
              left: 0,
              right: 0,
              bottom: 0,
              background: "rgba(0, 0, 0, 0.1)",
              zIndex: 999,
              pointerEvents: "none",
            }}
          />
          {/* 스피너 */}
          <div
            style={{
              position: "fixed",
              top: "50%",
              left: "50%",
              transform: "translate(-50%, -50%)",
              zIndex: 1000,
              pointerEvents: "none",
            }}
          >
            <div
              style={{
                width: "40px",
                height: "40px",
                border: "4px solid rgba(0, 0, 0, 0.1)",
                borderTop: "4px solid rgba(0, 0, 0, 0.3)",
                borderRadius: "50%",
                animation: "spin 1s linear infinite",
              }}
            />
          </div>
        </>
      );
    }

    3. pointer-events 속성의 다른 옵션들

    CSS의 pointer-events 속성은 none 외에도 다양한 값을 가질 수 있다.

    3-1. 기본 값들

    • auto (기본값): 요소가 마우스 이벤트의 대상이 될 수 있다.
    • none: 요소가 마우스 이벤트의 대상이 되지 않으며, 이벤트가 뒤쪽 요소로 전달된다.
    .loading-overlay {
      pointer-events: auto; /* 기본 동작 */
      pointer-events: none; /* 이벤트 무시 */
    }

    3-2. SVG 전용 값들

    SVG 요소에서는 더 세밀한 제어가 가능하다. SVG는 HTML 요소와 달리 fill(채우기)과 stroke(테두리)를 구분하여 이벤트를 처리할 수 있다.

    3-2-1. SVG 구조적 특성

    SVG 요소는 다음 두 가지 주요 속성을 가진다:

    • fill: 도형의 내부 채우기
    • stroke: 도형의 테두리 선

    3-2-2. 각 값의 상세 동작

    • visiblePainted (기본값): 요소가 보이고 채우기나 선이 있을 때만 이벤트 대상이 된다
    <circle cx="50" cy="50" r="40"
            fill="blue" stroke="red"
            style="pointer-events: visiblePainted;" />
    <!-- 파란 채우기와 빨간 테두리가 있으므로 클릭 가능 -->
    
    <circle cx="150" cy="50" r="40"
            fill="none" stroke="none"
            style="pointer-events: visiblePainted;" />
    <!-- 채우기도 선도 없으므로 클릭 불가능 -->
    • visibleFill: 요소가 보이고 채우기가 있을 때만 이벤트 대상이다
    <circle cx="50" cy="50" r="40"
            fill="blue" stroke="red" stroke-width="10"
            style="pointer-events: visibleFill;" />
    <!-- 파란 채우기 부분만 클릭 가능, 빨간 테두리는 클릭 불가 -->
    • visibleStroke: 요소가 보이고 선이 있을 때만 이벤트 대상이다
    <circle cx="50" cy="50" r="40"
            fill="blue" stroke="red" stroke-width="10"
            style="pointer-events: visibleStroke;" />
    <!-- 빨간 테두리 부분만 클릭 가능, 파란 채우기는 클릭 불가 -->
    • visible: 요소가 보이기만 하면 채우기나 선의 유무와 관계없이 전체 영역이 이벤트 대상이다
    <circle cx="50" cy="50" r="40"
            fill="none" stroke="none"
            style="pointer-events: visible;" />
    <!-- 투명하지만 전체 원 영역이 클릭 가능 -->
    • painted: visibility 속성과 관계없이 채우기나 선이 있으면 이벤트 대상이다
    • fill: visibility와 관계없이 채우기가 있는 부분만 이벤트 대상이다
    • stroke: visibility와 관계없이 선이 있는 부분만 이벤트 대상이다
    • all: 모든 조건을 무시하고 항상 이벤트 대상이다

    3-2-3. SVG 활용 예제

    도넛 차트에서 내부 구멍 클릭 방지:

    <svg width="200" height="200">
      <!-- 외부 원: 클릭 가능 -->
      <circle cx="100" cy="100" r="80"
              fill="lightblue"
              style="pointer-events: visibleFill;" />
    
      <!-- 내부 원: 클릭으로 구멍 뚫기 -->
      <circle cx="100" cy="100" r="40"
              fill="white"
              style="pointer-events: none;" />
    </svg>

    복잡한 그래프에서 선만 클릭 가능하게 하는 방법:

    <svg width="300" height="200">
      <!-- 배경 영역: 클릭 불가 -->
      <rect width="300" height="200"
            fill="lightgray"
            style="pointer-events: none;" />
    
      <!-- 그래프 선: 클릭 가능 -->
      <path d="M10,100 L50,50 L100,80 L150,30 L200,60"
            fill="none"
            stroke="blue"
            stroke-width="3"
            style="pointer-events: visibleStroke;" />
    </svg>

    3-2-4. HTML 요소와의 차이점

    HTML 요소에서는 이런 세밀한 제어가 불가능하다:

    /* HTML에서는 전체 요소 단위로만 제어 */
    .html-element {
      pointer-events: auto; /* 전체 요소 */
      pointer-events: none; /* 전체 요소 무시 */
    }

    반면 SVG에서는 부분별 제어가 가능하다:

    /* SVG에서는 세밀한 제어 가능 */
    .svg-element {
      pointer-events: visibleFill; /* 채우기만 */
      pointer-events: visibleStroke; /* 테두리만 */
      pointer-events: visible; /* 전체 영역 */
    }

    3-3. 실제 활용 예시

    // 조건부 pointer-events 적용
    function Loading({ allowInteraction = false }) {
      return (
        <div
          style={{
            position: "fixed",
            top: 0,
            left: 0,
            right: 0,
            bottom: 0,
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            zIndex: 1000,
            pointerEvents: allowInteraction ? "none" : "auto",
          }}
        >
          <div
            style={{
              width: "40px",
              height: "40px",
              border: "4px solid rgba(0, 0, 0, 0.1)",
              borderTop: "4px solid rgba(0, 0, 0, 0.3)",
              borderRadius: "50%",
              animation: "spin 1s linear infinite",
            }}
          />
        </div>
      );
    }

    3-4. 부분적 상호작용 허용

    특정 요소만 클릭 가능하게 하고 싶을 때의 방법이다.

    function Loading() {
      return (
        <div
          style={{
            position: "fixed",
            top: 0,
            left: 0,
            right: 0,
            bottom: 0,
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            zIndex: 1000,
            pointerEvents: "none", // 전체적으로 이벤트 무시
          }}
        >
          <div style={{ pointerEvents: "auto", textAlign: "center" }}>
            {/* 이 부분만 클릭 가능 */}
            <div
              style={{
                width: "40px",
                height: "40px",
                border: "4px solid rgba(0, 0, 0, 0.1)",
                borderTop: "4px solid rgba(0, 0, 0, 0.3)",
                borderRadius: "50%",
                animation: "spin 1s linear infinite",
                margin: "0 auto 20px",
              }}
            />
            <button onClick={() => console.log("취소")}>로딩 취소</button>
          </div>
        </div>
      );
    }

    4. 주의사항

    • pointer-events: none을 사용할 때는 스피너 자체도 클릭할 수 없게 된다.
    • 로딩 중에 사용자 상호작용을 완전히 차단하고 싶다면 이 방법을 사용하지 않아야 한다.
    • 접근성을 고려하여 로딩 상태를 스크린 리더에게 알려주는 것도 중요하다.
    • SVG 관련 값들은 일반 HTML 요소에서는 autonone과 동일하게 동작한다.

    5. 결론

    pointer-events: none은 로딩 스피너 뒤에서도 사용자 상호작용을 가능하게 하는 간단하고 효과적인 방법이다. 사용자 경험을 개선하면서도 로딩 상태를 명확히 표시할 수 있어 많은 상황에서 유용하게 활용할 수 있다.


    728x90
    반응형
    댓글