<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>착하게살자</title>
    <link>https://mark340.tistory.com/</link>
    <description>착하게삽시다</description>
    <language>ko</language>
    <pubDate>Fri, 14 Aug 2026 23:13:14 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>mark340</managingEditor>
    <image>
      <title>착하게살자</title>
      <url>https://tistory1.daumcdn.net/tistory/5622930/attach/2c636f5da4864ba38920ca1128b7569e</url>
      <link>https://mark340.tistory.com</link>
    </image>
    <item>
      <title>Bitbucket App Password 종료 대응 &amp;mdash; API Token 전환 및 Credential Helper 점검</title>
      <link>https://mark340.tistory.com/102</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Atlassian의 정책 변경(CHANGE-3222)으로 Bitbucket의 App Password가 폐기되었다. 기존 App Password로 인증하던 환경에서는 remote 접근 시 &lt;code&gt;410&lt;/code&gt; 에러가 발생한다. 본 글은 API Token 전환 절차와, 그 과정에서 함께 점검해야 할 credential helper 설정을 정리한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;증상&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;remote: CHANGE-3222 - Functionality has been deprecated
remote: App passwords are deprecated and must be replaced with API tokens.
fatal: unable to access '.../*.git/': The requested URL returned error: 410&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP &lt;code&gt;410 Gone&lt;/code&gt;은 해당 인증 경로가 영구적으로 제거되었음을 의미한다. 재시도로 해결되지 않으며, 인증 수단을 API Token으로 교체해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. API Token 발급&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Atlassian account &amp;rarr; Security &amp;rarr; API tokens&lt;/b&gt;에서 생성&lt;/li&gt;
&lt;li&gt;scope는 필요한 최소 권한(repository read/write 등)으로 한정&lt;/li&gt;
&lt;li&gt;토큰 문자열은 생성 직후 한 번만 노출되므로 즉시 보관&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Credential Helper 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 자격증명이 어디에 저장되어 있는지 먼저 확인한다. helper에 따라 제거 위치가 다르다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;git config --get credential.helper&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;helper&lt;/th&gt;
&lt;th&gt;저장 위치&lt;/th&gt;
&lt;th&gt;자격증명 제거 방법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;store&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.git-credentials&lt;/code&gt; (평문)&lt;/td&gt;
&lt;td&gt;파일에서 해당 호스트 줄 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;osxkeychain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;macOS Keychain (암호화)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;security delete-internet-password -s bitbucket.org&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;manager&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Git Credential Manager&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git credential-manager erase&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;osxkeychain&lt;/code&gt;으로 알고 keychain을 확인했으나 항목이 없다면, helper가 &lt;code&gt;store&lt;/code&gt;로 설정되어 자격증명이 평문 파일에 저장되어 있을 가능성이 높다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 평문 저장(store) &amp;rarr; Keychain(osxkeychain) 전환&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;store&lt;/code&gt; helper는 자격증명을 &lt;code&gt;~/.git-credentials&lt;/code&gt;에 평문으로 보관하므로, 토큰 전환을 계기로 암호화 저장 방식으로 변경하는 것을 권장한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 평문 자격증명 파일 제거
rm ~/.git-credentials

# helper를 osxkeychain으로 변경
git config --global credential.helper osxkeychain&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. API Token으로 재인증&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;remote URL은 변경하지 않고, 인증 시 비밀번호 자리에 API Token을 입력한다.&lt;/p&gt;
&lt;pre class=&quot;objectivec&quot;&gt;&lt;code&gt;git fetch origin
# Username: &amp;lt;Atlassian 계정 또는 x-token-auth&amp;gt;
# Password: &amp;lt;API Token&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Username 기준&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;계정 API Token &amp;rarr; Atlassian 계정 이메일 / username&lt;/li&gt;
&lt;li&gt;Repository &amp;middot; Project Access Token &amp;rarr; &lt;code&gt;x-token-auth&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증에 성공하면 자격증명이 keychain에 저장되어 이후 재입력이 불필요하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고: 토큰을 URL에 직접 삽입하는 방식&lt;/h2&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;git remote set-url origin https://&amp;lt;user&amp;gt;:&amp;lt;TOKEN&amp;gt;@bitbucket.org/&amp;lt;repo&amp;gt;.git&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작은 하지만 토큰이 &lt;code&gt;.git/config&lt;/code&gt;, 쉘 히스토리, 로그에 평문으로 노출될 수 있어 권장하지 않는다. credential helper에 위임하는 방식이 안전하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;App Password 폐기로 &lt;code&gt;410&lt;/code&gt; 발생 시 API Token 전환이 필수&lt;/li&gt;
&lt;li&gt;자격증명 제거 전 &lt;code&gt;credential.helper&lt;/code&gt; 설정을 먼저 확인할 것&lt;/li&gt;
&lt;li&gt;&lt;code&gt;store&lt;/code&gt;(평문) 사용 환경은 이번 기회에 &lt;code&gt;osxkeychain&lt;/code&gt;(암호화)으로 전환 권장&lt;/li&gt;
&lt;li&gt;remote URL은 그대로 두고 비밀번호 자리에 API Token 입력으로 마무리&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>mark340</author>
      <guid isPermaLink="true">https://mark340.tistory.com/102</guid>
      <comments>https://mark340.tistory.com/102#entry102comment</comments>
      <pubDate>Wed, 24 Jun 2026 10:23:21 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] Xcode &amp;quot;no such module Flutter&amp;quot; Error</title>
      <link>https://mark340.tistory.com/101</link>
      <description>&lt;h4 id=&quot;1-lib폴더가-있는-위치로-이동합니다pubspecyaml과-같은-위치&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;1. /lib&lt;/h4&gt;
