[Flutter] StackTrace를 활용한 디버깅: 호출 경로 추적의 모든 것

길위에 히피·2025년 11월 20일

Flutter

목록 보기
55/56

들어가며

Flutter/Dart 개발 중 "이 코드가 어디서 호출되었는지"를 알아야 하는 상황이 자주 발생합니다. 특히 복잡한 상태 관리나 예상치 못한 값 변경을 디버깅할 때, 단순히 로그만으로는 부족합니다. 이번 글에서는 StackTrace를 활용한 호출 경로 추적 기법을 깊이 있게 다뤄보겠습니다.

StackTrace란?

StackTrace는 Dart에서 현재 실행 중인 코드의 호출 스택 정보를 담고 있는 객체입니다. 예외가 발생했을 때 자동으로 생성되지만, StackTrace.current를 사용하면 예외 없이도 현재 실행 위치의 스택 정보를 얻을 수 있습니다.

기본 사용법

void someFunction() {
  print(StackTrace.current);
}

이렇게 하면 다음과 같은 출력을 볼 수 있습니다:

#0      someFunction (package:my_app/main.dart:10:5)
#1      main (package:my_app/main.dart:5:3)

실제 문제 상황: 필드 변경 추적

다음과 같은 상황을 가정해봅시다:

class TransactionCreateForm {
  int amount;
  // ... 다른 필드들
}

amount 필드가 여러 곳에서 변경되는데, 어느 시점에 어떤 값으로 변경되는지, 그리고 어디서 호출되었는지를 추적하고 싶습니다.

StackTrace를 활용한 해결 방법

1단계: 기본 스택 트레이스 출력

가장 간단한 방법은 StackTrace.current를 직접 출력하는 것입니다:

set amount(int value) {
  _amount = value;
  print('amount 변경: $value');
  print(StackTrace.current);
}

하지만 이 방법은 출력이 너무 길고, 필요한 정보를 찾기 어렵습니다.

2단계: 스택 트레이스 파싱 및 필터링

스택 트레이스를 파싱하여 필요한 정보만 추출하는 방법입니다:

set amount(int value) {
  final oldValue = _amount;
  _amount = value;
  print('🔍 [TransactionCreateForm] amount 변경: $oldValue -> $value');

  // 호출 위치 추적
  try {
    final stackTrace = StackTrace.current;
    final stackLines = stackTrace.toString().split('\n');

    // transaction_form.dart를 제외한 첫 번째 관련 위치 찾기
    for (final line in stackLines) {
      final trimmed = line.trim();
      if (trimmed.isEmpty) continue;

      // transaction_form.dart 관련 줄은 건너뛰기
      if (trimmed.contains('transaction_form.dart')) continue;

      // 실제 호출 위치 찾기 (파일명과 라인 번호 포함)
      if (trimmed.contains('.dart:') || trimmed.contains('package:')) {
        print('   📍 호출 위치: $trimmed');
        break; // 첫 번째 관련 위치만 표시
      }
    }
  } catch (e) {
    // 스택 트레이스 파싱 실패 시 무시
  }
}

스택 트레이스 구조 이해하기

스택 트레이스의 각 줄은 다음과 같은 형식을 가집니다:

#0      ClassName.methodName (package:app/path/file.dart:123:45)
  • #0: 스택 프레임 번호
  • ClassName.methodName: 클래스와 메서드 이름
  • package:app/path/file.dart: 파일 경로
  • 123:45: 라인 번호와 컬럼 번호

고급 활용 기법

1. 여러 호출 위치 추적

첫 번째 호출 위치뿐만 아니라 호출 체인 전체를 추적하고 싶다면:

void trackCallStack(String message) {
  final stackTrace = StackTrace.current;
  final stackLines = stackTrace.toString().split('\n');

  print('🔍 $message');
  print('   호출 체인:');

  int count = 0;
  for (final line in stackLines) {
    final trimmed = line.trim();
    if (trimmed.isEmpty) continue;

    // 현재 파일은 제외
    if (trimmed.contains('transaction_form.dart')) continue;

    // 관련 위치만 표시
    if (trimmed.contains('.dart:') || trimmed.contains('package:')) {
      print('   ${count + 1}. $trimmed');
      count++;
      if (count >= 5) break; // 최대 5개만 표시
    }
  }
}

2. 정규식을 활용한 정확한 파싱

더 정확한 파싱을 위해 정규식을 사용할 수 있습니다:

import 'dart:core';

