React, express, NCP를 활용한 파일 업로드 및 접근 (1)

박기범·2021년 11월 16일
4

본 게시글은 부스트캠프 6기 웹 풀스택 트랙의 SWS (ShallWeSound) 프로젝트를 진행하며 겪었던 문제점과 해결방안을 정리합니다.

더불어 본 게시글의 SWS 프로젝트는 doggydeok2, seongjunme, jsl0149님과 같이 개발했으며 문제 해결에 도움을 받았음을 명확히 밝힙니다.

0. 개요

  1. 겪었던 문제점
  2. 리서치
  3. 해결방안

1. 겪었던 문제점

우선, 현재 진행중인 프로젝트 소개를 다음의 링크로 갈음한다.
https://github.com/boostcampwm-2021/WEB21-ShallWeSound

이 프로젝트를 진행하면서, 웹 사이트에서 사용자들에게 제공할 음악 파일들을 서버 어딘가에 저장할 수 있어야 했고, 마찬가지로 업로드된 음악 및 썸네일 파일에 사용자들이 접근할 수 있어야 했다.

이 과정에서 우리는 naver cloud platformobject storage를 사용하여 파일보관 및 접근을 구현하기로 계획했다. 그런데 이 과정에서 다음과 같은 문제점이 있었다.

  1. 근본적으로, 어떻게 파일을 업로드할것인가? 네이버 클라우드 플랫폼의 object storage에 접근하기 위한 라이브러리, SDK를 알고있는가?

  2. 클라이언트가 서버를 거쳐 object storage에 파일을 업로드한다면, 서버의 디스크에 지속적으로 read/write가 일어나지 않겠는가? 이러한 io 작업은 서버에 많은 부담을 주지 않는가?

  3. 1번 문제점과 마찬가지로, object sotrage에서 서버를 거쳐 음악 스트리밍을 제공받는다면, 이 과정에서도 디스크게 read/write가 빈번하게 일어나지 않겠는가?

  4. 만약 서버에 음악을 지속적으로 다운로드 받고 지우는 방법을 택한다면, 특정 사용자가 음악을 재생하는 중에 다른 사용자가 음악을 다 청취하여 음악이 자동으로 삭제되는 현상이 발생할수도 있다. 이런 문제는 어떻게 해결할것인가?

우리는 위와 같은 문제점을 해결하기 위해 리서치를 수행했다.

2. 리서치

1. multer

기본적으로 express에서 클라이언트의 파일 업로드에 대응하기 위해 multer middleware를 활용한다. multer에서는 여러가지 업로드 형태에 대응하는 메소드를 제공해주는데, 자세한 사용법은 다음의 링크로 갈음하며 본 게시글에서는 동시에 여러 input tag를 통한 파일 업로드를 제어하는 방법을 다룬다.

multer 사용법(공식문서)
무려 공식문서가 한글이다...

우리는 썸네일에 해당하는 이미지 파일과 곡 파일을 모두 업로드해야했다. 따라서 multerfields 옵션을 활용했다.

자세한 코드는 해결방안 파트에서 다루겠다.

2. aws-sdk

네이버 클라우드 플랫폼의 object storage는 aws-sdk의 S3 메소드를 통해서 제어가 가능했다. 이 과정에서 모든 부분은 네이버 클라우드 플랫폼에서 제공하는 object stroage 공식문서와 aws-sdk의 공식문서만을 활용했으므로 해당 문서들을 링크함으로써 설명을 갈음한다.

네이버 클라우드 플랫폼 object storage 공식문서
aws-sdk s3 공식문서

3. 해결방안

우선 다음과 같은 방식으로 multer를 활용하여 클라이언트로부터 파일을 요청받고, 백앤드에서 제어할 수 있도록 하였다. (백앤드 코드의 uploadLogic 함수는 넘겨받은 파일 데이터를 이용하여 object storage에 업로드하기위해 직접 제작한 함수이다. 차차 공개하겠다.) 그리고 프론트앤드(리액트)에서 multerfields에서 인식할 수 있는 값을 input 태그의 name 옵션으로 부여했다.

#backend code

const upload = multer({
    storage: multer.memoryStorage()
});
const router = express.Router();
const cpUpload = upload.fields([{name:'userFile1'}, {name:'userFile2'}])


