[서버/렌더링] 파일을 Base64로 인코딩하고 SSR하는 API 구현

황록·5일 전
0

서버

목록 보기
4/4
post-thumbnail

배경

이전 렌더링 관련 포스팅에서 최근 5개의 velog 게시물을 카드로 렌더링하는 API를 구현하는 과정을 작성했었습니다.

그때는 일단 디자인보다는 기능에 집중해서 구현했었습니다.

그 후에 이제 좀 더 예쁘게 꾸미고 싶어서
디자이너 친구한테 좀 더 예쁘게 꾸미는게 가능한지 물었습니다.

저도 구상할 때 포스트잇에다가 적는 것을
생각만 했었는데 이렇게 생각을 디자인으로 해주니까
생각이 명쾌해졌습니다.
(개발자 친구보다 디자이너 친구를 더 많이 만들고 싶습니다 ㅎㅎ)

그렇게 레이아웃, 색, 이미지, 폰트 다 결정되고
구현 또한 순조롭게 다 됐었습니다.

로컬에서 테스트를 다 마치고,
서버에 배포하고 API를 해봤습니다.

하 근데 이때 이제 대참사가 발생했었습니다.

이미지하고 폰트가 다 날라갔습니다.

이렇게 이제 이번 문제 해결 여정이 시작됐습니다.


Challenge point

SSR에서 이미지하고 폰트가 렌더링되지 않는다.


문제 분석

일단 응답된 카드를 보면
텍스트하고 색 같이 코드 외부에 저장할 필요 없는 데이터는 잘 왔는데,
폰트하고 이미지같이 코드 외부에 저장할 필요 있는 파일은 오지 않았습니다.

하지만, 로컬에서는 잘 동작한걸 보면 경로 문제는 절대로 아니었습니다.

2가지를 원인으로 생각했었습니다.

1번째는 서버에 이미지와 폰트가 들어가지 않은 상황.
2번째는 프록시에 의해 거부되서 이미지와 폰트가 나오지 않은 상황.

1번째가 문제일 경우에는 (배포된 서버 URL) / (이미지 경로)를 했을 때,
이미지가 없는 404 오류가 떠야하는데, 오류가 뜨지 않아서 1번째는 아니라고 확신했습니다.

2번째는 처음부터 확신을 가졌습니다.
gitgub readme는 서버로 바로 가지 않고,
camo.githubusercontent.com 라는 곳이 프록시를 하고 렌더링됩니다.

그러다보니 사진과 폰트같은 파일을 배포된 서버에서 받는 것으로 코딩하면 프록시에 의해 요청이 취소될 수 있습니다.

확실히 배포된 서버 URI로 해보니까 렌더링이 잘 되는데,
github readme나 notion 같이 프록시가 붙는 곳에만 되지 않았습니다.

이로써 저의 문제는 더 구체화 됐습니다.

프록시에 의해 파일 요청이 거부된다.

근데 문제를 구체화되도 어떻게 해결해야 하나 감이 전혀 잡히지 않았습니다..


해결 사례 탐색

'설마 이런 SSR로 뱃지 만드는 프로젝트는 다 이미지나 폰트를 안쓰나?'
같은 의문이 들어서 비슷한 프로젝트들을 탐색해봤습니다.

가장 먼저 떠오른게 백준 랭크를 보여주는 뱃지 프로젝트였습니다.

https://github.com/mazassumnida/mazassumnida

지금 하고 있는 velog 뱃지 개발 프로젝트를 하고 싶게 만든 장본인이기도 합니다 ㅎ

아무튼 그래서 api/image 로 가보니까 png들이 있었습니다.

그 뜻은 이 프로젝트는 파일을 프록시를 넘어서 렌더링 하는 것에 성공했다는 뜻이므로 집중해서 살펴봤습니다.

보다가 api/image.py로 가보니까, 이런 것들이 있었습니다.

.py인데 암만봐도 파이썬 코드는 아니라서 GPT한테 뭔지 물어보니까,
PNG 파일을 base64로 인코딩한 데이터 URI 스킴이라고 했습니다.

data:image/png;base64, 가 앞에 붙은 걸 보니
맞는 말인거 같았습니다.

코드들을 읽어보고 생각해보니까
프록시를 건너뛰고 서버에 요청을 보내려하는 문제를 어떻게 해결하려하는지 감이 잡혔습니다.


Solution - base64로 정적 데이터 생성

저는 기존에 < image /> 태그를 사용하면서
href 뒤에 상대 경로가 담긴 URL을 사용해서 사진을 불러왔습니다.

이렇게 하면 태그를 만났을 때
서버는 클라이언트한테 사진을 요청하게 됩니다.

하지만, 프록시 서버는 요청을 대행하는 서버이므로 일단
상대 경로로 접근을 하지만, 파일이 없어서 데이터를 주지 못하게 됩니다.

