클라우드 스토리지
서버를 거치지 않고 클라우드 스토리지 공급자에 직접 파일을 업로드하세요.
Amazon S3
설치
npm install @samithahansaka/dropup
기본 사용법
import { useDropup } from '@samithahansaka/dropup';
import { createS3Uploader } from '@samithahansaka/dropup/cloud/s3';
function S3Uploader() {
const { files, actions, getDropProps, getInputProps } = useDropup({
upload: createS3Uploader({
getPresignedUrl: async (file) => {
// 백엔드를 호출하여 미리 서명된 URL 가져오기
const response = await fetch('/api/s3/presign', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
contentType: file.type,
}),
});
return response.json();
},
}),
onUploadComplete: (file) => {
console.log('Uploaded to S3:', file.uploadedUrl);
},
});
return (
<div {...getDropProps()}>
<input {...getInputProps()} />
<p>Drop files to upload to S3</p>
</div>
);
}
백엔드: 미리 서명된 URL 생성
// Node.js / Express 예제
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3 = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
app.post('/api/s3/presign', async (req, res) => {
const { filename, contentType } = req.body;
const key = `uploads/${Date.now()}-${filename}`;
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
ContentType: contentType,
});
const url = await getSignedUrl(s3, command, { expiresIn: 3600 });
res.json({
url,
fields: {}, // 단순 PUT의 경우 추가 필드 불필요
});
});
S3 POST 사용 (멀티파트 폼)
S3 POST 정책의 경우:
createS3Uploader({
getPresignedUrl: async (file) => {
const response = await fetch('/api/s3/presign-post', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
contentType: file.type,
}),
});
const { url, fields } = await response.json();
return {
url, // S3 버킷 URL
fields, // 폼에 포함할 정책 필드
};
},
});
Google Cloud Storage
import { createGCSUploader } from '@samithahansaka/dropup/cloud/gcs';
function GCSUploader() {
const { files, getDropProps, getInputProps } = useDropup({
upload: createGCSUploader({
getSignedUrl: async (file) => {
const response = await fetch('/api/gcs/sign', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
contentType: file.type,
}),
});
return response.json();
},
}),
});
return (
<div {...getDropProps()}>
<input {...getInputProps()} />
<p>Upload to Google Cloud Storage</p>
</div>
);
}
백엔드: GCS 서명된 URL
// Node.js 예제
import { Storage } from '@google-cloud/storage';
const storage = new Storage();
const bucket = storage.bucket(process.env.GCS_BUCKET);
app.post('/api/gcs/sign', async (req, res) => {
const { filename, contentType } = req.body;
const blob = bucket.file(`uploads/${Date.now()}-${filename}`);
const [url] = await blob.getSignedUrl({
version: 'v4',
action: 'write',
expires: Date.now() + 15 * 60 * 1000, // 15분
contentType,
});
res.json({ url });
});
Azure Blob Storage
import { createAzureUploader } from '@samithahansaka/dropup/cloud/azure';
function AzureUploader() {
const { files, getDropProps, getInputProps } = useDropup({
upload: createAzureUploader({
getSasUrl: async (file) => {
const response = await fetch('/api/azure/sas', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
contentType: file.type,
}),
});
return response.json();
},
}),
});
return (
<div {...getDropProps()}>
<input {...getInputProps()} />
<p>Upload to Azure Blob Storage</p>
</div>
);
}
백엔드: Azure SAS URL
// Node.js 예제
import {
BlobServiceClient,
generateBlobSASQueryParameters,
BlobSASPermissions,
} from '@azure/storage-blob';
const blobServiceClient = BlobServiceClient.fromConnectionString(
process.env.AZURE_STORAGE_CONNECTION_STRING
);
app.post('/api/azure/sas', async (req, res) => {
const { filename, contentType } = req.body;
const containerClient = blobServiceClient.getContainerClient('uploads');
const blobName = `${Date.now()}-${filename}`;
const blobClient = containerClient.getBlockBlobClient(blobName);
const sasToken = generateBlobSASQueryParameters(
{
containerName: 'uploads',
blobName,
permissions: BlobSASPermissions.parse('cw'), // 생성, 쓰기
expiresOn: new Date(Date.now() + 15 * 60 * 1000),
},
blobServiceClient.credential
).toString();
res.json({
url: `${blobClient.url}?${sasToken}`,
headers: {
'x-ms-blob-type': 'BlockBlob',
'Content-Type': contentType,
},
});
});
Cloudflare R2
R2는 S3 호환이므로 S3 업로더를 사용하세요:
import { createS3Uploader } from '@samithahansaka/dropup/cloud/s3';
function R2Uploader() {
const { files, getDropProps, getInputProps } = useDropup({
upload: createS3Uploader({
getPresignedUrl: async (file) => {
const response = await fetch('/api/r2/presign', {
method: 'POST',
body: JSON.stringify({ filename: file.name }),
});
return response.json();
},
}),
});
return (
<div {...getDropProps()}>
<input {...getInputProps()} />
<p>Upload to Cloudflare R2</p>
</div>
);
}
백엔드: R2 미리 서명된 URL
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3 = new S3Client({
region: 'auto',
endpoint: `https://${process.env.CF_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
},
});
app.post('/api/r2/presign', async (req, res) => {
const { filename } = req.body;
const command = new PutObjectCommand({
Bucket: process.env.R2_BUCKET,
Key: `uploads/${Date.now()}-${filename}`,
});
const url = await getSignedUrl(s3, command, { expiresIn: 3600 });
res.json({ url });
});
DigitalOcean Spaces
또한 S3 호환:
// S3와 동일하지만 백엔드 엔드포인트 구성만 업데이트
const s3 = new S3Client({
region: 'nyc3',
endpoint: 'https://nyc3.digitaloceanspaces.com',
credentials: {
accessKeyId: process.env.DO_SPACES_KEY,
secretAccessKey: process.env.DO_SPACES_SECRET,
},
});
사용자 정의 클라우드 공급자
모든 클라우드 서비스에 대한 자체 업로더를 만드세요:
import { useDropup, type CustomUploader } from '@samithahansaka/dropup';
const customCloudUploader: CustomUploader = async (file, options) => {
// 1. 백엔드에서 업로드 URL 가져오기
const { uploadUrl, fileUrl } = await fetch('/api/custom-cloud/init', {
method: 'POST',
body: JSON.stringify({ filename: file.name, size: file.size }),
}).then(r => r.json());
// 2. 파일 업로드
const xhr = new XMLHttpRequest();
return new Promise((resolve, reject) => {
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
options.onProgress((e.loaded / e.total) * 100);
}
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve({ url: fileUrl });
} else {
reject(new Error('Upload failed'));
}
};
xhr.onerror = () => reject(new Error('Network error'));
// 취소 처리
options.signal.addEventListener('abort', () => xhr.abort());
xhr.open('PUT', uploadUrl);
xhr.send(file.file);
});
};
function CustomCloudUploader() {
const { files, getDropProps, getInputProps } = useDropup({
upload: customCloudUploader,
});
return (
<div {...getDropProps()}>
<input {...getInputProps()} />
<p>Upload to custom cloud</p>
</div>
);
}
보안 모범 사례
- 클라이언트에 자격 증명을 노출하지 마세요 - 항상 백엔드에서 서명된 URL을 생성하세요
- 짧은 만료 시간을 사용하세요 - 5-15분이면 일반적으로 충분합니다
- 백엔드에서 파일 유형을 검증하세요 - 클라이언트 측 검증에만 의존하지 마세요
- 클라우드 스토리지에 적절한 CORS 정책을 설정하세요
- 미리 서명된 URL 정책에서 파일 크기를 제한하세요
- 사용자 업로드와 애플리케이션 자산에 별도의 버킷을 사용하세요