Never give up

React - image resize example 본문

WEB

React - image resize example

대기만성 개발자 2023. 7. 16. 00:07
반응형

최근에 이미지 사이즈 처리용도로 개발 해놓은 유틸을 만들었고

 

정리 + 포스팅용도 예제코드를 간단하게 만들어봤습니다

 

예제 필요없이 변환 부분만 필요하신 분들은 바로 아래 util부분만 확인하시면 되겠습니다

 

src/utils/imageResizeUtil.ts

export interface ImgSize {
  width: number;
  height: number;
}

export interface ImgInfo extends ImgSize {
  src: string;
}

const imageResizeUtil = () => {
  const readImageInfo = async (img: File | string): Promise<ImgInfo> => {
    if (typeof img === "string") {
      const size = await getImageSize(img);

      return { ...size, src: img };
    } else {
      const reader = new FileReader();
      reader.readAsDataURL(img);

      return new Promise<ImgInfo>((res, rej) => {
        try {
          reader.onload = async () => {
            const src = `${reader.result}`;

            const size = await getImageSize(src);

            res({ ...size, src });
          };
        } catch (e) {
          rej(e);
        }
      });
    }
  };

  const getImageSize = async (url: string): Promise<ImgSize> => {
    return new Promise((res) => {
      const img = new Image();

      img.src = url;

      img.onload = () => {
        const { width, height } = img;

        const size: ImgSize = { width, height };

        res(size);
      };
    });
  };

  const imageUrlToBase64 = async (url: string): Promise<string> => {
    const response = await fetch(url);
    const blob = await response.blob();
    return new Promise((res, rej) => {
      try {
        const reader = new FileReader();
        reader.onload = function () {
          res(`${this.result}`);
        };
        reader.readAsDataURL(blob);
      } catch (e) {
        rej(e);
      }
    });
  };

  const resizeImage = async (url: string, target: ImgSize): Promise<string> => {
    const img = new Image();
    img.src = url;

    return new Promise((res, rej) => {
      try {
        img.onload = () => {
          const canvas = document.createElement("canvas");
          const resizingCtx = canvas.getContext("2d")!;

          canvas.width = target.width;
          canvas.height = target.height;

          resizingCtx.drawImage(
            img,
            0,
            0,
            img.width,
            img.height,
            0,
            0,
            canvas.width,
            canvas.height
          );

          const url = canvas.toDataURL("image/jpg");

          res(url);
        };
      } catch (e) {
        rej(e);
      }
    });
  };

  return {
    imageUrlToBase64,
    resizeImage,
    readImageInfo,
  };
};

export default imageResizeUtil;

먼저 사이즈 체크 용도로 readImageInfo 함수를 보면

 

File 혹은 base64 이미지를 받아서 width와 height, base64 이미지를 반환 하는데

 

file을 받는 경우 FileReader를 이용해서 base64로 변환해주고

 

변환된 url을 getImageSize 함수로 size를 측정(?) 합니다

 

그리고 imageUrlToBase64는 해당 예제에서는 사용하지 않았지만

 

이미지 url을 받아와서 변환해야될 때 사용할 용도로 만들어봤습니다

 

(참고로 promise를 반환하는 모든 함수들 이전에 만든 completer로 대체 가능합니다)

 

src/hooks/useImgExample.ts

import { useState, ChangeEvent } from "react";
import imageResizeUtil, { ImgInfo, ImgSize } from "src/util/imageResizeUtil";

const useImgExample = () => {
  const [showDialog, setShowDialog] = useState<boolean>(false);
  const [imageList, setImageList] = useState<ImgInfo[]>([]);
  const [size, setSize] = useState<ImgSize>({
    width: 0,
    height: 0,
  });

  const imgUtil = imageResizeUtil();

  const onChangeSize = (
    e: ChangeEvent<HTMLInputElement>,
    type: keyof ImgSize
  ) => {
    const value = Number(e.target.value);

    setSize((state) => {
      state[type] = value;

      return { ...state };
    });
  };

  const imageSelect = async (e: ChangeEvent<HTMLInputElement>) => {
    const files = e.target.files;

    console.log({ files });

    let fileList: ImgInfo[] = [];

    for (let file of files ?? []) {
      const imgInfo = await imgUtil.readImageInfo(file);

      fileList = [...fileList, imgInfo];
    }

    setImageList(fileList);
  };

  const apply = async () => {
    // const base64Img = await imgUtil.imageUrlToBase64(imageList[0].src);

    const resizedImg = await imgUtil.resizeImage(imageList[0].src, size);

    const resized = await imgUtil.readImageInfo(resizedImg);

    const imgInfo = {
      ...resized,
      src: resizedImg,
    };

    setImageList([...imageList, imgInfo]);
    setShowDialog(false);
  };

  return {
    imageSelect,
    imageList,
    showDialog,
    setShowDialog,
    size,
    onChangeSize,
    apply
  }
};

export default useImgExample;

로직 처리용도로 분리해놓은 customHooks인데

 

간단하게 요약해보자면

 

1. 이미지 선택

2. 선택된 이미지 정보를 imageList에 넣어줌

3.  size 변환 후 imageList에 같이 넣어줌

 

여기서 size 변환 이후에 readImageInfo를 다시 돌리는건 낭비인데

 

정확한 이미지 크기 표시 용도로 넣어봤습니다

(고로 실제로 사용할 때는 빼시면 됩니다)

 

