IOS 환경서 html-to-image png 다운로드 문제(3)

싱판다·2023년 5월 30일
1

끝난줄 알았는데..
사라지지 않았다..

간헐적인게 제일 싫은데 ㅠㅠㅠ 최악이다

우선 svg로 잘 그려지는 걸 확인했다.
근데 캔버스에 drawImage가 실행될 때 내용이 사라지는 걸 확인했다...캔버스.. 캔버스 무슨 일인데요..
왜 그릴때 배경만 사라지는 그런게 생기지? svg파일에는 있는데..
캔버스 최적화를 찾으면서 offscreenCanvas도 알게 되었는데, 이 문제에

오늘도 또다시 검색과 여러 실험을 하다 이런 글을 확인했다
https://github.com/bubkoo/html-to-image/issues/361

toPng를 여러번...실행..하면서.. 최소 데이터 길이를 가지고 확인해 빠져나오는 것이었다..

결국엔 drawImage를 여러 번 호출해, 특정 이미지 크기를 넘을 때, 빠져나오게 해서 다운로드를 하게 하는 것.

어떠한 이유인지는 모르겠으나 canvas.drawImage에서 제대로 그려지지 않는 건 맞는 사실이기에, 이전에 toSvg를 호출하고 Canvas에 그려 setTimeout으로 지연을 준 함수는 그대로 두고, 진행해보려 시도 했다.

그 과정에서 canvas 최적화, requestAnimationFrame에 대해 알게 되었고 offscreenCanvas와 requestAnimationFrame을 우선 사용해보았다.
이 과정에서 문제가 해결되긴 하였다.

offScreenCanvas를 찾아 사용했던 이유는 지푸라기라도 잡는 심정, 이거라도 최적화를 시킨다면 사파리에서 drawImage를 할 때 문제가 줄어들지 않을까 하는 심정이었다. 그리고 어차피 화면에 보이려는 것도 아니니까 사실 이름만 보면 더 의도에 적합하지 않을까 했다.
requestAnimationFrame은 위 offScreenCanvas로도 해당 문제 해결이 되지 않아 추가하였다. 위에 해결했다는 글을 보면 await toPng 구문을 여러번 호출, loop를 돌려 호출하는 방법을 택했다고 한다. 캔버스에 조금의 여유를 주면서 모두 다 그려진 데이터를 추출하고 싶었다. 그리고 그 과정에서 requestAnimationFrame을 사용했다. 그리고 빠져나오는 구문은 배경이미지가 그려질 때 최소 크기를 잡아 도달 시 빠져나오도록 했다.
한번 제대로 그려지면 그 다음은 잘 나오기 때문이었다.

그렇지만, 이 상황에서 offscreenCanvas와 requestAnimationFrame의 사용이 적절했는가에 대한 의문, 내가 다른 사람한테 왜 사용했는지에 대한 타당한 이유를 대기엔 부족함을 느꼈다.
사실 결국 지푸라기라도 잡는 심정으로 여러 방법을 시도한 것이기 때문이다.

사용한 요소에 대해 알아보면서 나의 사용이 적절한지 제거해도 되는 부분인지, 사용을 해서 정말 개선이 되었던 점이 있었는지를 알아보며 나 나름대로도 정리를 해보고자 한다.


먼저 사파리 환경에서 캔버스 안에 drawImage가 잘 되지 않는 부분에 대해 검색하면서 MDN 에 캔버스 최적화 글을 읽었다.(원글)

canvas는 웹에서 2D그래픽을 렌더링하는 데 가장 널리 사용되는 도구 중 하나로, 웹 사이트와 앱이 Canvas API를 최대한으로 한계치까지 사용하면 성능이 저하되기 시작한다.

1. 비슷한 원시 혹은 반복 객체는 표시되지 않는 캔버스(offscreenCanvas)에 미리 그려라
캔버스에 애니메이션 프레임을 그리며 반복적인 작업이 발견된다면, 눈에 보이지 않는 숨겨진 캔버스 요소를 새로 만들고 그 캔버스에 미리 그려 넣는 방법을 고려해라. 그렇게 하면 필요한 순간에 숨긴 캔버스에 그려진 이미지를 주 캔버스 이미지에 그려넣어, 불필요한 렌더링 반복 작업을 줄여 성능 향상을 꾀할 수 있다.

myCanvas.offscreenCanvas = document.createElement('canvas');
myCanvas.offscreenCanvas.width = myCanvas.width;
myCanvas.offscreenCanvas.height = myCanvas.height;

