특징
비동기 통신
- 페이지 새로 고침 없이 통신
- AJAX를 사용하면 파일 업로드를 포함한 서버와의 통신을 비동기적으로 처리 가능
- 사용자 경험이 향상되고, 페이지 새로고침 없이 데이터를 전송 및 받기 가능
사용자 인터페이스 향상
- 실시간 응답
- 파일 업로드 진행 상황을 실시간으로 사용자에게 보여줄 수 있음
- 대기 시간 동안의 피드백을 제공 가능
- 다양한 알림
- 업로드 성공, 실패, 진행 상황 등 다양한 상태를 사용자에게 즉시 알림 가능
보안
- CSRF 보호
- AJAX 요청은 일반적으로 CSRF 토큰을 포함하여 전송
- 보안 위협으로부터 데이터를 보호 가능
- HTTPS 사용 권장
- 민감한 데이터 전송 시 HTTPS를 사용하여 데이터의 무결성과 기밀성을 보장 가능
활용
[file2.mustache 파일 - 파일 전송 폼]
- form 타입은 file로 한 다음 JavaScript로 JSON으로 변환
- JavaScript - 파일 입력 이벤트 리스너
- 이벤트 리스너는 타겟이 행위 하는 것을 도움
- let imgInput = document.querySelector("#img")
- imgInput 변수는 파일 입력 필드(input type="file") 선택
- imgInput.addEventListener("change", (e)=>{ }
- JavaScript의 change 이벤트는 사용자가 <input>, <select>, <textarea>와 같은 폼 요소의 값을 변경하고 나서 포커스가 다른 요소로 이동할 때 발생
- 파일 입력 필드의 변경 이벤트 감지
- 파일이 선택되면 file 변수에 파일 객체 저장
- FileReader 객체를 사용하여 파일 읽음 ⇒ reader.readAsDataURL(file)
- 파일이 읽히면 onload 이벤트 핸들러가 호출되어 작업 수행 ⇒ 사용자 이름과 파일의 Base64 인코딩 문자열 추출 ⇒ myUpload 함수 호출하여 파일을 서버에 업로드
- JavaScript - 비동기 파일 업로드 함수
- async function myUpload(username, img) { }
- myUpload 함수는 사용자 이름과 Base64 인코딩된 파일을 매개변수로 받음
- 사용자 이름과 이미지를 포함하는 객체를 생성하여 JSON 문자열로 변환
- fetch API를 사용하여 서버에 비동기적으로 POST 요청을 전송 ⇒ 요청 본문에는 JSON 문자열이 포함 ⇒ 헤더에는 Content-Type을 application/json; charset=utf-8로 설정
- 서버 응답을 JSON 형식으로 파싱하고, 응답이 성공적일 경우 메인 페이지로 리다이렉션
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Document</title>
</head>
<body>
<h1>사진 파일 전송</h1>
<hr>
<form>
<input type="text" id="username"> <br>
<input type="file" id="img" > <br>
<button type="button">전송</button>
</form>
<script>
let imgInput = document.querySelector("#img");
imgInput.addEventListener("change", (e)=>{
let file = imgInput.files[0];
console.log("file", file);
let reader = new FileReader();
reader.onload = ()=>{
let username = document.querySelector("#username").value;
let base64String = reader.result; // Base64 데이터만 추출
myUpload(username, base64String);
}
reader.readAsDataURL(file);
});
async function myUpload(username, img){
let user = {
username: username,
img: img
};
let requestBody = JSON.stringify(user);
console.log(requestBody);
let response = await fetch("/v2/upload", {
method: "post",
body: requestBody,
headers: {
"Content-Type":"application/json; charset=utf-8"
}
});
let responseBody = await response.json();
if(responseBody.success){
location.href="/";
}
}
</script>
</body>
</html>[file2-check.mustache 파일 - 업로드 된 파일 확인하는 화면]
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Document</title>
</head>
<body>
<h1>{{model.username}}</h1>
<img src="{{model.profileUrl}}">
</body>
</html>[Upload 클래스 - 엔티티]
@NoArgsConstructor
@Getter
@Table(name = "upload_tb")
@Entity
public class Upload {
@Id // PK
@GeneratedValue(strategy = GenerationType.IDENTITY) // AI
private Integer id;
private String username;
private String profileUrl;
@Builder
public Upload(Integer id, String username, String profileUrl) {
this.id = id;
this.username = username;
this.profileUrl = profileUrl;
}
}[UploadRepository 클래스]
@RequiredArgsConstructor
@Repository
public class UploadRepository {
private final EntityManager em;
// 이미지 파일 경로 DB 저장
public void save(Upload upload) {
em.persist(upload);
}
// 업로드된 이미지 보기
public Upload findById(Integer id) {
return em.find(Upload.class, id);
}
}[UploadService 클래스]
- MyFileUtil의 fileSave 메서드 호출해서 사용
@Transactional(readOnly = true) // 읽기 전용 트랜잭션
@RequiredArgsConstructor
@Service
public class UploadService {
private final UploadRepository uploadRepository;
public Upload 사진보기() {
return uploadRepository.findById(1);
}
@Transactional
public void v2사진저장(UploadRequest.V2DTO v2DTO) {
String profileUrl = MyFileUtil.fileSave(v2DTO.getImg());
uploadRepository.save(v2DTO.toEntity(profileUrl));
}
}[MyFileUtil 클래스 - fileSave 메서드]
- UploadService에서 사용
- base64.substring(5, base64.indexOf(";base64,"))
- Mime Type 추출
- mimeType.split("/")[1]
- Mime 타입에서 파일 확장자를 얻음
- UUID.randomUUID().toString()
- UUID를 사용해서 고유한 파일 이름을 생성
- String profileUrl = "images/"+imgName
- 서버의 파일 시스템에 저장될 파일의 경로
- String dbUrl = "/upload/"+imgName
- 데이터베이스에 저장될 파일의 URL 경로
- base64.split(",")[1]
- Base64 인코딩된 이미지 데이터 부분을 추출
- Base64.getDecoder().decode(base64Data)
- Base64 인코딩된 데이터를 디코딩하여 byte[ ]로 변환
- Path path = Paths.get(profileUrl)
- 파일 경로를 생성
- Files.write(path, decodedBytes)
- 디코딩된 바이트 데이터를 해당 경로에 파일로 씀
- return dbUrl;
- 파일 쓰기에 성공하면 DB에 기록할 url을 반환
- throw new RuntimeException(e.getMessage())
- 예외 발생 시 RuntimeException 발생
public class MyFileUtil {
public static String fileSave(String base64) {
// 1. 확장자 추출
String mimeType = base64.substring(5, base64.indexOf(";base64,"));
String result = mimeType.split("/")[1];
// 2. Base64를 Byte 배열로 변환
String imgName = UUID.randomUUID().toString() + result;
String profileUrl = "images/"+imgName; // 서버의 파일 시스템에 저장될 파일의 경로
String dbUrl = "/upload/"+imgName; // 데이터베이스에 저장될 파일의 URL 경로
String base64Data = base64.split(",")[1];
byte[] decodedBytes = Base64.getDecoder().decode(base64Data);
// 3. DTO에 사진을 파일로 저장 (images 폴더)
try {
Path path = Paths.get(profileUrl); // 파일 경로를 생성
Files.write(path, decodedBytes); // 디코딩된 바이트 데이터를 해당 경로에 파일로 씀
return dbUrl; // DB에 기록할 url을 반환
} catch (IOException e) {
throw new RuntimeException(e.getMessage());
}
}
}[Base64Test 클래스 - 테스트 클래스]
- Base64 인코딩 테스트
- base64Encoding_test()
- 데이터 타입을 알려주는 data:image/png;base64 부분을 잘라내고 base64로 인코딩 된 이미지를 byte[]로 변환
- mime_test()
- mime type과 extension을 추출하는 메서드
@SpringBootTest
public class Base64Test {
@Test
public void base64Encoding_test() {
String base64String = "data:image/png;base64,iVBO";
// 데이터 URI에서 Base64 부분만 추출
// base64String.split(",")[0] -> "data:image/png;base64"
String base64Data = base64String.split(",")[1];
// 디코딩
byte[] decodedBytes = Base64.getDecoder().decode(base64Data);
// 확인
for(byte b : decodedBytes){
System.out.println(b);
}
}
@Test
public void mime_test() {
String base64String = "data:image/png;base64,iVBO";
// MIME 타입 추출 함수
String mimeType = base64String.substring(5, base64String.indexOf(";base64,"));
System.out.println(mimeType); // image/png
String extension = mimeType.split("/")[1];
System.out.println(extension); // png
}
}[UploadRequest 클래스 - 파일 업로드에 사용하는 V2DTO]
- 이미지는 JSON으로 받기 때문에 String 타입
public class UploadRequest {
@Data
public static class V2DTO {
private String username;
private String img;
public Upload toEntity(String profileUrl) {
Upload upload = Upload.builder()
.username(username)
.profileUrl(profileUrl)
.build();
return upload;
}
}
}[UploadController 클래스]
- @RequestBody
- JSON 문자열인 요청의 body 내용을 Jackson 라이브러리를 통해 자바 객체로 변환
- Resp resp = new Resp(true, "성공", null)
- 응답 객체 생성
- Resp 객체 생성해서 클라이언트로 보낼 응답 데이터 구성 ⇒ 밑의 Resp 클래스 참고
- 업로드가 성공했음을 나타내는 true값
- 응답 메시지로 “성공” 설정
- 추가적인 데이터가 없는 경우 null로 설정
- return ResponseEntity.ok(resp)
- ResponseEntity로 반환할 경우 상태 코드(ResponseEntity.ok일 경우 200) 내용이 포함된 JSON 문자열로 변환해서 클라이언트에게 전달
- 이 경우 HTTP 200 OK 상태와 함께 Resp 객체를 응답 본문으로 반환
@RequiredArgsConstructor
@Controller
public class UploadController {
private final UploadService uploadService;
// ajax 이용 - html 형태의 page 달라고 요청
@GetMapping("/file2")
public String file2() {
return "file2";
}
// action
@PostMapping("/v2/upload")
public ResponseEntity<?> v2Upload(@RequestBody UploadRequest.V2DTO v2DTO) {
uploadService.v2사진저장(v2DTO);
Resp resp = new Resp(true, "성공", null);
return ResponseEntity.ok(resp);
}
@GetMapping("/file2-check")
public String file2Check(Model model) {
Upload upload = uploadService.사진보기();
model.addAttribute("model", upload);
return "file2-check";
}
}[Resp 클래스]
@AllArgsConstructor
@Data
public class Resp<T> {
private Boolean success;
private String msg;
private T data;
}Share article