team logo icon
article content thumbnail

Gemini Vision API 이미지 전처리- Base64 인코딩

Gemini Vision API를 사용하기 위한 이미지 파일을 Base64 스트링으로 인코딩하는 과정을 다룹니다.

안녕하세요, 지우다 팀의 두 명의 '우' 중 우리를 하나로 만드는 강우혁입니다.

사용자 경험을 중요시하는 프론트엔드 개발자로서, 팀의 기술적 기반을 단단하게 다지고 다양한 대안을 검토하여 최선의 선택을 하는 것을 목표로 하고 있습니다. 다양한 기술적 도전 속에서 최적의 솔루션을 찾아가는 과정을 공유하고자 합니다.

이번 글에서는 AI와의 효율적인 연동을 위해 이미지를 Base64로 인코딩하는 방법에 대해 소개하고자 합니다. 특히, 사용자가 업로드한 이미지를 변환하고 이를 AI API에 전달하기 위한 전처리 과정에서 Base64 인코딩이 어떻게 활용되는지를 실습 중심으로 다뤄보겠습니다.


📌 이미지 분석을 위한 전처리 - Base64 인코딩

저희 프로젝트에서는 Google의 Gemini Vision API를 이용하여 사진 속 음식의 메뉴명, 영양소(탄수화물, 단백질, 지방)와 칼로리 정보를 제공받습니다. Gemini Vision은 이미지 데이터를 기반으로 고급 분석 기능을 제공하는 AI 모델로, API 요청 시 이미지 데이터를 Base64 인코딩 형식으로 전달해야 합니다.

즉, Gemini Vision은 이미지의 Base64 인코딩 값과 파일 형식을 매개변수로 받아 이미지를 분석하기 때문에 유저에게 음식 사진을 전달 받아 Base64로 인코딩하는 전처리 과정이 필요합니다. 이 과정에서 우리는 어떤 방식으로 이미지 데이터를 처리할지 고민하게 되었습니다.


이미지 데이터를 받아오는 2가지 방법

프로젝트 내에서 이미지 데이터를 받아오는 경우는 다음과 같습니다.

  1. 사용자가 업로드한 파일 : 사용자가 업로드한 이미지 파일 객체를 통해 Base64 인코딩하는 방법

  2. 저장된 이미지 URL : 사용자의 이미지 파일을 클라우드 스토리지에 저장한 후, 해당 이미지 URL을 통해 Base64 인코딩하는 방법

두 경로 모두 최종 목표는 같지만(이미지 데이터를 Base64로 변환), 다루는 데이터의 유형(파일 vs URL)이 다르기 때문에 각각 다른 접근 방식이 필요합니다. 그럼 각 방식에 대해 자세히 살펴보겠습니다.



Image file을 Base64로 인코딩


1. FileReader API 이용하기

첫 번째 방법으로, 브라우저의 기본 API인 FileReader를 활용한 접근법을 살펴보겠습니다:

const convertImageFileToBase64 = async (file: File) => {
    const mimeType = file.type; // file의 mimeType 저장
    const base64 = await new Promise<string>((resolve, reject) => {
      const reader = new FileReader();
      reader.readAsDataURL(file);
      reader.onload = () => {
        if (typeof reader.result === 'string') {
          const result = reader.result;
          const extractBase64 = result.split(',')[1];
          console.log(extractBase64);
          resolve(extractBase64);
        }
      };
      // 에러 처리 생략
    });

    return {
      inlineData: {
        data: base64,
        mimeType
      }
    };
};






브라우저의 FileReader API를 이용하여 이미지를 Base64로 인코딩할 수 있습니다. readAsDataURL 메서드를 사용하면 File 객체를 받아서 Base64로 인코딩된 문자열을 반환할 수 있습니다.

