S3 이미지 삭제 API 구현하기

이종훈·2025년 4월 6일
1

개발 일지

목록 보기
7/21
post-thumbnail

개요

API 설계

모바일 청첩장 프로젝트에서 청첩장 삭제 및 S3 이미지 삭제 API를 담당하였습니다. 이때 청첩장을 삭제할 때 청첩장 삭제 API와 S3 이미지 삭제 API가 함께 호출됩니다. 청첩장 삭제 API는 DB의 invitation 테이블, 그리고 해당 테이블과 fk가 연결된 여러 테이블들을 모두 삭제합니다. S3 이미지 삭제 API는 각 테이블에서 imgUrl column에 저장되어있는 S3 url에 접근하여 해당 이미지를 삭제하도록 구현해야 합니다.

req, res 흐름 설계

앞서 다룬 것처럼 각 테이블의 imgUrl column에 저장되어있는 S3 url을 가져와서 해당 이미지를 삭제해야 합니다. 이때 해당 url 값은 DB에 저장되어 있기 때문에 삭제 API 호출 시 프론트로부터 url을 바로 받아올 수는 없습니다. 따라서 프론트로부터 청첩장의 id 값을 params로 받아온 후 해당 id의 청첩장, 그리고 청첩장과 fk가 연결된 테이블들에서 imgUrl 값을 반환하여 그 이미지들을 삭제하도록 로직을 구성했습니다.


폴더 구조 설계


기본적으로 백엔드의 폴더 구조는 router, controller, service, repository로 구성되어 있습니다. 하지만 S3 이미지 삭제 API는 repository와 service 필요 없이 router와 controller로 구성하였습니다. 이유는 S3 이미지 삭제 API가 해야할 일은 S3 버킷에 올라가있는 이미지를 삭제하는 것인데, 특정 url이 포함된 테이블을 삭제하는 것은 청첩장 삭제 API가 담당하고 있습니다. 따라서 테이블, 즉 DB에 직접 접근할 필요가 없기 때문에 DB에 접근하여 비즈니스 로직을 담당하는 repository와 service는 제외하는 것으로 구성하게 되었습니다.


구현

router

router.post('/', imageUploader.array("images"), postImage);
router.delete('/:id', deleteImageById);

라우터에서 delete 메서드를 통해 S3 삭제 라우팅을 구현하였습니다. 이때 params로 invitation의 id값을 받아와야 하므로 /:id 값을 설정하였습니다.

util

util에선 2가지 함수를 구현해야 합니다.

export const deleteImageFromS3 = async (key: string): Promise<void> => { // 이미지 삭제
    const bucket = process.env.AWS_BUCKET_NAME as string;
  
    // 해당 key가 실제로 존재하는지 먼저 확인
    try {
      await s3.headObject({ Bucket: bucket, Key: key }).promise();
    } catch (err: any) {
      if (err.code === 'NotFound') {
        throw new Error('해당 key의 파일이 존재하지 않습니다.');
      }
      throw new Error('S3 확인 중 오류 발생: ' + err.message);
    }
  
    // 존재하면 삭제 실행
    try {
      await s3.deleteObject({ Bucket: bucket, Key: key }).promise();
    } catch (err: any) {
      throw new Error('S3 삭제 중 오류 발생: ' + err.message);
    }
  };

첫 번째는 S3 이미지 삭제 함수입니다. S3에는 이미지가 버킷 / 디렉토리 / 이미지 구조로 저장되어 있습니다. 이때 버킷명은 env 파일에서 선언이 되어있기 때문에 env를 호출하여 선언했고, 디렉토리명은 DB에 저장된 url 값을 통해 추출해야 하므로 key라는 변수로 접근하도록 구현했습니다. 버킷안에 key가 존재하는지 확인 후 존재하면 삭제하도록 에러 처리도 포함하였습니다.

export const extractS3KeyFromUrl = (url: string): string | null => { // url 값 추출
  try {
    const { pathname } = new URL(url);
    return decodeURIComponent(pathname).slice(1); 
  } catch {
    return null;
  }
};

두 번째는 키 추출 함수입니다. DB에 저장된 이미지의 url 값은 전체 경로로 저장되어 있습니다. 이때 key 값은 디렉토리 명부터 시작한 이미지의 경로 값을 의미하기 때문에 url에서 key 값을 추출해야 합니다. 따라서 해당 함수를 통해 원하는 key 값만을 추출할 수 있습니다.
예를 들어 DB에 저장된 url이 https://wedding-project-bucket.s3.ap-northeast-2.amazonaws.com/invitation/1743921995449_88485229201_1024.jpg 일 때, 추출 함수를 거친다면 invitation/1743921995449_88485229201_1024.jpg로 key값을 뽑아낼 수 있습니다.

controller

controller에서는 params로 넘겨받은 id를 통해 DB에 접근하여 url값을 반환한 후, util에서 선언한 함수들을 호출하여 이미지를 삭제하도록 구현해야 합니다.

const invitation = await getInvitationById(id);

이때 invitation.repository에서 청첩장 조회 시 선언한 함수(getInvitationById)가 있습니다. 해당 함수를 활용하여 id를 통해 청첩장과 fk로 연결된 테이블들을 조회할 수 있습니다.

for (const url of urlsToDelete) {
      const key = extractS3KeyFromUrl(url);
      if (!key) {
        console.warn("S3 key 추출 실패:", url);
        continue;
      }
    
      try {
        await deleteImageFromS3(key);
        console.log(`삭제 성공: ${key}`);
      } catch (err) {
        console.error(`삭제 실패 (${key}):`, (err as Error).message);
      }
    }    

그 후 각 테이블들의 url 값들을 조회하여 urlsToDelete라는 배열에 저장한 후, 해당 배열을 모두 순회하며 util에서 선언한 함수들을 활용하여 이미지를 삭제합니다. 우선 url에서 key 값을 추출한 후 해당 key 값을 통해 이미지를 삭제하는 흐름으로 이루어집니다. 만약 key 값이 존재하지 않거나 이미지가 존재하지 않는 경우에 대한 에러 처리 또한 구현하였습니다.


실행 결과

구현한 모든 API들을 활용하여 청첩장 및 이미지를 등록하고 다시 삭제하는 전체적인 흐름대로 실행해보았습니다.

1: S3 이미지 등록

우선 S3 이미지 등록 API를 통해 이미지를 S3에 업로드 합니다.

업로드하면 위와 같이 S3 버킷에 이미지가 저장됩니다.

2: 청첩장 등록

앞서 res로 반환된 url들을 각 body의 imgUrl 값에 포함한 후 청첩장 등록 API를 실행합니다.

실행하면 위와 같이 DB의 각 테이블에 해당 url값이 저장됩니다.

3: S3 이미지 삭제

방금 등록한 청첩장의 id(31)을 params로 전달하여 해당 id 청첩장과 여러 테이블에 저장된 모든 이미지들을 삭제합니다.

실행 결과 앞서 등록했던 3가지 이미지가 모두 삭제된 것을 알 수 있습니다.(4월 6일자 이미지 모두 삭제)

4: 청첩장 삭제

마지막으로 해당 청첩장 또한 삭제합니다. 삭제하고자 하는 청첩장의 id가 마찬가지로 31이기 때문에 params로 넘겨주어 삭제합니다.

실행 결과 id 31의 invitation 테이블 및 연관된 모든 테이블이 삭제된 것을 확인할 수 있습니다.
profile
종훈리의 개발일지

0개의 댓글