본문 바로가기
개발

폼 데이터와 함께 파일 업로드

by ^..^v 2023. 2. 21.
728x90
반응형

사용자 아이디, 이름, 이메일과 첨부파일을 입력받아서 서버로 전달하면, 서버는 지정된 경로에 첨부파일을 저장하고, 전달받은 값과 첨부파일 정보를 이름 : 값 형식으로 반환하는 예제입니다.

 

프론트엔드 (리액트)

화면 구성

  return (
    <>
      <div>
        ID: <input type="text" value={userId} onChange={handlerChangeUserId} />
      </div>
      <div>
        NAME: <input type="text" value={userName} onChange={handlerChangeUserName} />
      </div>
      <div>
        EMAIL: <input type="text" value={userEmail} onChange={handlerChangeUserEmail} />
      </div>
      <div>
        FILES: <input type="file" ref={inputFiles} onChange={handleChangeFile} multiple accept="image/*" />
      </div>
      <div>
        <button type="button" onClick={handlerUploadDataWithFile}>업로드</button>
      </div>
    </>
  );

각각의 정보를 입력하는 입력창에 value 속성의 값으로 상태 변수를 지정하고, onChange 이벤트가 발생했을 때 동작할 이벤트 핸들러 함수를 지정합니다. 

파일 선택창에는 여러 개의 파일을 선택할 수 있도록 multiple 속성을 지정하고, 이미지 파일을 선택할 수 있도록 accept="image/*" 속성을 지정합니다. 

 

입력창 데이터를 관리할 상태 변수 및 이벤트 핸들러 함수 정의

  const [userId, setUserId] = useState('TESTER');
  const [userName, setUserName] = useState('테스터');
  const [userEmail, setUserEmail] = useState('tester@test.com');

  const handlerChangeUserId = e => setUserId(e.target.value);
  const handlerChangeUserName = e => setUserName(e.target.value);
  const handlerChangeUserEmail = e => setUserEmail(e.target.value);

입력창 데이터를 관리할 상태 변수와 이벤트 핸들러 함수를 정의합니다. 테스트의 편의를 위해 상태 변수의 초기값으로 빈문자열이 아닌 임의의 값을 설정해 둡니다.

 

ref 객체 변수 정의

const inputFiles = useRef();

useRef 훅 함수를 이용해 ref 객체 변수를 정의합니다. 해당 변수는 선택한 파일의 개수, 종류, 크기 제약 조건을 만족하지 않는 경우 파일 선택을 초기화하는 기능에서 사용됩니다.

 

최대 파일 크기 및 개수를 정의한 상수

  const MAX_FILE_SIZE = 1 * 1024 * 1024; //1MB
  const MAX_FILE_COUNT = 3;

업로드 가능한 파일의 최대 크기와 최대 개수를 상수로 정의합니다. 

 

업로드 파일 변경 시 처리

  const invalidFile = msg => {
    alert(msg);
    inputFiles.current.value = '';
    setImageFiles([]);
  };

  const [imageFiles, setImageFiles] = useState([]);

  const handleChangeFile = e => {
    const files = e.target.files;

    if (files.length > MAX_FILE_COUNT) {
      invalidFile("이미지는 최대 3개 까지 업로드가 가능합니다.");
      return;
    } 
    for (let i = 0; i < files.length; i++) {
      if (!files[i].type.match("image/.*")) {
        invalidFile("이미지 파일만 업로드 가능합니다.");
        return;
      } else if (files[i].size > MAX_FILE_SIZE) {
        invalidFile("이미지 크기는 1MB를 초과할 수 없습니다.");
        return;
      } 
    }

    setImageFiles([...files]);
  };

업로드 파일이 변경된 경우, 파일의 개수, 종류, 크기가 제약 조건을 만족하지 않는 경우, 메시지를 보여주고 입력값을 초기화하고 반환합니다. 모든 조건을 만족하는 경우, setter 함수를 이용해 상태변수를 업데이트 합니다. 

 

업로드 처리

  let datas = {
    userId, 
    userName, 
    userEmail
  };

  const formData = new FormData();
  formData.append(
    'data',
    new Blob([JSON.stringify(datas)], { type: 'application/json' })
  );
  Object.values(imageFiles).forEach(file => formData.append('files', file));
  
  const handlerUploadDataWithFile = () => {
    axios({
      method: 'POST',
      url: `http://localhost:8080/upload`,
      headers: { 'Content-Type': 'multipart/form-data;' }, 
      data: formData      
    })
    .then(response => {
      console.log(response);
      alert(`${response.data}\n정상적으로 업로드했습니다.`);
    })
    .catch(error => {
      console.log(error);
      alert(`업로드 중 오류가 발생했습니다.`);
    });
  };

서버로 전달할 입력값을 변수 이름 : 값 형식의 객체 데이터를 만듧니다. 이때, 객체의 속성 이름과 값을 가지고 있는 변수 이름이 동일하므로 객체 단축 속성명을 이용해서 코드를 단순하게 만듦니다.

서버로 전달할 입력값과 파일을 formData 객체의 값으로 설정한 후 axios 함수의 data 속성의 값으로 설정해서 서버로 전달하고, 서버 응답을 alert 창으로 출력해 줍니다. 

 

전체 코드

import { useRef, useState } from 'react';
import axios from 'axios';


