📌 작성자 개발 환경
OS: Mac OS
Tool: Visual Studio Code

5 분 소요

til

이미지 서버 전송

커스텀 페이지에서 제작한 그림판 시안을 서버로 전송하기 위한 서버 이미지 전송 구현

장바구니 버튼 레이아웃


//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 함수가 호출됩니다.

장바구니 모달


til

이미지 전송

  • 커스텀 그림판에서 시안을 제작한 후 서버로 이미지를 보내는 과정
    고려할점
  • 캔버스 내부의 그림, 드래그 이미지들을 이미지 파일로 변환시켜서 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는 이미지가 그려질 좌표를, widthheight는 이미지의 가로와 세로 크기를 나타낸다.

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;
}

마지막으로 사용자의 로그인 상태를 확인하고, 로그인이 되어 있다면 이미지를 서버에 저장하고, 그렇지 않다면 로그인 페이지로 이동하게 된다. 함수의 반환값은 저장 성공 여부를 나타낸다.

결과

til

댓글남기기