myCanvas.getContext('2d').drawImage(myCanvas.offScreenCanvas, 0, 0);

2. 부동 소수점 좌표를 피하고 대신 정수를 사용하라

ctx.drawImage(myImage, 0.3, 0.5);

실수를 사용하면 앤티 앨리어싱(anti-aliasing) 효과를 만들기 위해 브라우저에서 추가 연산을 수행한다고 한다. 이를 방지하려면 Math.floor()를 사용해 drawImage() 호출에 사용된 모든 좌표를 반올림해야 한다.

** 안티 앨리어싱 효과?

디지털 샘플링에서 샘플링 주파수가 원본 신호의 최대 주파수의 2배 보다 낮은 경우, 인접한 스팩트럼이 겹쳐서 출력이 왜곡되는 현상을 계단 현상(에일리어싱, Aliasing)이라 하며, 이는 렌더링된 컨텐츠를 재생할 때 출력되는 이미지의 픽셀조각이 튀거나, 선이나 도형의 가장자리가 우둘투둘하고 날카로워지는 형상으로 나타난다. 안티에일리어싱은 이를 해결하기 위한(Anti-) 기술이다.(참고글)

안티앨리어싱(Anti-aliasing) 효과는 앨리어싱 현상을 방지하고 그래픽스를 더 부드럽게 하는 알고리즘입니다. 앨리어싱 현상이란, 직선이 들쭉날쭉하거나 “계단” 모양으로 나타나는 경우를 의미합니다. 이 현상은 그래픽스 출력 디바이스가 직선을 표시하기에 충분히 높은 수준의 해상도를 가지고 있지 않을때 일어납니다. (참고글)

안티앨리어싱 효과는 들쭉날쭉한 선 주변에 컬러 셰이드를 덧붙여서 눈에 잘 띄지 않게 합니다. 이 효과는 직선이 들쭉날쭉해지는 현상을 줄여주지만, 직선을 흐릿하게 만듭니다.

3. drawImage에서 이미지 크기를 조절하지 마라
drawImage() 호출 시 즉시 크기를 조정하지 말고 다양한 이미지 크기를 오프스크린 캔버스에 캐싱하라.

4. 복잡한 장면에 여러 개의 레이어 캔버스를 사용하라
어플리케이션에서 일부 객체는 자주 이동하거나 변경해야하지만 다른 객체는 상대적으로 고정 위치에 남아야 한다. 이런 상황에서 대응 가능한 최적화는 여러 개의 캔버스 요소를 사용해 항목을 겹쳐서 만드는 것이다.

5. 큰 배경 이미지는 일반 CSS를 사용할 것
정적 배경 이미지가 있는 경우 CSS background 속성을 사용해 일반 <div> 요소에 그릴 수 있으며 캔버스 아래에 배치할 수 있다. 이렇게 하면 매 순간마다 배경을 캔버스에 렌더링할 필요가 없어진다.

6. CSS 변환(transform)을 사용해 캔버스 크기를 조정하라
CSS 변환(transform)은 GPU를 사용하기 때문에 더 빠르다. 가장 좋은 경우는 캔버스를 스케일링 하지 않거나, 큰 캔버스를 축소하기보다 작은 캔버스를 확대하는 것이다.

var scaleX = window.innerWidth / canvas.width;
var scaleY = window.innerHeight / canvas.height;

var scaleToFit = Math.min(scaleX, scaleY);
var scaleToCover = Math.max(scaleX, scaleY);

stage.style.transformOrigin = '0 0'; //scale from top left
stage.style.transform = 'scale(' + scaleToFit + ')';

7. 투명도를 사용하지 마라.
캔버스 사용 시 투명 배경이 필요하지 않을 경우, HTMLCanvasElement.getContext()를 사용하여 드로잉 컨텍스트를 만들 때 alpha 옵션을 false로 설정한다. 이 정보는 렌더링을 최적화하기 위해 브라우저에서 내부적으로 사용할 수 있다.

var ctx = canvas.getContext('2d', { alpha: false });

