웹 개발
파일 업로드 시 이름 충돌을 피하는 방법
사용자가 올린 파일을 원래 이름 그대로 저장하면 같은 이름의 파일이 덮어씌워지거나 경로가 노출된다. UUID나 해시로 파일명을 바꿔서 저장하는 방식을 정리했다.
파일 업로드 기능을 처음 만들면 originalname을 그대로 저장하는 코드를 쓰기 쉽다. 잠깐은 잘 돌아가는 것처럼 보이지만, 다른 사용자가 같은 이름의 파일을 올리면 이전 파일이 덮어써진다. 그리고 파일명에 사용자 정보가 담겨 있으면 URL에서 노출되는 문제도 생긴다.
서버에서 파일명을 새로 만드는 방법
Node.js 기준으로 가장 간단한 방법은 crypto.randomUUID()로 고유 이름을 만들고 원본 확장자를 붙이는 것이다.
import { randomUUID } from 'crypto';
import path from 'path';
function generateStorageName(originalFilename: string): string {
const ext = path.extname(originalFilename).toLowerCase();
return `${randomUUID()}${ext}`;
}
// 예: 'profile.JPG' → 'f47ac10b-58cc-4372-a567-0e02b2c3d479.jpg'
확장자를 소문자로 통일하는 건 S3나 오브젝트 스토리지에서 대소문자를 구분하는 경우를 막기 위해서다.
원본 파일명은 DB에 따로 저장한다
저장소에는 UUID 기반 이름을 쓰고, 원본 파일명은 DB에 별도 컬럼으로 보관한다. 그러면 사용자에게 다운로드를 제공할 때 원본 이름으로 내려줄 수 있다.
CREATE TABLE uploads (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
storage_key TEXT NOT NULL, -- 실제 저장된 경로
filename TEXT NOT NULL, -- 사용자가 올린 원본 파일명
mime_type TEXT NOT NULL,
size_bytes INTEGER NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
API에서 파일을 내려줄 때는 Content-Disposition 헤더에 원본 파일명을 실어서 보내면 브라우저가 그 이름으로 저장한다.
res.setHeader(
'Content-Disposition',
`attachment; filename="${encodeURIComponent(upload.filename)}"`
);
S3에 올릴 때 경로 구조
파일을 오브젝트 스토리지에 넣을 때 루트에 파일을 쌓으면 나중에 관리가 어렵다. 사용자 ID나 날짜로 경로를 구분해두면 정리가 편하다.
const storageKey = `uploads/${userId}/${generateStorageName(file.originalname)}`;
await s3.putObject({
Bucket: process.env.S3_BUCKET,
Key: storageKey,
Body: file.buffer,
ContentType: file.mimetype,
});
이렇게 하면 특정 사용자의 파일을 한꺼번에 삭제하거나 용량을 집계할 때 prefix만으로 필터링할 수 있다. UUID 하나로 고유성을 보장하면서 경로로 분류까지 되는 구조가 가장 다루기 편하다.