๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Storage ๋“œ๋ผ์ด๋ฒ„๋Š” ์ €์žฅ๋œ ํŒŒ์ผ์˜ URL์„ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. URL ๋นŒ๋”๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ CDN, ์ปค์Šคํ…€ ๋„๋ฉ”์ธ, Signed URL ๋“ฑ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

URL ๋นŒ๋”๋ž€?

URL ๋นŒ๋”๋Š” ํŒŒ์ผ ํ‚ค(key)๋ฅผ URL๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.
{
  urlBuilder: {
    generateURL: (key: string) => string
  }
}
์˜ˆ์‹œ:
key: 'uploads/avatar.png'
โ†’ '/uploads/avatar.png'  (fs)
โ†’ 'https://bucket.s3.amazonaws.com/uploads/avatar.png'  (s3)
โ†’ 'https://cdn.example.com/uploads/avatar.png'  (CDN)

fs ๋“œ๋ผ์ด๋ฒ„ URL ๋นŒ๋”

fs ๋“œ๋ผ์ด๋ฒ„๋Š” ๋ฐ˜๋“œ์‹œ urlBuilder ์„ค์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ์„ค์ •

import { drivers } from "sonamu";

storage: {
  default: 'fs',
  drivers: {
    fs: drivers.fs({
      location: './uploads',
      urlBuilder: {
        generateURL: (key) => `/uploads/${key}`,
      }
    }),
  }
}
๊ฒฐ๊ณผ:
// key: 'avatars/user-1.png'
// location: './uploads'
// ํŒŒ์ผ ๊ฒฝ๋กœ: ./uploads/avatars/user-1.png
// URL: /uploads/avatars/user-1.png

์ปค์Šคํ…€ ๋„๋ฉ”์ธ

storage: {
  default: 'fs',
  drivers: {
    fs: drivers.fs({
      location: './uploads',
      urlBuilder: {
        generateURL: (key) => 
          `https://files.example.com/uploads/${key}`,
      }
    }),
  }
}
๊ฒฐ๊ณผ:
// key: 'avatars/user-1.png'
// URL: https://files.example.com/uploads/avatars/user-1.png
์‚ฌ์šฉ ์‚ฌ๋ก€: ๋ณ„๋„ ํŒŒ์ผ ์„œ๋ฒ„๋‚˜ CDN

์ •์  ํŒŒ์ผ ์„œ๋น™

fs ๋“œ๋ผ์ด๋ฒ„๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” Fastify static ํ”Œ๋Ÿฌ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
import path from "path";
import { type SonamuConfig } from "sonamu";

export const config: SonamuConfig = {
  server: {
    // ์ •์  ํŒŒ์ผ ์„œ๋น™
    plugins: {
      static: {
        root: path.join(process.cwd(), 'uploads'),
        prefix: '/uploads/',
      }
    },
    
    // Storage ์„ค์ •
    storage: {
      default: 'fs',
      drivers: {
        fs: drivers.fs({
          location: './uploads',
          urlBuilder: {
            generateURL: (key) => `/uploads/${key}`,
          }
        }),
      }
    }
  }
};
ํšจ๊ณผ:
http://localhost:3000/uploads/avatars/user-1.png
โ†’ ./uploads/avatars/user-1.png ํŒŒ์ผ ์„œ๋น™

S3 ๋“œ๋ผ์ด๋ฒ„ URL ๋นŒ๋”

S3 ๋“œ๋ผ์ด๋ฒ„๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ S3 URL์„ ์ƒ์„ฑํ•˜์ง€๋งŒ, urlBuilder๋กœ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ๋™์ž‘ (urlBuilder ์—†์Œ)

storage: {
  default: 's3',
  drivers: {
    s3: drivers.s3({
      credentials: { /* ... */ },
      region: 'ap-northeast-2',
      bucket: 'my-bucket',
      // urlBuilder ์—†์Œ
    }),
  }
}
๊ฒฐ๊ณผ:
// key: 'uploads/avatar.png'
// URL: https://my-bucket.s3.ap-northeast-2.amazonaws.com/uploads/avatar.png

CloudFront CDN

CDN์„ ํ†ตํ•ด ํŒŒ์ผ์„ ์„œ๋น™ํ•˜๋ ค๋ฉด:
storage: {
  default: 's3',
  drivers: {
    s3: drivers.s3({
      credentials: { /* ... */ },
      region: 'ap-northeast-2',
      bucket: 'my-bucket',
      urlBuilder: {
        generateURL: (key) => 
          `https://d111111abcdef8.cloudfront.net/${key}`,
      }
    }),
  }
}
CLOUDFRONT_DOMAIN=d111111abcdef8.cloudfront.net
์žฅ์ :
  • ๋น ๋ฅธ ์ „์†ก ์†๋„ (์—ฃ์ง€ ๋กœ์ผ€์ด์…˜)
  • ๋Œ€์—ญํญ ๋น„์šฉ ์ ˆ๊ฐ
  • ์บ์‹ฑ

