S3 이미지 프론트에서 처리 vs 서버에서 처리 고민 및 해결

두의 개발 고민 블로그·2023년 4월 27일

CarryAWay 프로젝트

목록 보기
2/7

S3 버킷에 웹 프로젝트에서 사용하는 이미지를 담기로 한 상황

S3를 프론트에서 사용하는 방법

  • 위 링크에 들어가서 댓글을 보고,, 이 내용에 대해 고민을 시작하게 됨

이미지는 다양하게 처리할 수 있다.
1. 프론트는 리엑트를 사용하니까 JS에서 S3와 통신하는 방식
2. 백엔드는 스프링부트를 사용하니까 Java로 S3와 통신하는 방식
3. S3를 사용하기로 했으니 다른 방식은 아래 링크에서 !
웹에서 이미지 관리하는 방법 - 두의 개발

두 가지 방법의 특징을 비교하고 하나를 선택해서 개발해야겠다.

1. 프론트에서 처리

장점

  • 프론트에서 S3와 직접 통신하여 이미지를 업로드 하는 방법은, 사용자에게 더 빠르게 반응해줄 수 있다.
    -> 결국 통신 속도가 빨라지게 되면 사용자들은 기다리는 시간이 줄어들게 된다.

단점

  • 보안문제 발생.
    -> 배포를 하게 되면 개발자 설정에서 JS파일을 볼 수 있다. 그러게 되면 보안상의 문제가 발생할 수 있다. S3 엑세스키, 비밀 엑세스키와같은 보안 자격증명을 프론트에서 직접 사용하게 되기 때문이다.
    -> 해결방법 AWS 서버리스 기능(AWS Lambda)를 사용해서 서명된 URL을 사용하는 방법도 고려할 수 있다 !

2. 서버에서 처리

장점

  • 이미지를 업로드 하는 동안 보안 자격 증명을 안전하게 보호할 수 있다.
  • 이미지애 추가적인 처리를 수행할 수 있으므로, 이미지를 변경하거나 크기를 조절하는 등의 작업을 수행할 수 있다.

단점

  • 업로드 하는 시간이 길어져 사용자가 오래 기다릴 수 있다.

결론

💡 서버와의 통신이 길어져 사용자의 불변함이 생길지 몰라도 보안이 우수한게 사용자와 서비스 사이의 신뢰를 보존하고 법적 문제도 일으키지 않을 수 있다 !

개발 (서버에서 처리 방법)

우선 프론트에서 IMG 받아오기

멀티파트(form-data)를 이용한 POST 요청

첫 번째
<input type="file" /> // img 파일을 input 태그를 통하여 데이터를 받는다.

두 번째
const formData = new FormData();
formData.append('file', imageFile);

세 번째
fetch('/api/upload', { // Content-Type 헤더를 'multipart/form-data'로 
						  설정하여 멀티파트 데이터를 전송
  method: 'POST',
  body: formData,
  headers: {
    'Content-Type': 'multipart/form-data'
  }
});

서버

Controller

    // 프로필 이미지
    @PostMapping("file/profile")
    public String uploadProfileImage(@RequestParam("image") MultipartFile multipartFile) throws IOException {
        return userService.uploadProfile(multipartFile);
    }

컨트롤러에서 front에서 받은 이미지를 MultipartFile 객체로 받을 수 있다.

Service

    public String uploadProfile(MultipartFile multipartFile) throws IOException {
        String url = s3Uploader.upload(multipartFile, "static/profile");
        System.out.println(url);
        return url;
    }

S3Uploader

@Component
@RequiredArgsConstructor
public class S3Uploader {

    private final AmazonS3Client amazonS3Client;

    @Value("${cloud.aws.s3.bucket}")
    public String bucket;

    //List 로 받을 경우
    public List<String> upload(List<MultipartFile> multipartFiles, String dirName) throws IOException{
        List<String> urlList = new ArrayList<>();
        for(MultipartFile multipartFile : multipartFiles) {
            File uploadFile = convert(multipartFile).orElseThrow(() -> new IllegalArgumentException("파일 전환 실패"));
            urlList.add(upload(uploadFile, dirName));
        }
        return urlList;
    }

