λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°
πŸ‘¨‍πŸ’» web.dev/fe

AWS SDK 둜 s3 파일 μ‚¬μ΄μ¦ˆ μ‘°νšŒν•˜κΈ°

by HandHand 2022. 11. 26.

 

πŸ“Œ μ—…λ‘œλ“œ ν•  λ°”μš°μ²˜μ— 크기 μ œν•œμ„ μΆ”κ°€ν•  수 μžˆμ„κΉŒμš”?

νŠΈλ¦¬ν”Œ TNA νŒŒνŠΈλ„ˆμ›Ή μ—λŠ” νŒŒνŠΈλ„ˆκ°€ κ³ κ°μ—κ²Œ μ˜ˆμ•½ λ°”μš°μ²˜λ₯Ό μ „μ†‘ν•˜λŠ” κΈ°λŠ₯이 μžˆμŠ΅λ‹ˆλ‹€.

κΈ°μ‘΄μ—λŠ” 파일 μ‚¬μ΄μ¦ˆμ˜ μ œν•œ 없이 μ „μ†‘ν•˜λ„λ‘ ν–ˆλŠ”λ°, μ‚¬μ΄μ¦ˆκ°€ 컀질 경우 μ΄μŠˆκ°€ μƒκ²¨μ„œ

μ΄λ²ˆμ— λ¦¬λ‰΄μ–Όν•˜λ©΄μ„œ μ„œλ²„μ—μ„œ λ°”μš°μ²˜ μ‚¬μ΄μ¦ˆκ°€ 클 경우 이λ₯Ό λΆ„λ¦¬ν•΄μ„œ 전솑 μ²˜λ¦¬ν•˜λŠ” 것 이외에

ν”„λ‘ νŠΈμ—”λ“œμ—μ„œλ„ κ°œλ³„ λ°”μš°μ²˜μ— μš©λŸ‰ μ œν•œ(15MB)을 μΆ”κ°€ν•˜κΈ°λ‘œ ν–ˆμŠ΅λ‹ˆλ‹€.

 

이λ₯Ό μœ„ν•΄ λΈŒλΌμš°μ €μ—μ„œ μ˜¬λ¦¬λŠ” 파일 μ‚¬μ΄μ¦ˆ 체크 이외에도

S3 에 이미 μ—…λ‘œλ“œ 된 λ°”μš°μ²˜ 파일 μ‚¬μ΄μ¦ˆλ₯Ό μ‘°νšŒν•΄μ„œ

각 λ°”μš°μ²˜ ν•­λͺ©μ— μš©λŸ‰μ •λ³΄λ₯Ό ν•¨κ»˜ λ…ΈμΆœν•΄μ€˜μ•Ό ν–ˆμŠ΅λ‹ˆλ‹€.

κ·Έλž˜μ•Ό 고객이 μ–΄λ–€ νŒŒμΌμ— λ¬Έμ œκ°€ μžˆλŠ”μ§€ ν•œλ²ˆμ— 확인이 κ°€λŠ₯ν•  ν…Œλ‹ˆκΉŒμš”.

 

πŸ“Œ S3 파일 μ‚¬μ΄μ¦ˆ (HeadObject)

λΈŒλΌμš°μ €μ—μ„œ μ—…λ‘œλ“œν•œ 파일 μ‚¬μ΄μ¦ˆλ₯Ό μ•Œμ•„μ˜€λŠ” 것은 별닀λ₯Έ μ²˜λ¦¬κ°€ ν•„μš”μ—†μ§€λ§Œ,

이미 S3 에 μ—…λ‘œλ“œν•œ νŒŒμΌμ„ μ•Œμ•„μ˜€κΈ° μœ„ν•΄μ„œλŠ” S3 의 HeadObject 접근이 ν•„μš”ν•©λ‹ˆλ‹€.

HTTP μš”μ²­ μ€‘μ—μ„œ HEAD λŠ” request λŒ€μƒμ˜ content 메타 정보λ₯Ό 얻을 수 μžˆλŠ” μš”μ²­μž…λ‹ˆλ‹€.

 

S3 μ—μ„œλ„ μ ‘κ·Όν•  λŒ€μƒ 파일의 HEAD 정보λ₯Ό HeadObject λ₯Ό 톡해 μ œκ³΅ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

νŒŒνŠΈλ„ˆμ›Ήμ€ Next.js 의 API Routes λ₯Ό μ΄μš©ν•΄μ„œ API ν”„λ‘μ‹œλ₯Ό κ΅¬μ„±ν•˜κ³  있기 λ•Œλ¬Έμ—

여기에 ν•Έλ“€λŸ¬λ₯Ό μΆ”κ°€ν•΄μ£ΌλŠ” λ°©ν–₯으둜 μ§„ν–‰ν–ˆμŠ΅λ‹ˆλ‹€.

 