์ปค์Šคํ…€ ๋„๋ฉ”์ธ

storage: {
  default: 's3',
  drivers: {
    s3: drivers.s3({
      credentials: { /* ... */ },
      region: 'ap-northeast-2',
      bucket: 'my-bucket',
      urlBuilder: {
        generateURL: (key) => 
          `https://cdn.example.com/${key}`,
      }
    }),
  }
}
์„ค์ • ๋ฐฉ๋ฒ•:
  1. CloudFront ๋ฐฐํฌ ์ƒ์„ฑ
  2. Origin: S3 ๋ฒ„ํ‚ท
  3. CNAME: cdn.example.com
  4. DNS: cdn.example.com โ†’ CloudFront

๋™์  URL ์ƒ์„ฑ

URL ๋นŒ๋”์—์„œ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋‚˜ ๋กœ์ง์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ™˜๊ฒฝ๋ณ„ URL

const baseUrl = process.env.NODE_ENV === 'production'
  ? 'https://cdn.example.com'
  : 'http://localhost:3000/uploads';

storage: {
  default: 'fs',
  drivers: {
    fs: drivers.fs({
      location: './uploads',
      urlBuilder: {
        generateURL: (key) => `${baseUrl}/${key}`,
      }
    }),
  }
}
๊ฒฐ๊ณผ:
// ๊ฐœ๋ฐœ: http://localhost:3000/uploads/avatar.png
// ํ”„๋กœ๋•์…˜: https://cdn.example.com/avatar.png

๊ฒฝ๋กœ ๋ณ€ํ™˜

storage: {
  default: 'fs',
  drivers: {
    fs: drivers.fs({
      location: './uploads',
      urlBuilder: {
        generateURL: (key) => {
          // ๊ณต๋ฐฑ์„ ํ•˜์ดํ”ˆ์œผ๋กœ ๋ณ€ํ™˜
          const safeKey = key.replace(/\s+/g, '-');
          return `/uploads/${safeKey}`;
        }
      }
    }),
  }
}

๋ฒ„์ „ ๊ด€๋ฆฌ

const version = 'v1';

storage: {
  default: 'fs',
  drivers: {
    fs: drivers.fs({
      location: './uploads',
      urlBuilder: {
        generateURL: (key) => `/uploads/${version}/${key}`,
      }
    }),
  }
}
๊ฒฐ๊ณผ:
// URL: /uploads/v1/avatars/user-1.png

Signed URL (S3)

S3๋Š” ์ž„์‹œ ์ ‘๊ทผ URL์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

getSignedUrl()

import { Sonamu } from "sonamu";

@api({ httpMethod: 'GET' })
async getPrivateFileUrl(key: string) {
  const disk = Sonamu.storage.use('s3');
  
  // Signed URL ์ƒ์„ฑ (1์‹œ๊ฐ„ ์œ ํšจ)
  const signedUrl = await disk.getSignedUrl(key, {
    expiresIn: '1h'
  });
  
  return { url: signedUrl };
}
๊ฒฐ๊ณผ:
https://bucket.s3.amazonaws.com/private/doc.pdf?X-Amz-Algorithm=...&X-Amz-Expires=3600
์šฉ๋„:
  • Private ํŒŒ์ผ ์ž„์‹œ ๊ณต์œ 
  • ๋ณด์•ˆ์ด ํ•„์š”ํ•œ ๋‹ค์šด๋กœ๋“œ
  • ์‹œ๊ฐ„ ์ œํ•œ ์•ก์„ธ์Šค

UploadedFile์˜ signedUrl

@upload({ mode: 'single' })
@api({ httpMethod: 'POST' })
async uploadPrivateFile() {
  const { file } = Sonamu.getUploadContext();
  
  if (!file) {
    throw new Error('ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค');
  }
  
  // ์ €์žฅ
  await file.saveToDisk('private/doc.pdf', 's3');
  
  return {
    url: file.url,           // Unsigned URL
    signedUrl: file.signedUrl  // Signed URL
  };
}

์‹ค์ „ ์˜ˆ์ œ

1. CDN + ๋ฒ„์ „ ๊ด€๋ฆฌ

const cdnDomain = process.env.CDN_DOMAIN;
const version = process.env.ASSET_VERSION || 'v1';

export const config: SonamuConfig = {
  server: {
    storage: {
      default: 's3',
      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-bucket',
          urlBuilder: {
            generateURL: (key) => 
              `https://${cdnDomain}/${version}/${key}`,
          }
        }),
      }
    }
  }
};
CDN_DOMAIN=d111111abcdef8.cloudfront.net
ASSET_VERSION=v2
๊ฒฐ๊ณผ:
// key: 'images/logo.png'
// URL: https://d111111abcdef8.cloudfront.net/v2/images/logo.png

