같은 서비스에서 파일을 두 가지 방식으로 내려주는 이유
관리자 대시보드에서 Presigned URL과 File Gateway를 함께 쓰고 있었습니다. 두 방식의 차이와 언제 어떤 걸 쓰는지 실제 코드로 비교합니다.
관리자 대시보드의 파일 표시
관리자 대시보드에는 파일을 보여주는 화면이 여러 군데 있습니다.
- 문서 관리 → 첨부파일 미리보기
- 이벤트 관리 → 배너 이미지
- 상품 관리 → 상품 이미지
처음에는 전부 같은 방식으로 파일을 가져오는 줄 알았습니다. 그런데 인수인계 문서를 작성하면서 코드를 다시 읽어보니 두 가지 방식이 섞여 있었습니다.
Presigned URL 방식
문서와 이벤트의 첨부파일은 백엔드에서 presigned URL을 내려줍니다.
// 백엔드 응답에 이미 URL이 포함됨
const response = await api.get("/documents/123");
// response.data.fileUrl → "https://storage.example.com/files/doc.pdf?signature=abc..."
프론트엔드에서는 받은 URL을 그대로 사용하면 됩니다.
<img src={document.fileUrl} alt="첨부파일" />
// 또는
<a href={document.fileUrl} download>다운로드</a>
특징:
- 프론트엔드에서 별도의 인증 처리가 필요 없음
- URL 자체에 인증 정보(signature)가 포함됨
- URL에 만료 시간이 있어서 일정 시간 후 접근 불가
File Gateway 방식
상품 이미지는 다른 방식입니다. 별도의 파일 토큰을 발급받고, 그 토큰으로 Gateway에 요청해서 blob 데이터를 받아옵니다.
// 1. 파일 토큰 발급
const tokenResponse = await axios.get("/token/file", {
headers: { Authorization: `Bearer ${accessToken}` },
});
const fileToken = tokenResponse.data.fileAccessToken;
// 2. Gateway에 파일 요청
const imageResponse = await axios.get(
`${GATEWAY_URL}/${encodeURIComponent(filePath)}`,
{
responseType: "blob",
headers: { Authorization: `Bearer ${fileToken}` },
}
);
// 3. blob을 URL로 변환
const imageUrl = URL.createObjectURL(imageResponse.data);
특징:
- 파일 토큰을 별도로 관리해야 함
- blob으로 받아서
URL.createObjectURL()로 변환 - 메모리 해제를 위해
URL.revokeObjectURL()도 필요
왜 두 가지를 섞어 쓰는가
문서나 이벤트 첨부파일은 조회 빈도가 낮고, 한 번 다운로드하면 끝인 경우가 많습니다. 이런 파일은 백엔드에서 presigned URL을 바로 내려주는 게 프론트 입장에서도 간단하고 서버 부하도 적습니다. 반면 상품 이미지는 목록 페이지에서 한꺼번에 여러 장이 노출되고, 같은 이미지가 반복적으로 요청됩니다. Gateway를 두면 캐싱이나 접근 제어를 중앙에서 관리할 수 있어서 이쪽이 더 적합했습니다.
일반적으로 이런 차이가 생기는 이유는:
Presigned URL이 적합한 경우:
- 접근 빈도가 낮은 파일 (문서, 리포트)
- 일회성 다운로드가 많은 경우
- 서버 부하를 줄이고 싶을 때 (클라이언트가 스토리지에 직접 접근)
File Gateway가 적합한 경우:
- 접근 빈도가 높은 파일 (상품 이미지)
- Gateway 레벨에서 캐싱, 리사이징 등 처리가 필요한 경우
- 파일 접근 로그를 중앙에서 관리하고 싶을 때
실제 사용 시 주의점
File Gateway 방식을 쓸 때 주의할 점이 몇 가지 있습니다.
1. blob URL 메모리 관리
URL.createObjectURL()은 메모리에 blob 참조를 생성합니다. 컴포넌트가 언마운트될 때 해제해야 합니다.
useEffect(() => {
return () => {
if (imageUrl && imageUrl.startsWith("blob:")) {
URL.revokeObjectURL(imageUrl);
}
};
}, [imageUrl]);
2. 토큰 만료 처리
파일 토큰이 만료되면 Gateway에서 401 응답이 옵니다. 이때 토큰을 자동으로 갱신하고 재시도하는 로직이 필요합니다.
3. 동시 호출 방지
여러 이미지 컴포넌트가 동시에 토큰을 요청하면 중복 API 호출이 발생합니다. Promise 캐싱 패턴으로 해결할 수 있는데, 이 내용은 Promise 캐싱으로 중복 API 호출을 방지하는 패턴에서 다뤘습니다.
정리
| Presigned URL | File Gateway | |
|---|---|---|
| 인증 | URL에 포함 | 별도 토큰 필요 |
| 응답 | URL 문자열 | blob 데이터 |
| 프론트 처리 | 거의 없음 | blob → ObjectURL 변환 |
| 만료 관리 | URL 만료 | 토큰 만료 |
| 서버 부하 | 낮음 (직접 접근) | 중간 (Gateway 경유) |
인수인계 문서를 쓰지 않았으면 이 차이를 계속 모르고 있었을 겁니다. 코드를 다시 읽는 계기가 된 셈입니다.