ERP-node/버튼_제어관리_성능_최적화_전략.md

15 KiB

버튼 제어관리 성능 최적화 전략

🎯 성능 목표 설정

허용 가능한 응답 시간

  • 즉시 반응: 0-100ms (사용자가 지연을 느끼지 않음)
  • 빠른 응답: 100-300ms (약간의 지연이지만 허용 가능)
  • 보통 응답: 300-1000ms (Loading 스피너 필요)
  • 느린 응답: 1000ms+ (사용자 불만 발생)

현실적 목표

  • 간단한 제어관리: 200ms 이내
  • 복잡한 제어관리: 500ms 이내
  • 매우 복잡한 로직: 1초 이내 (비동기 처리)

🚀 핵심 최적화 전략

1. 즉시 응답 + 백그라운드 실행 패턴

const handleButtonClick = async (component: ComponentData) => {
  const config = component.webTypeConfig;

  // 🔥 즉시 UI 응답 (0ms)
  setButtonState("executing");
  toast.success("처리를 시작했습니다.");

  try {
    // Step 1: 기존 액션 우선 실행 (빠른 응답)
    if (config?.actionType && config?.dataflowTiming !== "replace") {
      await executeOriginalAction(config.actionType, component);
      // 사용자에게 즉시 피드백
      toast.success(`${getActionDisplayName(config.actionType)} 완료`);
    }

    // Step 2: 제어관리는 백그라운드에서 실행
    if (config?.enableDataflowControl) {
      // 🔥 비동기로 실행 (UI 블로킹 없음)
      executeDataflowInBackground(config, component.id)
        .then((result) => {
          if (result.success) {
            showDataflowResult(result);
          }
        })
        .catch((error) => {
          console.error("Background dataflow failed:", error);
          // 조용히 실패 처리 (사용자 방해 최소화)
        });
    }
  } finally {
    setButtonState("idle");
  }
};

// 백그라운드 실행 함수
const executeDataflowInBackground = async (
  config: ButtonTypeConfig,
  buttonId: string
): Promise<ExecutionResult> => {
  // 성능 모니터링
  const startTime = performance.now();

  try {
    const result = await apiClient.post("/api/button-dataflow/execute-async", {
      buttonConfig: config,
      buttonId: buttonId,
      priority: "background", // 우선순위 낮게 설정
    });

    const executionTime = performance.now() - startTime;
    console.log(`⚡ Dataflow 실행 시간: ${executionTime.toFixed(2)}ms`);

    return result.data;
  } catch (error) {
    // 에러 로깅만 하고 사용자 방해하지 않음
    console.error("Background dataflow error:", error);
    throw error;
  }
};

2. 스마트 캐싱 시스템

// 다층 캐싱 전략
class DataflowCache {
  private memoryCache = new Map<string, any>(); // L1: 메모리 캐시
  private persistCache: IDBDatabase | null = null; // L2: 브라우저 저장소

  constructor() {
    this.initPersistentCache();
  }

  // 버튼별 제어관리 설정 캐싱
  async getButtonDataflowConfig(
    buttonId: string
  ): Promise<ButtonDataflowConfig | null> {
    const cacheKey = `button_dataflow_${buttonId}`;

    // L1: 메모리에서 확인 (1ms)
    if (this.memoryCache.has(cacheKey)) {
      console.log("⚡ Memory cache hit:", buttonId);
      return this.memoryCache.get(cacheKey);
    }

    // L2: 브라우저 저장소에서 확인 (5-10ms)
    const cached = await this.getFromPersistentCache(cacheKey);
    if (cached && !this.isExpired(cached)) {
      console.log("💾 Persistent cache hit:", buttonId);
      this.memoryCache.set(cacheKey, cached.data);
      return cached.data;
    }

    // L3: 서버에서 로드 (100-300ms)
    console.log("🌐 Loading from server:", buttonId);
    const serverData = await this.loadFromServer(buttonId);

    // 캐시에 저장
    this.memoryCache.set(cacheKey, serverData);
    await this.saveToPersistentCache(cacheKey, serverData);

    return serverData;
  }

