react-zoom-pan-pinch 라이브러리에 이미지 회전 + 확대 기능 추가하기

정여름·2024년 11월 13일
0

REACT

목록 보기
1/1

사용한 라이브러리 (v3.6.1 기준)
react-zoom-pan-pinch

서론과 본론은 회고록이므로 코드만 확인하고 싶으시면 결론을 보시면 됩니다.


서론

프로젝트 진행 중 이미지 미리보기 모달을 구현할 일이 있었는데, 편집 툴이 아닌 라이브러리 중에서 이미지 확대/축소와 회전 기능을 동시에 제공하는 걸 찾지 못했다.

물론 여러 스타일 프레임워크에서 지원하는 기능이긴 하지만, 이 기능만을 위해 스타일 프레임워크를 도입하는 건 비효율적이라 판단해서 react-zoom-pan-pinch 라이브러리를 조금 손봐서 회전 후에도 확대 기능을 문제없이 사용할 수 있게 해봤다.


본론

문제점1. img 요소를 회전하면 TransformComponent와 축이 어긋남

처음엔 간단하게 img 요소에 rotate 값을 주면 되겠다고 생각했다. 하지만 그렇게 하면 img 요소의 회전만 바뀌고 TransformComponent는 회전하지 않아서, TransformWrapper 영역에 img 요소가 일부 가려지게 된다.

사진이 차지하는 영역이 img 요소이고, 파란 영역은 TransformComponent다.

이렇게 되면 img 요소는 왼쪽에 컨텐츠가 있는데, TransformComponent는 세로 방향으로 고정되어 있어서 TransformWrapper 영역 내에서 img 요소의 일부가 잘리거나 흰 여백이 생긴다.

그래서 두 번째로 생각한 건, TransformComponent도 같이 돌리면 되지 않을까? 하는 거였다.


문제점2. TransformComponent를 회전하면 마우스 이동축도 회전됨

TransformComponent와 img의 방향을 맞추려면 TransformComponent 자체를 회전시키면 해결될 줄 알았다.

문제는 TransformComponent를 회전시키면 마우스 이동축도 함께 회전된다는 거다. 그래서 화면상에서 마우스를 왼쪽으로 드래그해도 회전된 이동축 때문에 img 요소는 위로 이동하게 된다.

이 문제를 해결하려고 회전 각도에 따른 마우스 이동축 보정값을 구해보려 했지만 실패했다.


이 두 문제로 파악한 해결 조건은 다음과 같다.

  1. img 요소와 TransformComponent의 방향을 동일하게 맞출 것.
  2. TransformComponent는 회전시키지 않을 것.

실패 원인을 고찰하던 중 문득 img 요소만 회전시키되, TransformComponent의 너비와 높이를 회전된 img 요소와 같게 유지하면 되지 않을까? 하는 생각이 들었다.


문제점3. TransformComponent와 img 요소의 위치 불일치

img 요소의 너비와 높이를 상태에 저장하고, TransformComponent의 width와 height 속성에 주입하는 방식으로 이 문제는 쉽게 해결할 수 있었다. img가 회전하면 TransformComponent의 width와 height 값을 반대로 설정해주면 된다.

문제는 이렇게 설정하면 img 요소와 TransformComponent의 위치가 미세하게 어긋나 다시 1번과 같은 문제가 발생한다는 점이었다.

이를 해결하기 위해 TransformComponent에 relative 값을 주고 img 요소에 absolute 값을 주어 강제로 중앙에 위치시켜 문제를 해결했다.


결론

완성 코드

import { useEffect, useRef, useState } from "react";
import { TransformWrapper, TransformComponent, ReactZoomPanPinchRef } from "react-zoom-pan-pinch";

type ImageModalProps = {
  imageUrl: string;
};

