[Typescript] Nest.js Multer Storage Engine에 Cloudflare Images 적용기
서론
이번 포스팅에서는 nest.js multer의 storage engine으로 Cloudflare Images 서비스를 URL 업로드 방식으로 연동해보려는 시도를 담았다.
1. Cloudflare Images가 뭔데?
Cloudflare Images는 Amazon S3나 Cloudflare R2 처럼 파일을 저장하고 서빙해주는 서비스인데, 그 파일이 "이미지"에 초점이 맞춰져 있는 서비스이다. 그래서 Cloudflare Images는 이미지 파일 서빙을 위한 강력한 기능, 가격정책을 제공한다.
Cloudflare Images의 가격정책은 용량단위의 가격이 아닌 이미지 개수의 대한 가격으로 책정된다. 그래서 이미지 10만개를 호스팅 하는데 $5, 이미지 10만개를 전송하는데 $1의 비용만 발생한다. Cloudflare Images는 여기에 더불어 URL 파라미터를 통한 이미지 리사이징 기능을 제공해 이미지 사이즈를 최적화하는데 편리하다. 또한 "Direct upload" 방식을 제공하기 때문에 클라이언트가 서버에 파일을 업로드하여 서버가 파일을 임시저장하고 서버가 다시 Cloudflare Images로 업로드 하는 두개의 과정대신, 클라이언트가 Cloudflare Images로 바로 파일을 업로드 하는 방식을 취할 수 있기 때문에 보다 빠르고 안전한 구조를 위할 수 있다고 한다. 그리고 이미지 보안이 필요할 경우 Cloudflare Images에서 제공하는 "Signed URL" 기능으로 서명된 URL의 유효한 토큰을 가진 사용자만 이미지에 접근할 수 있도록 할 수도 있다.
근데 "Amazon S3나 Cloudflare R2로 파일 서빙을 해주는 서비스인데 왜 굳이 자료도 별로 없는 Cloudflare Images를 써야하냐"라는 의문이 생길 수도 있다.
Cloudflare Images를 사용하려는 이유를 요약하자면 다음과 같다:
존12나너무 저렴하다 (이미지 10만개당 월 호스팅 비용 $5, 10만개 전송에 발생하는 비용 $1)- 세팅과정이
존12나간단하다 (그냥 Cloudflare 로그인해서 Images 들어간 뒤 플랜만 선택해주면 됨. 이후 토큰 복사해서 REST API로 요청) - 이미지 리사이징등 이미지 처리만을 위한 제공 기능들이
존12나편리하다.
S3나 R2는 단순히 용량단위로 가격을 책정하여 파일을 저장하고 서빙하기 위한 기능만을 제공하지만 Cloudflare Images는 이미지 호스팅, 서빙, 가공을 위한 전용 가격 정책과 기능을 제공하기 때문에, 이미지 파일들은 굳이 S3나 R2에 저장하는 것보단 Cloudflare Images에 업로드하여 사용하는 것이 훨씬 유리하기 때문이다.
2. Muler Storage Engine은 뭔데?
Multer는 express 계열 백엔드 프레임워크에서 multipart/form-data 파일 업로드를 위해 사용되는 미들웨어이다. 그리고 Storage Engine은 Multer가 파일을 저장하는 방식을 정의해놓은 구현체이다. 그래서 Multer는 DiskStorage(디스크 저장 방식), MemoryStorage(메모리 저장 방식)을 기본 제공하고있지만, 이 Storage Engine을 커스터마이징할 수 있도록 인터페이스를 제공한다. 그래서 이 인터페이스를 사용해 Amazon S3, Cloudflare R2 등으로 파일을 저장하도록 구현할 수 있는데, 필자는 이걸 활용하여 Cloudflare Images에 Multer가 파일을 저장하도록 구현해볼 생각이었다(하지만 순탄치만은 않았다...)
3. Cloudflare Images의 업로드 방식
Cloudflare Images는 크게 3가지 업로드 방식을 제공한다. 아쉽게도 프로그래머블한 방법 중에선 파일을 단순히 직접 업로드 하는 방식은 제공해주지 않고 있다는것이 많이 아쉽다.
3-1. 이미지 대시보드 수동 업로드
직접 Cloudflare Images 대시보드에 들어가 이미지 파일을 수동으로 업로드 하는 방법이다. 이미지 파일을 직접적으로 올릴 수 있는 몇 안되는 수단이지만... 수동이라는게 문제다.
3-2. 크리에이터 직접 업로드
이 방식은 아까 설명했던 Direct Upload 방식이다. 서버에서 Cloudflare Images에 Direct Upload URL을 발급받아 클라이언트에게 전달하면, 클라이언트는 전달 받은 Direct Upload URL을 통해 이미지 파일을 직접 Cloudflare Images로 업로드하게 된다. 이렇게 되면 서버가 임시로 파일을 디스크나 메모리에 저장하지 않아도 되기 때문에 성능과 보안 향상의 장점이 있다.
그리고 업로드 URL을 발급해주면서 Cloudflare Images에 저장될 이미지 파일의 ID 값도 함께 미리 발급해주기 때문에, 이를 이용하면 클라이언트가 직접 올리는 것이 아닌 서버가 파일을 올리는 방식으로도 구현이 가능하다. 하지만 그렇게 구현하기 위해선 구조와 의존성 관계가 복잡하며 이미지 ID의 업로드 완료가 보장되지 않기 때문에 크리에이터 직접 업로드로 올라온 이미지는 사용하기 전 업로드 상태를 조회해야하고, 클라이언트가 직접 이미지 업로드를 하기 위해선 URL 발급 엔드포인트를 따로 분리해주어야 하는 단점이 있다.
3-3. URL 업로드
이 방식은 특이하게 Cloudflare Images로 직접 파일을 업로드 하는 것이 아니라, 외부에서 접속 가능한 파일의 URL을 Cloudflare Images로 전달하면 Cloudflare Images가 해당 URL에서 파일을 다운 받아 저장하는 방식이다. 이 방식은 사용 방법이 간단하고 직접 파일을 multipart로 전송하는게 아니기 때문에 요청 시간이 짧고, 발급된 이미지 ID에 대해선 업로드 완료를 보장해줘 매번 이미지를 사용하기 전에 조회를 하지 않아도 되는 장점은 있으나, 반대로 Cloudflare Images가 파일을 다운 받아야 하기 때문에 응답시간이 매우 길어진다는 단점이 있다(Cloudflare Images는 이미지 다운로드하고 저장이 모두 완료되면 그때 저장된 이미지에 대한 메타 데이터들을 응답으로 내려준다.)
이번 포스팅에선 이 URL 업로드 방식을 이용해서 Storage Engine을 구현해봤다(Cloudflare Images에 서버가 직접 파일을 업로드하는 방식중 제일 구현이 간단했기 때문...)
4. Multer Cloudflare Images Storage Engine 개발 (with URL Upload)
우선 가장 개발 난이도가 낮은 URL 업로드 방식을 이용해 Storage Engine과 Nest.js interceptor를 구현해봤다.
Storage Engine은 _handleFile()과 _removeFile()을 구현하면 되는데, Cloudflare Images에는 따로 이미지를 삭제할 수 있는 기능은 제공하지 않아서 _removeFile()은 callback을 바로 실행하게 해놨다.
만들어놓은 Cloudflare Images storage engine을 Nest.js의 FileFieldsInterceptor에 넣어 이미지인 파일만 저장이 되도록 해놨다.
4-1. 그래서 결과는?
일단 multipart/form-data로 이미지 파일을 받아서 임시로 정적 서빙이 가능한 경로에 저장해두고, 해당 파일의 외부에서 접속이 가능한 URL을 Cloudflare Images로 보내기때문에 정상적으로 저장이 이루어지긴한다. 하지만 테스트를 해보니 단점이 너무 명확하다.
클라이언트 입장에서는 이미지를 서버로, 서버에서 Cloudflare Images로 파일이 업로드 되어야 하는 것이기 때문에 업로드 시간이 2배, 응답을 돌려받기까지의 시간이 2배가 되는것이다. URL 업로드 방식은 Cloudflare Images가 서버로 부터 받은 URL로 이미지를 모두 다운 받아야 해당 이미지의 ID와 접근 URL을 반환해주기 때문에, 서버는 Cloudflare Images가 파일을 다운 받을때까지 클라이언트에게 응답을 전달하지 못하고 장시간 대기 상태가 되어야 한다. 이 단점이 너무 커서 URL 업로드 방식을 이용한 Cloudflare Images 서버 직접 업로드는 실제 서비스에 이용하기 매우 어려워 보인다.
4-2. 그럼 대안은?
URL 업로드 방식을 버리고 크리에이터 직접 업로드 방식을 활용해보면 어떨까싶다.
URL업로드 방식은 Cloudflare Images가 URL로부터 이미지 다운로드가 완료되어야만 이미지에 대한 ID와 접근 URL을 응답으로 내려주지만, 크리에이터 직접 업로드 방식은 클라이언트가 업로드 URL과 ID을 발급 받아서 해당 URL로 파일을 업로드하는 방식이기 때문에 파일에 대한 ID를 빠르게 획득할 수 있다는 장점이 있다.
이를 이용하면 서버가 클라이언트로부터 multipart로 파일을 받으면 로컬에 임시저장하고 업로드 URL과 ID를 발급 받은 후 별도의 업로더에 업로드할 파일의 경로와 URL을 전달하여 요청을 보내고 그 요청을 대기하지 않은 상태로 클라이언트에게 바로 이미지 ID를 응답으로 내려주면 되기 때문이다. 이러한 방식을 구현한다면 별도의 업로드 URL 발급 엔드포인트를 따로 두지 않아도 되는 장점이 덤으로 딸려오게 된다.
다만, 이런 방식으로 Storage Engine을 구현하게 되면 Storage Engine이 별도의 업로더(혹은 업로드 컨트롤러)에 의존성을 가지게되므로 관계가 많이 복잡해지긴하겠지만 현실적으로 가장 가능성 있는 방법은 이 방법일 것 같다.
클라이언트 업로드를 활용한 서버 직접 업로드 방식으로 Storage Engine을 구현하는 것은 다음 포스팅에서 해보겠다.