# 기술 구현 문서 > 라온누리 실시간 피드백 시스템의 기술적 구현 방법과 최적화 전략 --- ## 📋 목차 1. [실시간 피드백 비용 최적화](#1-실시간-피드백-비용-최적화) 2. [한글 패턴 매칭 문제 해결](#2-한글-패턴-매칭-문제-해결) 3. [구현 우선순위](#3-구현-우선순위) 4. [성능 벤치마크](#4-성능-벤치마크) --- ## 1. 실시간 피드백 비용 최적화 ### 문제 정의 실시간으로 글을 평가하면서 매번 AI API를 호출하면: - **비용 폭증**: 5분 작성 시 30회 호출 = $0.90/글 - **응답 지연**: GPT-4 평균 2초 → UX 저하 - **과도한 트래픽**: 불필요한 중복 평가 --- ### ⭐ 해결책: 3-Tier 하이브리드 시스템 #### 아키텍처 개요 ``` ┌─────────────────────────────────────────┐ │ Tier 1: 로컬 규칙 (클라이언트) │ │ - 글자 수, 문장 수, 패턴 매칭 │ │ - 비용: 무료 | 속도: 즉시 (0ms) │ │ - 정확도: 70% │ └─────────────────────────────────────────┘ ↓ (5초 debounce) ┌─────────────────────────────────────────┐ │ Tier 2: Mecab 형태소 분석 (서버) │ │ - node-mecab-ya │ │ - 비용: 무료 | 속도: 50-200ms │ │ - 정확도: 95% │ └─────────────────────────────────────────┘ ↓ ("구체화하기" 클릭) ┌─────────────────────────────────────────┐ │ Tier 3: Gemini AI 모델 (서버) │ │ - Gemini 2.5 Flash │ │ - 비용: $0.3/1M | 속도: 1s │ │ - 정확도: 90% (창의성, 맞춤법) │ └─────────────────────────────────────────┘ ``` #### 비용 비교 | 방식 | 호출 횟수 (5분) | 모델 | 총 비용 | 응답속도 | |------|----------------|------|---------|---------| | 순수 AI (Gemini Pro) | 30회 | Gemini Pro | $0.15 | 느림 ⚠️ | | AI (Debounce 5초) | 10회 | Gemini Pro | $0.05 | 보통 | | **3-Tier (Mecab+Gemini)** ⭐ | 1회 | Local+Mecab+Gemini Flash | **$0.0001** | **즉시** | **비용 절감률**: 99.9% --- ### 구현 방법 #### 1.1. Tier 1 - 로컬 규칙 기반 스코어링 ```typescript // src/utils/localScoring.ts interface LocalScore { total: number; breakdown: { length: number; sentences: number; sensory: number; dialogue: number; descriptive: number; }; } export function calculateLocalScore(text: string): LocalScore { const breakdown = { length: 0, sentences: 0, sensory: 0, dialogue: 0, descriptive: 0, }; // 1. 길이 점수 (100자당 1점, 최대 3점) breakdown.length = Math.min(Math.floor(text.length / 100), 3); // 2. 문장 수 (3개 이상 +1점) const sentences = text.split(/[.!?]/).filter(s => s.trim().length > 0); breakdown.sentences = sentences.length >= 3 ? 1 : 0; // 3. 감각 동사 (각 1점, 최대 3점) const sensoryPatterns = [ /보(다|고|니|면|았|았다|여|임|는|던)/, /듣(다|고|니|으며|었|자|는|던)/, /만지(다|고|니|면|었|자|는)/, /냄새(가|를|나|났|나는)/, /맛(을|이|보|봤|보는)/, ]; sensoryPatterns.forEach(pattern => { if (pattern.test(text) && breakdown.sensory < 3) { breakdown.sensory += 1; } }); // 4. 대화 (+2점) if (/"[^"]+"|'[^']+'/.test(text)) { breakdown.dialogue = 2; } // 5. 감각 형용사 (각 1점, 최대 2점) const descriptivePatterns = [ /아름답|예쁘|곱(다|게|고)/, /무섭|두렵|떨리/, /따뜻|차갑|뜨거|시원/, /부드럽|거칠|딱딱/, ]; descriptivePatterns.forEach(pattern => { if (pattern.test(text) && breakdown.descriptive < 2) { breakdown.descriptive += 1; } }); const total = Object.values(breakdown).reduce((sum, val) => sum + val, 0); return { total: Math.min(total, 10), breakdown }; } // 점수 → 영역 개수 변환 export function scoreToRegions(score: number): number { if (score <= 2) return 1; if (score <= 4) return 2; if (score <= 6) return 3; if (score <= 8) return 4; return 5; // 최대 5개 } ``` #### 1.2. React 컴포넌트에서 사용 ```tsx // src/components/writing/WritingEditor.tsx import { calculateLocalScore, scoreToRegions } from '@/utils/localScoring'; import { useDebouncedCallback } from 'use-debounce'; export function WritingEditor() { const [content, setContent] = useState(''); const [quickScore, setQuickScore] = useState(0); const [mecabScore, setMecabScore] = useState(null); const [regions, setRegions] = useState(1); // Tier 1: 즉시 응답 (간단한 정규식) useEffect(() => { const score = calculateLocalScore(content); setQuickScore(score.total); setRegions(scoreToRegions(score.total)); }, [content]); // Tier 2: Mecab 정밀 분석 (5초 debounce) const debouncedMecab = useDebouncedCallback(async (text: string) => { if (text.length < 100) return; // 100자 미만은 로컬만 try { const response = await fetch('/api/analyze-korean', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }), }); const { score } = await response.json(); setMecabScore(score); setRegions(scoreToRegions(score)); } catch (error) { console.error('Mecab analysis failed:', error); } }, 5000); useEffect(() => { if (content.length >= 100) { debouncedMecab(content); } }, [content, debouncedMecab]); // Tier 3: Gemini AI 최종 평가 (버튼 클릭) const handleFinalize = async () => { const response = await fetch('/api/evaluate-gemini', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: content }), }); const { score, suggestions } = await response.json(); // 최종 점수 및 수정 제안 표시 }; return (
{/* 실시간 점수 표시 */} {/* 글쓰기 영역 */}