종식당

현대오토에버 모빌리티 SW 스쿨 웹/앱 스프링부트 파일 업로드 2 본문

현대오토에버 모빌리티 sw 스쿨

현대오토에버 모빌리티 SW 스쿨 웹/앱 스프링부트 파일 업로드 2

종식당 2024. 10. 16. 20:37
728x90
반응형

 

저번 파일 업로드에 이어서 리액트를 이용해서 프런트엔드랑 벡엔드를 연결해서 진행하는 작업을 해보려 한다.

 

 

현대오토에버 모빌리티 SW 스쿨 웹/앱 스프링부트 파일 업로드

이번 시간에는 스프링 부트를 통해 파일 업로드하는 것을 간단하게 알아보려 한다. 이미지를 업로드하는 과정을 알아보겠다.파일 구조  간단하게라도 다양한 프로젝트를 만들어보면서 배운

jxxng-syykk.tistory.com

 


Controller 코드 추가

@GetMapping("/filelist")
	public ResponseEntity<List<FileEntity>> getFileDataList() {

		List<FileEntity> fileDataList = fileDataService.findAll();
		if (!fileDataList.isEmpty()) {
			return ResponseEntity.status(HttpStatus.OK).body(fileDataList);
		} else {
			return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
		}
	}

 

fileDataService.findAll() 메서드를 통해 데이터베이스에서 파일 목록을 가져온다.

만약 파일 목록이 비어있으면 204 No Content상태 코드를 반환하며 body 없이 상태 코드만을 반환한다.

파일 목록에 데이터가 있으면 HTTP상태 코드는 200 OK로 설정하고 body에 fileDataList를 담아서 응답을 보낸다.

Service 코드 추가

public List<FileEntity> findAll() {
		return filedDataRepository.findAll();
	}

 

모든 파일의 정보를 가져오기 위해서 추가해주었다. 

Postman을 통해 확인

 

데이터베이스에 저장되어 있는 파일들의 정보를 모두 가져오는 모습을 확인할 수 있다.


 

위 내용을 바탕으로 리액트를 이용해서 프론트엔드와 작업하기 위해 연결을 해줄 것이다. 

 

React프로젝트 생성

 

 

파일 구조

.env.development파일

 

. env.development파일은 환경 변수를 정의하는 파일이다. React 애플리케이션에서는 모든 환경 변수 이름이 REACT_APP_으로 시작해야 빌드 시 정상적으로 인식된다고 한다👌 👌 👌

 

스프링부트 서버가 실행 중인 서버의 주소가  "http://localhost:8080/api"이기 때문에 이렇게 설정해 주었다.

 

React에서 환경 변수 사용

  • 이 환경 변수는 React 애플리케이션 내에서 process.env.REACT_APP_DEV_HOST로 접근할 수 있고 이를 통해 코드에서 백엔드 API의 기본 URL을 동적으로 설정할 수 있다.

 

apiFile.js

import axios from "axios";
const baseURL = process.env.REACT_APP_DEV_HOST;

const axiosInstance = axios.create({
  baseURL,
  headers: {
    "Content-Type": "application/json",
  },
});

export const postFile = async (data) => {
  try {
    const response = await axiosInstance.post("/file", data, {
      headers: {
        "Content-Type": "multipart/form-data",
      },
    });
    return response.data;
  } catch (error) {
    throw error;
  }
};

export const getFileList = async (data) => {
  try {
    const response = await axiosInstance.get("/filelist");
    return response.data;
  } catch (error) {
    throw error;
  }
};

export const getFileById = async (id) => {
  try {
    const response = await axiosInstance.get(`/file/${id}`);
    return response.data;
  } catch (error) {
    throw error;
  }
};

 

axiosInstance생성

import axios from "axios";
const baseURL = process.env.REACT_APP_DEV_HOST;

const axiosInstance = axios.create({
  baseURL,
  headers: {
    "Content-Type": "application/json",
  },
});

 

axios.create()를 통해 Axios인스턴스를 만들었다. axiosInstance를 가지고 모든 요청에 대해 시본 설정을 적용할 수 있다.

위에서 봤던 거처럼 가지고 baseURL을 설정해 주었다. 

기본적으로 모든 요청에서 Content-Type을 application/json으로 설정하여 JSON 형식으로 데이터 전송을 처리하도록 하였다.

위처럼 설정을 해주고 나면 이제 모든 요청들은 자동으로 http://localhost:8080/api 이 URL을 기반으로 전송된다. 

그리고 이제 axiosInstance를 사용하여 get, post 등 여러 종류의 HTTP 요청을 사용할 수 있다.

 

postFile함수

export const postFile = async (data) => {
  try {
    const response = await axiosInstance.post("/file", data, {
      headers: {
        "Content-Type": "multipart/form-data",
      },
    });
    return response.data;
  } catch (error) {
    throw error;
  }
};

 

이 함수는 /file 엔드포인트로 POST요청을 보내며 이때 data는 전송할 파일 데이터이다.
그리고 여러 개의 파일을 보내기 위해서 Content-Type을 multipart/form-data로 설정하였다.

@PostMapping("/file")
	public ResponseEntity<?> uploadImage(@RequestParam(name = "images") List<MultipartFile> files) {
		List<String> uploadResult = files.stream().map(file -> {
			try {
				return fileDataService.uploadImageToFileSystem(file);
			} catch (IOException e) {
				e.printStackTrace();
				return "Failed to upload: " + file.getOriginalFilename(); // 실패 메시지를 반환
			}
		}).collect(Collectors.toList());

		return ResponseEntity.status(HttpStatus.OK).body(uploadResult); // 업로드 결과를 반환
	}

 

리액트에서 post요청을 보내면 스프링 부트의 controller에서 postmapping을 통해 데이터를 주고받을 수 있다.

getFileList함수

export const getFileList = async (data) => {
  try {
    const response = await axiosInstance.get("/filelist");
    return response.data;
  } catch (error) {
    throw error;
  }
};

 

/filelist엔드포인트로 GET요청을 보내며 파일의 목록들을 가져온다. 그리고 서버에서 반환된 파일의 목록들을 response.data에 담아서 반환한다.

@GetMapping("/filelist")
	public ResponseEntity<List<FileEntity>> getFileDataList() {

		List<FileEntity> fileDataList = fileDataService.findAll();
		if (!fileDataList.isEmpty()) {
			return ResponseEntity.status(HttpStatus.OK).body(fileDataList);
		} else {
			return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
		}
	}

 

위 함수 또한 리액트에서 get요청을 보내면 스프링부트에서 해당 URL의 getmapping이 작동되고 해당 함수가 작동되어 파일의 목록들을 가져오는 원리이다.

getFileById함수

export const getFileById = async (id) => {
  try {
    const response = await axiosInstance.get(`/file/${id}`);
    return response.data;
  } catch (error) {
    throw error;
  }
};


파일 ID기반으로 서버로 get요청을 보내서 해당 ID에 맞는 파일 정보를 가져온다.

@GetMapping("/file/{id}")
	public ResponseEntity<?> downImage(@PathVariable(name = "id") Long id) throws IOException {
		byte[] downLoadImage = fileDataService.downLoadImageFileSystem(id);

		if (downLoadImage != null) {
			return ResponseEntity.status(HttpStatus.OK).contentType(MediaType.valueOf("image/png")).body(downLoadImage);
		} else {
			return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
		}

	}

 

해당 getmapping에 작동하여 서버에서 ID기반으로 파일의 정보를 가져온다.


App.js

import { ToastContainer } from "react-toastify";
import { getFileList } from "./api/apiFile";
import "./App.css";
import "react-toastify/ReactToastify.css";
import { useEffect, useState } from "react";
import ImageList from "./components/ImageList";
import UploadFile from "./components/UploadFile";

function App() {
  const [imgData, setImgData] = useState([]);
  const [imageUpload, setImageUpload] = useState(false);

  useEffect(() => {
    fetchData();
  }, [imageUpload]); //imageUpload가 변화가 될때!

  const fetchData = async () => {
    const updateImgList = await getFileList();
    setImgData(updateImgList);
  };

  const handleImageUpload = () => {
    setImageUpload(!imageUpload);
  };

  return (
    <div className="App">
      <h2>사진첩</h2>

      <UploadFile handleImageUpload={handleImageUpload} />
      <ImageList imgData={imgData} />
      <ToastContainer
        position="bottom-center"
        autoClose={5000}
        hideProgressBar={false}
        newestOnTop={false}
        closeOnClick
        rtl={false}
        pauseOnFocusLoss
        draggable
        pauseOnHover
        theme="light"
      />
    </div>
  );
}

export default App;

 

 

return (
  <div className="App">
    <h2>사진첩</h2>

    <UploadFile handleImageUpload={handleImageUpload} />
    <ImageList imgData={imgData} />
    <ToastContainer
      position="bottom-center"
      autoClose={5000}
      hideProgressBar={false}
      newestOnTop={false}
      closeOnClick
      rtl={false}
      pauseOnFocusLoss
      draggable
      pauseOnHover
      theme="light"
    />
  </div>
);

 

먼저 렌더링 하는 부분부터 살펴보겠다.

크게 UploadFile 컴포넌트, ImageList 컴포넌트, react-toastify의 ToastContainer 컴포넌트가 있다. 이렇게 3개의 컴포넌트별로 더 자세히 살펴보려 한다.

