<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Developer Noah Log</title>
        <link>https://velog.io/</link>
        <description>Flutter Specialist</description>
        <lastBuildDate>Wed, 07 May 2025 05:54:27 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Developer Noah Log</title>
            <url>https://velog.velcdn.com/images/developer_noah/profile/dbe86ff2-a840-4e16-9f8b-ef407a85212c/image.JPG</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Developer Noah Log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/developer_noah" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Flutter] Isolate 완전 정복 - UI 멈춤 없는 앱 만들기]]></title>
            <link>https://velog.io/@developer_noah/Flutter-Isolate-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-UI-%EB%A9%88%EC%B6%A4-%EC%97%86%EB%8A%94-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@developer_noah/Flutter-Isolate-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-UI-%EB%A9%88%EC%B6%A4-%EC%97%86%EB%8A%94-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Wed, 07 May 2025 05:54:27 GMT</pubDate>
            <description><![CDATA[<p>Flutter 앱을 개발하다 보면 무거운 연산(예: JSON 파싱, 이미지 처리) 때문에 UI가 <strong>멈추는 현상</strong>을 겪을 수 있다. 이럴 때 사용하는 것이 바로 <strong><code>Isolate</code></strong>이다.</p>
<p>이 글에서는 <code>Isolate</code>가 <strong>무엇인지</strong>, <strong>언제 사용하는지</strong>, <strong>어떻게 사용하는지</strong>, 그리고 <strong>실제 예시</strong>까지 정리한다.</p>
<hr>
<h2 id="🧠-isolate란">🧠 Isolate란?</h2>
<p>Flutter는 기본적으로 단일 스레드(UI Thread)에서 실행된다. 따라서 무거운 작업을 하게 되면 UI가 멈추거나 버벅일 수 있다.</p>
<p>이 문제를 해결하기 위해 사용하는 것이 바로 <strong><code>Isolate</code></strong>이다.<br><code>Isolate</code>는 Dart의 <strong>멀티스레딩 방식</strong>이며, 일반적인 스레드와 다르게 작동한다.</p>
<blockquote>
<p>🔹 각 <code>Isolate</code>는 독립적인 메모리 공간을 가지며<br>🔹 서로 데이터를 직접 공유하지 않고, <strong>메시지를 통해 통신</strong>한다.</p>
</blockquote>
<hr>
<h2 id="🛠️-isolate-사용-방법">🛠️ Isolate 사용 방법</h2>
<h3 id="✅-isolatespawn-사용법">✅ Isolate.spawn 사용법</h3>
<p>복잡하거나 장기 실행 작업에 적합</p>
<pre><code class="language-dart">import &#39;dart:isolate&#39;;

void isolateEntryPoint(SendPort sendPort) {
  // 무거운 작업 실행
  int result = heavyCalculation();
  sendPort.send(result);
}

Future&lt;void&gt; runIsolate() async {
  ReceivePort receivePort = ReceivePort();

  await Isolate.spawn(isolateEntryPoint, receivePort.sendPort);

  // isolate로부터 결과 받기
  int result = await receivePort.first;
  print(&quot;결과: $result&quot;);
}</code></pre>
<h3 id="✅-isolaterun-사용법">✅ Isolate.run 사용법</h3>
<p>간단한 비동기 작업 실행에 적합</p>
<pre><code class="language-dart">import &#39;dart:isolate&#39;;

// 무거운 작업 함수
int heavyCalculation() {
  // 예: 연산이 많은 계산
  int sum = 0;
  for (int i = 0; i &lt; 100000000; i++) {
    sum += i;
  }
  return sum;
}

Future&lt;void&gt; runIsolate() async {
  // Isolate.run을 사용하여 작업 실행
  int result = await Isolate.run(() =&gt; heavyCalculation());
  print(&quot;결과: $result&quot;);
}</code></pre>
<blockquote>
<p>⚠️ <code>Isolate.run</code> 은 Dart 3.0 이상부터 사용가능</p>
</blockquote>
<h3 id="✅-compute-함수-사용법">✅ compute 함수 사용법</h3>
<p>간단한 연산 처리 (JSON 파싱 등)</p>
<pre><code class="language-dart">import &#39;package:flutter/foundation.dart&#39;;

int heavyTask(int value) {
  return value * 2;
}

Future&lt;void&gt; useCompute() async {
  int result = await compute(heavyTask, 10);
  print(&quot;결과: $result&quot;);
}</code></pre>
<blockquote>
<p>⚠️ <code>compute</code> 는 함수와 파라미터 모두 직렬화 가능해야 하며, 파라미터는 하나만 받을 수 있다.</p>
</blockquote>
<h3 id="사용법-비교">사용법 비교</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>Isolate.spawn</th>
<th>Isolate.run</th>
<th>compute (Flutter 전용)</th>
</tr>
</thead>
<tbody><tr>
<td>도입 시기</td>
<td>오래전부터 존재</td>
<td>Dart 3.0부터 도입</td>
<td>Flutter에서 기본 제공</td>
</tr>
<tr>
<td>용도</td>
<td>복잡하거나 장기 실행 작업에 적합</td>
<td>간단한 비동기 작업 실행에 적합</td>
<td>간단한 연산 처리 (JSON 파싱 등)</td>
</tr>
<tr>
<td>코드 구조</td>
<td><code>ReceivePort</code> / <code>SendPort</code> 필요</td>
<td><code>async/await</code>로 결과 처리 용이</td>
<td>Future 기반으로 간단히 처리 가능</td>
</tr>
<tr>
<td>리턴 타입</td>
<td>직접 반환 없음 (포트로 수동 전달)</td>
<td><code>Future&lt;T&gt;</code>로 직접 결과 반환</td>
<td><code>Future&lt;R&gt;</code> 형태로 결과 반환</td>
</tr>
<tr>
<td>코드 위치</td>
<td>반드시 top-level 함수 or static 함수 필요</td>
<td>클로저, 익명 함수 사용 가능</td>
<td>top-level 함수만 허용</td>
</tr>
<tr>
<td>리소스 관리</td>
<td>수동으로 종료 필요 (<code>kill()</code>, <code>exit()</code>)</td>
<td>자동으로 isolate 종료</td>
<td>내부적으로 리소스 자동 관리</td>
</tr>
<tr>
<td>플랫폼 제한</td>
<td>Dart 전반에서 사용 가능</td>
<td>Dart (콘솔, 서버, Flutter 등)</td>
<td>Flutter 프레임워크에서만 사용 가능</td>
</tr>
</tbody></table>
<h2 id="📦-사용-예제">📦 사용 예제</h2>
<h3 id="1-대용량-json-파싱">1. 대용량 JSON 파싱</h3>
<pre><code class="language-dart">Future&lt;List&lt;User&gt;&gt; parseJson(String jsonString) async {
  return compute(_parseAndDecode, jsonString);
}

List&lt;User&gt; _parseAndDecode(String responseBody) {
  final parsed = jsonDecode(responseBody).cast&lt;Map&lt;String, dynamic&gt;&gt;();
  return parsed.map&lt;User&gt;((json) =&gt; User.fromJson(json)).toList();
}</code></pre>
<h3 id="2-이미지-처리">2. 이미지 처리</h3>
<p>이미지를 필터링하거나 리사이징하는 작업에도 Isolate를 사용할 수 있다.</p>
<pre><code class="language-dart">import &#39;dart:isolate&#39;;
import &#39;dart:io&#39;;
import &#39;dart:typed_data&#39;;

void imageProcessingIsolate(SendPort sendPort) async {
  // 메인 Isolate에서 작업 요청을 받을 포트
  final port = ReceivePort();

  // 메인 Isolate에게 자신의 포트를 전달
  sendPort.send(port.sendPort);

  // 요청 대기
  await for (final message in port) {
    // message 구조: [Uint8List imageBytes, SendPort replyPort]
    final Uint8List imageBytes = message[0];
    final SendPort replyPort = message[1];

    // 예시: 이미지의 크기를 단순히 구하는 작업 (실제 처리 로직은 더 복잡할 수 있음)
    final imageSize = imageBytes.length;

    // 결과 전송
    replyPort.send(&#39;이미지 크기: $imageSize bytes&#39;);

    // 작업이 한 번만 필요한 경우 아래로 종료
    // port.close(); // 반복 수신이 필요 없다면 사용
  }
}</code></pre>
<ul>
<li>ReceivePort를 생성하여 메인 Isolate로부터 메시지를 받을 준비를 함.</li>
<li>메인 Isolate에게 자신의 SendPort를 전달.</li>
<li>메인 Isolate가 보내는 메시지(보통은 [작업 데이터, 응답용 SendPort] 형태)를 기다림.</li>
<li>메시지를 처리한 뒤, 결과를 replyPort를 통해 메인으로 다시 보냄.</li>
<li>이 구조는 양방향 통신이 필요한 Isolate 패턴에서 많이 사용됨.</li>
</ul>
<hr>
<h2 id="⚠️-주의할-점">⚠️ 주의할 점</h2>
<ul>
<li><code>Isolate</code> 간 메모리는 공유되지 않는다. 데이터를 보낼 때는 직렬화 가능한 형태로 전송해야 한다.</li>
<li><code>compute</code> 는 간단한 작업에만 적합하다. 복잡한 작업에는 직접 Isolate를 생성하여 사용해야 한다.</li>
<li><code>Isolate</code> 는 리소스를 소비하므로 남용하지 않아야 하며, 재사용 혹은 적절한 관리가 필요하다.</li>
</ul>
<hr>
<h2 id="✅-정리">✅ 정리</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Isolate란?</strong></td>
<td>독립된 메모리 공간에서 실행되는 Dart의 멀티스레드 방식</td>
</tr>
<tr>
<td><strong>왜 사용하나?</strong></td>
<td>UI 스레드를 차단하지 않기 위해</td>
</tr>
<tr>
<td><strong>언제 사용하나?</strong></td>
<td>JSON 파싱, 이미지 처리, 암호화 등 무거운 연산이 필요한 경우</td>
</tr>
<tr>
<td><strong>어떻게 사용하나?</strong></td>
<td><code>Isolate.spawn</code>, <code>ReceivePort</code>, 혹은 <code>Isolate.run</code> 또는 <code>compute()</code> 활용</td>
</tr>
</tbody></table>
<p>Flutter에서 <code>Isolate</code> 를 적절히 활용하면 앱의 부드러운 UX와 성능 향상을 모두 달성할 수 있다. 무조건 사용하는 것이 아니라, 적절한 시점과 상황에서 사용하는 것이 중요하다.</p>
<h2 id="🔗-참고-링크">🔗 참고 링크</h2>
<ul>
<li><a href="https://dart.dev/language/concurrency">Dart 공식 Isolate 문서</a></li>
<li><a href="https://api.flutter.dev/flutter/foundation/compute.html">Flutter compute 함수</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Dart] Flutter에서 extends, mixin, implements의 차이와 활용]]></title>
            <link>https://velog.io/@developer_noah/Dart-Flutter%EC%97%90%EC%84%9C-extends-mixin-implements%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%99%80-%ED%99%9C%EC%9A%A9</link>
            <guid>https://velog.io/@developer_noah/Dart-Flutter%EC%97%90%EC%84%9C-extends-mixin-implements%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%99%80-%ED%99%9C%EC%9A%A9</guid>
            <pubDate>Fri, 02 May 2025 01:11:34 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Flutter는 Dart 언어를 기반으로 하며, 객체지향 프로그래밍(OOP) 개념을 바탕으로 클래스 상속 구조를 지원한다. 이 글에서는 Dart에서 클래스 간 상속과 재사용을 위한 주요 키워드인 <code>extends</code>, <code>with</code>, <code>implements</code>에 대해 정리하고, 각각의 차이점을 비교해본다.</p>
</blockquote>
<hr>
<h2 id="1-extends---클래스-상속-단일-상속">1. <code>extends</code> - 클래스 상속 (단일 상속)</h2>
<p><code>extends</code>는 한 클래스가 다른 클래스의 기능을 상속받고, 필요에 따라 오버라이드할 수 있게 해준다. Dart는 단일 상속만 지원하기 때문에, 한 번에 하나의 클래스만 상속 가능하다.</p>
<pre><code class="language-dart">class Animal {
  void sound() =&gt; print(&quot;Some sound&quot;);
}

class Dog extends Animal {
  @override
  void sound() =&gt; print(&quot;Bark&quot;);
}</code></pre>
<p><strong>특징</strong></p>
<ul>
<li>부모 클래스의 속성과 메서드를 그대로 가져온다.</li>
<li><code>super</code> 키워드로 부모 클래스의 기능에 접근할 수 있다.</li>
<li>생성자도 상속 대상이며, 명시적으로 호출해야 한다.</li>
</ul>
<hr>
<h2 id="2-with---믹스인mixin">2. <code>with</code> - 믹스인(Mixin)</h2>
<p><code>with</code>는 코드 재사용을 위한 방식으로, 여러 클래스의 기능을 조합해서 사용할 수 있다. 다중 믹스인을 허용하기 때문에 복수의 클래스로부터 메서드와 속성을 가져올 수 있다.</p>
<pre><code class="language-dart">mixin CanSwim {
  void swim() =&gt; print(&quot;Swimming&quot;);
}

