Hengxi's 개발 블로그

[React] Image crop 기능 구현하기 본문

개발/React

[React] Image crop 기능 구현하기

HENGXI 2022. 10. 17. 14:45

이번에는 이미지 크롭 기능을 구현해보았다.

여러 라이브러리가 있겠지만, 사용하기 쉽고 간단한 react-easy-crop이라는 라이브러리를 사용했다.

라이브러리 사용보다는 스타일을 커스텀하는 게 더 어려웠던 듯...ㅎ

 

react-easy-crop 공식 github

 https://github.com/ricardo-ch/react-easy-crop

기본 예제 코드들을 볼 수 있어 많은 도움이 되었다.

완성된 영상부터 올려보았다.

react easy crop을 이용한 배경화면 crop 기능

 

일단 기본적인 설치부터 하자

yarn add react-easy-crop

or

npm install react-easy-crop --save

 

image crop 컴포넌트 만들어주기

//imageCropper.js
import React, { useCallback, useState } from 'react';
import Cropper from 'react-easy-crop';
import styled from 'styled-components';

const ImageCropper = ({
  croppedImage,  // crop할 이미지 
  setCroppedAreaPixels, // 이미지 {width: , height: , x: , y: } setstate, 잘린 이미지 값
  width = '4',	// 이미지 비율
  height = '2',	// 이미지 비율
  cropShape = 'none', // 이미지 모양 round 설정 시 원으로 바뀜
}) => {
  const [crop, setCrop] = useState({ x: 0, y: 0 });
  const [zoom, setZoom] = useState(1);

  const onCropComplete = useCallback((croppedAreaPixel) => {
    setCroppedAreaPixels(croppedAreaPixel);
  }, []);

  return (
    <Container>
      <Cropper
        image={croppedImage}
        crop={crop}
        zoom={zoom}
        aspect={width / height}
        onCropChange={setCrop}
        onCropComplete={onCropComplete}
        onZoomChange={setZoom}
        cropShape={cropShape}
      />
      <ZoomBox>
        <ZoomInput
          type="range"
          value={zoom}
          min={1}
          max={3}
          step={0.1}
          aria-labelledby="Zoom"
          onChange={(e) => {
            setZoom(e.target.value);
          }}
        />
      </ZoomBox>
    </Container>
  );
};

export default ImageCropper;

 

zoom 기능은 혹시 필요한 사람을 위하여 지우지는 않고 따로 설명은 스킵하려고 한다.

 

여러 곳에서 사용하기 위해서 컴포넌트로 따로 만들어주었다. props로 위 코드에도 적어 놓았지만, crop 할 이미지,  최종 원하는 크기로 자르기 위한 값 가져오기 위한 state, 상자 비율, 상자 모양을 받도록 설정했다.

props로 받아온 데이터를 react-easy-crop으로부터 import 해 온 Cropper에 보내주기만 하면 된다. 그럼 이제 사용법을 알아보자

input 버튼과 함수 만들어주기

//Img.js
import React { useRef } from 'react';
import ImageCropper from '../../../helpers/ImageCropper';
import getCroppedImg from '../../../hooks/GetCrop';

...

const [croppedImage, setCroppedImage] = useState(null);
const [croppedAreaPixels, setCroppedAreaPixels] = useState(null);
const imgInput = useRef();

...

<ImageCropper
    croppedImage={croppedImage}
    setCroppedAreaPixels={setCroppedAreaPixels}
/>
<Button
  onClick={() => {
    imgInput.current.click();
  }}
>
  배경변경
  <input type="file" ref={imgInput} onChange={handleInputImageChange} />
</Button>

 

선택된 이미지를 저장할 수 있도록 input 태그의 file 타입을 사용해주었다. useRef를 사용하여 버튼 클릭할 때 input 태그가 눌리도록 만들어주고 input의 display는 none으로 설정해준다.

이미지 저장할 때, 잘린 이미지의 값을 알기 위해 croppedAreaPixels라는 state를 만들어주어 값을 ImageCropper 컴포넌트로부터 가져왔다.(뒤에서 추가 설명)

ImageCopper가 props로 여러 데이터를 받고 있지만, 기본값을 설정해주어 이미지와, setstate만 전달!

onChange 함수

