본문 바로가기
개발/React

리액트 - 깃허브 API로 이미지 업로드하기 (Upload Images with GitHub RESTful API)

by 피로물든딸기 2024. 1. 17.
반응형

깃허브 데스크탑으로 프로젝트 관리하기 강의 오픈!! (인프런 바로가기)

 

리액트 전체 링크

Git / GitHub 전체 링크

 

참고

- multer로 이미지 업로드하기

 

깃허브 리포지토리 이미지 불러오기
깃허브 API로 이미지 업로드하기
깃허브에 업로드된 이미지 삭제하기
캡처한 이미지를 깃허브에 업로드하기
캡처한 이미지 여러 개 업로드하기
Toast UI 에디터로 이미지를 포함한 깃허브 마크다운 저장하기

 

아래와 같이 깃허브 API를 이용하여 이미지 파일을 업로드하고, 다시 이미지를 잘 불러오는지 확인해 보자.

 

테스트하기 편하게 이전 글의 코드를 유지하고 깃허브 리포지토리에서 이미지 파일 하나만 남겨두었다.

 

ReactImageList.js

import React, { useEffect, useState } from "react";
import Box from "@mui/material/Box";
import ImageList from "@mui/material/ImageList";
import ImageListItem from "@mui/material/ImageListItem";
import ImageListItemBar from "@mui/material/ImageListItemBar";
import ListSubheader from "@mui/material/ListSubheader";
import IconButton from "@mui/material/IconButton";
import InfoIcon from "@mui/icons-material/Info";

import * as gh from "../githublibrary.js";

const ReactImageList = () => {
  const [itemData, setItemData] = useState([]);

  const fileLoad = async () => {
    let result = await gh.fileRead("images");
    let fileList = result.data.map((item) => item.path);

    let temp = [];
    let count = 1;
    for (let path of fileList) {
      let obj = {
        img: `https://github.com/bloodstrawberry/auto-test/raw/main/${path}`,
        title: `image_${count++}`,
        author: "bloodstrawberry",
      };

      temp.push(obj);
    }

    setItemData(temp);
  };

  useEffect(() => {
    fileLoad();
  }, []);

  return (
    <Box sx={{ m: 3 }}>
      <ImageList sx={{ width: 248 * 3 }} cols={3}>
        <ImageListItem key="Subheader" cols={3}>
          <ListSubheader component="div">GitHub Images Directory</ListSubheader>
        </ImageListItem>
        {itemData.map((item) => (
          <ImageListItem key={item.img}>
            <img
              srcSet={`${item.img}?w=248&fit=crop&auto=format&dpr=2 2x`}
              src={`${item.img}?w=248&fit=crop&auto=format`}
              alt={item.title}
              loading="lazy"
            />
            <ImageListItemBar
              title={item.title}
              subtitle={item.author}
              actionIcon={
                <IconButton
                  sx={{ color: "rgba(255, 255, 255, 0.54)" }}
                  aria-label={`info about ${item.title}`}
                >
                  <InfoIcon />
                </IconButton>
              }
            />
          </ImageListItem>
        ))}
      </ImageList>
    </Box>
  );
};

export default ReactImageList;

파일 업로드 버튼 추가하기

 

업로드 버튼은 다음과 같다.

// upload button
import { styled } from "@mui/material/styles";
import Button from "@mui/material/Button";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";

const VisuallyHiddenInput = styled("input")({
  clip: "rect(0 0 0 0)",
  clipPath: "inset(50%)",
  height: 1,
  overflow: "hidden",
  position: "absolute",
  bottom: 0,
  left: 0,
  whiteSpace: "nowrap",
  width: 1,
});