src/routes/resizeExample/index.tsx

import React from "react";
import ResizeDialog from "./ResizeDialog";
import ImgViewer from "./ImgViewer";
import useImgExample from "src/hooks/useImgExample";

const ImageResizeExample = () => {
  const {
    imageSelect,
    imageList,
    showDialog,
    setShowDialog,
    size,
    onChangeSize,
    apply,
  } = useImgExample();

  return (
    <div className="main_app">
      <ImgViewer
        imageSelect={imageSelect}
        imageList={imageList}
        setShowDialog={setShowDialog}
      />
      <ResizeDialog
        showDialog={showDialog}
        setShowDialog={setShowDialog}
        size={size}
        onChangeSize={onChangeSize}
        apply={apply}
      />
    </div>
  );
};

export default ImageResizeExample;

위에 만든 hooks를 필요한 부분에 잘 넣어줍니다

 

src/routes/resizeExample/ImgViewer.tsx

import React, { ChangeEvent } from "react";
import CommonBtn from "src/components/CommonBtn";
import { ImgInfo } from "src/utils/imageResizeUtil";
import styles from "./image_resize.module.css";

type ImgViewerProps = {
  imageSelect: (e: ChangeEvent<HTMLInputElement>) => void;
  imageList: ImgInfo[];
  setShowDialog: React.Dispatch<React.SetStateAction<boolean>>;
};

const ImgViewer = ({
  imageSelect,
  imageList,
  setShowDialog,
}: ImgViewerProps) => {
  return (
    <div>
      <input type="file" onChange={imageSelect} />
      {imageList.length !== 0 && (
        <div className={styles.div_imgbox}>
          <div>
            {imageList.map((e, i) => {
              return (
                <div key={`img_${i}`}>
                  <img src={e.src} />
                  <p>width : {e.width}</p>
                  <p>height : {e.height}</p>
                </div>
              );
            })}
          </div>
          <CommonBtn
            margin="20px auto 0 auto"
            onClick={() => setShowDialog(true)}>
            이미지 크기 변환
          </CommonBtn>
        </div>
      )}
    </div>
  );
};

export default ImgViewer;

이미지 선택하고 보여주는 부분 그리고 dialog 여는 버튼부분을 보여주는 부분입니다

 

src/routes/resizeExample/ResizeDialog.tsx

import React, { ChangeEvent } from "react";
import CommonBtn from "src/components/CommonBtn";
import CommonInput from "src/components/CommonInput";
import Dialog from "src/components/Dialog";
import { ImgSize } from "src/utils/imageResizeUtil";

type ResizeDialogProps = {
  showDialog: boolean;
  setShowDialog: React.Dispatch<React.SetStateAction<boolean>>;
  size: ImgSize;
  onChangeSize: (e: ChangeEvent<HTMLInputElement>, type: keyof ImgSize) => void;
  apply: () => void;
};

const ResizeDialog = ({
  showDialog,
  setShowDialog,
  size,
  onChangeSize,
  apply,
}: ResizeDialogProps) => {
  return (
    <>
      {showDialog && (
        <Dialog title="사이즈 선택" onClose={() => setShowDialog(false)}>
          <div>
            <div>
              넓이 :
              <CommonInput
                type="number"
                border="outline"
                width="60px"
                value={size.width}
                onChange={(e) => onChangeSize(e, "width")}
              />
            </div>
            <div>
              높이 :
              <CommonInput
                type="number"
                border="outline"
                width="60px"
                value={size.height}
                onChange={(e) => onChangeSize(e, "height")}
              />
            </div>
            <CommonBtn margin="10px auto 0 auto" onClick={apply}>
              확인
            </CommonBtn>
          </div>
        </Dialog>
      )}
    </>
  );
};

export default ResizeDialog;

사이즈 선택 및 적용을 하는 부분입니다

 

군데 군데 common으로 만들은 컴포넌트들은 button, input 등으로 만든 공용 컴포넌트들입니다

(vanilla extract로 갈아엎어야되는데 귀찮아서 안하고 있는애들...)

 

< 결과물 >

(자소설 부분은 못본척 해주시면 되겠습니다)

 

추가로 다시 file 객체로 만들어야되는 경우 다음과 같이 하면 되겠습니다

  const convertToFile = (img: string) => {
    const blob = dataURLtoBlob(img)
    const file = new File([blob], "fileName.jpg", { type: "image/jpg" })

    return file
  }

  const dataURLtoBlob = (dataURL: string) => {
    const byteString = atob(dataURL.split(",")[1])
    const mimeString = dataURL.split(",")[0].split(":")[1].split(";")[0]
    const ab = new ArrayBuffer(byteString.length)
    const ia = new Uint8Array(ab)
    for (let i = 0; i < byteString.length; i++) {
      ia[i] = byteString.charCodeAt(i)
    }
    return new Blob([ab], { type: mimeString })
  }

 

 

전체 소스코드는 필요하신분들이 있으면 github에 따로 올려드리겠습니다만 없을거 같으니 패스..

반응형

'WEB' 카테고리의 다른 글

React - code editor 1(feat. babel standalone)  (0) 2023.12.13
React - localization without library  (2) 2023.11.18
React, Node - SSE example  (0) 2023.03.04
React - Axios common settings  (0) 2022.12.21
React - html to doc (feat.quill2)  (0) 2022.11.15
Comments