[React] 미리보기 및 텍스트,이미지 동시 전송(React-Hook-Form)
정리 배경
도서 등록 요구사항 중 크게 아래와 같이 두 가지가 있었다.
1. 기등록한 이미지 파일 미리보기 제공
2. 도서제목,저자 등등의 텍스트와 도서 이미지를 동시에 서버로 요청
보통 텍스트만 요청해봤지 파일과 같이 요청한 적은 없었기에 방식을 몰랐다.(정확히는 배경 지식 부족..)
따라서, React Hook Form을 통하여 텍스트, 파일(이미지)를 전송하는 방식을 정리하려 한다.
백서버 컨트롤러
@PostMapping(value = "/reg")
public ApiResponseDto regBook(@RequestPart BookRegReqDto bookRegReqDto ,@RequestPart(value = "file") MultipartFile file){
log.info(bookRegReqDto.toString());
log.info(String.valueOf(file.getSize()));
log.info(String.valueOf(file.getOriginalFilename()));
return ApiResponseDto.createRes(ErrorCode.SUC);
}
1. 기등록한 이미지 파일 미리보기 제공( URL.createObjectUrl)
URL.createObjectUrl
해당 메소드를 사용하면 메모리에 있는 객체(Blob이나 File객체) 에 대한 임시 URL을 생성할 수 있다.
https://developer.mozilla.org/ko/docs/Web/API/URL/createObjectURL_static
RegBook.tsx
우선, react hook form으로 기본 코드 구조는 아래와 같다.
export interface IRegBookParams{
bookName:string,
bookAuthor:string,
bookContent:string,
bookPublisher:string,
isbn:string,
bookLocation:string,
pubDt:string,
bookImage: FileList
}
function RegBook(){
const {
register,
formState: { errors },
handleSubmit,
getValues,
watch
} = useForm<IRegBookParams>();
const onSubmit=(data:IRegBookParams)=>{
console.log(data);
return ;
}
const onClick=(e:React.MouseEvent<HTMLButtonElement>)=>{
// e.preventDefault(); // submit을 막음 / 용도 : input값에 대한 검증 외 추가검증 필요시(ex. 서버로 아이디 중복체크했는지?)
console.log("onClick");
}
return (
<>
<h1>도서 등록</h1>
<form onSubmit={handleSubmit(onSubmit)} >
....
<div>
<label>도서 이미지</Label>
<input type="file" {
...register("bookImage")
}
/* 미리보기 이미지 파일 노출 부분 */
/>
</div>
{errors.bookImage && <p>{errors.bookImage.message}</p>}
<button onClick={onClick}>등록 요청</button>
</form>
</>
)
}
파일 변화 감지 및 state변수 선언
input태그 내 첨부된 이미지 파일(watch)의 변화를 감지해야 했으며
변화 감지(useEffect) 시, 파일에 대한 임시 URL을 생성하여 state변수(preview)에 담아 리랜더링을 통하여 이미지 파일을 노출시켜줄 것이다.
RegBook.tsx
const files = watch("bookImage");
const [preview,setPreview) = useState("");
..
useEffect(()=>{
if(files){ //첫 랜더링 시 undefined
const filesArr = Array.from(files); //FileList 배열이 아니기에 배열로 변경
if(filesArr.length>0){ //파일 선택한 경우에만
const imgFile = filesArr[0];
const previewUrl = URL.createObjectURL(imgFile);
setPreview(previewUrl);
}
}
},[files]);
return (
...
<InputWrapper>
<Label >도서 이미지</Label>
<Input type="file" {
...register("bookImage")
}
/>
/* 미리보기 이미지 파일 노출 */
{preview ?
<Img
src={preview}
/>
:
<div/>
}
</InputWrapper>
{errors.bookImage && <p>{errors.bookImage.message}</p>}
);
bookImage[0] ??
출력된 데이터 중 빨간 줄에 해당하는 부분만 필요하기에 bookImage[0]으로 변수 선언해당 데이터를 통하여 임시URL 생성 가능하다.