const ReactImageList = () => {

  ...

      <Button
        component="label"
        variant="contained"
        startIcon={<CloudUploadIcon />}
        onChange={handleFileUpload}
      >
        Upload file
        <VisuallyHiddenInput type="file" accept="image/*" multiple />
      </Button>

 

파일 업로드는 input 태그에 type="file"을 설정하면 된다.

accept업로드 가능한 파일을 설정할 수 있고, multiple여러 파일을 선택하도록 할 수 있다.

 

만약 jpegpng 확장자를 하나만 업로드하고 싶다면 아래와 같이 설정하면 된다.

<VisuallyHiddenInput type="file" accept="image/jpeg, image/png" />

 

이제 handleFileUpload를 구현해 보자.


handleFileUpload 구현

 

파일을 업로드하면 onChange에 업로드한 파일들의 정보를 알 수 있다. (target.files)

파일을 여러 개 업로드한다면 각 파일마다 github에 업로드하면 된다.

  const handleFileUpload = (e) => {
    const selectedImages = e.target.files;

    for (let selectedImage of selectedImages) {
      githubUpload(selectedImage);
    }
  };

 

githubUpload는 다음과 같다.

RESTful API로 이미지를 그대로 업로드(file write)하는 것은 불가능하다.

base64encoding 한 후 아래 API를 이용하면 이미지를 업로드할 수 있다.

  const githubUpload = (image) => {
    const reader = new FileReader();

    reader.onloadend = () => {
      const base64encoded = reader.result.split(",")[1];
      const apiURL = `https://api.github.com/repos/bloodstrawberry/auto-test/contents/images/${image.name}`;
      const accessToken = process.env.REACT_APP_MY_TOKEN;

      fetch(apiURL, {
        method: "PUT",
        headers: {
          Authorization: `token ${accessToken}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          message: "Add image",
          content: base64encoded,
          branch: "main",
        }),
      })
        .then((response) => response.json())
        .then((data) => {
          console.log("Image uploaded successfully:", data.content.name);
        })
        .catch((error) => {
          console.error("Error uploading image:", error);
        });
    };

    reader.readAsDataURL(image);
  };

 

axios 버전은 다음과 같다.

import axios from "axios";

  const githubUpload = async (image) => {
    const reader = new FileReader();
  
    reader.onloadend = async () => {
      const base64encoded = reader.result.split(",")[1];
      const apiURL = `https://api.github.com/repos/bloodstrawberry/auto-test/contents/images/${image.name}`;
      const accessToken = process.env.REACT_APP_MY_TOKEN;
  
      try {
        const response = await axios.put(
          apiURL,
          {
            message: "Add image",
            content: base64encoded,
            branch: "main",
          },
          {
            headers: {
              Authorization: `token ${accessToken}`,
              "Content-Type": "application/json",
            },
          }
        );
  
        console.log("Image uploaded successfully:", response.data.content.name);
      } catch (error) {
        console.error("Error uploading image:", error);
      }
    };
  
    reader.readAsDataURL(image);
  };
  
  const handleFileUpload = async (e) => {
    const selectedImages = e.target.files;

    for (let selectedImage of selectedImages) {
      await githubUpload(selectedImage);
    }
  };

 

참고로 apiURLEnterPrise ver의 경우 BASE URL을 설정해야 한다.

const apiURL = '[BASE URL]/api/v3/repos/YOUR_USERNAME/YOUR_REPO_NAME/contents/{path or fileName}';

충돌 해결

 

위의 코드를 토대로 여러 파일을 업로드해 보자.

 

아쉽게도 여러 파일을 선택해서 업로드하면 conflict이 발생(409 Error)한다.

아래 로그는 5개 파일을 업로드하였을 때, 2개의 파일만 성공한 것을 보여준다.

 

이 에러를 해결하기 위해, 업로드할 때마다 delay를 주자.

여러 이미지가 있는 경우 5초마다 Upload 하도록 Promise를 추가하였다.

  const handleFileUpload = (e) => {
    const selectedImages = e.target.files;
  
    const uploadImageWithDelay = async () => {
      
      githubUpload(selectedImages[0]);
      for (let i = 1; i < selectedImages.length; i++) {
        await new Promise((resolve) => {
          setTimeout(() => {
            githubUpload(selectedImages[i]);
            resolve();
          }, 5000); // 5초 지연
        });
      }
    };
  
    uploadImageWithDelay();
  };

 

또는 서버를 이용하여 파일들을 한 번에 node js의 multer로 업로드하고 서버에서 git push를 할 수도 있다.

 

전체 코드는 다음과 같다.

 

ReactImageList.js

import React, { useEffect, useState } from "react";
import Box from "@mui/material/Box";
import ImageList from "@mui/material/ImageList";
import ImageListItem from "@mui/material/ImageListItem";
import ImageListItemBar from "@mui/material/ImageListItemBar";
import ListSubheader from "@mui/material/ListSubheader";
import IconButton from "@mui/material/IconButton";
import InfoIcon from "@mui/icons-material/Info";

// upload button
import { styled } from "@mui/material/styles";
import Button from "@mui/material/Button";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";

import axios from "axios";
import * as gh from "../githublibrary.js";

const VisuallyHiddenInput = styled("input")({
  clip: "rect(0 0 0 0)",
  clipPath: "inset(50%)",
  height: 1,
  overflow: "hidden",
  position: "absolute",
  bottom: 0,
  left: 0,
  whiteSpace: "nowrap",
  width: 1,
});

const ReactImageList = () => {
  const [itemData, setItemData] = useState([]);

  const fileLoad = async () => {
    let result = await gh.fileRead("images");
    let fileList = result.data.map((item) => item.path);

    let temp = [];
    let count = 1;
    for (let path of fileList) {
      let obj = {
        img: `https://github.com/bloodstrawberry/auto-test/raw/main/${path}`,
        title: `image_${count++}`,
        author: "bloodstrawberry",
      };

      temp.push(obj);
    }

    setItemData(temp);
  };

  useEffect(() => {
    fileLoad();
  }, []);

  // const githubUpload = (image) => {
  //   const reader = new FileReader();

  //   reader.onloadend = () => {
  //     const base64encoded = reader.result.split(",")[1];
  //     const apiURL = `https://api.github.com/repos/bloodstrawberry/auto-test/contents/images/${image.name}`;
  //     const accessToken = process.env.REACT_APP_MY_TOKEN;

  //     fetch(apiURL, {
  //       method: "PUT",
  //       headers: {
  //         Authorization: `token ${accessToken}`,
  //         "Content-Type": "application/json",
  //       },
  //       body: JSON.stringify({
  //         message: "Add image",
  //         content: base64encoded,
  //         branch: "main",
  //       }),
  //     })
  //       .then((response) => response.json())
  //       .then((data) => {
  //         console.log("Image uploaded successfully:", data.content.name);
  //       })
  //       .catch((error) => {
  //         console.error("Error uploading image:", error);
  //       });
  //   };

  //   reader.readAsDataURL(image);
  // };

  const githubUpload = async (image) => {
    const reader = new FileReader();
  
    reader.onloadend = async () => {
      const base64encoded = reader.result.split(",")[1];
      const apiURL = `https://api.github.com/repos/bloodstrawberry/auto-test/contents/images/${image.name}`;
      const accessToken = process.env.REACT_APP_MY_TOKEN;
  
      try {
        const response = await axios.put(
          apiURL,
          {
            message: "Add image",
            content: base64encoded,
            branch: "main",
          },
          {
            headers: {
              Authorization: `token ${accessToken}`,
              "Content-Type": "application/json",
            },
          }
        );
  
        console.log("Image uploaded successfully:", response.data.content.name);
      } catch (error) {
        console.error("Error uploading image:", error);
      }
    };
  
    reader.readAsDataURL(image);
  };

  const handleFileUpload = (e) => {
    const selectedImages = e.target.files;
  
    const uploadImageWithDelay = async () => {
      
      githubUpload(selectedImages[0]);
      for (let i = 1; i < selectedImages.length; i++) {
        await new Promise((resolve) => {
          setTimeout(() => {
            githubUpload(selectedImages[i]);
            resolve();
          }, 5000); // 5초 delay
        });
      }
    };
  
    uploadImageWithDelay();
  };
  
  return (
    <Box sx={{ m: 3 }}>
      <Button
        component="label"
        variant="contained"
        startIcon={<CloudUploadIcon />}
        onChange={handleFileUpload}
      >
        Upload file
        <VisuallyHiddenInput type="file" accept="image/*" multiple />
      </Button>
      <ImageList sx={{ width: 248 * 3 }} cols={3}>
        <ImageListItem key="Subheader" cols={3}>
          <ListSubheader component="div">GitHub Images Directory</ListSubheader>
        </ImageListItem>
        {itemData.map((item) => (
          <ImageListItem key={item.img}>
            <img
              srcSet={`${item.img}?w=248&fit=crop&auto=format&dpr=2 2x`}
              src={`${item.img}?w=248&fit=crop&auto=format`}
              alt={item.title}
              loading="lazy"
            />
            <ImageListItemBar
              title={item.title}
              subtitle={item.author}
              actionIcon={
                <IconButton
                  sx={{ color: "rgba(255, 255, 255, 0.54)" }}
                  aria-label={`info about ${item.title}`}
                >
                  <InfoIcon />
                </IconButton>
              }
            />
          </ImageListItem>
        ))}
      </ImageList>
    </Box>
  );
};

export default ReactImageList;

 

githublibrary.js

import axios from "axios";
import { Octokit } from "@octokit/rest";

const myKey = process.env.REACT_APP_MY_TOKEN;
const repo = `auto-test`;

export const fileRead = async (path) => {
  try {
    const octokit = new Octokit({
      auth: myKey,
    });

    const result = await octokit.request(
      `GET /repos/bloodstrawberry/${repo}/contents/${path}`,
      {
        owner: "bloodstrawberry",
        repo: `${repo}`,
        path: `${path}`,
        encoding: "utf-8",
        decoding: "utf-8",
      }
    );
    return result;
  } catch (e) {
    console.log("error : ", e);
    return undefined;
  }
};
반응형

댓글