thumbnail

네이버 CDN 서버를 활용한 Markdown 이미지 첨부(3)

네이버 CDN 서버를 활용한 Markdown 이미지 첨부(3)

📋 [ next.js ] 시리즈 몰아보기 (4)

이미지 POST api 기본작성

node.js 환경에서 S3 버켓에 데이터를 전송하기 위해 다음 과정들을 수행합니다.


모듈설치 및 기본설정

@aws-sdk/client-s3는 node.js 환경에서 AWS SDK를 사용하기 위한 javascript Client 모듈입니다. 터미널에 다음 코드를 입력해 모듈을 설치합니다.

          
1 npm i @aws-sdk/client-s3
2
3 yarn add @aws-sdk/client-s3

설치가 완료되었다면 원하는 api endpoint에 route.ts 파일을 생성해줍시다. 저는 여러개의 이미지를 POST하는 임의의 api "images"를 app/api/images 폴더에 작성해보겠습니다.

          
1 // app/api/images/route.ts
2
3 import { NextRequest, NextResponse } from "next/server";
4 import {
5 PutObjectCommand
6 ObjectCannedACL,
7 S3Client,
8 } from "@aws-sdk/client-s3";

우선 필요한 모듈들을 import합니다. NextRequest, NextResponse는 next.js에서 제공하는 request, response의 타입입니다.

PutObjectCommand는 S3에 객체를 직접적으로 업로드하는 메소드,ObjectCannedACL은 객체의 접근 제어 목록을 설정하는 타입이며 S3Client는 AWS S3의 클라이언트 객체입니다.

          
1 export default async function POST(req: NextRequest) {
2 try {
3
4 /* ~~~ 코드 ~~~ */
5
6
7 } catch (e: unknown) {
8 console.error(e);
9 return NextResponse.json(
10 { error: "internal Server Error" },
11 { status: 500, headers: { "Content-Type": "application/json" } },
12 );
13 }
14 }

다음으로 기본적인 api 구조입니다. try, catch문을 이용해 간단한 예외처리도 수행해줍니다. 앞으로 작성할 코드들은 모두 try문 안에서 작성될 것입니다.


S3 클라이언트 객체 선언

다음과 같이 S3 클라이언트 객체를 선언합니다.

          
1 const client = new S3Client({
2 region: "kr-standard",
3 endpoint: process.env.AWS_HOSTNAME,
4 });

region에 지역을, endpoint에 본인의 S3 버킷 hostname을 입력해주시면 됩니다.

본인의 S3 버킷을 생성하는 과정은 인터넷에 좋은 자료가 많기 때문에 생략하겠습니다. 혹시나 버킷을 생성하는 과정도 필요하시다면 댓글로 의견 남겨주시면 별도의 포스트를 작성해보도록 하겠습니다.

S3 버킷 생성 튜토리얼 : https://aws.amazon.com/ko/s3/getting-started/


이미지 전처리 및 전송

다음으로 이미지 전처리 과정을 진행해봅시다. request로부터 전달받은 이미지가 여러개일 수 있으므로 전달받은 images의 크기만큼 반복해서 로직을 수행할 것입니다.

          
1 if (images && images.length > 0) {
2 for (let i = 0; i < images.length; i++) {
3 // ~~~ 코드 ~~~
4 }
5 }

images가 빈 배열일 경우. 즉 전달받은 이미지가 없는 경우도 있기 때문에 images가 유효한 경우에만 로직을 수행하도록 합시다.

          
1 const base64Data = images[i].split(",")[1];
2 const imageBuffer = Buffer.from(base64Data, "base64");
3 const contentType = imageTitle[i].split(".").pop();

다음으로 이미지 전처리를 수행합니다. images의 각 원소(이미지)에 대해, base64로 인코딩된 데이터를 분리하고, 이를 Buffer 객체로 변환, 확장자 명을 파일 명에서 추출합니다.

저희는 분명 이전 과정(1번 포스트)에서 이미지를 안전하게 base64로 인코딩하여 전송했습니다. 그렇다면 어째서 base64 데이터를 다시 분리하여 Buffer 클래스에 담는걸까요?