mixin CanRun {
  void run() =&gt; print(&quot;Running&quot;);
}

class Human with CanSwim, CanRun {}</code></pre>
<p><strong>특징</strong></p>
<ul>
<li>여러 믹스인을 한 클래스에 동시에 적용할 수 있다.</li>
<li>믹스인은 상태(state)를 가질 수 있다.</li>
<li>생성자를 정의할 수 없으며, 정의하면 에러가 발생한다.</li>
<li><code>mixin</code>, 또는 믹스인으로 사용할 수 있는 클래스에만 <code>with</code>를 사용할 수 있다.</li>
</ul>
<hr>
<h2 id="3-implements---인터페이스-구현">3. <code>implements</code> - 인터페이스 구현</h2>
<p><code>implements</code>는 특정 클래스나 믹스인의 구조만 가져오고, 그 안의 모든 메서드와 속성을 반드시 직접 구현해야 한다. 원본 클래스의 구현은 전혀 사용하지 않는다.</p>
<pre><code class="language-dart">class Flyer {
  void fly() {}
}

class Bird implements Flyer {
  @override
  void fly() =&gt; print(&quot;Flying&quot;);
}</code></pre>
<p><strong>특징</strong></p>
<ul>
<li>클래스의 인터페이스(형태)만 따르고, 구현은 전부 새로 작성해야 한다.</li>
<li>여러 개의 클래스를 동시에 구현할 수 있다.  </li>
<li>추상 메서드가 있든 없든, 모든 멤버를 직접 구현해야 한다.</li>
<li>상속처럼 부모 클래스의 구현을 사용하는 방식이 아니다.</li>
</ul>
<hr>
<h2 id="요약-비교">요약 비교</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>키워드</th>
<th>다중 적용</th>
<th>메서드 재사용</th>
<th>메서드 재정의 필수</th>
<th>생성자 상속</th>
</tr>
</thead>
<tbody><tr>
<td>상속</td>
<td><code>extends</code></td>
<td>❌</td>
<td>✅</td>
<td>❌ (선택적)</td>
<td>✅</td>
</tr>
<tr>
<td>믹스인</td>
<td><code>with</code></td>
<td>✅</td>
<td>✅</td>
<td>❌ (선택적)</td>
<td>❌</td>
</tr>
<tr>
<td>인터페이스 구현</td>
<td><code>implements</code></td>
<td>✅</td>
<td>❌</td>
<td>✅ (필수)</td>
<td>❌</td>
</tr>
</tbody></table>
<hr>
<h2 id="사용-예시">사용 예시</h2>
<pre><code class="language-dart">// 부모 클래스: Animal
abstract class Animal {
  void makeSound();
}

