[Spring boot + React] 게시글 작성하기(다중 파일 업로드 포함)
목표
리액트로 작성된 프론트단에서 게시글을 작성한 뒤 등록 버튼을 누르면
작성한 데이터들을 서버인 스프링으로 전송한다.
기본설정
<!-- 파일 업로드에 필요한 라이브러리 -->
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
pom.xml에 추가한다.
React
1) 입력 전
2) 입력 후
위처럼 게시판 종류, 제목, 내용, 파일들을 입력하고 등록버튼을 통해 스프링에 데이터를 전송할 것이다.
React(프론트) 전체 코드
import FloatingLabel from 'react-bootstrap/FloatingLabel';
import Form from 'react-bootstrap/Form';
import React, {useState} from 'react';
import axios from 'axios';
function PostCard() {
const [files, setFiles] = useState([]); //파일 상태 저장을 위한 state
// 이미지 파일 업로드에서 다이얼로그를 통해 특정 파일들을 선택한 경우
const handleChangeFile = (event) => {
if(event.target.files.length!=0){ // 파일 업로드시 이미지를 선택하지 않았을 경우 기존의 올리려한 이미지들이 유지됨
setFiles(Array.from(event.target.files || []));
}
}
// 선택한 이미지 파일들 중에서 특정 파일을 삭제하고 싶을 때
const handleDelete = (index) =>{
setFiles([...files.slice(0,index), ... files.slice(index+1)])
};
function Send(){
const fd = new FormData(); // 전송할 데이터들을 저장해둘 폼
Object.values(files).forEach((file) => fd.append("file", file)); // 입력한 파일들 추가
fd.append("category",document.getElementById('category').value); // 입력한 게시판 종류 추가
fd.append("subject",document.getElementById('subject').value); // 입력한 제목 추가
fd.append("content",document.getElementById('content').value); // 입려한 내용 추가
//axios를 통해 http://localhost:8080/user/AxiosFileTest로 fd 전송
axios.post('http://localhost:8080/user/AxiosFileTest', fd, {
headers: {
"Content-Type": `multipart/form-data; ` // 파일 데이터 전송을 위해서 명시필요
},
})
.then((response) => {
})
.catch((error) => {
// 예외 처리
})
}
return (
<>
<h1> 글쓰기 </h1>
// 게시판 선택을 위한 셀렉트폼
<Form.Select id="category" aria-label="Default select example">
<option>---게시판 선택---</option>
<option>---자유게시판1---</option>
<option>---자유게시판2---</option>
</Form.Select>
// 제목 입력을위한 textarea
<FloatingLabel controlId="subject" label="제목" className="mb-3">
<Form.Control as="textarea" placeholder="Leave a comment here" />
</FloatingLabel>
// 내용 입력을 위한 textarea
<FloatingLabel controlId="content" label="내용">
<Form.Control as="textarea" placeholder="Leave a comment here" style={{ height: '100px' }}/>
</FloatingLabel>
// 파일 업로드를 위한 폼, jpg/jpeg,png만 입력할 수 있도록 하였음, multiple 옵션을 통해 다중 파일 업로드 가능
<Form.Group controlId="formFileMultiple" className="mb-3">
<Form.Label>여러개의 이미지를 삽입하려면 드래그 및 컨트롤 클릭으로 다중 선택해주세요!</Form.Label>
<Form.Control type="file" accept=".jpg,.png,.jpeg" onChange={handleChangeFile} multiple />
</Form.Group>
// 선택한 파일들을 프론트 상에 출력시켜줌
<ul>
{files.map((file,index)=>(
<li key={`${file.name}_${index}`}>
{file.name}
<button onClick={()=>handleDelete(index)}>삭제</button>
</li>
))}
</ul>
// 등록 버튼
<div>
<button onClick={()=> Send()} class="btn write-button btn-primary">등록</button>
</div>
</>
);
}
export default PostCard;
위 코드에서 주목해야할 부분은 2 곳이다.
첫 번째는 데이터를 전송하는 Send 함수, 두 번째는 파일 업로드와 관련된 핸들러인 handleChangeFile, handleDelete다.
1. Send 함수
function Send(){
const fd = new FormData(); // 전송할 데이터들을 저장해둘 폼
Object.values(files).forEach((file) => fd.append("file", file)); // 입력한 파일들 추가
fd.append("category",document.getElementById('category').value); // 입력한 게시판 종류 추가
fd.append("subject",document.getElementById('subject').value); // 입력한 제목 추가
fd.append("content",document.getElementById('content').value); // 입력한 내용 추가
//axios를 통해 http://localhost:8080/user/AxiosFileTest로 fd 전송
axios.post('http://localhost:8080/user/AxiosFileTest', fd, {
headers: {
"Content-Type": `multipart/form-data; ` // 파일 데이터 전송을 위해서 명시필요
},
})
.then((response) => {
})
.catch((error) => {
// 예외 처리
})
}
files에는 현재 선택된 파일들의 상태 정보가 담겨있다.
이를 forEach를 통해 files 내부의 파일들을 하나하나 탐색하면서, 각 파일을 fd에 file이라는 이름으로 저장한다.
그리고 각각의 입력 폼에 작성된 게시판 종류, 제목, 내용 또한 fd에 추가 저장한다.
이해를 돕기위한 예로 JSON 형태로 이 데이터를 표현하면
{
file : {"img1과 관련된 파일정보"," img2과 관련된 파일정보", "img3과 관련된 파일정보"},
category : "자유게시판1",
subject : "제목임",
content : "나도 고수가 되고싶어요"
}
와 같은 형태로 fd가 존재하게 된다.
그 후 axios를 통해 우리가 원하는 url로 작성한 fd를 전송한다.
주의할 점은 파일을 전송하기 때문에, Content-Type을 multipart/form-data로 명시해야 한다는 점이다.
이 부분이 빠지면 스프링에서는 해당 데이터 형식을 이해할 수 없어 읽지 못한다.
2. handleChangeFile, handleDelete
const [files, setFiles] = useState([]); //파일
const handleChangeFile = (event) => {
console.log(event.target.files);
if(event.target.files.length!=0){ // 이 조건문으로 인해 파일 업로드시 이미지를 선택하지 않아도 기존의 업로드할 내역들이 유지됨
setFiles(Array.from(event.target.files || []));
}
}
const handleDelete = (index) =>{
setFiles([...files.slice(0,index), ... files.slice(index+1)])
};
handleChangeFile은 setFiles라는 state 변경 함수를 통해 files state를
입력받은 Array형태의 파일들로 변경한다.
handleDelete는 slice 메소드를 사용해서, 0~index-1까지의 값과 index+1에서 끝까지의 값으로 변경한다.
따라서 files[index]의 내용이 사라지면서 해당 이미지가 files state에서 삭제된다.
Spring
@RestController
@CrossOrigin(origins="*") // 이거 넣어야 CRos 에러 안남
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {
@RequestMapping(value="/AxiosFileTest", method=RequestMethod.POST)
public Map<String,Object> AxiosFileTest (HttpServletRequest request,
@RequestParam(value="file", required=false) MultipartFile[] files) throws SQLException {
System.out.println(request.getParameter("category"));
System.out.println(request.getParameter("subject"));
System.out.println(request.getParameter("content"));
Map<String,Object> resultMap = new HashMap<String,Object>();
String FileNames ="";
System.out.println("paramMap =>"+files[0]);
String filepath = "D:/saveFolder/"; // 해당 파일을 저장할 절대주소
for (MultipartFile mf : files) {
String originFileName = mf.getOriginalFilename(); // 원본 파일 명
long fileSize = mf.getSize(); // 파일 사이즈
System.out.println("originFileName : " + originFileName);
System.out.println("fileSize : " + fileSize);
String safeFile =System.currentTimeMillis() + originFileName;
FileNames = FileNames+","+safeFile;
try {
File f1 = new File(filepath+safeFile);
mf.transferTo(f1);
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
Map<String, Object> paramMap = new HashMap<String, Object>();
FileNames = FileNames.substring(1);
System.out.println("FileNames =>"+ FileNames);
resultMap.put("JavaData", paramMap);
return resultMap;
}
}
파일은 file이라는 파라미터로 저장해두었으므로 위 어노테이션을 통해 files에 저장되고
나머지 데이터들은 request.getParameter("파라미터이름"); 을 통해 수신할 수 있다.
전송받은 파일들을 위에서는 특정 디렉터리에 저장해두었는데, 이는 필요에 따라 수정하면 되겠다.
아래는 프론트단에서 전송한 데이터를 스프링에서 문제없이 수신하여 출력한 모습이다.