as any??
useForm에 선언한 인터페이스(IRegBookParams)를 보면 bookImage를 string으로 선언해두었다.
as any없이 URL.createObjectURL메소드 사용 시 매개변수(Blob이나 File만 가능)로 string이 들어왔기에 컴파일 에러가 발생한다.
as Blob,as File로 해결되지 않아 as any를 추가해줌으로써 해결했다.
추가) IRegBookParams.bookImage타입이 FileList로 변경하면서 무시해도 되는 내용!
2. 텍스트와 파일 동시 요청 (FormData)
RegBook.tsx 일부분
const onSubmit=(data:IRegBookParams)=>{
const formData= new FormData();
//json
//formData.append("dto",data); //Json형태로 보낼 시 알 수 없기에 JSON.stringify 이용해야 한다.
formData.append("dto",JSON.stringify(data));
//파일
formData.append("file",Array.from(data.bookImage)[0]);
//api 요청
regBookfetch(formData);
..
}
api.ts 일부분
export const regBookFetch= async (requsetParams: FormData)=>{
return await PrivateAPI.post(
`/book/reg`
,requsetParams //body
,{
headers:{
"Content-Type":"multipart/form-data"
}
}
)
.then(response=>response.data);
}
그런데 !!!! 위처럼 요청 시 415 에러 발생!!!??
415 Unsupported Media Type ????
클라이언트가 보낸 페이로드가 지원하지 않는 형식이기 때문에 서버가 요청을 수락하지 않음을 나타냅니다.
출처: https://developer.mozilla.org/ko/docs/Web/HTTP/Status/415
백엔드 로그 확인
Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/octet-stream' not supported]
application/octect-stream ???
MIME의 개별 타입 중 application에 속하는 타입이며, 8비트 단위의 binary data라는 뜻"특별히 표현할 수 있는 프로그램이 존재하지 않는 데이터의 경우 기본값으로 octet-stream을 사용한다." = 브라우저가 보통 자동으로 실행하지 않거나 실행할지 묻기도 하는 타입
출처: https://velog.io/@kim_sunnnny/what-is-applicationoctet-stream
두 가지를 종합해 판단해봤을때 ,
리액트 서버에서 보낸 contentType(application/octect-stream)을 백엔드 서버에서 처리할 수 없다는 말이다.
나는 application/json으로 요청해야 하기에 리액트서버에서 contentType을 바꿔주면 될 것으로 보인다.
그렇다면 contentType을 어떻게 바꿀까??
Blob !!!
Blob은 대형 이진 객체(Binary Large Object)를 의미하며 일련의 데이터를 처리하거나 간접 참조하는 객체입니다
Blob은 대개 바이트의 크기를 알아내거나 해당 MIME 타입이 무엇인지 요청하며,
데이터를 작은 Blob으로 잘게 나누는 등의 작업에 사용됩니다
출처: https://velog.io/@minh0518/Blob%EA%B0%9D%EC%B2%B4%EB%9E%80
Blob 생성자 옵션 중에 타입을 선언해주는 부분이 있었기에 해당 코드로 아래와 같이 구현했다.
최종코드
const onSubmit=(data:IRegBookParams)=>{
const formData= new FormData();
//json
//formData.append("dto",data); //Json형태로 보낼 시 알 수 없기에 JSON.stringify 이용해야 한다.
//formData.append("dto",JSON.stringify(data)); //contentType을 지정할 수 없기에 415 발생
formData.append('bookRegReqDto', new Blob([JSON.stringify(data)], {type:'application/json'}));
//파일
formData.append("file",Array.from(data.bookImage)[0]);
regBookFetch(formData);
...
}
백엔드 출력 로그
파일 이름과 객체 내 필드 값들까지 잘 나온다!!!