하지만 이 방식에는 몇 가지 단점이 있었습니다:

  1. 타입 안전성 문제: reader.result의 타입이 string | ArrayBuffer | null로 null 타입을 포함하고 있어, 타입스크립트에서 사용하려면 타입 가드가 필요합니다. 따라서 명시적으로 typeof result === 'string' 같은 처리를 해줘야 합니다.

  2. 수동 파싱 필요: reader.resultdata:[mimeType];base64,... 형식의 문자열을 반환하기 때문에, 실제 Base64 부분만 추출하기 위해 result.split(',')[1]과 같은 수동 파싱이 필요합니다. 이는 'split(',')'의 결과가 항상 [메타데이터, base64데이터]라는 가정에 의존하게 됩니다.

이런 구조적인 문제들로 인해 더 안정적이고 타입 안전한 방법을 찾아보게 되었습니다.

2. File 객체의 arrayBuffer를 이용한 인코딩

두 번째 방법으로, File 객체의 arrayBuffer 메서드를 활용한 접근법을 살펴보겠습니다:

const convertImageFileToBase64 = async (file: File) => {
    const mimeType = file.type;
    const arrayBuffer = await file.arrayBuffer();
    const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));

    return {
      inlineData: {
        data: base64,
        mimeType
      }
    };
};






이 방식은 File 객체가 제공하는 arrayBuffer() 메서드를 직접 활용합니다. 처리 과정은 다음과 같습니다:

  1. file.arrayBuffer()로 바이너리 데이터를 가져옵니다.

  2. new Uint8Array(arrayBuffer)로 8비트 부호 없는 정수 배열로 변환하여 개별 바이트에 접근 가능하도록 합니다.

  3. 전개 연산자(...)를 사용하여 모든 바이트를 String.fromCharCode()에 전달하여 해당 바이트들을 문자로 변환합니다.

  4. 마지막으로 btoa()를 사용해 이 문자열을 Base64 인코딩된 문자열로 변환합니다.


✅ ArrayBuffer란

ArrayBuffer는 고정 길이의 바이너리 데이터 버퍼입니다. 이는 메모리 상의 원시 데이터 블록이며, 직접 읽고 쓸 수 없습니다. 데이터를 읽거나 쓰기 위해서는 TypedArray(예: Uint8Array, Float32Array 등)나 DataView를 사용해야 합니다.

btoa() 메서드는 문자열의 각 문자가 이진 데이터의 바이트 단위로 처리되는 이진 문자열을 Base64로 인코딩한 ASCII 문자열을 생성합니다.

최종 선택: arrayBuffer 방식

두 가지 방법을 비교 분석한 결과, 최종적으로 두 번째 방법인 arrayBuffer를 이용한 접근법을 선택했습니다. 그 이유는 다음과 같습니다:

  1. 타입 안전성: file.arrayBuffer()는 명확히 Promise<ArrayBuffer>를 반환하기 때문에 불필요한 타입 가드가 필요 없습니다.

  2. 유연성: arrayBuffer를 이용하여 우리가 원하는 파일 크기에 맞춰 Uint8Array, Float32Array 등으로 변환하여 커스텀할 수 있습니다.

  3. 명확성: split 등을 사용한 문자열 파싱이 필요 없어 코드가 더 명확하고 에러 가능성이 줄어듭니다.


Image URL을 Base64로 인코딩


이제 두 번째 경로인 이미지 URL을 Base64로 인코딩하는 방법에 대해 알아보겠습니다.



1. fetch 함수를 이용해 얻은 arrayBuffer를 이용한 변환

// URL에서 Base64 인코딩 및 파일형식 읽기const handleImageURLUpload = async (url: string) => {const response = await fetch(url);const buffer = await response.arrayBuffer();

  const mimeType = response.headers.get('content-type') || 'image/jpg';

  return {
    inlineData: {
      data: Buffer.from(buffer).toString('base64'),
      mimeType
    }
  };
};

<button onClick={() => handleImageURLUpload(url)}>image url로 제출하기</button>