void parseStackTrace(StackTrace stackTrace) {
  final regex = RegExp(r'#\d+\s+(\S+)\s+\((.+):(\d+):(\d+)\)');
  final matches = regex.allMatches(stackTrace.toString());

  for (final match in matches) {
    final methodName = match.group(1);
    final filePath = match.group(2);
    final lineNumber = match.group(3);
    final columnNumber = match.group(4);

    // 현재 파일은 제외
    if (filePath?.contains('transaction_form.dart') ?? false) continue;

    print('📍 $methodName in $filePath:$lineNumber:$columnNumber');
    break; // 첫 번째만 표시
  }
}

3. 조건부 스택 트레이스 추적

특정 조건에서만 상세한 스택 트레이스를 출력:

set amount(int value) {
  final oldValue = _amount;
  _amount = value;

  // 특정 조건에서만 상세 추적
  if (value == 0 && oldValue != 0) {
    print('⚠️ [경고] amount가 0으로 변경됨: $oldValue -> $value');
    print('상세 스택 트레이스:');
    print(StackTrace.current);
  } else {
    // 일반적인 경우 간단한 추적
    print('🔍 amount 변경: $oldValue -> $value');
    // 간단한 호출 위치만 표시
  }
}

4. 성능 최적화: 지연 평가

스택 트레이스 생성은 비용이 들 수 있으므로, 필요할 때만 생성하도록 최적화:

set amount(int value) {
  final oldValue = _amount;
  _amount = value;

  // 디버그 모드에서만 스택 트레이스 생성
  if (kDebugMode) {
    _logChange(oldValue, value);
  }
}

void _logChange(int oldValue, int newValue) {
  print('🔍 amount 변경: $oldValue -> $newValue');

  try {
    final stackTrace = StackTrace.current;
    final stackLines = stackTrace.toString().split('\n');

    for (final line in stackLines) {
      final trimmed = line.trim();
      if (trimmed.isEmpty) continue;
      if (trimmed.contains('transaction_form.dart')) continue;

      if (trimmed.contains('.dart:') || trimmed.contains('package:')) {
        print('   📍 호출 위치: $trimmed');
        break;
      }
    }
  } catch (e) {
    // 무시
  }
}

실제 사용 예시

로그 출력 예시

앱을 실행하면 다음과 같은 로그를 확인할 수 있습니다:

flutter: 🔍 [TransactionCreateForm] amount 변경: 0 -> 1
flutter:    📍 호출 위치: #1      TransactionCreationBottomsheetLogic.onAmountChanged (transaction_creation_bottom_sheet_logic.dart:183:5)
flutter: check !! 1
flutter: 🔍 [TransactionCreateForm] amount 변경: 1 -> 0
flutter:    📍 호출 위치: #1      TransactionCreationBottomsheetLogic.changeAmount (transaction_creation_bottom_sheet_logic.dart:195:5)

이를 통해:

  • 언제: 값이 변경된 시점
  • 어디서: 어떤 파일의 몇 번째 줄에서 호출되었는지
  • 어떻게: 이전 값에서 새 값으로의 변경 내역
  • 누가: 어떤 메서드에서 호출되었는지

모두 확인할 수 있습니다.

StackTrace 활용 패턴

패턴 1: 에러 로깅 강화

에러 발생 시 스택 트레이스를 함께 로깅:

void handleError(dynamic error, StackTrace stackTrace) {
  print('❌ 에러 발생: $error');
  print('스택 트레이스:');

  final lines = stackTrace.toString().split('\n');
  for (final line in lines.take(10)) {
    print('   $line');
  }
}

패턴 2: 성능 프로파일링

특정 메서드의 호출 경로를 추적하여 성능 병목 지점 파악:

void expensiveOperation() {
  final startTime = DateTime.now();

  // 작업 수행
  // ...

  final duration = DateTime.now().difference(startTime);
  if (duration.inMilliseconds > 100) {
    print('⚠️ 느린 작업 감지: ${duration.inMilliseconds}ms');
    print('호출 위치:');
    _printCallLocation();
  }
}

void _printCallLocation() {
  final stackTrace = StackTrace.current;
  final lines = stackTrace.toString().split('\n');
  // 호출 위치 출력
}

패턴 3: 조건부 디버깅

특정 조건에서만 상세한 디버깅 정보 출력:

class DebugTracker {
  static bool _isEnabled = false;

  static void enable() => _isEnabled = true;
  static void disable() => _isEnabled = false;

  static void track(String message) {
    if (!_isEnabled) return;

    print('🔍 $message');
    _printCallLocation();
  }

  static void _printCallLocation() {
    final stackTrace = StackTrace.current;
    // 스택 트레이스 파싱 및 출력
  }
}

