๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Sonamu๋Š” ํŒŒ์ผ ์—…๋กœ๋“œ์™€ ์ €์žฅ์„ ์œ„ํ•œ ํ†ตํ•ฉ ์Šคํ† ๋ฆฌ์ง€ ์‹œ์Šคํ…œ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋กœ์ปฌ ํŒŒ์ผ ์‹œ์Šคํ…œ(fs)๊ณผ AWS S3๋ฅผ ์ง€์›ํ•˜๋ฉฐ, ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ์‰ฝ๊ฒŒ ์ „ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ๊ตฌ์กฐ

import path from "path";
import { defineConfig } from "sonamu";
import { drivers } from "sonamu/storage";

export default defineConfig({
  server: {
    storage: {
      default: process.env.DRIVE_DISK ?? "fs",
      drivers: {
        fs: drivers.fs({ /* ... */ }),
        s3: drivers.s3({ /* ... */ }),
      },
    },
  },
  // ...
});

default

๊ธฐ๋ณธ์œผ๋กœ ์‚ฌ์šฉํ•  ์Šคํ† ๋ฆฌ์ง€ ๋“œ๋ผ์ด๋ฒ„๋ฅผ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ž…: string
export default defineConfig({
  server: {
    storage: {
      default: "fs",  // fs ๋“œ๋ผ์ด๋ฒ„ ์‚ฌ์šฉ
      // ...
    },
  },
});
ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ์ „ํ™˜:
export default defineConfig({
  server: {
    storage: {
      default: process.env.DRIVE_DISK ?? "fs",  // ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ์ „ํ™˜
      // ...
    },
  },
});
.env:
# ๊ฐœ๋ฐœ: ๋กœ์ปฌ ํŒŒ์ผ ์‹œ์Šคํ…œ
DRIVE_DISK=fs

# ํ”„๋กœ๋•์…˜: AWS S3
DRIVE_DISK=s3
ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ฝ”๋“œ ๋ณ€๊ฒฝ ์—†์ด ์Šคํ† ๋ฆฌ์ง€๋ฅผ ์ „ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

drivers

์‚ฌ์šฉํ•  ์Šคํ† ๋ฆฌ์ง€ ๋“œ๋ผ์ด๋ฒ„๋“ค์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ž…: Record<string, Driver>
import { drivers } from "sonamu/storage";

export default defineConfig({
  server: {
    storage: {
      default: "fs",
      drivers: {
        fs: drivers.fs({ /* ... */ }),    // ๋กœ์ปฌ ํŒŒ์ผ ์‹œ์Šคํ…œ
        s3: drivers.s3({ /* ... */ }),    // AWS S3
      },
    },
  },
});

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

region

S3 ๋ฒ„ํ‚ท์ด ์œ„์น˜ํ•œ AWS ๋ฆฌ์ „์ž…๋‹ˆ๋‹ค. ํƒ€์ž…: string (ํ•„์ˆ˜)
drivers.s3({
  // ...
  region: "ap-northeast-2",  // ์„œ์šธ ๋ฆฌ์ „
})
์ฃผ์š” ๋ฆฌ์ „:
  • ap-northeast-2 - ์„œ์šธ
  • us-east-1 - ๋ฒ„์ง€๋‹ˆ์•„ ๋ถ๋ถ€
  • us-west-2 - ์˜ค๋ ˆ๊ณค
  • eu-west-1 - ์•„์ผ๋žœ๋“œ
  • ap-southeast-1 - ์‹ฑ๊ฐ€ํฌ๋ฅด

bucket

ํŒŒ์ผ์„ ์ €์žฅํ•  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: {
      default: "fs",
      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: {
      default: process.env.DRIVE_DISK ?? "fs",  // ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ์ „ํ™˜
      
      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:
DRIVE_DISK=fs
.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

๋‹ค์ค‘ S3 ๋ฒ„ํ‚ท

์šฉ๋„๋ณ„๋กœ ์—ฌ๋Ÿฌ S3 ๋ฒ„ํ‚ท์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ:
import { defineConfig } from "sonamu";
import { drivers } from "sonamu/storage";

export default defineConfig({
  server: {
    storage: {
      default: "s3-public",
      
      drivers: {
        // ๊ณต๊ฐœ ํŒŒ์ผ์šฉ (ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ๋“ฑ)
        "s3-public": drivers.s3({
          credentials: {
            accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? "",
            secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? "",
          },
          region: "ap-northeast-2",
          bucket: "my-app-public",
          visibility: "public",
        }),
        
        // ํ”„๋ผ์ด๋น— ํŒŒ์ผ์šฉ (๋ฌธ์„œ, ์˜์ˆ˜์ฆ ๋“ฑ)
        "s3-private": drivers.s3({
          credentials: {
            accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? "",
            secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? "",
          },
          region: "ap-northeast-2",
          bucket: "my-app-private",
          visibility: "private",
        }),
      },
    },
  },
  // ...
});
์‚ฌ์šฉ ์˜ˆ์‹œ:
import { StorageManager } from "sonamu/storage";

// ๊ณต๊ฐœ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ
await StorageManager.use("s3-public").put("profile.jpg", buffer);

// ํ”„๋ผ์ด๋น— ๋ฌธ์„œ ์—…๋กœ๋“œ
await StorageManager.use("s3-private").put("invoice.pdf", buffer);

ํŒŒ์ผ ์—…๋กœ๋“œ ์‚ฌ์šฉ

์Šคํ† ๋ฆฌ์ง€ ์„ค์ • ํ›„ API์—์„œ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
import { api } from "sonamu";
import { StorageManager } from "sonamu/storage";
import type { UploadedFile } from "sonamu";

export class FileModel {
  @api({ httpMethod: "POST" })
  static async upload(file: UploadedFile) {
    // ํŒŒ์ผ ์ €์žฅ
    const key = `uploads/${Date.now()}-${file.filename}`;
    await StorageManager.put(key, file.buffer);
    
    // URL ์ƒ์„ฑ
    const url = await StorageManager.url(key);
    
    return { key, 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`;

๋‹ค์Œ ๋‹จ๊ณ„

์Šคํ† ๋ฆฌ์ง€ ์„ค์ •์„ ์™„๋ฃŒํ–ˆ๋‹ค๋ฉด:
  • file-upload - API์—์„œ ํŒŒ์ผ ์—…๋กœ๋“œ ๊ตฌํ˜„
  • cache - ์บ์‹œ ์„ค์ •
  • advanced-features/storage - ๊ณ ๊ธ‰ ์Šคํ† ๋ฆฌ์ง€ ๊ธฐ๋Šฅ