Skip to main content

아키텍처 개요

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   앱            │────▶│   백엔드        │────▶│  사주           │
│   (프론트엔드)  │     │   (프록시)      │     │  API            │
└─────────────────┘     └─────────────────┘     └─────────────────┘
        │                       │
        │                       ▼
        │               ┌─────────────────┐
        │               │   캐시          │
        │               │   (Redis)       │
        │               └─────────────────┘


┌─────────────────┐
│  로컬 사주      │  ← 클라이언트 계산 (API 호출 없음)
│  계산기         │
└─────────────────┘

1단계: 백엔드 프록시

API 키를 클라이언트에 절대 노출하지 마세요. 백엔드 프록시를 생성하세요:
// app/api/fortune/route.ts
import { SajuClient } from '@sajuapi/sdk';

const client = new SajuClient({
  apiKey: process.env.SAJU_API_KEY!
});

export async function POST(request: Request) {
  // 사용자 세션 검증
  const session = await getSession(request);
  if (!session) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // 사용자별 요청 제한
  const rateLimited = await checkRateLimit(session.userId);
  if (rateLimited) {
    return Response.json({ error: 'Too many requests' }, { status: 429 });
  }

  const { saju, userName } = await request.json();

  const fortune = await client.getDailyFortune({ saju, userName });

  return Response.json(fortune);
}

2단계: 클라이언트 사주 계산

API 호출을 줄이기 위해 사주를 로컬에서 계산하세요:
// 클라이언트 계산기 사용 (API 호출 없음)
import { SajuCalculator } from '@sajuapi/sdk';

function useSaju(birthData: BirthData) {
  const [saju, setSaju] = useState<Saju | null>(null);

  useEffect(() => {
    // 클라이언트에서 완전히 실행
    const calculated = SajuCalculator.calculate({
      year: birthData.year,
      month: birthData.month,
      day: birthData.day,
      hour: birthData.hour,
      gender: birthData.gender
    });
    setSaju(calculated);
  }, [birthData]);

  return saju;
}

3단계: 캐싱 구현

비용 절감을 위해 운세를 캐싱하세요:
// Redis를 사용한 백엔드 캐싱
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);
const CACHE_TTL = 86400; // 24시간

async function getCachedFortune(saju: Saju, userName: string) {
  // 일간(日干) + 날짜 기반 캐시 키
  const today = new Date().toISOString().split('T')[0];
  const cacheKey = `fortune:${saju.dayMaster.stem}:${today}`;

  // 캐시 확인
  const cached = await redis.get(cacheKey);
  if (cached) {
    return JSON.parse(cached);
  }

  // API에서 조회
  const fortune = await client.getDailyFortune({ saju, userName });

  // 결과 캐싱
  await redis.setex(cacheKey, CACHE_TTL, JSON.stringify(fortune));

  return fortune;
}

4단계: 사용자 요청 제한

사용자별 요청 제한을 구현하세요:
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '1 h'),  // 시간당 10회 요청
  analytics: true
});

async function checkRateLimit(userId: string): Promise<boolean> {
  const { success, limit, remaining, reset } = await ratelimit.limit(userId);

  if (!success) {
    console.log(`사용자 ${userId} 요청 제한됨. ${reset}에 초기화`);
    return true;
  }

  return false;
}

5단계: 에러 처리

우아한 폴백을 구현하세요:
async function getFortuneWithFallback(saju: Saju, userName: string) {
  try {
    return await client.getDailyFortune({ saju, userName });
  } catch (error) {
    if (error instanceof RateLimitError) {
      // 캐시된 데이터가 있으면 반환
      const cached = await getCachedFortune(saju);
      if (cached) return { ...cached, _fromCache: true };
    }

    // 로컬에서 폴백 운세 생성
    return generateFallbackFortune(saju, userName);
  }
}

function generateFallbackFortune(saju: Saju, userName: string): FortuneData {
  // 일간별 사전 정의된 운세
  const fallbacks = {
    '병화': { score: 72, analysis: '오늘은 안정적인 하루입니다...' },
    '정화': { score: 68, analysis: '신중한 판단이 필요한 날...' },
    // ... 다른 일간들
  };

  return fallbacks[saju.dayMaster.name] || fallbacks['병화'];
}

6단계: 분석 및 모니터링

사용량과 에러를 추적하세요:
async function trackFortuneRequest(userId: string, result: FortuneResult) {
  await analytics.track({
    event: 'fortune_requested',
    userId,
    properties: {
      model: result.meta.model,
      cached: result.meta.cached,
      cost: result.meta.cost,
      latency: result.meta.latency,
      dayMaster: result.data.character.dayMaster
    }
  });
}

// 에러 추적
client.on('error', (error) => {
  Sentry.captureException(error, {
    tags: { api: 'sajuapi' },
    extra: { requestId: error.requestId }
  });
});

프로덕션 체크리스트

1

API 키 보안

  • API 키를 환경 변수에 저장
  • 클라이언트 코드에 키 노출 금지
  • git에 키 커밋 금지
2

백엔드 프록시

  • 모든 API 호출을 백엔드를 통해 처리
  • 사용자 인증 필수
  • 요청 검증 구현
3

캐싱

  • Redis 또는 유사 캐시 구성
  • 캐시 키에 날짜 포함 (일별 교체)
  • 폴백 운세 준비
4

요청 제한

  • 사용자별 요청 제한 구성
  • 안전망으로 전역 요청 제한 설정
  • 제한 도달 시 우아한 폴백
5

모니터링

  • 에러 추적 (Sentry 등)
  • 사용량 분석
  • 비용 모니터링 알림
6

테스팅

  • 사주 계산 유닛 테스트
  • 테스트 API 키로 통합 테스트
  • 예상 트래픽 부하 테스트