export const ImageModal = ({ imageUrl }: ImageModalProps) => {
  const [imgRotateDeg, setImgRotateDeg] = useState(0); // 이미지 회전각도
  const [zoomScale, setZoomScale] = useState(1); // 현재 줌 배율
  const [contentDimensions, setContentDimensions] = useState<{
    width: number;
    height: number;
  }>({ width: 0, height: 0 });
  const [isImageEmpty, setIsImageEmpty] = useState(false); // 이미지 액박 확인
  const zoomInRef = useRef<() => void>();
  const zoomOutRef = useRef<() => void>();
  const resetTransformRef = useRef<() => void>();
  const imgRef = useRef<HTMLImageElement>(null); // img 요소 참조 생성
  const transformRef = useRef<ReactZoomPanPinchRef | null>(null);

  // 이미지 로드 시 크기 업데이트
  useEffect(() => {
    if (imgRef.current && !isImageEmpty) {
      const { naturalWidth, naturalHeight } = imgRef.current;
      setContentDimensions({
        width: naturalWidth,
        height: naturalHeight,
      });
    }
  }, [imageUrl, imgRotateDeg, zoomScale]);

  // 줌 변경 시 호출되는 함수
  const handleZoomChange = (ref: ReactZoomPanPinchRef) => {
    setZoomScale(ref.state.scale);
  };

  const contentStyle: React.CSSProperties = {
    width:
      imgRotateDeg % 180 === 0 && contentDimensions.width !== 0
        ? `${contentDimensions.width}px`
        : contentDimensions.height !== 0
        ? `${contentDimensions.height}px`
        : "auto",
    height:
      imgRotateDeg % 180 === 0 && contentDimensions.height !== 0
        ? `${contentDimensions.height}px`
        : contentDimensions.width !== 0
        ? `${contentDimensions.width}px`
        : "auto",
    position: "relative",
  };

  return (
    <div style={{ width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center" }}>
      <TransformWrapper
        initialScale={1}
        minScale={0.5}
        maxScale={10}
        centerOnInit
        onInit={(ref) => {
        // 줌 기능 버튼을 외부에 구현하기 위해 함수를 외부에 선언하고 주입함
          zoomInRef.current = ref.zoomIn;
          zoomOutRef.current = ref.zoomOut;
          resetTransformRef.current = ref.resetTransform;
          transformRef.current = ref;
        }}
        onZoom={(ref) => handleZoomChange(ref)}
        onZoomStop={(ref) => handleZoomChange(ref)}
      >
        <TransformComponent wrapperStyle={{ width: "100%", height: "100%" }} contentStyle={contentStyle}>
          <img
            ref={imgRef}
            src={imageUrl}
            style={{
              position: "absolute",
              top: "50%",
              left: "50%",
              transform: `translate(-50%, -50%) rotate(${imgRotateDeg}deg)`,
              transformOrigin: "center center",
              transition: "transform 200ms",
            }}
            onError={() => setIsImageEmpty(true)} // 액박 체크
          />
        </TransformComponent>
      </TransformWrapper>
      // 이미지 액박 시 예외처리
      {isImageEmpty && <div>이미지를 불러올 수 없습니다.</div>}
    </div>
  );
};

img 요소의 너비와 높이를 contentDimensions에 초기화하고, 이미지 회전 상태(imgRotateDeg)에 따라 TransformComponent에 주입할 스타일 값을 contentStyle에 정의했다.

3번 문제를 해결하기 위해 img 요소는 TransformComponent에서 중앙에 정렬되도록 스타일 값을 추가했다.

이렇게 해서 이미지 회전 후 확대를 해도 TransformWrapper의 경계를 벗어나지 않고 잘림 없이 영역 내에서 상호작용할 수 있게 되었다.


추가적으로 보완하고 싶은 부분은 이미지가 회전한 후 TransformComponent가 바로 중앙에 정렬되도록 하는 것이다. 이 부분은 적용 후에 포스팅을 업데이트할 예정이다.

끝!

profile
개발 lev.1

0개의 댓글