function App() {
  // FORM DATA를 저장할 상태 변수 
  // - INPUT, TEXTAREA 등에서 입력한 내용을 저장
  // - 최종 상태의 값을 서버로 전달
  // - 서버에서는 DTO 객체로 받아서 처리
  const [userId, setUserId] = useState('');
  const [userName, setUserName] = useState('');
  const [userEmail, setUserEmail] = useState('');

  const handlerChangeUserId = e => setUserId(e.target.value);
  const handlerChangeUserName = e => setUserName(e.target.value);
  const handlerChangeUserEmail = e => setUserEmail(e.target.value);
  
  // 파일 선택창의 값을 직접 제어하기 위해 ref 객체 변수를 정의 
  const inputFiles = useRef();

  // 제한할 파일의 크기와 개수를 정의
  const MAX_FILE_SIZE = 1 * 1024 * 1024; //1MB
  const MAX_FILE_COUNT = 3;

  // 파일의 종류, 크기, 개수 제한을 만족하지 않는 경우, 메시지를 보여주고 파일 입력창의 값을 초기화하는 함수
  const invalidFile = msg => {
    alert(msg);
    inputFiles.current.value = '';
    setImageFiles([]);
  };

  // 업로드할 파일 데이터를 저장할 상태 변수와 이벤트 핸들러
  const [imageFiles, setImageFiles] = useState([]);

  const handleChangeFile = e => {
    const files = e.target.files;

    if (files.length > MAX_FILE_COUNT) {
      invalidFile("이미지는 최대 3개 까지 업로드가 가능합니다.");
      return;
    } 
    for (let i = 0; i < files.length; i++) {
      if (!files[i].type.match("image/.*")) {
        invalidFile("이미지 파일만 업로드 가능합니다.");
        return;
      } else if (files[i].size > MAX_FILE_SIZE) {
        invalidFile("이미지 크기는 1MB를 초과할 수 없습니다.");
        return;
      } 
    }

    setImageFiles([...files]);
  };

  // 서버로 전달할 입력창 내용을 객체로 정의 (단축 속성명)
  let datas = {
    userId, 
    userName, 
    userEmail
  };

  // 서버로 전달할 폼 데이터(입력창 내용과 첨부 파일)를 설정
  const formData = new FormData();
  formData.append(
    'data',
    new Blob([JSON.stringify(datas)], { type: 'application/json' })
  );
  Object.values(imageFiles).forEach(file => formData.append('files', file));
  
  // 설정한 폼 데이터를 multipart/form-data 형식으로 서버로 전달
  const handlerUploadDataWithFile = () => {
    axios({
      method: 'POST',
      url: `http://localhost:8080/upload`,
      headers: { 'Content-Type': 'multipart/form-data;' }, 
      data: formData      
    })
    .then(response => {
      console.log(response);
      alert(`${response.data}\n정상적으로 업로드했습니다.`);
    })
    .catch(error => {
      console.log(error);
      alert(`업로드 중 오류가 발생했습니다.`);
    });
  };

  return (
    <>
      <div>
        ID: <input type="text" value={userId} onChange={handlerChangeUserId} />
      </div>
      <div>
        NAME: <input type="text" value={userName} onChange={handlerChangeUserName} />
      </div>
      <div>
        EMAIL: <input type="text" value={userEmail} onChange={handlerChangeUserEmail} />
      </div>
      <div>
        FILES: <input type="file" ref={inputFiles} onChange={handleChangeFile} multiple accept="image/*" />
      </div>
      <div>
        <button type="button" onClick={handlerUploadDataWithFile}>업로드</button>
      </div>
    </>
  );
}

export default App;

 

 

백엔드 (스프링 부트)

편의를 위해 컨트롤러에서 모든 기능을 처리했으나, 파일 저장을 비롯한 비즈니스 로직은 서비스로 이관해서 구현하는 것이 좋은 방법입니다.

	@PostMapping("/upload")
	public ResponseEntity<String> updateMember(
			@RequestPart(value = "files", required = false) MultipartFile[] files,
			@RequestPart(value = "data", required = false) UserDto data) throws Exception {
		final String UPLOAD_DIR = "C:/temp/upload/";

		String uploadedDatas = "";
		uploadedDatas += "userId: " + data.getUserId() + "\n";
		uploadedDatas += "userName: " + data.getUserName() + "\n";
		uploadedDatas += "userEmail: " + data.getUserEmail() + "\n";
		
		if (files != null) {
			for (MultipartFile mf : files) {
				String originFileName = mf.getOriginalFilename();
				String savedFileName = UUID.randomUUID().toString();
				uploadedDatas += "원본파일명: " + originFileName + "\n";
				uploadedDatas += "저장파일명: " + savedFileName + "\n";
				try {
					File f = new File(UPLOAD_DIR + savedFileName);
					mf.transferTo(f);
				} catch (Exception e) {
					e.printStackTrace();
					return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("오류");
				}
			}
		}
		return ResponseEntity.ok(uploadedDatas);
	}

multipart/form-data 형식으로 전달된 내용을 @RequestPart 어노테이션을 이용해서 data 이름으로 전달받은 폼 데이터와 files 이름으로 전달받은 첨부파일을 UserDto 객체와 MultipartFile[] 객체로 주입받아서 처리합니다. 

첨부파일이 존재하는 경우, UUID 형식(32자리의 16진수 표현)의 난수를 파일명으로 설정해 첨부파일을 저장합니다. 

[참고] 해당 예제에서는 첨부파일을 포함하고 있는 폼 데이터를 서버로 전달하고, 전달받는 방법을 설명하기 위한 것으로, 첨부파일의 저장경로와 전달받은 폼 데이터를 DB에 저장하는 것과 같은 이후 처리 로직은 생략되었습니다. 

 

 

 

 

728x90
반응형

댓글