[스프링부트] 파일 데이터 전송 - AJAX 활용

이나겸's avatar
Dec 06, 2024
[스프링부트] 파일 데이터 전송 - AJAX 활용
💡

AJAX(Asynchronous JavaScript and XML)

웹 페이지가 전체를 새로 고침하지 않고도 데이터를 서버로부터 비동기적으로 가져올 수 있도록 해주는 기술
사용자 경험을 개선 가능

특징

비동기 통신

  • 페이지 새로 고침 없이 통신
    • 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

Nakyeom's Study