&lt;pre class=&quot;properties&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;rm -rf pubspec.lock
flutter pub cache clean
flutter clean
flutter pub get&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;4-아래-코드를-또-복붙한다&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;2. /ios&lt;/h4&gt;
&lt;pre class=&quot;properties&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;rm -rf Podfile.lock
rm -rf Pods 
rm -rf ~/Library/Developer/Xcode/DerivedData/*
pod deintegrate
pod install
pod cache clean --all
pod install --repo-update --clean-install&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 애플 실리콘이 아닌, 인텔 맥북에서 제대로 작동이 안 될 경우 아래와 같이&lt;span&gt;&amp;nbsp;&lt;/span&gt;arch&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 붙여 arch -x86_64 pod deintegrate, arch -x86_64 pod install을 입력.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;properties&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;rm -rf Podfile.lock
rm -rf Pods 
rm -rf ~/Library/Developer/Xcode/DerivedData/*
arch -x86_64 pod deintegrate
arch -x86_64 pod install
arch -x86_64 pod cache clean --all
arch -x86_64 pod install --repo-update --clean-install&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;5-xcode를-열어-product---clean-build-folder를-클릭한다&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;3. XCode Product -&amp;gt; Clean Build Folder&lt;br /&gt;4. Build&lt;/h4&gt;</description>
      <author>mark340</author>
      <guid isPermaLink="true">https://mark340.tistory.com/101</guid>
      <comments>https://mark340.tistory.com/101#entry101comment</comments>
      <pubDate>Tue, 18 Mar 2025 18:25:58 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] Flutter의 동작원리 및 Isolate와 쓰레드</title>
      <link>https://mark340.tistory.com/100</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter를 사용하여 애플리케이션을 개발하는 과정에서 UI가 버벅거리거나 성능이 저하되는 현상을 경험한 개발자들이 많을 것이다. 이는 주로 메인 스레드에서 무거운 작업을 처리할 때 발생하는 문제다. 이 문서에서는 Flutter의 내부 아키텍처와 Isolate 시스템의 작동 방식, 그리고 이를 활용한 최적화 방법에 대해 기술적 관점에서 심층적으로 살펴본다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Flutter 아키텍처 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter는 단일 코드베이스로 다양한 플랫폼에서 동작하는 UI 프레임워크다. Flutter 애플리케이션은 다음과 같은 계층 구조로 구성된다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;Flutter Application
│
├── Framework Layer (Dart)
│   ├── Material/Cupertino (디자인 시스템)
│   ├── Widgets (UI 컴포넌트)
│   ├── Rendering (레이아웃, 페인팅)
│   └── Foundation (기본 클래스, 유틸리티)
│
└── Engine Layer (C/C++)
    ├── Skia (그래픽 엔진)
    ├── Dart Runtime
    └── Platform-specific Embedders
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter 애플리케이션은 OS 수준에서 하나의 프로세스로 실행되며, 기본적으로 싱글 스레드 모델을 사용한다. 이 스레드는 UI 렌더링, 이벤트 처리, 비즈니스 로직 실행 등 모든 작업을 담당하며, 일반적으로 &quot;메인 스레드&quot; 또는 &quot;UI 스레드&quot;라고 불린다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 프로세스와 쓰레드의 기본 개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter의 병렬 처리 모델을 이해하기 위해서는 먼저 프로세스와 쓰레드의 개념을 정확히 이해해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구분 프로세스(Process) 쓰레드(Thread)&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;정의&lt;/td&gt;
&lt;td&gt;독립된 메모리 공간을 가지는 실행 단위&lt;/td&gt;
&lt;td&gt;프로세스 내에서 메모리를 공유하는 실행 흐름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메모리 공유 여부&lt;/td&gt;
&lt;td&gt;독립적인 메모리 영역&lt;/td&gt;
&lt;td&gt;동일 프로세스 내에서 메모리 공유&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;생성 비용&lt;/td&gt;
&lt;td&gt;높음 (메모리 할당, 자원 초기화 등)&lt;/td&gt;
&lt;td&gt;낮음 (상대적으로 빠른 생성)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;컨텍스트 스위칭 비용&lt;/td&gt;
&lt;td&gt;높음&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;오류 격리&lt;/td&gt;
&lt;td&gt;완벽한 격리&lt;/td&gt;
&lt;td&gt;제한적인 격리 (한 스레드의 오류가 전체 프로세스에 영향)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 모바일 애플리케이션은 하나의 프로세스로 실행되며, 내부적으로 여러 스레드를 활용하여 다양한 작업을 동시에 처리한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Flutter의 이벤트 루프 모델&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter는 이벤트 루프 기반의 실행 모델을 사용한다. 이는 다음과 같은 구조로 동작한다:&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;메인 스레드 (이벤트 루프)
┌─────────────────────────────────────────┐
│                                         │
│  ┌───────────┐      ┌───────────────┐   │
│  │           │      │               │   │
│  │  이벤트   │ ──▶ │  이벤트 처리  │   │
│  │  큐(Queue)│      │               │   │
│  │           │ ◀─── │               │   │
│  └───────────┘      └───────────────┘   │
│                                         │
└─────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 루프는:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;이벤트 큐에서 다음 이벤트를 가져온다 (사용자 입력, 타이머, 네트워크 응답 등)&lt;/li&gt;
&lt;li&gt;해당 이벤트에 대한 처리 함수를 실행한다&lt;/li&gt;
&lt;li&gt;필요한 경우 UI를 업데이트한다&lt;/li&gt;
&lt;li&gt;다시 1번으로 돌아가 다음 이벤트를 처리한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모델의 핵심적인 특징은 한 번에 하나의 작업만 처리한다는 점이다. 따라서 시간이 오래 걸리는 작업을 실행하면 전체 애플리케이션의 응답성이 저하될 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Isolate: Flutter의 병렬 처리 솔루션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter는 UI 응답성 문제를 해결하기 위해 Isolate라는 독특한 병렬 처리 메커니즘을 제공한다. Isolate는 다음과 같은 특징을 가진다:&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 Isolate의 핵심 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;메모리 격리&lt;/b&gt;: 각 Isolate는 완전히 독립된 메모리 공간(힙)을 가지며, 다른 Isolate와 직접적인 메모리 공유가 불가능하다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메시지 기반 통신&lt;/b&gt;: Isolate 간 데이터 교환은 메시지 전달 방식으로만 이루어진다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;독립적인 이벤트 루프&lt;/b&gt;: 각 Isolate는 자체적인 이벤트 루프를 가지고 독립적으로 작업을 처리한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 Isolate와 OS 스레드의 관계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 내부 구조는 다음과 같다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Flutter 앱은 OS 관점에서 &lt;b&gt;하나의 프로세스&lt;/b&gt;로 실행된다&lt;/li&gt;
&lt;li&gt;Dart VM은 이 프로세스 내에서 여러 개의 &lt;b&gt;OS 스레드&lt;/b&gt;를 관리한다&lt;/li&gt;
&lt;li&gt;각 Isolate는 별도의 OS 스레드 위에서 실행된다&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;Flutter 애플리케이션 (단일 OS 프로세스)
│
├── Main Isolate (UI Thread)
│    └── Dart Event Loop
│
└── Background Isolates (별도 OS 스레드)
     ├── Isolate 1
     │    └── Dart Event Loop
     └── Isolate 2
          └── Dart Event Loop
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Isolate 간 통신 메커니즘&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Isolate는 메모리를 공유하지 않으므로, 데이터 교환을 위해서는 메시지 패싱(Message Passing) 방식을 사용해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 SendPort와 ReceivePort&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter는 Isolate 간 통신을 위해 다음 두 가지 주요 인터페이스를 제공한다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ReceivePort: 메시지를 수신하는 엔드포인트&lt;/li&gt;
&lt;li&gt;SendPort: 다른 Isolate로 메시지를 보내는 엔드포인트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 기본적인 Isolate 통신 예제이다:&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;import 'dart:isolate';

void isolateFunction(SendPort mainSendPort) {
  // 새 Isolate에서 ReceivePort 생성
  final isolateReceivePort = ReceivePort();
  
  // 메인 Isolate에게 이 Isolate의 SendPort 전송
  mainSendPort.send(isolateReceivePort.sendPort);
  
  // 메시지 수신 대기
  isolateReceivePort.listen((message) {
    // 계산 수행 (예: 숫자의 제곱)
    int result = message * message;
    
    // 결과를 메인 Isolate로 전송
    mainSendPort.send(result);
  });
}

Future&amp;lt;void&amp;gt; main() async {
  // 메인 Isolate에서 ReceivePort 생성
  final mainReceivePort = ReceivePort();
  
  // 새 Isolate 생성 및 메인 Isolate의 SendPort 전달
  await Isolate.spawn(isolateFunction, mainReceivePort.sendPort);
  
  // 새 Isolate로부터 SendPort 수신
  final isolateSendPort = await mainReceivePort.first as SendPort;
  
  // 데이터 전송 (숫자 10)
  isolateSendPort.send(10);
  
  // 결과 수신 대기
  mainReceivePort.listen((response) {
    print('결과: $response'); // 결과: 100
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 데이터 전달 시 고려사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Isolate 간 메시지 전달 시 주의해야 할 점:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데이터는 **복사(copy)**되므로, 대용량 데이터 전송 시 성능 저하 가능성&lt;/li&gt;
&lt;li&gt;전송 가능한 데이터 타입이 제한됨 (기본 타입, List, Map, 등)&lt;/li&gt;
&lt;li&gt;참조 타입의 경우 객체의 참조가 아닌 내용이 복사됨&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대용량 데이터를 효율적으로 전송하려면 TransferableTypedData를 사용하여 복사 없이 소유권을 이전하는 방식을 활용할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Flutter 엔진과 Dart VM의 역할&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter 앱의 실행 환경은 두 가지 주요 구성 요소로 이루어져 있다:&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 Flutter 엔진&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;C/C++로 구현된 핵심 런타임 환경&lt;/li&gt;
&lt;li&gt;Skia 그래픽 라이브러리를 통한 렌더링 처리&lt;/li&gt;
&lt;li&gt;플랫폼별 특화 기능 제공&lt;/li&gt;
&lt;li&gt;Dart VM 임베딩 및 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 Dart VM&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dart 코드 실행 환경 제공&lt;/li&gt;
&lt;li&gt;JIT(Just-In-Time) 및 AOT(Ahead-Of-Time) 컴파일 지원&lt;/li&gt;
&lt;li&gt;가비지 컬렉션(GC) 수행&lt;/li&gt;
&lt;li&gt;Isolate 생성 및 OS 스레드 할당&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dart VM은 Isolate가 생성될 때마다 내부적으로 새로운 OS 스레드를 할당하여 해당 Isolate를 실행한다. 이는 Flutter에서 진정한 병렬 처리가 가능한 이유다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 실제 개발에서의 Isolate 활용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 적합한 사용 시나리오&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;CPU 집약적 작업&lt;/b&gt;: 이미지 처리, 암호화, 복잡한 알고리즘 연산&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대용량 데이터 처리&lt;/b&gt;: JSON 파싱, 데이터 변환, 필터링&lt;/li&gt;
&lt;li&gt;&lt;b&gt;백그라운드 작업&lt;/b&gt;: 주기적 데이터 동기화, 대용량 파일 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 이미지 처리 예제&lt;/h3&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;import 'dart:isolate';
import 'dart:typed_data';
import 'package:image/image.dart' as img;

// Isolate에서 실행될 함수
void processImageIsolate(Map&amp;lt;String, dynamic&amp;gt; data) {
  SendPort sendPort = data['sendPort'];
  Uint8List imageBytes = data['imageBytes'];
  
  // 이미지 디코딩
  img.Image? image = img.decodeImage(imageBytes);
  
  if (image != null) {
    // 이미지 처리 (흑백 변환)
    img.Image grayscale = img.grayscale(image);
    
    // 처리된 이미지 인코딩
    Uint8List processedBytes = img.encodeJpg(grayscale) as Uint8List;
    
    // 결과 반환
    sendPort.send(processedBytes);
  } else {
    sendPort.send(null);
  }
}

// 메인 코드에서 호출하는 함수
Future&amp;lt;Uint8List?&amp;gt; processImage(Uint8List imageBytes) async {
  final ReceivePort receivePort = ReceivePort();
  
  await Isolate.spawn(
    processImageIsolate, 
    {
      'sendPort': receivePort.sendPort,
      'imageBytes': imageBytes,
    }
  );
  
  // 결과 대기
  final processedBytes = await receivePort.first;
  return processedBytes != null ? processedBytes as Uint8List : null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.3 compute() 함수 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter는 Isolate를 간편하게 사용할 수 있는 compute() 유틸리티 함수를 제공한다:&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;import 'package:flutter/foundation.dart';
import 'dart:typed_data';

Future&amp;lt;Uint8List?&amp;gt; processImage(Uint8List imageBytes) async {
  return await compute(_processImageFunction, imageBytes);
}

// 독립적인 순수 함수로 구현해야 함
Uint8List? _processImageFunction(Uint8List imageBytes) {
  // 이미지 처리 로직
  return processedImageBytes;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수는 내부적으로 Isolate 생성, 메시지 전달, Isolate 종료 등의 작업을 자동으로 처리해준다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. Isolate 사용 시 주의사항 및 최적화 기법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.1 주의사항&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Isolate 생성 비용&lt;/b&gt;: Isolate 생성은 비용이 큰 작업이므로, 빈번한 생성/소멸은 피해야 한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UI 접근 불가&lt;/b&gt;: 백그라운드 Isolate에서는 UI 위젯이나 상태에 직접 접근할 수 없다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;플랫폼 채널 제한&lt;/b&gt;: 백그라운드 Isolate에서는 플랫폼 채널을 직접 호출할 수 없다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디버깅 복잡성&lt;/b&gt;: 다중 Isolate 환경에서는 디버깅이 더 복잡해질 수 있다&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.2 최적화 기법&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Isolate 풀링&lt;/b&gt;: 여러 작업을 처리하기 위해 Isolate를 재사용하는 패턴 구현&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 최소화&lt;/b&gt;: Isolate 간 전달되는 데이터를 최소화하여 복사 비용 절감&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TransferableTypedData&lt;/b&gt;: 대용량 데이터 전송 시 소유권 이전 방식 활용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;적절한 작업 크기&lt;/b&gt;: 너무 작은 작업에 Isolate를 사용하면 오히려 오버헤드가 커질 수 있음&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 Isolate 풀링 패턴의 간단한 예제이다:&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;class IsolatePool {
  final List&amp;lt;_IsolateData&amp;gt; _isolates = [];
  final int maxIsolates;
  
  IsolatePool({this.maxIsolates = 4});
  
  Future&amp;lt;void&amp;gt; initialize() async {
    for (int i = 0; i &amp;lt; maxIsolates; i++) {
      final receivePort = ReceivePort();
      final isolate = await Isolate.spawn(_isolateEntryPoint, receivePort.sendPort);
      final sendPort = await receivePort.first as SendPort;
      
      _isolates.add(_IsolateData(
        isolate: isolate,
        sendPort: sendPort,
        busy: false,
      ));
    }
  }
  
  Future&amp;lt;T&amp;gt; compute&amp;lt;T&amp;gt;(Function(dynamic) func, dynamic message) async {
    // 사용 가능한 Isolate 찾기
    final isolateData = await _getAvailableIsolate();
    final completer = Completer&amp;lt;T&amp;gt;();
    
    // 작업 실행 및 결과 수신 로직
    // ...
    
    return completer.future;
  }
  
  // 사용 가능한 Isolate 찾거나 대기
  Future&amp;lt;_IsolateData&amp;gt; _getAvailableIsolate() async {
    // 구현 로직
  }
  
  // 리소스 정리
  void dispose() {
    for (final data in _isolates) {
      data.isolate.kill();
    }
    _isolates.clear();
  }
}

class _IsolateData {
  final Isolate isolate;
  final SendPort sendPort;
  bool busy;
  
  _IsolateData({
    required this.isolate,
    required this.sendPort,
    this.busy = false,
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. Isolate vs 일반적인 멀티스레딩 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter의 Isolate 모델은 일반적인 멀티스레딩과 다음과 같은 차이점을 가진다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구분 일반 멀티스레딩 Flutter Isolate&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;메모리 모델&lt;/td&gt;
&lt;td&gt;공유 메모리&lt;/td&gt;
&lt;td&gt;메모리 격리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;동기화 메커니즘&lt;/td&gt;
&lt;td&gt;락(Lock), 세마포어 등&lt;/td&gt;
&lt;td&gt;메시지 패싱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 접근&lt;/td&gt;
&lt;td&gt;직접 접근 (동기화 필요)&lt;/td&gt;
&lt;td&gt;메시지를 통한 간접 접근&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;복잡성&lt;/td&gt;
&lt;td&gt;교착 상태, 경쟁 조건 등 발생 가능&lt;/td&gt;
&lt;td&gt;단순하고 안전한 병렬 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;디버깅&lt;/td&gt;
&lt;td&gt;복잡함&lt;/td&gt;
&lt;td&gt;상대적으로 용이함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Isolate 모델은 멀티스레딩의 복잡한 동기화 문제를 설계 단계에서 원천적으로 방지하여 안정적인 병렬 처리를 가능하게 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter의 Isolate는 UI 응답성을 유지하면서 복잡한 작업을 효율적으로 처리할 수 있는 강력한 메커니즘이다. 요약하면:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구분 설명&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Flutter 앱(OS 관점)&lt;/td&gt;
&lt;td&gt;하나의 프로세스로 실행됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dart VM 내부 구조&lt;/td&gt;
&lt;td&gt;여러 OS 스레드를 관리하며 각 스레드 위에 Dart Isolate를 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;각 Isolate 특징&lt;/td&gt;
&lt;td&gt;독립된 메모리 공간, 메시지 기반 통신, 자체 이벤트 루프&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Isolate의 메모리 격리 모델은 멀티스레딩의 복잡성 없이도 효율적인 병렬 처리를 가능하게 하며, 이를 통해 Flutter 애플리케이션은 복잡한 작업을 처리하면서도 60fps의 부드러운 UI 성능을 유지할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올바른 Isolate 활용 방법을 익히고 적용한다면, 복잡한 Flutter 애플리케이션에서도 뛰어난 성능과 사용자 경험을 제공할 수 있을 것이다.&lt;/p&gt;</description>
      <author>mark340</author>
      <guid isPermaLink="true">https://mark340.tistory.com/100</guid>
      <comments>https://mark340.tistory.com/100#entry100comment</comments>
      <pubDate>Fri, 14 Mar 2025 15:05:58 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] 백그라운드에서의 BLE 데이터 동기화: Android Native 통합</title>
      <link>https://mark340.tistory.com/99</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;모바일 애플리케이션에서 백그라운드 작업 처리, 특히 블루투스 저전력(BLE) 장치와의 지속적인 통신은 기술적으로 복잡한 문제입니다. 이러한 도전은 Flutter와 같은 크로스 플랫폼 프레임워크를 사용할 때 더욱 증폭됩니다. 본 글에서는 Flutter 애플리케이션이 백그라운드 상태에서도 BLE 디바이스와 안정적으로 통신하기 위한 아키텍처를 설계하고 구현한 과정을 상세히 다룹니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제 정의와 기술적 도전&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 프로젝트 배경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스마트 웨어러블 디바이스와 연동되는 건강 관리 애플리케이션에서는 사용자가 앱을 적극적으로 사용하지 않거나 디바이스 화면이 꺼진 상태에서도 지속적인 데이터 수집이 필요합니다. 이 애플리케이션은 다음과 같은 기능이 요구되었습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;실시간 데이터 수집&lt;/b&gt;: 걸음 수, 심박수, 수면 패턴 등의 사용자 건강 데이터를 지속적으로 수집&lt;/li&gt;
&lt;li&gt;&lt;b&gt;백그라운드 동기화&lt;/b&gt;: 앱이 포그라운드에 있지 않을 때도 일정 주기로 웨어러블 디바이스와 데이터 동기화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배터리 효율성&lt;/b&gt;: 지속적인 BLE 연결을 유지하면서도 배터리 소모 최소화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안정적인 연결 유지&lt;/b&gt;: 앱 상태 변화와 무관하게 BLE 연결 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 기술적 제약 사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 요구사항은 다음과 같은 기술적 제약에 직면했습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;안드로이드 백그라운드 제한&lt;/b&gt;: 안드로이드 8.0(Oreo) 이상에서 도입된 백그라운드 실행 제한으로 인해 앱이 백그라운드 상태일 때 지속적인 작업 수행이 제한됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Flutter 백그라운드 처리 한계&lt;/b&gt;: Flutter 자체만으로는 안드로이드의 백그라운드 서비스를 완전히 활용하기 어려움&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BLE 연결 지속성&lt;/b&gt;: BLE 연결은 앱의 생명주기와 독립적으로 유지되어야 함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;플랫폼 간 호환성&lt;/b&gt;: 네이티브 백그라운드 기능을 최대한 활용하면서도 Flutter의 크로스 플랫폼 이점을 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 여러 접근법과 실패 사례&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 Flutter 플러그인 기반 접근법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 순수한 Flutter 솔루션을 찾기 위해 여러 플러그인을 시도했습니다:&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.1.1 flutter_background_service&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flutter_background_service 플러그인은 Flutter 애플리케이션에서 백그라운드 서비스를 제공하기 위해 설계되었습니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// flutter_background_service 구현 시도
import 'package:flutter_background_service/flutter_background_service.dart';

void initBackgroundService() async {
  final service = FlutterBackgroundService();
  
  await service.configure(
    androidConfiguration: AndroidConfiguration(
      onStart: onStart,
      autoStart: true,
      isForegroundMode: true,
    ),
    iosConfiguration: IosConfiguration(
      autoStart: true,
      onForeground: onStart,
      onBackground: onIosBackground,
    ),
  );
  
  service.startService();
}

@pragma('vm:entry-point')
void onStart(ServiceInstance service) async {
  // 백그라운드 동기화 로직
  while (true) {
    // BLE 데이터 동기화
    await syncBleData();
    await Future.delayed(Duration(minutes: 1));
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실패 원인&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스가 장시간 실행될 때 간헐적인 연결 끊김 발생&lt;/li&gt;
&lt;li&gt;안드로이드의 Doze 모드와 배터리 최적화에 의해 실행 간격이 불규칙해짐&lt;/li&gt;
&lt;li&gt;타사 BLE SDK와의 통합 과정에서 복잡성 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.1.2 flutter_workmanager&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WorkManager를 활용하여 주기적인 백그라운드 작업을 구현하려 했습니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;// flutter_workmanager 구현 시도
import 'package:workmanager/workmanager.dart';

void initializeWorkManager() {
  Workmanager().initialize(callbackDispatcher);
  
  Workmanager().registerPeriodicTask(
    &quot;ble-sync-task&quot;,
    &quot;syncBleData&quot;,
    frequency: Duration(minutes: 15),
    constraints: Constraints(
      networkType: NetworkType.not_required,
      requiresBatteryNotLow: false,
    ),
  );
}

@pragma('vm:entry-point')
void callbackDispatcher() {
  Workmanager().executeTask((taskName, inputData) async {
    switch (taskName) {
      case &quot;syncBleData&quot;:
        await BleService().syncData();
        break;
    }
    return Future.value(true);
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실패 원인&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WorkManager는 지연 가능한 작업에 적합하나, 실시간에 가까운 데이터 수집에는 부적합&lt;/li&gt;
&lt;li&gt;작업 실행 간격이 OS에 의해 조정되어 정확한 주기 보장 불가&lt;/li&gt;
&lt;li&gt;작업 실행 시간이 제한되어 복잡한 BLE 동기화 작업 완료 불가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.1.3 background_fetch&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS의 Background Fetch와 안드로이드의 JobScheduler/AlarmManager를 활용하는 background_fetch 플러그인도 시도했습니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// background_fetch 구현 시도
import 'package:background_fetch/background_fetch.dart';

void initBackgroundFetch() {
  BackgroundFetch.configure(
    BackgroundFetchConfig(
      minimumFetchInterval: 15,
      stopOnTerminate: false,
      enableHeadless: true,
      startOnBoot: true,
    ),
    (String taskId) async {
      // BLE 데이터 동기화
      await BleService().syncData();
      BackgroundFetch.finish(taskId);
    },
    (String taskId) async {
      // 타임아웃 처리
      BackgroundFetch.finish(taskId);
    }
  );
}

// 헤드리스 모드 설정
@pragma('vm:entry-point')
void backgroundFetchHeadlessTask(HeadlessTask task) async {
  String taskId = task.taskId;
  bool isTimeout = task.timeout;
  
  if (isTimeout) {
    BackgroundFetch.finish(taskId);
    return;
  }
  
  // BLE 데이터 동기화
  await BleService().syncData();
  BackgroundFetch.finish(taskId);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실패 원인&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실행 간격의 최소값이 15분으로 제한되어 있어 더 빈번한 동기화가 필요한 경우 적합하지 않음&lt;/li&gt;
&lt;li&gt;운영체제에 의해 백그라운드 실행이 지연되거나 제한될 수 있음&lt;/li&gt;
&lt;li&gt;복잡한 BLE 동작에 필요한 기능이 제한적&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 Flutter Isolate 활용 시도&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter의 Isolate를 사용하여 메인 스레드와 독립적인 백그라운드 처리를 시도했습니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// Isolate 기반 접근법
import 'dart:isolate';
import 'dart:ui';

@pragma('vm:entry-point')
void isolateEntryPoint(SendPort sendPort) async {
  final receivePort = ReceivePort();
  sendPort.send(receivePort.sendPort);
  
  receivePort.listen((message) async {
    if (message == 'start_sync') {
      while (true) {
        try {
          // BLE 데이터 동기화
          await syncBleData();
          await Future.delayed(Duration(minutes: 1));
        } catch (e) {
          print('Sync error: $e');
        }
      }
    }
  });
}

void startBackgroundProcessing() async {
  final receivePort = ReceivePort();
  await Isolate.spawn(isolateEntryPoint, receivePort.sendPort);
  
  final SendPort sendPort = await receivePort.first;
  sendPort.send('start_sync');
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실패 원인&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Flutter 엔진이 중지될 때 Isolate도 함께 중지되므로 앱이 백그라운드로 전환될 때 작업 지속 불가&lt;/li&gt;
&lt;li&gt;네이티브 BLE 스택과의 통신을 위해 추가적인 채널 설정이 필요하여 복잡성 증가&lt;/li&gt;
&lt;li&gt;메모리 관리와 리소스 할당 문제 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 하이브리드 솔루션: 최종 아키텍처&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 접근법의 한계를 극복하기 위해, Flutter와 네이티브 안드로이드의 기능을 결합한 하이브리드 아키텍처를 개발했습니다. 이 접근법은 네이티브 기능의 강력함과 Flutter의 사용자 경험 이점을 모두 활용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 아키텍처 개요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 아키텍처는 다음 요소로 구성됩니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;안드로이드 포그라운드 서비스&lt;/b&gt;: 지속적인 실행 보장과 BLE 연결 유지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Flutter 백그라운드 엔진&lt;/b&gt;: 앱이 백그라운드에 있을 때도 Flutter 코드 실행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Method Channel&lt;/b&gt;: Flutter와 네이티브 코드 간 양방향 통신&lt;/li&gt;
&lt;li&gt;&lt;b&gt;알람 매니저&lt;/b&gt;: 주기적인 동기화 작업 스케줄링&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 안드로이드 포그라운드 서비스 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포그라운드 서비스는 지속적인 BLE 연결을 위한 핵심 컴포넌트입니다:&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// BleService.kt
class BleService : BluetoothLeService() {
    companion object {
        const val CHANNEL_ID = &quot;BRingServiceChannel&quot;
        const val NOTIFICATION_ID = 1
        const val ACTION_UPDATE_STEP_COUNT = &quot;com.apposter.smart_device.UPDATE_STEP_COUNT&quot;
        const val ACTION_LOW_BATTERY = &quot;com.apposter.smart_device.LOW_BATTERY_ALERT&quot;
        private const val BACKGROUND_SYNC_INTERVAL: Long = 1 * 60 * 1000 // 1분 간격
    }

    private var smartDeviceService: IRemoteService? = null
    private lateinit var flutterEngine: FlutterEngine
    private lateinit var backgroundMethodChannel: MethodChannel

    override fun onCreate() {
        super.onCreate()
        
        // 포그라운드 서비스 설정
        createNotificationChannel()
        val notification = createNotification(0, 0, 0)
        startForegroundService(notification)
        
        // Flutter 엔진 초기화
        initializeFlutterEngine()
        
        // BLE 서비스 초기화
        initializeSmartDeviceService()
        
        // 주기적 백그라운드 동기화 설정
        setupBackgroundAlarmManager()
    }

    private fun initializeFlutterEngine() {
        val prefs = getSharedPreferences(&quot;flutter_background_prefs&quot;, Context.MODE_PRIVATE)
        val callbackHandle = prefs.getLong(&quot;callback_handle&quot;, 0L)
        
        val flutterLoader = FlutterInjector.instance().flutterLoader()
        flutterLoader.startInitialization(this)
        flutterLoader.ensureInitializationComplete(this, null)
        
        if (callbackHandle == 0L) {
            Log.e(TAG, &quot;No callback handle registered yet.&quot;)
            flutterEngine = FlutterEngine(this)
        } else {
            val callbackInfo = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle)
            if (callbackInfo == null) {
                Log.e(TAG, &quot;Invalid callback information.&quot;)
                flutterEngine = FlutterEngine(this)
            } else {
                flutterEngine = FlutterEngine(this)
                flutterEngine.dartExecutor.executeDartCallback(
                    DartExecutor.DartCallback(
                        assets,
                        flutterLoader.findAppBundlePath(),
                        callbackInfo
                    )
                )
                Log.d(TAG, &quot;Dart callback executed successfully.&quot;)
            }
        }
        
        backgroundMethodChannel = MethodChannel(
            flutterEngine.dartExecutor.binaryMessenger,
            &quot;com.apposter.smart_device/channel/background&quot;
        )
        
        backgroundMethodChannel.setMethodCallHandler { call, result -&amp;gt;
            // 메서드 콜 처리 로직
            when (call.method) {
                &quot;getDataByDay&quot; -&amp;gt; {
                    try {
                        val type: Int? = call.argument(&quot;type&quot;)
                        val day: Int? = call.argument(&quot;day&quot;)
                        val safeType = type ?: 1
                        val safeDay = day ?: 0
                        
                        if (smartDeviceService != null) {
                            val data = smartDeviceService?.getDataByDay(safeType, safeDay)
                            result.success(data)
                        } else {
                            result.error(&quot;UNAVAILABLE&quot;, &quot;Service not connected&quot;, null)
                        }
                    } catch (e: RemoteException) {
                        result.error(&quot;REMOTE_ERROR&quot;, e.message, null)
                    }
                }
                else -&amp;gt; result.notImplemented()
            }
        }
        
        FlutterEngineCache.getInstance().put(&quot;background_engine&quot;, flutterEngine)
    }

    private fun setupBackgroundAlarmManager() {
        val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
        val intent = Intent(this, BleService::class.java).apply {
            action = &quot;BACKGROUND_SYNC&quot;
        }
        val pendingIntent = PendingIntent.getService(
            this, 1001, intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
        
        val triggerTime = System.currentTimeMillis() + BACKGROUND_SYNC_INTERVAL
        if (Build.VERSION.SDK_INT &amp;gt;= Build.VERSION_CODES.M) {
            alarmManager.setExactAndAllowWhileIdle(
                AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent
            )
        } else {
            alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent)
        }
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        if (intent?.action == &quot;BACKGROUND_SYNC&quot;) {
            backgroundSync()
        }
        return START_STICKY
    }

    private fun backgroundSync() {
        try {
            val isConnected = smartDeviceService?.isConnectBt() ?: false
            if (isConnected) {
                backgroundMethodChannel.invokeMethod(&quot;backgroundServiceInit&quot;, true)
            }
            
            setupBackgroundAlarmManager()
        } catch (e: RemoteException) {
            Log.e(TAG, &quot;Error checking connection state&quot;, e)
        }
    }

    // 기타 필요한 메서드들...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 포그라운드 서비스는:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자에게 알림을 표시하여 시스템에 의해 종료되지 않도록 함&lt;/li&gt;
&lt;li&gt;Flutter 엔진의 백그라운드 인스턴스를 초기화하여 앱이 백그라운드에 있을 때도 Dart 코드 실행 가능&lt;/li&gt;
&lt;li&gt;AlarmManager를 사용하여 주기적인 동기화 작업을 예약&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 MainActivity에서의 BLE 서비스 연결 및 Flutter 통합&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MainActivity는 Flutter UI와 네이티브 BLE 서비스 간의 브릿지 역할을 합니다:&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// MainActivity.kt
class MainActivity : FlutterActivity() {
    private val CHANNEL_SDK = &quot;com.apposter.smart_device/channel/sdk&quot;
    private val CHANNEL_UI = &quot;com.apposter.smart_device/channel/ui&quot;
    private val CHANNEL_MODE = &quot;com.apposter.smart_device/channel/mode&quot;
    private val CHANNEL_BACKGROUND = &quot;com.apposter.smart_device/channel/background&quot;
    
    private var smartDeviceService: IRemoteService? = null
    private var isBind = false
    private lateinit var methodChannel: MethodChannel
    private var isActive = true // 앱 활성화 상태 추적

    private val connection = object : ServiceConnection {
        override fun onServiceConnected(className: ComponentName, service: IBinder) {
            smartDeviceService = IRemoteService.Stub.asInterface(service)
            try {
                smartDeviceService?.registerCallback(mServiceCallback)
                // 기타 초기화 코드...
            } catch (e: RemoteException) {
                Log.e(TAG, &quot;Failed to register callback: ${e.message}&quot;)
            }
        }

        override fun onServiceDisconnected(arg0: ComponentName) {
            isBind = false
        }
    }

    private val mServiceCallback = object : IServiceCallback.Stub() {
        @Throws(RemoteException::class)
        override fun onGetDataByDay(type: Int, timestamp: Long, step: Int, heartrate: Int) {
            val date = Date(timestamp * 1000)
            val sdf = SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;, Locale.getDefault())
            val recorddate = sdf.format(date)
            val data = mapOf(
                &quot;type&quot; to type,
                &quot;time&quot; to recorddate,
                &quot;step&quot; to step,
                &quot;heartrate&quot; to heartrate
            )

            if (isActive) {
                // 앱이 포그라운드 상태일 때 UI로 데이터 전송
                runOnUiThread {
                    methodChannel.invokeMethod(&quot;onGetDataByDay&quot;, data)
                }
            } else {
                // 앱이 백그라운드 상태일 때 백그라운드 엔진으로 데이터 전송
                sendToBackgroundFlutter(&quot;background_onGetDataByDay&quot;, data)
            }
        }

        // 기타 콜백 메서드들...
    }

    private fun sendToBackgroundFlutter(method: String, data: Any?) {
        val cachedFlutterEngine = FlutterEngineCache.getInstance().get(&quot;background_engine&quot;)
        
        if (cachedFlutterEngine != null) {
            val backgroundMethodChannel = MethodChannel(
                cachedFlutterEngine.dartExecutor.binaryMessenger,
                &quot;com.apposter.smart_device/channel/background&quot;
            )
            
            Handler(Looper.getMainLooper()).post {
                try {
                    backgroundMethodChannel.invokeMethod(method, data)
                } catch (e: Exception) {
                    Log.e(TAG, &quot;Failed to send to background Flutter: ${e.message}&quot;)
                }
            }
        }
    }

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        
        methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_SDK)
        
        methodChannel.setMethodCallHandler { call, result -&amp;gt;
            when (call.method) {
                &quot;registerBackgroundCallback&quot; -&amp;gt; {
                    val handle = call.arguments as Long
                    val prefs = getSharedPreferences(&quot;flutter_background_prefs&quot;, Context.MODE_PRIVATE)
                    prefs.edit().putLong(&quot;callback_handle&quot;, handle).apply()
                    result.success(null)
                }
                // 기타 메서드 처리...
            }
        }
    }

    override fun onStart() {
        super.onStart()
        isActive = true
        
        // BLE 서비스 시작 및 바인딩
        val intent = Intent(this, BleService::class.java)
        if (Build.VERSION.SDK_INT &amp;gt;= Build.VERSION_CODES.O) {
            startForegroundService(intent)
        } else {
            startService(intent)
        }
        bindService(intent, connection, Context.BIND_AUTO_CREATE)
    }

    override fun onStop() {
        super.onStop()
        isActive = false
        if (isBind) {
            unbindService(connection)
            isBind = false
        }
    }

    override fun onPause() {
        super.onPause()
        isActive = false
    }

    override fun onResume() {
        super.onResume()
        isActive = true
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 포인트:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;여러 MethodChannel을 통해 Flutter와 네이티브 코드 간 통신 지원&lt;/li&gt;
&lt;li&gt;앱 상태(포그라운드/백그라운드)에 따라 데이터를 적절한 Flutter 인스턴스로 라우팅&lt;/li&gt;
&lt;li&gt;BLE 서비스와의 연결 관리 및 콜백 처리&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4 Flutter 백그라운드 처리 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter 측에서는 백그라운드 처리를 위한 전용 엔트리 포인트를 구현했습니다:&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// background_service.dart
import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:smart_device/constants/device_modes.dart';
import 'package:smart_device/services/smart_device_service.dart';
import 'package:smart_device/utils/app_logger.dart';

const backgroundChannel =
    MethodChannel('com.apposter.smart_device/channel/background');

@pragma('vm:entry-point')
void backgroundMain() {
  WidgetsFlutterBinding.ensureInitialized();

  int currentSyncStep = 0;

  backgroundChannel.setMethodCallHandler((call) async {
    switch (call.method) {
      case 'backgroundServiceInit':
        if (call.arguments == true) {
          currentSyncStep = 0;
          BackgroundService().executeNextSyncStep(currentSyncStep);
        }
        AppLogger.debug(&quot;[Background Method Call] - backgroundServiceInit&quot;);
        break;
      case 'background_onGetDataByDay':
        await SmartDeviceService.instance.onGetDataByDay(call);
        break;
      case 'background_onGetDataByDayEnd':
        currentSyncStep++;
        BackgroundService().executeNextSyncStep(currentSyncStep);
        AppLogger.debug('[Background] Sync step $currentSyncStep completed');
        break;
      default:
        AppLogger.error('Unknown Background Method Call: ${call.method}');
    }
  });
}

class BackgroundService {
  static final BackgroundService _instance = BackgroundService._internal();
  factory BackgroundService() =&amp;gt; _instance;
  BackgroundService._internal();

  static Future&amp;lt;void&amp;gt; initialize() async {
    const channel = MethodChannel('com.apposter.smart_device/channel/sdk');
    final callbackHandle = PluginUtilities.getCallbackHandle(backgroundMain);

    await channel.invokeMethod(
        'registerBackgroundCallback', callbackHandle!.toRawHandle());
  }

  void executeNextSyncStep(int step) {
    if (step &amp;gt; 3) {
      AppLogger.debug('[Background] All sync steps completed!');
      return;
    }

    AppLogger.debug('[Background] Executing sync step: $step');

    switch (step) {
      case 0: // 첫 번째 단계: 당일 활동,수면 데이터
        backgroundChannel.invokeMethod(
            'getDataByDay', {'type': DEVICE_MODE_EXERCISE, 'day': 0});
        break;
      case 1: // 두 번째 단계: 당일 심박수 데이터
        backgroundChannel.invokeMethod(
            'getDataByDay', {'type': DEVICE_MODE_HEART_RATE, 'day': 0});
        break;
      case 2: // 세 번째 단계: 전날 활동,수면 데이터
        backgroundChannel.invokeMethod(
            'getDataByDay', {'type': DEVICE_MODE_EXERCISE, 'day': 1});
        break;
      case 3: // 네 번째 단계: 전날 심박수 데이터
        backgroundChannel.invokeMethod(
            'getDataByDay', {'type': DEVICE_MODE_HEART_RATE, 'day': 1});
        break;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 포인트:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;@pragma('vm:entry-point') 어노테이션으로 백그라운드 시작점 지정&lt;/li&gt;
&lt;li&gt;단계적 동기화 과정을 관리하여 체계적인 데이터 수집&lt;/li&gt;
&lt;li&gt;네이티브에서 전달된 데이터를 적절히 처리하고 다음 단계 실행&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.5 데이터 처리 및 서비스 클래스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter 내부에서 BLE 데이터를 처리하기 위한 서비스 클래스를 구현했습니다:&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// smart_device_service.dart
class SmartDeviceService {
  // Private constructor
  SmartDeviceService._privateConstructor();

  // 활동(Step) 서비스
  static final _userStepService = UserStepService();
  // 심박수 서비스
  static final _userHeartRateService = UserHeartRateService();
  // SPO2 서비스
  static final _userBloodOxygenService = UserBloodOxygenService();
  // Sleep 서비스
  static final _userSleepService = UserSleepService();

  // The single instance of the class
  static final SmartDeviceService instance = SmartDeviceService._privateConstructor();

  // 네이티브에서 전달받은 데이터 처리
  Future&amp;lt;void&amp;gt; onGetDataByDay(MethodCall call) async {
    Map&amp;lt;String, dynamic&amp;gt; data = Map&amp;lt;String, dynamic&amp;gt;.from(call.arguments);
    int type = data['type'];
    String timestamp = data['time'];
    int step = data['step'];
    int heartrate = data['heartrate'];
    
    // 데이터 타입에 따라 적절한 처리
    switch (type) {
      case DEVICE_MODE_EXERCISE:
        await _processStepData(timestamp, step);
        break;
      case DEVICE_MODE_HEART_RATE:
        await _processHeartRateData(timestamp, heartrate);
        break;
      // 기타 데이터 타입 처리...
    }
    
    AppLogger.debug(
        'Processed data - Type: $type, Time: $timestamp, Step: $step, HeartRate: $heartrate');
  }

  Future&amp;lt;void&amp;gt; _processStepData(String timestamp, int step) async {
    if (step &amp;gt; 0) {
      await _userStepService.createUserStep(UserStep(
        stepCount: step,
        distance: calculateDistance(step),
        calories: calculateCalories(step),
        recordedAt: DateTime.parse(timestamp),
      ));
      
      // 데이터 집계 처리
      await DailyRecordsService().aggregateSteps(timestamp, step);
    }
  }

  Future&amp;lt;void&amp;gt; _processHeartRateData(String timestamp, int heartrate) async {
    if (heartrate &amp;gt; 0) {
      await _userHeartRateService.createUserHeartRate(UserHeartRate(
        bpm: heartrate,
        sleepStatus: 0,
        measurementMethod: 'A',
        recordedAt: DateTime.parse(timestamp),
      ));
    }
  }
  
  // 기타 필요한 메서드들...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 서비스 클래스는:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;싱글톤 패턴을 사용하여 일관된 인스턴스 접근 제공&lt;/li&gt;
&lt;li&gt;네이티브에서 전달받은 데이터를 적절한 데이터 모델로 변환하고 처리&lt;/li&gt;
&lt;li&gt;다양한 건강 데이터 유형을 각각의 서비스 클래스가 담당하도록 분리&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 핵심 기술 구현과 도전 해결&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 Flutter와 네이티브 통신 최적화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter와 네이티브 안드로이드 간의 통신은 프로젝트의 성공에 매우 중요했습니다. 이를 최적화하기 위해 다음과 같은 전략을 채택했습니다:&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4.1.1 다중 채널 아키텍처&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 Method Channel을 사용하여 데이터 흐름을 논리적으로 분리했습니다:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// method_channels.dart
import 'package:flutter/services.dart';

class Channels {
  // SDK 관련 기능을 위한 채널
  static const MethodChannel methodSDKChannel = 
      MethodChannel('com.apposter.smart_device/channel/sdk');
  
  // UI 업데이트를 위한 채널
  static const MethodChannel methodUIChannel = 
      MethodChannel('com.apposter.smart_device/channel/ui');
  
  // 운동 모드 설정을 위한 채널
  static const MethodChannel methodModeChannel = 
      MethodChannel('com.apposter.smart_device/channel/mode');
  
  // 백그라운드 작업을 위한 채널
  static const MethodChannel backgroundChannel = 
      MethodChannel('com.apposter.smart_device/channel/background');
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 채널 분리는:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 구조를 더 명확하게 만듦&lt;/li&gt;
&lt;li&gt;각 채널이 특정 책임에 집중하도록 함&lt;/li&gt;
&lt;li&gt;데이터 흐름을 더 효과적으로 추적할 수 있게 함&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4.1.2 앱 상태에 따른 데이터 라우팅&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이 포그라운드에 있는지 백그라운드에 있는지에 따라 데이터를 적절한 대상으로 라우팅하는 메커니즘을 구현했습니다:&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// MainActivity.kt의 콜백 메서드
@Throws(RemoteException::class)
override fun onGetDataByDay(type: Int, timestamp: Long, step: Int, heartrate: Int) {
    val data = mapOf(
        &quot;type&quot; to type,
        &quot;time&quot; to recorddate,
        &quot;step&quot; to step,
        &quot;heartrate&quot; to heartrate
    )

    if (isActive) {
        // 앱이 활성 상태일 때 UI 채널로 데이터 전송
        runOnUiThread {
            methodChannel.invokeMethod(&quot;onGetDataByDay&quot;, data)
        }
    } else {
        // 앱이 백그라운드 상태일 때 백그라운드 채널로 데이터 전송
        sendToBackgroundFlutter(&quot;background_onGetDataByDay&quot;, data)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 접근법의 이점:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앱이 백그라운드에 있을 때도 데이터 처리 가능&lt;/li&gt;
&lt;li&gt;UI 스레드 차단 방지&lt;/li&gt;
&lt;li&gt;일관된 데이터 흐름 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 안드로이드 Doze 모드 우회&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드의 Doze 모드는 배터리 최적화를 위해 백그라운드 작업을 제한하는데, 이를 극복하기 위한 방법을 구현했습니다:&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;private fun setupBackgroundAlarmManager() {
    val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
    val intent = Intent(this, BleService::class.java).apply {
        action = &quot;BACKGROUND_SYNC&quot;
    }
    val pendingIntent = PendingIntent.getService(
        this, 1001, intent,
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
    )
    
    val triggerTime = System.currentTimeMillis() + BACKGROUND_SYNC_INTERVAL
    
    // Doze 모드에서도 실행 보장을 위한 설정
    if (Build.VERSION.SDK_INT &amp;gt;= Build.VERSION_CODES.M) {
        alarmManager.setExactAndAllowWhileIdle(
            AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent
        )
    } else {
        alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;setExactAndAllowWhileIdle는 안드로이드 Doze 모드에서도 알람이 작동하도록 보장합니다. 이 방법은 완벽하진 않지만, 제한된 백그라운드 동작 내에서 최대한의 신뢰성을 확보할 수 있었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 Flutter 엔진의 복수 인스턴스 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter 엔진의 여러 인스턴스를 관리하는 것은 메모리 사용량과 성능 관점에서 도전적이었습니다:&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;private fun initializeFlutterEngine() {
    // Flutter 로더 초기화
    val flutterLoader = FlutterInjector.instance().flutterLoader()
    flutterLoader.startInitialization(this)
    flutterLoader.ensureInitializationComplete(this, null)
    
    // 콜백 핸들 가져오기
    val prefs = getSharedPreferences(&quot;flutter_background_prefs&quot;, Context.MODE_PRIVATE)
    val callbackHandle = prefs.getLong(&quot;callback_handle&quot;, 0L)
    
    if (callbackHandle != 0L) {
        val callbackInfo = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle)
        if (callbackInfo != null) {
            // 백그라운드용 Flutter 엔진 생성 및 Dart 엔트리포인트 실행
            flutterEngine = FlutterEngine(this)
            flutterEngine.dartExecutor.executeDartCallback(
                DartExecutor.DartCallback(
                    assets,
                    flutterLoader.findAppBundlePath(),
                    callbackInfo
                )
            )
            
            // 엔진 캐싱
            FlutterEngineCache.getInstance().put(&quot;background_engine&quot;, flutterEngine)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구현에서 주의해야 할 점:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Flutter 엔진의 생성과 초기화는 리소스 집약적인 작업임&lt;/li&gt;
&lt;li&gt;엔진 인스턴스의 수명 주기를 적절히 관리하여 메모리 누수 방지&lt;/li&gt;
&lt;li&gt;FlutterEngineCache를 활용하여 엔진 재사용 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.4 데이터 일관성 보장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백그라운드에서의 데이터 처리에서 중요한 요소는 데이터 일관성 유지입니다:&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;Future&amp;lt;void&amp;gt; _processStepData(String timestamp, int step) async {
  if (step &amp;lt;= 0) return; // 유효하지 않은 데이터 무시
  
  final DateTime recordDate = DateTime.parse(timestamp);
  final String dateKey = DateFormat('yyyy-MM-dd').format(recordDate);
  
  // 트랜잭션으로 일관성 보장
  await database.transaction((txn) async {
    // 기존 데이터 확인
    final existingData = await txn.query(
      'user_steps',
      where: 'date = ?',
      whereArgs: [dateKey]
    );
    
    if (existingData.isNotEmpty) {
      // 기존 데이터가 있을 경우, 더 큰 값으로만 업데이트
      final int currentSteps = existingData.first['steps'] as int;
      if (step &amp;gt; currentSteps) {
        await txn.update(
          'user_steps',
          {'steps': step, 'updated_at': DateTime.now().toIso8601String()},
          where: 'date = ?',
          whereArgs: [dateKey]
        );
      }
    } else {
      // 새 데이터 삽입
      await txn.insert('user_steps', {
        'date': dateKey,
        'steps': step,
        'recorded_at': timestamp,
        'created_at': DateTime.now().toIso8601String()
      });
    }
  });
  
  // 집계 데이터 업데이트
  await DailyRecordsService().aggregateSteps(timestamp, step);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 접근법은:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션을 사용하여 ACID 속성 보장&lt;/li&gt;
&lt;li&gt;중복 데이터 처리 로직 구현&lt;/li&gt;
&lt;li&gt;데이터 정합성 검증 및 보호&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 백그라운드 작업의 순차적 실행 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백그라운드 작업의 순차적 실행은 데이터의 완전성과 시스템 리소스 관리에 중요했습니다:&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;void executeNextSyncStep(int step) {
  if (step &amp;gt; 3) {
    AppLogger.debug('[Background] All sync steps completed!');
    return;
  }

  AppLogger.debug('[Background] Executing sync step: $step');

  switch (step) {
    case 0: // 첫 번째 단계: 당일 활동,수면 데이터
      backgroundChannel.invokeMethod(
          'getDataByDay', {'type': DEVICE_MODE_EXERCISE, 'day': 0});
      break;
    case 1: // 두 번째 단계: 당일 심박수 데이터
      backgroundChannel.invokeMethod(
          'getDataByDay', {'type': DEVICE_MODE_HEART_RATE, 'day': 0});
      break;
    case 2: // 세 번째 단계: 전날 활동,수면 데이터
      backgroundChannel.invokeMethod(
          'getDataByDay', {'type': DEVICE_MODE_EXERCISE, 'day': 1});
      break;
    case 3: // 네 번째 단계: 전날 심박수 데이터
      backgroundChannel.invokeMethod(
          'getDataByDay', {'type': DEVICE_MODE_HEART_RATE, 'day': 1});
      break;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단계가 완료되면 다음 단계가 실행되도록 하는 이 패턴은:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 수집 작업을 작은 단위로 분할하여 안정성 향상&lt;/li&gt;
&lt;li&gt;각 작업의 진행 상황을 추적 가능&lt;/li&gt;
&lt;li&gt;오류 발생 시 특정 지점부터 복구 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 피드백 메커니즘 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백그라운드 작업의 성공 여부를 확인하기 위한 피드백 메커니즘이 중요했습니다:&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// 백그라운드 Dart 코드에서
backgroundChannel.setMethodCallHandler((call) async {
  switch (call.method) {
    case 'background_onGetDataByDay':
      try {
        await SmartDeviceService.instance.onGetDataByDay(call);
        // 성공 시 네이티브에 알림
        backgroundChannel.invokeMethod('onDataProcessed', {'success': true});
      } catch (e) {
        // 실패 시 오류 정보 전달
        backgroundChannel.invokeMethod('onDataProcessed', {
          'success': false,
          'error': e.toString()
        });
        AppLogger.error('Error processing data: $e');
      }
      break;
    case 'background_onGetDataByDayEnd':
      currentSyncStep++;
      // 다음 단계 실행 전 잠시 지연
      await Future.delayed(Duration(milliseconds: 500));
      BackgroundService().executeNextSyncStep(currentSyncStep);
      AppLogger.debug('[Background] Sync step $currentSyncStep started');
      break;
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이티브 쪽에서의 처리:&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;// BleService.kt
backgroundMethodChannel.setMethodCallHandler { call, result -&amp;gt;
  when (call.method) {
    &quot;onDataProcessed&quot; -&amp;gt; {
      val success = call.argument&amp;lt;Boolean&amp;gt;(&quot;success&quot;) ?: false
      if (!success) {
        val error = call.argument&amp;lt;String&amp;gt;(&quot;error&quot;)
        Log.e(TAG, &quot;Data processing failed: $error&quot;)
        // 필요한 경우 오류 처리 로직 실행
      }
      result.success(null)
    }
    // 기타 메서드 처리...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 피드백 루프는:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작업 성공/실패 여부를 네이티브 코드에 알려줌&lt;/li&gt;
&lt;li&gt;오류 발생 시 적절한 로깅 및 복구 조치 가능&lt;/li&gt;
&lt;li&gt;전체 작업 흐름의 상태 추적 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 성능 최적화 및 배터리 효율성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 배터리 사용량 최소화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BLE 연결과 백그라운드 작업은 배터리를 빠르게 소모할 수 있으므로, 이를 최소화하기 위한 전략을 채택했습니다:&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;private fun determineOptimalSyncInterval(): Long {
    val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
    val batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
    val isCharging = batteryManager.isCharging()
    
    return when {
        isCharging -&amp;gt; 5L * 60 * 1000 // 충전 중일 때 5분마다
        batteryLevel &amp;gt; 50 -&amp;gt; 15L * 60 * 1000 // 배터리가 충분할 때 15분마다
        else -&amp;gt; 30L * 60 * 1000 // 배터리가 부족할 때 30분마다
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 적응형 동기화 간격 조정은:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배터리 상태에 따라 동기화 빈도 조절&lt;/li&gt;
&lt;li&gt;충전 중일 때 더 빈번한 업데이트 제공&lt;/li&gt;
&lt;li&gt;배터리가 부족할 때 필수 업데이트만 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 리소스 사용량 모니터링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백그라운드 작업의 리소스 사용량을 모니터링하고 제한하는 것도 중요했습니다:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private fun monitorResourceUsage() {
    val runtime = Runtime.getRuntime()
    val usedMemory = (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024
    Log.d(TAG, &quot;Used memory: $usedMemory MB&quot;)
    
    if (usedMemory &amp;gt; 100) { // 메모리 사용량이 100MB를 초과하면
        // 불필요한 리소스 정리
        System.gc()
        
        // 필요한 경우 일부 기능 비활성화
        if (usedMemory &amp;gt; 150) {
            Log.w(TAG, &quot;Memory usage too high, disabling non-critical features&quot;)
            // 비필수 기능 비활성화 로직
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스 모니터링의 이점:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리 누수 조기 발견&lt;/li&gt;
&lt;li&gt;과도한 리소스 사용 방지&lt;/li&gt;
&lt;li&gt;안정적인 장기 실행 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 학습된 교훈과 모범 사례&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 플랫폼별 접근 방식의 중요성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter의 크로스 플랫폼 특성에도 불구하고, 백그라운드 작업과 BLE 연결 같은 기능은 플랫폼별 구현이 필요했습니다:&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;Future&amp;lt;bool&amp;gt; isConnectBt() async {
  try {
    if (Platform.isIOS) {
      return await _iosConnectivityCheck();
    } else {
      return await methodChannel.invokeMethod('isConnectBt');
    }
  } catch (e) {
    AppLogger.error(&quot;Failed to check Bluetooth connection: $e&quot;);
    return false;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교훈:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;백그라운드 처리 및 하드웨어 통합은 플랫폼별 구현 필요&lt;/li&gt;
&lt;li&gt;플랫폼별 코드와 공통 코드의 균형 찾기&lt;/li&gt;
&lt;li&gt;각 플랫폼의 특성과 제한 사항 이해하기&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 데이터 흐름 설계의 중요성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 시스템에서 데이터 흐름을 명확히 설계하는 것은 매우 중요했습니다:&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;[BLE 디바이스] &amp;rarr; [네이티브 BLE 서비스] &amp;rarr; [Method Channel] &amp;rarr; [Flutter 로직] &amp;rarr; [로컬 DB]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름을 분리하고 각 단계에 대한 책임을 명확히 정의했습니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;네이티브 BLE 서비스&lt;/b&gt;: 장치 연결 및 원시 데이터 수신&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Method Channel&lt;/b&gt;: 데이터 변환 및 전달&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Flutter 로직&lt;/b&gt;: 비즈니스 로직 및 데이터 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로컬 DB&lt;/b&gt;: 영구 저장 및 쿼리&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.3 점진적 구현과 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 기능을 한 번에 구현하는 대신, 점진적인 접근 방식을 채택했습니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;먼저 간단한 포그라운드 통신 구현&lt;/b&gt;: 기본 Flutter-네이티브 통신 검증&lt;/li&gt;
&lt;li&gt;&lt;b&gt;포그라운드 서비스 추가&lt;/b&gt;: 지속적인 BLE 연결 확립&lt;/li&gt;
&lt;li&gt;&lt;b&gt;백그라운드 Flutter 엔진 통합&lt;/b&gt;: 백그라운드 데이터 처리 능력 추가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;알람 매니저를 통한 주기적 동기화 추가&lt;/b&gt;: 지속적인 데이터 수집 보장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배터리 최적화 및 성능 튜닝&lt;/b&gt;: 사용자 경험 향상&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단계에서 철저한 테스트를 수행하고 안정성을 확인한 후 다음 단계로 진행했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter와 네이티브 안드로이드 기능을 결합한 하이브리드 접근법을 통해 안정적인 백그라운드 BLE 데이터 동기화를 구현할 수 있었습니다. 순수 Flutter 플러그인이나 단일 접근법의 한계를 극복하기 위해 여러 기술을 조합한 것이 핵심 성공 요인이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구현은 다음과 같은 이점을 제공합니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;안정적인 백그라운드 동작&lt;/b&gt;: 안드로이드의 포그라운드 서비스를 통해 지속적인 실행 보장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Flutter 코드 재사용&lt;/b&gt;: 백그라운드와 포그라운드에서 동일한 Flutter 로직 활용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;효율적인 리소스 사용&lt;/b&gt;: 배터리 및 메모리 최적화 전략 적용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장 가능한 아키텍처&lt;/b&gt;: 새로운 기능이나 데이터 유형 쉽게 추가 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 접근법은 건강 모니터링 앱 외에도 백그라운드 작업과 하드웨어 통합이 필요한 다양한 Flutter 애플리케이션에 적용할 수 있습니다. 플랫폼별 특성을 이해하고 활용하는 것이 크로스 플랫폼 개발에서도 중요하다는 점을 보여주는 사례라 할 수 있습니다.&lt;/p&gt;</description>
      <author>mark340</author>
      <guid isPermaLink="true">https://mark340.tistory.com/99</guid>
      <comments>https://mark340.tistory.com/99#entry99comment</comments>
      <pubDate>Thu, 13 Mar 2025 17:35:00 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] CocoaPods could not find compatible versions for pod &amp;quot;Firebase/Auth&amp;quot;- CocoaPods 의존성 및 버전 관련 에러 해결하기</title>
      <link>https://mark340.tistory.com/98</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CocoaPods를 통한 iOS 의존성 설치 중 다음과 같은 에러가 발생했습니다:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;[!] :
  In snapshot (Podfile.lock):
    Firebase/Auth (= 11.0.0)
  In Podfile:
    firebase_auth depends on Firebase/Auth (= 11.6.0)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;발생 원인 분석&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Firebase 패키지 버전 간의 불일치&lt;/li&gt;
&lt;li&gt;CocoaPods 스펙 저장소의 만료&lt;/li&gt;
&lt;li&gt;Podfile.lock과 실제 의존성 요구사항의 충돌&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;에러 유형별 해결 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 버전 불일치 에러&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Could not find compatible versions for pod &quot;Firebase/Auth&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;해결 방법:&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;pubspec.yaml의 Firebase 패키지 버전 동기화:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dependencies: firebase_core: ^2.24.2 firebase_auth: ^4.15.3 firebase_analytics: ^10.7.4&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;이전 설치 정보 제거:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cd ios rm -rf Pods/ rm -rf Podfile.lock&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. CocoaPods 스펙 저장소 만료&lt;/h3&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;Error: CocoaPods's specs repository is too out-of-date to satisfy dependencies.&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;해결 방법:&lt;/h4&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;pod repo remove trunk
pod setup
pod install&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 의존성 충돌&lt;/h3&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;[!] CocoaPods could not resolve dependencies for target 'Runner'&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;해결 방법:&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;의존성 캐시 정리:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pod deintegrate pod cache clean --all&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Podfile 업데이트:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;platform :ios, '12.0'&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;platform :ios, '12.0'

target 'Runner' do
  use_frameworks!
  use_modular_headers!
  
  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
    end
  end
end&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CocoaPods 의존성 관리 이해하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Podfile.lock의 역할&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설치된 의존성의 정확한 버전을 기록&lt;/li&gt;
&lt;li&gt;팀원 간 동일한 버전 사용 보장&lt;/li&gt;
&lt;li&gt;의존성 잠금으로 예기치 않은 업데이트 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;버전 지정 방식&lt;/h3&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;pod 'Firebase/Auth', '11.6.0'         # 정확한 버전
pod 'Firebase/Auth', '~&amp;gt; 11.6.0'      # 패치 업데이트만 허용
pod 'Firebase/Auth', '&amp;gt;= 11.6.0'      # 최소 버전 지정&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CDN vs. Git Sources&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# CDN 사용
source 'https://cdn.cocoapods.org/'

# Git 소스 사용
pod 'CustomPod', :git =&amp;gt; 'https://github.com/user/repo.git'&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;종합적인 해결 프로세스&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;의존성 초기화:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cd ios rm -rf Pods/ rm -rf Podfile.lock pod deintegrate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;CocoaPods 업데이트:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pod repo update pod install --repo-update&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Flutter 프로젝트 클린:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cd .. flutter clean flutter pub get&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;재설치:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cd ios pod install&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 해결 Tips&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;버전 호환성 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Firebase 등 주요 패키지의 호환성 매트릭스 확인&lt;/li&gt;
&lt;li&gt;Flutter 버전과 iOS 최소 지원 버전 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Pod 설치 옵션&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pod install --verbose # 자세한 로그 확인 pod update --no-repo-update # 저장소 업데이트 없이 설치 pod install --repo-update # 저장소 업데이트 포함 설치&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;CocoaPods 캐시 관리&lt;/li&gt;
&lt;li&gt;&lt;code&gt;# 특정 Pod 캐시 삭제 pod cache clean 'Firebase'&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;전체 캐시 삭제&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pod cache clean --all&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;```&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://guides.cocoapods.org/&quot;&gt;CocoaPods Guides&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://firebase.google.com/docs/ios/setup&quot;&gt;Firebase iOS Installation Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://guides.cocoapods.org/using/the-podfile.html&quot;&gt;CocoaPods Dependency Resolution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://firebase.flutter.dev/docs/overview/&quot;&gt;Flutter Firebase Setup&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>mark340</author>
      <guid isPermaLink="true">https://mark340.tistory.com/98</guid>
      <comments>https://mark340.tistory.com/98#entry98comment</comments>
      <pubDate>Fri, 7 Feb 2025 15:09:47 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] Error (Xcode): Undefined symbol - iOS 빌드 시 링커 에러 해결</title>
      <link>https://mark340.tistory.com/97</link>
      <description>&lt;h2&gt;문제 상황&lt;/h2&gt;
&lt;p&gt;Flutter iOS 빌드 중 다음과 같은 링커 에러가 발생했습니다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Error (Xcode): Undefined symbol: _GULIsLoggableLevel
Error (Xcode): Undefined symbol: _GULOSLogBasic
Error (Xcode): Undefined symbol: _GULOSLogError
Error (Xcode): Undefined symbol: _GULOSLogInfo
Error (Xcode): Undefined symbol: _GULOSLogWarning
Error (Xcode): Undefined symbol: _GULSetLoggerLevel
Error (Xcode): Undefined symbol: *OBJC*CLASS_$_GULAppDelegateSwizzler
...&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Undefined Symbol 에러란?&lt;/h2&gt;
&lt;p&gt;링커 단계에서 발생하는 이 에러는 컴파일된 코드가 참조하는 심볼(함수, 클래스, 변수 등)을 찾지 못할 때 발생합니다. 이 경우는 Google Utilities Library(GUL) 관련 심볼들을 찾지 못해 발생한 문제입니다.&lt;/p&gt;
&lt;h2&gt;원인 분석&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;라이브러리 링킹 실패&lt;/li&gt;
&lt;li&gt;빌드 설정의 부적절한 구성&lt;/li&gt;
&lt;li&gt;Framework 검색 경로 문제&lt;/li&gt;
&lt;li&gt;Swift 표준 라이브러리 포함 설정 문제&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;해결 과정&lt;/h2&gt;
&lt;h3&gt;1. Framework 검색 경로 확인&lt;/h3&gt;
&lt;p&gt;Xcode에서 Framework 검색 경로가 올바르게 설정되어 있는지 확인:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Build Settings &amp;gt; Framework Search Paths&lt;/li&gt;
&lt;li&gt;Pod 설치 경로가 올바르게 포함되어 있는지 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 링커 플래그 설정&lt;/h3&gt;
&lt;p&gt;OTHER_LDFLAGS에 필요한 플래그들이 포함되어 있는지 확인:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Build Settings &amp;gt; Other Linker Flags &amp;gt; $(inherited)&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;3. Swift 표준 라이브러리 설정&lt;/h3&gt;
&lt;p&gt;ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES 설정 확인:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Build Settings &amp;gt; Always Embed Swift Standard Libraries &amp;gt; $(inherited)&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;4. 최종 해결: Xcode 빌드 설정 수정&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Xcode에서 Runner.xcworkspace 열기&lt;/li&gt;
&lt;li&gt;Runner 프로젝트의 Runner 타겟 선택&lt;/li&gt;
&lt;li&gt;Build Settings 탭에서:&lt;pre&gt;&lt;code&gt;ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = $(inherited)
OTHER_LDFLAGS = $(inherited)&lt;/code&gt;&lt;/pre&gt;모든 빌드 구성(Debug, Release, Profile)에 대해 적용&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;기술적 이해&lt;/h2&gt;
&lt;h3&gt;$(inherited) 설정의 중요성&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$(inherited)&lt;/code&gt;는 상위 설정(예: CocoaPods 설정)의 값을 상속받도록 지정&lt;/li&gt;
&lt;li&gt;이 설정이 없으면 필수적인 링커 플래그나 프레임워크 설정이 무시될 수 있음&lt;/li&gt;
&lt;li&gt;특히 CocoaPods를 사용할 때 중요한 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;링커 작동 방식&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;컴파일러가 소스 코드를 목적 파일로 변환&lt;/li&gt;
&lt;li&gt;링커가 목적 파일들을 하나의 실행 파일로 결합&lt;/li&gt;
&lt;li&gt;이 과정에서 모든 심볼 참조가 해결되어야 함&lt;/li&gt;
&lt;li&gt;해결되지 않은 심볼이 있으면 Undefined Symbol 에러 발생&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Framework 검색 과정&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;링커는 지정된 Framework 검색 경로를 순서대로 검색&lt;/li&gt;
&lt;li&gt;필요한 심볼이 포함된 Framework를 찾으면 링킹&lt;/li&gt;
&lt;li&gt;검색 경로에서 필요한 Framework를 찾지 못하면 에러 발생&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/XcodeBuildSystem/000-Introduction/Introduction.html&quot;&gt;Xcode Build System Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/DynamicLibraries/100-Articles/LinkingDynamicLibraries.html&quot;&gt;Link-Time Issues&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/WeakLinking.html&quot;&gt;Working with Frameworks and Weak Linking&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>mark340</author>
      <guid isPermaLink="true">https://mark340.tistory.com/97</guid>
      <comments>https://mark340.tistory.com/97#entry97comment</comments>
      <pubDate>Fri, 7 Feb 2025 15:05:38 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] Android build.gradle minifyEnabled/shrinkResources - 안드로이드 빌드 최적화와 flutter_local_notifications 패키지 release 모드 에러 해결</title>
      <link>https://mark340.tistory.com/96</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter 앱을 릴리즈할 때 안드로이드 빌드 최적화는 필수적입니다. 하지만 최적화 과정에서 예상치 못한 런타임 에러를 마주치기도 합니다. 이번 글에서는 안드로이드 빌드 최적화 설정을 자세히 살펴보고, 실제 발생한 에러 케이스와 해결 과정을 공유하고자 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;안드로이드 빌드 최적화 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;minifyEnabled의 동작 원리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;minifyEnabled&lt;/code&gt;는 R8 컴파일러(이전의 ProGuard)를 사용하여 다음과 같은 최적화를 수행합니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;코드 축소(Code Shrinking)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용되지 않는 클래스, 필드, 메서드 제거&lt;/li&gt;
&lt;li&gt;라이브러리에서 사용하지 않는 부분 제거&lt;/li&gt;
&lt;li&gt;데드 코드 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;난독화(Obfuscation)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클래스, 메서드, 필드명을 의미 없는 짧은 이름으로 변경&lt;/li&gt;
&lt;li&gt;리버스 엔지니어링 방지&lt;/li&gt;
&lt;li&gt;앱 크기 축소 효과&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;최적화(Optimization)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메서드 인라이닝&lt;/li&gt;
&lt;li&gt;중복 코드 제거&lt;/li&gt;
&lt;li&gt;상수 전파(Constant propagation)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;shrinkResources의 역할과 중요성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;shrinkResources&lt;/code&gt;는 리소스 최적화를 담당하며 다음과 같은 작업을 수행합니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;리소스 정리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용하지 않는 레이아웃 파일 제거&lt;/li&gt;
&lt;li&gt;참조되지 않는 이미지 리소스 제거&lt;/li&gt;
&lt;li&gt;미사용 문자열 리소스 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;리소스 최적화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리소스 파일 압축&lt;/li&gt;
&lt;li&gt;중복 리소스 제거&lt;/li&gt;
&lt;li&gt;리소스 병합&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;android {
    buildTypes {
        release {
            minifyEnabled true    // 코드 최적화 활성화
            shrinkResources true  // 리소스 최적화 활성화
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최적화로 인한 런타임 에러 케이스 스터디&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;발생한 문제 상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter Local Notifications 플러그인을 사용하여 로컬 알림을 구현했는데, 디버그 빌드에서는 정상 작동하지만 릴리즈 빌드에서 다음과 같은 에러가 발생했습니다:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;E/AndroidRuntime: FATAL EXCEPTION: main
    Process: cohttp://m.apposter.bring, PID: 28133
    java.lang.RuntimeException: Unable to start receiver com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver: java.lang.RuntimeException: Missing type parameter.
    at android.app.ActivityThread.handleReceiver(ActivityThread.java:4905)
    ...
    Caused by: java.lang.RuntimeException: Missing type parameter.
    at cohttp://m.google.gson.reflect.a.getSuperclassTypeParameter(Unknown Source:26)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 분석&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스택 트레이스 분석
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GSON 라이브러리의 리플렉션 관련 에러&lt;/li&gt;
&lt;li&gt;타입 파라미터 정보가 최적화 과정에서 손실됨&lt;/li&gt;
&lt;li&gt;Local Notifications 플러그인의 리시버 클래스에서 문제 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;원인 파악
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;minifyEnabled로 인한 GSON 클래스 정보 손실&lt;/li&gt;
&lt;li&gt;제네릭 타입 정보가 난독화 과정에서 제거됨&lt;/li&gt;
&lt;li&gt;플러그인의 직렬화/역직렬화 과정 실패&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단계별 해결 과정&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. proguard-rules.pro&lt;br /&gt;기본 ProGuard 규칙 추가&lt;/p&gt;
&lt;pre id=&quot;code_1736735767143&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Flutter Local Notifications Plugin
-keep class com.dexterous.** { *; }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➜&amp;nbsp;에러&amp;nbsp;지속&amp;nbsp;발생&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. GSON 관련 규칙 추가&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# Flutter Local Notifications Plugin
-keep class com.dexterous.** { *; }

# GSON 기본 설정
-keepattributes Signature
-keepattributes *Annotation*
-dontwarn sun.misc.**
-keep class com.google.gson.** { *; }

# GSON 직렬화 관련 클래스 보존
-keep class * extends com.google.gson.TypeAdapter
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;3&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;빌드 캐시 정리 및 재빌드&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;flutter clean
cd android
./gradlew clean
cd ..
flutter build apk --release&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;검증 및 테스트&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;알림 예약 기능 테스트&lt;/li&gt;
&lt;li&gt;알림 수신 확인&lt;/li&gt;
&lt;li&gt;백그라운드 동작 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최적화 설정 사용 시 주의사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 라이브러리 호환성 확인&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용하는 라이브러리의 ProGuard 설정 문서 확인&lt;/li&gt;
&lt;li&gt;라이브러리 GitHub 이슈 트래커 참고&lt;/li&gt;
&lt;li&gt;테스트 빌드로 충분한 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 단계적 최적화 적용&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;android {
    buildTypes {
        release {
            // 1단계: 코드 축소만 적용
            minifyEnabled true
            shrinkResources false

            // 2단계: 리소스 축소까지 적용
            minifyEnabled true
            shrinkResources true

            // 3단계: 추가 최적화 규칙 적용
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 디버깅 설정&lt;/h3&gt;
&lt;pre class=&quot;ldif&quot;&gt;&lt;code&gt;# 에러 추적을 위한 소스 파일명 보존
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성능 최적화 결과&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;최적화 설정&lt;/th&gt;
&lt;th&gt;APK 크기&lt;/th&gt;
&lt;th&gt;장단점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;미적용&lt;/td&gt;
&lt;td&gt;24MB&lt;/td&gt;
&lt;td&gt;안정적이나 큰 용량&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;minifyEnabled만 적용&lt;/td&gt;
&lt;td&gt;18MB&lt;/td&gt;
&lt;td&gt;중간 크기, 높은 안정성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모두 적용&lt;/td&gt;
&lt;td&gt;12MB&lt;/td&gt;
&lt;td&gt;최소 크기, 추가 설정 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 빌드 최적화는 앱의 크기를 줄이고 성능을 향상시키는 중요한 과정입니다. 하지만 최적화 과정에서 발생할 수 있는 문제들을 이해하고, 적절한 ProGuard 규칙을 작성하는 것이 중요합니다. 특히 서드파티 라이브러리를 사용할 때는 더욱 신중한 접근이 필요합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/studio/build/shrink-code&quot;&gt;Android Developer Guide - Shrink, obfuscate, and optimize your app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://r8.dev/&quot;&gt;R8 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pub.dev/packages/flutter_local_notifications&quot;&gt;Flutter Local Notifications Plugin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.guardsquare.com/manual/configuration/usage&quot;&gt;ProGuard Manual&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>mark340</author>
      <guid isPermaLink="true">https://mark340.tistory.com/96</guid>
      <comments>https://mark340.tistory.com/96#entry96comment</comments>
      <pubDate>Mon, 13 Jan 2025 11:33:10 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] Isolate에 대한 이해와 활용 사례 / PlatformException(PermissionHandler.PermissionManager, Unable to detect current Android Activity., null, null)</title>
      <link>https://mark340.tistory.com/95</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;본 문서는 Flutter의 Isolate 개념에 대해 기술적 배경과 함께, 실제 애플리케이션 개발 중 발생할 수 있는 문제와 해결 방법을 중심으로 다룹니다. 특히, 권한 요청(permission_handler) 또는 Notification 초기화 과정에서 발생하는 &quot;Unable to detect current Android Activity&quot; 예외 상황을 예시로 삼아, 이를 해결하기 위한 방법을 제시합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Isolate의 개념&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1. 정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dart 언어의 가장 큰 특징 중 하나는 Isolate라는 단위로 프로세스를 분할한다는 점입니다. 전통적인 스레드(thread)와 달리, 하나의 Isolate는 고유한 메모리 힙(Heap)을 가지고 완전히 독립적으로 동작합니다. 따라서 여러 Isolate 간에 객체를 직접 공유할 수 없으며, 필요한 경우 메시지 패싱으로만 통신할 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Key Point&lt;/b&gt;: Isolate 간에는 공유 메모리가 없으므로, 동시성 문제(데이터 경합, 락 경쟁 등)가 비교적 단순해지는 반면, 통신 비용이 발생합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2. 용도&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;UI 및 이벤트 루프 처리(메인 Isolate)&lt;/b&gt;: Flutter 앱은 기본적으로 메인 Isolate 하나를 사용해 UI 렌더링과 사용자 이벤트를 처리합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;백그라운드 연산 분산(서브 Isolate)&lt;/b&gt;: 이미지 변환, JSON 파싱, 대규모 계산 등 CPU 사용이 많은 작업은 별도의 Isolate에서 처리함으로써 UI를 부드럽게 유지할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dart에서 별도의 Isolate를 간단히 활용하기 위해 compute() 함수가 제공됩니다. 이를 통해 함수 실행을 별도의 Isolate로 분산하고, 결과를 받아올 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 별도의 Isolate 생성 예시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1. compute() 함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 간단한 예시로, 대규모 반복 계산을 별도의 Isolate에서 처리하는 코드입니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Future&amp;lt;int&amp;gt; intensiveCalculation(int max) async {
  int sum = 0;
  for (int i = 0; i &amp;lt; max; i++) {
    sum += i;
  }
  return sum;
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 별도의 Isolate를 통해서 intensiveCalculation 실행
  final result = await compute(intensiveCalculation, 1000000000);

  runApp(MyApp());
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;compute()는 내부적으로 별도의 Isolate를 생성한 뒤, 함수(intensiveCalculation)와 매개변수(1000000000)를 메시지로 전달합니다.&lt;/li&gt;
&lt;li&gt;함수가 종료되면 결과(result)를 메인 Isolate로 다시 전달합니다.&lt;/li&gt;
&lt;li&gt;이 접근을 통해 메인 Isolate에서 긴 루프를 돌려도 UI가 멈추지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2. 라이브러리/플러그인의 내부 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자가 명시적으로 compute()를 사용하지 않아도, 이미지 처리나 파일 압축 등 일부 라이브러리는 성능 향상을 위해 내부적으로 별도의 Isolate를 생성할 수 있습니다. 따라서 디버깅 시 &quot;메인 Isolate&quot; 이외의 &quot;Background Isolate&quot;가 보이는 것은 지극히 정상적인 동작입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 실제 문제 사례: 권한 요청 시 Activity 인식 실패&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1. 문제 상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 예외가 발생하며, 안드로이드 플랫폼에서 권한 요청이나 알림 초기화가 제대로 이뤄지지 않는 사례가 보고될 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;PlatformException(PermissionHandler.PermissionManager, Unable to detect current Android Activity., null, null)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 permission_handler나 flutter_local_notifications 등의 플러그인이 안드로이드의 현재 Activity를 찾지 못해 발생하는 오류입니다. Flutter에서는 안드로이드 네이티브와의 상호작용을 위해 &quot;지금 어떤 Activity가 실행 중인지&quot; 알아야 하는데, 아래와 같은 경우에 Activity 정보를 확보하지 못할 수 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;별도의 Isolate에서 권한 요청 메서드를 호출&lt;/li&gt;
&lt;li&gt;메인 Isolate이 완전히 초기화되기 전에, 너무 이른 시점에서 권한을 요청&lt;/li&gt;
&lt;li&gt;백그라운드 상태에서 권한 요청이 트리거되어 Activity가 존재하지 않는 상황&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2. 문제 원인 분석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter 플러그인은 보통 ActivityPluginBinding(또는 레지스트리)을 통해 현재 Activity 레퍼런스를 얻습니다. 그러나 별도의 Isolate나 초기화가 완료되지 않은 시점에서는 이 레퍼런스가 null 상태일 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(a) 별도의 Isolate에서 요청&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도의 Isolate는 메인 Isolate와 Activity 정보를 공유하지 않으므로, permission_handler가 Activity 레퍼런스를 찾지 못해 예외가 발생합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(b) 조기 호출&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;runApp() 호출 직후, 아직 FlutterActivity 또는 MainActivity가 UI에 완전히 연결되지 않은 상태에서 권한 요청을 하면 실패할 가능성이 높아집니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 해결 방안 및 구현 예시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1. 메인 Isolate에서 권한 요청 로직 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 안전한 해결책은 메인 Isolate가 UI를 렌더링할 준비를 마친 뒤에 권한 요청을 수행하는 것입니다. 예를 들어, 아래와 같이 첫 번째 화면(예: HomeScreen)의 initState() 내에서 권한 요청을 처리하되, WidgetsBinding.instance.addPostFrameCallback를 사용해 프레임 렌더링이 완료된 후 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@override
void initState() {
  super.initState();

  // 위젯 트리가 생성되고 나서 권한 요청을 수행
  WidgetsBinding.instance.addPostFrameCallback((_) async {
    final permissionStatus = await Permission.notification.request();
    if (permissionStatus.isGranted) {
      // 알림 초기화 로직 수행
      await NotificationManager().initialize();
    }
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 방법은 &quot;현재 Activity&quot;가 정상적으로 설정된 상태이므로 permission_handler가 Activity 레퍼런스를 올바르게 인식할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2. 백그라운드 Isolate 사용 시 메시지 패싱 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 별도의 Isolate에서 연산 도중 권한이 필요해졌다면, 직접 그 위치에서 권한 요청 메서드를 호출해서는 안 됩니다. 대신 메인 Isolate에 요청을 위임해야 합니다. 다음과 같은 시나리오로 구현할 수 있습니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;백그라운드 Isolate &amp;rarr; &quot;권한 요청 필요&quot; 메시지 전송&lt;/li&gt;
&lt;li&gt;메인 Isolate &amp;rarr; 권한 요청 수행 및 결과(승인/거부) 수신&lt;/li&gt;
&lt;li&gt;메인 Isolate &amp;rarr; 결과를 백그라운드 Isolate로 다시 전달&lt;/li&gt;
&lt;li&gt;백그라운드 Isolate &amp;rarr; 결과에 따라 후속 로직 처리&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 접근 방식은 Isolate 간 메시지 채널(포트)을 설정해야 하며, 구현 난이도가 다소 높습니다. 따라서 권장되는 접근은, 가능하다면 UI/권한 관련 작업은 메인 Isolate에서만 수행하고, CPU 집약 작업만 별도의 Isolate로 분리하는 것입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Isolate는 Dart/Flutter가 제공하는 독립 실행 환경으로, UI 스레드(메인 Isolate)와 작업 스레드(백그라운드 Isolate)를 분리함으로써 앱 성능을 향상시킬 수 있습니다.&lt;/li&gt;
&lt;li&gt;권한 요청, 카메라/갤러리 접근, 알림 초기화 등 Activity 객체가 필요한 작업은 반드시 메인 Isolate에서 실행해야 합니다.&lt;/li&gt;
&lt;li&gt;예외가 발생하는 경우는 대부분 Isolate 분리나 초기화 순서(타이밍) 문제이므로, WidgetsBinding.instance.addPostFrameCallback을 통해 프레임 렌더링 이후에 메인 Isolate에서 해당 로직을 실행하는 방법이 안전합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상으로 Flutter Isolate의 개념과 실제 발생한 문제 상황, 그리고 해결 방안을 살펴보았습니다. 개발 환경 및 플러그인 호환성에 따라 일부 세부 구현이 달라질 수 있으나, 핵심은 &quot;Activity와 직접 상호작용하는 작업은 반드시 메인 Isolate에서&quot;라는 원칙임을 기억하시기 바랍니다.&lt;/p&gt;</description>
      <author>mark340</author>
      <guid isPermaLink="true">https://mark340.tistory.com/95</guid>
      <comments>https://mark340.tistory.com/95#entry95comment</comments>
      <pubDate>Fri, 10 Jan 2025 11:17:37 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] Google Dialogflow ES vs CX 챗봇 구현 비교 가이드</title>
      <link>https://mark340.tistory.com/94</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter 앱에서 Dialogflow를 사용하여 챗봇을 구현하면서 겪었던 경험을 바탕으로, Dialogflow ES(Essentials)와 CX의 차이점 및 각각의 구현 방법에 대해 상세히 알아봤습니다..&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Dialogflow ES와 CX의 주요 차이점&lt;/li&gt;
&lt;li&gt;Flutter에서의 구현 방법 비교&lt;/li&gt;
&lt;li&gt;실제 구현 중 발생한 문제와 해결 방법&lt;/li&gt;
&lt;li&gt;어떤 버전을 선택해야 할까?&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Dialogflow ES와 CX의 주요 차이점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 복잡성과 사용 목적&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Dialogflow ES&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간단한 챗봇 구현에 적합&lt;/li&gt;
&lt;li&gt;기본적인 대화 흐름 관리&lt;/li&gt;
&lt;li&gt;작은 규모의 프로젝트에 이상적&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Dialogflow CX&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;복잡한 대화 시나리오 처리 가능&lt;/li&gt;
&lt;li&gt;시각적 흐름 관리&lt;/li&gt;
&lt;li&gt;대규모 엔터프라이즈 프로젝트에 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 주요 기능 비교&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;Dialogflow ES&lt;/th&gt;
&lt;th&gt;Dialogflow CX&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;대화 흐름&lt;/td&gt;
&lt;td&gt;의도(Intent) 기반&lt;/td&gt;
&lt;td&gt;상태 머신 모델&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;시각적 빌더&lt;/td&gt;
&lt;td&gt;기본적인 인터페이스&lt;/td&gt;
&lt;td&gt;고급 시각적 흐름 빌더&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가격&lt;/td&gt;
&lt;td&gt;상대적으로 저렴&lt;/td&gt;
&lt;td&gt;더 높은 비용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;확장성&lt;/td&gt;
&lt;td&gt;제한적&lt;/td&gt;
&lt;td&gt;높은 확장성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;통합 용이성&lt;/td&gt;
&lt;td&gt;간단함&lt;/td&gt;
&lt;td&gt;더 복잡함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Flutter에서의 구현 방법 비교&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 Dialogflow ES 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES 버전은 &lt;code&gt;dialogFlowtter&lt;/code&gt; 패키지를 사용하여 간단히 구현할 수 있습니다:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;dependencies:
  dialogFlowtter: ^0.9.1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 구현 코드:&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;void sendMessage(String text) async {
  DialogAuthCredentials credentials = await DialogAuthCredentials.fromFile(
    path: &quot;assets/dialog_flow_auth.json&quot;
  );

  DialogFlowtter dialogFlowtter = DialogFlowtter(
    credentials: credentials,
  );

  DialogResponse response = await dialogFlowtter.detectIntent(
    queryInput: QueryInput(text: TextInput(text: text)),
  );

  // 응답 처리
  if (response.message != null) {
    // UI 업데이트
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 Dialogflow CX 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CX 버전은 Google의 공식 API를 직접 사용해야 합니다:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;dependencies:
  googleapis_auth: ^1.1.0
  googleapis: ^11.0.0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 구현 코드:&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;class ChatbotClient {
  final String projectId;
  final String agentId;
  final String location;

  Future&amp;lt;String&amp;gt; sendMessage(String sessionId, String message) async {
    final dialogflow = await AuthClient.getDialogflowApi();
    final sessionPath = 'projects/$projectId/locations/$location/agents/$agentId/sessions/$sessionId';

    final queryInput = GoogleCloudDialogflowCxV3QueryInput(
      languageCode: 'ko',
      text: GoogleCloudDialogflowCxV3TextInput(text: message),
    );

    final response = await dialogflow.projects.locations.agents.sessions.detectIntent(
      GoogleCloudDialogflowCxV3DetectIntentRequest(queryInput: queryInput),
      sessionPath,
    );

    return response.queryResult?.responseMessages?.first.text?.text?.first ?? '응답 없음';
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 실제 구현 중 발생한 문제와 해결 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 ES 구현 시 발생한 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Exception: NOT_FOUND: com.google.apps.framework.request.NotFoundException: 
No DesignTimeAgent found for project 'project-id'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결 방법:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dialogflow ES 프로젝트를 새로 생성&lt;/li&gt;
&lt;li&gt;올바른 서비스 계정 키 설정&lt;/li&gt;
&lt;li&gt;API 활성화 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 CX 구현 시 발생한 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;DetailedApiRequestError(status: 400, message: Please refer to 
https://cloud.google.com/dialogflow/cx/docs/concept/region#avail...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결 방법:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;지역별 올바른 엔드포인트 설정&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-dart&quot;&gt;final Endpoint = 'https://asia-northeast1-dialogflow.googleapis.com/';&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;sessionPath 형식 수정&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-dart&quot;&gt;final sessionPath = 'projects/$projectId/locations/$location/agents/$agentId/sessions/$sessionId';&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 어떤 버전을 선택해야 할까?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ES를 선택해야 하는 경우:&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간단한 대화 시나리오&lt;/li&gt;
&lt;li&gt;빠른 프로토타이핑이 필요할 때&lt;/li&gt;
&lt;li&gt;비용 효율성이 중요할 때&lt;/li&gt;
&lt;li&gt;Flutter 패키지 지원이 필요할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CX를 선택해야 하는 경우:&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;복잡한 대화 흐름 관리가 필요할 때&lt;/li&gt;
&lt;li&gt;대규모 프로젝트&lt;/li&gt;
&lt;li&gt;시각적 흐름 관리가 중요할 때&lt;/li&gt;
&lt;li&gt;높은 확장성이 필요할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 경험을 바탕으로, 작은 규모의 챗봇 프로젝트라면 ES를, 복잡한 대화 흐름이 필요한 프로젝트라면 CX를 추천드립니다. ES는 구현이 간단하고 &lt;code&gt;dialogFlowtter&lt;/code&gt; 패키지 덕분에 Flutter 통합이 쉽습니다. 반면 CX는 구현이 더 복잡하지만, 강력한 기능과 확장성을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 주의할 점은 CX를 사용할 때 지역 설정과 엔드포인트 구성에 신경 써야 한다는 것입니다. 처음에는 ES로 시작하고, 필요에 따라 CX로 마이그레이션하는 것도 좋은 전략이 될 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://cloud.google.com/dialogflow/docs&quot;&gt;Dialogflow 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pub.dev/packages/dialogFlowtter&quot;&gt;dialogFlowtter 패키지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://console.cloud.google.com/&quot;&gt;Google Cloud Console&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>mark340</author>
      <guid isPermaLink="true">https://mark340.tistory.com/94</guid>
      <comments>https://mark340.tistory.com/94#entry94comment</comments>
      <pubDate>Wed, 23 Oct 2024 17:57:25 +0900</pubDate>
    </item>
    <item>
      <title>안드로이드 재난문자 차단 안 됨 해결 방법 (공기계)</title>
      <link>https://mark340.tistory.com/93</link>
      <description>&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;공기계 사용시 재난문자 기능을 꺼도 계속 올 경우&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;비행기 모드 설정 후 &amp;gt; 와이파이 사용하여 임시방편으로 해결가능&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <author>mark340</author>
      <guid isPermaLink="true">https://mark340.tistory.com/93</guid>
      <comments>https://mark340.tistory.com/93#entry93comment</comments>
      <pubDate>Thu, 5 Sep 2024 13:41:49 +0900</pubDate>
    </item>
  </channel>
</rss>