결과물
오른쪽 페이지에서 그림을 모두 그린 후, 다그렸다버튼을 클릭했을때, 아래와 같이 PNG image data 와 모든 좌표 데이터를 출력하는 것을 확인할 수 있다. PNG 파일은 multer 를 이용하여 S3 이미지 버킷에, url 은 데이터베이스에 저장하고 모든 좌표 데이터 또한 데이터베이스에 저장할 계획이다.
PNG 이미지를 이용하는 이유는,
만약 좌표데이터로 이미지를 불러와 렌더한다면 그림이 그려진 순서대로 그림이 완성되어 가는 모습이 노출되기 때문에,
이미지 데이터로 한번에 렌더하는 것이 사용자 입장(경험적인측면)에서 더 쾌적(?)할 것이라 생각하기 때문이다.
하지만 본인의 이미지를 수정할 때에는 react-canvas-draw method 인 loadSaveData(saveData: String, immediate: Boolean)를 이용하여 좌표데이터를 불러오기 때문에 그림이 그려지는 모든 과정을 직접 볼 수 있고, 마지막 지우기 버튼을 이용하여 마지막 획부터 차례대로 지워 나갈 수도 있을 것이다.
시행착오
그림판 구현을 완료한 후 데이터를 이미지 파일로 저장하기 위해 다양한 방법을 알아보았다.
해당 react-canvas-draw 라이브러리(그림판)의 경우, 이미지를 저장하는 메소드가 따로 없었기 때문이다.
1. 첫번째 시도. canvas 메소드 사용
먼저, 아래와 같이 노마드 코더님의 '그림판 구현' 강의에서의 저장방법인 canvas 엘리먼트의 내장 메소드인 toDataURL 을 사용하여 시도해 보았지만,
const saveAsPNG = () => {
const canvas = document.getElementsByClass("CanvasDraw") as HTMLCanvasElement;
const image = canvas.toDataURL("image/png");
};
그림판 라이브러리의 CanvasDraw 엘리먼트(또는 컴포넌트)가 HTML Collection 타입이었기 때문에 typescipt 의 type 에러가 지속적으로 발생했다. 에러의 주된 내용은 해당 엘리먼트를 HTMLCanvasElement 타입으로 지정하면 안된다는 것과 HTMLElement 가 HTML Collection 보다 더 많은 property 들을 가지고 있다는 것이었다.
그냥, 안된다는 말이다.
2. 두번째 시도. 스크린샷 라이브러리를 사용
여러 라이브러리가 있었지만, 타입스크립트가 유일하게 내장된 html2canvas 라이브러리를 사용해봤다.
(아래 코드는 정확하지 않고, 단지 방식을 설명하기 위함)
const screenShot = () => {
const Canvas = document.getElementsByClass("CanvasDraw") as HTMLCanvasElement;
html2canvas(Canvas).then(function(canvas) {
Canvas.appendChild(canvas);
......
});
};
저 canvas 엘리먼트가 들어가는 부분의 type 이 HTMLElement 여야 했기 때문에, 역시나 type error 가 발생했다.
이를 해결하기 위해 아래와 같이 HTMLElement 인 div 엘리먼트로 CanvasDraw 를 둘러싸는 방식을 시도했다.
const screenShot = () => {
const Canvas = document.getElementById("canvasTest") as HTMLCanvasElement;
html2canvas(Canvas).then(function(canvas) {
Canvas.appendChild(canvas);
......
});
};
<Canvas id="canvasTest">
<CanvasDraw
className="CanvasDraw"
brushRadius={brushSize}
brushColor={color}
lazyRadius={1}
catenaryColor="red"
hideGrid
ref={firstCanvas}
style={canvasStyle}
/>
</Canvas>
캡쳐는 성공적으로 실행이 되었다. 하지만, 붓 색상 변경이나 굵기 조절을 하게 되면 상태가 변화하여 모든 HTMLElement (캔버스를 둘러싸고있는 Canvas 엘리먼트를 포함)가 리렌더링 되는 바람에 그림판이 매번 최신화가 되는 오작동이 발생했다.
그렇게 3월 3일 새벽3시... 잠이 들었다.
3. 세번째 시도. 우연하게 발견한 CanvasDraw 안의 4가지 canvas 엘리먼트들. toDataURL 사용.
답답하여 이것저것 확인해보다가 개발자 도구에서 엘리먼트들을 둘러보게 되었다.
canvas 를 발견했다. 노마드코더 강의가 떠오르며 toDataURL 을 사용할 수 있지 않을까?
하는 생각이 들어 해당 라이브러리의 코드를 확인해보았다. 아래와 같이 4가지 캔버스로 이루어져 있는 것을 확인했다.
그 중에서도 두번째 canvas 가 그림을 그리는 기능에 해당하는 엘리먼트라는 것을 깨달았다.
마침내, CanvasDraw 안에 있는 두번째 canvas 엘리먼트를 사용하여 이미지 URL 을 산출해낼 수 있었다.
실제로, 모든 canvas 엘리먼트를 테스트해보면 두번째 canvas 에서 뽑아낸 URL 의 데이터가 가장 많다.(그림을 그렸기 때문)
const saveAsPNG = () => {
const canvas = document.querySelector(".CanvasDraw canvas:nth-child(2)") as HTMLCanvasElement;
const image = canvas.toDataURL("image/png");
console.log("PNG image data");
console.log(image);
};
const handleSaveClick = () => {
saveAsPNG();
const data = firstCanvas.current.getSaveData();
console.log("모든좌표 데이터", data);
// secondCanvas.current.loadSaveData(data);
};
toDataURL 메소드를 이용하여 canvas 의 base64 문자열로 가져온다. (**base64 Encoding)
, (쉼표) 뒤에서 시작하는 것이 캔버스의 base64 표현이므로 쉼표로 문자열을 분할해 인덱스 1내의 데이터만 가져온다.
*인코딩이란?
인코딩(encoding)은 정보의 형태나 형식을 표준화, 보안, 처리 속도 향상, 저장 공간 절약 등을 위해서 다른 형태나 형식으로 변환하는 처리 혹은 그 처리 방식을 말한다.
*base64 인코딩이란?
Binary Data를 Text로 바꾸는 Encoding 의 하나로써 Binary Data를 Character set에 영향을 받지 않는 공통 ASCII 영역의 문자로만 이루어진 문자열로 바꾸는 Encoding이다.
Base64를 글자 그대로 직역하면 64진법이라는 뜻이다. 64진법은 컴퓨터한테 특별한데 그 이유는 64가 2의 제곱수 64=2^6이며 2의 제곱수에 기반한 진법 중 화면에 표시되는 ASCII 문자들로 표시할 수 있는 가장 큰 진법이기 때문이다.
핵심은 Base64 Encoding은 Binary Data를 Text로 변경하는 Encoding이다.
base64 인코딩 사용이유와 장단점
사용이유
Base64로 인코딩을 하게 되면 6bit당 2bit의 Overhead가 발생하여 전송해야 될 데이터의 크기가 약 33% 정도 늘어납니다.
33%나 데이터의 크기가 증가하고, 인코딩과 디코딩의 추가 연산까지 필요한데 Base64 인코딩을 사용하는 이유는 무엇일까요?
통신과정에서 바이너리 데이터의 손실을 막기 위해 사용됩니다. 플랫폼 독립적으로 Binary Data(이미지나 오디오)를 전송할 필요가 있을 때, ASCII로 Encoding 하여 전송하게 되면 여러 가지 문제가 발생할 수 있습니다.
대표적인 문제는
- ASCII는 7 bits Encoding인데 나머지 1bit를 처리하는 방식이 시스템 별로 상이합니다.
- 일부 제어 문자 (e.g. Line ending)의 경우 시스템 별로 다른 코드값을 가집니다.
위와 같은 문제로 ASCII는 시스템 간 데이터를 전달하기에 안전하지 않습니다.
Base64는 ASCII 중 제어 문자와 일부 특수문자를 제외한 64개의 안전한 출력 문자만 사용합니다.
(* 안전한 출력 문자란 문자 코드에 영향을 받지 않는 공통 ASCII를 의미)
즉, Base64는 HTML 또는 Email과 같이 문자를 위한 Media에 Binary Data를 포함해야 될 필요가 있을 때, 포함된 Binary Data가 시스템 독립적으로 동일하게 전송 또는 저장되는 걸 보장하기 위해 사용합니다.
참고: ko.wikipedia.org/wiki/%EB%B2%A0%EC%9D%B4%EC%8A%A464#cite_ref-2
장점
- 서버에 이미지를 넣지 않아도 되므로 간단한 구현이 가능하다.
- 렌더시, 문서로딩과 같이 로딩되기에 끊기지 않고 불려온다.
단점
- 코딩시 가독성이 떨어진다.
- 용량이 커진다
256가지를 표현할 수 있는 바이트를 printable한 64가지를 사용해서 표현하니 당연하다.
다시 말해, 8비트를 6비트로 표현하는 것이다.
3개의 8비트는 4개의 6비트로 표현할 수 있다.
따라서 Base64 인코딩을 사용하면 원본보다 33%의 크기 증가가 발생한다.
출처: velog.io/@byeol4001/Base-64%EC%99%80-base64-img-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0
S3 버킷에 PNG 이미지 전송 로직 구현
const saveAsPNG = () => {
const canvas = document.querySelector(".CanvasDraw canvas:nth-child(2)") as HTMLCanvasElement;
const imgUrl = canvas.toDataURL("image/png");
console.log("PNG image data");
console.log(imgUrl);
dataURLtoFile(imgUrl);
/* 이미지 데이터로 원하는 이미지 엘리먼트에 이미지를 만들 수 있다.
const newImage = document.createElement("img");
newImage.src = image;
document.querySelector(Main)?.append(newImage);
*/
/* 이미지 파일을 다운받을수 있다.
downloadImage(image, "my-canvas.png");
*/
};
function dataURLtoFile(dataurl: string) {
const blobBin = atob(dataurl.split(",")[1]); // base64 데이터 디코딩
const array = [];
for (let i = 0; i < blobBin.length; i += 1) {
array.push(blobBin.charCodeAt(i)); //인코드된 문자들을 0번째부터 끝까지 해독하여 유니코드 값을 array 에 저장한다.
}
const u8arr = new Uint8Array(array); //8비트의 형식화 배열을 생성한다.
const file = new Blob([u8arr], { type: "image/png" }); // Blob 생성
const formdata = new FormData(); // formData 생성
formdata.append("drawImg", file); // file data 추가
console.log(file);
// axios 로 서버에 img 파일 보내기
// const imgUrl = axios.post("https://royaldiary.ml", formdata, {
// headers: { "content-Type": "multipart/form-data" },
// });
}
/* 이미지 파일 다운받는 함수
function downloadImage(data: string, filename: string) {
const a = document.createElement("a");
a.href = data;
a.download = filename;
document.body.appendChild(a);
a.click();
}
*/
window.atob() (base64 디코드 메소드):
The atob() method decodes a base-64 encoded string. base64 로 인코드된 문자열을 디코드할 때 사용하는 window 객체의 메소드
window.btoa() (base64 인코드 메소드):
자바스크립트 문자열을 base64 로 인코드할 때 사용하는 window 객체의 메소드
new Blob? Blob() 생성자?
Blob 객체는 파일류의 불변하는 미가공 데이터를 나타냅니다. 텍스트와 이진 데이터의 형태로 읽을 수 있으며, ReadableStream으로 변환한 후 그 메서드를 사용해 데이터를 처리할 수도 있습니다.
매개변수로 제공한 배열의 모든 데이터를 합친 데이터를 담은 새로운 Blob 객체를 반환합니다.
JavaScript에서 Blob(Binary Large Object, 블랍)은 이미지, 사운드, 비디오와 같은 멀티미디어 데이터를 다룰 때 사용할 수 있습니다. 대개 데이터의 크기(Byte) 및 MIME 타입을 알아내거나, 데이터를 송수신을 위한 작은 Blob 객체로 나누는 등의 작업에 사용합니다.
해야할것
- 이미지 URL 과 좌표 문자열 데이터를 서버에 보내는 로직 구현(API)
- 원고지 로직 구현
- 로그인/로그아웃/회원가입/소셜로그인 등 로직 구현 (API)
도움받은 사이트
react-canvas-draw Github: github.com/embiem/react-canvas-draw/blob/develop/src/index.js
노마드코더 이미지 저장하기: nomadcoders.co/javascript-for-beginners-2/lectures/1494
HTMLCanvasElements.toDataURL(): developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL
HTMLCollection: developer.mozilla.org/ko/docs/Web/API/HTMLCollection
-> HTML DOM 내의 HTMLCollection은 문서가 바뀔 때 실시간으로 업데이트됩니다.
HowToChangeBackgroundColor(미적용): stackoverflow.com/questions/27736288/how-to-fill-the-whole-canvas-with-specific-color
html2canvas(미적): www.npmjs.com/package/html2canvas
화면 캡쳐 라이브러리 참고 사이트(미적용):nowonbun.tistory.com/637
캔버스 데이터(canvas)를 이미지(PNG)로 변환하기
블롭이해하기: heropy.blog/2019/02/28/blob/
블롭에 파일이름 붙이기: stackoverflow.com/questions/19327749/javascript-blob-filename-without-link
캔버스 데이터 URL 사용하여 이미지 나타내기: m.mkexdev.net/106
캔버스 데이터 URL 로 이미지 다운받기: stackoverflow.com/questions/10673122/how-to-save-canvas-as-an-image-with-canvas-todataurl
'2020_BootCamp_Codestates > Final Project' 카테고리의 다른 글
20210305 17일차 회원가입 로직 구현 완료 (0) | 2021.03.06 |
---|---|
20210304 16일차 회원가입 로직 구현(미완) (0) | 2021.03.04 |
20210301 13일차 그림판 적용 (0) | 2021.03.01 |
20210228 12일차 Sub메뉴바/React 에서 페이지 이동 (0) | 2021.03.01 |
20210227 11일차 재충전 (0) | 2021.03.01 |