    public String upload(MultipartFile multipartFile, String dirName) throws IOException{
        File uploadFile = convert(multipartFile).orElseThrow(() -> new IllegalArgumentException("파일 전환 실패"));

        return upload(uploadFile, dirName);
    }
    // S3로 파일 업로드하기
    private String upload(File uploadFile, String dirName) {
        String fileName = dirName + "/" + UUID.randomUUID() + uploadFile.getName();   // S3에 저장된 파일 이름
        String uploadImageUrl = putS3(uploadFile, fileName); // s3로 업로드
        removeNewFile(uploadFile);
        return uploadImageUrl;
    }

    // S3로 업로드
    private String putS3(File uploadFile, String fileName) {
        amazonS3Client.putObject(new PutObjectRequest(bucket, fileName, uploadFile).withCannedAcl(CannedAccessControlList.PublicRead));
        return amazonS3Client.getUrl(bucket, fileName).toString();
    }

    // 로컬에 저장된 이미지 지우기
    private void removeNewFile(File targetFile) {
        if (targetFile.delete()) {
            log.info("File delete success");
            return;
        }
        log.info("File delete fail");
    }

    private Optional<File> convert(MultipartFile multipartFile) throws IOException{
        File convertFile = new File(System.getProperty("user.dir") + "/" + multipartFile.getOriginalFilename());
        // 바로 위에서 지정한 경로에 File이 생성됨 (경로가 잘못되었다면 생성 불가능)
        if (convertFile.createNewFile()) {
            try (FileOutputStream fos = new FileOutputStream(convertFile)) { // FileOutputStream 데이터를 파일에 바이트 스트림으로 저장하기 위함
                fos.write(multipartFile.getBytes());
            }
            return Optional.of(convertFile);
        }

        return Optional.empty();

    }
}

@Value 어노테이션은 yaml이나 properties 파일에 중요한 정보를 저장했을 때 사용하는 방법이다. 필자는 버켓 주소를 yaml파일에 저장해서 사용하였다.
그리고 AmazonS3Client 클래스는 Config파일로써 S3와의 자격증명을 해주는 클래스이다.

테스트

postman test
postman으로 테스트를 진행해보면, s3에 저장된 url 주소가 반환된다 !!! 이로써 front에서 받은 이미지 파일 s3에 저장하기 성공하였다.

다음은 여러개의 파일을 받았을 때, 어떻게 업로드 하는지에 대해 포스팅 하도록 하겠습니다 !

개발 전 주의사항

주의사항)
1.s3에서는 파일 이름이 동일할 경우 파일이 올라가지 않기 때문에 uuid를 통해 고유 키를 나눠준다.
2.파일확장자가 jpeg,png,jpg가 아닐경우에도 파일이 올라가지기 때문에 if문을 걸어 예외처리해준다.
3.파일업로드가 완료되어 204를 받았을 경우 axios로 서버에 location 정보를 보내주어야한다. 언제 보내줄지 처리하는게 관건이고 성공적으로 유저 프로필을 변경하였을 경우 s3에 남아있는 기존 파일을 삭제시켜야한다. (이 과정이 생각보다 굉장히 복잡하다)
4.만약 취소를 누를경우 file.type 이 undefined 되기 때문에 if(file)로 핸들링해줘야함
5.파일사이즈가 1mb 이하일때만 서버에 보내야함
6.파일선택창에서 취소를 누를경우 file을 초기화해줘야함
7.images가 아닌 파일들을 선택하지 못하도록 accept="image/*"로 이미지만 선택
8. delete로 이전 이미지 삭제하기 위해선 버킷 cors정책에서 delete 추가해야합니다.
9. 로컬스토리지 업데이트할때 setitem으로 기존 걸 구조분해해서 업데이트해야합니다..
10. delete할때 파일이름만 적어야하는데 앞에 주소까지 같이 적혀서 split으로 잘라야 한다.
11. 처음 가입한 회원일 경우 /images/user.jpeg 라는 파일로 지정되어 있어서 s3에서 삭제하면 안된다. if로 예외처리 해줘야한다

출처 여기

profile
고민이 많은 개발자

0개의 댓글