router.post('/', cpUpload, async (req, res, next)=>{
    const bucket_name = 'sws';
    const files = req.files as { [fieldname: string]: Express.Multer.File[] };
    const object_name = `${files.userFile1[0].originalname}`;
    const contentHash = makeHash(files.userFile1[0].buffer.toString());
    const thumbnailName = object_name.split('.')[0]+'.'+files.userFile2[0].originalname.split('.')[1];
    const singer = req.body.singer;
    const description = req.body.description
    uploadLogic(bucket_name, files, object_name, contentHash, thumbnailName, singer, description);
    res.send(200);
})
#frontend code

<div className={styles.title}>음악파일</div>
<input
  className={styles.musicName}
  value={uploadedFile.musicName}
  placeholder="음악파일"
  disabled={true}
/>

<label htmlFor="musicFile">첨부</label>
<input
  {...uploadedFile.musicFile}
  className={styles.input}
  id="musicFile"
  type="file"
  name="userFile1"
  onChange={isFileUpload}
/>

...

<div className={styles.title}>썸네일 이미지</div>
<input
  className={styles.thumbnailName}
  value={uploadedFile.thumbnailName}
  placeholder="썸네일"
  disabled={true}
/>
<label htmlFor="thumbnailFile">첨부</label>
<input
  {...uploadedFile.thumbnailFile}
  className={styles.input}
  id="thumbnailFile"
  type="file"
  name="userFile2"
  onChange={isThumbUpload}
/>

다음으로 object storage에 업로드하기 위한 과정이다. 이 과정에서 aws-sdk의 S3객체와 관련 메소드들을 사용했다. 우선, 다음과 같이 S3객체를 정의하고 export하여 외부에서 사용할 수 있도록 하였다.

import * as AWS from 'aws-sdk';
const region = 'kr-standard';
const access_key = `${process.env.ACCESS_KEY}`;
const secret_key = `${process.env.SECRET_KEY}`;
export const S3 = new AWS.S3({
    endpoint:'https://kr.object.ncloudstorage.com',
    region,
    credentials: {
        accessKeyId : access_key,
        secretAccessKey: secret_key
    }
});

그리고 다음과 같이 파일을 직접 업로드한다.

await S3.putObject({
            Bucket: bucketName,
            Key: folder_name
        }).promise();
        await Promise.all([
            S3.upload({
                Bucket: bucketName,
                Key: folder_name+objectName,
                Body: Readable.from(files.userFile1[0].buffer)
            }, options).promise(),
            S3.upload({
                Bucket: bucketName,
                Key: folder_name+thumbnailName,
                Body: Readable.from(files.userFile2[0].buffer)
            }, options).promise()
        ])

이 과정에서 해결하려한것이 바로 업로드 시 disk read / write 과정에 의한 서버에 주는 부담이다. 파일 업로드의 backend 코드를 보면, multer에 memory storage 옵션을 부여했다. 다시 코드를 보자.

const upload = multer({
    storage: multer.memoryStorage()
});

이렇게 multer의 storage 옵션에 memoryStorage를 선언하면, multer는 클라이언트로부터 넘겨받은 파일 정보를 disk가 아니라 memory에 임시로 들고있게 된다. 그리고 multer middleware를 거친 req가 끝나면 js의 gc가 자동으로 해당 메모리를 회수한다고 한다. 이 얼마나 좋은 옵션인가!

이렇게 disk io작업에 따른 서버 부하를 줄였다. 하지만 문제는 여기서 끝나지 않는다. 네이버 클라우드 플랫폼의 공식문서를 확인해보자.

await S3.upload({
        Bucket: bucket_name,
        Key: object_name,
        Body: fs.createReadStream(local_file_name)
    }, options).promise();

이런!
Body를 주목하라. 기본적으로 aws-sdk는 파일을 업로드하기 위해 파일 내용을 readable stream 형태로 제공받는다.
하지만 multer의 memory storage는 메모리에 파일을 buffer의 형태로 저장한다.
즉, 메모리의 저장된 버퍼를 readable stream의 형태로 변환해야만 정상적으로 파일 업로드가 가능하다는 뜻이다! 이제 어떻게 해야할까?

다음 게시글로 계속하여 서술하도록 하겠다.

-다음편에 계속-

profile
원리를 좋아하는 개발자

0개의 댓글