역시 자본주의는 돈이 돈을 버는 구조다(flutter)

원장·2025년 6월 11일

플러터 기초

목록 보기
28/36

이번 주 느낀 점

AI는 이번 주도 사기다.

부자들이 AI에 돈을 왜 이렇게 투자하는지 이제야 알 것 같다.

claude code를 100달러를 주고 결제했다.

1달 결제인데 이제 앞으로 매 달 결제할 것 같다.

이유는 그냥 개 잘하는 개발자가 나랑 같이 일하는 것 같은 느낌이다.

요즘 코딩을 2가지 방법으로만 한다.

Case1. 회사에서 Cursor로 미친듯이 질문하면서 엄청난 생산성으로 작업

Case2. 퇴근하고 claude code를 활성화해놓고 계속 질문 > 디벨롭 > 디버깅의 반복 / 걍 게임하면서 해도 되고 책 읽으면서 해도됨. 길면 5분동안 돌아가니까.

4~5월에는 여의도의 회사에서 gitlab으로만 일하고.. 거의 퇴근하고 코딩을 안했다. 다 핑계고 놀기만 했기 때문이다..

그러다가 5월에 나오게 되면서 마지막 날 거기 있던 선임 개발자분이 claude code라는 걸로 매일 바이브 코딩한다고 하는데 추천해주신다고 써보라고 하셨다. 바로 앞에서 결제했다. ㅋㅋ

지금 중고차 직거래 + AI 를 만들고있는데 매일 하면 될 것 같다는 확신이 든다.

그리고 써보니 개사기다.. 퇴근하고 코딩이 하고 싶어진다...

공짜로 사업을 하는 느낌이다.

일단 이제 스터디 시작!!

TickerProviderStateMixin

먼저 팰 놈이다.

얘만 상속하면 보이지 않는 애니메이션일 때 작동하지 않아서 불필요하게 리소스를 사용하지 않는다고 한다.

원리가 뭘까?

핵심 원리

Ticker는 Flutter의 애니메이션 엔진에서 프레임마다 콜백을 호출하는 객체다.

TickerProviderStateMixin은 이 Ticker들을 생성하고 관리하는 역할을 해요.

프레임마다 ping을 쏜다고 생각하면 된다.

dart
// 이런 식으로 동작한다고 보면 됨
Ticker ticker = Ticker((elapsed) {
  print('Ping! 경과시간: $elapsed'); // 매 프레임마다 호출
});

ticker.start(); // ping 시작!
// 16.67ms마다 (60fps) 또는 11.11ms마다 (90fps) ping!

자동 일시정지 매커니즘

dart
// TickerProviderStateMixin 내부 동작 원리
class _MyWidgetState extends State<MyWidget> 
    with TickerProviderStateMixin {
  
  @override
  Ticker createTicker(TickerCallback onTick) {
    return Ticker(onTick, debugLabel: 'created by $this')
      ..muted = !mounted; // 핵심: mounted 상태에 따라 음소거
  }
}

이렇게 작동한다.

_controller.dispose(); 안해주면 memory leak가 발생한다고함. Ticker가 계속 살아있어서.

왜 자동으로 메모리 관리 안해주는데?

Dart는 수동 메모리 관리 언어가 아니지만, 네이티브 리소스(Ticker는 엔진과 연결됨)는 명시적으로 해제해야 합니다.

class PageA extends StatefulWidget {
  @override
  _PageAState createState() => _PageAState();
}

class _PageAState extends State<PageA> with TickerProviderStateMixin {
  late AnimationController _controller;
  
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );
    _controller.repeat(); // 계속 돌아가는 애니메이션
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // 다른 페이지로 이동 → 현재 페이지 dispose() 호출됨
            Navigator.push(context, MaterialPageRoute(
              builder: (context) => PageB(),
            ));
          },
          child: Text('다른 페이지로 이동'),
        ),
      ),
    );
  }
  
  @override
  void dispose() {
    print('PageA dispose! 애니메이션 중단됨');
    _controller.dispose(); // 여기서 Ticker도 자동으로 정리
    super.dispose();
  }
}

앱이 백그라운드로 갈 때 (AppLifecycleState.paused)

class BackgroundAwareWidget extends StatefulWidget {
  @override
  _BackgroundAwareWidgetState createState() => _BackgroundAwareWidgetState();
}

class _BackgroundAwareWidgetState extends State<BackgroundAwareWidget> 
    with TickerProviderStateMixin, WidgetsBindingObserver {
  
  late AnimationController _controller;
  
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this); // 앱 생명주기 관찰
    _controller = AnimationController(
      duration: Duration(seconds: 1),
      vsync: this,
    );
    _controller.repeat();
  }
  
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    
    switch (state) {
      case AppLifecycleState.paused:
        print('앱이 백그라운드로! 애니메이션 자동 중단');
        // TickerProviderStateMixin이 알아서 ticker 중단
        break;
      case AppLifecycleState.resumed:
        print('앱이 다시 포그라운드로! 애니메이션 재시작');
        // ticker 자동으로 다시 시작
        break;
      default:
        break;
    }
  }
  
  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _controller.dispose();
    super.dispose();
  }
}

위젯트리에서 제거될 때 (mounted = false)