  // 관계도별 실행 계획 캐싱
  async getCachedExecutionPlan(
    diagramId: number
  ): Promise<ExecutionPlan | null> {
    // 자주 사용되는 실행 계획을 캐시
    const cacheKey = `execution_plan_${diagramId}`;
    return this.getFromCache(cacheKey, async () => {
      return await this.loadExecutionPlan(diagramId);
    });
  }
}

// 사용 예시
const dataflowCache = new DataflowCache();

const optimizedButtonClick = async (buttonId: string) => {
  // 🔥 캐시에서 즉시 로드 (1-10ms)
  const config = await dataflowCache.getButtonDataflowConfig(buttonId);

  if (config) {
    // 설정이 캐시되어 있으면 즉시 실행
    await executeDataflow(config);
  }
};

3. 데이터베이스 최적화

-- 🔥 버튼별 제어관리 조회 최적화 인덱스
CREATE INDEX CONCURRENTLY idx_dataflow_button_fast_lookup
ON dataflow_diagrams
USING GIN ((control->'buttonId'))
WHERE category @> '["button-trigger"]'
AND company_code IS NOT NULL;

-- 🔥 실행 조건 빠른 검색 인덱스
CREATE INDEX CONCURRENTLY idx_dataflow_trigger_type
ON dataflow_diagrams (company_code, ((control->0->>'triggerType')))
WHERE control IS NOT NULL;

-- 🔥 자주 사용되는 관계도 우선 조회
CREATE INDEX CONCURRENTLY idx_dataflow_usage_priority
ON dataflow_diagrams (company_code, updated_at DESC)
WHERE category @> '["button-trigger"]';
// 최적화된 데이터베이스 조회
export class OptimizedEventTriggerService {
  // 🔥 버튼별 제어관리 직접 조회 (전체 스캔 제거)
  static async getButtonDataflowConfigs(
    buttonId: string,
    companyCode: string
  ): Promise<DataflowConfig[]> {
    // 기존: 모든 관계도 스캔 (느림)
    // const allDiagrams = await prisma.$queryRaw`SELECT * FROM dataflow_diagrams WHERE...`

    // 🔥 새로운: 버튼별 직접 조회 (빠름)
    const configs = await prisma.$queryRaw`
      SELECT 
        diagram_id,
        control,
        plan,
        category
      FROM dataflow_diagrams 
      WHERE company_code = ${companyCode}
        AND control @> '[{"buttonId": ${buttonId}}]'
        AND category @> '["button-trigger"]'
      ORDER BY updated_at DESC
      LIMIT 5; -- 최대 5개만 조회
    `;

    return configs as DataflowConfig[];
  }

  // 🔥 조건 검증 최적화 (메모리 내 처리)
  static evaluateConditionsOptimized(
    conditions: DataflowCondition[],
    data: Record<string, any>
  ): boolean {
    // 간단한 조건은 메모리에서 즉시 처리 (1-5ms)
    for (const condition of conditions) {
      if (condition.type === "condition") {
        const fieldValue = data[condition.field!];
        const result = this.evaluateSimpleCondition(
          fieldValue,
          condition.operator!,
          condition.value
        );

        if (!result) return false;
      }
    }

    return true;
  }

  private static evaluateSimpleCondition(
    fieldValue: any,
    operator: string,
    conditionValue: any
  ): boolean {
    switch (operator) {
      case "=":
        return fieldValue === conditionValue;
      case "!=":
        return fieldValue !== conditionValue;
      case ">":
        return fieldValue > conditionValue;
      case "<":
        return fieldValue < conditionValue;
      case ">=":
        return fieldValue >= conditionValue;
      case "<=":
        return fieldValue <= conditionValue;
      case "LIKE":
        return String(fieldValue)
          .toLowerCase()
          .includes(String(conditionValue).toLowerCase());
      default:
        return true;
    }
  }
}