성능 고려사항

StackTrace 생성 비용

StackTrace.current는 상대적으로 비용이 큰 작업입니다. 스택 정보를 수집하고 포맷팅하는 과정이 필요하기 때문입니다.

성능 측정 예시:

void performanceTest() {
  final stopwatch = Stopwatch();

  // 일반 print
  stopwatch.start();
  for (int i = 0; i < 1000; i++) {
    print('test');
  }
  stopwatch.stop();
  print('일반 print: ${stopwatch.elapsedMicroseconds}μs');

  // StackTrace.current 포함
  stopwatch.reset();
  stopwatch.start();
  for (int i = 0; i < 1000; i++) {
    print('test ${StackTrace.current}');
  }
  stopwatch.stop();
  print('StackTrace 포함: ${stopwatch.elapsedMicroseconds}μs');
}

최적화 전략

  1. 디버그 모드에서만 활성화

    if (kDebugMode) {
      // StackTrace 추적
    }
  2. 조건부 활성화

    static const bool _enableStackTrace = bool.fromEnvironment('ENABLE_STACK_TRACE');
  3. 비동기 처리

    void trackAsync(String message) {
      // 즉시 실행하지 않고 나중에 처리
      Future.microtask(() {
        if (kDebugMode) {
          _printStackTrace(message);
        }
      });
    }
  4. 샘플링

    static int _callCount = 0;
    
    void trackWithSampling(String message) {
      _callCount++;
      // 100번 중 1번만 추적
      if (_callCount % 100 == 0) {
        _printStackTrace(message);
      }
    }

주의사항 및 제한사항

1. 릴리스 빌드에서의 동작

릴리스 빌드에서는 스택 트레이스 정보가 최적화되어 제한적일 수 있습니다. 디버그 빌드에서만 완전한 정보를 얻을 수 있습니다.

2. 비동기 코드에서의 주의

비동기 코드에서는 스택 트레이스가 예상과 다를 수 있습니다:

Future<void> asyncFunction() async {
  await Future.delayed(Duration(seconds: 1));
  // 이 시점의 스택 트레이스는 asyncFunction 호출 위치가 아닐 수 있음
  print(StackTrace.current);
}

3. 최적화된 코드

Dart 컴파일러가 최적화한 코드에서는 정확한 라인 번호를 얻기 어려울 수 있습니다.

실전 활용 예시

완전한 구현 예시

class TransactionCreateForm {
  int _amount;

  int get amount => _amount;

  set amount(int value) {
    final oldValue = _amount;
    _amount = value;

    if (kDebugMode) {
      _trackChange('amount', oldValue, value);
    }
  }

  void _trackChange(String fieldName, dynamic oldValue, dynamic newValue) {
    print('🔍 [TransactionCreateForm] $fieldName 변경: $oldValue -> $newValue');

    try {
      final stackTrace = StackTrace.current;
      final stackLines = stackTrace.toString().split('\n');

      // 현재 파일 제외하고 첫 번째 호출 위치 찾기
      for (final line in stackLines) {
        final trimmed = line.trim();
        if (trimmed.isEmpty) continue;
        if (trimmed.contains('transaction_form.dart')) continue;

        if (trimmed.contains('.dart:') || trimmed.contains('package:')) {
          print('   📍 호출 위치: $trimmed');
          break;
        }
      }
    } catch (e) {
      // 스택 트레이스 파싱 실패 시 무시
    }
  }
}

결론

StackTrace.current를 활용한 호출 경로 추적은 Flutter/Dart 개발에서 강력한 디버깅 도구입니다. 특히:

  • 복잡한 상태 관리에서 예상치 못한 값 변경을 추적할 때
  • 성능 병목 지점을 찾을 때
  • 에러 발생 경로를 파악할 때
  • 코드 실행 흐름을 이해할 때

매우 유용합니다.

다만 성능을 고려하여:

  • 디버그 모드에서만 활성화
  • 조건부로 추적
  • 필요한 경우에만 사용

하는 것이 좋습니다.

이 기법을 활용하면 디버깅 시간을 크게 단축하고, 코드의 실행 흐름을 더 잘 이해할 수 있습니다.


참고:

  • StackTrace.current는 Dart 2.0 이상에서 사용 가능합니다
  • 릴리스 빌드에서는 정보가 제한될 수 있으므로 디버그 빌드에서 테스트하는 것을 권장합니다
  • 성능이 중요한 부분에서는 조건부로만 활성화하세요
profile
마음맘은 히피인 일꾼러

0개의 댓글