이진 데이터(Binary Data)와 Buffer 클래스

Buffer 클래스는 node.js에서 바이너리 데이터를 효과적으로 처리할 수 있게 해주는 내장 클래스인 반면 base64는 바이너리 데이터를 ASCII 문자열로 변환한 데이터입니다.

서버에 바이너리 데이터를 안전하게 전송하기 위해 base64로 인코딩 하였지만 이미 서버에 도착하였다면 다시 바이너리 데이터로 변환하여 처리하는것이 효율적입니다.

이유는 다양하지만 핵심적인 이유만 정리해보자면 다음과 같습니다.

  • 데이터 처리 : 이미지 파일은 기본적으로 바이너리 데이터입니다. 데이터를 처리하기에 ASCII 문자열보다 훨씬 자연스럽습니다.

  • 효율성 : base64로 인코딩된 데이터는 바이너리 데이터보다 약 33%정도 크기가 큽니다. 바이너리 데이터를 사용하면 이러한 크기의 낭비를 개선할 수 있습니다.

  • 호환성 : 대부분의 외부 API(AWS S3 포함)들은 바이너리 데이터를 선호합니다. 특히나 파일 시스템에 이미지를 저장할 때에는 바이너리 데이터를 사용하기 때문이기도 합니다.

Buffer 클래스는 성능상으로도 뛰어나지만 바이너리 데이터를 쉽고 간편하게 조작할 수 있기 때문에 채택하였습니다.


이미지 전송

드디어 이미지를 전송하는 단계입니다. S3에 데이터를 업로드하기 위해 파라미터를 작성합니다.

          
1 const params = {
2 Bucket: "${버켓 이름}",
3 Key: `images/${imageTitle[i]}`,
4 Body: imageBuffer,
5 ACL: ObjectCannedACL.public_read,
6 ContentType: `image/${contentType}`,
7 };

파라미터의 각 필드들에 대해 자세히 알아봅시다.

  • Bucket : 버켓의 이름을 입력합니다.

  • Key : 객체를 식별하는 고유 식별자이자 데이터가 저장될 파일 경로를 뜻합니다. 예시에서는 /images/${이미지 이름} 에 저장됩니다.

  • Body : 버켓에 저장될 실질적인 데이터입니다.

  • ACL : ACL은 객체에 대한 접근 권한을 듯합니다. 이미지가 누구에게나 보여지게 하기 위해서는 public_read 옵션을 사용하는게 맞겠습니다.

  • ContentType : 파일의 확장자명입니다. 이미지 전처리 과정에서 얻은 확장자 명을 전달합니다.

          
1 await client
2 .send(new PutObjectCommand(params))
3 .then(() => console.log("success"))
4 .catch(e => {
5 console.log(e);
6 });

최종적으로 이미지를 클라이언트에 전송하는 코드입니다. putImagesCommand 메소드를 이용해 클라이언트에 파라미터를 전달합니다.

그냥 전송만 하면 섭섭하니 성공하면 "success"를, 실패하면 에러를 출력하도록 코드를 작성합니다.

이걸로 이미지를 전송하는것은 끝입니다.

          
1 const parsedUrl = imageTitle.map((title: string) => {
2 return `${CDN 경로}/${title}`
3 });
4
5 return NextResponse.json(
6 { parsedUrl: parsedUrl },
7 { status: 200, headers: { "Content-Type": "application/json" } },
8 );

이미지 전송이 무사히 끝마쳤을 경우 S3 버켓의 변경사항을 CDN origin서버가 감지, CDN 서버에 등록했을 것이므로 각 이미지가 배포된 CDN 서버의 url을 return하면 next.js에서 이를 활용할 수 있습니다.

          
1 const res = await fetch(`${URL}/api/images`, {
2 method: "POST",
3 headers: { "Content-Type": "application/json" },
4 });
5
6 const { parsedUrl } = await res.json();
7
8 // ~~~ parsedUrl을 활용하는 코드 ~~~

이렇게 말이죠.


(선택)데이터 분산 저장

지금까지는 S3 버켓에만 이미지를 전송했지만 전달받은 이미지 파일을 별도의 DB에 저장하여 여러 이점을 챙길 수 있습니다.

