[MainProject] 커스텀 그림판 이미지 변환/서버전송
이미지 서버 전송
커스텀 페이지에서 제작한 그림판 시안을 서버로 전송하기 위한 서버 이미지 전송
구현
장바구니 버튼 레이아웃
//CartButton.tsx
import styled from "styled-components";
import modal_cart from "../../../assets/images/img_modal/modal_cart.png";
import { CartButtonContainer } from "./CartButtonContainer";
// 스타일드 컴포넌트를 사용하여 이미지 스타일링
const CartImage = styled.img`
width: 30px;
height: 30px;
margin-bottom: 8px;
cursor: pointer;
&:hover {
filter: brightness(0.8);
}
&:active {
filter: brightness(0.6);
}
`;
// 스타일드 컴포넌트를 사용하여 버튼 텍스트 스타일링
const CartButtonText = styled.span`
color: var(--white);
font-size: 12px;
`;
// CartButtonProps 타입 정의
type CartButtonProps = {
onSaveImage: () => Promise<void>,
};
// CartButton 컴포넌트 정의
function CartButton({ onSaveImage }: CartButtonProps) {
return (
<CartButtonContainer>
{/* 장바구니 아이콘 이미지 */}
<CartImage src={modal_cart} alt="Cart" onClick={onSaveImage} />
{/* 버튼 텍스트 */}
<CartButtonText>장바구니 담기</CartButtonText>
</CartButtonContainer>
);
}
export default CartButton;
CartButton
: 실제로 장바구니 버튼을 렌더링하는 컴포넌트 CartImage
컴포넌트로 장바구니 아이콘 이미지를 표시하고, CartButtonText
컴포넌트로 버튼 텍스트를 표시합니다. 이미지를 클릭하면 onSaveImage
함수가 호출됩니다.
장바구니 모달
이미지 전송
- 커스텀 그림판에서 시안을 제작한 후 서버로 이미지를 보내는 과정
고려할점- 캔버스 내부의 그림, 드래그 이미지들을 이미지 파일로 변환시켜서 s3에 저장해야한다.
- 저장하는 방법: 임시 캔버스에 기존의 그림을 다시 그려서 저장하는 방식,
Blob
형식으로 변환
Blob형식으로 변환한 이유
서버로 전송하기 위한 데이터 포맷
: 웹 애플리케이션에서 이미지를 서버로 전송할 때, 이미지 파일을 서버로 직접 업로드하는 것이 아니라 이미지 데이터를 바이너리 형태
로 변환하여 전송하는 것이 일반적이다. Blob 형식은 이러한 바이너리 데이터를 표현하기에 적합한 형식이기 때문이다.
데이터 URL의 한계
: Data URL은 이미지를 Base64
인코딩하여 문자열 형태로 표현하는 방식이다. 하지만 큰 이미지의 경우 Base64
인코딩은 데이터 양을 크게 증가시키고, 따라서 전송 시간과 성능을 저하시킬 수 있다. Blob
형식을 사용하면 데이터 양을 줄이고 전송 성능을 향상시킬 수 있다.
이미지 처리 및 저장
: Blob
형식으로 변환한 이미지는 파일로 저장하거나, 다른 이미지와 병합하거나, 서버에 업로드하는 등의 다양한 작업을 수행할 수 있다. 이러한 작업을 할 때 Blob 형식을 사용하면 효율적으로 이미지를 다룰 수 있다.
코드 분석
import { RefObject } from 'react';
import { addCustom } from '../../../api/customApis';
import { LocalStorage } from '../../../utils/browserStorage';
import { LOCAL_STORAGE_KEY_LIST } from '../../../assets/constantValue/constantValue';
// 이미지 데이터 형식 정의
type ImageData = {
imageUrl: string;
x: number;
y: number;
width: number;
height: number;
};
// 이미지를 저장하는 함수
const saveAsImage = async (
images: ImageData[],
canvasRef: RefObject<HTMLCanvasElement>,
storeId: number,
productId: number,
navigate: (path: string) => void
) => {
// 캔버스와 컨텍스트 확인
const canvas = canvasRef.current;
if (!canvas) {
return;
}
const ctx = canvas.getContext('2d');
if (!ctx) {
console.error('Context 2D is not available');
return;
}
// 임시 캔버스 생성 및 이미지 그리기
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
if (!tempCtx) {
console.error('Failed to get 2D context for temporary canvas');
return;
}
tempCanvas.width = canvas.width;
tempCanvas.height = canvas.height;
tempCtx.drawImage(canvas, 0, 0);
// Data URL 검사 함수 정의
const isDataURL = (s: string) => {
return !!s.match(/^data:image\/([a-zA-Z]*);base64,([^"]*)/);
};
// 이미지 로드 및 그리기 함수 정의
const loadAndDrawImage = async (imageData: ImageData) => {
return new Promise<void>((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
if (isDataURL(imageData.imageUrl)) {
img.src = imageData.imageUrl;
} else {
img.src = `${imageData.imageUrl}?timestamp=${new Date().getTime()}`;
}
img.onload = () => {
tempCtx.drawImage(img, imageData.x, imageData.y, imageData.width, imageData.height);
resolve();
};
img.onerror = () => {
reject(new Error(`Failed to load image at ${imageData.imageUrl}`));
};
});
};
// 이미지 로드 및 그리기 함수들을 Promise.all로 실행
await Promise.all(images.map(loadAndDrawImage));
// 이미지 데이터의 불투명한 부분 처리
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
for (let i = 0; i < imageData.data.length; i += 4) {
if (imageData.data[i + 3] === 0) {
imageData.data[i] = 255;
imageData.data[i + 1] = 255;
imageData.data[i + 2] = 255;
imageData.data[i + 3] = 255;
}
}
tempCtx.putImageData(imageData, 0, 0);
// 이미지를 Blob 형식으로 변환
const dataUrl = tempCanvas.toDataURL('image/png');
const byteString = atob(dataUrl.split(',')[1]);
const arrayBuffer = new ArrayBuffer(byteString.length);
const int8Array = new Uint8Array(arrayBuffer);
for (let i = 0; i < byteString.length; i++) {
int8Array[i] = byteString.charCodeAt(i);
}
const blob = new Blob([int8Array], { type: 'image/png' });
const file = new File([blob], 'canvas.png', { type: 'image/png' });
// 로그인 상태 확인 후 이미지 추가
const accessToken = LocalStorage.get(LOCAL_STORAGE_KEY_LIST.AccessToken);
if (accessToken) {
try {
await addCustom(storeId, productId, file);
return true;
} catch (error) {
alert('이미지 저장에 실패했습니다.');
return false;
}
} else {
alert('로그인 먼저 해주세요!');
navigate('/auth');
return false;
}
};
export default saveAsImage;
이미지 데이터 형식
// 이미지 데이터 형식 정의
type ImageData = {
imageUrl: string,
x: number,
y: number,
width: number,
height: number,
};
imageUrl
은 이미지의 URL을 나타내고, x
와 y
는 이미지가 그려질 좌표
를, width
와 height
는 이미지의 가로와 세로 크기를 나타낸다.
saveAsImage 함수
const saveAsImage = async (
images: ImageData[],
canvasRef: RefObject<HTMLCanvasElement>,
storeId: number,
productId: number,
navigate: (path: string) => void
) => {};
saveAsImage
함수는 이미지 데이터와 캔버스 레퍼런스, 상점 ID, 제품 ID, 경로 변경 함수를 인자로 받아 이미지를 저장하는 함수
이미지 가져오기
const canvas = canvasRef.current;
if (!canvas) {
return;
}
const ctx = canvas.getContext("2d");
if (!ctx) {
console.error("Context 2D is not available");
return;
}
캔버스를 가져오고, 2D 컨텍스트를 얻는 부분입니다. 캔버스와 2D 컨텍스트를 가져오지 못할 경우 에러를 출력하고 함수를 종료합니다.
const tempCanvas = document.createElement("canvas");
const tempCtx = tempCanvas.getContext("2d");
if (!tempCtx) {
console.error("Failed to get 2D context for temporary canvas");
return;
}
tempCanvas.width = canvas.width;
tempCanvas.height = canvas.height;
tempCtx.drawImage(canvas, 0, 0);
임시 캔버스
를 생성하고, 그 위에 현재 캔버스의 이미지를 복사
하여 그리는 방식 -> 이렇게 함으로써 이미지를 가공할 수 있는 임시 공간을 확보
const isDataURL = (s: string) => {
return !!s.match(/^data:image\/([a-zA-Z]*);base64,([^"]*)/);
};
isDataURL
함수는 Data URL 형식인지를 판단하는 함수 Data URL은 이미지가 Base64
로 인코딩된 형태이므로 이를 체크하기 위한 함수
const loadAndDrawImage = async (imageData: ImageData) => {
return new Promise<void>((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
if (isDataURL(imageData.imageUrl)) {
img.src = imageData.imageUrl;
} else {
img.src = `${imageData.imageUrl}?timestamp=${new Date().getTime()}`;
}
img.onload = () => {
tempCtx.drawImage(img, imageData.x, imageData.y, imageData.width, imageData.height);
resolve();
};
img.onerror = () => {
reject(new Error(`Failed to load image at ${imageData.imageUrl}`));
};
});
};
await Promise.all(images.map(loadAndDrawImage));
loadAndDrawImage
함수는 이미지를 로드하고 임시 캔버스 위에 그리는 부분 Data URL
인지 아닌지를 체크하고, 이미지를 로드한 후에 그립니다. 모든 이미지들에 대해 loadAndDrawImage
함수를 사용하여 그린 후, Promise.all
로 모든 Promise를 처리합니다.
이미지 변환
const dataUrl = tempCanvas.toDataURL("image/png");
const byteString = atob(dataUrl.split(",")[1]);
const arrayBuffer = new ArrayBuffer(byteString.length);
const int8Array = new Uint8Array(arrayBuffer);
for (let i = 0; i < byteString.length; i++) {
int8Array[i] = byteString.charCodeAt(i);
}
const blob = new Blob([int8Array], { type: "image/png" });
const file = new File([blob], "canvas.png", { type: "image/png" });
임시 캔버스를 Data URL 형식으로 변환하고, 이를 바탕으로 Blob
형식으로 변환합니다. Blob
형식을 사용하여 이미지 파일로 다룰 수 있게 된다.
const accessToken = LocalStorage.get(LOCAL_STORAGE_KEY_LIST.AccessToken);
if (accessToken) {
try {
await addCustom(storeId, productId, file);
return true;
} catch (error) {
alert("이미지 저장에 실패했습니다.");
return false;
}
} else {
alert("로그인 먼저 해주세요!");
navigate("/auth");
return false;
}
마지막으로 사용자의 로그인 상태
를 확인하고, 로그인이 되어 있다면 이미지를 서버에 저장하고, 그렇지 않다면 로그인 페이지로 이동하게 된다. 함수의 반환값은 저장 성공 여부를 나타낸다.
댓글남기기