일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 백준#BOJ#8012#한동이는영업사원
- 백준#BOJ#1939#중량제한
- 백준#BOJ#12865#평범한배낭
- 백준#BOJ#14501#퇴사#브루트포스
- 백준#boj#12755
- 백준#BOJ#2615#오목
- 백준#boj#16932#모양만들기
- Today
- Total
순간을 성실히, 화려함보단 꾸준함을
[Javascript, Spring]fetch API 로 데이터 서버로 전송하기 본문
안녕하세요. 폭발토끼입니다.
오늘은 제가 사이드 프로젝트를 진행하면서 겪었던 까다로움(?)을 어떻게 코드를 작성하였는지 더불어 어떤 점을 공부하였는지 여러분들께 공유를 하고 싶어 글을 적기 시작했습니다.
아마 이 글이 글또 2번째 글이 되기도 하겠네요 ㅎㅎ
먼저 fetch API 란 무엇일까요???
많은 분들이 jsp 를 사용하신 경험이 있으셨으면 ajax 라는 비동기 http 통신 기술을 사용해본적이 있으셨을 겁니다. 그러나 javascript 도 점점 발전하게 되었고 너무 오래된 기술이 되어버린 ajax는 가독성도 좋지 못하고 태그들이 추가되면 파일 사이즈가 커진다는 단점 때문데 최근에는 프로미스 기반의 fetch API 혹은 axios 를 많이 사용하는 추세입니다.
(https://developer.mozilla.org/ko/docs/Web/API/Fetch_API/Using_Fetch)
그럼 fetch API 를 사용해서 데이터를 서버로 전송해보겠습니다.
먼저 간단한 html, controller 를 작성하겠습니다.
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
</head>
<body>
<div class="container h-100">
<div class="row align-items-center h-100">
<div class="jumbotron align-self-center">
<h1 class="text-center"><b>Eun <span class="highlight">stargram</span>!</b></h1>
<label for="name">이름</label>
<input type="text" id="name" th:value="Karina"/>
<label for="age">나이</label>
<input type="text" id="age" th:value="22"/>
<div>
<button type="submit" id="submit">전송</button>
</div>
</div>
</div>
</div>
<script src="/js/index.js"></script>
</body>
</html>
package jipdol2.fetchapitest.controller;
import jipdol2.fetchapitest.dto.SaveDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
@Slf4j
@Controller
public class IndexController {
@GetMapping("/")
public String index(){
return "index";
}
@ResponseBody
@PostMapping("/save")
public SaveDTO save(@RequestBody SaveDTO saveDTO){
log.info("name={},age={}",saveDTO.getName(),saveDTO.getAge());
return saveDTO;
}
}
간단하게 설명하자면 이름과 나이를 받는 html 코드가 있고 전송 버튼을 누르면 /save의 url 을 받는 컨트롤러가 존재합니다.
const addIndexEvent = () => {
const buttonEvent = document.getElementById("submit");
buttonEvent.addEventListener("click",submitServer)
};
const submitServer = async (event) =>{
event.preventDefault();
const COMMON_URL = 'http://localhost:8080';
const param = {
'name' : document.getElementById('name').value,
'age' : document.getElementById('age').value
};
const option = {
method : 'POST',
headers:{
'Content-Type' : 'application/json'
},
body: JSON.stringify(param)
};
const res = await fetch(`${COMMON_URL}/save`, {
...option
});
console.log(res.json());
};
addIndexEvent();
fetch API를 사용하는 js 소스입니다.
잘들어왔죠?! 기본적인 문자열 데이터는 성공!!했습니다 ㅎㅎ
근데 어느날 사진을 같이 전송해야 되는 일이 발생했다고 해봅시다.
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
</head>
<body>
<div class="container h-100">
<div class="row align-items-center h-100">
<div class="jumbotron align-self-center">
<h1 class="text-center"><b>Eun <span class="highlight">stargram</span>!</b></h1>
<label for="name">이름</label>
<input type="text" id="name" th:value="Karina"/>
<label for="age">나이</label>
<input type="text" id="age" th:value="22"/>
<input type="file" id="file" />
<div>
<button type="submit" id="submit">전송</button>
</div>
</div>
</div>
</div>
<script src="/js/index.js"></script>
</body>
</html>
file 을 첨부하는 input 태그를 추가 한뒤
const addIndexEvent = () => {
const buttonEvent = document.getElementById("submit");
buttonEvent.addEventListener("click",submitServer)
};
const submitServer = async (event) =>{
event.preventDefault();
const COMMON_URL = 'http://localhost:8080';
const param = {
'name' : document.getElementById('name').value,
'age' : document.getElementById('age').value,
'image' : document.getElementById("file").files[0]
};
const option = {
method : 'POST',
headers:{
'Content-Type' : 'application/json'
},
body: JSON.stringify(param)
};
const res = await fetch(`${COMMON_URL}/save`, {
...option
});
console.log(res.json());
};
addIndexEvent();
이미지 파일을 js 에서 전송하려는 데이터에 추가해주었습니다.
DTO 파일에도 MultipartFile 을 추가 해주죠 ㅎㅎ
package jipdol2.fetchapitest.dto;
import lombok.Getter;
import org.springframework.web.multipart.MultipartFile;
@Getter
public class SaveDTO {
private String name;
private String age;
private MultipartFile image;
}
그리고 전송을 누르면 어떻게 될까요?
Exception 이 발생했네요....왜 그런걸까요????
바로 MultipartFile 형태는 클라이언트에서 요청보낸 Json 타입을 역직렬화(deserialize) 할 수 없기 때문입니다. 파일형태의 요청은 json 이 아닌 multipar/form-data 형식으로 보내야 합니다.
그럼 js 파일도 FormData 형식으로 전송해야 되고 Controller 에서도 데이터를 받는 형식을 수정해야 합니다.
const addIndexEvent = () => {
const buttonEvent = document.getElementById("submit");
buttonEvent.addEventListener("click",submitServer)
};
const submitServer = async (event) =>{
event.preventDefault();
const COMMON_URL = 'http://localhost:8080';
/* const param = {
'name' : document.getElementById('name').value,
'age' : document.getElementById('age').value,
'image' : document.getElementById("file").files[0]
};*/
const formData = new FormData();
formData.append('name',document.getElementById('name').value);
formData.append('age',document.getElementById('age').value);
formData.append('image',document.getElementById('file').files[0]);
const option = {
method : 'POST',
headers:{
// 'Content-Type' : 'multipart/form-data'
// 'Content-Type' : 'application/json'
},
body: formData
};
const res = await fetch(`${COMMON_URL}/save`, {
...option
});
console.log(res.json());
};
addIndexEvent();
package jipdol2.fetchapitest.controller;
import jipdol2.fetchapitest.dto.SaveDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
@Controller
public class IndexController {
@GetMapping("/")
public String index(){
return "index";
}
@ResponseBody
@PostMapping("/save")
public SaveDTO save(
@RequestParam String name,
@RequestParam String age,
@RequestParam MultipartFile image
){
// log.info("name={},age={},image={}",saveDTO.getName(),saveDTO.getAge(),saveDTO.getImage());
log.info("name={},age={},image={}",name,age,image);
return new SaveDTO();
}
}
번거롭지만 천천히 보면 FormData 형식으로 전송해야 하기 때문에 객체를 생성해 주고 데이터 하나하나 append 해주었습니다.
그리고 기존 'application/json' 으로 Content-Type 을 정리해주었던 부분을 주석처리 해주었습니다.
컨트롤러에서는 @RequestBody 어노테이션으로 더 이상 받을 수 없으니 쿼리스트링 형식을 받을 수 있는 @RequestParam 어노테이션을 사용하여 객체들을 받아주었습니다.
문제없이 서버에서 받아왔습니다.
그런데 전송하려는 데이터의 수가 만약 30개정도 된다고 가정해보면 일일히 @RequestParam 을 사용해서 정의해주기 너무 번거롭죠 ㅠㅠ
그래서 @ModelAttribute 를 사용해서 받을 수 있습니다.ㅎㅎ
우리는 이제 파일까지 fetch API 를 사용하여 전송하는 방법을 터득하게 되었습니다 ㅎㅎㅎ
그런데!!!이상한점이 있지 않나요?????
Json 형태로 데이터를 보낼때는 'application/json' 이라고 Content-Type 을 정의해 주었는데 왜 'multipart/form-data' 로 보낼때는 Content-Type 을 정의해주지 않았음에도 전송이 되는 걸까요??
한번 Content-Type을 정의해 볼까요?
const addIndexEvent = () => {
const buttonEvent = document.getElementById("submit");
buttonEvent.addEventListener("click",submitServer)
};
const submitServer = async (event) =>{
event.preventDefault();
const COMMON_URL = 'http://localhost:8080';
/* const param = {
'name' : document.getElementById('name').value,
'age' : document.getElementById('age').value,
'image' : document.getElementById("file").files[0]
};*/
const formData = new FormData();
formData.append('name',document.getElementById('name').value);
formData.append('age',document.getElementById('age').value);
formData.append('image',document.getElementById('file').files[0]);
const option = {
method : 'POST',
headers:{
'Content-Type' : 'multipart/form-data'
// 'Content-Type' : 'application/json'
},
body: formData
};
const res = await fetch(`${COMMON_URL}/save`, {
...option
});
console.log(res.json());
};
addIndexEvent();
결과는?!
Exception이 발생하네요?! 이게 어떻게 된건가요?????
정답은 formData 에 파일을 포함하여 서버로 전송하게 될때는 Content-Type 을 정의하면 안됩니다(매우중요!!!!)
왜 그럴까요???
한번 성공했을 경우와 그렇지 않은 경우의 Request 를 확인해 볼까요?
- Content-Type 을 정의하지 않았을때
- Content-Type 을 multipart/form-data 로 정의했을때
차이점이 보이시죠? Content-Type 을 설정하지 않았을 경우에는 자동으로 boundary 가 붙여지게 됩니다.
Content-Type 을 설정하면 Override 되어서 boundary 가 사라지게 됩니다.
만약 boundary 를 Content-Type 에 각 데이터마다 정의해 줄 수 있다면 직접 정의해 주어도 되지만 굳이 그렇게 할 필요없이 Content-Type 을 정의하지 않아도 브라우저가 임의의 boundary 를 정의해주는 것 같습니다.
boundary 란?
- (chatGPT 답변)The boundary is a unique string of characters that separates each part of the request. It is specified in the Content-Type header of the request and is prefixed with two hyphens (--).
간략하게 설명하면 클라이언트에서 요청보낸 데이터들을 구분지어주는 고유한 식별자의 역할을 수행한다고 합니다.
(https://stackoverflow.com/questions/3508338/what-is-the-boundary-in-multipart-form-data)
어떤가요??도움이 많이 되셨으면 좋겠네요 ㅎㅎ
잘못된 내용이 기재되어 있거나 궁금한 사항 있으시면 언제라도 댓글로 남겨주세요!!
'나의 개발 메모장' 카테고리의 다른 글
@Value 어노테이션이 자꾸 null 이 나와요!!!! (0) | 2023.03.26 |
---|---|
@Transactional 은 mySql 의 auto_increment 값을 롤백시켜주지 않는다고요?! (0) | 2023.03.11 |
JPA metamodel must not be empty!!!!!! (0) | 2023.01.18 |
[JUnit] controller 테스트시 java.lang.IllegalStateException: Failed to load ApplicationContext 에러 (0) | 2023.01.13 |
2022년 회고록 (6) | 2023.01.08 |