지금 하고 있는 프로젝트인 '거북이 스터디'에 글을 쓸 때 이미지 업로드 기능을 만들고 싶었다.
어떻게 이미지를 업로드할 수 있을까?
일단, facebook에서 이미지를 어떻게 업로드하는지 살펴보았다.
이미지를 업로드하는 과정
과정을 간단하게 살펴보면,
- 사용자가 이미지 파일을 업로드 한다.
- 업로드한 파일을 서버에 저장한다.
- 클라이언트에서는 서버에 저장된 이미지의 url을 가져와서 사용한다.
페이스북이 피드에 이미지를 업로드 하는 요청 보기
-
Form Data를 POST 요청 보내면, 서버에 저장된 이미지 주소를 Response로 받는다.
-
request header
- response
Spring 서버 이미지 업로드 API 만들기
- 이미지 데이터베이스 테이블 추가해주기
-- schema.sql
CREATE TABLE image
(
id bigint primary key auto_increment,
name varchar(512),
type varchar(128),
pic_byte MEDIUMBLOB
);
- image upload API 추가하기
-
MultipartFile 인터페이스는 멀티 파트 요청에서 받은 업로드된 파일의 표현이다.
-
파일 내용은 메모리에 저장되거나 디스크에 임시 저장된다.
-
Deflater는 문자열을 압축한다.
-
압축한 byte를 데이터 베이스에 저장한다.
-
저장된 이미지의 id를 Response 메시지에 반환한다.
@RestController
public class FileUploadController {
FileService fileService;
public FileUploadController(FileService fileService) {
this.fileService = fileService;
}
@PostMapping("/images")
public Long uploadImage(@RequestParam("imageFile") MultipartFile file) throws IOException {
Image image = new Image(file.getOriginalFilename(), file.getContentType(), compressBytes(file.getBytes()));
return fileService.addImage(image);
}
public static byte[] compressBytes(byte[] data) {
Deflater deflater = new Deflater();
deflater.setInput(data);
deflater.finish();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(data.length);
byte[] buffer = new byte[1024];
while (!deflater.finished()) {
int count = deflater.deflate(buffer);
outputStream.write(buffer, 0, count);
try {
outputStream.close();
} catch (IOException e) {
}
}
return outputStream.toByteArray();
}
}
@Service
public class FileService {
ImageRepository imageRepository;
public FileService(ImageRepository imageRepository) {
this.imageRepository = imageRepository;
}
public Long addImage (Image image) {
return imageRepository.addImage(image);
}
}
@Repository
public class ImageRepository {
private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public ImageRepository(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public Long addImage(Image image) {
String sql = "INSERT INTO image (name, type, pic_byte) VALUES ( :name, :type, :picByte )";
SqlParameterSource parameter = new MapSqlParameterSource("name", image.getName())
.addValue("type", image.getType())
.addValue("picByte", image.getPicByte());
KeyHolder keyHolder = new GeneratedKeyHolder();
namedParameterJdbcTemplate.update(sql, parameter, keyHolder);
return keyHolder.getKey().longValue();
}
}
Spring 서버 이미지 가져오는 API
- 프로젝트에서 저장된 이미지를 가져온다.
- 압축을 풀어서 응답 바디에 넣어준다.
- 이 때 Content-Type 을 저장했던 image의 타입으로 해준다.
- Content-Type 을 지정해주지 않으면, 브라우저에서 파싱할 때 이미지 형식으로 파싱을 해주지 않는다.
@GetMapping("/images/{id}")
public byte[] getImage(@PathVariable Long id, HttpServletResponse response) {
Image image = fileService.getImageById(id);
image.setPicByte(decompressBytes(image.getPicByte()));
response.setContentType(image.getType());
return image.getPicByte();
}
public static byte[] decompressBytes(byte[] data) {
Inflater inflater = new Inflater();
inflater.setInput(data);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(data.length);
byte[] buffer = new byte[1024];
try {
while (!inflater.finished()) {
int count = inflater.inflate(buffer);
outputStream.write(buffer, 0, count);
}
outputStream.close();
} catch (IOException | DataFormatException ignored) {
}
return outputStream.toByteArray();
}
public Image getImageById(Long id) {
return imageRepository.findById(id).orElseThrow();
}
public Optional<Image> findById(Long id) {
String sql = "SELECT id, name, type, pic_byte FROM image WHERE id = :id";
SqlParameterSource parameter = new MapSqlParameterSource("id", id);
try {
Image image = namedParameterJdbcTemplate.queryForObject(sql, parameter, BeanPropertyRowMapper.newInstance(Image.class));
assert image != null;
return Optional.of(image);
} catch (Exception e) {
return Optional.empty();
}
}
클라이언트에서 이미지 렌더링하기
이제 이 이미지를 클라이언트에서 어떻게 렌더링해주면 좋을까?
서버에 게시글을 어떤 형식으로 저장했느냐에 따라 차이가 있을 것 같다.
서버에 게시글을 html 형식으로 저장을 할 수 있고, 자신이 정한 특정한 형식(마크다운같은)으로 저장할 수도 있다.
텍스트를 저장한 형식에 맞게 HTML로 파싱해주면 된다.
이번 프로젝트에서는 마크다운 형식으로 저장을 하고, 마크다운형식으로 작성한 텍스트를 HTML로 파싱하여 렌더링하였다.
markdown을 html로 변환하기
react-markdown 라이브러리를 사용하여 마크다운을 html로 파싱해주었다.
코드 highlighter 기능도 추가해줬다.
import React from 'react'
import ReactMarkdown from 'react-markdown'
import CodeBlock from './CodeBlock'
export const MarkdownToHtml = ({ source }: string | any) => {
return <ReactMarkdown source={source} renderers={{ code: CodeBlock }} />
}
import React from 'react'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { tomorrow } from 'react-syntax-highlighter/dist/esm/styles/prism'
const CodeBlock = ({ language, value }) => {
return (
<SyntaxHighlighter language={language} style={tomorrow}>
{value}
</SyntaxHighlighter>
)
}
export default CodeBlock
이미지 업로드 fetch 요청 보내기
이미지 업로드 요청을 보낸 뒤 이미지 id를 받아온 뒤 content 에 이미지를 마크다운 형식으로 추가해준다.
const onUploadImage = async e => {
const formData = new FormData()
if (e.target.files !== null) {
formData.append('imageFile', e.target.files[0])
const requestOptions: RequestInit = {
method: 'POST',
credentials: 'include',
body: formData
}
const imageId = await fetch(
`${process.env.REACT_APP_BASE_URL}/images`,
requestOptions
).then(response => response.json())
const imgSrc = `${process.env.REACT_APP_BASE_URL}/images/${imageId}`
// 현재 작성중인 content에 업로드한 이미지의 주소를 추가해줌
onChangeContent(PostInput.content, `${content}\n![](${imgSrc}) \n`)
}
}
완성된 이미지
개선할 점
image는 데이터가 크기 때문에 이미지를 배포 서버에 직접 저장하는 것이 아니라, AWS s3같은 서비스를 사용하여 s3 서버에 저장한 후에 배포 서버에서는 content에 이미지 url만 저장하면 좋을 것 같다.