8. 캔버스 호출을 일괄적으로 처리하라. 예를 들어, 여러 개의 개별 선 대신 연속되는 선을 그려라.
9. 불필요한 캔버스 상태 변경을 피하세요.
10. 화면에 변경된 점만 렌더하고, 완전히 새로운 상태로 렌더링하지 마라.
11. 가능하면 shadowBlur 속성을 사용하지 마라.
12. 가능하면 텍스트 렌더링을 피하라. (fillText, strokeText, styling)
13. 캔버스를 지우는 여러 방법을 시도해라 (clearRect() vs fillRect() vs resizing the canvas)
14. 애니메이션에서는, window.setInterval() 대신 window.requestAnimationFrame()을 사용하세요.
15. 무거운 물리 연산 라이브러리를 주의하세요.


offscreenCanvas
화면 밖에서 렌더링되는 캔버스. 인터페이스 window 객체와 worker 객체 모두 지원한다

** OffscreenCanvas.getContext()
렌더링된 캔버스 컨텍스트 객체를 반환합니다.

사실 이것만 봐선 뭐 어떻게 동작을 하길레 최적화가 된다는 건지 이해가 안 갔다.
그래서 이 글을 봤다.

FE개발자의 성장 스토리 09 : Offscreencanvas 적용기 - Kakao
여기에 OffscreenCanvas Web API를 이용하면 DOM 엘리먼트인 canvas를 떼어내 Web Worker에서 처리함으로써 메인 쓰레드에서 동작하던 캔버스 코드를 Worker thread에서 실행해 성능을 향상할 수 있다.

뭔가 이해가 갔지만 worker 객체를 사용하지 않고 window 객체만으로도 과연 효과가 있을까 궁금해지긴 했다.

MDN에 작성된 메인 스레드에 대한 글이다.

메인 스레드
메인 스레드는 브라우저가 사용자 이벤트를 처리하고 그림을 그리는 곳입니다. 기본적으로 브라우저는 단일 스레드를 사용하여 페이지의 모든 JavaScript를 실행하고 레이아웃, 리플로우 및 가비지 수집을 수행합니다.즉, JavaScript 함수를 장시간 실행하면 스레드가 차단되어 페이지가 응답하지 않고 사용자 환경이 나빠질 수 있습니다.
서비스 작업자와 같은 웹 작업자를 의도적으로 사용하지 않는 한 JavaScript는 메인 스레드에서 실행되므로 스크립트가 이벤트 처리 또는 페인팅에서 지연을 일으키기 쉽습니다.메인 스레드에 필요한 작업이 적을수록 스레드는 사용자 이벤트에 응답하고 페인트를 칠하며 일반적으로 사용자에게 응답할 수 있습니다.

짧은 설명으로 OffscreenCanvas를 사용하면 모든 최적화될거라 생각한게 잘못이다
OffscreenCanvas 영어 문서로 다시 읽어봤다.

OffscreenCanvas 인터페이스는 요소가 화면 밖에서 렌더링할 수 있는 캔버스를 제공한다. DOM과 Canvas API를 분리하여 <canvas> 요소가 더 이상 DOM에 전적으로 의존하지 않도록 합니다. 렌더링 작업을 worker 컨텍스트 내에서 실행할 수 있어, 별도의 스레드에서 일부 작업을 실행하고 기본 스레드에서 많은 작업을 수행하지 않을 수 있다.

이를 통해 OffscreenCanvas와 Web Worker를 함께 사용하지 않으면 별로 이득이 없다는 걸 깨닫게 된다. 내가 하려는 건 정상적으로 그려질때까지 context에 drawImage를 계속 호출하는 거니까..


requestAnimationFrame

단순하게 drawImage를 반복하지만, 빠른 피드백을 원해 setInterval 대신 사용한 requestAnimationFrame이 과연 좋은 해답이었을까 싶은 생각이 있었다.

우선 setTimeout, setInterval, requestAnimationFrame에 대한 내용은 다음 포스트에 설명이 잘 되어 있었다.
setTimeout, setInterval and requestAnimationFrame

requestAnimationFrame이 얼마나 호출될지 몰라 불안한 감이 있지만 사파리에서는 2~3번 호출했을 때 정상적인 이미지를 보여주며, 다른 브라우저는 1번 호출 시에도 정상적인 이미지를 보여줬다.

setInterval이나 setTimeout 을 사용해 재귀적으로 사용도 가능하긴 하겠지만.. 그렇게 되면 다른 브라우저에서도 기다림이 너무 길어 이를 그대로 사용해도 되지 않을까 라고 생각한다.

profile
뭐든 많이 배우고 싶다

1개의 댓글

comment-user-thumbnail
2024년 4월 8일

너무 잘 봤습니다. 도움 많이 됐어요! 감사합니다

답글 달기

관련 채용 정보