Sonamuλ νμΌ μ
λ‘λμ μ μ₯μ μν ν΅ν© μ€ν λ¦¬μ§ μμ€ν
μ μ 곡ν©λλ€. λ‘컬 νμΌ μμ€ν
(fs)κ³Ό AWS S3λ₯Ό μ§μνλ©°, νκ²½μ λ°λΌ μ½κ² μ νν μ μμ΅λλ€.
기본 ꡬ쑰
import path from "path";
import { defineConfig } from "sonamu";
import { drivers } from "sonamu/storage";
export default defineConfig({
server: {
storage: {
drivers: {
fs: drivers.fs({
/* ... */
}),
s3: drivers.s3({
/* ... */
}),
},
},
},
// ...
});
drivers
μ¬μ©ν μ€ν λ¦¬μ§ λλΌμ΄λ²λ€μ μ μν©λλ€. "fs"(λ‘컬 νμΌ μμ€ν
)μ "s3"(AWS S3) λ μ’
λ₯λ₯Ό μ§μν©λλ€.
νμ
: Record<DriverKey, () => DriverContract> (DriverKey = "fs" | "s3")
import { drivers } from "sonamu/storage";
export default defineConfig({
server: {
storage: {
drivers: {
fs: drivers.fs({
/* ... */
}), // λ‘컬 νμΌ μμ€ν
s3: drivers.s3({
/* ... */
}), // AWS S3
},
},
},
});
νμΌ μ μ₯ μ saveToDisk(diskName, key) νΈμΆμμ μ¬μ©ν λλΌμ΄λ²λ₯Ό λͺ
μμ μΌλ‘ μ§μ ν©λλ€. νκ²½λ³λ‘ λ€λ₯Έ λλΌμ΄λ²λ₯Ό μ¬μ©νλ €λ©΄ νκ²½ λ³μλ₯Ό νμ©νμ¬ μ½λ λ 벨μμ λΆκΈ°ν μ μμ΅λλ€.
fs λλΌμ΄λ²
λ‘컬 νμΌ μμ€ν
μ νμΌμ μ μ₯ν©λλ€. κ°λ° νκ²½μ μ ν©ν©λλ€.
import path from "path";
import { drivers } from "sonamu/storage";
export default defineConfig({
server: {
storage: {
drivers: {
fs: drivers.fs({
location: path.join(import.meta.dirname, "/../public/uploaded"),
visibility: "public",
urlBuilder: {
generateURL(key) {
return `/api/public/uploaded/${key}`;
},
generateSignedURL(key) {
return `/api/public/uploaded/${key}`;
},
},
}),
},
},
},
});
location
νμΌμ΄ μ μ₯λ λλ ν 리 κ²½λ‘μ
λλ€.
νμ
: string (νμ)
drivers.fs({
location: path.join(import.meta.dirname, "/../public/uploaded"),
// ...
});
κ²½λ‘ μμ:
visibility
νμΌμ μ κ·Ό κΆνμ μ€μ ν©λλ€.
νμ
: "public" | "private"
"public": λꡬλ URLλ‘ μ κ·Ό κ°λ₯
"private": μΈμ¦λ μ¬μ©μλ§ μ κ·Ό κ°λ₯
drivers.fs({
location: path.join(import.meta.dirname, "/../public/uploaded"),
visibility: "public", // κ³΅κ° νμΌ
// ...
});
urlBuilder
νμΌ URLμ μμ±νλ ν¨μλ€μ μ μν©λλ€.
drivers.fs({
// ...
urlBuilder: {
// μΌλ° URL μμ±
generateURL(key) {
return `/api/public/uploaded/${key}`;
},
// μλͺ
λ URL μμ± (μμ μ κ·Ό)
generateSignedURL(key, expiresIn) {
// fsμμλ λ³΄ν΅ μΌλ° URLκ³Ό λμΌ
return `/api/public/uploaded/${key}`;
},
},
});
generateURL: νμΌμ κ³΅κ° URLμ μμ±ν©λλ€.
- key: νμΌμ κ³ μ ν€ (μ:
"profile-images/user-123.jpg")
- λ°ν: μ κ·Ό κ°λ₯ν URL
generateSignedURL: μμλ‘ μ κ·Ό κ°λ₯ν μλͺ
λ URLμ μμ±ν©λλ€.
- key: νμΌμ κ³ μ ν€
- expiresIn: λ§λ£ μκ° (μ΄, μ νμ )
- λ°ν: μμ URL
s3 λλΌμ΄λ²
AWS S3μ νμΌμ μ μ₯ν©λλ€. νλ‘λμ
νκ²½μ μ ν©ν©λλ€.
import { drivers } from "sonamu/storage";
export default defineConfig({
server: {
storage: {
drivers: {
s3: drivers.s3({
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? "",
},
region: "ap-northeast-2",
bucket: "my-app-uploads",
visibility: "private",
}),
},
},
},
});
credentials
AWS μΈμ¦ μ 보λ₯Ό μ€μ ν©λλ€.
νμ
: (νμ)
credentials: {
accessKeyId: string;
secretAccessKey: string;
}
drivers.s3({
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? "",
},
// ...
});
AWS μΈμ¦ μ 보λ λ°λμ νκ²½ λ³μλ‘ κ΄λ¦¬νμΈμ. μ½λμ μ§μ μμ±νμ§ λ§μΈμ!
.env:
AWS_ACCESS_KEY_ID=your_access_key_here
AWS_SECRET_ACCESS_KEY=your_secret_key_here
S3 λ²ν·μ΄ μμΉν AWS 리μ μ
λλ€.
νμ
: string (νμ)
drivers.s3({
// ...
region: "ap-northeast-2", // μμΈ λ¦¬μ
});
μ£Όμ 리μ :
ap-northeast-2 - μμΈ
us-east-1 - λ²μ§λμ λΆλΆ
us-west-2 - μ€λ κ³€
eu-west-1 - μμΌλλ
ap-southeast-1 - μ±κ°ν¬λ₯΄
νμΌμ μ μ₯ν S3 λ²ν· μ΄λ¦μ
λλ€.
νμ
: string (νμ)
drivers.s3({
// ...
bucket: "my-app-uploads",
});
νκ²½λ³λ‘ λ€λ₯Έ λ²ν· μ¬μ©:
drivers.s3({
// ...
bucket: process.env.S3_BUCKET ?? "my-app-dev",
});
visibility
S3 κ°μ²΄μ ACL(Access Control List)μ μ€μ ν©λλ€.
νμ
: "public" | "private"
drivers.s3({
// ...
visibility: "private", // μΈμ¦ νμ
});
"public": νΌλΈλ¦ μ½κΈ° νμ© (public-read ACL)
"private": νλΌμ΄λΉ (private ACL, κΈ°λ³Έκ°)
μ€μ μμ
κ°λ° νκ²½: fsλ§ μ¬μ©
import path from "path";
import { defineConfig } from "sonamu";
import { drivers } from "sonamu/storage";
export default defineConfig({
server: {
storage: {
drivers: {
fs: drivers.fs({
location: path.join(import.meta.dirname, "/../public/uploaded"),
visibility: "public",
urlBuilder: {
generateURL(key) {
return `/api/public/uploaded/${key}`;
},
generateSignedURL(key) {
return `/api/public/uploaded/${key}`;
},
},
}),
},
},
},
// ...
});
fs + S3 (νκ²½λ³ μ ν)
λ λλΌμ΄λ²λ₯Ό λͺ¨λ λ±λ‘νκ³ , μ½λμμ νκ²½ λ³μλ₯Ό κΈ°λ°μΌλ‘ μ¬μ©ν λλΌμ΄λ²λ₯Ό μ νν©λλ€.
import path from "path";
import { defineConfig } from "sonamu";
import { drivers } from "sonamu/storage";
export default defineConfig({
server: {
storage: {
drivers: {
// κ°λ°: λ‘컬 νμΌ μμ€ν
fs: drivers.fs({
location: path.join(import.meta.dirname, "/../public/uploaded"),
visibility: "public",
urlBuilder: {
generateURL(key) {
return `/api/public/uploaded/${key}`;
},
generateSignedURL(key) {
return `/api/public/uploaded/${key}`;
},
},
}),
// νλ‘λμ
: AWS S3
s3: drivers.s3({
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? "",
},
region: process.env.S3_REGION ?? "ap-northeast-2",
bucket: process.env.S3_BUCKET ?? "my-app-prod",
visibility: "private",
}),
},
},
},
// ...
});
.env.development:
.env.production:
DRIVE_DISK=s3
S3_REGION=ap-northeast-2
S3_BUCKET=my-app-prod-uploads
AWS_ACCESS_KEY_ID=your_access_key_here
AWS_SECRET_ACCESS_KEY=your_secret_key_here
νμ¬ DriverKeyλ "fs"μ "s3" λ κ°μ§λ§ μ§μν©λλ€. 곡κ°/λΉκ³΅κ° νμΌμ λ³λ λ²ν·μ λλμ΄ μ μ₯νλ λ€μ€ λ²ν· ꡬμ±μ νμ¬ μ§μλμ§ μμ΅λλ€.
νμΌ μ
λ‘λ μ¬μ©
μ€ν λ¦¬μ§ μ€μ ν @upload λ°μ½λ μ΄ν°μ BufferedFile.saveToDisk()λ₯Ό μ¬μ©νμ¬ νμΌμ μ
λ‘λν μ μμ΅λλ€.
import { type DriverKey } from "sonamu/storage";
import { Sonamu } from "sonamu";
import { upload } from "sonamu/decorators";
import { BaseModelClass } from "sonamu";
export class FileModel extends BaseModelClass {
@upload()
static async upload() {
const ctx = Sonamu.getContext();
const file = ctx.bufferedFiles[0];
if (!file) throw new Error("μ
λ‘λλ νμΌμ΄ μμ΅λλ€.");
// νκ²½ λ³μλ‘ λλΌμ΄λ² μ ν
const diskName: DriverKey = process.env.DRIVE_DISK === "s3" ? "s3" : "fs";
const url = await file.saveToDisk(diskName, `uploads/${Date.now()}-${file.filename}`);
return { url };
}
}
β νμΌ μ
λ‘λ κ°μ΄λ
S3 λ²ν· μ€μ
S3λ₯Ό μ¬μ©νκΈ° μ μ AWS μ½μμμ λ²ν·μ μμ±νκ³ μ€μ ν΄μΌ ν©λλ€.
1. λ²ν· μμ±
# AWS CLIλ‘ λ²ν· μμ±
aws s3 mb s3://my-app-uploads --region ap-northeast-2
2. CORS μ€μ
νλ‘ νΈμλμμ μ§μ μ
λ‘λνλ €λ©΄ CORSλ₯Ό μ€μ ν©λλ€.
S3 μ½μ β λ²ν· β κΆν β CORS ꡬμ±:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
"AllowedOrigins": ["https://myapp.com"],
"ExposeHeaders": ["ETag"]
}
]
3. IAM κΆν
Sonamuκ° S3μ μ κ·Όνλ €λ©΄ μ μ ν IAM κΆνμ΄ νμν©λλ€.
μ΅μ κΆν μ μ±
:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject", "s3:ListBucket"],
"Resource": ["arn:aws:s3:::my-app-uploads", "arn:aws:s3:::my-app-uploads/*"]
}
]
}
μ£Όμμ¬ν
1. νκ²½ λ³μ 보μ
// β λμ μ: μ½λμ μΈμ¦ μ 보 μ§μ μμ±
drivers.s3({
credentials: {
accessKeyId: "AKIAXXXXXXXXXXXXXXXX",
secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCY...",
},
});
// β
μ’μ μ: νκ²½ λ³μ μ¬μ©
drivers.s3({
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? "",
},
});
2. visibility μ ν
// κ³΅κ° νμΌ (νλ‘ν μ΄λ―Έμ§, μν μ΄λ―Έμ§ λ±)
visibility: "public";
// νλΌμ΄λΉ νμΌ (κ°μΈ λ¬Έμ, μμμ¦, κ³μ½μ λ±)
visibility: "private";
3. νμΌ κ²½λ‘ μ€κ³
// β
μ’μ μ: 체κ³μ μΈ κ²½λ‘ κ΅¬μ‘°
const key = `users/${userId}/profile/${Date.now()}.jpg`;
const key = `documents/${year}/${month}/${documentId}.pdf`;
// β λμ μ: ννν ꡬ쑰
const key = `${Date.now()}.jpg`;
λ€μ λ¨κ³
μ€ν λ¦¬μ§ μ€μ μ μλ£νλ€λ©΄: