티스토리 뷰

반응형

 

 

오늘은 프로젝트에서 면접 영상을 찍은 후 업로드하는 과정에서 클라이언트, 서버, S3가 어떻게 영상을 주고받았는지에 대해 써보도록 하겠다. 

 

 


 

Pre-signed URL 방식을 택한 이유 

클라이언트에서 S3에 파일을 업로드 하는 방법으로는 크게 2가지가 있다. 

 

1. AWS SDK를 이용해 직접 업로드

2. API 서버에 파일을 전달하고 API 서버에서 S3에 업로드

 

1 번의 방법은 서버를 거치지 않지만, AWS SDK를 써서 S3이용이 가능해야 하기 때문에 브라우저에서 AWS SDK를 사용하는 시점에는 결국  AWS Access Key와 Secret Key 정보를 알고 있어야 한다. 이는 Key 정보가 브라우저에서 노출될 수 있고 악성 해커가 이를 이용하면 S3 리소스를 탈취해 갈 수 있는 위험성이 있다. 

 

2번의 방법은 API서버에서 파일을 업로드하기 때문에 AWS Access Key와 Secret Key 정보를 서버가 가지고 있어 Key 정보가 노출되는 위험은 없다. 이때 파일을 업로드하면 파일 전달 흐름이 브라우저 -> 서버 -> S3 순으로 되게 되는데 이는 저장하지도 않을 파일들이 서버를 통해가면서 불필요한 서버의 리소스를 사용하게 된다. 또한 과도한 업로드 작업이 생기면 서버에 과부하가 걸리게 되고 서버를 한 번 거쳐가는 지연시간이 생기게 된다. 

 

결국 위 두 가지 방법 모두 가능하지만 단점이 존재한다. 이러한 단점을 보완하기 위해 AWS에서는 S3에 객체를 업로드하고 다운로드하기 위한 방법으로 Pre-signed URL 기능을 지원하고 있고 이 방식이 우리가 영상을 S3에 전달하는 방식이 되겠다. 

 

 


Pre-signed URL Process

 

하나의 파일을 S3에 업로드하기 위한 Pre-signed URL 과정은 다음과 같다. 

 

1.  클라이언트에서 서버에 pre-signed URL를 받기 위한 API 호출 (POST 요청)
2. 서버에서 AWS S3에 pre-signed URL요청
3. AWS에서 pre-signed URL을 서버에 반환
4. 서버는 반환받은 pre-signed URL를 클라이언트에 전달
5. 클라이언트에서 AWS pre-signed URL로 이미지 upload (S3에 직접 업로드) (PUT 요청)
6. 서버에게 해당 요청이 종료 되었음을 알림

 

서버를 아예 통하지 않는 방식은 아니지만, 파일 업로드를 처리하는 것에서 단순 문자열을 주고받는 식으로 바뀌었기 때문에 프로세스가 훨씬 가벼워지고 브라우저에서 Key를 직접 만지지도 않아 보안성이 우수해졌다.

 

 


 

프로젝트에 적용한 코드

 

현재 우리 프로젝트에서는 S3 bucket에 영상과 영상의 썸네일(이미지) 두가지를 업로드하기 때문에 

한 번의 post요청에서 서버에서는 Video, Thumbnail용의 두가지 Pre-signed URL을 내려주고 있다. 

 

API 설계 (put 요청은 클라이언트가 직접 S3에 요청하기 때문에 설계도에 넣지 않았다.)

 

기능 URL Method Request Response
비디오 업로드용(썸네일 포함) presigned Post 요청 /api/interviews/draft Post {
  "headers": {
    "Authorization": 
     "Bearer ${ token }",
  }
}
"status": "200 Ok",
data: {
  interview:{
    id: Number,
  },
  presignedUrl: {
    video : String,
    thumbnail: String
  }
}

 

 

또한 axios interceptors를 통해 모든 헤더에 토큰을 넣어주기 때문에 프로젝트 모든 API의 header에 token을 따로 넣지 않았다. 

 

//interviewApis.js

import instance from "./axios";
import axios from "axios";

const interviewApis = {
  // Pre-signed Url을 서버에 요청하는 API
  getPresignedUrl: () =>
    instance.post(`${process.env.REACT_APP_API_FILE_URL}/api/interviews/draft`),
 
  // 서버로부터 전달받은 Pre-signed Url을 S3에 직접 업로드 하는 API
  s3VideoUpload: (presignedUrl, video) =>
    axios.put(presignedUrl, video, {
      headers: {
        "Content-Type": "video/webm",
      },
    }),
  s3ThumbnailUpload: (presignedUrl, thumbnail) =>
    axios.put(presignedUrl, thumbnail, {
      headers: {
        "Content-Type": "image/png",
      },
    }),
  // 인터뷰 게시글 생성 API
  createInterview: (interviewId, note, questionId, isPublic) =>
    instance.post(
      `${process.env.REACT_APP_API_FILE_URL}/api/interviews/${interviewId}`,
      {
        note,
        questionId,
        isPublic,
      }
    ),
};

export default interviewApis;

 

 

위 파일에서 작성한 API를 가지고 createInterviewHandler라는 함수에서 호출한다. 아래의 코드는 함수만 가져온 코드로 생략된 부분이 있다. 

// interviewForm.jsx

 const createInterviewHandler = async () => {
    if (isPublic.length === 0 || noteInfo.noteCount === 0) {
      alert("공개 여부 또는 내용을 작성해주세요");
      return;
    }

    try {
      loadingHandler();
     // 서버는 AWS로부터 받은 Pre-signed URL를 data에 담아 넘겨준다. 
      const { data } = await interviewApis.getPresignedUrl();
      const video = recorderRef.current.getBlob();
      // 서버에서 내려준 interview ID
      const interviewId = data.interview.id;
      // Video용 Pre-signed URL
      const presignedUrlVideo = data.presignedUrl.video;
      // Thumbnail용 Pre-signed URL
      const presignedUrlThumbnail = data.presignedUrl.thumbnail;
      // S3에 Video와 Thumbnail 직접 업로드
      await interviewApis.s3ThumbnailUpload(presignedUrlThumbnail, thumbnail);
      await interviewApis.s3VideoUpload(presignedUrlVideo, video);
      // 인터뷰 게시글 생성에 필요한 API
      await interviewApis.createInterview(
        interviewId,
        noteInfo.noteText,
        questionId,
        isPublic
      );

      recorderRef.current.reset();
      recorderRef.current = null;

      if (isPublic) {
        navigate(`/feedback/`);
      } else {
        navigate("/mypage/history");
      }
    } catch (err) {
      Sentry.captureException(`Interview Form : ${err}`);
    }
  };

 

 

 

 

 

Reference 

반응형