성능 최적화 평가 기준
AppsInToss Unity 미니앱의 성능을 정확하게 측정하고 평가할 수 있는 기준과 도구를 소개해요.
정확한 측정이 이루어져야, 그다음 단계인 최적화 전략도 제대로 세울 수 있어요.
1. 성능 측정 기준
핵심 성능 지표 (KPI)
🎯 중요도별 성능 지표
├── Critical (치명적 - 즉시 해결 필요)
│ ├── 시작 시간: > 5초
│ ├── 평균 FPS: < 20
│ ├── 메모리 사용량: > 300MB
│ └── 크래시율: > 1%
│
├── High (높음 - 우선 해결)
│ ├── 시작 시간: 3-5초
│ ├── 평균 FPS: 20-30
│ ├── 메모리 사용량: 200-300MB
│ └── 로딩 시간: > 10초
│
└── Medium (보통 - 점진적 개선)
├── 시작 시간: 2-3초
├── 평균 FPS: 30-50
├── 메모리 사용량: 150-200MB
└── 배터리 효율: 네이티브 대비 120%플랫폼별 성능 기준
iOS 기준
| 기기 등급 | 시작 시간 | 평균 FPS | 메모리 | 배터리 |
|---|---|---|---|---|
| High-end (iPhone 13+) | < 2초 | 60+ | < 150MB | 95% |
| Mid-range (iPhone 11–12) | < 3초 | 50+ | < 200MB | 90% |
| Low-end (iPhone X 이하) | < 5초 | 30+ | < 250MB | 85% |
Android 기준
| 기기 등급 | 시작 시간 | 평균 FPS | 메모리 | 배터리 |
|---|---|---|---|---|
| High-end (Flagship) | < 2초 | 50+ | < 150MB | 90% |
| Mid-range (중급형) | < 4초 | 40+ | < 200MB | 85% |
| Low-end (저가형) | < 6초 | 25+ | < 250MB | 80% |
2. 성능 측정 도구
Unity Built-in 측정 도구
c#
using UnityEngine;
using UnityEngine.Profiling;
using System.Collections.Generic;
using System.Text;
public class AppsInTossPerformanceMeasure : MonoBehaviour
{
[Header("측정 설정")]
public bool enableMeasurement = true;
public float measureInterval = 1.0f;
public int maxSampleCount = 300; // 5분간 데이터
[Header("성능 임계값")]
public float fpsWarningThreshold = 30f;
public float memoryWarningThreshold = 200f; // MB
public float startupTimeLimit = 5f; // 초
private Dictionary<string, PerformanceMetric> metrics = new Dictionary<string, PerformanceMetric>();
private float startTime;
private bool startupMeasured = false;
[System.Serializable]
public class PerformanceMetric
{
public string name;
public Queue<float> samples = new Queue<float>();
public float currentValue;
public float averageValue;
public float minValue = float.MaxValue;
public float maxValue = float.MinValue;
public void AddSample(float value)
{
samples.Enqueue(value);
if (samples.Count > 300) samples.Dequeue(); // 최대 샘플 수 유지
currentValue = value;
minValue = Mathf.Min(minValue, value);
maxValue = Mathf.Max(maxValue, value);
// 평균 계산
float sum = 0;
foreach (float sample in samples)
sum += sample;
averageValue = sum / samples.Count;
}
}
void Awake()
{
startTime = Time.realtimeSinceStartup;
InitializeMetrics();
}
void Start()
{
if (enableMeasurement)
{
InvokeRepeating(nameof(MeasurePerformance), 0f, measureInterval);
}
}
void InitializeMetrics()
{
metrics["FPS"] = new PerformanceMetric { name = "FPS" };
metrics["Memory"] = new PerformanceMetric { name = "Memory (MB)" };
metrics["DrawCalls"] = new PerformanceMetric { name = "Draw Calls" };
metrics["Triangles"] = new PerformanceMetric { name = "Triangles" };
metrics["CPUFrameTime"] = new PerformanceMetric { name = "CPU Frame Time (ms)" };
metrics["GPUFrameTime"] = new PerformanceMetric { name = "GPU Frame Time (ms)" };
}
void MeasurePerformance()
{
// FPS 측정
float fps = 1.0f / Time.unscaledDeltaTime;
metrics["FPS"].AddSample(fps);
// 메모리 측정 (MB)
float memoryMB = Profiler.GetTotalAllocatedMemory(false) / (1024f * 1024f);
metrics["Memory"].AddSample(memoryMB);
// 렌더링 통계
metrics["DrawCalls"].AddSample(UnityEngine.Rendering.FrameDebugger.enabled ?
UnityEngine.Rendering.FrameDebugger.GetFrameEventCount() : 0);
// CPU/GPU 프레임 시간
float cpuTime = Time.deltaTime * 1000f; // ms로 변환
metrics["CPUFrameTime"].AddSample(cpuTime);
// 시작 시간 측정 (한 번만)
if (!startupMeasured && Time.realtimeSinceStartup > 1f)
{
float startupTime = Time.realtimeSinceStartup - startTime;
LogStartupTime(startupTime);
startupMeasured = true;
}
// 경고 임계값 체크
CheckPerformanceWarnings();
}
void CheckPerformanceWarnings()
{
// FPS 경고
if (metrics["FPS"].currentValue < fpsWarningThreshold)
{
Debug.LogWarning($"Low FPS detected: {metrics["FPS"].currentValue:F1}");
SendWarningToAppsInToss("low_fps", metrics["FPS"].currentValue);
}
// 메모리 경고
if (metrics["Memory"].currentValue > memoryWarningThreshold)
{
Debug.LogWarning($"High memory usage: {metrics["Memory"].currentValue:F1}MB");
SendWarningToAppsInToss("high_memory", metrics["Memory"].currentValue);
}
}
void LogStartupTime(float startupTime)
{
Debug.Log($"[Performance] Startup time: {startupTime:F2}s");
if (startupTime > startupTimeLimit)
{
Debug.LogWarning($"Startup time exceeds limit: {startupTime:F2}s > {startupTimeLimit}s");
}
// AppsInToss 분석 시스템에 전송
SendMetricToAppsInToss("startup_time", startupTime);
}
void SendWarningToAppsInToss(string warningType, float value)
{
var data = new Dictionary<string, object>
{
{"warning_type", warningType},
{"value", value},
{"timestamp", System.DateTime.UtcNow.ToString("o")},
{"device_model", SystemInfo.deviceModel},
{"memory_size", SystemInfo.systemMemorySize},
{"processor_type", SystemInfo.processorType}
};
// AppsInToss Native 호출
Application.ExternalCall("SendPerformanceWarning", JsonUtility.ToJson(data));
}
void SendMetricToAppsInToss(string metricName, float value)
{
var data = new Dictionary<string, object>
{
{"metric_name", metricName},
{"value", value},
{"timestamp", System.DateTime.UtcNow.ToString("o")}
};
Application.ExternalCall("SendPerformanceMetric", JsonUtility.ToJson(data));
}
// 성능 리포트 생성
public string GeneratePerformanceReport()
{
var report = new StringBuilder();
report.AppendLine("=== AppsInToss 성능 측정 리포트 ===");
report.AppendLine($"측정 시간: {System.DateTime.Now:yyyy-MM-dd HH:mm:ss}");
report.AppendLine($"기기 정보: {SystemInfo.deviceModel}");
report.AppendLine($"메모리: {SystemInfo.systemMemorySize}MB");
report.AppendLine($"프로세서: {SystemInfo.processorType}");
report.AppendLine();
foreach (var metric in metrics.Values)
{
report.AppendLine($"[{metric.name}]");
report.AppendLine($" 현재값: {metric.currentValue:F2}");
report.AppendLine($" 평균값: {metric.averageValue:F2}");
report.AppendLine($" 최소값: {metric.minValue:F2}");
report.AppendLine($" 최대값: {metric.maxValue:F2}");
report.AppendLine($" 샘플 수: {metric.samples.Count}");
report.AppendLine();
}
return report.ToString();
}
// GUI 성능 표시
void OnGUI()
{
if (!enableMeasurement) return;
GUILayout.BeginArea(new Rect(10, 10, 300, 150));
GUILayout.Box("AppsInToss 성능 모니터");
GUILayout.Label($"FPS: {metrics["FPS"].currentValue:F1} (평균: {metrics["FPS"].averageValue:F1})");
GUILayout.Label($"메모리: {metrics["Memory"].currentValue:F1}MB");
GUILayout.Label($"Draw Calls: {metrics["DrawCalls"].currentValue:F0}");
// 성능 상태 표시
if (metrics["FPS"].currentValue < fpsWarningThreshold)
{
GUI.color = Color.red;
GUILayout.Label("⚠ FPS 경고");
}
if (metrics["Memory"].currentValue > memoryWarningThreshold)
{
GUI.color = Color.red;
GUILayout.Label("⚠ 메모리 경고");
}
GUI.color = Color.white;
GUILayout.EndArea();
}
}3. 자동화된 성능 테스트
성능 테스트 스크립트
c#
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class AutomatedPerformanceTest : MonoBehaviour
{
[System.Serializable]
public class TestScenario
{
public string name;
public string sceneName;
public float testDuration = 60f; // 초
public int targetFPS = 30;
public float maxMemoryMB = 200f;
}
public TestScenario[] testScenarios;
private PerformanceTestResult currentResult;
[System.Serializable]
public class PerformanceTestResult
{
public string scenarioName;
public float averageFPS;
public float minFPS;
public float maxMemoryMB;
public float testDuration;
public bool passed;
public List<string> issues = new List<string>();
}
public void RunAllTests()
{
StartCoroutine(RunTestSequence());
}
IEnumerator RunTestSequence()
{
var allResults = new List<PerformanceTestResult>();
foreach (var scenario in testScenarios)
{
Debug.Log($"Starting test: {scenario.name}");
yield return StartCoroutine(RunSingleTest(scenario));
allResults.Add(currentResult);
// 테스트 간 쿨다운
yield return new WaitForSeconds(2f);
}
GenerateTestReport(allResults);
}
IEnumerator RunSingleTest(TestScenario scenario)
{
currentResult = new PerformanceTestResult
{
scenarioName = scenario.name,
testDuration = scenario.testDuration
};
// 씬 로드
var loadOp = UnityEngine.SceneManagement.SceneManager.LoadSceneAsync(scenario.sceneName);
yield return loadOp;
// 안정화 대기
yield return new WaitForSeconds(2f);
// 성능 측정 시작
float startTime = Time.time;
float fpsSum = 0f;
int fpsCount = 0;
float minFPS = float.MaxValue;
float maxMemory = 0f;
while (Time.time - startTime < scenario.testDuration)
{
float fps = 1.0f / Time.unscaledDeltaTime;
fpsSum += fps;
fpsCount++;
minFPS = Mathf.Min(minFPS, fps);
float memoryMB = UnityEngine.Profiling.Profiler.GetTotalAllocatedMemory(false) / (1024f * 1024f);
maxMemory = Mathf.Max(maxMemory, memoryMB);
yield return new WaitForSeconds(0.1f); // 10Hz 측정
}
// 결과 계산
currentResult.averageFPS = fpsSum / fpsCount;
currentResult.minFPS = minFPS;
currentResult.maxMemoryMB = maxMemory;
// 성능 기준 체크
EvaluateTestResult(scenario);
}
void EvaluateTestResult(TestScenario scenario)
{
currentResult.passed = true;
currentResult.issues.Clear();
if (currentResult.averageFPS < scenario.targetFPS)
{
currentResult.passed = false;
currentResult.issues.Add($"평균 FPS 부족: {currentResult.averageFPS:F1} < {scenario.targetFPS}");
}
if (currentResult.minFPS < scenario.targetFPS * 0.7f) // 30% 드롭 허용
{
currentResult.passed = false;
currentResult.issues.Add($"최소 FPS 부족: {currentResult.minFPS:F1}");
}
if (currentResult.maxMemoryMB > scenario.maxMemoryMB)
{
currentResult.passed = false;
currentResult.issues.Add($"메모리 사용량 초과: {currentResult.maxMemoryMB:F1}MB > {scenario.maxMemoryMB}MB");
}
}
void GenerateTestReport(List<PerformanceTestResult> results)
{
var report = new System.Text.StringBuilder();
report.AppendLine("=== 자동화 성능 테스트 결과 ===");
report.AppendLine($"테스트 일시: {System.DateTime.Now:yyyy-MM-dd HH:mm:ss}");
report.AppendLine($"기기: {SystemInfo.deviceModel}");
report.AppendLine();
int passedCount = 0;
foreach (var result in results)
{
report.AppendLine($"[{result.scenarioName}] - {(result.passed ? "PASS" : "FAIL")}");
report.AppendLine($" 평균 FPS: {result.averageFPS:F1}");
report.AppendLine($" 최소 FPS: {result.minFPS:F1}");
report.AppendLine($" 최대 메모리: {result.maxMemoryMB:F1}MB");
if (!result.passed)
{
foreach (var issue in result.issues)
{
report.AppendLine($" ❌ {issue}");
}
}
else
{
passedCount++;
}
report.AppendLine();
}
report.AppendLine($"전체 결과: {passedCount}/{results.Count} 통과");
Debug.Log(report.ToString());
// AppsInToss 리포팅 시스템에 전송
SendTestReportToAppsInToss(report.ToString(), results);
}
void SendTestReportToAppsInToss(string report, List<PerformanceTestResult> results)
{
var data = new Dictionary<string, object>
{
{"report", report},
{"results", results},
{"device_info", new Dictionary<string, object>
{
{"model", SystemInfo.deviceModel},
{"memory", SystemInfo.systemMemorySize},
{"processor", SystemInfo.processorType},
{"graphics", SystemInfo.graphicsDeviceName}
}}
};
Application.ExternalCall("SendPerformanceTestReport", JsonUtility.ToJson(data));
}
}4. 실시간 성능 대시보드
웹 기반 성능 모니터링
tsx
// TypeScript - AppsInToss 성능 대시보드
interface PerformanceData {
timestamp: number;
fps: number;
memory: number;
drawCalls: number;
cpuTime: number;
gpuTime: number;
}
class AppsInTossPerformanceDashboard {
private performanceData: PerformanceData[] = [];
private maxDataPoints = 300; // 5분간 데이터 (1초 간격)
private charts: { [key: string]: any } = {};
constructor() {
this.initializeDashboard();
this.startDataCollection();
}
initializeDashboard() {
// Chart.js 또는 다른 차트 라이브러리 초기화
this.createFPSChart();
this.createMemoryChart();
this.createDrawCallChart();
}
createFPSChart() {
const ctx = document.getElementById('fpsChart') as HTMLCanvasElement;
this.charts.fps = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'FPS',
data: [],
borderColor: '#4CAF50',
backgroundColor: 'rgba(76, 175, 80, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
max: 60,
ticks: {
callback: function(value) {
return value + ' FPS';
}
}
}
},
plugins: {
legend: {
position: 'top'
},
title: {
display: true,
text: 'FPS 모니터링'
}
}
}
});
}
createMemoryChart() {
const ctx = document.getElementById('memoryChart') as HTMLCanvasElement;
this.charts.memory = new Chart(ctx, {
type: 'area',
data: {
labels: [],
datasets: [{
label: 'Memory (MB)',
data: [],
borderColor: '#FF9800',
backgroundColor: 'rgba(255, 152, 0, 0.2)',
fill: true
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return value + ' MB';
}
}
}
}
}
});
}
startDataCollection() {
setInterval(() => {
this.collectPerformanceData();
}, 1000); // 1초마다 데이터 수집
}
collectPerformanceData() {
// Unity에서 성능 데이터 요청
if ((window as any).unityInstance) {
(window as any).unityInstance.SendMessage('AppsInTossPerformanceMeasure', 'SendCurrentMetrics', '');
}
}
updateDashboard(data: PerformanceData) {
// 데이터 저장
this.performanceData.push(data);
// 최대 데이터 포인트 수 유지
if (this.performanceData.length > this.maxDataPoints) {
this.performanceData.shift();
}
// 차트 업데이트
this.updateCharts();
// 경고 체크
this.checkPerformanceAlerts(data);
}
updateCharts() {
const labels = this.performanceData.map(d => new Date(d.timestamp).toLocaleTimeString());
// FPS 차트 업데이트
this.charts.fps.data.labels = labels;
this.charts.fps.data.datasets[0].data = this.performanceData.map(d => d.fps);
this.charts.fps.update('none');
// 메모리 차트 업데이트
this.charts.memory.data.labels = labels;
this.charts.memory.data.datasets[0].data = this.performanceData.map(d => d.memory);
this.charts.memory.update('none');
}
checkPerformanceAlerts(data: PerformanceData) {
const alerts = [];
if (data.fps < 20) {
alerts.push({
type: 'critical',
message: `심각한 FPS 저하: ${data.fps.toFixed(1)} FPS`
});
} else if (data.fps < 30) {
alerts.push({
type: 'warning',
message: `FPS 경고: ${data.fps.toFixed(1)} FPS`
});
}
if (data.memory > 200) {
alerts.push({
type: 'critical',
message: `높은 메모리 사용량: ${data.memory.toFixed(1)} MB`
});
} else if (data.memory > 150) {
alerts.push({
type: 'warning',
message: `메모리 경고: ${data.memory.toFixed(1)} MB`
});
}
if (alerts.length > 0) {
this.showAlerts(alerts);
}
}
showAlerts(alerts: any[]) {
const alertContainer = document.getElementById('performanceAlerts');
if (alertContainer) {
alertContainer.innerHTML = '';
alerts.forEach(alert => {
const alertElement = document.createElement('div');
alertElement.className = `alert alert-${alert.type}`;
alertElement.textContent = alert.message;
alertContainer.appendChild(alertElement);
});
}
}
generateReport() {
if (this.performanceData.length === 0) return;
const latest = this.performanceData[this.performanceData.length - 1];
const avgFPS = this.performanceData.reduce((sum, d) => sum + d.fps, 0) / this.performanceData.length;
const maxMemory = Math.max(...this.performanceData.map(d => d.memory));
return {
currentFPS: latest.fps,
averageFPS: avgFPS,
currentMemory: latest.memory,
maxMemory: maxMemory,
sampleCount: this.performanceData.length,
duration: (latest.timestamp - this.performanceData[0].timestamp) / 1000 // 초
};
}
}
// Unity에서 호출하는 글로벌 함수
(window as any).UpdatePerformanceData = (dataJson: string) => {
const data = JSON.parse(dataJson) as PerformanceData;
dashboard.updateDashboard(data);
};
// 대시보드 인스턴스 생성
const dashboard = new AppsInTossPerformanceDashboard();5. 성능 평가 체크리스트
출시 전 성능 검증 체크리스트
- 시작 시간 5초 이내 (모든 대상 기기)
- 평균 FPS 30 이상 유지 (1분 이상 테스트)
- 메모리 사용량 200MB 이하 (장시간 플레이)
- 크래시율 1% 이하 (100명 이상 베타 테스트)
- 배터리 효율성 네이티브 앱 대비 85% 이상
- 네트워크 끊김 상황에서 정상 동작
- 백그라운드 진입/복귀 시 정상 동작
- 다양한 화면 비율에서 정상 동작
- 메모리 부족 상황 대응 (우아한 저하)
- 자동화된 성능 테스트 통과
정기 모니터링 체크리스트
- 주간 성능 리포트 검토
- 사용자 피드백 중 성능 관련 이슈 분석
- 성능 저하 트렌드 모니터링
- A/B 테스트를 통한 최적화 효과 검증
- 새로운 기기/OS 버전 호환성 테스트
