thumbnail

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

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

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

마크다운(Markdown)에서 이미지를 첨부하는 방법은 다음과 같습니다.

          
1 ![이미지 예시]("https://example.com/image/test_1.png)

대괄호[] 내부에 해당 이미지의 설명과 소괄호() 내부에 이미지의 url을 전달합니다. html의 img 와 같이 url을 넘겨줘야 하기 때문에 혹여나 여러분이 README를 작성하거나 저처럼 블로그 등지에서 마크다운을 활용하는 경우 첨부하고자 하는 이미지의 전처리 과정이 요구됩니다.




url 형식으로 파싱하기

그렇다면 이미지 파일 자체를 url 형식으로 파싱하여 그대로 넘겨주면 되는걸까요? 물론 가능합니다. 라이브러리가 필요 없고 구현이 쉽다는 장점이 있으나 후술할 단점들이 명확하기에 추천드리는 방법은 아닙니다.

하지만 그대로 사용하는것이 좋지 않다는것이지 해당 과정이 필요하지 않다는 것은 아니기 때문에 간단하게만 알아보기로 합시다.


base64 인코딩

base64 란 8비트 이진 데이터(실행 파일, ZIP 파일 등)를 문자 코드에 영향을 받지 않는 공통 ASCII 영역의 문자들로만 이루어진 일련의 문자열로 바꾸는 인코딩 방식을 뜻합니다. 이미지 파일 자체가 이진 데이터(Binary Data)의 형태를 띄기 때문에 손쉽게 인코딩이 가능합니다.

굳이 Binary Data를 base64 기반의 텍스트 데이터로 인코딩 하는 이유는 JSON 기반의 HTTP 통신에서 보다 안전하게 데이터를 주고받을 수 있기 때문입니다.

모든 Binary Data가 ASCII 코드에 포함되는것이 아니므로 신뢰할 수 있는 base64 형식으로 인코딩하여 데이터의 손상을 방지하는 것입니다.

base64는 일부 특수문자를 제외한 53개의 안전한 출력문자만을 사용하며 UTF-8과 같은 인코딩 방식과도 호환되기 때문에 보다 적합하다고 볼 수 있겠습니다. 그렇다면 base64로 인코딩한 결과는 어떨까요?

base64_image

결과값은 위와 같습니다. 네 끔찍합니다. base64로 인코딩한 데이터는 기존의 데이터보다 통상적으로 30%정도 크기가 커지게 됩니다. 게다가 가독성도 좋지 않죠. 만약 base64 데이터를 그대로 클라이언트에 전달하거나 DB에 저장한다면... 여러모로 좋지 않겠죠.

워낙 가독성이 떨어지니 아래와 같이 디코딩 했을 때 참고할 세부 정보를 첨부해 보내는 경우도 있습니다.

          
1 {
2 "title": "imageTitle",
3 "data": "~~"
4 }

그렇다면 Next.js(React)에서 이미지를 base64로 인코딩 하는 방법을 알아봅시다.


          
1 <input
2 className="write__image"
3 type="file"
4 onChange={handleIamgeUpload}
5 multiple
6 />

우선 이미지를 첨부할 form이 있어야겠죠. javascript에서는 input태그에 type = file 옵션을 주어 파일을 첨부할 수 있습니다. 이때 여러개의 이미지를 첨부할 수 있게 하기위해 multiple 옵션을 추가해줍니다.

          
1 import { useState } from 'react';
2
3 //...
4
5 const [images, setImages] = useState<any[]>([]);
6 const [imageTitle, setImageTitle] = useState<string[]>([]);

이미지와 파일명을 저장하는 변수입니다. 첨부할 이미지가 여러개일 가능성이 있기 때문에 배열의 형태로 선언해줍니다.

          
1 import { parseImage } from 'util/parseImg'
2
3 //...
4
5 const handleIamgeUpload = async (e: ChangeEvent<HTMLInputElement>) => {
6 if (e.target.files) {
7 const { images, imageTitle } = await parseImage(e.target.files);
8 setImages(images);
9 setImageTitle(imageTitle);
10 }
11 };

다음은 이미지를 업로드하는 handleImgUpload 함수입니다. 해당 함수는 input태그에 전달되는 함수로 e.target.file. 즉 input으로부터 전달받은 파일들을 이미지를 base64로 인코딩하는 함수 uploadImage 에 전달, 그 리턴값인 images, imageTitle을 각각 해당하는 useState 변수에 덮어씁니다.

useState의 set 메소드는 비동기 작업을 수행하기 때문에 async await 옵션을 주어 이미지의 인코딩이 끝난 후 변수에 전달하도록 합니다.

          
1 export const parseImage = async (
2 files: FileList,
3 ): Promise<{ images: string[]; imageTitle: string[] }> => {
4 const images: string[] = [];
5 const imageTitle: string[] = [];
6
7 for (let i = 0; i < files.length; i++) {
8 const file = files[i];
9 const reader = new FileReader();
10 reader.readAsDataURL(file);
11
12 imageTitle.push(file.name);
13
14 const base64 = await new Promise<string>((resolve, reject) => {
15 reader.onload = () => {
16 resolve(reader.result as string);
17 };
18 reader.onerror = () => {
19 reject(new Error("Error occurred while encoding image file."));
20 };
21 });
22
23 images.push(base64);
24 }
25
26 return { images, imageTitle };
27 };
28

실직적으로 이미지를 base64로 인코딩하는 함수입니다. 해당 함수를 해석하면 다음과 같습니다.


  • 이미지와 파일명을 저장할 배열 images, imageTitle을 선언합니다.

  • 전달받은 배열(이미지 파일이 담긴 배열)의 크기만큼 반복하며 다음 로직을 수행합니다.

    • js에서 파일을 읽어들이는 객체 FileReader를 변수 reader로 선언, reader.readAsDataURL 메소드를 이용해 url 형식으로 데이터를 읽어들입니다.
    • imageTitle에 파일명을 삽입합니다.
    • 변수 base64를 선언, 읽기가 성공했을 경우 그 결과값을, 실패했을 경우 에러를 전달합니다.
    • images에 base64를 삽입합니다.
  • images와 imageTitle을 반환합니다.


FileReader 객체에 대해 더 자세히 알려드리자면 FileReader가 데이터를 성공 여부와 관계없이 읽어들이는 행위가 종료되면 FileReader의 readyStateDONE이 되며 loadend 이벤트가 트리거됩니다.

FileReader.onload는 load 이벤트로 파일을 읽어들이는데 성공했을 경우 발생하며 FileReader.onerror는 그 반대의 경우 발생합니다. 위 두 이벤트는 비동기적 특성을 띄고 있으므로 위 코드에서처럼 Promise 객체를 이용하여 상태에 따라 원하는 작업을 수행하면 되겠습니다.


load이벤트가 정확히 이해되지 않으시면 >>해당 링크<< 를 참고해주시면 감사하겠습니다.


blob url 인코딩

더 손쉬운 방법도 있습니다. 바로 blob객체를 이용하는 것입니다.

blob 객체는 대용량 이진 데이터 객체(Binary Large Objects)로 이미지를 텍스트로의 인코딩 없이 Binary Data 그 자체로 다룰 수 있습니다. blob은 브라우저에서 지원되는 형태이기 때문에 간단히 사용할 수 있습니다.

          
1 const blob = new Blob(image, {type : 'application/json'});

위 코드는 blob 객체를 생성하는 코드입니다. 첫번째 인자로 image를, 두번째 인자로 파싱할 데이터의 타입을 전달합니다.

blob 객체는 HTTP 통신에서도 유효함으로 해당 blob객체를 FormData에 담아 전달할 수 있습니다. FormData는 XMLHttpRequest 전송을 위하여 설계된 key, value 형식의 객체로 주로 파일의 전달에 사용됩니다.

다음은 blob 객체를 이용해 가상의 url을 생성하는 방법입니다.

          
1 const url = URL.createObjectURL(blob);

놀랍게도 단 1줄로 이미지를 가상의 url로 파싱하였습니다. URL.createObjectURL() 메소드는 가리키는 url을 DOM string 형태로 변환하는 역할을 하는데 주의할 점은 이는 실제로 존재하는 url이 아니라는 것입니다.

이는 작업을 수행한 브라우저에 한해 존재하며 브라우저를 닫으면 사라집니다. 때문에 재사용이 불가능하고 수명 또한 한정되어있습니다.

          
1 URL.revokeObjectURL(url);

더구나 위와같이 사용한 후 url을 제거하지 않으면 garbage collecter에 의해 제거되지 않고 메모리에 계속해서 남게되어 비효율을 야기하게 됩니다. 또한 용량이 큰 파일을 클라이언트에서 처리하는 것이므로 사용자 경험에 치명적입니다. 때문에 이 방법도 추천드리지 않습니다.




CDN 서버 활용

CDN서버 이미지

저는 이미지 처리를 담당할 CDN 서버를 두는것으로 문제를 해결했습니다. CDN(Contents Delivery Network)는 전 세계 여러 지역에 분산된 서버 네트워크로 구성된 시스템으로 다음과 같은 특징을 가집니다.


  • 분산서버 : 전 세계 각지에 서버를 분산시켜 사용자의 요청으로부터 가장 가까운 지역(region)으로부터 데이터를 선제적으로 요청하여 빠른 응답속도를 보입니다. 트래픽이 분산되기 때문에 로드 밸런싱 수행에도 이점이 있습니다.

  • 캐싱 : 이미지, 영상과 같은 정적 데이터의 경우 해당 데이터를 캐싱하여 마찬가지로 빠른 응답속도를 보입니다.

  • 보안 및 안정성 : 아무래도 WAS와는 별도로 두기도 하고 분산 시스템이기 때문에 하나의 서버가 다운되더라도 문제없이 기능을 수행할 수 있습니다.


이외에도 제공 업체에 따라 저장소를 지원하거나 이미지 압축을 지원하기도 합니다. 조금 더 자세히 알아볼까요?


요약도

간단하게 요약도를 그려봤습니다. CDN 서버의 이미지 요청 수행은 다음과 같은 단계로 진행됩니다.


  1. 사용자(User)가 페이지(md파일)에 접근할 경우 이미지의 주소를 찾기 위해 DNS에 ip주소를 요청합니다.

  2. DNS는 CDN 서버의 DNS 주소를 반환, 요청 위치에 따라 가장 가까운 region의 CDN 서버. 즉 엣지 서버를 매핑합니다.

  3. 만약 엣지 서버에 캐싱된 데이터가 이미 존재한다면 해당 데이터를 반환합니다. 그렇지 않을 경우 CDN의 원본 서버에 해당 데이터를 요청, 캐싱한 후 반환합니다.


이렇듯 CDN 서버를 활용하면 사용자에게 더 나은 경험을 제공할 수 있습니다. 실제로 CDN 서버를 부설하고 사용하는 방법은 다음 포스트에서 알아보도록 하겠습니다.

# javascript
# 이미지
# CDN
# next.js
# 문제해결
# base64
# blob
# url
# markdown
# md

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

👨‍💻 관련 포스트

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

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

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

AWS S3에서 제공하는 javascript SDK를 이용해 node.js 환경에서 파일(이미지)를 api 요청을 통해 전송해보겠습니다. @aws-sdk/client-s3 모듈을 설치 후 next.js의 원하는 api 엔드포인트에서 route.ts를 작성합니다. S3 클라이언트에 연결하기 위한 객체를 선언하고 접근권한(ACL), 경로 등을 설정하고 Buffer 객체에 이미지를 담아 S3에 전송합니다. 바이너리 데이터로 이미지를 변환해 api의 요청사항을 만족시킵니다. 이미지를 별도의 DB에 분산저장(백업) 하여 데이터의 무결성을 유지합니다.

  2023-12-16

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