앞서 다룬 것처럼 각 테이블의 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.post('/', imageUploader.array("images"), postImage);
router.delete('/:id', deleteImageById);
라우터에서 delete 메서드를 통해 S3 삭제 라우팅을 구현하였습니다. 이때 params로 invitation의 id값을 받아와야 하므로 /:id 값을 설정하였습니다.
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에서는 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들을 활용하여 청첩장 및 이미지를 등록하고 다시 삭제하는 전체적인 흐름대로 실행해보았습니다.