단순히 데이터를 백업해둔다는 개념과는 별개로 DB에 저장함으로써 데이터의 무결성을 유지할 수 있습니다.

정말 드물지만 만약 이미지 전송 과정에서 데이터가 손상되거나 S3 버킷에 문제가 생긴다면 원본 데이터가 필요할 것입니다.

이외에도 DB에 데이터를 저장하였기 때문에 검색/인덱싱 수행에 활용하는 등 확장성에도 도움이 되기도 합니다.

          
1 import { connectToDatabase } from "util/mongodb";
2
3 const { db } = await connectToDatabase();
4
5 // ~~~ S3 전송코드 ~~~
6
7 await db.collection(${collection명}).~~
8

mongoDB에 원본 데이터를 저장한다면 대충 위와같은 흐름일 것입니다.

굳이 mongoDB인 이유는 json 친화적이며 mongoDB에서 이미지를 바이너리 데이터의 형식으로 저장하기 때문에 코드의 재사용이 가능하기 때문입니다.

무엇보다 mongoDB가 next.js의 serverless 환경과 궁합이 좋습니다.



참고자료

# javascript
# CDN
# next.js
# markdown
# AWS
# S3
# mongoDB
# 데이터무결성

💡 로그인 하지 않아도 댓글을 등록할 수 있습니다!

👨‍💻 관련 포스트

card Img

[next.js] CDN 서버를 활용한 Markdown 이미지 첨부(1)

[next.js] CDN 서버를 활용한 Markdown 이미지 첨부(1)

마크다운에서 이미지를 첨부하기 위해선 img태그의 src에 url을 전달하듯 url 형식의 문자열을 전달해야 합니다. 때문에 README를 작성하거나 저처럼 블로그 등지에서 마크다운을 활용하는 경우 첨부하고자 하는 이미지의 전처리 과정이 요구됩니다. 이미지 파일을 base64 형식으로 인코딩하여 안전한 문자열로 사용할 수 있습니다. HTTP 통신에서 JSON 형태의 통신을 위해 base64 형태로 인코딩할 필요가 있습니다. blob 객체 또한 이용 가능합니다. js의 내부 메소드로 간단하게 blob 객체로 인코딩, 브라우저에서 가상 url을 생성할 수 있습니다.

  2023-11-11

card Img

[next.js] 네이버 CDN 서버를 활용한 Markdown 이미지 첨부(2)

[next.js] 네이버 CDN 서버를 활용한 Markdown 이미지 첨부(2)

안전한 HTTP 통신을 위해 이미지 파일을 base64 형태로 인코딩, next.js 서버단에서 CDN 서버에 업로드 하는것이 이상적입니다. github issue에 이미지를 등록하는것은 CDN 서버를 구성할 필요 없이 간편하게 이미지를 업로드 할 수 있지만 궁극적인 해결방안은 아닙니다. 네이버 클라우드 플랫폼의 CDN+와 Object Storage, AWS S3 bucket를 이용해 CDN 서버를 구성합니다. 네이버 클라우드 플랫폼의 API 인증키를 발급받고 서비스 사용 신청을 합니다.

  2023-11-13

card Img

Next.js 13+에서 무한 대댓글 구현 (with mongoDB)

Next.js 13+에서 무한 대댓글 구현 (with mongoDB)

next.js 13 이상의 버전에서 mongoDB와 계층형 트리구조를 활용한 무한 대댓글을 구현하였습니다. 댓글가 대댓글의 그룹으로 묶어 관리하는 REF, 댓글의 순서를 결정하는 RE_STEP, 댓글의 들여쓰기 레벨을 의미하는 RE_LEVEL, 부모 댓글의 고유 식별자를 의미하는 RE_PARENT 총 4개의 속성을 이용해 자동으로 대댓글의 위치를 조정하도록 하였습니다. 클라이언트에서는 React의 hooks를 활용해 interactive한 사용자 경험을 제공하고 next.js의 serverless function을 활용해 대댓글 작성 api를 손쉽게 구현하였습니다.

  2024-01-04