[React] 웹 화면(DOM) 캡쳐 및 기능 구현하기 with html2canvas

Bomin·2024년 4월 29일
1

[React]

목록 보기
5/5
post-thumbnail
post-custom-banner

AI 기반 톡방 대화 분석 서비스 '대화 평화상' 프로젝트를 진행하면서 구현한 내용입니다.

배경

프로젝트 진행 중 컴포넌트를 캡쳐해서 다운받는 기능을 구현해야했다.
PM분이 요청하신 내용은 아래와 같았는데, 바로 상장 다운로드 기능이다.

목적

현재 프로젝트에서는 대화 분석 결과 페이지의 대화 평화상(or 욕쟁이상)을 리워드하는 상장 컴포넌트가 존재하는데 이를 웹에서 사진으로 저장할 수 있게 만들어야한다. 또한 해커톤에서 진행한 프로젝트로 짧은 시간안에, 최대한 빨리 구현해야만 한다.

html2canvas

먼저 DOM 캡쳐가 가능한 지 알아보면서 화면 캡쳐 라이브러리에 대해 알게 되었다. 대표적으로 html2canvas, dom-to-image, html-to-image 등이 있었다.
image

html2canvas

널리 사용되고 검증된 라이브러리. 오랜 기간 동안 많은 프로젝트에서 사용되어 왔다. proxy기능을 지원하며 다양한 웹 브라우저에서 안정적으로 작동한다. 복잡한 CSS 스타일이나 HTML 구조는 정확하게 변환되지 않을 수 있다고 한다. 또한 크고 복잡한 페이지의 경우 변환 과정에서 성능 저하가 발생할 수 있다.

dom-to-image

html2canvas와 유사하지만 성능적으로 우수하고 속도가 빠르다고 한다.

  • dom-to-image 번들 크기(약 14kb)
  • html2canvas 번들 크기(약 140kb)

html-to-image

최신 웹 기술과 CSS 속성을 더 잘 지원하여, 더 정확한 이미지 변환을 제공한다고 한다. 또한 다양한 이미지 포맷으로의 변환을 지원하며, 품질 조절 등의 추가 옵션이 가능하다.하지만 상대적으로 새로운 라이브러리이기 때문에, 레퍼런스가 다양하지 않았다.

이 중 html2canvas를 선택한 이유는 다음과 같다.

  • npm trend에서 가장 많이 사용한 것과 react와 사용한 예제와 레퍼런스들이 많아서 상대적으로 코드 이해가 간단했다.
  • 화면 전체 캡쳐가 아니기 때문에 번들 크기나 속도 등의 성능적의 문제보다 빠른 구현에 초점을 두었다.

즉, 크게 성능을 고려하지 않아도 되는 범위이기 때문에 안정성과 빠른 구현에 초점을 두고 선택하였다.

file-saver

추가적으로 file-saver 라이브러리를 사용했다.

  • 아이폰 Safari외의 브라우저나 삼성 스마트폰의 자동 다운로드 차단 등의 문제로 정상적으로 다운로드가 되지 않는 경우가 있다.
  • 따라서 브라우저 호환성을 위해 file-saver를 사용해주었다.

구현 내용

사용할 라이브러리들을 설치해준다.

npm install html2canvas  

npm install file-saver --save

npm install @types/file-saver --save-dev   //TypeScript 환경시 타입 추가

먼저 캡쳐할 컴포넌트 즉 DOM을 선택해야한다.
React에서는 DOM을 다루는 방법은 useRef를 권장하고 있다. 따라서 먼저 빈 값의 useRef로 선택할 DOM을 선언해주었다.

const divRef = useRef<HTMLDivElement>(null);

캡쳐할 DOM이 존재한다면 html2canvas에 넘겨준다. 그 후 이 변환된 canvasblob으로 변환한 뒤 file-saver를 통해 저장가능하도록 해준다.

  • html2canvas(캡쳐할 요소, options) : scale 옵션을 통해 해상도를 높일 수 있다.
  • toBlob(callback) : canvas에 포함된 이미지를 나타내는 Blob 객체를 생성한다.
  • saveAs(blob, filename): file-saver의 메소드로 Blob 객체와 이름을 전달하면 파일을 저장할 수 있게 해준다.
const canvas = await html2canvas(div, { scale: 2 });
canvas.toBlob((blob) => {
				if (blob !== null) {
					saveAs(blob, "result.png");
				}

전체 코드

import html2canvas from "html2canvas";
import saveAs from "file-saver";

export default function Reward() {

	// 1. 캡쳐할 영역 DOM 조작을 위한 useRef 선언
	const divRef = useRef<HTMLDivElement>(null);

	// 2. 다운로드 버튼 클릭 시 실행될 함수
	const handleDownload = async () => {
		if (!divRef.current) return;
		
		try {
			const div = divRef.current;
			const canvas = await html2canvas(div, { scale: 2 });

			canvas.toBlob((blob) => {
				if (blob !== null) {
					saveAs(blob, "result.png");
				}
			});
		} catch (error) {
			console.error("Error converting div to image:", error);
		}
	};

	return (
	// 스타일 코드는 생략
		<section>
			<div ref={divRef}>
				<Card/>
			</div>
			<Button onClick={handleDownload}>다운받기</Button>
		</section>
	);
}

트러블 슈팅

그런데 문제가 발생했다. 웹 화면의 그대로가 아닌 카드 컴포넌트 특정 부분이 계속해서 이상하게 저장되었다.
image

문제 원인

Card 컴포넌트 CSS 관련 문제라고 생각했다.아니면 동적으로 받아오는 데이터 때문인가. 논리 연산자 때문인가...🥺
프론트엔드 팀원분과 함께 코드를 분석해보면서 무시되는 부분에 대한 일관성이 없어서 좀 헤맸었다.
그런데...언제나 등잔 밑이 어둡다. 그저 저 컴포지션 gif 때문이었다.

image

현재 축하를 나타내는 컴포지션은 디자인 한계로 투명 배경이 아니라 카드 배경색과 동일한 gif 파일로 구성되어있다. 그래서 CSS로 구현할 때 z-index로 뒤에 배치해두었는데 이게 파일 추출 시 무시가 된 것 같았다. 검색해보니 복잡한 CSS 스타일에는 종종 오류가 날 수 있다고 한다. 아마 position 속성과 z-index 속성을 복잡하게 구현해서 html2canvas 라이브러리가 인식하지 못하는 것 같았다.

문제 해결

그럼 컴포지션을 삭제하거나 CSS를 인식가능하게 고쳐야했다. 하지만 시간관계상 CSS를 다시 다 고친다는 것은 말이 되지않았고 다른 방법에 대해 고민하던 중 html2canvas에서 제공하는 속성을 찾게 되었다.

특정 요소를 추출 렌더링 시 제외하려면 data-html2canvas-ignore 속성을 추가하면 된다고 한다.
바로 컴포지션 img 태그에 해당 속성을 추가하고 테스트 해봤다.

// Card 컴포넌트 속 img 태그 부분
<img
	src={compositon_one}
	alt="compostion_1"
	data-html2canvas-ignore="true"
/>

테스트 후 바로 결과를 공유했고 PM님과 팀원들에게 OK 컨펌을 받았다!
image

최종 결과

웹, 모바일에서 모두 정상적으로 다운로드 되는 것을 확인할 수 있었다.
image

참고자료
Blog1
Blog2

profile
Frontend-developer
post-custom-banner

2개의 댓글

comment-user-thumbnail
2024년 4월 29일

정말 좋은기능이네요!

답글 달기
comment-user-thumbnail
2024년 5월 2일

정말 좋은 기능입니다

답글 달기