파일 뷰어가 간헐적으로 동작하지 않습니다.

junyeon·2025년 8월 16일

오답노트

목록 보기
4/4
post-thumbnail

S3 파일 미리보기 404 오류 해결기 (파일명 인코딩 문제)

개요

웹 뷰어에서 간헐적으로 S3 파일을 불러올 때 404 Not Found 에러가 발생했다.
문제의 원인을 파악해본 결과, 파일 이름이 URL에 포함될 때 한글 인코딩 이슈로 인해 S3에서 리소스를 찾지 못하는 상황이었고, 이를 해결하기 위해 파일명을 해시로 변환하는 방식으로 문제를 해결했다.


문제 상황

  • 뷰어에서 파일 미리보기를 요청했을 때 404 에러가 간헐적으로 발생
  • S3 presigned URL을 통해 파일을 불러오는 구조였음
  • 해당 presigned URL은 클라이언트로 전달되어 미리보기에 사용됨
  • 문제의 원인은 파일명이 한글이거나 너무 길 경우, URL 인코딩 후 길이가 길어져 S3에서 요청을 제대로 처리하지 못함

presigned URL 발급 로직

async function issuePresignedUrl(ctx: Context, params: Params, q: Query): Promise<string> {
  require(ctx.req && ctx.user && params.classId, '필수 입력 누락');

  const safeName = toHashedName(q.fileName); // ← 핵심: 안전한 해시 파일명 생성
  const base = normalizeKeyPrefix(q.keyName);
  const objectKey = `${base}/${safeName}`;

  const common = { bucket: env.S3_BUCKET, key: objectKey, expiresIn: 120 };
  const presign =
    q.action === 'download'
      ? await storage.presignGetObject({ ...common, responseContentDisposition: asDownload(q.fileName) })
      : await storage.presignPutObject({ ...common, contentType: guessMime(q.fileName) });

  audit.log({
    userId: ctx.user.id,
    action: q.action,
    keyPrefix: base,
    fileHashName: safeName,
    classId: params.classId,
  });

  require(presign, 'URL 발급 실패');
  return presign;
}

해결 방법: 파일명을 해시로 변환

/**
 * 파일명을 해시로 변환하여 S3에 안전하게 저장
 */
private generateHashFileName(originalFileName: string): string {
  const hash = crypto.createHash('md5').update(originalFileName).digest('hex').substring(0, 12);
  const extension = originalFileName.split('.').pop() || '';
  return `${hash}.${extension}`;
}
  • Node.js의 crypto 모듈을 활용해 파일명을 해싱
  • 확장자를 유지하면서 해시된 이름으로 대체
  • 덕분에 presigned URL이 더 이상 길거나 인코딩 문제로 깨지지 않음

추가 개선: 파일 다운로드 시 원래 이름 유지

  • presigned URL을 직접 링크에 사용하는 경우, 해시된 파일명이 그대로 노출됨
  • 사용자에게는 원래 파일명을 보여주고 싶었음

해결 방법

  • Blob을 사용해 응답 파일을 메모리에 담고
  • window.URL.createObjectURL(blob) 으로 로컬 URL 생성
  • link.download = decodeURIComponent(fileName) 을 통해 실제 사용자에게 보일 이름 설정
const fileRes = await axios.get(presignedUrl, { responseType: 'blob' });
const blob = new Blob([fileRes.data]);
const url = window.URL.createObjectURL(blob);

const link = document.createElement('a');
link.href = url;
link.download = decodeURIComponent(originalFileName); // 원래 파일명
link.click();
link.remove();

결과

  • 한글, 공백, 특수문자 등 어떤 파일명이든 안전하게 presigned URL 생성 가능
  • 사용자에게는 다운로드 시 원래 이름이 표시됨
  • 뷰어에서의 404 Not Found 에러가 완전히 해결됨

Blob이란?

참고 링크

profile
이봐 젊은 친구야 잃어버린 것들은 잃어버린 그 자리에

0개의 댓글