Codesigner

[React] React로 이미지 편집(크롭) 하기 본문

React

[React] React로 이미지 편집(크롭) 하기

eunsukimme 2019. 6. 30. 23:01

SW마에스트로 프로젝트를 진행하면서 웹 상에서 이미지를 크롭 할 필요가 생겼다. 주어진 이미지에서 사용자가 특정 영역을 드래그해서 표시하면, 해당 영역을 새롭게 보여주는 기능을 구현하고자 이와 관련된 라이브러리를 찾아보았다. 그 과정에서 React-image-crop이라는 오픈 소스를 찾게 되었고,  이를 간단하게 활용해본 내용을 이번 포스팅에서 정리해보고자 한다

 

먼저, 이미지를 크롭하는 과정을 다음과 같은 프로세스로 정의하였다

  1. 사용자가 이미지를 업로드한다(영수증, 사진 등)
  2. 사용자가 업로드한 이미지를 편집(크롭)한다

이를 구현한 데모 어플리케이션은 여기에서 확인할 수 있다. 데모 어플리케이션을 구동하는 GIF 그림은 다음과 같다. 또한 모든 소스코드는 여기에서 확인할 수 있다

 

<그림 1> react-image-crop 라이브러리를 활용한 이미지 크롭 과정

 

 

 

Preview

필자가 구현하고자 한 페이지는 다음과 같은 JSX로 작성되었다

<div className="App">
  <div>
    <input type="file" onChange={this.onSelectFile} />
  </div>
  {src && (
    <ReactCrop
      src={src}
      crop={crop}
      onImageLoaded={this.onImageLoaded}
      onComplete={this.onCropComplete}
      onChange={this.onCropChange}
    />
  )}
  {croppedImageUrl && (
    <img alt="Crop" style={{ maxWidth: "100%" }} src={croppedImageUrl} />
  )}
</div>

먼저 제일 최상위 div 태그를 만들고, 그 아래에 크게 3가지 영역으로 나누었다

  1. 파일을 선택하는 영역
  2. 선택된 파일을 편집(크롭)하는 원본 이미지 영역
  3. 크롭 된 이미지를 보여주는 영역

그런 다음, 리액트 컴포넌트의 상태(state)를 다음과 같이 정의하였다

state = {
  src: null, // 업로드할 image의 src
  crop: {
    // 크롭(편집)할 이미지의 정보
    unit: "px"
  }
};

상태는 크게 두 가지로, 이미지의 경로인 src와 편집할 이미지의 정보를 담은 crop이다. 자, 이제 본격 적으로 이를 구현하는 과정을 들여다보도록 하자

 

 

 

1. 사용자가 이미지를 업로드한다

HTML5에서는 FileAPI를 제공한다. FileReader로 이미지를 쉽게 업로드하고 이를 읽어들일 수 있다

onSelectFile = e => {
  // 파일이 등록되면
  if (e.target.files && e.target.files.length > 0) {
    // HTML5 의 FileAPI 를 사용한다
    // FileReader 객체를 reader 에 저장
    const reader = new FileReader();

    // readAsDataURL로 파일을 읽는다
    reader.readAsDataURL(e.target.files[0]);

    // readAsDataURL 메서드 실행이 완료되면 onload 이벤트가 발생한다
    // 이 이벤트가 발생하면(읽기가 완료되면) 해당 이미지를 src state에 저장한다
    reader.addEventListener("load", () => {
      this.setState({ src: reader.result });
    });
  }
};

파일을 등록하는 input 태그에 onChange 이벤트 리스너 함수인 onSelectFile을 작성하여 파일의 변화를 캐치하도록 하였다. e는 바로 이러한 이벤트를 캐치하는 파라미터이다. 일단 사용자가 파일을 등록하면, FileReader 객체를 생성하여 reader에 할당한 후 readAsDataURL로 파일을 읽어 들인다

 

여기서 readAsDataURL 메서드는 이미지를 DataURI로 읽어 들인다. DataURI는 RFC 2397에 정의되어 있는 방법으로 이미지와 같은 데이터를 data:image/png; base64, iVBORw... 형태로 된 URI로 표현하는 방식이다. 실제로 이미지 링크 대신 src 속성에 DataURI를 넣으면 이미지가 표시된다

 

이어서 살펴보면, readAsDataURL 메서드의 실행이 완료되어 파일의 읽기가 완료되면, onload 이벤트가 발생한다. 해당 이벤트 리스너를 reader 객체에 저장하여 완료된 결과를 src state에 저장하도록 하였다

 

 

 

2. 사용자가 업로드한 이미지를 편집(크롭) 한다

사용자가 업로드한 이미지를 직접 크롭 할 수 있게 하기 위해 먼저 원본 이미지를 화면에 나타내고, 그 위에 크롭 하는 이미지를 나타낼 것이다

{
  src && (
    <ReactCrop
      src={src}
      crop={crop}
      onImageLoaded={this.onImageLoaded}
      onComplete={this.onCropComplete}
      onChange={this.onCropChange}
    />
  );
}

