[HOTSPOT] 2026.06.09 - 이미지 업로드 안정화와 프로필 이미지 동기화를 실제 프로젝트에 적용한 과정

2026. 6. 9. 20:00프로젝트/HOTSPOT

1. 이미지 업로드 문제는 저장 성공만으로 끝나지 않는다

이번 변경의 중심은 이미지 업로드 흐름을 더 안정적으로 만드는 일이었다. 사용자가 프로필 이미지나 상품, 배너 이미지를 올릴 때 화면에서는 업로드가 진행 중인 것처럼 보이지만, 실제로는 요청 제한, timeout, content-type 보존, 프론트엔드 상태 동기화 중 하나만 어긋나도 결과가 비어 보일 수 있다.

특히 이미지는 텍스트 값과 다르게 서버 저장, 정적 파일 제공, 브라우저 캐시, 화면 상태 갱신이 모두 맞물린다. 그래서 이번에는 업로드 API가 성공했는지뿐 아니라 성공 이후 화면이 새 이미지 URL을 다시 들고 있는지까지 같이 확인하는 방향으로 정리했다.

2. GIF와 WebP는 확장자와 content-type을 같이 보존해야 한다

이미지 파일을 저장할 때 원본 확장자나 MIME 타입을 잃어버리면 브라우저가 파일을 올바르게 해석하지 못할 수 있다. PNG, JPG만 생각하고 처리하면 GIF나 WebP 같은 포맷에서 미리보기나 렌더링이 깨지는 문제가 생긴다.

String contentType = file.getContentType();
String extension = resolveExtension(file.getOriginalFilename(), contentType);
String storedName = UUID.randomUUID() + extension;

핵심은 저장 파일명과 응답 헤더가 같은 의미를 갖도록 맞추는 것이다. 서버가 WebP 파일을 저장하면서도 일반 바이너리처럼 내려주면, 프론트엔드 입장에서는 정상 URL을 받았는데도 이미지가 보이지 않는 것처럼 느껴진다.

3. 업로드 요청에는 사용자가 기다릴 수 있는 timeout이 필요하다

상품이나 배너 이미지는 파일 크기와 네트워크 상태에 따라 요청 시간이 길어질 수 있다. timeout이 너무 짧으면 정상 업로드도 실패로 보이고, timeout이 아예 없으면 화면이 멈춘 것처럼 보인다. 그래서 프론트엔드 요청에 명시적인 제한 시간을 두고 실패 상태를 사용자에게 돌려주는 편이 낫다.

await api.post('/images', formData, {
  headers: { 'Content-Type': 'multipart/form-data' },
  timeout: 60_000,
});

이렇게 하면 업로드가 느린 상황과 실제 실패 상황을 구분하기 쉬워진다. 운영 환경에서는 이미지 압축, 서버 업로드 제한, 프록시 제한도 함께 영향을 주기 때문에 클라이언트 timeout만 보지 않고 전체 경로를 같이 맞춰야 한다.

4. 프로필 이미지는 업로드 후 화면 상태까지 동기화되어야 한다

프로필 이미지 업로드가 성공해도 화면 상태가 이전 URL을 계속 들고 있으면 사용자는 업로드가 실패했다고 생각한다. 업로드 응답에서 새 이미지 URL을 받고, 사용자 프로필 상태와 화면 캐시를 같은 시점에 갱신해야 한다.

const uploaded = await uploadProfileImage(file);
setProfile(prev => prev ? { ...prev, profileImageUrl: uploaded.url } : prev);

이 흐름에서 중요한 점은 저장 API와 조회 API의 응답 필드명을 맞추는 것이다. 업로드 응답은 새 URL을 주는데 프로필 조회 응답은 다른 필드명을 쓰면, 다음 화면 진입 때 다시 기본 이미지로 보일 수 있다.

5. 정리

이미지 업로드 안정화는 파일 하나를 서버에 저장하는 작업처럼 보이지만 실제로는 요청 제한, 파일 타입, 응답 URL, 화면 상태 갱신이 이어진 하나의 흐름이다. 이번 작업에서는 GIF/WebP 보존, 업로드 timeout, 프로필 이미지 동기화를 같이 보면서 사용자가 올린 이미지가 실제 화면에 보이는 지점까지 확인했다.