4. 배치 처리 및 큐 시스템

// 🔥 제어관리 작업 큐 시스템
class DataflowQueue {
  private queue: Array<{
    id: string;
    buttonId: string;
    config: ButtonDataflowConfig;
    priority: "high" | "normal" | "low";
    timestamp: number;
  }> = [];

  private processing = false;

  // 작업 추가 (즉시 반환)
  enqueue(
    buttonId: string,
    config: ButtonDataflowConfig,
    priority: "high" | "normal" | "low" = "normal"
  ): string {
    const jobId = `job_${Date.now()}_${Math.random()
      .toString(36)
      .substr(2, 9)}`;

    this.queue.push({
      id: jobId,
      buttonId,
      config,
      priority,
      timestamp: Date.now(),
    });

    // 우선순위별 정렬
    this.queue.sort((a, b) => {
      const priorityWeight = { high: 3, normal: 2, low: 1 };
      return priorityWeight[b.priority] - priorityWeight[a.priority];
    });

    // 비동기 처리 시작
    this.processQueue();

    return jobId; // 작업 ID 즉시 반환
  }

  // 배치 처리
  private async processQueue(): Promise<void> {
    if (this.processing || this.queue.length === 0) return;

    this.processing = true;

    try {
      // 동시에 최대 3개 작업 처리
      const batch = this.queue.splice(0, 3);

      const promises = batch.map((job) =>
        this.executeDataflowJob(job).catch((error) => {
          console.error(`Job ${job.id} failed:`, error);
          return { success: false, error };
        })
      );

      await Promise.all(promises);
    } finally {
      this.processing = false;

      // 큐에 더 많은 작업이 있으면 계속 처리
      if (this.queue.length > 0) {
        setTimeout(() => this.processQueue(), 10);
      }
    }
  }

  private async executeDataflowJob(job: any): Promise<any> {
    const startTime = performance.now();

    try {
      const result = await OptimizedEventTriggerService.executeButtonDataflow(
        job.buttonId,
        job.config
      );

      const executionTime = performance.now() - startTime;
      console.log(
        `⚡ Job ${job.id} completed in ${executionTime.toFixed(2)}ms`
      );

      return result;
    } catch (error) {
      console.error(`❌ Job ${job.id} failed:`, error);
      throw error;
    }
  }
}

// 전역 큐 인스턴스
const dataflowQueue = new DataflowQueue();

// 사용 예시: 즉시 응답하는 버튼 클릭
const optimizedButtonClick = async (
  buttonId: string,
  config: ButtonDataflowConfig
) => {
  // 🔥 즉시 작업 큐에 추가하고 반환 (1-5ms)
  const jobId = dataflowQueue.enqueue(buttonId, config, "normal");

  // 사용자에게 즉시 피드백
  toast.success("작업이 시작되었습니다.");

  return jobId;
};

5. 프론트엔드 최적화

// 🔥 React 성능 최적화
const OptimizedButtonComponent = React.memo(
  ({ component }: { component: ComponentData }) => {
    const [isExecuting, setIsExecuting] = useState(false);
    const [executionTime, setExecutionTime] = useState<number | null>(null);

    // 디바운싱으로 중복 클릭 방지
    const handleClick = useDebouncedCallback(async () => {
      if (isExecuting) return;

      setIsExecuting(true);
      const startTime = performance.now();

      try {
        await optimizedButtonClick(component.id, component.webTypeConfig);
      } finally {
        const endTime = performance.now();
        setExecutionTime(endTime - startTime);
        setIsExecuting(false);
      }
    }, 300); // 300ms 디바운싱

    return (
      <Button
        onClick={handleClick}
        disabled={isExecuting}
        className={`
        transition-all duration-200
        ${isExecuting ? "opacity-75 cursor-wait" : ""}
      `}
      >
        {isExecuting ? (
          <div className="flex items-center space-x-2">
            <Spinner size="sm" />
            <span>처리중...</span>
          </div>
        ) : (
          component.label || "버튼"
        )}

        {/* 개발 모드에서 성능 정보 표시 */}
        {process.env.NODE_ENV === "development" && executionTime && (
          <span className="ml-2 text-xs opacity-60">
            {executionTime.toFixed(0)}ms
          </span>
        )}
      </Button>
    );
  }
);

