본문 바로가기

Develop/Front-End

배너배리에이션 기능 - 자바스크립트 캔버스 API 도전기

 

이번에 내가 맡게 된 업무는 배너 배리에이션 기능을 만드는 것이였다.

 

서비스에 없는 기능을 도입하는 것은 

언제나 두렵고 설렌다.

 

그동안 내가 개발했던 프로그램들 비중은 주로 BackEnd 영역이다..

그렇기에 개발하면서 많이 버벅거리고 답답했었지만 공식문서를 읽으면서 재미 있었던 것 같다.

이 기능을 개발하게 되는 사람들을 위해 도움이 되었으면 좋겠다 😊

 

* 배너 배리에이션 :  웹 페이지 또는 앱에서 사용되는 배너(Banner)나 광고 영역을 다양한 스타일로 변화시키는 기능을 말합니다. 이는 사용자에게 보다 다양하고 흥미로운 시각적 요소를 제공하여 웹 경험을 향상시키는 데에 사용됩니다.


 

진행상황

현재까지 기본적인 배너배리에이션 기능 틀을 갖추었고

특정 이미지 규격 파일을 업로드 하면 해당 이미지를 이용하여 여러 다양한 이미지를 자동으로 만들 수 있다.

캔버스(canvasAPI)는 줄바꿈(개행문자) 처리하는 기능이 없기에 이 기능을 만들어야 한다.


이슈 & 고민 및 해결

기능 개발 중 주로 겪은 고민은

서버에서 처리 할 지 클라이언트에서 처리 할 지에 대한 고민 이였다.

이 고민으로 시간을 소비하던 중 클라이언트에서 처리하는 걸로 결정했다.

만든 이미지를 바로 보여주어야 하였고 클라이언트와 서버간 통신이 많아지면 리소스 낭비라고 생각이 들었기 때문이다.

 

기존에 파일 업로드 할 때 사용하고 있는 오픈소스 dropzone.js를 이용하고 있었고..

(해당 코어 스크립트도 파악해야 했다.😩)

 

또 업로드 한 파일을 무해한지 확인하는 솔루션도 이용하고 있었고 

무해한 파일을 해당 솔루션 서버에 저장하고 내부 서버에서 파일을 다시 가지고 와서 저장하는 방식이였다

 

종합적으로 클라이언트에서 처리하는걸로 결정!

 

또 하나 이슈로는..

 

canvas와 html5 image tag 이미지 로드 방식 차이

cors 정책 위반으로 인해 삽질이였는데 ..

 

HTML5 이미지 태그(<img>)를 사용할 때는 보통 CORS(Cross-Origin Resource Sharing) 이슈가 발생하지 않지만
이는 이미지를 불러올 때 브라우저가 이미지 리소스에 대한 CORS 정책을 적용하지 않기 때문이고
그렇기에 다른 호스트에서 가져온 이미지를 <img> 태그를 사용하여 자유롭게 표시할 수 있었고

반대로, 캔버스(Canvas)를 사용하여 이미지를 로드할 때는 CORS 정책이 적용되어서, 
다른 호스트에서 가져온 이미지를 사용할 때 CORS 위배가 발생할 수 있고  
이는 보안 상의 이유로 인해 캔버스에서 이미지를 그리는 작업은 기본적으로 CORS 정책을 준수해야 하기 때문에
다른 호스트에서 가져온 이미지를 캔버스에 직접 로드하기 전에 CORS 정책을 설정하거나 서버 측에서 CORS를 허용 설정을 해야되는데.. 내가 이미지를 가지고 올 때는 cdn 리소스 또는 호스트 다른 주소를 가져와야 했고...

정리하자면, HTML5 이미지 태그(<img>)는 CORS 정책을 적용하지 않아 이미지 로드에 문제가 없지만, 
캔버스(Canvas)에서 이미지를 로드할 때는 다른 호스트의 리소스를 사용할 때 CORS 정책을 위배할 수 있으니 주의해야 한다... 나 처럼 삽질을 안하려면... 😥

 

cors를 우회하기 위해 잔머리를 굴려

서버에서 이미지를 가지고 오고

