Angular, Firebase AI Logic, 및 Gemini 3를 사용한 AI 기반 Alt 텍스트 생성기 구축

발행: (2025년 12월 11일 오후 11:46 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

Overview

이 프로젝트에서는 Gemini 2.5에서 Gemini 3 (Pro Preview)로 업그레이드하여 지능형 이미지 Alt‑Text 생성기를 만들었습니다. 이 앱은 설명적인 대체 텍스트를 생성할 뿐만 아니라 해시태그, 권장 사항, 그리고 내장된 Google Search 도구를 활용한 놀라운 혹은 희귀한 사실까지 제공합니다. 모델이 답을 도출하는 과정을 사용자가 확인할 수 있도록 추론(“thinking”) 과정이 공개됩니다.

Tech Stack

ComponentTechnology
Front‑endAngular v21
Back‑endFirebase AI Logic (Vertex AI)
ModelGemini 3.0 Pro Preview
Cloud servicesFirebase App Check, Firebase Remote Config
FeaturesJSON‑structured output, Thinking mode, Google Search grounding

Structured Output

모델은 엄격히 형식화된 JSON 객체를 반환하므로 UI에서 데이터를 렌더링하기가 매우 간단합니다.

Thinking Mode

Gemini 3.0은 기본적으로 “thinking”을 지원합니다. 생각, 입력, 출력에 사용된 토큰을 추적하여 모델 추론 비용을 이해할 수 있습니다.

Tool Calling

앱은 내장된 Google Search 도구를 사용해 근거 조각, 제안 및 웹 검색 쿼리를 가져옵니다. 이러한 내용은 모델이 반환하는 근거 메타데이터에 포함됩니다.


Remote Config Setup

// src/app/firebase/remote-config.ts
import { initializeApp, FirebaseApp } from 'firebase/app';
import { getRemoteConfig, RemoteConfig } from 'firebase/remote-config';

function createRemoteConfig(firebaseApp: FirebaseApp): RemoteConfig {
  const remoteConfig = getRemoteConfig(firebaseApp);

  // Default values for Remote Config parameters.
  remoteConfig.defaultConfig = {
    geminiModelName: 'gemini-3-pro-preview',
    vertexAILocation: 'global',
    includeThoughts: true,
    thinkingBudget: 512,
  };

  return remoteConfig;
}

Configuration Parameters

ParameterDescriptionDefault Value
geminiModelNameGemini 모델 식별자gemini-3-pro-preview
vertexAILocationVertex AI 위치 (예: global)global
includeThoughts모델이 추론 과정을 생성할지 여부true
thinkingBudget추론 단계에 할당된 토큰 예산512

앱이 원격 값을 가져오지 못하면 이 기본값을 사용합니다.


Firebase Bootstrap

// src/app/firebase/bootstrap.ts
import { inject } from '@angular/core';
import { initializeApp } from 'firebase/app';
import { initializeAppCheck, ReCaptchaEnterpriseProvider } from 'firebase/app-check';
import { fetchAndActivate } from 'firebase/remote-config';
import { ConfigService } from './config.service';
import { firebaseConfig } from '../environments/environment';
import { createRemoteConfig } from './remote-config';

export async function bootstrapFirebase(): Promise {
  try {
    const configService = inject(ConfigService);
    const firebaseApp = initializeApp(firebaseConfig.app);

    // Protect the AI endpoint from abuse.
    initializeAppCheck(firebaseApp, {
      provider: new ReCaptchaEnterpriseProvider(firebaseConfig.recaptchaEnterpriseSiteKey),
      isTokenAutoRefreshEnabled: true,
    });

    const remoteConfig = createRemoteConfig(firebaseApp);
    await fetchAndActivate(remoteConfig);

    configService.loadConfig(firebaseApp, remoteConfig);
  } catch (err) {
    console.error('Remote Config fetch failed', err);
    throw err;
  }
}

Application Initialization

// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideAppInitializer } from '@angular/core';
import { AppComponent } from './app/app.component';
import { ApplicationConfig } from '@angular/core';
import { bootstrapFirebase } from './app/firebase/bootstrap';

const appConfig: ApplicationConfig = {
  providers: [
    provideAppInitializer(() => bootstrapFirebase()),
  ],
};

bootstrapApplication(AppComponent, appConfig)
  .catch(err => console.error(err));

Image Analysis Schema

// src/app/models/image-analysis.schema.ts
import { Schema } from '@angular/fire/ai';

export const ImageAnalysisSchema = Schema.object({
  properties: {
    tags: Schema.array({ items: Schema.string() }),
    alternativeText: Schema.string(),
    recommendations: Schema.array({
      items: Schema.object({
        properties: {
          id: Schema.integer(),
          text: Schema.string(),
          reason: Schema.string(),
        },
      }),
    }),
    fact: Schema.string(),
  },
});

Generative AI Model Setup

// src/app/firebase/ai-model.ts
import { inject } from '@angular/core';
import { getAI, getGenerativeModel, VertexAIBackend } from '@angular/fire/ai';
import { RemoteConfig, getValue } from 'firebase/remote-config';
import { FirebaseApp } from 'firebase/app';
import { ImageAnalysisSchema } from '../models/image-analysis.schema';
import { AI_MODEL } from '../tokens';

function getGenerativeAIModel(firebaseApp: FirebaseApp, remoteConfig: RemoteConfig) {
  const model = getValue(remoteConfig, 'geminiModelName').asString();
  const vertexAILocation = getValue(remoteConfig, 'vertexAILocation').asString();
  const includeThoughts = getValue(remoteConfig, 'includeThoughts').asBoolean();
  const thinkingBudget = getValue(remoteConfig, 'thinkingBudget').asNumber();

  const ai = getAI(firebaseApp, { backend: new VertexAIBackend(vertexAILocation) });

  return getGenerativeModel(ai, {
    model,
    generationConfig: {
      responseMimeType: 'application/json',
      responseSchema: ImageAnalysisSchema,
      thinkingConfig: {
        includeThoughts,
        thinkingBudget,
      },
    },
    tools: [{ googleSearch: {} }],
  });
}

Provider for Dependency Injection

// src/app/firebase/ai-provider.ts
import { makeEnvironmentProviders, inject } from '@angular/core';
import { ConfigService } from './config.service';
import { AI_MODEL } from '../tokens';
import { getGenerativeAIModel } from './ai-model';

export function provideFirebase() {
  return makeEnvironmentProviders([
    {
      provide: AI_MODEL,
      useFactory: () => {
        const configService = inject(ConfigService);

        if (!configService.remoteConfig) {
          throw new Error('Remote config does not exist.');
        }
        if (!configService.firebaseApp) {
          throw new Error('Firebase App does not exist');
        }

        return getGenerativeAIModel(configService.firebaseApp, configService.remoteConfig);
      },
    },
  ]);
}

앱 설정에 프로바이더를 추가합니다:

// src/main.ts (excerpt)
import { provideFirebase } from './app/firebase/ai-provider';

const appConfig: ApplicationConfig = {
  providers: [
    provideAppInitializer(() => bootstrapFirebase()),
    provideFirebase(),
  ],
};

Angular Service for Alt‑Text Generation

// src/app/services/firebase.service.ts
import { Injectable, inject } from '@angular/core';
import { AI_MODEL } from '../tokens';
import { fileToGenerativePart } from '@angular/fire/ai';
import { ImageAnalysisResponse } from '../models/image-analysis.model';

@Injectable({ providedIn: 'root' })
export class FirebaseService {
  private aiModel = inject(AI_MODEL);

  async generateAltText(image: File): Promise {
    const imagePart = await fileToGenerativePart(image);

    const prompt = `
You are asked to perform four tasks:
1. Generate an alternative text for the image (max 125 characters).
2. Generate at least 3 tags describing the image.
3. Based on the alternative text and tags, suggest ways to make the image more interesting.
4. Search for a surprising or obscure fact that interconnects the tags. If no direct link exists, find a conceptual link.
`;

    const result = await this.aiModel.generateContent({
      contents: [{ role: 'user', parts: [{ text: prompt }, imagePart] }],
    });

    if (result?.response) {
      const response = result.response;
      const thought = response.thoughtSummary?.() ?? '';
      const rawText = response.text?.() ?? '';

      // Strip possible markdown fences.
      const cleaned = rawText.replace(/```json\s*|```/g, '').trim();

      const parsed = JSON.parse(cleaned) as ImageAnalysisResponse;
      const tokenUsage = this.getTokenUsage(response.usageMetadata);

      return {
        ...parsed,
        thought,
        tokenUsage,
      };
    }

    throw new Error('No response from Gemini model');
  }

  private getTokenUsage(usage: any) {
    // Implementation omitted for brevity – extracts input/output token counts.
    return usage;
  }
}

서비스는 다음을 수행합니다:

  1. 업로드된 이미지를 Vertex AI가 받아들일 수 있는 형식으로 변환합니다.
  2. Gemini 3에게 alt 텍스트, 태그, 권장 사항, 사실을 생성하도록 지시하는 다단계 프롬프트를 전송합니다.
  3. JSON 응답을 파싱하고 “thought” 요약을 추출한 뒤 토큰 사용 메타데이터와 함께 반환합니다.
Back to Blog

관련 글

더 보기 »