UploadFile컴포넌트

import React, { useState } from "react";
import { postFile } from "../api/apiFile";
import { toast } from "react-toastify";

function UploadFile({ handleImageUpload }) {
  const [files, setFiles] = useState([]);
  const [fileName, setFileName] = useState("이미지를 업로드 해주세요!");

  const imageSelectHandler = (e) => {
    const imageFiles = e.target.files; //[0]이렇게 쓰면 이미지 한장만 사용할 수 있음
    console.log(imageFiles);

    if (imageFiles.length > 0) {
      setFiles(imageFiles);
      setFileName(imageFiles[0].name);
    } else {
      setFiles(null);
      setFileName("이미지를 업로드 해주세요!");
    }
  };

  const onSubmit = async (e) => {
    e.preventDefault(); //이러면 새로고침(깜빡이는 현상) 일어나지 않음
    const formData = new FormData();

    for (let file of files) formData.append("images", file);

    try {
      const res = await postFile(formData);
      console.log(res);
      handleImageUpload();
      toast.success("이미지 업로드 성공");
    } catch (error) {
      toast.error("이미지 업로드 실패");
    }
  };

  return (
    <>
      <div>
        <form onSubmit={onSubmit}>
          <div>
            {fileName}
            <br />
            <input
              type="file"
              name="image"
              id="inp"
              multiple
              onChange={imageSelectHandler}
            />
          </div>
          <button type="submit">파일 전송</button>
        </form>
      </div>
    </>
  );
}

export default UploadFile;

 

여기서도 렌더링 하는 부분부터 살펴보면 이미지 파일을 선택하고 업로드하는 폼이다. 파일을 선택하면 onChange를 통해 imageSelectHandler함수가 호출된다. 

const imageSelectHandler = (e) => {
  const imageFiles = e.target.files; // 사용자가 선택한 파일 목록
  console.log(imageFiles);

  if (imageFiles.length > 0) {
    setFiles(imageFiles); // 선택한 파일들을 상태로 저장
    setFileName(imageFiles[0].name); // 첫 번째 파일 이름을 상태로 저장
  } else {
    setFiles(null); // 파일이 없으면 상태를 초기화
    setFileName("이미지를 업로드 해주세요!");
  }
};

 

e.target.files를 통해 사용자가 선택한 파일 목록을 받아서 imageFiles에 저장하고 이를 useState를 통해 files와 fileName를 set 해준다.

const onSubmit = async (e) => {
  e.preventDefault(); // 새로고침 방지

  const formData = new FormData(); // FormData 객체 생성
  for (let file of files) formData.append("images", file); // 선택한 파일을 FormData에 추가

  try {
    const res = await postFile(formData); // 서버로 파일 전송
    console.log(res);
    handleImageUpload(); // 파일 업로드 성공 시 부모 컴포넌트에게 알림 (목록 새로고침)
    toast.success("이미지 업로드 성공"); // 성공 알림
  } catch (error) {
    toast.error("이미지 업로드 실패"); // 실패 알림
  }
};

 

또한 파일 전송 버튼을 누르면 위 함수가 호출되는데  먼저 FormData객체를 생성한다. 그리고 위에서 useState를 통해 set 된 files 즉 파일들의 목로를 for문을 통해 돌면서 formData에 append 해준다. 

이때 append에서 첫 번째 인자는 key역할을 하고 두 번째 인자는 value 즉 실제 데이터이다. 여기서는 파일이라고 할 수 있다.

그리고 apiFile에서 정의한 함수 postFile함수를 호출해서 formData를 담아서 post요청을 보낸다. 

파일 전송에 성공하면 handleImageUpload함수를 통해 목록을 새로고침을 하는데 이는 뒤에서 가시 알아보겠다. 그리고 toast메시지를 통해서 성공과 실패를 알린다. 

 

<ToastContainer
        position="bottom-center"
        autoClose={5000}
        hideProgressBar={false}
        newestOnTop={false}
        closeOnClick
        rtl={false}
        pauseOnFocusLoss
        draggable
        pauseOnHover
        theme="light"
      />

 

이런 식으로 ToastContainer에서 설정한 옵션을 바탕으로 toast메시지가 뜨게 된다. 

 

https://www.npmjs.com/package/react-toastify?activeTab=readme

 

react-toastify

React notification made easy. Latest version: 10.0.6, last published: 3 days ago. Start using react-toastify in your project by running `npm i react-toastify`. There are 2637 other projects in the npm registry using react-toastify.

www.npmjs.com

 

react-toastify에 관해서는 위 사이트를 참고하면 좋을 듯하다. 


ImageList컴포넌트

import React from "react";

