My Boundary As Much As I Experienced

프론트엔드 단에서 '유저 서명이 박힌 이미지' 다운로드 기능 구현하기 (feat. canvas 태그로 이미지 로드 후 text 넣기, a태그로 download 링크 구현) 본문

FrontEnd/Frontend etc.

프론트엔드 단에서 '유저 서명이 박힌 이미지' 다운로드 기능 구현하기 (feat. canvas 태그로 이미지 로드 후 text 넣기, a태그로 download 링크 구현)

Bumang 2024. 8. 9. 10:33

내 스코어다
이미지 다운로드 뿐만 아니라 'copyLink'와 'X로 공유하기'도 구현했다.

 

구현해야 될 기능 - 이미지 다운로드 기능 구현하기

이번에 내가 제안한 아이디어를 바로 기능으로 구현할 수 있는 기회가 주어졌다.

바로, 자신의 KYC스코어를 인증서 이미지로 저장하거나,

이를 X(전 트위터)에 공유할 수 있는 기능을 구상하였다.

 

이때, 인증서 이미지를 다운받는 과정은 따로 백엔드 없이

프론트엔드에서 이미지에 텍스트만 박아서 다운로드 받게 할 수 있을 것 같았고,

서버 호출을 줄이기 위해 프론트엔드 단에서 기능구현을 마무리해보겠다고 제안했다.

 

 

구현 아이디어 1단계:  Share모달을 실행할 때 canvas에 이미지를 로드하기

useEffect에 generateImage라는 핸들러로 nickname, kycScore, maxScore 등을 제공한다.

  useEffect(() => {
    generateImage(user?.nickname ?? 'User', kycScore, maxScore);
    // eslint-disable-next-line
  }, []);

 

그리고 GenerateImage는 아래처럼 구성되어 있다.

캔버스를 포착하여 이미지를 채워넣고, describeNickname과 describeScore를 실행시켜준다.

  // generateImage함수는 아래와 같다.
  const generateImage = (
    nickname: string,
    kycScore: number,
    maxScore: number
  ) => {
    const canvas = canvasRef.current;
    if (!canvas) return; // 타입가드
    const ctx = canvas.getContext('2d')!; // 캔버스를 생성
    const image = new Image(); // 새로운 이미지를 생성
    image.src = '/SHARE.png'; // 이미지 템플릿 로드 (그런데 스코어랑 유저네임은 비어져 있는..)

    // 닉네임 새기기 함수
    const describeNickname = (nickname: string) => {
      ...
    };

    // 스코어 새기기 함수
    const describeScore = (
      kycScore: number,
      maxScore: number,
      round: number
    ) => {
      ...
    };

    // 이미지가 로드되면 캔버스에 이미지를 꽉차게 로드하고 이름과 스코어를 새긴다.
    image.onload = () => {
      canvas.width = image.width;
      canvas.height = image.height;
      ctx.drawImage(image, 0, 0);

      describeNickname(nickname);
      describeScore(kycScore, maxScore, round);
    };
  };

 

describeNickname

describeNickname은 아래처럼 생겼다.

canvas 상에 x, y 좌표를 계산하여 닉네임을 새겨준다.

    const describeNickname = (nickname: string) => {
      // 텍스트 설정
      ctx.fillStyle = '#FFFFFF';
      ctx.font = 'bold 50px Poppins';

      // 텍스트 위치 계산 (이미지 하단 중앙)
      const x = 130;
      const y = canvas.height - 170; // 하단에서 170픽셀 위

      // 텍스트 추가
      ctx.fillText(nickname, x, y);
    };

 

 

describeScore

describeScore은 아래처럼 생겼다.

이 역시 canvas 상에 x, y 좌표를 계산하여 닉네임을 새겨준다.

이때 SCORE / MAXSCORE 식으로 표기해줘야하는데,

SCORE와 MAXSCORE의 폰트스타일이 달라 한 번에 처리할 순 없었다.

그리고 SCORE가 최대 4자리, 최소 1자리여서, `${/ MAXSCORE}`의 위치가 동적으로 바뀌어야 했다.

(안 그러면 매우 벙벙해지거나 글자가 겹치거나... 둘 중 하나가 된다.)

 