// 믹스인: 수영하는 능력
mixin Swimmer {
  void swim() =&gt; print(&#39;수영 중 🏊‍♀️&#39;);
}

// 인터페이스로 사용될 클래스
class Flyer {
  void fly() {} // 인터페이스처럼 사용
}

// extends 사용: Dog는 Animal의 기능을 상속
class Dog extends Animal {
  @override
  void makeSound() =&gt; print(&#39;멍멍 🐶&#39;);
}

// extends + with 사용: Duck은 Animal을 상속하고 Swimmer 믹스인 추가
class Duck extends Animal with Swimmer {
  @override
  void makeSound() =&gt; print(&#39;꽥꽥 🦆&#39;);
}

// implements 사용: Airplane은 Flyer 인터페이스 구현
class Airplane implements Flyer {
  @override
  void fly() =&gt; print(&#39;비행기 이륙 중 ✈️&#39;);
}

// extends + with + implements 조합
class FlyingFish extends Animal with Swimmer implements Flyer {
  @override
  void makeSound() =&gt; print(&#39;촤르르... 🐟&#39;);

  @override
  void fly() =&gt; print(&#39;물 위로 잠깐 비행!&#39;);
}

// 테스트 실행용 main 함수
void main() {
  final dog = Dog();
  dog.makeSound(); // 멍멍 🐶

  final duck = Duck();
  duck.makeSound(); // 꽥꽥 🦆
  duck.swim(); // 수영 중 🏊‍♀️

  final airplane = Airplane();
  airplane.fly(); // 비행기 이륙 중 ✈️

  final flyingFish = FlyingFish();
  flyingFish.makeSound(); // 촤르르... 🐟
  flyingFish.swim(); // 수영 중 🏊‍♀️
  flyingFish.fly(); // 물 위로 잠깐 비행!
}</code></pre>
<hr>
<h2 id="마무리">마무리</h2>
<ul>
<li><code>extends</code>는 부모 클래스의 기능을 확장하거나 재정의하고 싶을 때 사용한다.</li>
<li><code>with</code>은 여러 기능을 하나의 클래스에 조합해서 재사용하고 싶을 때 쓰인다.</li>
<li><code>implements</code>는 어떤 클래스나 인터페이스의 구조만 따르되, 로직은 모두 직접 정의해야 할 때 선택한다.</li>
</ul>
<p>각 키워드의 목적이 뚜렷하니, 상황에 맞게 적절히 선택해서 사용하는 게 중요하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] Modular]]></title>
            <link>https://velog.io/@developer_noah/Flutter-Modular</link>
            <guid>https://velog.io/@developer_noah/Flutter-Modular</guid>
            <pubDate>Tue, 04 Feb 2025 07:18:16 GMT</pubDate>
            <description><![CDATA[<h2 id="flutter_modular"><a href="https://pub.dev/packages/flutter_modular">flutter_modular</a></h2>
<p>Flutter 애플리케이션 개발에서 라우팅과 의존성 주입을 모듈화하여 관리할 수 있도록 도와주는 라이브러리이다. 애플리케이션의 유지보수성과 확장성을 개선하기 위해 큰 규모의 프로젝트에 유용하다.</p>
<p><img src="https://velog.velcdn.com/images/developer_noah/post/79ee2cb7-eff2-43a3-ba5c-de3e418393f8/image.png" alt=""></p>
<h2 id="주요-기능">주요 기능</h2>
<p><strong>모듈화된 라우팅</strong>
각 기능별로 독립된 라우트를 가진다.</p>
<p><strong>모듈화된 의존성 주입</strong>
각 모듈 또는 기능에 필요한 의존성을 독립적으로 주입할 수 있다.</p>
<h2 id="사용-방법">사용 방법</h2>
<h4 id="1-modularapp-위젯-추가">1. ModularApp 위젯 추가</h4>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;

void main(){
  return runApp(
    ModularApp(
      module: /*&lt;MainModule&gt;*/, 
      child: /*&lt;MainWidget&gt;*/,
    ),
  );
}</code></pre>
<h4 id="2-appmodule-추가">2. AppModule 추가</h4>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:flutter_modular/flutter_modular.dart&#39;;

void main(){
  return runApp(
    ModularApp(
      module: AppModule(), 
      child: /*&lt;MainWidget&gt;*/,
    ),
  );
}

class AppModule extends Module {
  @override
  void binds(i) {}

  @override
  void routes(r) {}
}</code></pre>
<h4 id="3-materialapp-or-cupertinoapp-인스턴스-추가">3. MaterialApp or CupertinoApp 인스턴스 추가</h4>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:flutter_modular/flutter_modular.dart&#39;;

void main(){
  return runApp(
    ModularApp(
      module: AppModule(), 
      child: AppWidget(),
    ),
  );
}

class AppWidget extends StatelessWidget {
  Widget build(BuildContext context){
    return MaterialApp.router(
      title: &#39;My Smart App&#39;,
      theme: ThemeData(primarySwatch: Colors.blue),
      routerConfig: Modular.routerConfig,
    );
  }
}

class AppModule extends Module {
  @override
  void binds(i) {}

  @override
  void routes(r) {}
}</code></pre>
<h4 id="4-navigation">4. Navigation</h4>
<p><code>pushNamed</code>, <code>popUntil</code> 도 사용 가능하지만 여기선 웹 환경과 유사한 <code>navigate</code> 사용</p>
<pre><code class="language-dart">class AppModule extends Module {
  @override
  void routes(r) {
    r.child(&#39;/&#39;, child: (context) =&gt; HomePage());
    r.child(&#39;/second&#39;, child: (context) =&gt; SecondPage());
  }
}

class HomePage extends StatelessWidget {
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(&#39;Home Page&#39;)),
      body: Center(
        child: ElevatedButton(
          onPressed: () =&gt; Modular.to.navigate(&#39;/second&#39;),
          child: Text(&#39;Navigate to Second Page&#39;),
        ),
      ),
    );
  }
}

class SecondPage extends StatelessWidget {
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(&#39;Second Page&#39;)),
      body: Center(
        child: ElevatedButton(
          onPressed: () =&gt; Modular.to.navigate(&#39;/&#39;),
          child: Text(&#39;Back to Home&#39;),
        ),
      ),
    );
  }
}</code></pre>
<h4 id="5-dependency-injection">5. Dependency Injection</h4>
<pre><code class="language-dart">class AppModule extends Module {
  @override
  void binds(i) {
    i.addSingleton(() =&gt; EventBus());
  }
}

// 사용 시
final EventBus _eventBus = Modular.get&lt;EventBus&gt;();</code></pre>
<h4 id="6-imports">6. Imports</h4>
<p>각 기능에서 정의 된 모듈을 <code>imports</code> 하여 사용</p>
<pre><code class="language-dart">class AppModule extends Module {
  @override
  void binds(Injector i) {
    i.addSingleton(() =&gt; EventBus());
    imports.map((import) =&gt; import.binds(i)).toList();
  }

  @override
  void routes(RouteManager r) {
    r.child(&#39;/&#39;, child: (context) =&gt; HomePage());
    imports.map((import) =&gt; import.routes(r)).toList();
  }

  @override
  List&lt;Module&gt; get imports =&gt; [
        AddModule(),
        SettingModule(),
      ];</code></pre>
<h2 id="참조">참조</h2>
<p><a href="https://pub.dev/packages/flutter_modular">https://pub.dev/packages/flutter_modular</a>
<a href="https://modular.flutterando.com.br/docs/intro">https://modular.flutterando.com.br/docs/intro</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] Event Bus]]></title>
            <link>https://velog.io/@developer_noah/Flutter-Event-Bus</link>
            <guid>https://velog.io/@developer_noah/Flutter-Event-Bus</guid>
            <pubDate>Thu, 23 Jan 2025 01:54:31 GMT</pubDate>
            <description><![CDATA[<p><a href="https://github.com/jsk9106/flutter_event_bus">샘플 앱 코드</a></p>
<h2 id="event-bus란">Event Bus란?</h2>
<p>Flutter 에서 이벤트 버스는 컴포넌트 간의 통신을 위해 사용되는 디자인 패턴이다. 앱 내에서 이벤트를 발생시키고, 이를 다른 부분에서 수신하여 반응하는 형식이다.</p>
<h2 id="핵심-개념">핵심 개념</h2>
<p><strong>이벤트(Event)</strong>
어떤 사건이 발생했음을 알리는 신호나 데이터</p>
<p><strong>퍼블리셔(Publisher)</strong>
이벤트를 생성하고 발송하는 주체</p>
<p><strong>서브스크라이버(Subscriber)</strong>
이벤트를 수신하여 그에 맞는 반응을 하는 컴포넌트</p>
<h2 id="사용-이점">사용 이점</h2>
<p><strong>결합도 감소</strong>
컴포넌트들이 서로 직접적으로 의존하지 않고 이벤트를 통해 통신하기 때문에 코드의 결합도가 낮아진다.</p>
<p><strong>재사용성 향상</strong>
이벤트를 통해 통신하면, 특정 이벤트를 수신할 수 있는 모든 컴포넌트에 동일한 방식으로 데이터를 전달할 수 있어 컴포넌트의 재사용성이 증가한다.</p>
<p><strong>유지보수 용이성</strong>
이벤트 기반 통신은 시스템의 각 부분을 독립적으로 수정하거나 업데이트를 할 수 있어 전체적인 시스템의 유지보수가 용이해진다.</p>
<h2 id="결합도-감소하는-과정">결합도 감소하는 과정</h2>
<p><em>예시) MVC</em></p>
<p>MVC 그룹 하나는 문제가 되지 않는다.
<img src="https://velog.velcdn.com/images/developer_noah/post/fa8eb47b-0fe1-4943-afe7-799cf67c7b7e/image.png" alt=""></p>
<p>하지만 MVC 그룹이 여러 개 생기면 그 그룹들이 서로 통신해야 하는 상황이 생긴다. 이는 컨트롤러 간에 결합도를 높인다.
<img src="https://velog.velcdn.com/images/developer_noah/post/5a9f3960-93a2-43ab-bac7-d70920988d14/image.png" alt=""></p>
<p>이벤트 버스 패턴은 결합도를 낮춘다.
<img src="https://velog.velcdn.com/images/developer_noah/post/521c7052-e9b0-4583-9666-70496fbe3d11/image.png" alt=""></p>
<h2 id="사용-방법">사용 방법</h2>
<h4 id="1-이벤트-버스-생성">1. 이벤트 버스 생성</h4>
<pre><code class="language-dart">import &#39;package:event_bus/event_bus.dart&#39;;

EventBus eventBus = EventBus();</code></pre>
<h4 id="2-이벤트-정의">2. 이벤트 정의</h4>
<pre><code class="language-dart">class UserLoggedInEvent {
  User user;

  UserLoggedInEvent(this.user);
}

class NewOrderEvent {
  Order order;

  NewOrderEvent(this.order);
}</code></pre>
<h4 id="3-리스너-등록">3. 리스너 등록</h4>
<p>특정 이벤트에 대한 리스너 등록</p>
<pre><code class="language-dart">eventBus.on&lt;UserLoggedInEvent&gt;().listen((event) {
  print(event.user);
});</code></pre>
<p>모든 이벤트에 대한 리스너 등록</p>
<pre><code class="language-dart">eventBus.on().listen((event) {
  print(event.runtimeType);
});</code></pre>
<p>Dart Streams 으로 리스너 등록</p>
<pre><code class="language-dart">StreamSubscription loginSubscription = eventBus.on&lt;UserLoggedInEvent&gt;().listen((event) {
  print(event.user);
});

loginSubscription.cancel();</code></pre>
<h4 id="4-이벤트-발생">4. 이벤트 발생</h4>
<pre><code class="language-dart">User myUser = User(&#39;Mickey&#39;);
eventBus.fire(UserLoggedInEvent(myUser));</code></pre>
<h2 id="참조">참조</h2>
<p><a href="https://pub.dev/packages/event_bus">event_bus</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] OS별 스토어 최신 버전 확인 방법]]></title>
            <link>https://velog.io/@developer_noah/Flutter-OS%EB%B3%84-%EC%8A%A4%ED%86%A0%EC%96%B4-%EC%B5%9C%EC%8B%A0-%EB%B2%84%EC%A0%84-%ED%99%95%EC%9D%B8-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@developer_noah/Flutter-OS%EB%B3%84-%EC%8A%A4%ED%86%A0%EC%96%B4-%EC%B5%9C%EC%8B%A0-%EB%B2%84%EC%A0%84-%ED%99%95%EC%9D%B8-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Sun, 11 Aug 2024 05:18:50 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>강제 업데이트, 버전 노출 등에 쓰이는 OS별 최신 버전 확인 방법에 대해 소개한다.</p>
</blockquote>
<p>pub.dev의 패키지를 활용하는 방법도 있지만, 많은 패키지 의존성을 좋아하지 않아서 <strong>http 통신</strong>으로 <strong>스토어</strong>에서 직접 버전을 확인하는 방법을 찾았다.</p>
<h2 id="app-storeios">App Store(iOS)</h2>
<pre><code class="language-dart">/// 앱 스토어 버전 확인
Future&lt;Either&lt;Failure, String?&gt;&gt; getAppStoreVersion(String bundleId) async {
    const GetStoreVersionFailure failure = GetStoreVersionFailure(&#39;Error getting store version from App Store&#39;);
    try {
      Uri _uri = Uri.https(
        &#39;itunes.apple.com&#39;,
        &#39;/lookup&#39;,
        {&#39;bundleId&#39;: bundleId},
      );
      final http.Response _response = await http.get(_uri);
      if (_response.statusCode == 200) {
        final jsonObj = json.decode(_response.body);
        String? _version = jsonObj[&#39;results&#39;][0][&#39;version&#39;];
        return Right(_version);
      }
      return const Left(failure);
    } catch (e) {
      return const Left(failure);
    }
  }</code></pre>
<h2 id="play-storeaos">Play Store(AOS)</h2>
<pre><code class="language-dart">/// 플레이 스토어 버전 확인
Future&lt;Either&lt;Failure, String?&gt;&gt; getPlayStoreVersion(String packageName) async {
    const GetStoreVersionFailure failure = GetStoreVersionFailure(&#39;Error getting store version from Google Play&#39;);
    try {
      final http.Response _response =
          await http.get(Uri.parse(&#39;https://play.google.com/store/apps/details?id=$packageName&#39;));
      if (_response.statusCode == 200) {
        RegExp regexp = RegExp(r&#39;\[\[\[&quot;(\d+\.\d+(\.[a-z]+)?(\.([^&quot;]|\\&quot;)*)?)&quot;\]\]&#39;);
        String? _version = regexp.firstMatch(_response.body)?.group(1);
        return Right(_version);
      }
      return const Left(failure);
    } catch (e) {
      return const Left(failure);
    }
  }</code></pre>
<p>스토어 별로 가끔 <strong>페이로드 값</strong>이 변경되는 것 같다. 값을 가져오는데 실패했을 때는 각자 원하는 방식으로 예외 처리하면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] 상태관리 Signals]]></title>
            <link>https://velog.io/@developer_noah/Flutter-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC-Signals</link>
            <guid>https://velog.io/@developer_noah/Flutter-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC-Signals</guid>
            <pubDate>Sun, 07 Jul 2024 10:17:16 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Flutter에서 사용할 수 있는 상태관리 중 하나인 Signal을 소개한다.</p>
</blockquote>
<p><strong>Signal</strong>은 새로운 것이 아니며 오랫동안 존재해왔다.
<strong>javascript</strong> 프레임워크에는 핵심 라이브러리의 일부로 포함되어 있다.</p>
<h2 id="signals-기능">Signals 기능</h2>
<h4 id="미세한-반응성">미세한 반응성</h4>
<ul>
<li>Preact 신호를 기반으로 하며 종속성을 자동으로 추적하고 더 이상 필요하지 않을 때 해제하는 세분화된 반응성 시스템을 제공한다.<h4 id="lazy">lazy</h4>
</li>
<li>signals은 읽을 때만 값을 계산한다. 신호를 읽지 않으면 계산되지 않는다.<h4 id="효과적인-렌더링">효과적인 렌더링</h4>
</li>
<li>위젯은 업데이트가 필요한 위젯 트리 부분과 마운트된 부분만 표시하여 다시 구축할 수 있다.<h4 id="100-dart-native">100% Dart Native</h4>
</li>
<li>Dart JS(HTML), Shelf Server, CLI(및 기본), VM, Flutter(웹, 모바일 및 데스크톱)를 지원합니다. signals은 모든 Dart 프로젝트에서 사용할 수 있다.<h4 id="심플하고-유연한-api">심플하고 유연한 API</h4>
</li>
<li>모든 앱은 다르며 signals은 다양한 방식으로 구성될 수 있다. 따라야 할 몇 가지 규칙이 있지만 API 표면이 작아 쉽게 적용할 수 있다.</li>
</ul>
<h2 id="signals-쓰는-이유">Signals 쓰는 이유</h2>
<h4 id="너무-복잡하고-비대한-상태관리들">너무 복잡하고 비대한 상태관리들</h4>
<ul>
<li>Riverpod, Bloc, Getx 등의 상태관리들은 너무 많은 기능을 제공하여, 높은 Learning curve가 있다.<h4 id="심플하고-필요한-기능만-제공">심플하고 필요한 기능만 제공</h4>
</li>
<li>상태관리에 필요한 기능만을 제공하기 때문에 어떤 서비스든 간단하고, 유연하게 적용할 수 있다.</li>
<li>Bloc과 같이 Architecture를 강요하지 않는다.<h4 id="가독성-좋은-코드">가독성 좋은 코드</h4>
<pre><code class="language-dart">// 생성
final counter = signal(0);
</code></pre>
</li>
</ul>
<pre><code>```dart
// 사용
Watch((context) =&gt; Text(&#39;Counter: $counter&#39;))</code></pre><h2 id="signals">Signals</h2>
<pre><code class="language-dart">import &#39;package:signals/signals.dart&#39;;

final counter = signal(0);

// Read value from signal, logs: 0
print(counter.value);

// Write to a signal
counter.value = 1;</code></pre>
<h4 id="value">.value</h4>
<pre><code class="language-dart">final counter = signal(0);

effect(() {
  print(counter.value);
});

counter.value = 1;</code></pre>
<h4 id="previousvalue">.previousValue</h4>
<pre><code class="language-dart">final counter = signal(0);

effect(() {
  print(&#39;Current value: ${counter.value}&#39;);
  print(&#39;Previous value: ${counter.previousValue}&#39;);
});

counter.value = 1;</code></pre>
<h4 id="force-update">Force Update</h4>
<pre><code class="language-dart">final counter = signal(0);
counter.set(1, force: true);</code></pre>
<h4 id="disposing">Disposing</h4>
<pre><code class="language-dart">final s = signal(0, autoDispose: true);
s.onDispose(() =&gt; print(&#39;Signal destroyed&#39;));
final dispose = s.subscribe((_) {});
dispose();
final value = s.value; // 0
// prints: Signal destroyed</code></pre>
<pre><code class="language-dart">final s = signal(0);
s.dispose();
final c = computed(() =&gt; s.value);
// c will not react to changes in s</code></pre>
<pre><code class="language-dart">final s = signal(0);
print(s.disposed); // false
s.dispose();
print(s.disposed); // true</code></pre>
<h4 id="computed">Computed</h4>
<pre><code class="language-dart">import &#39;package:signals/signals.dart&#39;;

final name = signal(&quot;Jane&quot;);
final surname = signal(&quot;Doe&quot;);

final fullName = computed(() =&gt; name.value + &quot; &quot; + surname.value);

// Logs: &quot;Jane Doe&quot;
print(fullName.value);

// Updates flow through computed, but only if someone
// subscribes to it. More on that later.
name.value = &quot;John&quot;;
// Logs: &quot;John Doe&quot;
print(fullName.value);</code></pre>
<h4 id="effect">Effect</h4>
<pre><code class="language-dart">import &#39;package:signals/signals.dart&#39;;

final name = signal(&quot;Jane&quot;);
final surname = signal(&quot;Doe&quot;);
final fullName = computed(() =&gt; name.value + &quot; &quot; + surname.value);

// Logs: &quot;Jane Doe&quot;
effect(() =&gt; print(fullName.value));

// Updating one of its dependencies will automatically trigger
// the effect above, and will print &quot;John Doe&quot; to the console.
name.value = &quot;John&quot;;</code></pre>
<h4 id="batch">Batch</h4>
<pre><code class="language-dart">import &#39;package:signals/signals.dart&#39;;

final name = signal(&quot;Jane&quot;);
final surname = signal(&quot;Doe&quot;);
final fullName = computed(() =&gt; name.value + &quot; &quot; + surname.value);

// Logs: &quot;Jane Doe&quot;
effect(() =&gt; print(fullName.value));

// Combines both signal writes into one update. Once the callback
// returns the `effect` will trigger and we&#39;ll log &quot;Foo Bar&quot;
batch(() {
  name.value = &quot;Foo&quot;;
  surname.value = &quot;Bar&quot;;
});</code></pre>
<h2 id="참조">참조</h2>
<p><a href="https://dartsignals.dev/">https://dartsignals.dev/</a>
<a href="https://pub.dev/packages/signals">https://pub.dev/packages/signals</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Mac] terminal 자동완성 및 하이라이팅]]></title>
            <link>https://velog.io/@developer_noah/mac-terminal-%EC%9E%90%EB%8F%99-%EC%99%84%EC%84%B1-%EB%B0%8F-%ED%95%98%EC%9D%B4%EB%9D%BC%EC%9D%B4%ED%8C%85</link>
            <guid>https://velog.io/@developer_noah/mac-terminal-%EC%9E%90%EB%8F%99-%EC%99%84%EC%84%B1-%EB%B0%8F-%ED%95%98%EC%9D%B4%EB%9D%BC%EC%9D%B4%ED%8C%85</guid>
            <pubDate>Sat, 08 Jun 2024 05:56:50 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>zsh를 활용한 터미널 자동완성 및 하이라이팅 기능이다.</p>
</blockquote>
<h2 id="자동-완성-autosuggestions">자동 완성 (AutoSuggestions)</h2>
<h3 id="1-git-source-code-download">1. git source code download</h3>
<pre><code>git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions</code></pre><h3 id="2-zshrc-추가">2. ~/.zshrc 추가</h3>
<pre><code>source ~/.oh-my-zsh/custom/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh</code></pre><h3 id="3-zshrc-다시-시작">3. zshrc 다시 시작</h3>
<pre><code>source ~/.zshrc</code></pre><h2 id="하이라이팅-syntax-highlighting">하이라이팅 (Syntax Highlighting)</h2>
<h3 id="1-git-source-code-download-1">1. git source code download</h3>
<pre><code>git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting</code></pre><h3 id="2-zshrc-추가-1">2. ~/.zshrc 추가</h3>
<pre><code>source ~/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh</code></pre><h3 id="3-zshrc-다시-시작-1">3. zshrc 다시 시작</h3>
<pre><code>source ~/.zshrc</code></pre><h2 id="단축키-변경">단축키 변경</h2>
<h3 id="1-zshrc에-추가">1. zshrc에 추가</h3>
<p>자동완성 <code>tab</code> 으로 변경</p>
<pre><code>bindkey &#39;\t&#39; autosuggest-accept</code></pre><h3 id="2-zshrc-다시-시작">2. zshrc 다시 시작</h3>
<pre><code>source ~/.zshrc</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] Multi Package 사용하기]]></title>
            <link>https://velog.io/@developer_noah/Flutter-Multi-Package-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@developer_noah/Flutter-Multi-Package-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 06 May 2024 12:22:52 GMT</pubDate>
            <description><![CDATA[<h2 id="mutil-package란">Mutil Package란?</h2>
<p>멀티 패키지는 <strong>하나의 프로젝트</strong>에서 쓰이는 코드를 <strong>여러 패키지</strong>로 나누어 관리하는 방법이다. 각 패키지는 <strong>관련된 기능</strong>을 담당하고, 프로젝트 내에서 <strong>서로 의존성</strong>이 있을 수 있다.</p>
<h3 id="왜-multi-package를-사용해야-하지">왜 Multi Package를 사용해야 하지?</h3>
<p>프로젝트가 거대화되면서 모노리딕 시스템은 문제를 야기한다. 대표적으로 <strong>높은 결합도</strong>와 <strong>낮은 응집력</strong>을 예로 들 수 있다. 이를 해결하기 위해서 개발 조직은 시스템의 각 부분을 <strong>도메인 별로 분리</strong>해서 마이크로 서비스로 구성하기 시작한다. 이때 멀티 패키지를 사용하면 <strong>쉽게 공유할 수 있는</strong> 모듈식 코드를 만들 수 있다.</p>
<p>이때 쪼개진 각 서비스를 <strong>하나의 리포지토리</strong>에서 관리할지, 각자 <strong>다른 리포지토리</strong>에서 관리할지 고민하게 된다.
리포지토리를 관리하는 방법은 시스템의 각 모듈을 <strong>개별 리포지토리</strong>에서 관리할 것인지, <strong>하나의 리포지토리</strong>에서 관리할 것인지에 따라서 달라진다. 이때 나눠서 관리하는 것을 <strong>Multirepo</strong>, 하나로 관리하는 것을 <strong>Monorepo</strong>라 정의한다.</p>
<p>각자의 장단점이 있지만 오늘은 Monorepo에 대해 살펴 보겠다.</p>
<h2 id="monorepo란">Monorepo란?</h2>
<ul>
<li><strong>단일 저장소</strong>에 여러 프로젝트를 저장하는 소프트웨어 개발 전략</li>
<li>코드를 패키지 별로 나누어 관리
<img src="https://velog.velcdn.com/images/developer_noah/post/033b410d-79ec-4d0d-8a5d-bf08406e06c8/image.webp" alt=""></li>
</ul>
<h3 id="장점">장점</h3>
<ul>
<li>지속적인 소스의 무결성 보장
  리포지토리는 항상 모든 서비스가 연동된 올바른 상태를 유지함</li>
<li>통합된 버전 관리
  모든 서비스가 연동된 상태에서 손쉽게 하나의 버전으로 관리 가능</li>
<li>코드의 공유와 재사용이 용이
소스 단위의 연동이 이루어진 상태</li>
<li>의존성 관리가 쉬움
  전체 서비스의 의존 관계가 한 리포지토리에서 확인 및 설정 가능</li>
<li>원자 단위 변화
  변화가 여러 스텝이 아니라 한 리포지토리에서 한 스텝으로 이루어짐</li>
<li>여러 프로젝트 팀 간의 협업이 쉬움
  하나의 리포지토리에서 함께 작업하며, 여러 서비스에 손쉽게 접근 가능</li>
<li>유연한 팀 바운더리 설정과 코드 오너쉽을 가져갈 수 있음
  하나의 리포지토리, 하나의 서비스에 제한된 코드 오너쉽을 유지하지 않아도 됨</li>
<li>통합 CI 및 테스트
  모든 소스가 연동된 상태. CI 및 테스트 구성이 손쉬움</li>
<li>전체 코드가 트리 구조로 명확히 보임</li>
<li>한 번의 코드 리뷰에 모든 변화가 요약</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li>무분별한 의존성 연결 가능
  의존성 연결이 쉽기 때문에 오히려 과도한 의존 관계가 나타날 수 있음</li>
<li>형상 관리 및 CI 속도 저하
  리포지토리의 크기가 크기 때문에, 리포지토리 훅을 기반으로 동작하는 도구들의 속도가 느려짐</li>
</ul>
<h3 id="예시">예시</h3>
<pre><code>coupang_eats_monorepo
    -&gt; app
      -&gt; 고객용 앱A
      -&gt; 드라이버용 앱B
      -&gt; 관리자용 웹C
    -&gt; eats_common_features
    -&gt; eats_design_system
    -&gt; eats_network
    -&gt; eats_utils</code></pre><p>이렇게 각각의 패키지로 분할하는 것은 협업/재사용성/유지보수에 매우 유용하다. 그러나 <strong>각각의 버전</strong>을 가지고 있는 <strong>많은 패키지</strong>를 관리하고 변경하는 것은 복잡하고, 추적 및 테스트가 어렵다.</p>
<p><strong>Melos</strong>는 여러 패키지가 서로 완전히 독립적이면서도 하나의 저장소 내에서 함께 작동할 수 있도록 하여 이러한 문제를 해결하는 데 도움을 준다.</p>
<p><strong>Multi Package</strong>로 프로젝트를 관리하기로 하였다면 이를 좀 더 쉽게 도와주는 도구 <strong>Melos</strong>가 있다.</p>
<h2 id="melos란">Melos란?</h2>
<ul>
<li>Dart에서 <strong>여러 패키지를 관리</strong>하는 데 사용되는 CLI도구</li>
<li><a href="https://docs.page/invertase/melos">Melos</a>는 Flutter 커뮤니티에서 잘 알려진 팀, invertase 에 의해 개발되었다.</li>
</ul>
<pre><code>my_project
├── apps
│   ├── apps_1
│   └── apps_2
├── packages
│   ├── package_1
│   └── package_2
├── melos.yaml
├── pubspec.yaml
└── README.md
</code></pre><h3 id="기능">기능</h3>
<ul>
<li>자동 버전 관리 및 변경 로그 생성.</li>
<li>pub.dev에 패키지를 자동으로 게시.</li>
<li>로컬 패키지 연결 및 설치.</li>
<li>패키지 전체에 걸쳐 동시 명령을 실행.</li>
<li>로컬 패키지 및 해당 종속성 목록 조회.</li>
</ul>
<p><strong>Melos</strong>는 Dart 분석기가 패키지를 읽는데 사용하는 로컬 파일을 재정의하여 문제를 해결한다. <code>melos.yaml</code> 에 정의된 로컬 패키지가 존재하고 다른 로컬 패키지에 해당 패키지가 종속성으로 나열되어 있는 경우 버전 지정 여부에 관계없이 연결된다.</p>
<h3 id="melosyaml">melos.yaml</h3>
<pre><code>name: my_project

packages:
  - apps/**
  - packages/**
</code></pre><h3 id="melos-bootstrap">melos bootstrap</h3>
<ul>
<li>모든 패키지 종속성을 설치한다.(내부적으로 <code>pub get</code>을 사용하여) </li>
<li>pubspec.yaml 을 편집할 필요 없이 경로 종속성 재정의를 통해 모든 패키지를 로컬로 연결한다.</li>
</ul>
<pre><code>melos bootstrap
# or
melos bs</code></pre><h3 id="melos-script-링크">melos script (<a href="https://melos.invertase.dev/configuration/scripts">링크</a>)</h3>
<ul>
<li>스크립트는 melos 명령의 수명 주기 <strong>Hooks</strong>로 실행되거나 <code>melos run</code>으로 실행된다.</li>
<li>스크립트는 쉘에서 실행된다. Windows에서 셸은 <code>cmd.exe</code>이고 다른 모든 플랫폼에서는 <code>sh</code>이다.<pre><code>scripts:
hello:
  name: hey
  description: Greet the world
  run: echo &#39;$GREETING World&#39;
  env:
    GREETING: &#39;Hey&#39;</code></pre></li>
<li>스크립트에서 여러 명령이 실행되고 있고 명령이 실패한 후 더 이상 명령을 실행해서는 안 되는 경우 다음을 사용하여 명령을 연결한다. <code>&amp;&amp;</code><pre><code>scripts:
prepare: melos bootstrap &amp;&amp; melos run build
</code></pre></li>
</ul>
<pre><code>#### steps
- 단일 스크립트 정의 내에서 여러 스크립트의 조합을 활성화한다. 아래 예에서 ```pre-commit``` 스크립트는 ```echo &#39;hello world&#39;``` 를 호출하고 Melos 명령인 ```format``` 및 ```analyze```를 순차적으로 호출하도록 구성된다.</code></pre><p>scripts:
  pre-commit:
    description: pre-commit git hook script
    steps:
      - echo &#39;hello world&#39;
      - format --output none --set-exit-if-changed
      - analyze --fatal-infos</p>
<pre><code>
#### exec
- ```melos exec```를 통해 여러 패키지의 스크립트를 실행한다.
- 이 옵션에는 여러 패키지에서 실행할 ```melos exec``` 명령이 포함되어야 한다.
- 기본 옵션을 사용하는 경우 ```melos exec```에서 명령을 지정하는 것이 가장 쉽다.</code></pre><p>scripts:
  hello:
    exec: echo &#39;Hello $(dirname $PWD)&#39;</p>
<pre><code>- ```run``` 명령에 대한 옵션을 제공해야 하는 경우 ```exec``` 에서 해당 옵션 명령을 지정한다. ```concurrency```</code></pre><p>scripts:
  hello:
    run: echo &#39;Hello $(dirname $PWD)&#39;
    exec:
      concurrency: 1</p>
<pre><code>
##### concurrency
- 동시에 명령을 실행할 패키지 수의 최대 값을 정의한다. 기본값은 ```5```

##### failFast
- ```exec``` 개별 패키지에서 스크립트가 실패하는 경우 빠르게 실패하고 추가 패키지에서 스크립트를 실행하지 않아야 하는지 여부. 기본값은 ```false```

##### orderDependents
- 패키지의 종속성 그래프를 기반으로 여러 패키지에서 스크립트 실행 순서를 지정해야 하는지 여부. 기본값은 ```false```

#### env
- 실행된 명령에 전달될 환경 변수의 맵.


#### packageFilters
- ```exec``` 명령을 사용하면 여러 패키지에 대한 명령을 실행할 수 있다. 스크립트에서 사용되는 경우 섹션에서 필터 옵션을 선언할 수 있다.
- 아래 스크립트 ```hello_flutter```는 ```Flutter``` 패키지에서만 실행된다.</code></pre><p>scripts:
  hello_flutter:
    exec: echo &#39;Hello $(dirname $PWD)&#39;
    packageFilters:
      flutter: true</p>
<pre><code>
#### hooks
- 특정 Melos 명령은 명령 실행 전후뿐만 아니라 명령 실행의 다른 흥미로운 지점에서도 스크립트 실행을 지원한다.
- 후크를 지원하는 모든 명령은 최소한 ```pre``` 및 ```post``` 후크를 지원합니다.
- ```hooks``` 는 ```melos.yaml``` 파일의 명령 섹션에서 구성된다.</code></pre><p>command:
  bootstrap:
    hooks:
      pre: echo <code>bootstrap command is running...</code>
      post: echo <code>bootstrap command is done</code></p>
<pre><code>
#### analyze
- 이 명령은 작업 영역의 모든 로컬 패키지를 분석한다.</code></pre><p>melos analyze</p>
<pre><code>
#### clean
- 이 명령은 현재 작업 공간과 임시 pub 및 생성된 Melos IDE 파일의 모든 패키지를 정리한다.</code></pre><p>melos clean</p>
<pre><code>파일에는 다음이 포함된다.</code></pre><p>{packageRoot}/.packages
{packageRoot}/.flutter-plugins
{packageRoot}/.flutter-plugins-dependencies
{packageRoot}/.dart_tool/package_config.json
{packageRoot}/.dart_tool/package_config_subset
{packageRoot}/.dart_tool/version
{workspaceRoot}/.idea/runConfigurations/melos_*.xml</p>
<pre><code>
#### list
- 로컬 패키지에 대한 정보를 나열한다.</code></pre><p>melos list</p>
<pre><code>
#### version
- 모든 패키지에 대한 변경 로그를 자동으로 버전화하고 생성한다.</code></pre><p>melos version</p>
<p>```</p>
<h2 id="결론-flutter에서의-multi-package">[결론] Flutter에서의 Multi Package</h2>
<p>그렇다면 Flutter에서 Multi Package를 어떻게 사용할까?
정답은 없지만 이상적이라고 생각되는 한 가지 예제를 가져왔다.</p>
<p><img src="https://velog.velcdn.com/images/developer_noah/post/c0b18bab-bef8-4182-aff9-e9d58704e6be/image.webp" alt=""></p>
<h3 id="app">App</h3>
<ul>
<li><p>애플리케이션을 의미한다.</p>
</li>
<li><p>비슷한 기능과 데이터를 사용하는데, 앱이 여러개로 나뉘는 경우에 유용하다.</p>
</li>
<li><p>모바일 카톡 PC 카톡과 같이, 같은 서비스지만 제공하는 플랫폼이 다를 때 사용할 수 있다.</p>
</li>
<li><p>각 App에는 Config, Router(Navigator), 그리고 Page(Screen)가 들어간다.</p>
</li>
</ul>
<h3 id="feature">Feature</h3>
<ul>
<li><p>Feature 패키지는 앱의 특정 기능을 구현하는 모듈들을 담고 있으며, 특정 기능을 완전히 독립적인 단위로 구성하여 개발 가능하다.</p>
</li>
<li><p>각각의 Feature 패키지는 다시 독립적인 하위 패키지들로 구성될 수 있다.</p>
</li>
<li><p>일반적으로 멀티패키지 아키텍처에서의 Feature는 클린 아키텍처와 유사한 방식을 취한다.</p>
</li>
<li><p>각 Feature 패키지는 최소한의 의존성을 가지며, 외부로 노출되는 인터페이스를 통해 다른 패키지와 소통한다.</p>
</li>
<li><p>패키지 간의 인터페이스를 정의함으로써 각 패키지는 독립적으로 개발, 배포, 유지보수할 수 있으며, 전체 시스템의 복잡도를 낮출 수 있다.</p>
</li>
<li><p>Feature가 다른 Feature와 강하게 결합되는 것을 피한다. 이를 통해 각 패키지는 서로 다른 비지니스 기능을 구현하면서 모듈성과 확장성을 유지할 수 있다.</p>
</li>
<li><p>그렇기 때문에 일반적으로 데이터와 도메인 로직이 한 패키지 안에 있는 것이 더 좋다. 각 Feature가 자체 데이터를 갖고 필요한 모든 비지니스 로직을 캡슐화 할 수 있으며, 독립적으로 변경 및 확장될 수 있기 때문이다.</p>
</li>
</ul>
<h3 id="shared-model">Shared model</h3>
<ul>
<li><p>다른 패키지에서 사용될 수 있는 공통 모델이 있는 패키지다.</p>
</li>
<li><p>각 패키지가 필요한 모델을 선택적으로 가져와 사용할 수 있으며, 패키지 간의 종속성을 최소화할 수 있다.</p>
</li>
</ul>
<h3 id="common">Common</h3>
<ul>
<li>프로젝트에서 공통으로 사용되는 기능을 담당한다. 보통 Utility, Helper(validator, parser), constants와 같이 다른 모듈이나 라이브러리에서 자주 사용하는 공통적인 요소를 담당한다.</li>
</ul>
<h3 id="core">Core</h3>
<ul>
<li><p>앱에서 전체적으로 사용되지만, 핵심이되는 부분들이다. 다른 모듈이나 라이브러리가 이를 참조하여 사용할 수 있도록 인터페이스를 제공한다. 보통 보안, 인증, 인프라 관리 등과 같이 프로젝트의 중요한 부분을 담당한다.</p>
</li>
<li><p>인증 서비스와 같은 서비스, exception이나 error 처리를 위한 규칙 및 로직, Http 통신을 위한 Http Client와 같은 것들이 들어간다.</p>
</li>
<li><p>core와 common 패키지는 프로젝트에서 공통으로 사용된다는 점에서 비슷해 보이지만 다르다. common과 core의 구분이 어렵다면, 기능을 제공하느냐, 사용되느냐를 개념적으로 구분하면 편하다. 왜냐하면 주체가 되어서 기능을 제공한다는 것이 핵심 기능이라는 근거가 될수 있기 때문이다.</p>
</li>
</ul>
<h3 id="design-system">Design System</h3>
<ul>
<li>Theme, Components, Typography 같은 디자인 시스템에 해당하는 것들이 있는 패키지다.</li>
</ul>
<blockquote>
<p>결국 이 모든 것은 <strong>협업</strong>, <strong>재사용성</strong>, <strong>테스트</strong>를 위한 것이라 생각한다. 완벽한 정답은 없으며 각자의 상황과 성향에 따라 최선의 방법을 찾아가야 한다.</p>
</blockquote>
<h2 id="참조">참조</h2>
<h4 id="multi-package-프로젝트">Multi Package 프로젝트</h4>
<ul>
<li><a href="https://github.com/Doohyeon-Kim/Flutter-Multi-Package-Architecture?source=post_page-----ba523d0eef67--------------------------------">https://github.com/Doohyeon-Kim/Flutter-Multi-Package-Architecture?source=post_page-----ba523d0eef67--------------------------------</a></li>
</ul>
<h4 id="melos-프로젝트">melos 프로젝트</h4>
<ul>
<li><a href="https://github.com/Doohyeon-Kim/flutter_monorepo?source=post_page-----5d72454d7da0--------------------------------">https://github.com/Doohyeon-Kim/flutter_monorepo?source=post_page-----5d72454d7da0--------------------------------</a></li>
<li><a href="https://github.com/rrifafauzikomara/youtube_video">https://github.com/rrifafauzikomara/youtube_video</a></li>
<li><a href="https://github.com/firebase/flutterfire">https://github.com/firebase/flutterfire</a></li>
<li><a href="https://github.com/fluttercommunity/plus_plugins">https://github.com/fluttercommunity/plus_plugins</a></li>
</ul>
<h4 id="참조-블로그">참조 블로그</h4>
<ul>
<li><a href="https://medium.com/doohyeon-kim/client-side-clean-architecture-flutter-69f7c0091c1f">https://medium.com/doohyeon-kim/client-side-clean-architecture-flutter-69f7c0091c1f</a></li>
<li><a href="https://medium.com/doohyeon-kim/flutter-multi-package-architecture-ba523d0eef67">https://medium.com/doohyeon-kim/flutter-multi-package-architecture-ba523d0eef67</a></li>
<li><a href="https://medium.com/doohyeon-kim/flutter-monorepo-5d72454d7da0">https://medium.com/doohyeon-kim/flutter-monorepo-5d72454d7da0</a></li>
<li><a href="https://medium.com/andrewlee1228/flutter-%EC%95%B1%EC%97%90%EC%84%9C-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81%EC%9D%84-%EB%8D%94-%EC%9E%98-%EA%B4%80%EB%A6%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-monorepo-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-e094219d80b0">https://medium.com/andrewlee1228/flutter-%EC%95%B1%EC%97%90%EC%84%9C-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81%EC%9D%84-%EB%8D%94-%EC%9E%98-%EA%B4%80%EB%A6%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-monorepo-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-e094219d80b0</a></li>
<li><a href="https://medium.com/andrewlee1228/melos%EB%A1%9C-multi-package-flutter%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B4%80%EB%A6%AC-1ba976f20a73">https://medium.com/andrewlee1228/melos%EB%A1%9C-multi-package-flutter%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B4%80%EB%A6%AC-1ba976f20a73</a></li>
<li><a href="https://tech.buzzvil.com/handbook/multirepo-vs-monorepo/">https://tech.buzzvil.com/handbook/multirepo-vs-monorepo/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] Riverpod 상태관리]]></title>
            <link>https://velog.io/@developer_noah/Flutter-Riverpod-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@developer_noah/Flutter-Riverpod-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Sun, 14 Apr 2024 10:14:21 GMT</pubDate>
            <description><![CDATA[<h2 id="패키지-설치">패키지 설치</h2>
<pre><code>flutter pub add flutter_riverpod
flutter pub add riverpod_annotation
flutter pub add dev:riverpod_generator
flutter pub add dev:build_runner
flutter pub add dev:custom_lint
flutter pub add dev:riverpod_lint</code></pre><p>pubspec.yaml</p>
<pre><code>name: my_app_name
environment:
  sdk: &quot;&gt;=3.0.0 &lt;4.0.0&quot;
  flutter: &quot;&gt;=3.0.0&quot;

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.5.1
  riverpod_annotation: ^2.3.5

dev_dependencies:
  build_runner:
  custom_lint:
  riverpod_generator: ^2.4.0
  riverpod_lint: ^2.3.10</code></pre><h2 id="code-generation">Code Generation</h2>
<p>작성한 Riverpod을 Generation 해준다. provider.g.dart 파일이 생긴다.</p>
<pre><code>dart run build_runner watch</code></pre><h2 id="종속성-주입-providerscope">종속성 주입 (ProviderScope)</h2>
<p>main.dart</p>
<pre><code>void main() {
  runApp(
    // Riverpod을 설치하려면 다른 모든 위젯 위에 이 위젯을 추가해야 합니다.
    // 이 위젯은 &quot;MyApp&quot; 내부가 아니라 &quot;runApp&quot;에 직접 파라미터로 추가해야 합니다.
    ProviderScope(
      child: MyApp(),
    ),
  );
}</code></pre><h2 id="annotation">Annotation</h2>
<p>모든 프로바이더는 @riverpod 또는 @Riverpod()로 어노테이션해야 한다. 이 어노테이션은 전역 함수나 클래스에 배치할 수 있습니다.</p>
<p>예를 들어, @Riverpod(keepAlive: true)를 작성하여 &quot;auto-dispose&quot;를 비활성화할 수 있다.</p>
<pre><code>@riverpod
Result myFunction(MyFunctionRef ref) {
  &lt;your logic here&gt;
}</code></pre><h2 id="ref">Ref</h2>
<p>다른 providers와 상호작용하는 데 사용되는 객체다.
모든 providers에는 provider 함수의 매개변수(parameter) 또는 Notifier의 속성(property)으로 하나씩 가지고 있다. 이 객체의 타입은 함수/클래스의 이름에 의해 결정된다.</p>
<h2 id="http-get-요청">Http Get 요청</h2>
<p>provider.dart</p>
<pre><code>import &#39;dart:convert&#39;;
import &#39;package:http/http.dart&#39; as http;
import &#39;package:riverpod_annotation/riverpod_annotation.dart&#39;;
import &#39;activity.dart&#39;;

// 코드 생성이 작동하는 데 필요합니다.
part &#39;provider.g.dart&#39;;

/// 그러면 `activityProvider`라는 이름의 provider가 생성됩니다.
/// 이 함수의 결과를 캐시하는 공급자를 생성합니다.
@riverpod
Future&lt;Activity&gt; activity(ActivityRef ref) async {
  // package:http를 사용하여 Bored API에서 임의의 Activity를 가져옵니다.
  final response = await http.get(Uri.https(&#39;boredapi.com&#39;, &#39;/api/activity&#39;));
  // 그런 다음 dart:convert를 사용하여 JSON 페이로드를 맵 데이터 구조로 디코딩합니다.
  final json = jsonDecode(response.body) as Map&lt;String, dynamic&gt;;
  // 마지막으로 맵을 Activity 인스턴스로 변환합니다.
  return Activity.fromJson(json);
}</code></pre><ul>
<li>네트워크 요청은 UI가 provider를 한 번 이상 읽을 때까지 실행되지 않는다. (lazy)</li>
<li>이후 읽기는 네트워크 요청을 다시 실행하지 않고 이전에 가져온 활동을 반환한다.</li>
<li>UI가 이 공급자의 사용을 중단하면 캐시가 삭제된다. 그런 다음 UI가 이 공급자를 다시 사용하면 새로운 네트워크 요청이 이루어진다.</li>
</ul>
<h2 id="데이터-랜더링">데이터 랜더링</h2>
<p>consumer.dart</p>
<pre><code>import &#39;package:flutter/material.dart&#39;;
import &#39;package:flutter_riverpod/flutter_riverpod.dart&#39;;

import &#39;activity.dart&#39;;
import &#39;provider.dart&#39;;

/// The homepage of our application
class Home extends StatelessWidget {
  const Home({super.key});

  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, ref, child) {
        // Read the activityProvider. This will start the network request
        // if it wasn&#39;t already started.
        // By using ref.watch, this widget will rebuild whenever the
        // the activityProvider updates. This can happen when:
        // - The response goes from &quot;loading&quot; to &quot;data/error&quot;
        // - The request was refreshed
        // - The result was modified locally (such as when performing side-effects)
        // ...
        final AsyncValue&lt;Activity&gt; activity = ref.watch(activityProvider);

        return Center(
          /// Since network-requests are asynchronous and can fail, we need to
          /// handle both error and loading states. We can use pattern matching for this.
          /// We could alternatively use `if (activity.isLoading) { ... } else if (...)`
          child: switch (activity) {
            AsyncData(:final value) =&gt; Text(&#39;Activity: ${value.activity}&#39;),
            AsyncError() =&gt; const Text(&#39;Oops, something unexpected happened&#39;),
            _ =&gt; const CircularProgressIndicator(),
          },
        );
      },
    );
  }
}</code></pre><ul>
<li>Consumer의 ref를 통해 provider를 읽는다.</li>
<li>Stateless 대신 CunsumerWidget을 extends 하는 방법도 있다.</li>
<li>ref.watch를 통해 데이터를 구독하고 상태가 바뀔 때마다 Consumer Widget이 리빌드 된다.</li>
</ul>
<h2 id="notifier-정의">Notifier 정의</h2>
<h4 id="method-형식-notifier-x">Method 형식 (Notifier X)</h4>
<pre><code>@riverpod
Future&lt;List&lt;Todo&gt;&gt; todoList(TodoListRef ref) async {
  // 네트워크 요청을 시뮬레이션합니다. 이는 일반적으로 실제 API로부터 수신됩니다.
  return [
    Todo(description: &#39;Learn Flutter&#39;, completed: true),
    Todo(description: &#39;Learn Riverpod&#39;),
  ];
}</code></pre><h4 id="class-형식-notifier">Class 형식 (Notifier)</h4>
<pre><code>@riverpod
class MyNotifier extends _$MyNotifier {
  @override
  Result build() {
    &lt;your logic here&gt;
    state = AsyncValue.data(42);
  }
  &lt;your methods here&gt;
}</code></pre><h4 id="notifier">notifier</h4>
<ul>
<li>Notifiers는 providers의 &quot;상태저장 위젯(stateful widget)&quot;이다.</li>
<li>@riverpod 어노테이션이 클래스에 배치되면 해당 클래스를 &quot;Notifier&quot;라고 부른다.</li>
<li>클래스는 _$NotifierName을 확장해야 하며, 여기서 NotifierName은 클래스 이름입니다.</li>
<li>Notifiers는 provider의 상태(state)를 수정하는 메서드를 노출할 책임이 있다.</li>
<li>이 클래스의 공개 메서드는 ref.read(yourProvider.notifier).yourMethod()를 사용하여 consumer가 액세스할 수 있다.</li>
</ul>
<h4 id="build-method">build method</h4>
<ul>
<li>모든 notifiers는 build 메서드를 재정의(override)해야 한다.</li>
<li>이 메서드는 일반적으로 notifier가 아닌 provider(non-notifier provider)에서 로직을 넣는 위치에 해당한다.</li>
<li>이 메서드는 직접 호출해서는 안 된다.</li>
</ul>
<h2 id="ref-method">ref Method</h2>
<h4 id="refwatch">ref.watch</h4>
<ul>
<li>일반적으로 유지 관리가 더 쉽기 때문에 일반적으로 다른 옵션보다 ref.watch를 사용할 수 있도록 코드를 설계하는 것이 좋습니다.</li>
<li>ref.watch 메서드는 provider를 받아 현재 상태를 반환합니다. 그러면 리스닝된 provider가 변경될 때마다 provider가 무효화(invalidated)되고 다음 프레임 또는 다음 읽기(read) 시 다시 빌드됩니다.</li>
<li>ref.watch를 사용하면 로직이 &quot;reactive&quot;이면서 &quot;declarative&quot;이게 됩니다.
즉, 필요할 때 로직이 자동으로 다시 계산(recompute)된다는 뜻입니다. 그리고 업데이트 메커니즘이 &#39;on change&#39;와 같은 부작용(side-effects)에 의존하지 않습니다. 이는 StatelessWidgets의 작동 방식과 유사합니다.</li>
<li>예를 들어 사용자의 위치를 수신하는 provider를 정의할 수 있습니다. 그런 다음 이 위치를 사용하여 사용자 근처의 레스토랑 목록을 가져올 수 있습니다.</li>
</ul>
<h4 id="refread">ref.read</h4>
<ul>
<li>이 옵션은 provider의 현재 상태를 반환한다는 점에서 ref.watch와 유사합니다. 하지만 ref.watch와 달리 공급자를 수신(listen)하지 않습니다.</li>
<li>따라서 ref.read는 Notifier의 메서드 내부와 같이 ref.watch를 사용할 수 없는 곳에서만 사용해야 합니다.</li>
</ul>
<h4 id="reflisten--listenself">ref.listen / listenSelf</h4>
<ul>
<li>ref.listen 메서드는 ref.watch의 대안입니다.
이 메서드는 기존의 &quot;listen&quot;/&quot;addListener&quot; 메서드와 유사합니다. 이 메서드는 provider와 callback을 받으며, provider의 콘텐츠가 변경될 때마다 해당 callback을 호출합니다.</li>
<li>ref.listen 대신 ref.watch를 사용할 수 있도록 코드를 리팩토링하는 것이 일반적으로 권장되는데, 전자는 명령형으로 인해 오류가 발생하기 쉽기 때문입니다.
하지만 ref.listen는 큰 리팩토링을 하지 않고도 빠른 로직을 추가하는 데 유용할 수 있습니다.</li>
</ul>
<h2 id="캐시-지우기-및-상태-폐기-disposal">캐시 지우기 및 상태 폐기 (disposal)</h2>
<ul>
<li><p>코드 생성(code-generation)을 사용할 때 기본적으로 provider가 수신이 중지되면 상태가 파괴됩니다.
이는 리스너에 전체 프레임에 대한 활성 리스너가 없을 때 발생합니다. 이 경우 상태가 소멸됩니다.</p>
</li>
<li><p>이 동작은 keepAlive: true를 사용하여 해제(opted out)할 수 있습니다.
이렇게 하면 모든 리스너가 제거될 때 상태가 소멸되는 것을 방지할 수 있습니다.</p>
<pre><code>// We can specify &quot;keepAlive&quot; in the annotation to disable
// the automatic state destruction
@Riverpod(keepAlive: true)
int example(ExampleRef ref) {
return 0;
}</code></pre><h4 id="상태-폐기될-때-로직-실행-방법">상태 폐기될 때 로직 실행 방법</h4>
<pre><code>@riverpod
Stream&lt;int&gt; example(ExampleRef ref) {
final controller = StreamController&lt;int&gt;();

// When the state is destroyed, we close the StreamController.
ref.onDispose(controller.close);

// TO-DO: Push some values in the StreamController
return controller.stream;
}</code></pre><h4 id="생명주기-life-cycles">생명주기 (Life Cycles)</h4>
</li>
<li><p>상태가 폐기될 때 호출되는 ref.onDispose</p>
</li>
<li><p>provider의 마지막 리스너가 제거될 때 호출되는 ref.onCancel.</p>
</li>
<li><p>onCancel이 호출된 후 새 리스너가 추가될 때 호출되는 ref.onResume.</p>
</li>
</ul>
<h4 id="수동으로-강제-삭제">수동으로 강제 삭제</h4>
<ul>
<li>ref.invalidate를 사용하면 현재 provider 상태가 파괴됩니다. 그러면 두 가지 결과가 발생할 수 있습니다:</li>
<li>provider가 청취되고 있으면 새 상태가 생성됩니다.
provider를 청취되고 있지 않으면 provider가 완전히 소멸됩니다.<pre><code>class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
  return ElevatedButton(
    onPressed: () {
      // On click, destroy the provider.
      ref.invalidate(someProvider);
    },
    child: const Text(&#39;dispose a provider&#39;),
  );
}
}</code></pre></li>
</ul>
<h4 id="refkeepalive를-사용하여-폐기disposal를-조정하기">ref.keepAlive를 사용하여 폐기(disposal)를 조정하기</h4>
<ul>
<li><p>위에서 언급했듯이 자동 폐기를 사용하도록 설정하면 provider에 전체 프레임에 대한 리스너가 없는 경우 상태가 삭제됩니다.</p>
</li>
<li><p>하지만 이 동작을 보다 세밀하게 제어하고 싶을 수도 있습니다. 예를 들어, 성공한 네트워크 요청의 상태는 유지하되 실패한 요청은 캐시하지 않으려 할 수 있습니다.</p>
</li>
<li><p>이는 자동 폐기를 활성화한 후 ref.keepAlive를 사용하면 가능합니다. 이 함수를 사용하면 상태의 자동 폐기를 중지하는 시점을 결정할 수 있습니다.</p>
<pre><code>@riverpod
Future&lt;String&gt; example(ExampleRef ref) async {
final response = await http.get(Uri.parse(&#39;https://example.com&#39;));
// We keep the provider alive only after the request has successfully completed.
// If the request failed (and threw), then when the provider stops being
// listened, the state will be destroyed.
ref.keepAlive();

// We can use the `link` to restore the auto-dispose behavior with:
// link.close();

return response.body;
}</code></pre></li>
</ul>
<h4 id="특정-시간-동안-상태를-살아있게-유지하기">특정 시간 동안 상태를 살아있게 유지하기</h4>
<ul>
<li><p>현재 Riverpod은 특정 시간 동안 상태를 유지하는 내장된 방법을 제공하지 않습니다.</p>
</li>
<li><p>하지만 지금까지 살펴본 도구를 사용하면 이러한 기능을 쉽게 구현하고 재사용할 수 있습니다.</p>
</li>
<li><p>Timer + ref.keepAlive를 사용하면 특정 시간 동안 상태를 유지할 수 있습니다. 이 로직을 재사용할 수 있게 하려면 확장 메서드(extension method)로 구현하면 됩니다.</p>
<pre><code>extension CacheForExtension on AutoDisposeRef&lt;Object?&gt; {
/// Keeps the provider alive for [duration].
void cacheFor(Duration duration) {
  // Immediately prevent the state from getting destroyed.
  final link = keepAlive();
  // After duration has elapsed, we re-enable automatic disposal.
  final timer = Timer(duration, link.close);

  // Optional: when the provider is recomputed (such as with ref.watch),
  // we cancel the pending timer.
  onDispose(timer.cancel);
}
}</code></pre><pre><code>@riverpod
Future&lt;Object&gt; example(ExampleRef ref) async {
/// Keeps the state alive for 5 minutes
ref.cacheFor(const Duration(minutes: 5));

return http.get(Uri.https(&#39;example.com&#39;));
}</code></pre></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CI/CD] Flutter 자동 배포 파이프라인 구축하기]]></title>
            <link>https://velog.io/@developer_noah/CICD-Flutter-%EC%9E%90%EB%8F%99-%EB%B0%B0%ED%8F%AC-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@developer_noah/CICD-Flutter-%EC%9E%90%EB%8F%99-%EB%B0%B0%ED%8F%AC-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 27 Mar 2024 01:11:29 GMT</pubDate>
            <description><![CDATA[<h2 id="cicd란">CI/CD란?</h2>
<p>단어의 뜻은 코드에 대한 지속적인 통합(Continous Integration) 및 지속적인 배포(Continous Delivery)이다.
나눠서 보자면 CI는 빌드 및 테스트 자동화하는 것, CD는 배포를 자동화하는 것이라고 볼 수 있다.
큰 틀에서는 앱 출시를 위한 과정을 자동화하는 과정이다.</p>
<h2 id="사용-툴">사용 툴</h2>
<ul>
<li>Github Actions</li>
<li>Fastlane</li>
</ul>
<h2 id="전체-그림">전체 그림</h2>
<p><img src="https://velog.velcdn.com/images/developer_noah/post/fca1fcad-b703-4658-8cf7-0032ab6bc621/image.PNG" alt=""></p>
<ol>
<li>main(master) push<ul>
<li>Fastlane으로 각 스토어에 자동 배포</li>
</ul>
</li>
<li>development, release push<ul>
<li>Test</li>
<li>Firebase 앱 배포 등 개인 또는 회사의 상황에 맞춰서 진행</li>
</ul>
</li>
</ol>
<h2 id="github-actions">Github Actions</h2>
<p>사용량에 따라 비용이 있어서 self-hosted 환경으로 진행했다.</p>
<p><img src="https://velog.velcdn.com/images/developer_noah/post/fd335b89-2203-4afc-a53e-7f14576609e4/image.png" alt=""></p>
<h3 id="self-hosted">self-hosted</h3>
<p><a href="https://danawalab.github.io/common/2022/08/24/Self-Hosted-Runner.html">https://danawalab.github.io/common/2022/08/24/Self-Hosted-Runner.html</a></p>
<h3 id="코드-github-actions">코드 (Github Actions)</h3>
<p>project/.github/workflows/test.yaml
테스트 부분 코드</p>
<pre><code>name: Test development branch

on:
  push:
      branches:
      - development

jobs:
  checkot:
    runs-on: macOS
    steps:
     - uses: actions/checkout@v4
     - run: ls -al</code></pre><ul>
<li>아직 안채움</li>
</ul>
<hr>
<p>project/.github/workflows/deploy_prod.yaml
스토어 배포 부분 코드</p>
<pre><code>name: deploy project to testFlight, playstore internal track

on:
  push:
    branches:
      - main

jobs:
  # IOS
  deploy_ios:
    runs-on: macOS
    steps:    
      - uses: actions/checkout@v4

      - name: Install fastlane
        run: brew install fastlane

      # 빌드 및 배포
      - name: Deploy Product to Store
        run: fastlane build_deploy_prod
        working-directory: ios
        env:
          FASTLANE_TEAM_ID: ${{ secrets.FASTLANE_TEAM_ID }}
          FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}

  # AOS
  deploy_android:
    runs-on: macOS
    needs: [ deploy_ios ]
    steps:
      - uses: actions/checkout@v4

      - name: Install fastlane
        run: brew install fastlane

      # upload key 복호화
      - name: Generate Android keystore
        id: android_keystore
        uses: timheuer/base64-to-file@v1.1
        with:
          fileName: key.jks
          encodedString: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}

      # key.properties 생성
      - name: Create key properties
        run: |
          echo &quot;storeFile=${{ steps.android_keystore.outputs.filePath }}&quot; &gt;&gt; android/key.properties
          echo &quot;storePassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}&quot; &gt;&gt; android/key.properties
          echo &quot;keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}&quot; &gt;&gt; android/key.properties
          echo &quot;keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}&quot; &gt;&gt; android/key.properties

      # 빌드 및 배포
      - name: Deploy Product to Store
        run: fastlane build_deploy_prod
        working-directory: android</code></pre><ul>
<li>gitHub Secrets 이용해서 필요한 값들 저장</li>
<li>FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD -&gt; <a href="https://appleid.apple.com/">https://appleid.apple.com/</a> 에서 만든 앱 비밀번호</li>
<li>ios는 match를 통해 인증 후 빌드 진행</li>
<li>android는 keystore와 key properties로 서명 후 빌드 진행</li>
</ul>
<h2 id="fastlane">Fastlane</h2>
<h3 id="fastlane-init">fastlane init</h3>
<p><a href="https://velog.io/@sangwoo24/Flutter-Fastlane-%EC%9C%BC%EB%A1%9C-CICD-%EA%B5%AC%EC%B6%95-Android">https://velog.io/@sangwoo24/Flutter-Fastlane-%EC%9C%BC%EB%A1%9C-CICD-%EA%B5%AC%EC%B6%95-Android</a></p>
<h3 id="match">match</h3>
<p><a href="https://velog.io/@parkgyurim/iOS-fastlane-match">https://velog.io/@parkgyurim/iOS-fastlane-match</a></p>
<h3 id="app-store-connect-api">App Store Connect API</h3>
<ul>
<li>공식 문서에서 가장 추천하는 방법</li>
<li>2FA(이중 인증)같은 귀찮은 절차를 피하기 위해 사용</li>
<li>성능 상에도 좋다고 함
<img src="https://velog.velcdn.com/images/developer_noah/post/0cd51bd8-bc2d-47fe-ba53-ce8e9a0dbf33/image.png" alt=""></li>
<li>사용자 및 엑세스 -&gt; 통합 -&gt; 팀 키에서 생성</li>
<li>key id, iuuser id, p8 키 파일(한 번만 다운로드 가능)</li>
<li>ios/fastlane/${keyId}.json 으로 파일 만든 후 upload_to_testflight에 api_key_path 등록</li>
</ul>
<p>ios/fastlane/${keyId}.json</p>
<pre><code>{
    &quot;key_id&quot;: keyID,
    &quot;issuer_id&quot;: issuerID,
    &quot;key&quot;: &quot;-----BEGIN PRIVATE KEY-----\nkey Conetent\n-----END PRIVATE KEY-----&quot;,
    &quot;duration&quot;: 1200,
    &quot;in_house&quot;: false
}</code></pre><h3 id="코드-ios">코드 (IOS)</h3>
<p>project/ios/fastlane/Appfile</p>
<pre><code>app_identifier &quot;com.foopolog&quot; # The bundle identifier of your app
apple_id &quot;apple@gmail.com&quot;  # Your Apple email address

# You can uncomment the lines below and add your own
# team selection in case you&#39;re in multiple teams
# team_name &quot;Felix Krause&quot;
team_id &quot;ZZZZZZZZZZ&quot;

# To select a team for App Store Connect use
# itc_team_name &quot;Company Name&quot;
# itc_team_id &quot;18742801&quot;</code></pre><ul>
<li>관리자로 초대되어 있는 team이 많아서 추후에 에러가 나기 때문에 <strong>team_id</strong> 명시</li>
</ul>
<hr>
<p>project/ios/fastlane/Matchfile</p>
<pre><code>git_url(&quot;git@github.com:git_id/Fastlane_Cert.git&quot;)

storage_mode(&quot;git&quot;)

type(&quot;appstore&quot;) # The default type, can be: appstore, adhoc, enterprise or development

app_identifier([&quot;com.foopolog&quot;])
username(&quot;apple@gmail.com&quot;) # Your Apple Developer Portal username

# For all available options run `fastlane match --help`
# Remove the # in the beginning of the line to enable the other options

# The docs are available on https://docs.fastlane.tools/actions/match</code></pre><ul>
<li>match를 통해 생성한 <strong>Certificate</strong> 와 <strong>Provisioning profile</strong>의 git_url 명시</li>
</ul>
<hr>
<p>project/ios/fastlane/Fastfile</p>
<pre><code>default_platform(:ios)

desc &quot;Deploy a product version to Apple App Store&quot;
lane :build_deploy_prod do
    ## match
    match(readonly: true)

    ## flutter init
    sh(&#39;flutter pub get&#39;)
    cocoapods(
        repo_update: true,
        use_bundle_exec: false,
    )

    ## build App
    build_app(
        clean: true,
        scheme: &quot;Runner&quot;,
        workspace: &quot;Runner.xcworkspace&quot;,
    )

    ## deploy App
    upload_to_testflight(
        api_key_path: &quot;fastlane/${keyId}.json&quot;,
        team_id: ENV[&quot;FASTLANE_TEAM_ID&quot;],
        skip_waiting_for_build_processing: true,
    )
end</code></pre><ul>
<li>Matchfile 정보로 match 진행</li>
<li>앱 빌드 후 테스트 플라이트에 배포</li>
</ul>
<hr>
<h3 id="코드-android">코드 (Android)</h3>
<p>project/andorid/fastlane/Appfile</p>
<pre><code>json_key_file(&quot;경로/serviceAccount.json&quot;)
package_name(&quot;com.foopolog&quot;)</code></pre><ul>
<li>json_key_file -&gt; <a href="https://console.cloud.google.com/">구글클라우드콘솔</a>에서 서비스 계정을 만들고 다운받은 키 파일이다. github Secrets에 저장 후 사용하려 했지만 파일을 못읽어서 일단 원격 저장소에 업로드해서 진행했다.</li>
</ul>
<p>project/andorid/fastlane/Fastfile</p>
<pre><code>desc &quot;Deploy a Product version to Google Play Store&quot;
lane :build_deploy_prod do
    ## build APP
    sh(&quot;flutter build appbundle&quot;)

    ## deploy APP
    upload_to_play_store(
        aab: &quot;../build/app/outputs/bundle/release/app-release.aab&quot;,
        track: &#39;internal&#39;,
        skip_upload_metadata: true,
    )
end</code></pre><ul>
<li>앱 번들 생성 후 스토어에 내부 테스트로 배포</li>
</ul>
<p><img src="https://velog.velcdn.com/images/developer_noah/post/5af01b97-36bb-4b20-828c-1c51f25d39f5/image.png" alt=""></p>
<h2 id="성과">성과</h2>
<ul>
<li>배포 및 테스트 자동화</li>
<li>테스트로 앱 품질 유지</li>
<li>Github Actions, Fastlane 사용법</li>
<li>반복적인 작업에 들어가는 리소스 감소</li>
</ul>
<h2 id="참조">참조</h2>
<ul>
<li><a href="https://docs.fastlane.tools/">공식문서</a></li>
<li><a href="https://medium.com/athenaslab/%ED%95%98%EB%A3%A8%EC%97%90%EB%8F%84-10%EB%B2%88-%EB%B0%B0%ED%8F%AC%ED%95%98%EB%8A%94-flutter-%EC%95%B1-ci-cd-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-9f2fbe080c2b">https://medium.com/athenaslab/%ED%95%98%EB%A3%A8%EC%97%90%EB%8F%84-10%EB%B2%88-%EB%B0%B0%ED%8F%AC%ED%95%98%EB%8A%94-flutter-%EC%95%B1-ci-cd-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-9f2fbe080c2b</a></li>
<li><a href="https://velog.io/@parkgyurim/iOS-fastlane-match">https://velog.io/@parkgyurim/iOS-fastlane-match</a></li>
<li><a href="https://velog.io/@sangwoo24/Flutter-Fastlane-%EC%9C%BC%EB%A1%9C-CICD-%EA%B5%AC%EC%B6%95-Android">https://velog.io/@sangwoo24/Flutter-Fastlane-%EC%9C%BC%EB%A1%9C-CICD-%EA%B5%AC%EC%B6%95-Android</a></li>
<li><a href="https://www.youtube.com/watch?v=dRKuDu9c1So&amp;t=1104s">https://www.youtube.com/watch?v=dRKuDu9c1So&amp;t=1104s</a></li>
<li><a href="https://danawalab.github.io/common/2022/08/24/Self-Hosted-Runner.html">https://danawalab.github.io/common/2022/08/24/Self-Hosted-Runner.html</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] 배포용 APK 빌드]]></title>
            <link>https://velog.io/@developer_noah/Flutter-%EB%B0%B0%ED%8F%AC%EC%9A%A9-APK-%EB%B9%8C%EB%93%9C</link>
            <guid>https://velog.io/@developer_noah/Flutter-%EB%B0%B0%ED%8F%AC%EC%9A%A9-APK-%EB%B9%8C%EB%93%9C</guid>
            <pubDate>Sat, 23 Mar 2024 06:32:45 GMT</pubDate>
            <description><![CDATA[<h2 id="1-런처-아이콘-생성">1. 런처 아이콘 생성</h2>
<ul>
<li>앱을 실행할 때 사용할 런처 아이콘 생성
<a href="https://romannurik.github.io/AndroidAssetStudio/icons-launcher.html">https://romannurik.github.io/AndroidAssetStudio/icons-launcher.html</a></li>
</ul>
<h2 id="2-앱-서명하기">2. 앱 서명하기</h2>
<ul>
<li>업로드 키 생성</li>
</ul>
<pre><code>keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key</code></pre><h2 id="3-keystore-참조하기">3. keystore 참조하기</h2>
<ul>
<li>배포용 앱을 빌드할 때 참조하기 위해 프로젝트의 android/key.properties 파일을 생성한 후 다음과 같이 작성한다.</li>
</ul>
<pre><code>storePassword=&lt;키생성시 입력한 암호&gt;
keyPassword=&lt;키생성시 입력한 암호&gt;
keyAlias=key
storeFile=key.jks</code></pre><h2 id="4-gradle에서-서명-구성하기">4. Gradle에서 서명 구성하기</h2>
<ul>
<li>Gradle 빌드시 key.properties 파일을 참조하도록 android 블럭 상단에 아래의 내용을 추가한다.</li>
</ul>
<pre><code>// start of Gradle 서명 구성
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file(&#39;key.properties&#39;)
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
// end of Gradle 서명 구성

android {
...
}</code></pre><h2 id="5-proguard-사용">5. Proguard 사용</h2>
<ul>
<li><p>Proguard는 배포할 앱의 소스코드를 난독화하는 설정이다. APK 파일의 크기를 줄이고 코드를 디컴파일하여도 소스코드의 내용을 이해할 수 없도록 난독화할 수 있다.</p>
</li>
<li><p>Proguard Rule을 구성하기 위해 android/app/proguard-rules.pro 파일을 생성하고 다음과 규칙을 추가한다.</p>
</li>
</ul>
<pre><code>## Flutter wrapper
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.**  { *; }
-keep class io.flutter.util.**  { *; }
-keep class io.flutter.view.**  { *; }
-keep class io.flutter.**  { *; }
-keep class io.flutter.plugins.**  { *; }
-dontwarn io.flutter.embedding.**</code></pre><ul>
<li>Gradle 빌드시 proguard-rules.pro 파일을 참조하여 코드 난독화와 사이즈를 축소할 수 있도록 /android/app/build.gradle 파일의 buildTypes 블럭안에 다음의 내용을 추가한다.</li>
</ul>
<pre><code>android {
    ...

    buildTypes {
        release {
            // release 속성으로 변경
            signingConfig signingConfigs.release

            // start of 코드난독화 및 사이즈 축소
            minifyEnabled true
            proguardFiles getDefaultProguardFile(&#39;proguard-android.txt&#39;), &#39;proguard-rules.pro&#39;
            // end of 코드난독화 및 사이즈 축소
        }
    }
}</code></pre><h2 id="6-앱-번들-빌드">6. 앱 번들 빌드</h2>
<pre><code>flutter build appbundle</code></pre><h2 id="7-apk-빌드">7. APK 빌드</h2>
<pre><code>flutter build apk --split-per-abi</code></pre><p>참고
<a href="https://here4you.tistory.com/198">https://here4you.tistory.com/198</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] 튕기는 줄]]></title>
            <link>https://velog.io/@developer_noah/Flutter-%ED%8A%95%EA%B8%B0%EB%8A%94-%EC%A4%84</link>
            <guid>https://velog.io/@developer_noah/Flutter-%ED%8A%95%EA%B8%B0%EB%8A%94-%EC%A4%84</guid>
            <pubDate>Wed, 13 Mar 2024 09:30:58 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>튕기는 줄
<a href="https://bouncing-string.web.app/">https://bouncing-string.web.app/</a></p>
</blockquote>
<h2 id="프로젝트-목표">프로젝트 목표</h2>
<ul>
<li>사용자의 제스처에 따라 움직이는 줄 만들기</li>
<li>탄성이 있는 줄 표현</li>
</ul>
<h2 id="기능-소개">기능 소개</h2>
<ul>
<li>점과 점 사이의 거리 구하기</li>
<li>점을 잇는 곡선 만들기</li>
<li>설정한 범위를 벗어났을 때 줄을 놓치는 효과</li>
<li>줄의 탄성 표현</li>
</ul>
<h2 id="문제-사항">문제 사항</h2>
<h4 id="1-점을-잇는-곡선을-어떻게-만들까">1. 점을 잇는 곡선을 어떻게 만들까?</h4>
<ul>
<li>path.quadraticBezierTo()를 이용해 점과 점 사이를 잇는다.</li>
</ul>
<pre><code>    Path path = Path();
    Paint paint = Paint()
      ..color = $style.colors.primary
      ..style = PaintingStyle.stroke
      ..strokeWidth = 4;

    double preX = string.start.x;
    double preY = string.start.y;

    path.moveTo(preX, preY);

    for (int i = 1; i &lt; BouncingString.length; i++) {
      final cx = (preX + string.getPoint(i).x) / 2;
      final cy = (preY + string.getPoint(i).y) / 2;

      path.quadraticBezierTo(preX, preY, cx, cy);

      preX = string.getPoint(i).x;
      preY = string.getPoint(i).y;
    }

    path.lineTo(preX, preY);

    canvas.drawPath(path, paint);</code></pre><p><img src="https://velog.velcdn.com/images/developer_noah/post/4ef7282a-3799-4280-a21d-3283f8c0b74a/image.PNG" alt=""></p>
<h4 id="2-제스처로-일정-범위를-벗어나는-걸-어떻게-감지할까">2. 제스처로 일정 범위를 벗어나는 걸 어떻게 감지할까?</h4>
<ul>
<li>두 점(px, py), (cx, cy)을 이용하여 거리를 구하고, 그 거리와 범위(r)를 비교하여 교차되는지 확인</li>
</ul>
<pre><code>  // 피타고라스의 정리를 이용하여 두 점 사이의 직선 거리를 계산
  static double distance(double x1, double y1, double x2, double y2) {
    final double x = x2 - x1;
    final double y = y2 - y1;

    return math.sqrt(x * x + y * y);
  }

  // 두 점(px, py), (cx, cy)을 이용하여 거리를 구하고, 그 거리와 범위(r)를 비교하여 교차되는지 확인
  static bool lineCircle(double x1, double y1, double x2, double y2, double cx, double cy, double r) {
    final double lineLength = distance(x1, y1, x2, y2);
    final double p = (((cx - x1) * (x2 - x1)) + ((cy - y1) * (y2 - y1))) / math.pow(lineLength, 2);

    final px = x1 + (p * (x2 - x1));
    final py = y1 + (p * (y2 - y1));

    return distance(px, py, cx, cy) &lt; r;
  }</code></pre><p><img src="https://velog.velcdn.com/images/developer_noah/post/5527ba2b-4633-4af1-ab01-dec5b290ecae/image.PNG" alt=""></p>
<h4 id="3-줄의-탄성을-어떻게-표현할까">3. 줄의 탄성을 어떻게 표현할까?</h4>
<ul>
<li>교차 결과를 검사하고 그 결과에 따라 이동속도를 조절하여 애니메이션을 표현</li>
</ul>
<pre><code>    if (GlobalFunction.lineCircle(
      string.start.x,
      string.start.y,
      string.end.x,
      string.end.y,
      moveX,
      moveY,
      string.detect,
    )) {
      string.detect = 300;
      final double tx = (string.middle.ox + moveX) / 2;
      final double ty = moveY;
      string.middle.vx = tx - string.middle.x;
      string.middle.vy = ty - string.middle.y;
    } else {
      string.detect = 10;
      final double tx = string.middle.ox;
      final double ty = string.middle.oy;
      string.middle.vx += tx - string.middle.x;
      string.middle.vx *= bounce;
      string.middle.vy += ty - string.middle.y;
      string.middle.vy *= bounce;
    }

    string.middle.x += string.middle.vx;
    string.middle.y += string.middle.vy;</code></pre><h2 id="성과">성과</h2>
<ul>
<li>CustomPainter에서 path를 쓸 수 있다는 걸 확인</li>
<li>피타고라스의 정의를 이용해 점과 점 사이의 거리를 재는 방법</li>
<li>특정 범위를 벗어나는지 확인하는 방법</li>
<li>탄성을 속도를 이용해 표현하는 방법론 습득</li>
</ul>
<blockquote>
<p>소스 코드
<a href="https://github.com/jsk9106/bouncing_string">https://github.com/jsk9106/bouncing_string</a></p>
</blockquote>
<blockquote>
<p>해당 프로젝트는 Interactive Developer님의 영상을 참고했습니다.
<a href="https://www.youtube.com/watch?v=dXhAQbE8iBg&amp;t=149s">https://www.youtube.com/watch?v=dXhAQbE8iBg&amp;t=149s</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] 양들의 언덕]]></title>
            <link>https://velog.io/@developer_noah/Flutter-%EC%96%91%EB%93%A4%EC%9D%98-%EC%96%B8%EB%8D%95</link>
            <guid>https://velog.io/@developer_noah/Flutter-%EC%96%91%EB%93%A4%EC%9D%98-%EC%96%B8%EB%8D%95</guid>
            <pubDate>Tue, 05 Mar 2024 03:14:42 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>양들의 언덕
<a href="https://hill-of-sheep.web.app/">https://hill-of-sheep.web.app/</a></p>
</blockquote>
<h2 id="프로젝트-개요">프로젝트 개요</h2>
<ul>
<li>데이터를 받아서 보여주는 단순 프론트엔드 개발은 누구나 할 수 있다는 생각이 들었다.</li>
<li>애니메이션이 들어가고, 상호작용이 있는 작업을 하며 성장하고 싶었다.</li>
</ul>
<h2 id="프로젝트-목표">프로젝트 목표</h2>
<ul>
<li>움직이는 언덕과 양을 표현</li>
<li>이글거리는 태양 표현</li>
<li>문제 해결 능력 키우기</li>
</ul>
<h2 id="기능-소개">기능 소개</h2>
<ul>
<li>곡선으로 언덕 그리기</li>
<li>뎁스에 따라 속도가 다른 연출</li>
<li>FPS 개념을 코드에 추가</li>
<li>곡선 위의 좌표와 각도 찾기</li>
<li>선이 지글지글 거리는 스케치 효과</li>
<li>스크린 사이즈에 따라 달라지는 언덕과 양의 속도</li>
<li>계속해서 생성되는 요소를 지니는 리스트의 관리</li>
</ul>
<h2 id="문제-사항">문제 사항</h2>
<h4 id="1-언덕을-어떻게-그릴까">1. 언덕을 어떻게 그릴까?</h4>
<ul>
<li>CustomClipper의 path.quadraticBezierTo()를 이용해 점과 점 사이를 잇는다.<pre><code>final double cx = (pre.x + cur.x) / 2;
final double cy = (pre.y + cur.y) / 2;
</code></pre></li>
</ul>
<p>// 곡선 그리기
path.quadraticBezierTo(pre.x, pre.y, cx, cy);</p>
<pre><code>![](https://velog.velcdn.com/images/developer_noah/post/329d9020-21fc-4657-b4ab-5c5f4d08d7ab/image.PNG)

#### 2. 언덕 위에 움직이는 양의 y좌표를 어떻게 구할까?
- 언덕의 곡선을 잘게 나누고 각 영역의 x, y좌표를 구한다. (2차 베지어 수식 사용)
- 양의 현재 x좌표와 가장 근접한 x좌표의 y좌표를 양의 y좌표로 한다.</code></pre><p>// 2차 베지어 곡선 수식 (곡선)
double getQuadValue(double p0, double p1, double p2, double t) {
  return (1 - t) * (1 - t) * p0 + 2 * (1 - t) * t * p1 + t * t * p2;
}</p>
<pre><code>![](https://velog.velcdn.com/images/developer_noah/post/473abe44-7438-426a-bc99-2600e3f32cec/image.PNG)

#### 3. 양의 기울기는 어떻게 표현할까?
- 언덕의 곡선을 잘게 나누고 각 영역의 x, y좌표를 구한다. (2차 베지어 수식 사용)
- 아크탄젠트를 이용해서 각도를 구한다.
- Transform.rotate로 기울기를 표현한다.</code></pre><p>// 2차 베지어 곡선 수식 (직선)
double quadTangent(double a, double b, double c, double t) {
  return 2 * (1 - t) * (b - a) + 2 * (c - b) * t;
}</p>
<pre><code></code></pre><p>// 각도 구하기
final tx = quadTangent(x1, x2, x3, t);
final ty = quadTangent(y1, y2, y3, t);
final double rotation = atan2(ty, tx);</p>
<pre><code>![](https://velog.velcdn.com/images/developer_noah/post/16cb30f7-df48-4e97-a0c8-a976d0521dbc/image.PNG)

#### 4. 태양의 이글거리는 모습은 어떻게 표현할까?
- 원을 그리는 좌표를 2개의 리스트에 담는다. (Sine, Cosine 이용)</code></pre><p>// 원 좌표 가져오기
Point getCirclePoint({required double t}) {
  final double theta = pi * 2 * t;</p>
<p>  return Point(cos(theta) * radius, sin(theta) * radius);
}</p>
<pre><code></code></pre><p>const double gap = 1 / total;</p>
<p>// 포인트 세팅
for (int i = 0; i &lt; total; i++) {
  final Point point = getCirclePoint(t: gap * i);
  originPoints.add(point);
}</p>
<p>// element 얕은 복사를 위해
points = originPoints.map((e) =&gt; Point(e.x, e.y)).toList();</p>
<pre><code>![](https://velog.velcdn.com/images/developer_noah/post/5b0d47d9-e42c-4d9c-a98f-13738f055416/image.PNG)

- 보여주는 리스트의 좌표를 원조 리스트의 좌표를 참고하여 랜덤으로 변경한다.</code></pre><p>// 포인트 랜덤 변경
List<Point> updatePoints() {
  for (int i = 1; i &lt; total; i++) {
    final Point p = originPoints[i];</p>
<pre><code>points[i].x = p.x + Random().nextInt(sunVariable);
points[i].y = p.y + Random().nextInt(sunVariable);</code></pre><p>  }</p>
<p>  return points;
}</p>
<pre><code>![](https://velog.velcdn.com/images/developer_noah/post/65759e19-2581-4938-8116-731ab5eb33c1/image.PNG)

#### 5. FPS개념을 어떻게 도입할까?
- Timer.periodic을 이용해 초당 30번 랜더링한다.</code></pre><p>Timer.periodic(
  $style.times.ms33, // 30fps
  (_) {
    updatePoints();
    update();
  },
);</p>
<pre><code>
## 성과
- CustomClipper의 활용법
- 2차 베지어 곡선의 활용법
- 아크탄젠트의 활용법
- Sine, Cosine을 이용해 원의 좌표를 얻는 방법
- FPS 개념 도입 경험

&gt; 소스 코드
https://github.com/jsk9106/hill_of_sheep

## 회고
그동안 개발 자체보다는 비즈니스에 중점을 뒀다. 개발은 문제를 해결하기 위한 **수단**이라 생각해 쉽고 빠르게 프로덕트를 만들어내는 것에 집중했다.

하지만 여러 서비스를 만들며 느낀 점 &quot;기술 없이 나오는 서비스는 큰 밸류를 만들어내기 어렵다.&quot;과 무서운 속도로 발전하는 AI는 **특별한 성장** 방법을 찾게 만들었다.

그러던 중 **Interactive 개발자**의 영상을 접했다. 컴퓨터 기술로 표현하고자 하는 것을 멋지게 해내는 것이 내겐 신선한 충격이었다. &quot;나도 저렇게 할 수 있을까?&quot;라는 도전의식을 불태우게 되는 계기가 되었다.

그래서 시작한 프로젝트가 &quot;**양들의 언덕**&quot;이다. Interactive 개발자가 javascript로 만든 영상을 보며 Flutter로 만들었다. 핵심 기능과 개념이 소개가 되어있어서 크게 어렵지 않게 만들 수 있었다. 라고 하고 싶지만, 꼬박 5일 동안 작업을 했다.

방법이 나와있는 데 오래 걸린 이유는 모든 코드와 수식을 **이해**하고 싶었기 때문이다.
- 움직이는 언덕을 어떻게 만드는지
- 배열에 좌표를 계속 추가한다면, length 관리는 어떤 식으로 하는지
- 30fps로 화면을 빌드하면 성능에 무리가 없는지
- 베지어 곡선, 아크탄젠트는 무엇이고 어떤 상황에 쓰이는지
- Stateful 위젯과 GetBuilder를 사용한 것중 어떤 것이 더 효과적인지
- 지글거리는 태양의 랜덤 좌표는 왜 -가 아닌 +로 했는지

등 많은 궁금증이 있었다. **성장하기 위해** 스스로 질문하고 답을 찾아가는 과정이었다. 단순히 클론 코딩이 아니라 문제를 찾아 해결했다. (상태관리, 스크린 사이즈에 따라 속도가 달라지는 기능 등)

이번 프로젝트를 통해 얻은 게 몇 가지 있다.
- 수학을 통해 불가능해 보이는 것들을 해낼 수 있다.
(수식 자체를 이해하지 못해도 **쓰임새**를 알면 충분히 활용 가능하다.)
- 모든 복잡해 보이는 코드는 **하나의 점**에서부터 시작된다.
ex) 움직이는 언덕을 만들기 위해 가장 먼저 점을 찍는 것처럼
- 개발은 문제를 해결하기 위한 **수단**이지만, **실력**이 있어야 문제를 해결할 수 있다.
- 가진 실력을 멋지게 풀어내려면 비즈니스에 대한 **이해**와 **기획 능력**이 필요하다.

당분간은 Interactive 개발 공부를 할 생각이다. **Flutter 스페셜리스트**가 되려면 상상 가능한 모든 UI를 그릴 수 있어야 한다.

&gt; 해당 프로젝트는 Interactive Developer님의 영상을 참고했습니다.
https://www.youtube.com/watch?v=hCHL7sydzn0&amp;t=233s
</code></pre>]]></description>
        </item>
    </channel>
</rss>