이 문제를 해결하기 위해서 base64로 파일을 문자열로 인코딩해줘서
href 뒤에 인코딩한 데이터 자체를 넣음으로
프록시에 요청하는 것을 막을 수 있습니다.


Solution 적용

solution은 아래 단계로 생각해봤습니다.

먼저 사진과 폰트를 base64 인코딩해주는 것부터 시작했습니다.

const fs = require("fs");
const path = require("path");

const outDir = path.join(process.cwd(), "src", "lib");
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });

const image = fs.readFileSync(path.join("public", "postit.png")).toString("base64");
const image2 = fs.readFileSync(path.join("public", "postit2.png")).toString("base64");
const image3 = fs.readFileSync(path.join("public", "postit3.png")).toString("base64");
const font = fs.readFileSync(path.join("public", "font", "NanumJinJuBagGyeongACe.ttf")).toString("base64");

const content = `// auto-generated by scripts/encode-assets.js
export const POSTIT_1__BASE64 = "data:image/png;base64,${image}";
export const POSTIT_2__BASE64 = "data:image/png;base64,${image2}";
export const POSTIT_3__BASE64 = "data:image/png;base64,${image3}";
export const NANUM_FONT_BASE64 = "data:font/ttf;base64,${font}";
`;

fs.writeFileSync(path.join(outDir, "base64-assets.ts"), content, "utf8");
console.log("폰트와 이미지 base64 문자열 생성됨");

이 코드를 통해 사진과 폰트를 각각 base64로 인코딩 해줌으로써
직접 정적 데이터인 코드와 함께 전송 가능해졌습니다.

다음은 route 코드에서 인코딩된 파일들을 import하고
받은 velog API 데이터를 가공해주고 모두 SVG를 만드는
함수에 넣어주면 됩니다.

export const runtime = "nodejs";

import { parseStringPromise } from "xml2js";
import { velogSvg } from "./blogSvg";
import {
  POSTIT_1__BASE64,
  POSTIT_2__BASE64,
  POSTIT_3__BASE64,
  NANUM_FONT_BASE64
} from "@/lib/base64-assets"; // prebuild로 생성된 파일 경로

export async function GET(request: Request) {
  try {
    const { searchParams } = new URL(request.url);
    const id = searchParams.get("id") || "";

    const res = await fetch(`https://v2.velog.io/rss/${id}`);
    const xml = await res.text();
    const data = await parseStringPromise(xml, { explicitArray: false });

    const rawPosts = data.rss.channel.item;
    const posts = Array.isArray(rawPosts) ? rawPosts : [rawPosts];
    const slicedPosts = posts.slice(0, 5);

    const imgs = [
      POSTIT_1__BASE64,
      POSTIT_2__BASE64,
      POSTIT_3__BASE64,
      POSTIT_1__BASE64,
      POSTIT_2__BASE64,
    ];

    const svg = velogSvg(id, slicedPosts, { inlineImages: imgs, inlineFontDataUri: NANUM_FONT_BASE64 });

    return new Response(svg, { headers: { "Content-Type": "image/svg+xml; charset=utf-8" } });
  } catch (err: unknown) {
    const message = err instanceof Error ? err.message : String(err);
    return new Response(JSON.stringify({ error: message }), {
      status: 500,
      headers: { "Content-Type": "application/json" },
    });
  }
}

SVG 만드는 코드는 너무 길어서 생략하겠습니다.

이렇게 하고 서버에 배포하고 API를 보면,

이젠 프록시가 있어도 잘 렌더링되는 것을 볼 수 있습니다.


한계점

하지만 아쉽게도 아직 완벽하게 극복한 것은 아닙니다.

응답시간이 너무 느림


요청 하나에 3초가 걸릴 정도로 너무 느립니다.
멀티 스레드를 두든,
시간 복잡도를 최적화시키든 생각을 해봐야 할 거 같습니다.

클릭하면 폰트가 적용 안됨

이건 잘 모르겠더라고요..
github 화면에서는 잘 나오는데,
뱃지를 클릭하면 왜 갑자기 폰트가 없어지는지 모르겠습니다..

좀 더 생각해봐야 할 거 같습니다.


후기

이미지 전송에 대한 생각을 많이 해봤습니다.

기존에는 S3에 저장하고 url을 전송하는 것만을 했었는데
상황에 따라서는 이미지를 base64로 인코딩하고, 인코딩 문자열을
DB에 저장하고 데이터 그 자체를 보내주는 것 또한 할 수 있을거 같습니다.

또 오픈소스의 중요성 또한 다시금 느꼈습니다.

막힌 문제가 있을 때, 유사한 문제를 다룬 프로젝트를 찾아보면서
인사이트를 기를 수 있다는 것을 느꼈고 아티클을 읽는 것 또한 좋지만,
오픈소스를 분석해보는 것 또한 성장에 좋다고 느꼈습니다.

한 걸음 더 성장한 과제였습니다.

profile
알고리즘과 SW 설계를 좋아합니다.

0개의 댓글