최근 취준생을 위한 AI 자소서 프롬프트 생성 서비스 Poromy AI를 개발하면서 흥미로운 기술적 도전에 직면했습니다. 우리 서비스는 사용자가 공유한 채용공고 URL을 분석하여 해당 기업의 로고, 채용 페이지 미리보기 이미지 등 다양한 외부 이미지를 표시해야 했습니다. 하지만 Next.js의 보안 정책으로 인해 예상치 못한 문제가 발생했죠.
이번 포스팅에서는 Next.js의 외부 도메인 이미지 제한 문제를 이미지 프록시 API로 해결한 경험을 공유하고자 합니다. 특히 채용 플랫폼 특성상 수백 개의 기업 도메인을 다뤄야 했던 상황에서 어떻게 유연하고 확장 가능한 솔루션을 구현했는지 상세히 다루겠습니다.
문제 상황: 채용 플랫폼의 특수한 요구사항
Poromy AI는 취준생들이 채용공고를 분석하여 맞춤형 자기소개서를 작성할 수 있도록 돕는 AI 프롬프트 생성 서비스입니다. 서비스의 핵심 기능 중 하나는 사용자가 관심 있는 채용공고 URL을 공유하면, 해당 페이지의 메타데이터를 추출하여 시각적으로 매력적인 문의 카드를 생성하는 것입니다.
예를 들어, 사용자가 "https://career.kakao.com/jobs/12345" 같은 채용공고 URL을 공유하면, 우리 서비스는 다음과 같은 정보를 추출하여 표시합니다:
- 카카오 기업 로고 이미지
- 채용공고 대표 이미지
- Open Graph 이미지
- 직무 관련 아이콘
Next.js의 외부 도메인 제한
하지만 외부 사이트의 채용공고 URL에서 이미지를 추출하여 사용하는 과정에서 다음과 같은 오류를 수없이 마주했습니다:
Error: Invalid src prop (https://career.kakao.com/logo.png) on `next/image`,
hostname "career.kakao.com" is not configured under images in your `next.config.js`
Next.js는 보안상의 이유로 next.config.js에 명시적으로 허용된 도메인의 이미지만 로드할 수 있도록 제한합니다. 일반적인 해결책은 다음과 같이 도메인을 추가하는 것입니다:
// next.config.js
module.exports = {
images: {
domains: [
'career.kakao.com',
'recruit.navercorp.com',
'careers.samsung.com',
// 수백 개의 기업 도메인을 일일이 추가해야 함...
],
},
}
하지만 Poromy 서비스는 일반적인 웹 애플리케이션과 달리 다음과 같은 특수한 요구사항이 있었습니다:
- 예측 불가능한 도메인: 사용자가 어떤 기업의 채용공고를 공유할지 예측할 수 없습니다. 대기업부터 스타트업까지, 국내외 수천 개의 기업 채용 페이지를 다뤄야 했습니다.
- 실시간 대응 필요: 새로운 기업이 채용을 시작할 때마다 도메인을 추가하고 재배포하는 것은 현실적으로 불가능했습니다.
- 메타데이터 기반 동작: 단순히 이미지를 표시하는 것이 아니라, Open Graph 태그에서 추출한 이미지를 동적으로 표시해야 했습니다.
- 사용자 경험: 사용자가 URL을 공유했을 때 즉시 미리보기가 생성되어야 했습니다.
이제부터 위의 요구사항을 만족하기 위해 도입한 이미지 프록시 솔루션을 소개하겠습니다.
해결책: 이미지 프록시 API 구현
위의 요구사항을 모두 충족하기 위해 Next.js의 API 라우트를 활용한 이미지 프록시 솔루션을 설계 및 도입하였습니다. 모든 외부 이미지를 우리 서버를 통해 중계함으로써 도메인 제한을 우회하면서도 보안을 유지할 수 있었습니다.
다음은 실제 구현한 이미지 프록시 API의 코드입니다:
// app/api/image-proxy/route.ts
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const imageUrl = searchParams.get('url')
if (!imageUrl) {
return new NextResponse('Image URL is required', { status: 400 })
}
try {
// 보안: 허용된 프로토콜만 처리
const url = new URL(imageUrl)
if (!['http:', 'https:'].includes(url.protocol)) {
return new NextResponse('Invalid protocol', { status: 400 })
}
// 이미지 가져오기
const response = await fetch(imageUrl, {
headers: {
// 일부 서버는 User-Agent를 확인합니다
'User-Agent': 'Mozilla/5.0 (compatible; PoromyBot/1.0)',
},
})
if (!response.ok) {
return new NextResponse('Failed to fetch image', {
status: response.status,
})
}
// 이미지 데이터와 Content-Type 헤더 가져오기
const imageBuffer = await response.arrayBuffer()
const contentType = response.headers.get('content-type') || 'image/jpeg'
// 이미지 타입 검증
if (!contentType.startsWith('image/')) {
return new NextResponse('Invalid content type', { status: 400 })
}
// 이미지를 그대로 반환
return new NextResponse(imageBuffer, {
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=3600, s-maxage=86400',
'X-Content-Type-Options': 'nosniff',
},
})
} catch (error) {
console.error('Error proxying image:', error)
return new NextResponse('Error proxying image', { status: 500 })
}
}
그리고 프록시 URL을 쉽게 생성하기 위한 헬퍼 함수를 만들었습니다:
// lib/utils/image-proxy.ts
export const getProxyImageUrl = (originalUrl: string): string => {
if (!originalUrl) return '/images/placeholder.png'
// 이미 프록시된 URL인지 확인
if (originalUrl.startsWith('/api/image-proxy')) {
return originalUrl
}
// 상대 경로는 그대로 반환
if (originalUrl.startsWith('/')) {
return originalUrl
}
return `/api/image-proxy?url=${encodeURIComponent(originalUrl)}`
}
그리고 실제로 채용공고 문의 카드에선 이미지 프록시를 다음과 같이 사용했습니다:
// components/inquiry/InquiryCard.tsx
import Image from 'next/image'
import { getProxyImageUrl } from '@/lib/utils/image-proxy'
interface InquiryCardProps {
inquiry: {
id: string
url: string
metadata: {
title: string
description: string
image: string
favicon: string
siteName: string
}
}
}
export default function InquiryCard({ inquiry }: InquiryCardProps) {
const { metadata } = inquiry
return (
<div className="border rounded-lg overflow-hidden hover:shadow-lg transition-shadow">
{/* 채용공고 대표 이미지 */}
{metadata.image && (
<div className="relative h-48 w-full">
<Image
src={getProxyImageUrl(metadata.image)}
alt={metadata.title}
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
)}
...
</div>
)
}
실제 적용 결과 및 성과
이미지 프록시 API를 구현한 후 Poromy AI 서비스에서 얻은 성과를 정리하면 다음과 같습니다.
우선 이런 점은 좋았어요:
1. 개발 효율성 향상
도메인 제한으로 인한 개발 중단이 사라졌습니다. 이전에는 새로운 기업의 채용공고를 테스트할 때마다 next.config.js를 수정하고 서버를 재시작해야 했지만, 이제는 어떤 URL이든 즉시 테스트할 수 있게 되었습니다.
2. 사용자 경험 개선
사용자가 공유한 채용공고를 즉시 시각적으로 표시할 수 있게 되었습니다. 카카오, 네이버, 쿠팡 등 대기업부터 스타트업까지 모든 기업의 채용공고를 동일하게 지원할 수 있게 되었습니다.
3. 운영 부담 감소
새로운 도메인 추가를 위한 배포가 불필요해졌습니다. 이는 특히 채용 시즌에 새로운 기업들이 대거 채용을 시작할 때 큰 이점이 되었습니다.
이처럼 이미지 프록시 접근법은 유용했지만 몇 가지 트레이드오프가 있었습니다:
1. 서버 부하 증가
모든 이미지 요청이 서버를 거치므로 트래픽이 증가합니다. 향후 적극적인 캐싱 전략과 CDN 활용으로 완화할 수 있지만, 현재로서는 부하의 수준이 낮다고 판단하여 적용하지 않았습니다.
2. 지연 시간
첫 번째 요청에서는 프록시 서버가 원본 이미지를 가져오는 추가 네트워크 홉이 발생합니다.
3. CORS 및 방화벽 이슈
일부 기업 서버는 봇 접근을 차단하거나 특정 User-Agent만 허용합니다. 이를 위해 적절한 User-Agent 설정과 예외 처리를 해야합니다.
마치며
지금까지 이미지 프록시 API를 활용하여 Next.js의 도메인 제한이라는 기술적 제약을 해결한 사례를 공유했습니다. 특히 Poromy 서비스처럼 예측할 수 없는 다양한 외부 리소스를 다뤄야 하는 서비스에서는 고려해볼 만한 솔루션이라고 생각됩니다.
Poromy AI를 개발하면서 배운 가장 중요한 교훈은 프레임워크의 제약사항을 단순히 받아들이는 것이 아니라, 서비스의 특성에 맞게 해결할 수 있다는 것입니다. Next.js가 제공하는 API 라우트와 같은 강력한 기능을 활용하면, 보안과 유연성을 모두 만족시키는 솔루션을 구현할 수 있습니다.
이 접근법이 비슷한 문제를 겪고 있는 개발자들에게 도움이 되길 바랍니다. 특히 다양한 외부 콘텐츠를 다루는 플랫폼을 개발하신다면, 이미지 프록시 API를 활용한 해결법을 고민해보시는 것을 추천드리겠습니다.
읽어주셔서 감사합니다.
참고
Poromy - GPT/Claude AI 자소서 프롬프트 아카이브
ChatGPT, Claude 등 AI 모델을 활용한 자소서 작성, 기업 분석, 채용 공고 분석을 위한 최고의 AI 프롬프트 아카이브
poromy.ai.kr
'Troble Shooting' 카테고리의 다른 글
React Query의 Query Key 효율적으로 관리해보자(feat. @lukemorales/query-key-factory) (4) | 2025.06.01 |
---|---|
Next.js 15에서 TypeScript 파일과 next-sitemap 통합하기 (0) | 2025.06.01 |
Next.js 15 성능 최적화 실전기 - Chrome DevTools로 찾아낸 숨겨진 병목들 (0) | 2025.05.31 |