function ImageList({ imgData }) {
  return (
    <div style={{ display: "flex", flexWrap: "wrap", gap: 10 }}>
      {imgData.length}
      {imgData.length > 0 ? (
        imgData.map((item, i) => {
          return (
            <div key={i}>
              <img
                src={`${process.env.REACT_APP_DEV_HOST}/file/${item.id}`}
                style={{ width: "400px" }}
              />
            </div>
          );
        })
      ) : (
        <p>no image</p>
      )}
    </div>
  );
}

export default ImageList;

 

App.js에서 props를 통해 imgData를 전달받고  여기에는 이미지 파일에 대한 정보가 들어있다. map을 통해 imgData를 돌면서 imgData에 들어있는 각각의 id값을 가져와서 이미지 URL을 {`${process.env.REACT_APP_DEV_HOST}/file/${item.id}`}로 설정해 준다.

이를 통해 서버에서 각 이미지 파일을 가져와서 화면에 보여줄 수 있다.

 

그럼 다시 App.js로 넘어와서 좀 더 살펴보겠다.

App.js

import { ToastContainer } from "react-toastify";
import { getFileList } from "./api/apiFile";
import "./App.css";
import "react-toastify/ReactToastify.css";
import { useEffect, useState } from "react";
import ImageList from "./components/ImageList";
import UploadFile from "./components/UploadFile";

function App() {
  const [imgData, setImgData] = useState([]);
  const [imageUpload, setImageUpload] = useState(false);

  useEffect(() => {
    fetchData();
  }, [imageUpload]); //imageUpload가 변화가 될때!

  const fetchData = async () => {
    const updateImgList = await getFileList();
    setImgData(updateImgList);
  };

  const handleImageUpload = () => {
    setImageUpload(!imageUpload);
  };

  return (
    <div className="App">
      <h2>사진첩</h2>

      <UploadFile handleImageUpload={handleImageUpload} />
      <ImageList imgData={imgData} />
      <ToastContainer
        position="bottom-center"
        autoClose={5000}
        hideProgressBar={false}
        newestOnTop={false}
        closeOnClick
        rtl={false}
        pauseOnFocusLoss
        draggable
        pauseOnHover
        theme="light"
      />
    </div>
  );
}

export default App;

 

const fetchData = async () => {
  const updateImgList = await getFileList();
  setImgData(updateImgList);
};

 

fetchData함수를 통해 apiFile에서 작성한 getFileList로 데이터를 가져오고 이를 imgData에 set 해준다.
위에서 ImageList컴포넌트에서 props에서 넘긴 값이 바로 이 값이다✌️

useEffect(() => {
    fetchData();
  }, [imageUpload]); //imageUpload가 변화가 될때!

 

그리고 fetchData는 컴포넌트가 처음 렌더링될 때와 imageUpload의 상태가 변경될 때마다 호출된다. 

이때 imageUpload는 boolean으로 작성하였으며 이 값이 바뀔 때마다 최신 파일 목록을 가져온다. 그럼 값이 언제 마다 바뀌는지 살펴보겠다.

const handleImageUpload = () => {
    setImageUpload(!imageUpload);
  };

 

여기서 useState를 통해서 imageUpload를 반대 값으로 바꿔주는 것을 확인할 수 있다. 그럼 handleImageUpload는 언제 호출되는지 또한 살펴보겠다.

 

<UploadFile handleImageUpload={handleImageUpload} />

 

UploadFile컴포넌트에 props로 이 함수를 보내는 것을 확인할 수 있고 

const onSubmit = async (e) => {
    e.preventDefault(); //이러면 새로고침(깜빡이는 현상) 일어나지 않음
    const formData = new FormData();

    for (let file of files) formData.append("images", file);

    try {
      const res = await postFile(formData);
      console.log(res);
      handleImageUpload();
      toast.success("이미지 업로드 성공");
    } catch (error) {
      toast.error("이미지 업로드 실패");
    }
  };

 

UploadFile컴포넌트에서 파일 전송 버튼을 누르고 파일 전송에 성공했을 때 호출하는 모습을 볼 수 있다. 


결과화면

 

 

 

 

이미지 파일들을 잘 가져와서 출력하는 모습이고 파일을 전송할 때마다 db에도 데이터들이 잘 저장되는 모습을 확인할 수 있다.

정말 간단하게 리액트와 스프링부트를 사용해서 파일 업로드를 구현해 보았다. 

아마 cors문제가 발생할 텐데 이는 @CrossOrigin("*") 어노테이션을 이용해서 해결할 수 있다.

한번 정리하고 나니까 리액트와 스프링부트가 어떻게 서로 작동하고 데이터를 주고받는지 조금은 알 것 같다. 앞으로 더 배우고 다른 프로젝트를 진행하면서 또 정리해 보는 시간을 가지면 좋을 것 같다.

728x90
반응형