class ConditionalAnimationWidget extends StatefulWidget {
  @override
  _ConditionalAnimationWidgetState createState() => 
      _ConditionalAnimationWidgetState();
}

class _ConditionalAnimationWidgetState extends State<ConditionalAnimationWidget> 
    with TickerProviderStateMixin {
  
  bool showAnimation = true;
  late AnimationController _controller;
  
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );
    _controller.repeat();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          ElevatedButton(
            onPressed: () {
              setState(() {
                showAnimation = !showAnimation; // 위젯 숨기기/보이기
              });
            },
            child: Text(showAnimation ? '애니메이션 숨기기' : '애니메이션 보이기'),
          ),
          
          // 조건부 렌더링 - false일 때 위젯트리에서 완전 제거
          if (showAnimation)
            AnimatedWidgetChild()
          else
            Text('애니메이션이 숨겨짐 (위젯트리에서 제거됨)'),
        ],
      ),
    );
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

class AnimatedWidgetChild extends StatefulWidget {
  @override
  _AnimatedWidgetChildState createState() => _AnimatedWidgetChildState();
}

class _AnimatedWidgetChildState extends State<AnimatedWidgetChild> 
    with TickerProviderStateMixin {
  
  late AnimationController _childController;
  
  @override
  void initState() {
    super.initState();
    _childController = AnimationController(
      duration: Duration(milliseconds: 500),
      vsync: this,
    );
    _childController.repeat(reverse: true);
    print('자식 위젯 생성! 애니메이션 시작');
  }
  
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _childController,
      builder: (context, child) {
        return Container(
          width: 100 + (_childController.value * 50),
          height: 100 + (_childController.value * 50),
          color: Colors.blue,
        );
      },
    );
  }
  
  @override
  void dispose() {
    print('자식 위젯 dispose! mounted = false, 애니메이션 중단');
    _childController.dispose();
    super.dispose();
  }
}

반드시 정리해줘야하는 네이티브 리소스들


애니메이션

// 플랫폼 vsync와 연결된 리소스들
AnimationController controller; // dispose() 필요
Ticker ticker; // dispose() 필요
VideoPlayerController videoController; // dispose() 필요

스트림 & 구독

// 네이티브 이벤트 스트림들
StreamSubscription subscription; // cancel() 필요
Timer timer; // cancel() 필요

// 예시
StreamSubscription? _subscription;

@override
void initState() {
  _subscription = someStream.listen((data) {
    // 처리
  });
}

@override
void dispose() {
  _subscription?.cancel(); // 필수!
  super.dispose();
}

카메라 & 미디어

CameraController cameraController; // dispose() 필요
VideoPlayerController videoPlayer; // dispose() 필요
AudioPlayer audioPlayer; // dispose() 필요
ImagePicker picker; // 자동 정리되지만 캐시는 수동 정리

파일 & 네트워크

// 파일 핸들러들
File file; // 대부분 자동이지만 스트림은 수동
HttpClient client; // close() 필요
WebSocket webSocket; // close() 필요

// 예시
HttpClient? _client;

void makeRequest() {
  _client = HttpClient();
  // 사용...
}

@override
void dispose() {
  _client?.close(); // 필수!
  super.dispose();
}

플랫폼 채널

// 네이티브와 통신하는 채널들
MethodChannel channel;
EventChannel eventChannel;
BasicMessageChannel messageChannel;

// 예시 - 배터리 정보 채널
static const platform = MethodChannel('com.example/battery');

@override
void dispose() {
  // MethodChannel은 보통 자동 정리되지만
  // EventChannel의 경우 구독 취소 필요
  super.dispose();
}

센서 & 하드웨어

// accelerometer, gyroscope 등
StreamSubscription? _accelerometerSubscription;

@override
void initState() {
  _accelerometerSubscription = accelerometerEvents.listen((event) {
    // 센서 데이터 처리
  });
}

@override
void dispose() {
  _accelerometerSubscription?.cancel(); // 센서 연결 해제
  super.dispose();
}

텍스처 & 그래픽

// OpenGL 텍스처들
TextureController textureController; // dispose() 필요
CustomPainter painter; // 보통 자동이지만 복잡한 리소스는 수동

class MyCustomPainter extends CustomPainter {
  Paint? _paint;
  
  @override
  void paint(Canvas canvas, Size size) {
    _paint ??= Paint()..color = Colors.blue;
    // 그리기
  }
  
  // dispose 패턴은 없지만 큰 리소스는 관리 필요
}

데이터베이스

// SQLite, Hive 등
Database database; // close() 필요
Box hiveBox; // close() 필요

@override
void dispose() async {
  await database.close();
  await hiveBox.close();
  super.dispose();
}

// Flutter 구조
┌─────────────────┐
│   Dart Code     │ ← 우리가 작성하는 코드
├─────────────────┤
│ Flutter Engine  │ ← C++로 작성된 엔진
├─────────────────┤
│Platform Channel │ ← 네이티브와 통신
├─────────────────┤
│   Native Code   │ ← Android(Java/Kotlin) / iOS(Swift/ObjC)
└─────────────────┘

네이티브 리소스들은 Flutter Engine이나 플랫폼 레벨에서 관리되기 때문에 Dart의 가비지 컬렉터가 자동으로 정리해주지 못함!

profile
나 원장이 아니다

0개의 댓글