[TIL/React] 2024/12/30

원민관·2024년 12월 30일

[TIL]

목록 보기
164/201
post-thumbnail

Client-Server: HTML 렌더링 및 이미지 처리 ✍️

측정할 수 없다면 관리할 수 없고, 관리할 수 없으면 개선할 수 없다. 프로젝트를 진행하는 것도 중요하지만, 진행 상황을 정확히 계측하고 정리하는 시간도 분명히 필요하다.

이 글을 두 가지 질문을 중심으로 전개될 예정이고, 그리 긴 분량을 차지하지는 않을 것이다.

1) HTML 데이터를 어떻게 렌더링 하지?, 2) 이미지 데이터를 어떤 방식으로 처리해야 하지?가 이번 글의 핵심이다.

1. HTML 데이터 처리 ⚙️

1-1. 클라이언트에서의 HTML 생성 🔍

클라이언트에서 submit 버튼을 클릭하는 순간, 다음 로직이 실행된다.

const parser = new DOMParser();
const doc = parser.parseFromString(content, "text/html");

위 코드는 문자열 형태의 HTML 콘텐츠를, DOM 구조로 변환하는 코드다. DOM에 대해서는 이전 글을 참고하면 많은 도움이 될 것이다.

1-2. 서버로 HTML 전송 🔍

 try {
      const response = await axios.post("http://localhost:3000/questions", {
        title,
        content: doc.getElementsByTagName("body")[0].innerHTML,
        tags,
      }

DOM 파서를 통해 파싱한 값이 doc이다. 서버로 데이터를 전송할 때, 콘텐츠는 doc.getElementsByTagName으로 조작된 내용이다. body 태그 내부의 HTML 요소들을 꺼내오기(=조작하기) 위해, 파싱 작업이 선행되어야 했던 것이다.

마크업이 적용된 위와 같은 DOM의 body 내부에 위치한 태그들은, 문자 형태로 서버에 전송된다.

1-3. 클라이언트에서의 HTML 처리 🔍

그런데 클라이언트에서 서버로 fetch 요청을 보냈다고 가정해 보자. 클라이언트가 반환받는 값은 HTML tag 모양의 '문자열'이다.

에디터에 preview 기능으로 마크업이 적용된 형태를 미리 보여주고 싶다는 말은, 클라이언트에서 HTML 문자열을 HTML 콘텐츠로 변환해야 함과 동일한 의미를 갖는다.

 <div
          className="ql-editor"
          dangerouslySetInnerHTML={{
            __html: DOMPurify.sanitize(content),
          }}
          style={{
            overflow: "hidden",
            whiteSpace: "pre-wrap",
          }}
        />

dangerouslySetInnerHTML 속성을 사용해, 받아온 HTML tag 문자열을 실제 DOM에 삽입한다.

그런데 만약 서버에서 데이터를 처리할 때 악성코드가 삽입된 채로 HTML이 클라이언트로 전송되면 문제가 발생할 수 있다. 이러한 보안 위협을 XSS(Cross-Site Scripting)이라 부른다.

XSS에 대처하기 위해 sanitize 과정이 요구된다. 위 코드는 DOMPurify에서 제공하는 sanitize 메서드를 적용해놓은 상태다.

2. AWS S3를 통한 이미지 처리 ⚙️

2-1. Base64 인코딩 이슈 🔍

다음으로 content에서 이미지를 처리하는 과정에서 문제가 발생했다.

꿀 귀요미 사진을 submit 하려는 순간 말도 안 되는 내용이 console에 입력되었다.

에디터에서 이미지나 미디어 파일을 HTML 콘텐츠에 포함시킬 때, 일반적으로 Base64 인코딩 과정을 거치게 된다.

Base64 Encoding은 이진 데이터, 즉 binary data를 text data로 변경하는 Encoding을 뜻한다. 귀요미 사진의 이진 데이터를 text data로 변환한 결과가 위 콘솔의 말도 안 되는 문자열이다.

이진 데이터를 텍스트 데이터로 변환하는 과정에서 파일 크기가 증가하여 네트워크 대역폭을 많이 차지하게 되고, 서버에서 Base64 데이터를 디코딩 하는 작업 역시, 서버의 CPU 자원을 많이 소모한다.

"그래서 문제가 뭐야? 문제 정의를 어떻게 했어?"라고 스스로 물어보는 게 습관이 됐다.

서버에 이미지 데이터를 전송할 때, Base64 인코딩 형식으로 보내기 싫음

2-2. Amazon S3란? 🔍

Amazon S3(Simple Storage Service)는 본인들 피셜, 업계 최고의 확장성, 데이터 가용성 및 보안과 성능을 제공하는 '객체 스토리지 서비스'라고 한다.

스토리지에 데이터는 객체 단위로 저장되고, 해당 객체는 파일 이름, 크기, 업로드 시간 등의 메타 데이터를 포함한다.

구체적으로 프로젝트 내에서 로직이 어떻게 구현되었는지 살펴보자.

2-3. 처리 함수 및 로직 🔍

2-3-1. Amazon S3 Setting ✅

  AWS.config.update({
    accessKeyId: import.meta.env.VITE_AWS_ACCESS_KEY_ID,
    secretAccessKey: import.meta.env.VITE_AWS_SECRET_ACCESS_KEY,
    region: import.meta.env.VITE_AWS_REGION,
  });

  const s3 = new AWS.S3();

accessKeyId, secretAccessKey, region에 관한 정보를 객체에 담아서 AWS.config.update() 메서드를 통해 AWS SDK를 초기화한다.

웹상에서의 초기 설정은 다음 레퍼런스를 참고하자.

reference: https://celdan.tistory.com/36

AWS SDK 설정을 완료한 뒤 s3 객체를 생성한다. 이를 통해 s3와 상호작용하며 파일 업로드, 다운로드, 버킷 관리 등의 작업을 수행할 수 있게 된다.

2-3-2. convertBase64ToFile() ✅

 const convertBase64ToFile = async (src: string) => {
    const res = await fetch(src);
    const blob = await res.blob();
    return new File([blob], `image-${Date.now()}.jpg`, { type: "image/jpeg" });
  };

유저가 업로드하고자 하는 이미지 src의 파일의 src를 파라미터로 지정한다. 이어서 해당 파일의 response를 blob으로 변환한다. blob은 binary large object 즉, 이진 데이터를 뜻한다.

유저가 업로드하고자 하는 이미지를 이진 데이터로 변환하여 blob 변수에 할당했다. 이후 해당 이진 데이터를 몇 가지 속성을 추가하여 File 객체로 변환한 뒤 해당 객체를 반환하는 것으로 함수 실행이 종료된다.

File 객체는 blob의 확장이다.

blob의 데이터를 유지하되, 파일명이나 파일 크기, 마지막 수정 시간 등과 같은 메타 데이터를 포함하는 객체다.

2-3-3. uploadFileToS3() ✅

 const uploadFileToS3 = async (file: File) => {
    const params = {
      Bucket: import.meta.env.VITE_AWS_BUCKET_NAME,
      Key: `images/${file.name}`,
      Body: file,
      ContentType: file.type,
    };

    return new Promise<string>((resolve, reject) => {
      s3.upload(
        params,
        (
          err: Error | null,
          data: AWS.S3.ManagedUpload.SendData | undefined
        ) => {
          if (err) {
            console.error("S3 Upload Error:", err);
            reject(err);
          } else {
            console.log("File uploaded successfully:", data);
            resolve(data?.Location || "");
          }
        }
      );
    });
  };

uploadFileToS3() 함수는 위에서 return 한 File 객체를 파라미터로 전달받는다.

params애 S3와 관련된 몇 가지 업로드 매개변수를 설정한다. 업로드할 S3 버킷 이름, 파일이 저장될 S3 내의 경로, 업로드할 파일, 파일의 MIME 타입 등을 지정한다.

이후 s3의 upload 메서드를 통해 해당 파일을 업로드하면, 결과값을 볼 수 있게 된다.

3. 요약 ⚙️

이번 글에서는 HTML 데이터 처리와 이미지 데이터를 다루는 두 가지 주요 질문을 중심으로 논리를 전개했다.

  1. HTML 데이터 처리

    • 클라이언트에서 HTML 생성: DOMParser를 사용해 문자열 형태의 HTML을 DOM 구조로 변환하고, 이를 서버에 전송하기 위해 getElementsByTagName("body")[0].innerHTML로 HTML 콘텐츠를 추출했다.
    • 서버로 HTML 전송: 서버로 데이터를 전송할 때 HTML 요소들은 문자열로 보내지며, 이를 dangerouslySetInnerHTML을 사용해 클라이언트에서 다시 HTML로 변환하여 렌더링한다. 이때, XSS 공격을 방지하기 위해 DOMPurify.sanitize를 통한 악성 코드 제거 과정이 요구됨을 확인했다.
  2. 이미지 처리 (AWS S3 사용)

    • Base64 인코딩 문제: 이미지 파일을 Base64로 인코딩하여 전송할 때, 파일 크기가 증가하고 서버의 CPU 자원을 소모하는 문제를 해결하기 위해, Base64 인코딩을 사용하지 않고 S3에 이미지를 업로드하는 방법을 제시했다.
    • Amazon S3: S3는 안전하고 확장성이 뛰어난 객체 스토리지 서비스로, 이미지를 업로드하기 위해 AWS SDK를 설정했다.
    • 파일 업로드 로직: convertBase64ToFile 함수로 이미지를 File 객체로 변환한 후, uploadFileToS3 함수를 통해 S3에 업로드했다.

결론적으로, HTML과 이미지 데이터를 효율적으로 처리하고, 보안 문제를 고려한 방법들을 제시하여, 클라이언트-서버 환경에서의 성능과 안정성을 높이는 방법을 고민했다고 정리할 수 있겠다.

회고 🌿

문서나 코드를 '정리'하는 일에서 느껴지는 몰입감은 사람을 즐겁게 만든다. 수동적으로 읽는 것이 아니라 능동적으로 옥석을 가려낸다는 느낌을 받을 때, 주변에서 뜯어말려도 그 일에 계속 빠져들게 된다.

바쁜데 힘들지 않고, 느긋한데 괴롭다. 중요한 것은 "내 삶을 얼마나 주도적으로 이끌어 갈 수 있는가"이다. 내가 뒷자리에 앉아 있는지, 운전대를 잡고 있는지 계속해서 자가 진단을 해야 하는 것 같다. 운전대 잡고!, 2024 바이!

profile
Write a little every day, without hope, without despair ✍️

0개의 댓글