[AWS] S3와 이미지 파일 다루기

15437 단어 awsaws

S3를 기존에 사용해왔기에 AWS 자원에 연결하고 관리하는 방법보다는 이미지 파일을 업로드 및 다운로드하면서 발생한 여러 문제에 대한 해결방법을 목적에 둔 글입니다 🙂

S3에 파일 업로드하기

아래 코드는 클라이언트에서 전송한 파일을 읽기가 가능한 스트림으로 만들어 S3에 업로드하는 간단한 방식입니다.

 async uploadImage(file: any): Promise<string> {
    const fileKey = `...`;

    const s3Params = {
      Bucket: process.env.BUCKET_NAME || AwsInfo.DEFAULT_BUCKET_NAME,
      Key: fileKey,
      Body: file.createReadStream(),
      ContentType: file.mimeType,
      ContentEncoding: file.encoding,
    };

    return this.putS3(s3Params);
  }
  
  
/**
 * AWS S3에 put 하고 해당 객체의 URL을 반환하는 메소드 입니다.
 *
 * @param putObjectRequest
 * @private
 */ 
async putS3(putObjectRequest: awsS3.Types.PutObjectRequest): Promise<string> {
    return this.s3
      .upload(putObjectRequest)
      .promise()
      .then((data) => data.Location);
}

테스트할 때는 전혀 문제가 없는 것처럼 보였지만,
실제 클라이언트에서 파일을 전송해보니 S3에 업로드된 파일의 Content-Typeapplication/octet-stream으로 설정되어 파일이 출력되지 않는 문제를 발견했습니다.

서버에서는 GraphQL 실행기를 통해 파일이 전송되어 자동으로 이미지 타입이 설정되어 들어왔는데, 클라이언트에서는 그렇지 않아서 MIME-TYPE을 출력해보니 image/png 인것도 있고 application/octet-stream인 것도 있었습니다.

이렇게 파일의 MIME-TYPE(마임 타입으로 읽는다고 합니다🙂)이 제대로 설정되지 않았기에, 서버에서 파일의 확장자 명을 통해 타입을 지정해 업로드 하도록 로직을 수정했습니다.

export const getContentType = (fileName: string): string => {
  const extensions = {...};

  const contentType = extensions[path.extname(fileName).replace('.', '')]; // ⬅️ 
  if (!contentType) {
    // throw error ...
    // 사전에 협의된 파일 형식이 아닌 경우, 에러를 반환합니다.
  }

  return contentType;
};


async uploadImage(file: any): Promise<string> {
	const fileKey = `...`;

    const s3Params = {
      Bucket: process.env.BUCKET_NAME || AwsInfo.DEFAULT_BUCKET_NAME,
      Key: fileKey,
      Body: file.createReadStream(),
      ContentType: this.getContentType(file.filename),	// ⬅️ 바뀐 부분
      ContentEncoding: file.encoding,
    };

    return this.putS3(s3Params);
}

웹에서 파일을 주고받는 작업을 할 때, 항상 문제가 많이 발생하는 것 같습니다.
엑셀 파일을 처리하는 방식은 이미지와 또 다른 접근이 필요했고, 클라이언트까지 잘 알고있어야하는 등... 이런 문제들을 정리한 글을 조만간 업로드해보겠습니다 💪

S3 파일에 접근하기

객체 URL 활용하기

S3에 업로드된 객체의 속성을 살펴보면 아래와 같습니다.
객체 URL을 통해 바로 클라이언트에서 인라인하여 화면에 출력할 수 있습니다.
하지만 대부분의 S3 버킷이 퍼블릭 엑세스에 제한되어 있기에 정책을 설정할 필요가 있습니다.

권한이 없는 경우의 응답은 아래와 같습니다.

<Error>
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
<RequestId>...</RequestId>
<HostId>...</HostId>
</Error>

저는 운영환경(local, dev, staging, prod) 별로 버킷을 별도로 관리하고 있기에 조금씩 다른 버킷 정책을 적용할 것 입니다.

로컬, 개발, 스테이징 환경의 경우 개발 및 테스트 용도로 활용되기에
접근 권한을 모두 허용하도록 설정했습니다.

{
	"Sid": "Statement1",
	"Effect": "Allow",
	"Principal": "*",
	"Action": "s3:GetObject", // ⬅️
	"Resource": [
		"arn:aws:s3:::.../local/*",
		"arn:aws:s3:::.../dev/*",
		"arn:aws:s3:::.../staging/*"
	]
}

운영의 경우, 보안이 필요한 민감한 데이터가 존재하기에
HTTP 요청 헤더를 분석해서 특정 도메인에서 요청한 경우에만 접근할 수 있도록 설정했습니다.

{
	"Sid": "Statement1",
	"Effect": "Allow",
	"Principal": "*",
	"Action": "s3:GetObject", 
	"Resource": "arn:aws:s3:::.../prod/*",
    "Condition": {
		"StringLike": {
			"aws:Referer": [운영 도메인 주소]	// ⬅️
		}
	}
}

referer란, 특정 요청을 보낸 경우 헤더에 포함된 속성 중 하나입니다.

예를 들어 아래는 네이버에서 특정 이미지를 요청하는 헤더 중 일부입니다.
referer를 보면 네이버 도메인(https://www.naver.com/) 값을 표현하고 있습니다. 즉, 네이버 도메인에서 이미지를 불러오는 요청을 한 것입니다.

이를 바탕으로 운영 도메인에서 S3의 운영 버킷에 있는 객체에 접근할 수 있도록 정책을 설정한 것입니다.

presignedURL 활용하기

위 방식 이전에 사용했던 해결방안으로 presignedURL이 있습니다.

presignedURL이란, 말 그대로 미리 서명된 URL 입니다. 권한이 있는 사용자에 의해 접근가능한 URL을 생성하여 공유하는 방식입니다.

async getPresignedUrl(fileKey: string): Promise<string> {
    const s3Params = {
      Bucket: process.env.BUCKET_NAME || AwsInfo.DEFAULT_BUCKET_NAME,
      Key: fileKey,
  	  // Expires: 120  
    };

    return this.s3.getSignedUrl('getObject', s3Params);
}

this.s3.getSignedUrl('getObject', s3Params) 이 코드로 퍼블릭 엑세스가 아닌 버킷에 접근할 수 있는 이유는,
현재 제 계정이 접근 권한이 있는 사용자임으로 서명된 URL을 생성할 수 있는 것입니다.
presignedURL의 세션이 만료되는 기본 값은 15분이며 Expires 옵션으로 조절할 수 있습니다.

이 방식을 활용하지 않게 된 이유는 DB 저장방식에 있습니다.
이미지 파일에 대한 정보를 DB에 저장하고 있는데, presignedURL을 사용할 경우 유효시간이 있기에 값에 변동이 있어 주기적으로 갱신해주는 데 비용이 들게 됩니다.
그래서 파일을 업로드함과 동시에 객체 URL을 DB에 저장하여 해당 정보를 응답할 때, 파일 정보를 함께 보내줄 수 있도록 구현했습니다.

좋은 웹페이지 즐겨찾기