//Img.js
const handleInputImageChange = (e) => {
    const reader = new FileReader();
    if (e.target.files[0]) {
      setCropperModal(true);
      reader.readAsDataURL(e.target.files[0]);
      reader.onload = () => {
        setCroppedImage(reader.result);
      };
    }
  };

input 태그로 업로드한 이미지를 crop 하기 위해서는 선택한 이미지를 띄워주어야 하기 때문에 FileReader를 활용하여 업로드한 파일의 내용 읽어와 state에 담아주었다. 

이미지가 state에 담기면 ImgCropper가 작동하도록 설정해두었다.

자, 이제 저장버튼만 만들어 주면 완성이다.

  • blob url (URL.createObjectURL(), URL.revokeObjectURL()) 을 사용해도 되고, base64 데이터를 사용해도 된다고 합니다.

저장 버튼

// img.js
const handleCropImage = async () => {
    try {
      const cropped = await getCroppedImg(croppedImage, croppedAreaPixels);
      const data = new FormData();
      data.append('background', dataURLtoFile(cropped, 'background.png'));
      await api.put('/mypage/background', data);
      setCropperModal(false);
      setCroppedImage(null);
    } catch (e) {
      console.error(e);
    }
  };

이미지를 자른 후 저장 버튼을 만들어 주었다.

위에서 import 했던 getCroppedImg라는 함수에 자른 이미지와 자른 이미지를 계산한 값을 파라미터로 넣어준다. getCroppedImg 함수는 뒤에 설명하도록 하겠다.

함수에 넣은 값을 FormData를 통해 보내주면서 크롭 모달이 닫히고 state를 비워주었다. 비워주지 않으면 다음 변경을 눌렀을 때에 전에 crop 하던 이미지가 담겨있는 것이 보인다.

 

getCroppedImg Hook 만들기

//GetCrop.js
/**
 * This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop
 */
export default async function getCroppedImg(
  imageSrc,
  pixelCrop,
  rotation = 0,
  flip = { horizontal: false, vertical: false },
) {
  const image = await createImage(imageSrc);
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');

  if (!ctx) {
    return null;
  }

  const rotRad = getRadianAngle(rotation);

  // calculate bounding box of the rotated image
  const { width: bBoxWidth, height: bBoxHeight } = rotateSize(
    image.width,
    image.height,
    rotation,
  );

  // set canvas size to match the bounding box
  canvas.width = bBoxWidth;
  canvas.height = bBoxHeight;

  // translate canvas context to a central location to allow rotating and flipping around the center
  ctx.translate(bBoxWidth / 2, bBoxHeight / 2);
  ctx.rotate(rotRad);
  ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1);
  ctx.translate(-image.width / 2, -image.height / 2);

  // draw rotated image
  ctx.drawImage(image, 0, 0);

  // croppedAreaPixels values are bounding box relative
  // extract the cropped image using these values
  const data = ctx.getImageData(
    pixelCrop.x,
    pixelCrop.y,
    pixelCrop.width,
    pixelCrop.height,
  );

  // set canvas width to final desired crop size - this will clear existing context
  canvas.width = pixelCrop.width;
  canvas.height = pixelCrop.height;

  // paste generated rotate image at the top left corner
  ctx.putImageData(data, 0, 0);

  // As Base64 string
  return canvas.toDataURL('image/png');

  // As a blob
  //   return new Promise((resolve, reject) => {
  //     canvas.toBlob((file) => {
  //       resolve(URL.createObjectURL(file));
  //     }, 'image/jpeg');
  //   });
}

getCroppedImg라는 함수 자체를 위에 링크로 올려놓은 react-easy-crop 공식 github에서 제공하고 있다. 이 외에도 앵글, 각도 조절을 할 수 있는 함수들을 제공하고 있는 것으로 보았다.

 

이 함수를 하나하나 뜯어보지는 않았지만 crop이미지와 pixelCrop을 받아 잘린 부분을 계산하여 blob 또는 Base64 string으로 바꾸어 주는 함수이다. 마지막의 return 부분만 바꾸어주면 간단하게 원하는 값을 받을 수 있어 매우 편리했다.

 

아래는 추가로 구현한 프로필 이미지 crop기능

비율과 모양만 추가로 props로 전달하면 다른 부분은 똑같다!(zoom 기능은 웹에서만 작동 및 보이도록 설정, 앱은 손가락으로 당기면 된다.)

react easy crop을 이용한 프로필 crop 기능

 

Comments