import { NextApiRequest, NextApiResponse } from 'next'
import { S3 } from 'aws-sdk' // βœ… aws-sdk v2 λ₯Ό μ‚¬μš©ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  const s3 = new S3({
    ... access key, id λ“±λ“± μ„€μ •κ°’λ“€
  })

  const { fileKey } = req.body

  try {
    const data = await s3
      .headObject({
        Bucket: S3_BUCKET,
        Key: fileKey,
      })
      .promise()

    res.status(200).send(data)
  } catch (error) {
    // ...
  }
}

 

νŒŒμΌμ„ μš”μ²­ν•  S3 객체λ₯Ό μƒμ„±ν•œ 뒀에, headObject μš”μ²­μœΌλ‘œ

response body κ°€ λΉ„μ–΄μžˆλŠ” 메타 정보λ₯Ό λ‹΄κ³ μžˆλŠ” 응닡을 얻을 수 μžˆμŠ΅λ‹ˆλ‹€.

 

HTTP/1.1 200

Content-Length: ContentLength
ETag: ETag
Cache-Control: CacheControl
Content-Disposition: ContentDisposition
Content-Encoding: ContentEncoding
Content-Language: ContentLanguage
Content-Type: ContentType
Expires: Expires

// ...

 

λŒ€λž΅ μœ„μ™€ 같은 ν˜•μ‹μ„ κ°€μ§€λŠ” 응닡을 보내주며 μ—¬κΈ°μ„œ Content-Length λ₯Ό 톡해

파일의 크기λ₯Ό Byte λ‹¨μœ„λ‘œ μ½μ–΄μ˜¬ 수 μžˆμŠ΅λ‹ˆλ‹€.

HeadObject μš”μ²­μ€ μš”μ²­μ— λŒ€ν•œ 400, 403, 404 3κ°€μ§€μ˜ μ—λŸ¬ μΌ€μ΄μŠ€κ°€ μ‘΄μž¬ν•©λ‹ˆλ‹€.

 

이λ₯Ό μœ„ν•΄ aws-sdk v3 λΆ€ν„°λŠ” Error Class λ₯Ό μ •μ˜ν•΄μ„œ μ œκ³΅ν•΄μ£Όμ§€λ§Œ,

v2 μ—μ„œλŠ” AWSError λΌλŠ” μΈν„°νŽ˜μ΄μŠ€λ§Œ κ΅¬ν˜„λ˜μ–΄μžˆκ³ 

μ‹€μ œ μ—λŸ¬ ν΄λž˜μŠ€λŠ” μ œκ³΅ν•΄μ£Όμ§€ μ•ŠκΈ° λ•Œλ¬Έμ— error.name μ†μ„±μœΌλ‘œ μ—λŸ¬ νƒ€μž…μ„ κ΅¬λΆ„ν•˜λ„λ‘ ν–ˆμŠ΅λ‹ˆλ‹€.

 

try { /** headObject μš”μ²­ */ }
catch (error) {
  let errorCode = 'unknown'
  if (error instanceof Error) {
    errorCode = error.name
  }

  if (errorCode === 'BadRequest') {
    res.status(400).send(error)
  } else if (errorCode === 'Forbidden') {
    res.status(403).send(error)
  } else if (errorCode === 'NotFound') {
    res.status(404).send(error)
  } else {
    res.status(500).send('Error From AWS SDK')
  }
}

 

πŸ“Œ λΈŒλΌμš°μ € 파일 μ‚¬μ΄μ¦ˆ (file.size)

λΈŒλΌμš°μ €μ—μ„œ input νƒœκ·Έλ‘œ μ—…λ‘œλ“œλœ 파일의 μ‚¬μ΄μ¦ˆλŠ”

File 객체의 size μ†μ„±μœΌλ‘œ 접근이 κ°€λŠ₯ν•©λ‹ˆλ‹€.

여기도 λ§ˆμ°¬κ°€μ§€λ‘œ Byte λ‹¨μœ„λ‘œ λ°˜ν™˜ν•΄μ€λ‹ˆλ‹€.

 

const handleChange = (files: FileList) => {
  const sizes = [...files].map((file) => file.size) // βœ… File.size μ†μ„±μœΌλ‘œ μ ‘κ·Ό

  // ...
}

<input type="file" onChange={handleChange} />

 

πŸ“Œ Byte ν˜•μ‹ λ³€ν™˜

μ•„λ§ˆλ„ Byte λ‹¨μœ„λ‘œ ν‘œκΈ°λœ 크기λ₯Ό λ”± 보고 음, 이 파일이 xxMB μ—¬μ„œ λ¬Έμ œκ°€ μƒκΈ΄κ±°κ΅¬λ‚˜? 라며

ν•œλ²ˆμ— 식별할 수 μžˆλŠ” μ‚¬λžŒμ€ 없을 κ²λ‹ˆλ‹€.

이λ₯Ό μœ„ν•΄ μ‚¬λžŒμ΄ μ΄ν•΄ν•˜κΈ° μ‰¬μš΄ λ‹¨μœ„λ‘œ λ³€ν™˜μ„ ν•΄μ„œ λ³΄μ—¬μ€˜μ•Όν•˜λŠ”λ°,