이 접근 방식은 단순해 보이지만, 실제 테스트 결과 중요한 문제점이 발견되었습니다.


결과 - CORS 에러 발생

CORS 에러가 발생한 이유를 이해하기 위해 브라우저의 보안 정책에 대해 살펴볼 필요가 있습니다.

일반적으로 <img> 태그의 src 속성에 이미지 URL을 지정하는 경우에는 CORS 에러가 발생하지 않습니다. 이는 브라우저의 JavaScript가 이미지 데이터에 접근할 수 있느냐 없느냐에 따라 달라집니다:

  • <img> 태그는 단순 요청으로 결과를 받아 이미지 렌더링만 수행하고, 이미지 정보에 대한 코드상 접근은 불가능합니다.

  • 반면 Fetch API를 사용하면 응답으로 받은 이미지 정보에 JavaScript를 통해 직접 접근이 가능하기 때문에, 보안상의 이유로 CORS 정책이 적용됩니다.

    CORS 에러 발생

브라우저의 Network 탭에서 확인해보면, 요청한 이미지에 대해 서버는 Status 200으로 정상 응답을 했지만, 브라우저가 CORS 정책 위반을 감지하여 JavaScript에서 해당 응답 데이터에 접근하는 것을 차단했습니다.


브라우저에서 외부 서버로 리소스를 요청할 때는 'Origin' 헤더를 함께 전송하여 요청 출처를 밝히는데, 외부 서버는 응답 헤더에 'Access-Control-Allow-Origin'과 같은 CORS 관련 설정을 포함해야 합니다. 하지만 대부분의 외부 이미지 서버는 이러한 CORS 설정을 하지 않았기 때문에 브라우저에서 직접 접근하려 할 때 에러가 발생합니다.


💡CORS 설정이 되어 있는 페이지의 경우 응답 헤더에는 다음과 같은 정보가 포함됩니다:

CORS 설정이 되어 있는 이미지를 요청하였을때의 응답 헤더

✅ '*'는 모든 도메인의 요청을 허용한다는 의미입니다.


CORS 문제 해결 방법

CORS는 브라우저 보안 정책이기 때문에, 클라이언트 측에서 직접 우회하기 어렵습니다. 하지만 서버 간 통신에는 CORS 제한이 적용되지 않는다는 점을 활용할 수 있습니다. 따라서 Next.js 서버(또는 프록시 서버)를 활용하면 이 문제를 효과적으로 해결할 수 있습니다.

Next.js에서는 다음 두 가지 방법으로 CORS 문제를 해결할 수 있습니다:

방법 1. Server Action을 사용하는 방법

Server Action을 활용하면 클라이언트가 아닌 Next.js 서버에서 요청을 처리하기 때문에 CORS 문제에서 자유로워질 수 있습니다.

// /lib/utils/base64-incoding.util.ts'use server';

// URL로부터 Base64 인코딩 및 파일형식 읽기export const convertImageUrlToBase64 = async (url: string) => {const response = await fetch(url);const buffer = await response.arrayBuffer();

  const mimeType = response.headers.get('content-type') || 'image/jpg';

  console.log('buffer ===>', Buffer.from(buffer).toString('base64'));
  return {
    inlineData: {
      data: Buffer.from(buffer).toString('base64'),
      mimeType
    }
  };
};







이 방식은 클라이언트에서 Server Action을 호출하면 Next.js 서버 환경에서 외부 이미지 URL에 요청을 보내고, 그 결과를 클라이언트에 반환하는 방식입니다. 서버에서 실행되기 때문에 브라우저의 CORS 정책을 우회할 수 있습니다.


방법 2. Route Handler(API Route)를 사용하는 방법

Server Action 외에도 Next.js의 Route Handler를 사용하여 클라이언트가 직접 외부 리소스를 요청하지 않도록 중계할 수 있습니다. 이는 RESTful API 형태로 구현하여 클라이언트에서 요청 URL을 숨기거나, 외부에서 API를 호출해야 할 때 유용합니다.

