SNS ํ๋กํ ์ฌ์ง ํ๋๋ฅผ ๋ฐ๊พธ๋ ๊ฒ๋ง์ผ๋ก๋ ํผ๋ ๋ถ์๊ธฐ๊ฐ ๋ฌ๋ผ์ง๋ค. ๊ทธ๋ฐ๋ฐ ๋ง์ ๊ท์ฌ์ด ํ๋กํ ์ด๋ฏธ์ง๋ฅผ ์ง์ ๋ง๋ค๋ ค๋ฉด ๋์์ธ ํด์ ์ด์ด์ผ ํ๊ณ , ์์ดํจ๋๊ฐ ์์ด์ผ ํ๊ณ , ์๊ฐ์ด ํ์ํ๋ค. ๊ทธ๋ฅ ๋ด ์ผ๊ตด์ ๋ฃ์ผ๋ฉด ์์์ ๊ท์ฌ์ด ์บ๋ฆญํฐ๋ก ๋ฐ๊ฟ์คฌ์ผ๋ฉด ์ข๊ฒ ๋ค๋ ์๊ฐ์ด ๊ณ์ ๋ค์๋ค.
๋ง์นจ AI ์ด๋ฏธ์ง ์์ฑ ๋ชจ๋ธ์ด ๋น์ฝ์ ์ผ๋ก ๋ฐ์ ํ๋ฉด์ "์ด๊ฒ ์ง๊ธ ๊ฐ๋ฅํ๊ฒ ๋ค"๋ ํ์ด๋ฐ์ด ์๋ค. ๋จ์ํ ์ฌ์ง์ ์นดํฐํํ๋ ์๋น์ค๋ ์ด๋ฏธ ๋ง๋ค. ์ฐจ๋ณํ ํฌ์ธํธ๊ฐ ํ์ํ๋ค. ๊ทธ๋ฌ๋ค "๋๋ฌผ์"์ด๋ผ๋ ํค์๋๊ฐ ๋ ์ฌ๋๋ค.
"๊ณ ์์ด์์ด์์", "๊ฐ์์ง์์ด์์" โ ์ฐ๋ฆฌ๊ฐ ์ฌ๋ ์ธ์์ ๋ฌ์ฌํ ๋ ์ฐ๋ ์ด ํํ์ ๊ทธ๋๋ก ๊ธฐ๋ํผ๊ทธ ์บ๋ฆญํฐ์ ๋ น์ฌ์ฃผ๋ฉด ์ด๋จ๊น? ๋จ์ํ ์นดํฐ ๋ณํ์ด ์๋๋ผ, "๋ด ๋๋์ด ์ด์์๋ ๊ธฐ๋ํผ๊ทธ ํ๋กํ" ์ ๋ง๋ค์ด์ฃผ๋ ์๋น์ค. ๊ทธ๊ฒ์ด Ginini์ ์ถ๋ฐ์ ์ด์๋ค.
์ด ๊ธ์ Ginini๋ฅผ ๊ธฐํํ๊ณ ๊ฐ๋ฐํ๋ฉด์ ๊ฒช์ ๊ธฐ์ ์ ์์ฌ๊ฒฐ์ ๋ค, ํนํ AI ํ์ดํ๋ผ์ธ ์ค๊ณ์ ํ๋กฌํํธ ์์ง๋์ด๋ง ๊ณผ์ ์ ์ ๋ฆฌํ ๊ธฐ๋ก์ด๋ค.
Ginini๋ ์ฌ์ฉ์๊ฐ ์์ ์ ์ฌ์ง์ ์ ๋ก๋ํ๋ฉด AI๊ฐ ์ผ๊ตด ํน์ง์ ๋ถ์ํ๊ณ , ์ ํํ ๋๋ฌผ์(๊ณ ์์ด์, ๊ฐ์์ง์ ๋ฑ)์ ๋ฐ์ํ ๊ท์ฌ์ด ๊ธฐ๋ํผ๊ทธ ํ๋กํ ์บ๋ฆญํฐ๋ฅผ ์์ฑํด์ฃผ๋ ์น ์๋น์ค๋ค.
ํ๋ก์ฐ๋ ๊ฐ๋จํ๋ค.
์ฌ์ง ์
๋ก๋ โ ์ํ ํฌ๋กญ โ ์ฑ๋ณ + ๋๋ฌผ์ ์ ํ โ AI ๋ณํ โ ๊ฒฐ๊ณผ ํ์ธ/๊ณต์
ํต์ฌ ํฌ์ง์ ๋์ "์ ํํ ๋ณํ"์ด ์๋ "ํน์ง์ ๋ฐ์ํ ๊ธฐ๋ํผ๊ทธ ํ๋กํ ์บ๋ฆญํฐ ์์ฑ๊ธฐ" ๋ค. ์๋ฒฝํ๊ฒ ๋๊ฐ์ด ๋ง๋ค๋ ค๋ ์๋๋ณด๋ค, ๋ด ๋ถ์๊ธฐ์ ์ธ์์ด ๋ด๊ธด ๊ท์ฌ์ด ์บ๋ฆญํฐ๋ฅผ ๋ง๋๋ ๊ฒ์ด ๋ชฉํ์๋ค.
| ํญ๋ชฉ | ์ ํ |
| ---------------- | ------------------------------------------ |
| Framework | Next.js 16 (App Router) |
| Language | TypeScript |
| Styling | Tailwind CSS v3 |
| State Management | Zustand |
| Data Fetching | TanStack Query |
| Architecture | Feature-Sliced Design (FSD) |
| AI ๋ถ์ | Google Gemini 2.5 Flash Lite |
| AI ์์ฑ | Replicate โ black-forest-labs/flux-2-pro |
| Rate Limit | Upstash Redis |
| ์ด๋ฏธ์ง ์ ์ฅ | Vercel Blob |
| ๋ฐฐํฌ | Vercel |
ํ๋ก์ ํธ๊ฐ ์์๋ ์ฒ์๋ถํฐ ๊ตฌ์กฐ๋ฅผ ์ก๊ณ ์์ํ๋ค. ์ ํํ ๋ฐฉ์์ FSD(Feature-Sliced Design) ์ด๋ค.
FSD๋ ๋ ์ด์ด๋ฅผ app โ views โ widgets โ features โ entities โ shared ์์ผ๋ก ๋ช
ํํ๊ฒ ๋๋๊ณ , ์์ ๋ ์ด์ด๋ง ํ์ ๋ ์ด์ด๋ฅผ ์ฐธ์กฐํ ์ ์๋ค๋ ๋จ๋ฐฉํฅ ์์กด์ฑ ๊ท์น์ ๊ฐ๋๋ค. ๋๋ถ์ "์ด ์ฝ๋๋ ์ด๋์ ์์ด์ผ ํ์ง?"๋ผ๋ ์ง๋ฌธ์ ๋ต์ด ํญ์ ๋ช
ํํ๋ค.
src/
โโโ app/ # ๋ผ์ฐํ
+ API Routes (๋ฐฑ์๋ ํ๋ก์)
โโโ views/ # ๋ผ์ฐํธ๋ณ ํ๋ฉด ์กฐ๋ฆฝ
โโโ widgets/ # ๋
๋ฆฝ์ ์ธ UI ๋ธ๋ก (ConverterWidget ๋ฑ)
โโโ features/ # ์ฌ์ฉ์ ์ํธ์์ฉ ๋จ์ (convert-to-guinea, image-upload)
โโโ entities/ # ๋น์ฆ๋์ค ๋๋ฉ์ธ ๋ชจ๋ธ (image-session Zustand Store)
โโโ shared/ # ๊ณต์ฉ ์ปดํฌ๋ํธ, ์ ํธ, API ํด๋ผ์ด์ธํธ
์ค๋ฐ๋ถ์ ๋ฆฌํฉํ ๋ง์ ๊ฑฐ์น๋ฉด์ route.ts์ ๋ชฐ๋ ค์๋ ๋ก์ง์ ๊ฐ ๋ ์ด์ด๋ก ๋ถ๋ฆฌํ๋ค. analyzeFace, buildPrompt, generateImage, persistResult๊ฐ ๊ฐ๊ฐ ๋
๋ฆฝ ๋ชจ๋๋ก ๋๋์๊ณ , route.ts๋ ์ด๋ค์ ์ค์ผ์คํธ๋ ์ด์
ํ๋ ์์ ๋ ์ด์ด๋ก ๋จ์๋ค.
์ด๊ธฐ ์ค๊ณ๋ flux-kontext-pro ๋ชจ๋ธ์ ์ฌ์ฉํ img2img ๋ฐฉ์์ด์๋ค. ์ฌ์ง์ ์ง์ ๋ชจ๋ธ์ ์
๋ ฅํด์ ๊ธฐ๋ํผ๊ทธ๋ก ๋ณํํ๋ ์์ด๋์ด์๋๋ฐ, ๊ฒฐ๊ณผ ํ์ง ํธ์ฐจ๊ฐ ๋๋ฌด ์ปธ๋ค. ๊ฐ์ ์ฌ์ง์ ๋ฃ์ด๋ ๊ฒฐ๊ณผ๊ฐ ๋ค์ญ๋ ์ญํ๊ณ , ์คํจ์จ๋ ๋์๋ค.
ํต์ฌ ๋ฌธ์ ๋ img2img ๋ฐฉ์์ด "์ฌ๋ ์ผ๊ตด"์ด๋ผ๋ ์ปจํ ์คํธ๋ฅผ ๊ทธ๋๋ก ๋ฐ์ ์ฒ๋ฆฌํ๋ค ๋ณด๋ ๋ชจ๋ธ์ด ๋ฌด์์ ๋ณด์กดํ๊ณ ๋ฌด์์ ๋ณํํด์ผ ํ๋์ง ๋ช ํํ ๊ฐ์ด๋๊ฐ ์๋ค๋ ๊ฒ์ด์๋ค.
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด 2๋จ๊ณ ํ์ดํ๋ผ์ธ์ผ๋ก ์ ํํ๋ค.
[1๋จ๊ณ] ๋ถ์: ์ฌ์ง โ Gemini Vision โ ๊ตฌ์กฐํ๋ ์ผ๊ตด ํน์ง(JSON)
[2๋จ๊ณ] ์์ฑ: JSON + ๊ธฐ๋ํผ๊ทธ ํ๋กฌํํธ + ๋๋ฌผ์ โ Flux 2 Pro โ ์ด๋ฏธ์ง
์ด๋ฏธ์ง๋ฅผ ์ง์ ๋ฃ๋ ๋์ , ๋จผ์ ์ผ๊ตด ํน์ง์ ํ ์คํธ๋ก ์ถ์ถํ๊ณ ๊ทธ ํ ์คํธ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ด๋ฏธ์ง๋ฅผ ์์ฑํ๋ ๋ฐฉ์์ด๋ค. ๋ชจ๋ธ์๊ฒ "์ฌ๋ ์ผ๊ตด์ ๋ณด๊ณ ๊ธฐ๋ํผ๊ทธ๋ก ๋ณํํด"๊ฐ ์๋๋ผ, "์ด ํน์ง๋ค์ ๊ฐ์ง ๊ธฐ๋ํผ๊ทธ๋ฅผ ๊ทธ๋ ค์ค"๋ผ๊ณ ๋งํ๋ ๊ฒ์ด๋ค.
1๋จ๊ณ์์๋ gemini-2.5-flash-lite ๋ชจ๋ธ์ ์ฌ์ง์ ์ธ๋ผ์ธ ๋ฐ์ดํฐ๋ก ๋๊ธฐ๊ณ , Structured Output(JSON Schema)์ผ๋ก ์ผ๊ตด ํน์ง์ ์ถ์ถํ๋ค.
์ถ์ถํ๋ ํ๋๋ ๋ค์๊ณผ ๊ฐ๋ค.
typescript
interface FaceFeatures {
faceSilhouette: string; // ์ผ๊ตดํ, ๋ณผ, ํฑ์
eyes: string; // ๋ ํฌ๊ธฐยทํํยท๊ธฐ์ธ๊ธฐยท์๊ฐ
eyebrows: string; // ๋์น ๋๊ปยท์์นยท๊ฐ๋
hairstyle: string; // ํค์ด์คํ์ผ ์ ์ฒด
hairArchitecture: string; // ๊ฐ๋ฅด๋งยทํ๋ฆยท๋ฑ
ยท๋ณผ๋ฅจ ๊ตฌ์กฐ
hairColor: string; // ํค์ด ์์ (๊ตฌ์ฒด์ ๋ช
์นญ)
noseMouth: string; // ์ฝยท์
์ธ์ (ํด๋ถํ์ ๋ฌ์ฌ ๊ธ์ง)
expression: string; // ์ ์ฒด์ ์ธ ๋ถ์๊ธฐ
accessories: string; // ์๊ฒฝยท๊ท๊ฑธ์ดยทํค์ดํ ๋ฑ
headPose: string; // ์นด๋ฉ๋ผ ๊ฐ๋, ๊ณ ๊ฐ ๋ฐฉํฅ
signatureFeatures: string[]; // ๋ฎ์์๋ฅผ ์ก์์ฃผ๋ ํต์ฌ ํน์ง 3๊ฐ์ง
likenessAnchor: string; // ๋ฎ์์ ํต์ฌ ์์ฝ ํ ๋ฌธ์ฅ
genderPresentation: string; // ์คํ์ผ๋ง ๊ธฐ์ค ์ฑ๋ณ ํํ
guineaPigTranslation: string; // ๊ธฐ๋ํผ๊ทธ๋ก ๋ฒ์ญํ๋ ๋ฐฉ๋ฒ๋ก
}
๋ถ์ ํ๋กฌํํธ์์ ํนํ ์ ๊ฒฝ ์ด ๋ถ๋ถ์ ๋ ๊ฐ์ง์๋ค.
์ฒซ์งธ, ์ ์ ์๋ณ ๋ฐฉ์ง๋ค. "์ด ์ฌ๋์ ๋ฌ์ฌํ๋ผ"๊ฐ ์๋๋ผ "์บ๋ฆญํฐ ์์ฑ์ ํ์ํ ์๊ฐ์ ํน์ง๋ง ์ถ์ถํ๋ผ"๋ ๋ฐฉํฅ์ ๋ช ํํ ํ๋ค. ๋์ด, ๋ฏผ์กฑ, ๊ตญ์ ๊ฐ์ ์ ๋ณด๋ ์ถ์ถํ์ง ์๋๋ก ํ๋กฌํํธ์์ ๋ช ์์ ์ผ๋ก ์ฐจ๋จํ๋ค.
๋์งธ, ๊ตฌ์ฒด์ ์ธ ์ธ์ด๋ฅผ ์ ๋ํ๋ ๊ฒ์ด๋ค. "brown eyes" ๋์ "warm hazel-brown eyes"์ฒ๋ผ, ์ด๋ฏธ์ง ์์ฑ์ ์ค์ ๋ก ๋์์ด ๋๋ ๋ฌ์ฌ๋ฅผ ์์ฒญํ๋ค. temperature: 0.1, topP: 0.1๋ก ์ถ๋ ฅ ๊ฒฐ์ ์ฑ์ ๋์ฌ ๊ฐ์ ์ฌ์ง์ ๋ํด ํญ์ ๋์ผํ ๋ถ์ ๊ฒฐ๊ณผ๊ฐ ๋์ค๋๋ก ํ๋ค.
๊ทธ๋ฆฌ๊ณ ๋ชจ๋ธ์ด ์ผ๊ตด์ ์ ๋๋ก ์ธ์ํ์ง ๋ชปํ์ ๋ ๊ทธ๋ฅ ๋น ๊ฐ์ด ์ค๋ ๊ฒ๋ณด๋ค ํ์คํ๊ฒ ํ์ง ์คํจ๋ฅผ ํ๋จํ๊ธฐ ์ํด ํต์ฌ ํ๋ 6๊ฐ ์ค 4๊ฐ ์ด์์ด "unclear" ๋๋ "not visible"์ด๋ฉด FaceNotDetectedError๋ฅผ ๋์ง๋๋ก ํ๋ค.
Gemini๋ ์ผ๊ตด ๋ถ์ ๊ฒฐ๊ณผ์ genderPresentation ํ๋๋ฅผ ํฌํจํ๋ค. ํค์ด์คํ์ผ, ๋ฉ์ดํฌ์
, ์คํ์ผ๋ง ๋จ์๋ฅผ ๊ธฐ๋ฐ์ผ๋ก "masculine", "feminine", "neutral" ์ธ ๊ฐ์ง ์ค ํ๋๋ฅผ ๋ฐํํ๋ค.
์ด ๊ฐ์ ์ด๋ฏธ์ง ์์ฑ ํ๋กฌํํธ์์ ์บ๋ฆญํฐ ์คํ์ผ๋ง ์ง์นจ์ผ๋ก ์ฐ์ธ๋ค.
typescript
const GENDER_STYLING_MAP = {
masculine: "Style the guinea pig with a masculine-leaning presentation:
no eyelashes, no lipstick or lip tint, no blush, and a simple
unisex hairstyle silhouette.",
feminine: "Style the guinea pig with a feminine-leaning presentation:
subtle eyelashes and soft natural blush are okay, kept cute
rather than heavily made up.",
neutral: "Keep the guinea pig's styling gender-neutral: no eyelashes,
no lipstick or lip tint, no gendered makeup cues.",
};
์ฒ์์ Gemini ๋ถ์ ๊ฒฐ๊ณผ๋ง ์ฌ์ฉํ๋๋ฐ, ๋ฌธ์ ๊ฐ ์๊ฒผ๋ค. ์ฌ์ง์์ ๋ถ์๋ genderPresentation์ด ์ฌ์ฉ์์ ์ค์ ์๋์ ๋ค๋ฅผ ์ ์๋ค๋ ๊ฒ์ด๋ค. ์๋ฅผ ๋ค์ด ์ค์ฑ์ ์ธ ์คํ์ผ์ ์ฌ์ฑ์ด ์ฌ์ฑ์ค๋ฌ์ด ๊ธฐ๋ํผ๊ทธ๋ฅผ ์ํ ์๋ ์๋ค.
ํด๊ฒฐ์ฑ
์ ์ฌ์ฉ์๊ฐ ์ง์ ์ ํํ ์ฑ๋ณ์ด Gemini ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ๋ฎ์ด์ฐ๋๋ก ํ๋ ๊ฒ์ด์๋ค. "๋จ์ / ์ฌ์" ์ ํ์ด ํ์ ์
๋ ฅ์ด ๋์๊ณ , ์ ํ๊ฐ์ route.ts์์ faceFeatures.genderPresentation์ ์ง์ ๊ต์ฒดํ๋ค.
typescript
if (USER_GENDER_VALUES.has(gender)) {
faceFeatures.genderPresentation = gender;
}
AI๊ฐ ๋ถ์ํ ๊ฒฐ๊ณผ๋ฅผ ์ฌ์ฉ์ ์๋๋ก ๋ณด์ ํ๋ ๋จ์ํ ํจํด์ด์ง๋ง, ๊ฒฐ๊ณผ๋ฌผ์ ๋ง์กฑ๋์ ์ง์ ์ ์ธ ์ํฅ์ ์ค๋ค.
์ด๋ฏธ์ง ์์ฑ ํ๋กฌํํธ๋ ์ด๊ธฐ์ ๋จ์ํ ๋จ๋ฝ ํํ๋ก ์์ํ๋ค. "Create a cute guinea pig character that looks like a person with [features]..." ์์ค์ด์๋ค. ๊ฒฐ๊ณผ๋ ์์ธก ๋ถ๊ฐ๋ฅํ๋ค.
๋ฐ๋ณต ํ ์คํธ๋ฅผ ํตํด ์ฌ๋ฌ ๋ฌธ์ ๋ฅผ ๋ฐ๊ฒฌํ๋ค.
๋ฌธ์ 1 โ ๋ชจํผ ์์ ์ค์ผ: ๊ธฐ๋ํผ๊ทธ ๋ชจํผ๊ฐ ๋ฐ์ ํฌ๋ฆผ์์ด์ด์ผ ํ๋๋ฐ, ์ธ๋ฌผ ์ฌ์ง์ ํผ๋ถํค์ ๋ฐ๋ผ๊ฐ๋ฉฐ tan, orange, brown ํจ์น๊ฐ ์๊ฒผ๋ค.
๋ฌธ์ 2 โ ๋จธ๋ฆฌ์ ๋ถ์์ : ํค์ด์ปฌ๋ฌ๊ฐ black โ dark brown โ ๊ฑฐ์ ํฐ์/๊ธ๋ฐ๊น์ง ํ๋ค๋ ธ๋ค. ๊ฐ์ ์ฌ์ง์ ์ ๋ ฅํด๋ ๊ฒฐ๊ณผ๋ง๋ค ๋จธ๋ฆฌ ์์์ด ๋ฌ๋๋ค.
๋ฌธ์ 3 โ ์ ๋ชจ์ ์ผ๊ด์ฑ: ์ ์ ๋ฒ๋ฆฌ๊ณ ํ๊ฐ ๋ณด์ด๋ ๋ณ์ข ์ด ๊ฐํ์ ์ผ๋ก ๋ฐ์ํ๋ค. ๊ธฐ๋ํผ๊ทธ ์บ๋ฆญํฐ๋ ํญ์ ๋จ์ ํ๊ฒ ์ ์ ๋ซ๊ณ ์์ด์ผ ํ๋๋ฐ.
์ด ๋ฌธ์ ๋ค์ ํด๊ฒฐํ๊ธฐ ์ํด ํ๋กฌํํธ๋ฅผ ๋ฌธ์ฅ ๋จ์์ ๋ช ์์ ์ง์นจ ๋ฐฐ์ด๋ก ์ฌ์ค๊ณํ๋ค. ๊ฐ ์ง์นจ์ด ํ๋์ ๊ตฌ์ฒด์ ์ธ ์ ์ฝ์ ๋ด๋นํ๊ณ , ์ค์ํ ์ ์ฝ์ ๋๋ถ๋ถ์์ ํ ๋ฒ ๋ ๋ฆฌ๋ง์ธ๋๋ก ๋ฐ๋ณตํ๋ ๊ตฌ์กฐ๋ค.
ํต์ฌ ํด๊ฒฐ์ฑ ์ ์ธ ๊ฐ์ง์๋ค.
// 1. ๋ชจํผ ์์: ๊ทยท๋ณผยท์ฃผ๋ฅ์ด๊น์ง ๋ช
์ํ๊ณ negative ํํ ์ถ๊ฐ
"Use a single uniform warm ivory or light cream fur color across the entire head,
including the ears, cheeks, muzzle, and chin fur. Do not add tan, orange, brown,
or two-tone patches anywhere on the fur or ears."
// 2. ๋จธ๋ฆฌ์: ๋ชจํผ ์์๊ณผ ๋ช
์์ ์ผ๋ก ๋ถ๋ฆฌ
"This hairstyle color (${features.hairColor}) is fixed and independent from the
guinea pig's fur color โ never lighten, whiten, or blend the hair toward the
cream fur tone."
// 3. ์
๋ชจ์: ํ์ฉํ์ง ์๋ ์ํ๋ฅผ ๊ตฌ์ฒด์ ์ผ๋ก ๋์ด
"keep the mouth fully closed in a curved guinea pig smile โ no open mouth,
no visible tongue, no wide laughing mouth"
๊ทธ๋ฆฌ๊ณ ํ๋กฌํํธ ๋๋ถ๋ถ์ ์ด ์ธ ๊ฐ์ง๋ฅผ ํ ๋ฒ ๋ ์์ฝํด์ ์ต์ปค๋งํ๋ค.
"Reminder: the entire fur โ including both ears โ must stay one uniform light
cream or ivory color, with no tan, orange, or brown patches. The hairstyle stays
in its own ${features.hairColor} color, separate from the fur. The mouth stays
fully closed, with no tongue visible."
๋์ผ ์ฌ์ง์ผ๋ก ๋ณด๊ฐ ์ ํ๋ฅผ ๋น๊ตํ์ ๋, ๋ณด๊ฐ ์ ์๋ 8ํ ์ํ์์ ๋จธ๋ฆฌ์์ด black~๊ฑฐ์ ํฐ์~๊ธ๋ฐ๊น์ง ๋ถ์ฐ๋์ง๋ง, ๋ณด๊ฐ ํ์๋ 2ํ ์ฐ์ black hair / ๊ท ์ผ cream fur / ๋ซํ ์ ์ผ๋ก ์์ ์ ์ผ๋ก ์ฌํ๋๋ค.
๋ํ ๋๋ฌผ์ ํธ๋ ์์ "๋ณด์กฐ ํํธ"๋ก๋ง ์๋ํ๋๋ก ์ค๊ณํ๋ค. ๊ณ ์์ด์์ด๋ผ๊ณ ํด์ ๊ฒฐ๊ณผ๋ฌผ์ด ๊ณ ์์ด์ฒ๋ผ ๋ณด์ฌ์๋ ์ ๋๋ค. ์ด๋๊น์ง๋ ๊ธฐ๋ํผ๊ทธ์ฌ์ผ ํ๊ณ , ๊ณ ์์ด ์ธ์์ ๋ ํํ์ ํ์ ์์๋ง ์ด์ง ๋๊ปด์ง๋ฉด ์ถฉ๋ถํ๋ค.
typescript
const traitPart = traitDescription
? ` Subtle animal-impression cue for the eyes, expression, and cheek/jaw shape
โ apply gently without overriding the primary likeness anchor: ${traitDescription}.`
: "";
์๋น์ค๋ฅผ ์ด์ํ๋ค ๋ณด๋ฉด ์ธ๋ถ API๊ฐ ์ผ์์ ์ผ๋ก ์๋ตํ์ง ์๋ ์ํฉ์ด ๋ฐ์ํ๋ค. Gemini API์์ 503(์๋น์ค ๋ถ๊ฐ)์ด๋ 429(๊ณผ๋ถํ) ์๋ต์ด ์ค๋ ๊ฒฝ์ฐ๊ฐ ์ค์ ๋ก ์์๋ค.
์ฒซ ์๋์์ ์ด๋ฐ ์ค๋ฅ๊ฐ ๋๋ฉด ์ฌ์ฉ์ ์ ์ฅ์์๋ ์๋ฌด ์๋ชป๋ ์๋๋ฐ ๋ณํ์ด ์คํจํ๋ค. ์ฌ์๋ ๋ก์ง์ ์ถ๊ฐํ๋ค.
typescript
for (let attempt = 0; ; attempt++) {
try {
response = await getGenAI().models.generateContent({ ... });
break;
} catch (error) {
if (attempt >= MAX_RETRIES || !isRetryableError(error)) {
throw error;
}
await sleep(RETRY_DELAY_MS * (attempt + 1)); // ์ ํ ์ง์ฐ (1๋ฐฐ โ 2๋ฐฐ โ 3๋ฐฐ)
}
}
503๊ณผ 429๋ง ์ฌ์๋ ๋์์ผ๋ก ํ์ ํ๋ค. ๋ค๋ฅธ ์ค๋ฅ(์๋ชป๋ ์ ๋ ฅ, ์ธ์ฆ ์คํจ ๋ฑ)๋ฅผ ์ฌ์๋ํ๋ฉด ์คํ๋ ค ๋ฌธ์ ๋ฅผ ์ ํ์ํฌ ์ ์๋ค.
ํ๊ฒฝ๋ณ์ ๋๋ฝ๋ ์ด๊ธฐ์ ๋ฌธ์ ์๋ค. ๋ฐฐํฌ ํ GEMINI_API_KEY๊ฐ ๋น ์ ธ์์ผ๋ฉด ์ฒซ ์์ฒญ ๋์์ผ ๋ฐํ์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค. getRequiredEnv ํฌํผ๋ก ๋น๋ ์์ ์ ํ๊ฒฝ๋ณ์๋ฅผ ๊ฒ์ฆํ๋๋ก ๋ฐ๊ฟจ๋ค.
typescript
export function getRequiredEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`ํ๊ฒฝ๋ณ์ ${name}์ด(๊ฐ) ์ค์ ๋์ง ์์์ต๋๋ค.`);
}
return value;
}
ํด๋ผ์ด์ธํธ ์ด๊ธฐํ ์์ ์ ์ด ํจ์๋ฅผ ํธ์ถํ๋ฏ๋ก, ๋น ์ง ํ๊ฒฝ๋ณ์๊ฐ ์์ผ๋ฉด ์๋ฒ๊ฐ ๋จ๋ฉด์ ๋ฐ๋ก ์ ์ ์๋ค.
Replicate๊ฐ ๋ฐํํ๋ URL์ ์ฝ 1์๊ฐ ํ ๋ง๋ฃ๋๋ค. SNS ๊ณต์ ๋ฅผ ํต์ฌ ๊ธฐ๋ฅ์ผ๋ก ์ก์ ์๋น์ค์์ ๊ณต์ ๋งํฌ๊ฐ 1์๊ฐ ๋ค์ ๊นจ์ง๋ฉด ์๋ฏธ๊ฐ ์๋ค.
ํด๊ฒฐ์ฑ
์ Vercel Blob์ ์ด์ฉํ ๊ฒฐ๊ณผ ์ด๋ฏธ์ง ์์ํ๋ค. ์์ฑ ์งํ Replicate ์์ URL์์ ์ด๋ฏธ์ง๋ฅผ ๊ฐ์ ธ์ Blob ์คํ ๋ฆฌ์ง์ results/{nanoid}.jpg๋ก ์ ์ฅํ๋ค. API ์๋ต์๋ ์๊ตฌ URL๊ณผ ํจ๊ป ๋๋ค 12์๋ฆฌ resultId๋ฅผ ํจ๊ป ๋ฐํํ๋ค.
resultId๋ /r/[id] ๊ณต์ ํ์ด์ง์ ํค๊ฐ ๋๋ค. ๊ณต์ ํ์ด์ง์ ์ง์
ํ๋ฉด Blob์ ํจ๊ป ์ ์ฅํด๋ results/{id}.json ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์ฝ์ด ๊ฒฐ๊ณผ ์ด๋ฏธ์ง์ ๋๋ฌผ์ ์ ๋ณด๋ฅผ ๋ ๋๋งํ๋ค. ๋์ OG ์ด๋ฏธ์ง๋ ์์ฑํด์ ์นด์นด์คํก์ด๋ ํธ์ํฐ๋ก ๊ณต์ ํ์ ๋ ์ธ๋ค์ผ์ด ์ ๋๋ก ๋จ๋๋ก ํ๋ค.
๊ฒฐ๊ณผ ์ด๋ฏธ์ง๋ 30์ผ ํ ์๋ ์ญ์ ๋๋ค. Vercel Cron์ ์ด์ฉํด ๋งค์ผ ์์ 30์ผ ๊ฒฝ๊ณผ ๊ฒฐ๊ณผ๋ฌผ์ ์ ๋ฆฌํ๋ ์์ ์ ๋ฑ๋กํ๋ค.
๋ฌด๋ฃ ์๋น์ค๋ ์ ์ฉ ๊ฐ๋ฅ์ฑ์ ์ฒ์๋ถํฐ ๊ณ ๋ คํด์ผ ํ๋ค. Replicate API ๋น์ฉ์ด ๋๊ตฐ๊ฐ์ ๋ฐ๋ณต ์์ฒญ์ผ๋ก ํญ์ฃผํ ์ ์๋ค.
์ด๊ธฐ์๋ ์๋ฒ ๋ฉ๋ชจ๋ฆฌ Map์ ์ด์ฉํ ๋จ์ IP ๊ธฐ๋ฐ ์ ํ์ ์ฌ์ฉํ๋ค. ๋ฌธ์ ๋ Vercel ๊ฐ์ ์๋ฒ๋ฆฌ์ค ํ๊ฒฝ์์ ์ธ์คํด์ค๋ง๋ค ๋ฉ๋ชจ๋ฆฌ๊ฐ ๋ถ๋ฆฌ๋์ด ์ค์ ์ ํ์ด ์ ๋๋ก ๋์ํ์ง ์๋๋ค๋ ๊ฒ์ด๋ค.
Upstash Redis ๊ธฐ๋ฐ์ @upstash/ratelimit์ผ๋ก ๊ต์ฒดํ๋ฉด์ ๋ ๊ฒน์ ๋ฐฉ์ด๋ง์ ๋ง๋ค์๋ค.
DAILY_TOTAL_CAP, ๊ธฐ๋ณธ 500ํ)IP ์ ํ์ ๋จผ์ ํ์ธํด์ ๋จ์ฉ IP๊ฐ ์ผ์ผ ์ด๋ ์นด์ดํฐ๋ฅผ ์์งํ์ง ๋ชปํ๋๋ก ์์๋ฅผ ์ก์๋ค. ์ด๋ ์ด๊ณผ ์์๋ IP ์ ํ ๋ฉ์์ง๊ฐ ์๋ "์ค๋ ์ค๋น๋ ๊ธฐ๋ํผ๊ทธ๊ฐ ๋ชจ๋ ์์ง๋์ด์"๋ผ๋ ๋ณ๋ ์๋ด๋ก ๋ถ๊ธฐํ๋ค.
typescript
// IP ์ ํ์ ๋จผ์ ํ์ธ โ ๋จ์ฉ IP๋ฅผ ์ผ์ผ ์ด๋ ์นด์ดํฐ ์์ง ์ ์ ์ฐจ๋จ
const ipResult = await ipLimiter.limit(ip);
if (!ipResult.success) {
return { allowed: false, remaining: 0, reason: "ip" };
}
const dailyResult = await dailyLimiter.limit("global");
if (!dailyResult.success) {
return { allowed: false, remaining: 0, reason: "daily" };
}
Upstash ์๊ฒฉ์ฆ๋ช
์ ์ง์ ์ค์ ํ UPSTASH_REDIS_REST_* ํ๊ฒฝ๋ณ์์ Vercel-Upstash ์ฐ๋์ด ์๋ ์ฃผ์
ํ๋ KV_* ๋ณ์ ๋ชจ๋๋ฅผ ์ง์ํ๋๋ก ํด๋ฐฑ ์ฒ๋ฆฌํ๋ค. Upstash ๋ฏธ์ค์ ์์๋ in-memory ํด๋ฐฑ์ผ๋ก ๋์ํ๋, ํ๋ก๋์
์์๋ ๊ฒฝ๊ณ ๋ฅผ ์ถ๋ ฅํ๋ค.
์ผ๊ตด ์ฌ์ง์ ์ธ๋ถ AI API๋ก ๋ณด๋ด๋ ์๋น์ค์ธ ๋งํผ, ๊ฐ์ธ์ ๋ณด ์ฒ๋ฆฌ๋ฐฉ์นจ์ ์ ํ์ด ์๋๋ผ ํ์์๋ค.
๊ตฌ์ฒด์ ์ผ๋ก ๋ช ์ํด์ผ ํ ๋ด์ฉ์ด ๋ ๊ฐ์ง์๋ค. ์ฒซ์งธ๋ ์ฌ์ง์ด ์ด๋๋ก ์ ์ก๋๋๊ฐ์ด๋ค. Ginini๋ ์ ๋ก๋๋ ์ฌ์ง์ Google Gemini(์ผ๊ตด ๋ถ์)์ Replicate(์ด๋ฏธ์ง ์์ฑ) ๋ ์ธ๋ถ ์๋น์ค๋ก ์ ๋ฌํ๋ค. ์ฌ์ฉ์๊ฐ ์ด๋ฅผ ์๊ณ ๋์ํ ์ ์์ด์ผ ํ๋ค.
๋์งธ๋ ๋ฐ์ดํฐ ๋ณด๊ด ๊ธฐ๊ฐ์ด๋ค. ๊ฒฐ๊ณผ ์ด๋ฏธ์ง๋ฅผ Vercel Blob์ ์ ์ฅํ๋ ๊ตฌ์กฐ์ "์ธ์ ์ญ์ ๋๋๊ฐ"๋ฅผ ์ฝ์ํด์ผ ํ๋ค. ์ต๋ 30์ผ ๋ณด๊ด ํ ์๋ ์ญ์ ๋ก ์ ์ฑ ์ ์ ํ๊ณ , ๊ฐ์ธ์ ๋ณด์ฒ๋ฆฌ๋ฐฉ์นจ ํ์ด์ง์ ๋ช ์ํ๋ค.
์ด๊ธฐ ๊ฐ๋ฐ ์ค์๋ ์ผ๊ตด ๋ถ์ ๊ฒฐ๊ณผ(faceFeatures)๋ฅผ ์๋ฒ ๋ก๊ทธ์ ์ถ๋ ฅํ๊ณ ์์๋ค. ๋ถ์ ๋ด์ฉ์ด ํค์ด์คํ์ผ, ๋ ํํ, ํ์ ๋ฑ ์ธ๋ชจ ๋ฌ์ฌ๋ฅผ ๋ด๊ณ ์์ด PII(๊ฐ์ธ์๋ณ์ ๋ณด)์ ์คํ๋ ๋ฐ์ดํฐ๋ค. ๋ฐฐํฌ ์ ์ ํด๋น ๋ก๊ทธ๋ฅผ ๋ชจ๋ ์ ๊ฑฐํ๋ค.
30์ผ TTL Cron ์ ๋ฆฌ
Vercel Blob์ ์์ด๋ ๊ฒฐ๊ณผ ์ด๋ฏธ์ง๋ ๋ฐฉ์นํ๋ฉด ์คํ ๋ฆฌ์ง ๋น์ฉ์ด ๊ณ์ ๋์ด๋๋ค. ๊ฐ์ธ์ ๋ณด์ฒ๋ฆฌ๋ฐฉ์นจ์ "30์ผ ๋ณด๊ด" ์ ์ฝ์ํ์ผ๋ ์ค์ ๋ก ์ญ์ ํ๋ ๋ก์ง์ด ํ์ํ๋ค.
Vercel Cron์ ์ด์ฉํด /api/cron/cleanup ์๋ํฌ์ธํธ๋ฅผ ๋งค์ผ ์์ ์ ํธ์ถํ๋๋ก ๋ฑ๋กํ๋ค. ๋ก์ง์ ๋จ์ํ๋ค. Blob ์คํ ๋ฆฌ์ง์ results/ ํ๋ฆฌํฝ์ค๋ฅผ ํ์ด์ง๋ค์ด์
์ผ๋ก ์ํํ๋ฉด์ ์
๋ก๋ ์๊ฐ์ด 30์ผ์ ๋์ ํ์ผ์ ๋ชจ์ ํ ๋ฒ์ ์ญ์ ํ๋ค.
typescript
const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
const staleUrls: string[] = [];
let cursor: string | undefined;
do {
const result = await list({ prefix: "results/", cursor, limit: 1000 });
for (const blob of result.blobs) {
if (blob.uploadedAt.getTime() < cutoff) {
staleUrls.push(blob.url);
}
}
cursor = result.hasMore ? result.cursor : undefined;
} while (cursor);
if (staleUrls.length > 0) {
await del(staleUrls);
}
์ด๋ฏธ์ง(results/{id}.jpg)์ ๋ฉํ๋ฐ์ดํฐ(results/{id}.json)๊ฐ ๊ฐ์ ํ๋ฆฌํฝ์ค ์๋ ์์ด์ ๋ณ๋ ์ฒ๋ฆฌ ์์ด ํจ๊ป ์ ๋ฆฌ๋๋ค. Cron ์๋ํฌ์ธํธ๋ CRON_SECRET ํค๋๋ก ์ธ์ฆํ๋ฉฐ, ๋ฏธ์ค์ ์ ๊ฒฝ๊ณ ๋ง ์ถ๋ ฅํ๊ณ ๋์์ ํ์ฉํด ๋ก์ปฌ ํ
์คํธ๋ฅผ ๋ง์ง ์๋๋ก ํ๋ค.
GitHub Actions CI
main ๋ธ๋์น push์ PR์ ๋ํด ์ธ ๋จ๊ณ๊ฐ ์๋์ผ๋ก ๋์๊ฐ๋ค.
yaml
- run: npm run lint # ESLint
- run: npx tsc --noEmit # ํ์
์ฒดํฌ
- run: npm run build # Next.js ๋น๋
๋ณ๋ ํ
์คํธ ๋ฌ๋๋ ์์ง๋ง, ํ์
์ฒดํฌ์ ๋น๋๊ฐ ํต๊ณผํ๋ฉด ์ต์ํ ๋ฐํ์ ์๋ฌ๋ฅผ ์ ๋ฐํ๋ ์ฝ๋๊ฐ ๋จธ์ง๋๋ ๊ฒ์ ๋ง์ ์ ์๋ค. curly: ["error", "all"] lint ๊ท์น๋ CI์์ ๊ฐ์ ๋์ด, ์ค๊ดํธ ์๋ ๋จ์ผ ๋ฌธ์ฅ if๊ฐ ์ฝ๋๋ฒ ์ด์ค์ ๋ค์ด์ค์ง ๋ชปํ๋ค.
๊ฐ๋ฐ ์ด๋ฐ์ ๋คํฌ๋ชจ๋๋ฅผ ๊ธฐ๋ณธ์ผ๋ก ์ง์ํ๋ ๋ฐฉํฅ์ ๊ฒํ ํ๋ค. Next.js + Tailwind ์กฐํฉ์์ dark: ์ ํธ๋ฆฌํฐ๋ง ๋ถ์ด๋ฉด ๋๋ ๊ฒ์ฒ๋ผ ๋ณด์๋ค.
๊ทธ๋ฐ๋ฐ ์ด ์๋น์ค์ ํต์ฌ ๊ฒฝํ์ ์๊ฐํด๋ดค๋ค. ๊ฒฐ๊ณผ ํ๋ฉด์์ ๊ธฐ๋ํผ๊ทธ ์บ๋ฆญํฐ๊ฐ ํ์ ๋ฐฐ๊ฒฝ ์์ ๋ฑ์ฅํ๋ค. ๋ผ์ดํธ๋ฐ์ค ์คํ์ผ์ ๊ฒฐ๊ณผ ํ๋ฉด๋ ํฐ์ ๋ฐฐ๊ฒฝ ๊ธฐ๋ฐ์ด๋ค. ๋คํฌ๋ชจ๋๋ก ์ ํํ๋ฉด ์ด ๊ฒฝํ์ด ์์ฐ์ค๋ฝ์ง ์๋ค.
๋ ๋ธ๋๋ ์ปฌ๋ฌ ํ๋ ํธ๋ warm ivory, ํฌ๋ฆผ, ๋ธ๋ผ์ด ๊ณ์ด๋ก ์ค์ ํ๋๋ฐ, ์ด ํค์ ๋ผ์ดํธ ๋ชจ๋์์ ํจ์ฌ ์ ์ด์๋๋ค.
๊ฒฐ๋ก ์ ์ผ๋ก globals.css์์ ๋คํฌ ๋ชจ๋๋ฅผ ๋ช
์์ ์ผ๋ก ์ ๊ฑฐํ๊ณ ๋ผ์ดํธ ๋ชจ๋ ์ ์ฉ์ผ๋ก ๊ณ ์ ํ๋ค. CSS ๋ณ์๋ ๋จ์ผ ์ธํธ๋ง ๊ด๋ฆฌํ๋ฉด ๋๋ ์ฝ๋๊ฐ ๋จ์ํด์ก๋ค.
์ด ์๋น์ค์ ์ฃผ์ ์ฌ์ฉ ์๋๋ฆฌ์ค๋ ์นด์นด์คํก ํ๋กํ ๊ต์ฒด๋ค. ๋๋ถ๋ถ ์ค๋งํธํฐ์ผ๋ก ์ ์ํด์ ์ง์ ์ฌ์ง์ ์ฐ๊ฑฐ๋ ๊ฐค๋ฌ๋ฆฌ์์ ๊ณ ๋ฅด๊ณ , ๊ฒฐ๊ณผ๋ฅผ ์นด์นด์คํก์ผ๋ก ๊ณต์ ํ๋ค. ๋ฐ์คํฌํ ๋ ์ด์์์ด ์ฃผ๊ฐ ๋ ์ด์ ๊ฐ ์์๋ค.
๋ชจ๋ฐ์ผ ์ฑ์ฒ๋ผ ๋๊ปด์ง๋ UX๋ฅผ ๋ชฉํ๋ก ์ก๊ณ ์ ์ฒด ๊ตฌ์กฐ๋ฅผ ์ฌ์ค๊ณํ๋ค.
h-dvh๋ก ๋ทฐํฌํธ ์ ์ฒด๋ฅผ ์ฑ์ฐ๋ ํ์คํฌ๋ฆฐ step ๊ธฐ๋ฐ ๋ ์ด์์ScreenLayout ๊ณต์ฉ ์ปดํฌ๋ํธFullscreenSheet์ผ๋ก ํฌ๋กญ ํ๋ฉด๊ณผ ๊ฒฐ๊ณผ ๋ผ์ดํธ๋ฐ์ค๋ฅผ ์ค๋ฒ๋ ์ด ์ฒ๋ฆฌmin-h-[44px]๋ฅผ ๊ธฐ์ค์ผ๋ก ํ๋ณด๋ฐ์คํฌํ์์๋ ๋ชจ๋ฐ์ผ ๋๋น(max-w-sm)๋ฅผ ์ค์์ ์นด๋์ฒ๋ผ ๋ฐฐ์นํ๋ค. ์๋น์ค ํน์ฑ์ ๋ฐ์คํฌํ ๋ ์ด์์์ ๋ฐ๋ก ์ค๊ณํ๋ ๊ฒ๋ณด๋ค "ํฐ ์์์ ๋ณด๋ ๋๋"์ ๋ฐ์คํฌํ์์๋ ๊ทธ๋๋ก ์ ์งํ๋ ๊ฒ์ด ๋ ์์ฐ์ค๋ฝ๋ค๊ณ ํ๋จํ๋ค.
์ด๋ฏธ์ง ํฌ๋กญ๋ ์์ ํ ์ฌ๊ฐํ ํฌ๋กญ์์ ์ํ ํฌ๋กญ์ผ๋ก ๋ฐ๊ฟจ๋ค. ํ๋กํ ์ฌ์ง ํ์ฉ ๋ชฉ์ ์ ์ํ์ด ์ง๊ด์ ์ด๊ณ , ํฌ๋กญ ์๋ฃ ํ ์ธ๋ค์ผ๋ ์ํ์ผ๋ก ๋ณด์ฌ์ค์ ์ต์ข ๊ฒฐ๊ณผ๋ฌผ์ ๋ฏธ๋ฆฌ ์์ํ ์ ์๊ฒ ํ๋ค.
Chip, IconButton, ScreenHeader ๊ฐ์ ์์ ๊ณตํต ์ปดํฌ๋ํธ๋ค๋ ํฐ์น ์์ญ์ ์ฐ์ ์ผ๋ก ์ค๊ณํ๋ค. ์๊ฐ๋ฝ์ผ๋ก ํญํ์ ๋ ๋น ์ง์์ด ๋ฐ์ํ๋๋ก ํ๋ ๊ฒ์ด ์์ ํ๋ฉด์์์ ์ฌ์ฉ๊ฐ์ ํฌ๊ฒ ์ข์ฐํ๋ค.
๋์ผํ ์ฌ์ง์ผ๋ก ์ฌ๋ฌ ๋ฒ ๋ณํํ์ ๋ ๋งค๋ฒ ์์ ํ ๋ค๋ฅธ ๊ฒฐ๊ณผ๊ฐ ๋์ค๋ฉด "๋ด ๊ธฐ๋ํผ๊ทธ"๋ผ๋ ๋๋์ด ์ค์ด๋ ๋ค. ์ด๋ ์ ๋ ์ผ๊ด์ฑ์ ๊ฐ์ง๋ฉด์๋ ๋ํ ์ผ์ ์กฐ๊ธ์ฉ ๋ค๋ฅธ ๊ฒฐ๊ณผ๋ฅผ ๋ชฉํ๋ก ํ๋ค.
์ ๋ ฅ ์ด๋ฏธ์ง ๋ฐ์ดํธ๋ฅผ SHA-256์ผ๋ก ํด์ฑํด์ ์์ฑ ์๋๋ก ์ฌ์ฉํ๋ค. ๊ฐ์ ์ฌ์ง์ ์ฌ๋ฆฌ๋ฉด ๊ฐ์ ์๋๋ก ์์ฑ์ด ์์๋์ด ๊ฒฐ๊ณผ์ ์ผ๊ด์ฑ์ด ๋์์ง๋ค.
typescript
function getStableGenerationSeed(arrayBuffer: ArrayBuffer): number {
const hash = createHash("sha256").update(Buffer.from(arrayBuffer)).digest();
return (hash.readUInt32BE(0) % 2147483647) + 1;
}
๋ค๋ง Replicate ๋ชจ๋ธ์ด seed ํ๋ผ๋ฏธํฐ๋ฅผ ์ง์ํ์ง ์๊ฑฐ๋ validation ์๋ฌ๋ฅผ ๋ด๋ ๊ฒฝ์ฐ๋ ์์ด์, ๊ทธ๋ด ๋๋ seed ์์ด ์ฌ์๋ํ๋ ํด๋ฐฑ ๋ก์ง๋ ํจ๊ป ์ถ๊ฐํ๋ค.
Ginini๋ฅผ ๋ง๋ค๋ฉด์ ๊ฐ์ฅ ๋ง์ด ๋ฐฐ์ด ๊ฑด AI ํ๋กฌํํธ ์์ง๋์ด๋ง์ด๋ค. ์ฒ์์ "์ข์ ํ๋กฌํํธ๋ฅผ ํ ๋ฒ ์ฐ๋ฉด ๋"์ด๋ผ๊ณ ์๊ฐํ๋ค. ์ค์ ๋ก๋ ๋ฌ๋๋ค. ๊ฒฐ๊ณผ๋ฌผ์์ ๋ฌธ์ ๋ฅผ ๋ฐ๊ฒฌํ๊ณ , ๊ทธ ๋ฌธ์ ๊ฐ ํ๋กฌํํธ์ ์ด๋ ๋ถ๋ถ์์ ๋น๋กฏ๋๋์ง ํ์ ํ๊ณ , ๊ทธ ๋ถ๋ถ์ ๋ช ํํ๊ฒ ์์ ํ๋ ๋ฐ๋ณต ์์ ์ด์๋ค.
"~ํ์ง ๋ง ๊ฒ" ๊ฐ์ negative ์ง์นจ์ด ์์ธ๋ก ํจ๊ณผ์ ์ด์๋ค. "์์ด๋ณด๋ฆฌ ํฌ๋ฆผ์ ๋ชจํผ๋ฅผ ์ฌ์ฉํ๋ผ"๋ณด๋ค "tan, orange, brown ํจ์น๋ฅผ ์ ๋ ์ถ๊ฐํ์ง ๋ง๋ผ"๊ฐ ๋ชจ๋ธ์ ๋ ์ ํํ๊ฒ ์ ์ดํ๋ค. ์ค์ํ ์ ์ฝ์ผ์๋ก ํ๋กฌํํธ ์์ ํ ๋ฒ, ๋์ ํ ๋ฒ ๋ ๋ช ์ํด์ ์ต์ปค๋งํ๋ ํจํด๋ ํจ๊ณผ๊ฐ ์์๋ค.
์ํคํ ์ฒ ์ธก๋ฉด์์๋ FSD๊ฐ ์์ ํ๋ก์ ํธ์์๋ ์ถฉ๋ถํ ๊ฐ์น๊ฐ ์๋ค๋ ๊ฑธ ํ์ธํ๋ค. "์ด ์ฝ๋๋ ์ด๋์ ์์ด์ผ ํ์ง?"๋ผ๋ ์ง๋ฌธ์ FSD ๋ ์ด์ด ๊ท์น์ด ์ผ๊ด๋ ๋ต์ ์ฃผ๊ณ , ๋ฆฌํฉํ ๋งํ ๋ ์ด๋๋ฅผ ๊ฑด๋๋ ค์ผ ํ๋์ง๋ ๋ช ํํ๋ค.
๊ทธ๋ฆฌ๊ณ ์๋ฒ๋ฆฌ์ค ํ๊ฒฝ์์์ ์ํ ๊ด๋ฆฌ๋ ์ฃผ์๊ฐ ํ์ํ๋ค. in-memory Rate Limit์ด ํ๋ก๋์ ์์ ์๋ํ ๋๋ก ๋์ํ์ง ์๋๋ค๋ ๊ฑธ ์ง์ ๊ฒช๊ณ ๋์์ผ ์ธ๋ถ ์คํ ์ด(Upstash Redis)์ ํ์์ฑ์ ์ ๋๋ก ์ดํดํ๋ค.
์์ง ๋ฎ์๊ผด ๋ถ์ ์นด๋, ์ปคํ ๋ชจ๋, ์์ฆ ํ ๋ง ๊ฐ์ ๊ธฐ๋ฅ๋ค์ด ๋ก๋๋งต์ ๋จ์์๋ค. ์ง๊ธ์ ์ฝ๋ ๊ตฌ์กฐ ์์ ์ฐจ๊ทผ์ฐจ๊ทผ ์์๊ฐ ๊ณํ์ด๋ค.