여기에 μ‚¬μš©ν•  Byte λ‹¨μœ„ ν¬λ§·νŒ… ν•¨μˆ˜λ₯Ό κ΅¬ν˜„ν•΄μ€λ‹ˆλ‹€.

 

πŸ’‘ Note
μ‹€μ œ μ½”λ“œμ—μ„œλŠ” symbol λ…ΈμΆœ μ—¬λΆ€, μ†Œμˆ˜μ  자리수 등을 ν•¨μˆ˜μ˜ 인자둜 λ°›μ•„μ„œ μ²˜λ¦¬ν–ˆμ§€λ§Œ,
μ—¬κΈ°μ„œλŠ” μ˜ˆμ‹œλ₯Ό μœ„ν•΄ κ°„λž΅ν™”ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

 

function convertByte(value: number) {
  const kb = 1024
  const fraction = 2
  const notations = ['Byte', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']

  const exponent = Math.floor(Math.log(value) / Math.log(kb))
  const size = parseFloat((value / Math.pow(kb, exponent)).toFixed(frac))
  const notation = notations[exponent]

  return `${size} ${notation}`
}

 

μ μ ˆν•œ ν…ŒμŠ€νŠΈ μ½”λ“œλ„ μΆ”κ°€ν•΄μ„œ μ œλŒ€λ‘œ λ³€ν™˜μ΄ λ˜λŠ”μ§€ ν™•μΈν•΄λ΄…λ‹ˆλ‹€.

 

describe('Formatter > byte', () => {
  it('should change 1024byte to 1KB', () => {
    expect(Formatter.byte({ value: 1024 })).toBe('1 KB')
  })

  it('should change 10000byte to 9.77KB', () => {
    expect(Formatter.byte({ value: 10000 })).toBe('9.77 KB')
  })

  it('should change 100000000byte to 95.37MB', () => {
    expect(Formatter.byte({ value: 100000000 })).toBe('95.37 MB')
  })

  it('should change 100000000000byte to 93.13GB', () => {
    expect(Formatter.byte({ value: 100000000000 })).toBe('93.13 GB')
  })
})

 

πŸ’‘ 1K = 1000? 1024?

μ΄λ²ˆμ— μ•Œκ²Œλœ 사싀인데, 2진법 μ»΄ν“¨νŒ… μ‹œμŠ€ν…œμ—μ„œλŠ” 1MB = 1024Byte 이기 λ•Œλ¬Έμ—

10진법을 μ‚¬μš©ν•˜λŠ” ν™˜κ²½μ—μ„œ 보편적으둜 μ‚¬μš©λ˜λŠ” 1K = 1000 에 차이가 μžˆλ‹€κ³  ν•©λ‹ˆλ‹€.

λ•Œλ¬Έμ— 보톡 κΈ°κΈ° μŠ€νŽ™μ— λͺ…μ‹œλœ ν•˜λ“œμ›¨μ–΄ μš©λŸ‰λ³΄λ‹€ μ‹€μ œ μ‚¬μ΄μ¦ˆλŠ” μž‘κ²Œ ν‘œκΈ°λœλ‹€κ³  ν•˜λ„€μš”.

 

πŸ“Œ κ²€μ¦ 둜직 μΆ”κ°€ν•˜κΈ°

이제 μœ„μ—μ„œ κ΅¬ν˜„ν•œ ν•¨μˆ˜λ“€μ„ μ΄μš©ν•΄μ„œ λ°”μš°μ²˜ νŒŒμΌμ„ μ—…λ‘œλ“œ ν•˜κΈ° 전에

파일 μ‚¬μ΄μ¦ˆ 체크λ₯Ό 해주도둝 검증 과정을 μΆ”κ°€ν•©λ‹ˆλ‹€.

 

// ν˜„μž¬ μ»΄ν¬λ„ŒνŠΈμ˜ state에 파일 λͺ©λ‘μ΄ File[] ν˜•νƒœλ‘œ κ΄€λ¦¬λ˜κ³  μžˆλ‹€λŠ” κ°€μ •

const handleVoucherUpload = (files: File[]) => {
  const limit = 1024 * 1024 * 15 // 15MB
  const exceed = stagedFiles.some((file) => file.size > limit)
  if (exceed) {
    throw new Error('FileSizeLimitExceed')
  }
}

 

ν•΄λ‹Ή ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•˜λŠ” μͺ½μ—μ„œ μ—λŸ¬ νƒ€μž…μ„ μ²΄ν¬ν•΄μ„œ FileSizeLimitExceed 일 κ²½μš°μ—λŠ”

μ•ˆλ‚΄ λ©”μ‹œμ§€λ₯Ό 담은 ν† μŠ€νŠΈ λ©”μ‹œμ§€λ₯Ό λ„μ›Œμ€λ‹ˆλ‹€.

 

잘 λ™μž‘ν•˜λŠ”κ΅°μš” πŸ˜€

 

πŸ“Œ μ°Έκ³ μžλ£Œ

HeadObject

where to find all the exception classes in AWS-SDK for dynamodb in NodeJS typescript?

λ°˜μ‘ν˜•

πŸ’¬ λŒ“κΈ€