Spring Boot & React를 이용한 팀 프로젝트를 진행하면서 글을 등록할 때 이미지를 함께 저장하는 기능을 구현했다. 그런데 저장한 이미지를 글과 함께 화면에 출력하는 기능을 구현하면서 React를 담당하는 팀원과 꽤 오랜 시간 삽질을 했다. 삽질한 내용을 기록하려한다.
백엔드로는 이미지 파일을 로컬에 저장하는 방법, DB에 저장하는 방법 두가지를 모두 구현해놨다. DB에 저장할 경우 파일이 많아지면 무리가 갈 것 같아 우선 로컬에 저장하는 방법을 사용했다.
처음 구현할 때 프론트 코드에 대해 완전 무지했던 나는 dto에 이미지를 byte[] 코드로 넣어 다른 필드들과 함께 전달
Frontend에 전달한 Dto
public record FindPostResponse(Long id, String title, int heartCnt, int view, byte[] image,
LocalDateTime createdAt, LocalDateTime modifiedAt) {
public static FindPostResponse from(Post post, byte[] image) {
return new FindPostResponse(
post.getId(),
post.getTitle(),
post.getHearts().size(),
post.getView(),
image,
post.getCreatedAt(),
post.getModifiedAt()
);
}
}
결과
React에서 바이트를 이미지로 바꿀 방법이 있겠지~라는 생각이었는데, 덕분에 프론트 친구를 한참 힘들게했다..미안
DB에 저장된 이미지의 로컬 경로(filePath)를 전달
Not allowed to load local resource 에러 발생
이유 : 보안 문제로 프로젝트 외부에 존재하는 파일 업로드의 경우 접근할 수 없기 때문
에러 해결 방법으로 Tomcat xml 설정을 추가하거나 로컬에 저장하는 방법 등이 있다는데, backend에서 이미지만 출력하는 api가 잘 작동했었기에 분명 다른 방법이 있을 것이라 생각했다.
비동기 통신 라이브러리인 axios get을 이용해 이미지 조회하도록 구현
이 방법을 사용하면 페이지 변화 없이 해당 api에서 데이터만 가져올 수 있다. 기존 Dto엔 byte[]데이터 대신 fileId를 포함했다.
참고) Post엔티티와 FileData 엔티티는 서로 연관되어있지 않으며, 게시물 등록 시 Post엔티티에 fileId를 저장하도록 구현함
backend(controller)
@Operation(summary = "게시물 전체 조회(페이징)", description = "제목으로 검색, 추천순/최신순 정렬 가능")
@GetMapping("/list/paging")
public ResponseEntity<?> getPostListPaging(
@RequestParam(value = "title", required = false) String title,
@RequestParam(value = "sort", defaultValue = "createdAt", required = false) String sort) {
Pageable pageable = PageRequest.of(0, 20, Sort.by(sort));
return ResponseEntity.ok(postService.getPostsSearchList(title, pageable));
}
@Operation(summary = "이미지 다운로드(fileId)", description = "fileId를 통해 이미지를 다운로드하는 방식입니다.")
@GetMapping("/proudcat/api/image-file-path/{fileId}")
public ResponseEntity<?> downloadImage(@PathVariable("fileId") Long fileId) throws IOException{
byte[] downloadImage = fileDataService.downloadImage(fileId);
return ResponseEntity.status(HttpStatus.OK)
.contentType(MediaType.valueOf("image/png"))
.body(downloadImage);
}
frontend(이미지 데이터 가져오는 코드)
const [imageUrl, setImageUrl] = useState('');
useEffect(() => {
axios
.get(`http://localhost:8080/proudcat/api/image-file-path/${id}`, {
responseType: 'arraybuffer',
})
.then((response) => {
const url = URL.createObjectURL(
new Blob([response.data], { type: 'image/png' }),
);
setImageUrl(url);
})
.catch((error) => {
console.error(error);
});
}, []);
프로젝트 초반엔 나도 js에 대해 완전 무지해서 그냥 데이터만 전달하고 프론트에는 관여할 수 없었다. 그런데 학교 캡스톤 프로젝트를 하면서 반강제적으로 thymeleaf 템플릿을 이용해 프론트를 구현하게 됐고, 그 과정에서 비동기 통신 방법에 대해 알게 되었다. 이를 팀원에게 말했더니 바로 axios 라이브러리로 뚝딱 해결해줬다.
프론트를 구현해야 되는 상황이 왔을 때 처음엔 당황했는데, 이렇게 도움이 되네. 협업을 위해 프론트 공부도 틈틈이 해야할 것 같다