그런데 찾아보니 canvas에선measureText라는 메소드가 있어서, 텍스트의 길이를 측정할 수 있었다.

const textWidth = ctx.measureText(SCORE).width;

을 통해 SCORE의 길이를 찾아내고, 적절한 위치에 MAXSCORE까지 잘 기입할 수 있었다.

const describeScore = (
      kycScore: number,
      maxScore: number,
      round: number
    ) => {
      const SCORE = kycScore.toString();
      const MAXSCORE = maxScore.toString();
      const ROUND = round.toString();

      ctx.fillStyle = '#FFFFFF';
      ctx.font = '700 128px Poppins';

      let x = 700;
      let y = 514;
      ctx.fillText(ROUND, x, y);

      // 유저스코어 텍스트 설정
      // ctx.fillStyle = '#FFFFFF';
      const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
      gradient.addColorStop(0, '#54FF60');
      gradient.addColorStop(0.2, '#0BFFC5');
      ctx.fillStyle = gradient;
      ctx.font = 'bold 100px Poppins';

      // 텍스트 위치 계산 (이미지 하단 중앙)
      x = 130;
      y = canvas.height - 370; // 하단에서 170픽셀 위
      const textWidth = ctx.measureText(SCORE).width;

      // 유저스코어 추가
      ctx.fillText(SCORE, x, y);

      // 최대스코어 설정 및 추가
      ctx.fillStyle = '#FFFFFF';
      ctx.font = '600 60px Poppins';
      x = x + textWidth;
      ctx.fillText(` / ${MAXSCORE}(max)`, x, y);
    };

 

 

구현 아이디어 2단계: <a> 태그로 브라우저 다운로드 구현하기

이제 다운로드를 구현할 차례다. 브라우저에서 다운로드를 어떻게 구현할 수 있을까?

나는 <a> 태그에 href 속성과 download 속성을 사용하여 다운로드 링크를 제공하는 방법으로 구현하였디.

a태그.. 그냥 하이퍼링크를 만드는 기능만 있는줄 알았는데 아니었다.

 

<a> 태그의 href와 download 속성

  • href 속성: <a> 태그의 href 속성은 리소스의 경로를 지정합니다. 이 리소스는 웹 페이지일 수도 있고, 파일일 수도 있습니다. 브라우저는 href에 지정된 URL을 통해 파일을 가져오게 됩니다.
  • download 속성: 이 속성은 브라우저에 해당 링크를 클릭할 때 리소스를 새 창에서 열지 않고, 파일로 다운로드하도록 지시합니다. 또한, 다운로드될 파일의 이름도 지정할 수 있습니다.

라고 한다.

 

나는 handleDownload라는 핸들러를 만들어서, 다운로드 버튼을 클릭했을 때

a태그를 절차적으로 생성하여 download파일 이름을 설정하고, a태그에 url을 설정하고,

클릭을 실행하였다.

  const handleDownload = () => {
    const canvas = canvasRef.current!;

    const link = document.createElement('a');
    link.download = `${user?.nickname}_KYC.png`;
    link.href = canvas.toDataURL('image/png');
    link.click();
  };

 

당면한 문제점 -  모바일 브라우저에선 a태그 다운로드가 막힌 브라우저가 존재

a태그를 이용한 다운로드는 pc브라우저에서는 chrome, firefox, safari 모두 다 동작하였다.

하지만 a태그가 다운로드로 동작하지 않는 모바일 브라우저가 존재했다.

(웹3 사용자들이 많이 사용하는 brave브라우저에서는 download가 제대로 작동하지 않았고,

safari에서도 동작하지 않았다..)

 

 

FileSaver.js를 사용하여 구현

그래서 하는 수 없이 javascript로 FileSaver.js라는 콘텐츠 다운로드 라이브러리를 사용했다.

fileSaver의 saveAs 함수를 통해 canvas의 이미지를 Blob데이터로 환산하여 다운로드했다.

import { saveAs } from 'file-saver';
  const handleDownload = () => {
    const canvas = canvasRef.current!;

    canvas.toBlob(function (blob: any) {
      saveAs(blob, `${user?.nickname ?? 'user'}_KYC.png`);
    });
  };