[Typescript] Nest.js Cloudflare R2 + Images 통합 Multer storage engine 개발기
서론
이번 포스팅에선 Nest.js에서 Cloudflare R2와 Images를 통합하여 Multer Storage engine으로 사용하기 위한 개발기를 담았다.
이전 포스팅과 이어지는 내용이니 그 포스팅을 참고하길 바란다.
2023.03.08 - [SW/Typescript] - [Typescript] Nest.js Multer Storage Engine에 Cloudflare Images 적용기
1. Cloudflare Images 서버 업로드 문제
이전 포스팅에서 설명했듯이, Cloudflare Images는 서버가 직접 파일을 업로드할 수 있는 방법을 직접적으로 제공하진 않는다. 하지만 그렇다고 이미지를 R2에 올리기에는 Images의 저렴한 요금 정책과 이미지 최적화 기능을 이용할 수 없기 때문에 Images에 서버가 직접 파일을 올리는 방법을 포기할 수는 없었다.
Cloudflare Images는 크게 URL 업로드 방식과 크리에이터 직접 업로드 방식을 제공하는데, 이전 포스팅에서 URL업로드 방식으로 Multer storage engine을 만들어 사용해봤으나 Cloudflare Images가 서버로부터 이미지를 다운로드 받는데까지 다소 아쉬운 응답속도를 보여줬다. 그래서 대안으로 크리에이터 직접 업로드 방식을 이용해서 서버가 직접 업로드 할 수 있도록 해보자는 계획을 세우게 되었다.
2. Cloudflare Images 크리에이터 직접 업로드 방식을 통한 서버 직접 업로드 구현
위 시퀀스다이어그램은 Cloudflare Images의 크리에이터 직접 업로드 방식을 이용해서 서버가 직업 이미지 파일을 업로드하는 과정을 나타낸 것이다. 이 과정은 크게 클라이언트, 서버, 업로더로 나누어진다. 클라이언트가 서버로 파일을 업로드 하면 서버가 파일을 임시로 디스크에 저장한 뒤 Cloudflare Images로부터 업로드URL과 이미지ID를 발급받고 업로더에 파일업로드를 요청한뒤 클라이언트에게 이미지 ID를 내려주게 된다. 이때 파일업로드를 요청받은 업로더는 디스크에 저장된 파일을 Cloudflare Image로 업로드를 하고 디스크에서 파일을 제거하게 된다.
이 과정을 구현하는 중요포인트는 업로더를 어떻게 구현할 것이며, 업로더에게 요청을 보내는 방법을 어떻게 처리할 것이냐이다.
그래서 필자는 이 업로더를 Bull이라는 queue 관리 라이브러리를 활용하여 이 문제를 해결하기로 했다.
Nest.js에서 Bull를 통해 queue를 관리할 수 있도록 @nestjs/bull 공식 라이브러리를 제공한다. 이 라이브러리를 사용하면 간편하게 Redis queue를 사용할 수 있으며, queue에 데이터가 추가됐을때 트리깅이 되는 백그라운드 프로세스를 구현할 수 있다. 그래서 서버가 파일 업로드와 관련된 데이터를 queue에 추가하면, 해당 queue에 물려있는 백그라운드 프로세스가 Cloudflare Images에 이미지 파일을 업로드 하도록 구현할 계획이다. @nestjs/bull에 대한 더 자세한 내용은 공식 문서를 참고하길 바란다.
2-1. 프로젝트 디렉토리 구조
프로젝트 디렉토리 구조는 위와 같다. Cloudflare와 관련된 기능 구현체는 multer storage engine 외의 다른 모듈에서도 사용할 수 있도록 따로 전역 모듈로 분리했고, @nestjs/bull을 사용하기 위해선 Redis가 필요하기 때문에 Redis 전역 모듈을 만들어 거기에 BullModule을 import 및 initialize하고 export한다 (이후에 다른 Redis관련 기능 구현이 필요할 경우 이 Redis 전역 모듈에 붙이면 된다.)
2-2. Redis 모듈 구현
구현될 Cloudflare 모듈에서는 @nestjs/bull을 통해 queue를 사용할 것이기 때문에 전역 Redis 모듈을 만들어서 BullModule을 initialize 해주어야 한다. Redis와 관련된 다른 전역 구현이 필요하다면 이 모듈에 추가하면 된다.
2-2-1. redis.module.ts
RedisModule에 @Global() 데코레이터를 붙여 전역에서 사용될 수 있도록 만들었다. BullModule을 initialize할 때, ConfigModule을 주입하여 Redis 관련 환경변수를 받아 대입했다. 그 후, BullModule 전역에서 쓰일 수 있도록 exports 등록해주었다.
2-3. Cloudflare 모듈 구현
Cloudflare 관련 기능을 한 곳에 모은 모듈을 구현할 것이기 때문에 R2와 관련된 기능도 함께 구현할 것이다. 그래서 Cloudflare 모듈을 전역으로 정의하고, R2와 Images의 기능을 각각의 서비스 파일로 분리하여 구현한 뒤 Cloudflare 모듈에서 export 해주는 방식으로 구현했다. 또한, Images의 서비스에는 queue에 파일 업로드를 추가하는 기능도 포함되기 때문에 cloudflare-images-upload.processor.ts라는 Bull processor 파일도 Cloudflare 모듈 디렉토리 안에 함께 구현됐다.
2-3-1. cloudflare.module.ts
CloudflareModule에 @Global() 데코레이터를 붙여 전역에서 사용될 수 있도록 만들었다. CloudflareModule의 imports에서 'cloudflare-images-upload'라는 이름의 bull queue를 등록해주었는데, 이후에 설명할 CloudflareImagesUploadProcessor와 CloudflareImagesService에서 사용될 queue이다. CloudflareImagesUploadProcessor, CloudflareImagesService, CloudflareR2Service에 관한 설명은 후술하도록 하겠다.
2-3-2. cloudflare-images-upload.processor.ts
이 파일은 'cloudflare-images-upload'라는 이름의 bull queue에 데이터가 추가됐을때 실행될 백그라운드 프로세스를 정의한 것이다. queue의 추가될 데이터의 인터페이스를 정의해두고 이 데이터를 통해 Cloudflare Images로 이미지 파일을 업로드 하기 위한 파일 경로와 업로드 URL을 받아 업로드 작업을 처리한다. 업로드가 완료된 파일은 바로 제거하도록 만들었다.
2-3-3. cloudflare-images.service.ts
Cloudflare Images에 관한 기능을 구현한 서비스 파일이다. 'cloudflare-images-upload' bull queue를 이용하기 위해 @InjectQueue()로 주입받았다. 여기서 주목해야할 부분은 getDriectUploadURL()과 addUploadQueue()이다. getDirectUploadURL()은 Cloudflare Images로 부터 크리에이터 업로드 URL과 이미지 ID를 발급받아 반환하는 메소드이고, addUploadQueue()은 'cloudflare-images-upload' bull queue에 업로드할 이미지 파일의 ID와 URL을 추가하는 메소드이다.
이후 설명할 Cloudflare Multer storage engine에서 CloudflareImagesService를 주입 받아 이 메소드들을 이용해 Cloudflare Images로 이미지 파일 업로드를 처리하게 된다.
2-3-4. cloudflare-r2.service.ts
Cloudflare R2에 관한 기능을 구현한 서비스 파일이다. 기본적으로 Cloudflare R2는 AWS S3와 호환되기 때문에 공식문서에서도 'aws-sdk'의 S3 객체를 이용하여 R2를 이용하면 된다고 나와있다. 그래서 priavte로 AWS.S3 객체를 생성하여 R2 객체를 선언했는데, endpoint에는 'https://{Cloudflare Account ID}.r2.cloudflarestorage.com'라는 URL을 넣고 accessKey와 secretAccessKey에는 Cloudflare R2에서 발급받은 토큰을 넣어주면 된다.
2-4. Cloudflare Multer 모듈 구현
위에서 구현된 Cloudflare 모듈과 Redis 모듈을 바탕으로 Multer에 붙일 수 있는 storage engine을 만들어야 클라이언트가 파일을 업로드 할때 자동으로 Cloudflare R2나 Image로 업로드 되도록 만들 수 있다.
2-4-1. cloudflare-storage.engine.ts
Cloudflare R2와 Images를 이용하는 Multer storage engine의 구현 파일이다. CloudflareStorage는 CloudflareImagesService와 CloudflareR2Service에 대한 의존성을 갖는데, Multer의 Storage engine은 Nest.js module을 통해 의존성을 주입받을 수 없기 때문에 별도로 서비스 객체를 생성하고 인자를 통해 주입받아야한다.
파일의 이름을 확인하여 Cloudflare Images에 업로드 가능한 파일 형식일 경우 CloudflareImagesService를 이용해 업로드 하도록 했다. 그 외에 파일을 CludfalreR2Service를 통해 업로드 되도록 했다.
CloudflareStorage 내부에는 diskStorage를 가지고 있다. 파일을 임시로 디스크에 저장해두고 파일의 경로를 전달하거나 fs.readFile(or Sync)로 Buffer 객체를 전달하여 Cloudflare Images와 R2에 업로드 하기 위함이다.
2-4-2. cloudflare-multer.module.ts
기본적으로 전역 모듈이 아닌 MulterModule을 전역으로 사용할 수 있도록 하기 위해, CloudflareMulterModule에 @Global() 데코레이터를 붙이고 MulterModule을 exports에 등록하였다. CloudflareStorage는 multer storage engine이지만 CloudflareImagesService와 CloudflareR2Service에 의존성을 가지고 있기 때문에, MulterModule을 imports에 등록할때 registerAsync()를 사용하여 의존성을 주입해줬다.
2-5. Nest.js 전역 Multer 등록
이제 Nest.js에서 전역으로 사용할 수 있도록 app.module.ts imports에 등록해주어야 한다.
2-5-1. app.module.ts
3. 사용법
사용법은 다른 Storage engine과 차이가 없다. @UseInterceptors()를 사용하여 FilefieldsInterceptor() 등을 통해 파일을 받아오면 된다.
4. 여담
Cloudflare의 Images와 R2는 굉장히 저렴하고 제공하는 기능이 다양해서 적극적으로 도입해보고자 이런 통합 Multer storage engine을 만들게 되었다. 하지만 아무래도 Images와 R2가 출시된지 오래되지 않아 공식 문서 외에는 자료가 없는 편이다. 그래서 이번 포스팅을 위한 작업도 오랜기간의 삽질이 뒷바탕되었다... 이러한 블로그 포스팅 하나로부터 조금 더 Cloudflare 서비스들에 대한 사용례들이 활성화되고 더 많은 레퍼런스들이 생겨났으면 좋겠다..ㅠ