2. ๋‹ค์ค‘ CDN

export const config: SonamuConfig = {
  server: {
    storage: {
      default: 's3',
      drivers: {
        // ์ด๋ฏธ์ง€์šฉ CDN
        images: drivers.s3({
          credentials: { /* ... */ },
          region: 'ap-northeast-2',
          bucket: 'images-bucket',
          urlBuilder: {
            generateURL: (key) => 
              `https://images.cdn.example.com/${key}`,
          }
        }),
        
        // ๋น„๋””์˜ค์šฉ CDN
        videos: drivers.s3({
          credentials: { /* ... */ },
          region: 'ap-northeast-2',
          bucket: 'videos-bucket',
          urlBuilder: {
            generateURL: (key) => 
              `https://videos.cdn.example.com/${key}`,
          }
        }),
      }
    }
  }
};
์‚ฌ์šฉ:
// ์ด๋ฏธ์ง€
await file.saveToDisk('photo.jpg', 'images');
// URL: https://images.cdn.example.com/photo.jpg

// ๋น„๋””์˜ค
await file.saveToDisk('video.mp4', 'videos');
// URL: https://videos.cdn.example.com/video.mp4

3. ํ•ด์‹œ ๊ธฐ๋ฐ˜ URL

import { createHash } from "crypto";

storage: {
  default: 's3',
  drivers: {
    s3: drivers.s3({
      credentials: { /* ... */ },
      region: 'ap-northeast-2',
      bucket: 'my-bucket',
      urlBuilder: {
        generateURL: (key) => {
          // key๋กœ๋ถ€ํ„ฐ ํ•ด์‹œ ์ƒ์„ฑ (์บ์‹œ ๋ฒ„์ŠคํŒ…)
          const hash = createHash('md5')
            .update(key)
            .digest('hex')
            .substring(0, 8);
          
          return `https://cdn.example.com/${key}?v=${hash}`;
        }
      }
    }),
  }
}
๊ฒฐ๊ณผ:
// key: 'images/logo.png'
// URL: https://cdn.example.com/images/logo.png?v=5d41402a

4. ์กฐ๊ฑด๋ถ€ URL

storage: {
  default: 's3',
  drivers: {
    s3: drivers.s3({
      credentials: { /* ... */ },
      region: 'ap-northeast-2',
      bucket: 'my-bucket',
      urlBuilder: {
        generateURL: (key) => {
          // ์ด๋ฏธ์ง€๋Š” CDN, ๋‚˜๋จธ์ง€๋Š” S3 ์ง์ ‘
          if (key.match(/\.(jpg|jpeg|png|gif|webp)$/i)) {
            return `https://cdn.example.com/${key}`;
          }
          return `https://my-bucket.s3.amazonaws.com/${key}`;
        }
      }
    }),
  }
}

5. ๋ฆฌ์‚ฌ์ด์ง• ์„œ๋น„์Šค

storage: {
  default: 's3',
  drivers: {
    s3: drivers.s3({
      credentials: { /* ... */ },
      region: 'ap-northeast-2',
      bucket: 'my-bucket',
      urlBuilder: {
        generateURL: (key) => {
          // ์ด๋ฏธ์ง€ ๋ฆฌ์‚ฌ์ด์ง• ์„œ๋น„์Šค ํ†ต๊ณผ
          if (key.match(/\.(jpg|jpeg|png)$/i)) {
            return `https://img.example.com/resize/800x600/${key}`;
          }
          return `https://cdn.example.com/${key}`;
        }
      }
    }),
  }
}

URL ํƒ€์ž…

Sonamu Storage๋Š” ๋‘ ๊ฐ€์ง€ URL ํƒ€์ž…์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

Unsigned URL

์ผ๋ฐ˜ ๊ณต๊ฐœ URL
const url = await disk.getUrl('uploads/avatar.png');
// => https://bucket.s3.amazonaws.com/uploads/avatar.png
ํŠน์ง•:
  • ์˜๊ตฌ์ 
  • ๋งŒ๋ฃŒ ์—†์Œ
  • Public ํŒŒ์ผ์šฉ

Signed URL (S3)

์„œ๋ช…๋œ ์ž„์‹œ URL
const signedUrl = await disk.getSignedUrl('private/doc.pdf', {
  expiresIn: '1h'  // 1์‹œ๊ฐ„ ํ›„ ๋งŒ๋ฃŒ
});
// => https://bucket.s3.amazonaws.com/private/doc.pdf?X-Amz-Algorithm=...
ํŠน์ง•:
  • ์ž„์‹œ์  (๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์„ค์ •)
  • ์„œ๋ช…์œผ๋กœ ๋ณดํ˜ธ๋จ
  • Private ํŒŒ์ผ์šฉ
๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์˜ต์…˜:
{ expiresIn: '1h' }    // 1์‹œ๊ฐ„
{ expiresIn: '30m' }   // 30๋ถ„
{ expiresIn: '1d' }    // 1์ผ
{ expiresIn: '7d' }    // 7์ผ

์ฃผ์˜์‚ฌํ•ญ

URL ๋นŒ๋” ์‚ฌ์šฉ ์‹œ ์ฃผ์˜์‚ฌํ•ญ:
  1. fs๋Š” urlBuilder ํ•„์ˆ˜: fs ๋“œ๋ผ์ด๋ฒ„๋Š” ๋ฐ˜๋“œ์‹œ urlBuilder ์„ค์ •
    // โŒ urlBuilder ์—†์Œ
    fs: drivers.fs({
      location: './uploads',
    })
    
    // โœ… urlBuilder ์„ค์ •
    fs: drivers.fs({
      location: './uploads',
      urlBuilder: {
        generateURL: (key) => `/uploads/${key}`,
      }
    })
    
  2. ์ •์  ํŒŒ์ผ ์„œ๋น™: fs ์‚ฌ์šฉ ์‹œ static ํ”Œ๋Ÿฌ๊ทธ์ธ ํ•„์š”
    plugins: {
      static: {
        root: path.join(process.cwd(), 'uploads'),
        prefix: '/uploads/',
      }
    }
    
  3. HTTPS ์‚ฌ์šฉ: ํ”„๋กœ๋•์…˜์—์„œ๋Š” HTTPS URL ์‚ฌ์šฉ
    // โŒ HTTP
    generateURL: (key) => `http://cdn.example.com/${key}`
    
    // โœ… HTTPS
    generateURL: (key) => `https://cdn.example.com/${key}`
    
  4. ๊ฒฝ๋กœ ์ธ์ฝ”๋”ฉ: ํŠน์ˆ˜ ๋ฌธ์ž ์ฒ˜๋ฆฌ
    // โœ… ์•ˆ์ „ํ•œ ์ฒ˜๋ฆฌ
    generateURL: (key) => {
      const encoded = encodeURIComponent(key);
      return `/uploads/${encoded}`;
    }
    
  5. ์บ์‹œ ๋ฌดํšจํ™”: URL ๋ณ€๊ฒฝ ์‹œ CDN ์บ์‹œ ๊ณ ๋ ค
    // ๋ฒ„์ „ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€๋กœ ์บ์‹œ ๋ฌดํšจํ™”
    generateURL: (key) => 
      `/uploads/${key}?v=${Date.now()}`
    

CloudFront ์„ค์ •

CloudFront๋ฅผ URL ๋นŒ๋”์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

1. CloudFront ๋ฐฐํฌ ์ƒ์„ฑ

  1. AWS Console โ†’ CloudFront
  2. โ€œCreate Distributionโ€ ํด๋ฆญ
  3. Origin:
    • Origin Domain: my-bucket.s3.ap-northeast-2.amazonaws.com
    • Origin Path: (๋น„์›Œ๋‘ )
  4. Default Cache Behavior:
    • Viewer Protocol Policy: Redirect HTTP to HTTPS
    • Allowed HTTP Methods: GET, HEAD, OPTIONS
  5. Create Distribution

2. Domain ํ™•์ธ

๋ฐฐํฌ ์ƒ์„ฑ ํ›„ Domain Name ํ™•์ธ:
d111111abcdef8.cloudfront.net

3. URL ๋นŒ๋” ์„ค์ •

storage: {
  default: 's3',
  drivers: {
    s3: drivers.s3({
      credentials: { /* ... */ },
      region: 'ap-northeast-2',
      bucket: 'my-bucket',
      urlBuilder: {
        generateURL: (key) => 
          `https://d111111abcdef8.cloudfront.net/${key}`,
      }
    }),
  }
}

4. ์ปค์Šคํ…€ ๋„๋ฉ”์ธ (์„ ํƒ)

  1. Route 53์—์„œ CNAME ๋ ˆ์ฝ”๋“œ ์ƒ์„ฑ:
    cdn.example.com โ†’ d111111abcdef8.cloudfront.net
    
  2. CloudFront ๋ฐฐํฌ ์„ค์ •:
    • Alternate Domain Names (CNAMEs): cdn.example.com
    • SSL Certificate: ์ธ์ฆ์„œ ์ถ”๊ฐ€
  3. URL ๋นŒ๋” ์ˆ˜์ •:
    urlBuilder: {
      generateURL: (key) => 
        `https://cdn.example.com/${key}`,
    }
    

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