// app/api/image/convert/route.tsimport { NextRequest, NextResponse } from 'next/server';

export async function POST(req: NextRequest) {
  const { url } = await req.json();

  const response = await fetch(url);
  const buffer = await response.arrayBuffer();
  const mimeType = response.headers.get('content-type') || 'image/jpeg';

  return NextResponse.json({
    inlineData: {
      data: Buffer.from(buffer).toString('base64'),
      mimeType
    }
  });
}

최종 해결책 선택: Server Action

두 가지 해결책을 검토한 결과, 저는 서버 액션(Server Action)을 사용하는 방법을 선택했습니다. 이 결정에는 다음과 같은 고려 사항이 있었습니다:

  1. 단순성: Server Action은 별도의 API 라우트 설정 없이 바로 서버 기능을 호출할 수 있어 구현이 간단합니다.

  2. 사용 범위: 외부에서 API로 접근할 필요가 없고 내부 애플리케이션에서만 사용하는 기능이기 때문에 Server Action이 더 적합했습니다.

Route Handler는 다음과 같은 경우에 더 적합할 수 있습니다:

  • 외부 애플리케이션에서도 접근 가능한 API 엔드포인트가 필요할 때

  • RESTful API 형태로 구조화된 인터페이스가 필요할 때

하지만 저희 프로젝트에서는 단순히 이미지 URL을 Base64로 변환하는 기능만 필요했기 때문에, 더 간단한 Server Action이 더 적합한 선택이었습니다.

결론

지금까지 Gemini Vision API와 같은 AI 서비스를 위해 이미지를 Base64로 인코딩하는 두 가지 시나리오와 각각의 구현 방법에 대해 살펴보았습니다:

  1. 파일에서 Base64 변환: FileReader와 arrayBuffer 방식 중 타입 안전성과 유연성이 더 뛰어난 arrayBuffer 방식을 선택했습니다.

  2. URL에서 Base64 변환: CORS 문제를 해결하기 위해 Server Action을 활용하여 서버에서 이미지를 가져오고 변환하는 방식을 채택했습니다.

이러한 접근 방식은 Next.js 기반의 프로젝트에서 외부 이미지 자원을 효율적으로 활용하고, AI API와 원활하게 연동하는 데 큰 도움이 되었습니다. 특히 CORS와 같은 브라우저 보안 정책의 제약을 이해하고 서버 기능을 활용해 우회하는 방법을 익히는 과정에서 많은 것을 배웠습니다.

앞으로도 프론트엔드 개발에서 마주치는 다양한 기술적 도전과 해결 과정을 계속 공유하도록 하겠습니다. 감사합니다!


참고문서

최신 아티클
Article Thumbnail
강우혁
|
2025.04.29
React-Hook-Form으로 복잡한 폼 관리하기
React-Hook-Form을 사용해 복잡한 중첩 폼과 외부 라이브러리에 종속된 Input을 효과적으로 제어하고, Zod로 타입 안정성과 검증 로직을 간편하게 처리하여 개발 경험을 향상시키며, 불필요한 렌더링을 줄여 사용자 경험을 개선합니다.
Article Thumbnail
이다은
|
2025.04.29
useFunnel 훅 트러블슈팅: 리렌더링 및 Hydration 문제 해결
다단계 프로세스를 위한 useFunnel 훅은 불필요한 리렌더링, 데이터 유지, 그리고 SSR 불일치 문제를 해결하는 과정을 공유합니다.
Article Thumbnail
김우경
|
2025.04.29
클라이언트 상태는 언제 준비될까? : 새로고침 시 초기값이 표시되는 이슈
zustand 상태 초기화 시 발생할 수 있는 문제점과 그 대비책을 제 경험을 바탕으로 소개합니다.