내부 linux 환경에 이미지가 있을 경우 Backend에서 파일 ->  base64 문자열로 encoding 하여

화면에 내려주게끔 처리 🤣


참고 레퍼런스

https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API

 

Canvas API - Web APIs | MDN

The Canvas API provides a means for drawing graphics via JavaScript and the HTML <canvas> element. Among other things, it can be used for animation, game graphics, data visualization, photo manipulation, and real-time video processing.

developer.mozilla.org


https://www.w3schools.com/html/html5_canvas.asp

 

HTML Canvas

W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, Python, SQL, Java, and many, many more.

www.w3schools.com

https://cloud.google.com/apigee/docs/api-platform/reference/policies/cors-policy?hl=ko 

 

CORS 정책  |  Apigee  |  Google Cloud

organizations.environments.sharedflows.deployments

cloud.google.com


TO DO List

1. 개행문자 처리

2. 코드 리팩토링 (불필요 코드 제거 개발 중 디버깅 콘솔 로그 제거,변수명 정리)(FE,BE)

 


Example Code

이미지를 canvas를 통해 편집하는 function 중 일부
BE코드나 더 자세한 코드는 공개하기 어려워 일부만..

// 이미지 주소를 넣으면 해당 이미지를 이용하여 편집 후 base64로 반환하는 함수
async function resizeImage(imageBase64Url, newTemplate, newWidth, newHeight, idx, callback) {
  // imgnameSchonlogo 로고 , imgnameLogo 앱아이콘 , imgnameFvconlogo 파비콘
  var imgnameFvconlogo, imgnameLogo, imgnameSchonlogo;
  try {

    if (admember != null && admember != undefined) {
      imgnameFvconlogo = `${serverImageUrl}/ad/imgfile/${admember["imgnameFvconlogo"]}`;
      imgnameLogo = `${serverImageUrl}/ad/imgfile/${admember["imgnameLogo"]}`;
      imgnameSchonlogo = `${serverImageUrl}/ad/imgfile/${admember["imgnameSchonlogo"]}`;
    }

    var canvas = document.createElement('canvas');
    var backGroundImg = document.createElement('img');
        backGroundImg.src = `${imageBase64Url}`;
    var ctx = canvas.getContext('2d');

    async function drawCanvasUtil(_type, _param) {
      if (_type === "backgroundSetting") {
        ctx.drawImage(backGroundImg, 0, 0, newWidth, newHeight);
      } else if (_type === "backgroundSettingCustom") {
        ctx.fillStyle = _param["fillStyle"];
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.drawImage(backGroundImg, _param["x"], _param["y"], _param["width"], _param["height"]);
      } else if (_type === "backgroundSettingCustom2") {
        ctx.drawImage(backGroundImg, _param["imgX"], _param["imgY"], _param["imgWidth"], _param["imgHeight"]);
        ctx.fillStyle = _param["fillStyle"];
        ctx.fillRect(_param["x"], _param["y"], _param["width"], _param["height"]);
      } else if (_type === "fillStyle") {
        ctx.fillStyle = _param["color"];
      } else if (_type === "copyText" || _type === "Text") {
        if (_type === "copyText" && firstCopy === null || firstCopy === undefined) {
          return false;
        }
        ctx.font = _param["font"];
        ctx.fillStyle = _param["fillStyle"];
        ctx.textAlign = _param["textAlign"];
        ctx.textBaseline = _param["textBaseline"];
        ctx.fillText(_param["text"], _param["x"], _param["y"]);
      } else if (_type === "ctaBtn") {
        ctx.strokeStyle = _param["fillStyle"];
        ctx.fillStyle = _param["fillStyle"];
        ctx.beginPath();
        ctx.roundRect(_param["x"], _param["y"], _param["width"], _param["height"], _param["radii"]);
        ctx.stroke();
        ctx.fill();

        ctx.font = _param["font"];
        ctx.fillStyle = _param["fontFillStyle"];
        ctx.textAlign = _param["textAlign"];
        ctx.textBaseline = _param["textBaseline"];
        ctx.fillText(_param["text"], _param["textX"], _param["textY"]);
      } else if (_type === "drawImage") {
        return new Promise((resolve, reject) => {
          var validCheck =  _param["src"].replace(`${serverImageUrl}/ad/imgfile/`,"");
          if(validCheck === null || validCheck === undefined || validCheck === "" || validCheck === "null" ){
            resolve();
            return false;
          }

          var additionalImage = new Image();
              additionalImage.crossOrigin = `${serverImageUrl}`;
              additionalImage.onerror = function () {
            console.error("image load Error");
            resolve();
          };
          additionalImage.onload = function () {
            ctx.drawImage(additionalImage, _param["dx"], _param["dy"], _param["dWidth"], _param["dHeight"]);
            //console.debug(`image load complete${_param["src"]}`);
            resolve();
          };
          additionalImage.src = _param["src"];
        });
      }
    }

    backGroundImg.onload = async function () {
      if (isNaN(newWidth))
        newWidth = Number(newWidth);
      if (isNaN(newHeight))
        newWidth = Number(newHeight);

      canvas.width = newWidth;
      canvas.height = newHeight;

      switch (newTemplate) {
        case "850x850":
          await drawCanvasUtil("backgroundSetting");
          break;
        case "970x250":
          await drawCanvasUtil("backgroundSettingCustom", { "fillStyle": "#FFFFFF", "x": 0, "y": 0, "width": 250, "height": 250 });
          await drawCanvasUtil("Text", { "font": "bold 25px NotoSansB", "fillStyle": "#000000", "textAlign": "start", "textBaseline": "middle", "text": admember["corpname"], "x": 270, "y": 91 });
          await drawCanvasUtil("copyText", { "font": "20px NotoSansR", "fillStyle": "#000000", "textAlign": "start", "textBaseline": "middle", "text": firstCopy[1]["cpComment"], "x": 270, "y": 135 });
          await drawCanvasUtil("ctaBtn", { "fillStyle": "#85b74e", "x": 745, "y": 100, "width": 200, "height": 50, "radii": 4, "font": "18px NotoSansR", "fontFillStyle": "#FFFFFF", "textAlign": "start", "textBaseline": "middle", "text": "바로가기", "textX": 812, "textY": 125.5 });
          break;
        case "800x1500":
          await drawCanvasUtil("backgroundSettingCustom", { "fillStyle": "#FFFFFF", "x": 0, "y": 315, "width": 800, "height": 802 });
          await drawCanvasUtil("drawImage", { "src": imgnameSchonlogo, "dx": 251, "dy": 108, "dWidth": 299, "dHeight": 100 });
          await drawCanvasUtil("drawImage", { "src": imgnameLogo, "dx": 67, "dy": 1234, "dWidth": 300, "dHeight": 150 });
          await drawCanvasUtil("Text", { "font": "bold 25px NotoSansB", "fillStyle": "#000000", "textAlign": "start", "textBaseline": "middle", "text": admember["corpname"], "x": 435, "y": 1270});
          await drawCanvasUtil("copyText", { "font": "20px NotoSansR", "fillStyle": "#000000", "textAlign": "start", "textBaseline": "middle", "text": firstCopy[1]["cpComment"], "x": 435, "y": 1300 });
          await drawCanvasUtil("ctaBtn", { "fillStyle": "#85b74e", "x": 433, "y": 1343, "width": 300, "height": 50, "radii": 4, "font": "18px NotoSansR", "fontFillStyle": "#FFFFFF", "textAlign": "center", "textBaseline": "middle", "text": "바로가기", "textX": 570, "textY": 1370 });
          break;
        case "300x300":
          await drawCanvasUtil("backgroundSetting");
          break;
        case "336x280":
          await drawCanvasUtil("backgroundSettingCustom", { "fillStyle": "#FFFFFF", "x": 0, "y": 0, "width": 336, "height": 180 });
          await drawCanvasUtil("copyText", { "font": "13px NotoSansR", "fillStyle": "#000000", "textAlign": "center", "textBaseline": "middle", "text": firstCopy[1]["cpComment"], "x": 160, "y": 209 });
          await drawCanvasUtil("ctaBtn", { "fillStyle": "#85b74e", "x": 121, "y": 227, "width": 95, "height": 35, "radii": 4, "font": "16px NotoSansR", "fontFillStyle": "#FFFFFF", "textAlign": "center", "textBaseline": "middle", "text": "바로가기", "textX": 170, "textY": 245 });
          break;
        case "720x1230":
          await drawCanvasUtil("backgroundSettingCustom", { "fillStyle": "#FFFFFF", "x": 0, "y": 254, "width": 720, "height": 722 });
          await drawCanvasUtil("drawImage", { "src": imgnameSchonlogo, "dx": 260, "dy": 72, "dWidth": 200, "dHeight": 67 });
          await drawCanvasUtil("Text", { "font": "bold 25px NotoSansB", "fillStyle": "#000000", "textAlign": "center", "textBaseline": "middle", "text": admember["corpname"], "x": 348.5, "y": 159 });
          await drawCanvasUtil("ctaBtn", { "fillStyle": "#85b74e", "x": 210, "y": 1078, "width": 300, "height": 50, "radii": 4, "font": "18px NotoSansR", "fontFillStyle": "#FFFFFF", "textAlign": "center", "textBaseline": "middle", "text": "바로가기", "textX": 350, "textY": 1105 });
          break;
        case "320x200":
          await drawCanvasUtil("backgroundSettingCustom", { "fillStyle": "#FFFFFF", "x": 0, "y": 0, "width": 200, "height": 200 });
          await drawCanvasUtil("drawImage", { "src": imgnameSchonlogo, "dx": 210, "dy": 50, "dWidth": 100, "dHeight": 36 });
          await drawCanvasUtil("ctaBtn", { "fillStyle": "#85b74e", "x": 213, "y": 115, "width": 95, "height": 35, "radii": 4, "font": "16px NotoSansR", "fontFillStyle": "#FFFFFF", "textAlign": "center", "textBaseline": "middle", "text": "바로가기", "textX": 260, "textY": 135 });
          break;
        case "300x180":
          await drawCanvasUtil("backgroundSettingCustom", { "fillStyle": "#FFFFFF", "x": 0, "y": 0, "width": 180, "height": 180 });
          await drawCanvasUtil("drawImage", { "src": imgnameLogo, "dx": 215, "dy": 43, "dWidth": 50, "dHeight": 50 });
          await drawCanvasUtil("copyText", { "font": "bold 9px NotoSansB", "fillStyle": "#000000", "textAlign": "center", "textBaseline": "middle", "text": admember["corpname"], "x": 240, "y": 114 });
          break;
        case "300x150":
          await drawCanvasUtil("backgroundSettingCustom", { "fillStyle": "#FFFFFF", "x": 0, "y": 0, "width": 150, "height": 150 });
          await drawCanvasUtil("ctaBtn", { "fillStyle": "#85b74e", "x": 178, "y": 77, "width": 95, "height": 35, "radii": 4, "font": "16px NotoSansR", "fontFillStyle": "#FFFFFF", "textAlign": "center", "textBaseline": "middle", "text": "바로가기", "textX": 225, "textY": 94 });
          await drawCanvasUtil("copyText", { "font": "9px NotoSansR", "fillStyle": "#000000", "textAlign": "start", "textBaseline": "middle", "text": firstCopy[2]["cpComment"], "x": 160, "y": 45 });
          break;
        case "210x300":
          await drawCanvasUtil("backgroundSettingCustom2", { "fillStyle": "#FFFFFF", "imgX": 0, "imgY": 0, "imgWidth": 210, "imgHeight": 210, "x": 0, "y": 210, "width": 210, "height": 90 });
          await drawCanvasUtil("ctaBtn", { "fillStyle": "#85b74e", "x": 58, "y": 237, "width": 95, "height": 35, "radii": 4, "font": "16px NotoSansR", "fontFillStyle": "#FFFFFF", "textAlign": "center", "textBaseline": "middle", "text": "바로가기", "textX": 105.5, "textY": 254.5 });
          break;
      }

      var resizedImage = canvas.toDataURL('image/jpeg');
      callback(null, resizedImage, newTemplate, idx);
    };
  } catch (error) {
    console.error(error);
    callback(error, null, newTemplate, idx);
  }
}

 

반응형