// 리스트 가상화로 대량 버튼 렌더링 최적화
const VirtualizedButtonList = ({ buttons }: { buttons: ComponentData[] }) => {
  return (
    <FixedSizeList
      height={600}
      itemCount={buttons.length}
      itemSize={50}
      itemData={buttons}
    >
      {({ index, style, data }) => (
        <div style={style}>
          <OptimizedButtonComponent component={data[index]} />
        </div>
      )}
    </FixedSizeList>
  );
};

📊 성능 모니터링

// 실시간 성능 모니터링
class PerformanceMonitor {
  private metrics: {
    buttonClicks: number;
    averageResponseTime: number;
    slowQueries: Array<{ query: string; time: number; timestamp: Date }>;
    cacheHitRate: number;
  } = {
    buttonClicks: 0,
    averageResponseTime: 0,
    slowQueries: [],
    cacheHitRate: 0,
  };

  recordButtonClick(executionTime: number) {
    this.metrics.buttonClicks++;

    // 이동 평균으로 응답 시간 계산
    this.metrics.averageResponseTime =
      this.metrics.averageResponseTime * 0.9 + executionTime * 0.1;

    // 느린 쿼리 기록 (500ms 이상)
    if (executionTime > 500) {
      this.metrics.slowQueries.push({
        query: "button_dataflow_execution",
        time: executionTime,
        timestamp: new Date(),
      });

      // 최대 100개만 보관
      if (this.metrics.slowQueries.length > 100) {
        this.metrics.slowQueries.shift();
      }
    }

    // 성능 경고
    if (executionTime > 1000) {
      console.warn(`🐌 Slow button execution: ${executionTime}ms`);
    }
  }

  getPerformanceReport() {
    return {
      ...this.metrics,
      recommendation: this.getRecommendation(),
    };
  }

  private getRecommendation(): string[] {
    const recommendations: string[] = [];

    if (this.metrics.averageResponseTime > 300) {
      recommendations.push(
        "평균 응답 시간이 느립니다. 캐싱 설정을 확인하세요."
      );
    }

    if (this.metrics.cacheHitRate < 80) {
      recommendations.push("캐시 히트율이 낮습니다. 캐시 전략을 재검토하세요.");
    }

    if (this.metrics.slowQueries.length > 10) {
      recommendations.push("느린 쿼리가 많습니다. 인덱스를 확인하세요.");
    }

    return recommendations;
  }
}

// 전역 모니터
const performanceMonitor = new PerformanceMonitor();

// 사용 예시
const monitoredButtonClick = async (buttonId: string) => {
  const startTime = performance.now();

  try {
    await executeButtonAction(buttonId);
  } finally {
    const executionTime = performance.now() - startTime;
    performanceMonitor.recordButtonClick(executionTime);
  }
};

🎯 성능 최적화 로드맵

Phase 1: 즉시 개선 (1-2주)

  1. 즉시 응답 패턴 도입
  2. 기본 캐싱 구현
  3. 데이터베이스 인덱스 추가
  4. 성능 모니터링 설정

Phase 2: 고급 최적화 (3-4주)

  1. 🔄 작업 큐 시스템 구현
  2. 🔄 배치 처리 도입
  3. 🔄 다층 캐싱 완성
  4. 🔄 가상화 렌더링 적용

Phase 3: 고도화 (5-6주)

  1. 프리로딩 시스템
  2. CDN 캐싱 도입
  3. 서버 사이드 캐싱
  4. 성능 대시보드

이렇게 단계적으로 최적화하면 사용자가 체감할 수 있는 성능 개선을 점진적으로 달성할 수 있습니다!