현재 state의 src가 null이 아닌 등록한 이미지의 DataURI 이면, 즉 이미지가 등록되면 ReactCrop 컴포넌트가 렌더링 되게 하였다. 컴포넌트의 prop으로 src와 crop은 현재 state의 것들을 넘겨주었다. 다음으로 세 가지 이벤트를 감지하여 이를 핸들링하는 콜백 함수를 작성하였다

 

먼저, 이미지의 로드가 완료되었을 때 호출되는 onImageLoaded()라는 콜백 함수를 작성하였다. 로드된 이미지를 파라미터로 받아 이를 ref로 만들어서 나중에 접근하기 쉽도록 하였다

onImageLoaded = image => {
  this.imageRef = image;
};

그런 다음, 이미지 크롭 중(마우스 드래그 중) 발생하는 이벤트를 감지하여 crop 오브젝트를 업데이트시켜주는 onCropChange()라는 콜백 함수를 작성하였다

onCropChange = (crop, percentCrop) => {
  // 퍼센트 크롭을 사용해도 된다:
  //this.setState({ crop: percentCrop });
  this.setState({ crop });
};

마지막으로, 크롭이 완료되면(마우스 드래그가 끝나면) 해당 영역을 보여주는 onCropComplete()라는 콜백 함수를 작성하였다

onCropComplete = (crop, percentCrop) => {
  this.makeClientCrop(crop);
};

여기에서 호출하는 makeClientCrop() 이 바로 주어진 crop을 파라미터로 받아서 캔버스에 그리는 기능을 한다. makeClientCrop() 함수의 내용은 다음과 같다

 

async makeClientCrop(crop) {
    if (this.imageRef && crop.width && crop.height) {
      // getCroppedImg() 메서드 호출한 결과값을
      // state에 반영한다
      const croppedImageUrl = await this.getCroppedImg(
        this.imageRef,
        crop,
        "newFile.jpeg"
      );
      this.setState({ croppedImageUrl });
    }
}

이 함수에서 또 getCroppedImg()라는 함수를 호출하였는데, 이 함수는 파라미터로 주어진 this.imageRef와 crop, 파일 이름(newFile.jpeg)을 받아서 크롭 한 영역의 이미지를 전달한다. makeClientCrop() 함수는 최종적으로 croppedImageUrl 변수를 state에 저장하여 이를 갖고 나중에 화면에 크롭한 이미지를 렌더링 할 수 있게 만든다

 

자, 이제 getCroppedImg() 메서드를 살펴보도록 하자

getCroppedImg(image, crop, fileName) {
    const canvas = document.createElement("canvas"); // document 상에 canvas 태그 생성
    // 캔버스 영역을 크롭한 이미지 크기 만큼 조절
    canvas.width = crop.width;
    canvas.height = crop.height;
    // getContext() 메서드를 활용하여 캔버스 렌더링 컨텍스트 함수 사용
    // 이 경우 drawImage() 메서드를 활용하여 이미지를 그린다
    const ctx = canvas.getContext("2d");

    // 화면에 크롭된 이미지를 그린다
    ctx.drawImage(
      // 원본 이미지 영역
      image, // 원본 이미지
      crop.x, // 크롭한 이미지 x 좌표
      crop.y, // 크롭한 이미지 y 좌표
      crop.width, // 크롭한 이미지 가로 길이
      crop.height, // 크롭한 이미지 세로 길이
      // 캔버스 영역
      0, // 캔버스에서 이미지 시작 x 좌표
      0, // 캔버스에서 이미지 시작 y 좌표
      crop.width, // 캔버스에서 이미지 가로 길이
      crop.height //  캔버스에서 이미지 세로 길이
    );

    // canvas 이미지를 base64 형식으로 인코딩된 URI 를 생성한 후 반환한다
    return new Promise(resolve => {
      resolve(canvas.toDataURL());
    });
}

먼저 캔버스(canvas) 요소를 생성한 뒤, 가로길이와 세로 길이를 크롭 한 이미지의 것과 같게 만들어준다. 그런 다음, 렌더링에 관련된 메서드를 사용하기 위해 getContext() 메서드를 호출하고 이를 ctx라고 하였다. 다음으로 캔버스에 이미지를 그리는 drawImage() 함수를 호출하여 적절한 옵션을 부여하여 크롭 된 이미지를 캔버스상에 나타내었다. 최종적으로 해당 캔버스에 나타난 이미지의 URI를 생성하여 이를 반환하였다

 

 

 

Review

이미지 편집(크롭) 라이브러리는 위에서 소개한 react-image-crop 말고도 cropper 등 유명한 오픈소스들이 많이 만들어져 있다. 흥미로운 오픈소스들을 많이 사용해보고, 또 뜯어보면서 컨트리뷰션까지 해보는 경험을 더 늘려야겠다고 생각했다

 

Comments