오늘은 React Native Expo 환경에서 OpenAI의 Vision API를 사용하는 방법을 정리해보겠습니다. 기본적으로 Text Generation API를 사용해본 경험이 있어서 Vision API를 사용해보는 데 도전했습니다.
이 글에서는 이미지와 텍스트를 분석하는 기능을 구현하는 과정을 단계별로 설명합니다.
1. Vision API란?
OpenAI의 Vision API는 이미지 분석과 관련된 기능을 제공합니다. 텍스트와 이미지를 조합하여 더욱 정교한 분석을 수행할 수 있습니다. 공식 문서 링크는 여기를 참고하세요.
2. 환경 설정
React Native 프로젝트는 Expo 기반으로 설정하였으며, Node.js 환경에서 OpenAI와 Expo 라이브러리를 활용합니다.
주요 라이브러리:
- openai: OpenAI API와 통신
- axios: 이미지 URL을 Base64 형식으로 변환
- expo-image-picker: 사용자가 로컬 이미지 파일을 선택
Vision doc link
https://platform.openai.com/docs/guides/vision
3. 코드 구성
3.1 Vision API 요청 (vision.ts)
OpenAI Vision API와 통신하기 위한 코드를 작성합니다. 이미지 URL을 직접 전달하거나 Base64로 변환하여 분석 요청을 보냅니다.
import OpenAI from "openai"; // OpenAI SDK 임포트
import axios from "axios"; // axios는 이미지 URL을 base64로 변환하는 데 사용됩니다
// OpenAI 클라이언트 설정
const openai = new OpenAI({
apiKey: 'sk-aBZrn2L_j_E85kjuZNYOyAxr0EUfc16p4v2MZZa5iKT3BlbkFJzlThgWwrJELGTKQOh-9mmDB5hMBReLJGzVjTSuf3cA', // API 키 입력
});
// 이미지 URL과 텍스트를 사용한 요청
export const analyzeImage = async (imageUrl: string, inputText: string) => {
try {
// OpenAI API 호출
const response = await openai.chat.completions.create({
model: "gpt-4o-mini", // 사용하려는 모델
messages: [
{
role: "user",
content: inputText, // 텍스트 메시지
},
{
role: "user",
content: [
{
type: "text",
text: inputText,
},
{
type: "image_url", // 이미지 URL을 사용하도록 설정
image_url: { url: testimageurl }, // 이미지 URL
},
],
},
],
});
return response.choices[0].message.content; // 분석된 결과 반환
} catch (error) {
console.error("Error analyzing image:", error);
return "이미지 분석 실패"; // 오류 메시지 반환
}
};
{하지만 실제로 로컬 이미지를 분석하려면 base 64로 변환해야 하기 때문에 비용이 비싸집니다.}
2. ImagePicker.ts
import * as ImagePicker from 'expo-image-picker';
// 이미지 선택 함수
export const pickImage = async (): Promise<string | null> => {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
alert('이미지 접근 권한이 필요합니다.');
return null;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4, 3],
quality: 1,
});
// 선택이 취소된 경우
if (result.canceled) {
return null; // 사용자가 이미지를 선택하지 않았을 때
}
// result.assets 배열에서 첫 번째 이미지 선택
if (result.assets && result.assets.length > 0) {
return result.assets[0].uri || null; // 첫 번째 이미지 URI 반환
}
return null; // 'uri'가 존재하지 않으면 null 반환
};
사용자가 로컬 기기에서 이미지를 선택할 수 있도록 Expo의 ImagePicker 라이브러리를 사용합니다.
3.index.ts ( 어플 진입점)
import React, { useState } from "react";
import { Image, StyleSheet, Text, View, ScrollView } from "react-native";
import ParallaxScrollView from "@/components/ParallaxScrollView";
import { Box } from "@/components/ui/box";
import { Textarea, TextareaInput } from "@/components/ui/textarea";
import { Button, ButtonText } from "@/components/ui/button";
import { pickImage } from "@/src/service/ImagePicker"; // 이미지 선택 함수 import
import { analyzeImage } from "@/src/api/vision"; // 이미지 분석 함수 import
export default function HomeScreen() {
const [inputText, setInputText] = useState(""); // 입력 텍스트 상태
const [imageUri, setImageUri] = useState<string | null>(null); // 선택된 이미지 URI 상태
const [responseText, setResponseText] = useState<string | null>(null); // 분석 결과 상태
// 이미지 선택 함수
const handleImageSelect = async () => {
const selectedImageUri = await pickImage(); // pickImage 호출
if (selectedImageUri) {
setImageUri(selectedImageUri); // 선택된 이미지 URI 상태에 저장
}
};
// 텍스트 입력 및 이미지 분석 요청 함수
const handleAnalyze = async () => {
if (!imageUri || !inputText) {
setResponseText("이미지와 텍스트를 모두 입력해야 합니다.");
return;
}
// 텍스트와 이미지를 분석 함수에 전달
const result = await analyzeImage(imageUri, inputText);
setResponseText(result); // 분석된 결과 상태에 저장
};
return (
<ParallaxScrollView
headerBackgroundColor={{ light: "#A1CEDC", dark: "#1D3D47" }}
headerImage={
<Image
source={imageUri ? { uri: imageUri } : require("@/assets/images/partial-react-logo.png")}
style={styles.reactLogo}
/>
}
>
{/* 응답 결과를 표시할 박스 컴포넌트 */}
<Box className="bg-gray-200 p-4 rounded-lg flex-1 mt-4">
<ScrollView contentContainerStyle={{ paddingBottom: 20 }}>
{responseText && <Text style={styles.responseAnswer}>{responseText}</Text>}
</ScrollView>
</Box>
{/* 텍스트 입력 필드 */}
<Textarea
size="xl"
isReadOnly={false}
isInvalid={false}
isDisabled={false}
className="w-128 mt-4"
>
<TextareaInput
placeholder="질문을 입력하세요..."
value={inputText}
onChangeText={(text) => setInputText(text)} // 입력 값 업데이트
/>
</Textarea>
{/* 버튼 컴포넌트 - 이미지 선택 버튼 */}
<Button size="md" variant="outline" action="primary" onPress={handleImageSelect}>
<ButtonText>사진 선택</ButtonText>
</Button>
{/* 분석 버튼 */}
<Button size="md" variant="outline" action="primary" onPress={handleAnalyze}>
<ButtonText>분석하기</ButtonText>
</Button>
</ParallaxScrollView>
);
}
const styles = StyleSheet.create({
reactLogo: {
height: 178,
width: 290,
bottom: 0,
left: 0,
position: "absolute",
},
responseAnswer: {
fontSize: 16,
fontWeight: "bold",
color: "#333",
},
responseSource: {
fontSize: 14,
color: "#666",
},
selectedImage: {
width: 200,
height: 200,
marginBottom: 20,
borderRadius: 10,
alignSelf: "center",
},
});
간단히 구성된 홈스크린입니다. 여기서 원래 동작이라면 User 가 사진선택 누르면
이렇게 선택 하고
이렇게 뜨는게 동작 의도인대요 원래는 다 채워야되는데 일단 놔둘게요
그 다음 준비한 테스트 이미지 링크를 넣고 요구사항을 넣어줍니다.
제가 준비한 이미지는 이건데요 이 사진 링크를 Vision.ts 에 넣어주고
위에 텍스트 추출 해달라고 해봤습니다.
보이시나요 텍스트 꽤 잘하죠? 제가 많은 OCR 사용해본 결과 한국어 이정도 정확성 가진 OCR이 많이 없는데
간단하고 쉽게 API 사용하면서 구성이 되네요 여기서 추가로 추출된 텍스트로도 여러가지 응용이 가능하니
확실히 GPT APi 편하긴 합니다 . 하지만 큰 단점이 있는데요. 이렇게 이미지 보낼 때는 비용이 비쌉니다
보이시나요 110,574 token 3번인가 4번 요청 보냈는데 토큰이 이렇게 빠져나갑니다 .
가격이 너무 비싸요
GPT-4o 비전 모델:
- 가격: 이미지 처리에 대해 1K 입력 토큰당 0.00125달러입니다.
- 이 경우, 이미지 입력을 토큰 단위로 처리합니다.
- 즉, 이미지 크기와 세부 설정에 따라 입력 토큰의 수가 달라지며, 가격은 토큰 수에 비례해서 발생합니다.
GPT-4o mini 모델:
- 가격: 이미지 처리에 대해서는 이미지 한 장당 0.001275달러입니다.
- 이 모델은 이미지 처리에 대해 단일 요금을 부과합니다. 즉, 이미지의 크기나 토큰 수와 관계없이 매번 동일한 요금이 부과됩니다.
mini 로 해도 100번만 돌려도 0.1275 달러네요
mini 안하면 방금 11만 토큰이니까 0.1375 달러 사용한거네요