<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>문군의 개발일지</title>
    <link>https://moongoon72.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 14 Aug 2026 11:13:43 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>문군_</managingEditor>
    <image>
      <title>문군의 개발일지</title>
      <url>https://tistory1.daumcdn.net/tistory/5977953/attach/6063d393ecc346a3853e9e1423dbcbff</url>
      <link>https://moongoon72.tistory.com</link>
    </image>
    <item>
      <title>[ReactorKit] Reactor 살펴보기</title>
      <link>https://moongoon72.tistory.com/40</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactor는 View로부터 &lt;code&gt;Action&lt;/code&gt;을 입력받아 &lt;code&gt;Mutation&lt;/code&gt;을 생성하고, reduce를 통해 &lt;code&gt;State&lt;/code&gt;를 계산하여 View에 제공하는 역할을 한다.&lt;br /&gt;즉, Reactor는 단방향 데이터 흐름(Action -&amp;gt; Mutation -&amp;gt; State)을 강제하는 ViewModel이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reactor의 책임&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactor는 다음을 책임집니다.&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;입력(Action)을 정의한다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;View가 발생시키는 사용자 입력을 Action으로 모델링 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상태 변경 단위(Mutation)를 정의한다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;State를 변경하는 &quot;이유/사건&quot;을 Mutation으로 표현합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상태(State)를 정의한다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;View가 그려야 하는 UI 상태의 단일 진실원천 (SSOT)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비동기 작업을 수행한다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네트워크/DB 등 side effect가 존재하는 작업을 수행합니다.&lt;/li&gt;
&lt;li&gt;mutate() 메서드에서 수행&lt;/li&gt;
&lt;li&gt;해당 작업을 Mutation으로 변환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상태를 계산한다.(reduce).&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Mutation + 기존 State -&amp;gt; 새 State 로 전환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Action&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Action은 사용자 인터렉션, 즉 View에서 발생한 입력을 의미합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UI 이벤트(버튼 탭, 스크롤, 선택 등)&lt;/li&gt;
&lt;li&gt;생명주기 이벤트(viewDidLoad 등)&lt;/li&gt;
&lt;li&gt;외부 트리거(갱신 이벤트 등)Example예를 들어 값의 증가와 감소를 카운팅하는 &lt;b&gt;카운터&lt;/b&gt;를 만들었다고 해봅시다. 그렇다면 CounterViewReactor는 어떠한 Action을 가질 수 있을까요?&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;enum Action {
    case increase
    case decrease
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 action에는 값의 증가와 감소 두 가지 action을 할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Mutation&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mutation은 Action과 State 사이의 연결 다리입니다. 즉, Mutation은 State를 변경하기 위한 이벤트(상태 변경 단위) 입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Mutation은 &quot;State를 어떻게 바꿀지&quot;에 대한 최소 단위&lt;/li&gt;
&lt;li&gt;Mutation은 &lt;b&gt;상태 변경 결과&lt;/b&gt; (예: setValue)로 표현하는 편이 테스트에 유리합니다.Example예시의 Counter 앱이 1. 값의 증감 2. 증감 진행중 표시 3. 완료 표시 기능이 필요하다고 해봅시다. 그렇다면 다음과 같은 4가지의 Mutation이 필요할 것입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;enum Mutation {
    case increaseValue
    case decraseValue
    case setLoading(Bool)
    case setAlertMessage(String)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;State&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;State는 View가 그리는 UI 상태의 단일 진실원천(Single Source Of Truth) 입니다.&lt;br /&gt;View에서는 이 State를 바인딩하여 state의 변화에 따라 UI를 새로 그리는 역할을 수행합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;State는 UI 렌더링을 위한 데이터만 가짐&lt;/li&gt;
&lt;li&gt;Side effect 객체는 State에 넣지 않음&lt;/li&gt;
&lt;li&gt;일회성 이벤트(Alert/Toast)는 @Pulse 사용을 고려ExampleCounter에는 UI에 보여줄 &lt;b&gt;값&lt;/b&gt;, 로딩 인디케이터를 위한 &lt;b&gt;로딩 여부의 Bool 값&lt;/b&gt;, &lt;b&gt;alertMessage&lt;/b&gt;가 필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 설명한 &lt;b&gt;일회성 이벤트&lt;/b&gt;가 AlertMessage에 해당하므로 @Pulse를 고려할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;struct State {
    var value: Int
    var isLoading: Bool
    @Pulse var alertMessage: String?
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReactorKit의 기본 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;View가 UI 이벤트를 Action으로 변환해 reactor.action에 바인딩&lt;/li&gt;
&lt;li&gt;Reactor는 mutate(aciton:)에서 Mutation 스트림을 생성&lt;/li&gt;
&lt;li&gt;Reactor는 Mutation을 reduce(state:mutation:)로 누적 계산하여 State 방출&lt;/li&gt;
&lt;li&gt;View는 reactor.state를 구독해 UI 갱신&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. mutate(action:)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Action을 받아 Mutation 스트림을 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;golo&quot;&gt;&lt;code&gt;    func mutate(action: Action) -&amp;gt; Observable&amp;lt;Mutation&amp;gt; {
        switch action {
        case .increase:
            Observable.concat([
                Observable.just(Mutation.setLoading(true)),
                Observable.just(Mutation.increaseValue).delay(.milliseconds(500), scheduler: MainScheduler.instance),
                Observable.just(Mutation.setLoading(false)),
                Observable.just(Mutation.setAlertMessage(&quot;Increased!&quot;)),
            ])
        case .decrease:
            Observable.concat([
                Observable.just(Mutation.setLoading(true)),
                Observable.just(Mutation.decraseValue).delay(.milliseconds(500), scheduler: MainScheduler.instance),
                Observable.just(Mutation.setLoading(false)),
                Observable.just(Mutation.setAlertMessage(&quot;Decreased!&quot;)),
            ])
        }
    }&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;mutate에서 side effect가 수행되어야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. reduce(state:mutation:)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mutation을 기반으로 새로운 State를 계산합니다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;func reduce(state: State, mutation: Mutation) -&amp;gt; State {
    var state = state

    switch mutation {
    case .increaseValue:
        state.value += 1
    case .decraseValue:
        state.value -= 1
    case let .setLoading(isLoading):
        state.isLoading = isLoading
    case let .setAlertMessage(message):
        state.alertMessage = message
    }

    return state
}&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;reduce는 &lt;b&gt;순수 함수(pure funciton)&lt;/b&gt; 처럼 유지하는 것이 이상적입니다.&lt;/li&gt;
&lt;li&gt;네트워크 요청과 같은 side effect는 reduce에 두지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. transform&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReactorKit에는 transform이 3종류가 있습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;func transform(action: Observable&amp;lt;Action&amp;gt;) -&amp;gt; Observable&amp;lt;Action&amp;gt;
func transform(mutation: Observable&amp;lt;Mutation&amp;gt;) -&amp;gt; Observable&amp;lt;Mutation&amp;gt;
func transform(state: Observable&amp;lt;State&amp;gt;) -&amp;gt; Observable&amp;lt;State&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것들은 &quot;ReactorKit이 기본으로 만드는 스트림 파이프라인&quot; 중간에 끼워 넣을 수 있는 확장 포인트입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;View에서 들어오는 Action 스트림을 가공&lt;/li&gt;
&lt;li&gt;mutate에서 만들어진 Mutation 스트림을 조작&lt;/li&gt;
&lt;li&gt;최종 State 스트림을 조작/공유할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Swift</category>
      <category>reactorkit</category>
      <category>RX</category>
      <category>SWIFT</category>
      <category>UIKit</category>
      <author>문군_</author>
      <guid isPermaLink="true">https://moongoon72.tistory.com/40</guid>
      <comments>https://moongoon72.tistory.com/40#entry40comment</comments>
      <pubDate>Wed, 4 Feb 2026 17:11:06 +0900</pubDate>
    </item>
    <item>
      <title>[ReactorKit] ReactorKit 기본 개념 살펴보기</title>
      <link>https://moongoon72.tistory.com/39</link>
      <description>&lt;h1&gt;ReactorKit 개념&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ReactorKit이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReactorKit은 Flux 아키텍처의 &lt;b&gt;단방향 데이터 흐름&lt;/b&gt;과 Reactive Programming(RxSwift)의 &lt;b&gt;비동기 스트림 처리&lt;/b&gt;를 &lt;b&gt;결합&lt;/b&gt;한 iOS 아키텍처 프레임워크 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReactorKit은 위에서 서술한 것처럼 Reactive Programming의 개념을 채용했기 때문에 RxSwift와 함께 사용됩니다. 따라서, ReactorKit을 사용하려면 RxSwift에 대한 이해가 전제조건 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReactorKit의 핵심 개념은 &lt;b&gt;Reactor&lt;/b&gt; 입니다. Reactor는 Rx의 개념인 Observable과 함께 사용되며 비동기적으로 데이터를 처리하고, UI와의 상호작용을 쉽게 관리할 수 있도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 MVVM의 &lt;b&gt;뷰 모델(View Model)&lt;/b&gt; 의 역할을 한다고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;다만 ReactorKit의 차이점은 ViewModel을 만들 때 그 안에서의 상태변경 방식을 &lt;b&gt;강제된 규칙 (단방향 데이터 흐름 + 이벤트 모델링) 으로 고정&lt;/b&gt;해 둔 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 그림은 ReactorKit의 작동 방식을 간단하게 표현한 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;613&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ykf79/dJMcajubolH/qAPlkWs6UCMnoM8CzVwMAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ykf79/dJMcajubolH/qAPlkWs6UCMnoM8CzVwMAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ykf79/dJMcajubolH/qAPlkWs6UCMnoM8CzVwMAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fykf79%2FdJMcajubolH%2FqAPlkWs6UCMnoM8CzVwMAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;287&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;613&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 Action은 Reactor로, Reactor에서 방출된 State는 View로 Observable 스트림을 통해 전달됩니다. (단방향적 흐름)&lt;br /&gt;View는 Action만 방출하며, Reactor는 State만을 방출합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;단방향적 흐름으로 인해 &lt;b&gt;View와 비즈니스 로직을 분리&lt;/b&gt;할 수 있으며 &lt;b&gt;모듈 간 결합도를 낮출&lt;/b&gt; 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;View와 Reactor&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;View&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View는 데이터를 표시합니다. View Controller와 Cell은 하나의 뷰로 간주됩니다.&lt;br /&gt;View는 사용자의 입력을 &lt;b&gt;Action&lt;/b&gt; Stream에 바인딩하고, 뷰 &lt;b&gt;State&lt;/b&gt;를 각 UI 컴포넌트에 바인딩 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;뷰 레이어는 &lt;b&gt;비즈니스 로직이 없고,&lt;/b&gt; 단순히 액션 스트림과 상태 스트림을 매핑하는 방식을 정의합니다. (Action과 State를 각각의 요소에 바인딩하는 역할만을 수행)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이를 적용하기 위해서는 &lt;b&gt;View&lt;/b&gt; 프로토콜을 적용하는 것이 일반적입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1190&quot; data-origin-height=&quot;1028&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WMGTI/dJMcabwciX5/7qk5TQ8eUf3Vyba4dBQGfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WMGTI/dJMcabwciX5/7qk5TQ8eUf3Vyba4dBQGfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WMGTI/dJMcabwciX5/7qk5TQ8eUf3Vyba4dBQGfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWMGTI%2FdJMcabwciX5%2F7qk5TQ8eUf3Vyba4dBQGfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;518&quot; data-origin-width=&quot;1190&quot; data-origin-height=&quot;1028&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View 프로토콜은 다음과 같이 &lt;code&gt;DisposeBag&lt;/code&gt;, &lt;code&gt;Reactor&lt;/code&gt;, &lt;code&gt;bind(reactor:)&lt;/code&gt;가 존재하고 disposeBag과 bind(reactor:) 메서드는 반드시 재정의 해주어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;bind 내부에서&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UI 이벤트를 Action으로 변환(map)하여 Reactor의 입력(action)에 연결하고&lt;/li&gt;
&lt;li&gt;State 변화를 관찰하여 UI에 반영하는 바인딩을 정의합니다.&lt;br /&gt;이를 예시 코드로 살펴보면 다음과 같습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;import UIKit

import ReactorKit
import RxCocoa
import RxSwift

final class CounterViewController: UIViewController, View {
    @IBOutlet weak var valueLabel: UILabel!
    @IBOutlet weak var increaseButton: UIButton!
    @IBOutlet weak var decreaseButton: UIButton!
    @IBOutlet weak var activityIndicatorView: UIActivityIndicatorView!

    var disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        self.reactor = CounterViewReactor()
    }

    func bind(reactor: CounterViewReactor) {
        // Action
        increaseButton.rx.tap
            .throttle(
                .milliseconds(500),
                latest: false,
                scheduler: MainScheduler.instance
            )
            .map { CounterViewReactor.Action.increase }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)

        decreaseButton.rx.tap
            .throttle(
                .milliseconds(500),
                latest: false,
                scheduler: MainScheduler.instance
            )
            .map { CounterViewReactor.Action.decrease }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)

        // State
        reactor.state.map { $0.value }
            .distinctUntilChanged()
            .map { &quot;\($0)&quot; }
            .bind(to: valueLabel.rx.text)
            .disposed(by: disposeBag)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reactor&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ejsNzF/dJMcagxvb5V/kNKkrLpKCMSMYbzgSxI860/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ejsNzF/dJMcagxvb5V/kNKkrLpKCMSMYbzgSxI860/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ejsNzF/dJMcagxvb5V/kNKkrLpKCMSMYbzgSxI860/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FejsNzF%2FdJMcagxvb5V%2FkNKkrLpKCMSMYbzgSxI860%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;221&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;472&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Reactor는 View 로부터 Action Stream을 전달 받아, 내부에서 &lt;code&gt;mutate()&lt;/code&gt;와 &lt;code&gt;reduce()&lt;/code&gt; 과정을 거쳐서 State Stream으로 바꾸어 다시 View에 전달합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이를 위해서는 Reactor 프로토콜을 적용해야하고, 해당 프로토콜은 &lt;code&gt;Action&lt;/code&gt;, &lt;code&gt;Mutation&lt;/code&gt;, &lt;code&gt;State&lt;/code&gt; 를 반드시 정의해야 합니다. 또한 State 초기 값 설정을 위한 &lt;code&gt;initialState&lt;/code&gt;도 정의해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Action&lt;/code&gt;은 사용자 인터렉션을 나타내고, &lt;code&gt;State&lt;/code&gt;는 뷰의 상태를 나타냅니다. &lt;code&gt;Mutation&lt;/code&gt;은 &lt;code&gt;Action&lt;/code&gt;과 &lt;code&gt;State&lt;/code&gt; 사이의 브릿지(bridge) 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactor는 action stream과 state stream을 변환하는 두 단계인 &lt;code&gt;mutate()&lt;/code&gt;와 &lt;code&gt;reduce()&lt;/code&gt;를 수행합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;mutate()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;mutate()&lt;/code&gt;는 &lt;code&gt;Action&lt;/code&gt; 스트림을 &lt;code&gt;Mutation&lt;/code&gt; 스트림으로 변환합니다. 변환된 Mutation 스트림은 &lt;code&gt;reduce()&lt;/code&gt; 함수로 전달됩니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;func mutate(action: Action) -&amp;gt; Observable&amp;lt;Mutation&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 사이드 이펙트(예: 비동기 작업, API call)은 여기서 수행됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;reduce()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;reduce()&lt;/code&gt;는 이전 &lt;code&gt;State&lt;/code&gt;와 &lt;code&gt;Mutation&lt;/code&gt;을 활용하여 새로운 &lt;code&gt;State&lt;/code&gt;를 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;func reduce(state: State, mutation: Mutation) -&amp;gt; State&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View에서 reactor의 state를 바인딩 해둔 상태라면, 새로운 state가 방출될 때마다 바인딩 된 UI가 자동으로 업데이트 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://oliveyoung.tech/2023-05-20/OliveYoung-iOS-ReactorKit/&quot;&gt;(올리브영 테크 블로그) iOS ReactorKit 톺아보기&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://github.com/ReactorKit/ReactorKit&quot;&gt;ReactorKit&lt;/a&gt;&lt;/p&gt;</description>
      <category>Swift</category>
      <category>reactorkit</category>
      <category>SWIFT</category>
      <category>UIKit</category>
      <author>문군_</author>
      <guid isPermaLink="true">https://moongoon72.tistory.com/39</guid>
      <comments>https://moongoon72.tistory.com/39#entry39comment</comments>
      <pubDate>Tue, 20 Jan 2026 16:09:34 +0900</pubDate>
    </item>
    <item>
      <title>RxSwift - Observable 생성</title>
      <link>https://moongoon72.tistory.com/38</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WRkEz/dJMcacaL5cz/SfpYsKaFdhzxpMPA1LhNmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WRkEz/dJMcacaL5cz/SfpYsKaFdhzxpMPA1LhNmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WRkEz/dJMcacaL5cz/SfpYsKaFdhzxpMPA1LhNmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWRkEz%2FdJMcacaL5cz%2FSfpYsKaFdhzxpMPA1LhNmK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;303&quot; height=&quot;303&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;Observable&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Observer가 구독하는 대상&lt;/li&gt;
&lt;li&gt;Observable이 배출하는 하나 또는 연속된 항목을 Observer가 반응&lt;/li&gt;
&lt;li&gt;&amp;rarr; 이러한 패턴 덕분에 동시성 연산이 가능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Observable이 객체를 배출할 때까지 기다릴 필요 없이, 배출 시점을 관찰하는 관찰자를 두고 관찰자를 통해 배출 알림을 받으면 되기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;570&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k6gNU/dJMcahpCGv4/8719IQBDSMottG9p9kRVj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k6gNU/dJMcahpCGv4/8719IQBDSMottG9p9kRVj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k6gNU/dJMcahpCGv4/8719IQBDSMottG9p9kRVj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk6gNU%2FdJMcahpCGv4%2F8719IQBDSMottG9p9kRVj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;677&quot; height=&quot;319&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;570&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;Observable을 생성하는 방법 (create, just, of, from)&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Create&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Create 연산자를 사용하면 직접 Observable을 만들 수 있습니다. 이 연산자에는 Observser를 매개변수로 받는 클로저를 전달해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클로저 안에서는 실제 Observable처럼 동작하도록, observer의 onNext, onCompleted, onError 메서드를 적절히 호출해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올바르게 구성된 Observable은 Observer의 onCompleted 또는 onError 중 하나를 정확히 한 번만 호출해야 하며, 그 이후에는 Observer의 어떤 메서드도 더 이상 호출해서는 안 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bL5iwF/dJMcahXtc1X/pwk8sIQBNySIi4u754mAd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bL5iwF/dJMcahXtc1X/pwk8sIQBNySIi4u754mAd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bL5iwF/dJMcahXtc1X/pwk8sIQBNySIi4u754mAd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbL5iwF%2FdJMcahXtc1X%2Fpwk8sIQBNySIi4u754mAd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;400&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1662&quot; data-origin-height=&quot;106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0gQmR/dJMb99LR7CM/bHscZlNlDP8akO9cGMy4pk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0gQmR/dJMb99LR7CM/bHscZlNlDP8akO9cGMy4pk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0gQmR/dJMb99LR7CM/bHscZlNlDP8akO9cGMy4pk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0gQmR%2FdJMb99LR7CM%2FbHscZlNlDP8akO9cGMy4pk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1662&quot; height=&quot;106&quot; data-origin-width=&quot;1662&quot; data-origin-height=&quot;106&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수의 원형을 살펴보면, &amp;ldquo;&lt;b&gt;Observer를 매개변수로 받고, Disposable을 반환하는 클로저&amp;rdquo;&lt;/b&gt;를 매개변수로 받음&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;let observable = Observable&amp;lt;String&amp;gt;.create { observer in
    observer.onNext(&quot;first&quot;)
    observer.onNext(&quot;second&quot;)
    observer.onCompleted()

    return Disposables.create()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 create 매서드 다음 Trailing Closure를 이용해서 매개변수로 받은 observer를 통해 언제 onNext를 통해 이벤트를 방출할 것인지, 혹은 언제 onCompleted or onError를 통해 종료할 것인지를 &lt;b&gt;직접 처음부터 끝까지 직접 조절&lt;/b&gt;할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Just&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특정한 아이템(1개)를 방출&lt;/b&gt;하는 Observable을 생성하는 메서드&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caiHeO/dJMb99ZqwZE/ph1BaIJGVUsXkEXEwPsEmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caiHeO/dJMb99ZqwZE/ph1BaIJGVUsXkEXEwPsEmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caiHeO/dJMb99ZqwZE/ph1BaIJGVUsXkEXEwPsEmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaiHeO%2FdJMb99ZqwZE%2Fph1BaIJGVUsXkEXEwPsEmk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;620&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Create&lt;/b&gt; 메서드를 통해서도 동일하게 동작하지만, 단지 1개의 아이템만을 방출할 때는 Just를 사용해서 간략화 할 수있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Just의 경우 단 하나만을 방출하는 것을 전제로 하기 때문에, 따로 &lt;code&gt;onCompleted&lt;/code&gt; 를 호출하지 않아도 자동으로 호출된 후 dispose 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1044&quot; data-origin-height=&quot;114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6tehY/dJMcafk2dKi/HYPvQJ9WgHjFFIiqhfaAF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6tehY/dJMcafk2dKi/HYPvQJ9WgHjFFIiqhfaAF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6tehY/dJMcafk2dKi/HYPvQJ9WgHjFFIiqhfaAF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6tehY%2FdJMcafk2dKi%2FHYPvQJ9WgHjFFIiqhfaAF1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1044&quot; height=&quot;114&quot; data-origin-width=&quot;1044&quot; data-origin-height=&quot;114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1848&quot; data-origin-height=&quot;460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nSf0H/dJMb99Zqw0V/lTu9M088yWAUF5kaNvwutK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nSf0H/dJMb99Zqw0V/lTu9M088yWAUF5kaNvwutK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nSf0H/dJMb99Zqw0V/lTu9M088yWAUF5kaNvwutK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnSf0H%2FdJMb99Zqw0V%2FlTu9M088yWAUF5kaNvwutK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1848&quot; height=&quot;460&quot; data-origin-width=&quot;1848&quot; data-origin-height=&quot;460&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;golo&quot;&gt;&lt;code&gt;let observable = Observable.just(&quot;first&quot;)
// or
let observable = Observable&amp;lt;[String]&amp;gt;.just([&quot;first&quot;, &quot;second&quot;])&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Of&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1420&quot; data-origin-height=&quot;544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vvdT7/dJMcahpCGwN/ZKPLKImdQPYYlm7wsCgYi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vvdT7/dJMcahpCGwN/ZKPLKImdQPYYlm7wsCgYi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vvdT7/dJMcahpCGwN/ZKPLKImdQPYYlm7wsCgYi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvvdT7%2FdJMcahpCGwN%2FZKPLKImdQPYYlm7wsCgYi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1420&quot; height=&quot;544&quot; data-origin-width=&quot;1420&quot; data-origin-height=&quot;544&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;of 메서드는 가변 매개변수를 입력하여, &lt;b&gt;매개변수를 각각 방출&lt;/b&gt;합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;let observable = Observable.of(&quot;fisrt&quot;, &quot;second&quot;, &quot;third&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;From&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qjk2M/dJMcabQsCfY/4NDMNvEEn8v26UWLBh2Gk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qjk2M/dJMcabQsCfY/4NDMNvEEn8v26UWLBh2Gk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qjk2M/dJMcabQsCfY/4NDMNvEEn8v26UWLBh2Gk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqjk2M%2FdJMcabQsCfY%2F4NDMNvEEn8v26UWLBh2Gk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;630&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;From은 매개변수로 하나의 배열을 받으며, &lt;b&gt;배열의 요소들을 순서대로 방출&lt;/b&gt;하는 &lt;b&gt;Observable Sequence를 생성&lt;/b&gt;합니다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;let observable = Observable.from([&quot;frist&quot;, &quot;second&quot;, &quot;third&quot;])&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Swift</category>
      <category>RxSwift</category>
      <category>SWIFT</category>
      <author>문군_</author>
      <guid isPermaLink="true">https://moongoon72.tistory.com/38</guid>
      <comments>https://moongoon72.tistory.com/38#entry38comment</comments>
      <pubDate>Wed, 14 Jan 2026 17:09:14 +0900</pubDate>
    </item>
    <item>
      <title>RxSwift - Observable, RxCocoa, Driver</title>
      <link>https://moongoon72.tistory.com/37</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lHbMJ/dJMcabwadl9/Tbppp9e3kWq3gqFnDTK0R1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lHbMJ/dJMcabwadl9/Tbppp9e3kWq3gqFnDTK0R1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lHbMJ/dJMcabwadl9/Tbppp9e3kWq3gqFnDTK0R1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlHbMJ%2FdJMcabwadl9%2FTbppp9e3kWq3gqFnDTK0R1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;118&quot; height=&quot;118&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;RxSwift는?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기로 발생한 데이터를 Completion handler가 아닌 &lt;code&gt;return&lt;/code&gt; 값으로 전달해주는 클래스&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dgAnrP/dJMcaaquoJP/VLMVLZhLElpdg8Femkfs40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dgAnrP/dJMcaaquoJP/VLMVLZhLElpdg8Femkfs40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dgAnrP/dJMcaaquoJP/VLMVLZhLElpdg8Femkfs40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdgAnrP%2FdJMcaaquoJP%2FVLMVLZhLElpdg8Femkfs40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1456&quot; height=&quot;362&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;362&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;Observable 생성&lt;/h1&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Observable&amp;lt;String&amp;gt;.create() { observer in
    observer.onNext(&quot;Hello&quot;)
    observer.onCompleted()
    return Disposables.create()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Observable 이 방출하는 데이터 타입을 가지는 객체를 생성후 &lt;code&gt;.create()&lt;/code&gt; 메서드를 통해 Observer를 등록합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Observable의 생명주기&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;create()&lt;/code&gt; 를 통해 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;subscribe()&lt;/code&gt; 를 통해 사용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onNext()&lt;/code&gt; 를 통해 데이터 전송&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onCompleted()&lt;/code&gt; or &lt;code&gt;onError()&lt;/code&gt; 를 통해 종료&lt;/li&gt;
&lt;li&gt;&lt;code&gt;disposed()&lt;/code&gt; : 작업 취소&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Observable의 메서드&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;debug()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;observeOn()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;subscribe()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Observable의 operators&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;just()&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단일 데이터를 전달하는 경우, 정석적인 방법을 다 작성하지 않아도 사용하게 해주는 &lt;code&gt;just&lt;/code&gt;가 존재합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;func foo() -&amp;gt; Observable&amp;lt;String&amp;gt; { return Observable.just(&quot;Hello World&quot;) }&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;&lt;code&gt;from(optional:)&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 데이터를 전달하고 싶을 때는 from 내부에 배열을 만들어서 전송할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;func foo() -&amp;gt; Observable&amp;lt;String&amp;gt; { return Observable.from([&quot;Hello&quot;, &quot;World&quot;]) }&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;&lt;code&gt;subscribe&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;696&quot; data-origin-height=&quot;215&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bj08JD/dJMcag5kgD7/q2j2YSDN0Tn8hcDV2RReNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bj08JD/dJMcag5kgD7/q2j2YSDN0Tn8hcDV2RReNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bj08JD/dJMcag5kgD7/q2j2YSDN0Tn8hcDV2RReNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbj08JD%2FdJMcag5kgD7%2Fq2j2YSDN0Tn8hcDV2RReNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;696&quot; height=&quot;215&quot; data-origin-width=&quot;696&quot; data-origin-height=&quot;215&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Observable 객체의 이벤트를 받을 때 subscribe를 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;.subscribe { [weak self] event in
    switch event {
    case let .next(json):
        self?.editView.text = json
        self?.setVisibleWithAnimation(self?.activityIndicator, false)
    case .error(let e):
        print(&quot;Error: \(e.localizedDescription)&quot;)
        break
    case .completed:
        print(&quot;All observable's completed!!&quot;)
    }
}&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;event 전체 (onNext, onError, onCompleted)를 받고자 할 때는 위 방식을 사용하고,&lt;/li&gt;
&lt;li&gt;위 이벤트 중 일부만 받고싶다면 subscribe에서 각각의 closure 파라미터만 가져와서 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.observeOn(MainScheduler.instance)&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;observeOn 은 onNext를 통해 데이터를 받아온 뒤 메인 스레드에서 처리를 해줘야 할 때 &lt;code&gt;DispatchQueue.main.async{}&lt;/code&gt; 를 통해 메인 스레드에서 처리를 해주지만, &lt;code&gt;observeOn(MainScheduler.instance)&lt;/code&gt; 를 통해서 이를 생략할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;map&lt;/code&gt; , &lt;code&gt;filter&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oxkQq/dJMcaivfqKW/YREgvZdhVybKHpoxLLv5e1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oxkQq/dJMcaivfqKW/YREgvZdhVybKHpoxLLv5e1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oxkQq/dJMcaivfqKW/YREgvZdhVybKHpoxLLv5e1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoxkQq%2FdJMcaivfqKW%2FYREgvZdhVybKHpoxLLv5e1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;750&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;subscribeOn: 시작 메서드를 지정&lt;/li&gt;
&lt;li&gt;observeOn: 이후의 스레드를 지정&lt;/li&gt;
&lt;li&gt;scan&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Disposable&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Observable의 create의 리턴 값으로 나오는 Disposable은 평소에는 사용하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 특정 observable의 이벤트를 종료 or 끝내야 할 상황이라면 &lt;code&gt;.dispose()&lt;/code&gt; 메서드를 통해 종료할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 어떠한 이벤트들이 동작하는 도중, 뷰가 사라진다면 특정 이벤트를 종료해야 할 수 있다. 이 경우에 외부에 disposable을 담는 배열이나 변수를 선언해둔다면, &lt;code&gt;viewDidDisappear&lt;/code&gt; 에서 이벤트를 종료할 수 있게됩니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;var disposableBag: [Disposable] = []
...

override func viewDidDisappear(animated: Bool) {
    super.viewDidDisappear(animated)

    disposableBag.forEach { $0.dispose() }
}
...

func makeObservable() {
    ...
    disposableBag.append(disposable)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DisposableBag&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 &lt;code&gt;Disposable&lt;/code&gt; 배열을 선언해도 좋지만 해당 기능을 RxSwift에서는 미리 만들어두었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DisposeBag&lt;/code&gt; 객체를 생성하고, &lt;code&gt;append&lt;/code&gt; 대신 &lt;code&gt;insert&lt;/code&gt; 를 통해 삽입이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 변수를 할당하지 않고, 생성과 동시에 넣고 싶다면 &lt;code&gt;.disposed(by: disposeBag)&lt;/code&gt; 을 통해 생성과 동시에 &lt;code&gt;disposeBag&lt;/code&gt;에 넣을 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;var disposeBag = DisposeBag()
Observable.create()...
    .disposed(by: disposeBag)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Observable 단점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Observable은 이벤트를 방출하는 스트림이며, 외부에서 임의로 이벤트를 주입(push)할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서 이벤트를 발생시켜야 하는 경우, Observable 역할까지 포함한 &lt;b&gt;Subject&lt;/b&gt; 를 사용해 스트림에 값을 push할 수 있습니다.&lt;/p&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Subject&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Subject에는 총 4가지 종류가 존재합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. PublishSubject&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특징&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;b&gt;실시간 이벤트&lt;/b&gt;에 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. BehaviorSubject&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;항상 최신 값 1개를 저장하고 있음&lt;/li&gt;
&lt;li&gt;구독하면 즉시 최신 값 1개를 먼저 받고, 이후 이벤트도 받음&lt;/li&gt;
&lt;li&gt;반드시 초기값이 필요하거나, 최소 한 번은 onNext가 되어 있어야 함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;현재 상태 표현&lt;/b&gt;에 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. ReplaySubject&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특징&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;b&gt;최근 N개의 이벤트를 새 구독자들도 봐야 할 때&lt;/b&gt; 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. AsyncSubject&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;completed 될 때까지 값을 모음&lt;/li&gt;
&lt;li&gt;완료되면 마지막 값 딱 1개만 방출하고 종료&lt;/li&gt;
&lt;li&gt;완료되지 않으면 아무것도 나가지 않음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최종 결과만 의미 있는 작업&lt;/b&gt;일 때 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Observable vs. Subject&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Observable은 흐름입니다. Observable이 이벤트를 방출하면 Subscribe 한 구독자들이 받는 구조입니다.&lt;br /&gt;간단하게 비유를 들자면 Observable은 인터넷 방송입니다. 그저 흘러가고있을 뿐입니다. 시청자(Subscriber)들은 구독(Subscribe)해서 시청만을 하고 &lt;b&gt;방송을 보는 것 밖에 하지 못합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Subject는 방송국 콘솔입니다. 시청자처럼 방송을 볼 수도 있고(Observable), PD 처럼 방송을 송출할 수도 있습니다(Observer).&lt;br /&gt;즉 Subject는 두 가지 역할을 동시에 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Observable (구독할 수 있음)&lt;/li&gt;
&lt;li&gt;Observer (onNext로 이벤트를 밀어 넣을 수 있음)&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;RxCocoa&lt;/h1&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;viewModel.itemsCount
    .map { &quot;\($0)&quot; }
    .observeOn(MainScheduler.instance)
    .subscribe(onNext: { [weak self] in
        self.itemCountLabel.text = $0
    })
    .disposed(by: disposeBag)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 subscribe를 통해 방출된 이벤트를 처리하는 로직을&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;viewModel.itemsCount
    .map { &quot;\($0)&quot; }
    .observeOn(MainScheduler.instance)
    .bind(to: itemCountLabel.rx.text)
    .disposed(by: disposeBag)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 bind와 UIKit의 extension으로 만든 RxCocoa를 통해 &lt;code&gt;.rx&lt;/code&gt; 를 통해서 값을 바인딩 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 subscribe에서 순환 참조를 방지하기 위한 &lt;code&gt;[weak self]&lt;/code&gt; 캡처 리스트를 사용하지 않아도 순환 참조가 발생하지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;catchErrorJustReturn&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RxCocoa의 경우 UI와 관련되어 있기 때문에 항상 메인 스레드에서 동작하도록 해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 에러가 발생한 경우 해당 스트림은 끊어지게 되는데, UI는 그래서는 안됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 에러가 발생한 경우 끊어지지 않게 값을 내려보낼 수 있도록 하는 메소드가 존재합니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;viewModel.itemsCount
    .map { &quot;\($0)&quot; }
    .catchErrorJustReturn(&quot;&quot;)
    .observeOn(MainScheduler.instance)
    .bind(to: itemCountLabel.rx.text)
    .disposed(by: disposeBag)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 경우 에러가 발생하면 빈 문자열을 리턴해주도록 합니다.&lt;/p&gt;
&lt;h1&gt;Driver&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bG7fNF/dJMcahXtcHM/RiWALsyuHqwD92M4Lpf7i0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bG7fNF/dJMcahXtcHM/RiWALsyuHqwD92M4Lpf7i0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bG7fNF/dJMcahXtcHM/RiWALsyuHqwD92M4Lpf7i0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbG7fNF%2FdJMcahXtcHM%2FRiWALsyuHqwD92M4Lpf7i0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1448&quot; height=&quot;354&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;354&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHNORg/dJMcadU0a2Q/odPKvXHDXveUFiG9ktqn51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHNORg/dJMcadU0a2Q/odPKvXHDXveUFiG9ktqn51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHNORg/dJMcadU0a2Q/odPKvXHDXveUFiG9ktqn51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHNORg%2FdJMcadU0a2Q%2FodPKvXHDXveUFiG9ktqn51%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1228&quot; height=&quot;316&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Driver 객체는&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;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI는 항상 메인스레드에서 동작해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Observable을 사용해도 항상 &lt;code&gt;observeOn(MainScheduler.instance)&lt;/code&gt; 를 해줘야 하는데, 항상 메인 스레드에서 동작하는 Observable을 &lt;code&gt;Drive&lt;/code&gt; 라고 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;asDriver&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 &lt;code&gt;catchErrorJustReturn&lt;/code&gt; 과 &lt;code&gt;observeOn(MainScheduler.instance)&lt;/code&gt; 의 경우 UI 관련된 로직의 경우 필수로 들어가야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 하나로 축약한 메서드가 존재하는데, 이를 &lt;code&gt;asDriver(onErrorJustReturn:)&lt;/code&gt; 이라고 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1466&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6amKI/dJMcaf6oXt0/Cs3hh7gj5lCLDjQFaI1UK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6amKI/dJMcaf6oXt0/Cs3hh7gj5lCLDjQFaI1UK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6amKI/dJMcaf6oXt0/Cs3hh7gj5lCLDjQFaI1UK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6amKI%2FdJMcaf6oXt0%2FCs3hh7gj5lCLDjQFaI1UK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1466&quot; height=&quot;442&quot; data-origin-width=&quot;1466&quot; data-origin-height=&quot;442&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;viewModel.itemsCount
    .map { &quot;\($0)&quot; }
        .asDriver(onErrorJustReturn: &quot;&quot;)
    .drive(itemCountLabel.rx.text)
    .disposed(by: disposeBag)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;asDriver의 리턴 값은 Driver 객체입니다.&lt;/p&gt;
&lt;h1&gt;Relay&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI와 관련된 스트림의 경우 한 번 끊어지면 (UI 스트림이 아니더라도) 다시 연결할 수 없기 때문에, 끊어지지 않는 것이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 지원하는 것이 &lt;code&gt;RxRelay&lt;/code&gt; 라이브러리 내부의 Relay 들입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;844&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ukB3O/dJMcafZCMpQ/bqcFMzRtoXjec3jLC5uqv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ukB3O/dJMcafZCMpQ/bqcFMzRtoXjec3jLC5uqv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ukB3O/dJMcafZCMpQ/bqcFMzRtoXjec3jLC5uqv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FukB3O%2FdJMcafZCMpQ%2FbqcFMzRtoXjec3jLC5uqv1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;533&quot; height=&quot;486&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;844&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;766&quot; data-origin-height=&quot;152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckA99I/dJMcajt82xA/kSyrhoY1VjOefP61hIWJrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckA99I/dJMcajt82xA/kSyrhoY1VjOefP61hIWJrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckA99I/dJMcajt82xA/kSyrhoY1VjOefP61hIWJrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckA99I%2FdJMcajt82xA%2FkSyrhoY1VjOefP61hIWJrk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;519&quot; height=&quot;103&quot; data-origin-width=&quot;766&quot; data-origin-height=&quot;152&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Relay의 경우 &lt;code&gt;onNext&lt;/code&gt; 의 역할을 하는 것이 &lt;code&gt;accept&lt;/code&gt; 메서드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; relay는 &lt;code&gt;onCompleted&lt;/code&gt; 나 &lt;code&gt;onError&lt;/code&gt; 가 존재하지 않기 때문입니다.&lt;/p&gt;</description>
      <category>Swift</category>
      <category>RxSwift</category>
      <category>SWIFT</category>
      <author>문군_</author>
      <guid isPermaLink="true">https://moongoon72.tistory.com/37</guid>
      <comments>https://moongoon72.tistory.com/37#entry37comment</comments>
      <pubDate>Wed, 14 Jan 2026 17:05:52 +0900</pubDate>
    </item>
    <item>
      <title>ReactiveX Introduction 번역</title>
      <link>https://moongoon72.tistory.com/36</link>
      <description>&lt;h1&gt;ReactiveX란?&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1030&quot; data-origin-height=&quot;436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kPYLg/btsQYGpsb4a/1lVe1YROm83wftjzcK4Mwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kPYLg/btsQYGpsb4a/1lVe1YROm83wftjzcK4Mwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kPYLg/btsQYGpsb4a/1lVe1YROm83wftjzcK4Mwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkPYLg%2FbtsQYGpsb4a%2F1lVe1YROm83wftjzcK4Mwk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1030&quot; height=&quot;436&quot; data-origin-width=&quot;1030&quot; data-origin-height=&quot;436&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MS 사에서 만든 observable streams 기반의 비동기 프로그래밍 API&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;http://reactivex.io&quot;&gt;&lt;code&gt;reactivex.io&lt;/code&gt;&lt;/a&gt; 에서는 ReactiveX를 다음과 같이 설명합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;TheObserver.png&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ug5zJ/btsQZpucEUo/Ii3P2Vz6Mlw0qBloAeNhe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ug5zJ/btsQZpucEUo/Ii3P2Vz6Mlw0qBloAeNhe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ug5zJ/btsQZpucEUo/Ii3P2Vz6Mlw0qBloAeNhe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUg5zJ%2FbtsQZpucEUo%2FIi3P2Vz6Mlw0qBloAeNhe0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1338&quot; height=&quot;274&quot; data-filename=&quot;TheObserver.png&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReactiveX는 &lt;code&gt;옵저버 패턴&lt;/code&gt; , &lt;code&gt;이터레이터 패턴&lt;/code&gt; 그리고 &lt;code&gt;함수형 프로그래밍&lt;/code&gt; 의 장점(best ideas)들을 결합한 것입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;It extends &lt;a href=&quot;http://en.wikipedia.org/wiki/Observer_pattern&quot;&gt;the observer pattern&lt;/a&gt; to support sequences of data and/or events and adds operators that allow you to compose sequences together declaratively while abstracting away concerns about things like low-level threading, synchronization, thread-safety, concurrent data structures, and non-blocking I/O.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReactiveX는 옵저버 패턴을 확장해 데이터나 이벤트의 연속적인 흐름을 다룰 수 있도록 하고, 여러 시퀀스를 선언적으로 조합할 수 있는 연산자를 제공합니다. 이를 통해 개발자는 스레드 처리, 동기화, 스레드 안전성, 동시성 자료구조, 논블로킹 I/O와 같은 저수준 구현 문제를 직접 다루지 않고도 복잡한 비동기 로직을 표현할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Observable은 &lt;b&gt;다양한 아이템들의 비동기 흐름에 접근하는 가장 이상적인 방식으로&lt;/b&gt; 그 격차를 메웁니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;b&gt;Single items&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;b&gt;multiple items&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;synchronous&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;T getData()&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Iterable&amp;lt;T&amp;gt; getData()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;asynchronous&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Future&amp;lt;T&amp;gt; getData()&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Observable&amp;lt;T&amp;gt; getData()&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것은 때때로 &amp;ldquo;함수형 반응형 프로그래밍(functional reactive programming)&amp;rdquo;이라고 불리기도 하지만, 이는 잘못된 명칭(misnomer)입니다. ReactiveX는 함수형일 수도 있고, 반응형일 수도 있지만, &amp;ldquo;함수형 반응형 프로그래밍&amp;rdquo;은 전혀 다른 개념입니다. &lt;br /&gt;주요 차이점 중 하나는, 함수형 반응형 프로그래밍은 시간이 흐름에 따라 &lt;b&gt;연속적으로 변하는 값들&lt;/b&gt;을 다루는 반면, ReactiveX는 시간이 지남에 따라 &lt;b&gt;불연속적으로 발생(discrete)하는 값들&lt;/b&gt;을 다룬다는 것입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;왜 Observable을 사용하나요?&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rx의 Observable 모델은 배열과 같은 데이터 컬렉션을 다룰 때 사용하는 &lt;b&gt;간단하고 조합 가능한 연산자들과 동일한 방식으로&lt;/b&gt; 비동기 이벤트 스트림을 다룰 수 있게 해줍니다. 또한 복잡하게 얽힌 콜백 구조에서 벗어나도록 도와주며, 그 결과 코드가 더 읽기 쉬워지고 버그가 발생할 가능성도 줄어듭니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Observable은 조합 가능(Composable)합니다.&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java의 Futures 같은 &lt;b&gt;단일 비동기 실행&lt;/b&gt;에는 간단히 쓸 수 있지만, &lt;b&gt;여러 단계로 중첩되면&lt;/b&gt; 금방 복잡성이 커집니다.&lt;br /&gt;Futures를 이용해 &lt;b&gt;조건부 비동기 실행 흐름&lt;/b&gt;을 최적으로 구성하기는 어렵습니다. &lt;br /&gt;심지어 각 요청의 지연 시간(latency)이 실행 시점마다 달라지므로 사실상 불가능 할 수도 있습니다. &lt;br /&gt;(&amp;rarr; 즉, &amp;ldquo;만약 이 결과가 true인 경우 다음 요청 실행&amp;rdquo; 같은 조건부 플로우를 짜기가 힘들고, 요청 지연시간이 불규칙해서 더 어려워짐) &lt;br /&gt;물론 할 수는 있지만, 그렇게 하면 금방 복잡해져서 에러가 생기기 쉽거나, 아니면 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;Future.get()&lt;/span&gt; 을 너무 일찍 호출해서 &lt;b&gt;차라리 동기 실행처럼&lt;/b&gt; 되버립니다.&lt;br /&gt;그 결과 비동기의 장점(논블로킹)이 사라져 버립니다.&lt;br /&gt;반대로 ReactiveX의 Observables는 &lt;b&gt;비동기 데이터 흐름과 시퀀스를 조합하기 위해 설계된 것&lt;/b&gt;입니다.&lt;br /&gt;(즉, Futures 처럼 단일 작업을 다루는 게 아니라, 여러 비동기 흐름을 조합/연결하는 데 최적화되어 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Observable은 유연(Flexible)합니다.&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReactiveX의 Observable은 (Future 가 하는 것처럼) 단일 값하나의 방출만을 지원하는 것이 아니라, 값들의 연속된 시퀀스나 심지어 무한 스트림도 지원합니다. &lt;b&gt;Observable&lt;/b&gt; 은 이 모든 경우에 사용할 수 있는 단일한 추상화이며, 이는 &lt;b&gt;Iterable&lt;/b&gt;이 가진 유연함과 우아함을 모두 갖추고 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Observable은 특정 방식에 얽매이지 않습니다.&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReactiveX는 특정한 동기/비동기 처리 방식에 치우치지 않습니다. &lt;b&gt;Observable&lt;/b&gt;은 스레드 풀, 이벤트 루프, 논블로킹 I/O, 엑터 모델(Akka 등) 등, 어떤 방식으로든 구현될 수 있습니다. 즉, 개발자의 필요, 스타일, 전문성에 맞는 방식이면 무엇이든 사용할 수 있습니다. 클라이언트 코드 입장에서는, &lt;b&gt;Observable&lt;/b&gt;과 상호작용할 때 항상 비동기적으로 보인다는 것입니다. 실제 내부 구현이 blocking인지 non-blocking인지 무관하게 말입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Observable은 Iterable의 쌍둥이 같은 개념으로, Iterable이 동기적&amp;middot;pull 기반이라면 Observable은 비동기적&amp;middot;push 기반입니다.&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.031%;&quot;&gt;event&lt;/td&gt;
&lt;td style=&quot;width: 36.7054%;&quot;&gt;Iterable (pull)&lt;/td&gt;
&lt;td style=&quot;width: 44.2635%;&quot;&gt;Observable (push)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.031%;&quot;&gt;retrieve data&lt;/td&gt;
&lt;td style=&quot;width: 36.7054%;&quot;&gt;T next()&lt;/td&gt;
&lt;td style=&quot;width: 44.2635%;&quot;&gt;onNext(T)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.031%;&quot;&gt;discover error&lt;/td&gt;
&lt;td style=&quot;width: 36.7054%;&quot;&gt;throws Exception&lt;/td&gt;
&lt;td style=&quot;width: 44.2635%;&quot;&gt;onError (Exception)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.031%;&quot;&gt;complete&lt;/td&gt;
&lt;td style=&quot;width: 36.7054%;&quot;&gt;!hasNext()&lt;/td&gt;
&lt;td style=&quot;width: 44.2635%;&quot;&gt;onCompleted()&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Observable은 어떻게 구현되어 있나요?&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;public Observable&amp;lt;data&amp;gt; getData();&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;호출자와 같은 스레드에서 동작하나요?&lt;/li&gt;
&lt;li&gt;다른 스레드에서 비동기적으로 동작하나요?&lt;/li&gt;
&lt;li&gt;여러 개의 스레드로 일을 나눠서, 순서에 상관없이 데이터를 돌려주나요?&lt;/li&gt;
&lt;li&gt;스레드 풀 대신 엑터를 사용하나요?&lt;/li&gt;
&lt;li&gt;NIO + 이벤트 루프로 네트워크를 비동기 처리하나요?&lt;/li&gt;
&lt;li&gt;이벤트 루프로 작업 스레드와 콜백 스레드를 분리하나요?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;관찰자 입장에서는 중요하지 않습니다!&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 중요한 점: ReactiveX에서는 나중에 마음이 바뀌어도, Observable의 내부 구현 방식을 과감히 바꾸면서도, 그 Observable을 소비하는 코드(클라이언트)를 깨뜨리지 않을 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;콜백에도 나름의 문제가 있습니다.&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콜백은 어떤 것도 블로킹하지 않음으로써&lt;span style=&quot;background-color: #ffc9af;&quot;&gt; Future.get()&lt;/span&gt;에서 발생하는 조기 블로킹 문제를 해결합니다. 콜백은 응답이 준비되었을 때만 실행되므로, 본질적으로 효율적입니다. 하지만 Future와 마찬가지로, 콜백도 단일 레벨의 비동기 실행에는 사용하기 쉽지만, 중첩된 구조로 합성되면 다루기 어렵고 복잡해집니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ReactiveX는 다언어(Polyglot) 구현체입니다.&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReactiveX는 여러 프로그래밍 언어에서 구현된 라이브러리로, 각 언어의 문법적 특성과 관용적 스타일을 반영합니다. 또한 현재도 빠른 속도로 더 많은 언어로 확장되고 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Reactive Programming&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReactiveX는 Observable을 다룰 수 있는 다양한 연산자(filter, select, transform, combine 등)를 제공합니다. 이를 통해 효율적인 실행과 조합이 가능합니다. Observable 클래스는 Iterable의 &amp;ldquo;pull&amp;rdquo; 방식에 대응하는 &amp;ldquo;push&amp;rdquo; 방식이라고 생각하면 됩니다. Iterable에서는 소비자(consumer)가 생산자로부터 값을 끌어오며(pull), 값이 준비될 때까지 스레드가 블로킹 됩니다. 반대로, Observable에서는 생산자(producer)가 값이 준비되는 즉시 소비자에게 값을 밀어(push)줍니다. 이 방식은 값이 동기적으로든 비동기적으로든 도착할 수 있기 때문에 더 유연합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Iterable과 Observable 모두에 map, filter 같은 고차함수를 적용할 수 있는데, 그 유사성을 보여주는 예제 코드&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;Iterable&lt;/code&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;getDataFromLocalMemory()
  .skip(10)
  .take(5)
  .map({ s -&amp;gt; return s + &quot; transformed&quot; })
  .forEach({ println &quot;next =&amp;gt; &quot; + it })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;Observable&lt;/code&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;getDataFromNetwork()
  .skip(10)
  .take(5)
  .map({ s -&amp;gt; return s + &quot; transformed&quot; })
  .subscribe({ println &quot;onNext =&amp;gt; &quot; + it })&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Observable 타입은 기존 GoF Observable 패턴에 부족했던 두 가지 의미(sementics)를 추가하여, Iterable 타입에서 제공되는 것과 대응되도록 합니다.&lt;/p&gt;
&lt;/blockquote&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;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Iterable 에서는 foreach 루프가 정상적으로 끝나는 것이 데이터 없음 신호&lt;/li&gt;
&lt;li&gt;Observable 에서는 &lt;code&gt;onComplete()&lt;/code&gt; 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;생산자가 소비자에게 &lt;b&gt;에러 발생을 알릴 수있는 기능&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Iterable 에서는 순회중 예외가 발생하면 throw&lt;/li&gt;
&lt;li&gt;Observable 에서는 &lt;code&gt;onError()&lt;/code&gt; 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 가지가 추가됨으로써, ReactiveX는 Iterable과 Observable 타입을 조화시킵니다. 둘의 유일한 차이는 데이터가 흐르는 방향 입니다. 이것은 매우 중요한데, 이제 Iterable에서 할 수 있는 모든 연산을 Observable에서도 동일하게 할 수 있기 때문입니다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Swift</category>
      <category>ReactiveX</category>
      <category>RX</category>
      <category>RxSwift</category>
      <category>SWIFT</category>
      <category>SwiftUI</category>
      <category>UIKit</category>
      <author>문군_</author>
      <guid isPermaLink="true">https://moongoon72.tistory.com/36</guid>
      <comments>https://moongoon72.tistory.com/36#entry36comment</comments>
      <pubDate>Wed, 1 Oct 2025 16:59:59 +0900</pubDate>
    </item>
    <item>
      <title>[Swift] CollectionViewCell에서 이미지 요청을 보낼 때 취소될 수 있는 문제</title>
      <link>https://moongoon72.tistory.com/35</link>
      <description>&lt;p&gt;기존 구조에서는 CharacterListViewController에서 &lt;code&gt;supabaseMager.fetchCharacters()&lt;/code&gt;를 통해 캐릭터 정보를 받아오고, SwiftUI 셀에서 직접 이미지 요청을 수행하였습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;// CharacterListViewController
private func fetchCharacters() {
    Task {
        do {
            let fetchedCharacters: [Character] = try await supabaseManager.fetchCharacter()
            characters = fetchedCharacters
            updateSnapshot()

        } catch {
            NSLog(&amp;quot;❌ Error fetching characters: \(error)&amp;quot;)
        }
    }
}
...
// MARK: - UICollectionViewDiffableDataSource method
func setupDiffalbeDataSource() {
    dataSource = CharacterDataSource( collectionView: characterListView.characterCollectionView)
    { collectionView, indexPath, itemIdentifier in
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CharacterCell.reuseIdentifier, for: indexPath)
        cell.contentConfiguration = UIHostingConfiguration {
            CharacterCell(character: itemIdentifier)
        }
        cell.layer.borderWidth = 0.5
        cell.layer.cornerRadius = 5
        return cell
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;struct CharacterCell: View, ReuseIdentifiable {
    var character: Character
    @State private var image: UIImage? = nil

    var body: some View {
        HStack {
            if let image {
                Image(uiImage: image)
                    .resizable()
                    .frame(width: 80, height: 80)
                    .clipShape(.rect(cornerRadius: 15))
            } else {
                ProgressView()
                    .frame(width: 80, height: 80)
                    .background(Color.gray.opacity(0.3))
                    .clipShape(.rect(cornerRadius: 15))
                    .task {
                        image = await ImageCacheManager.shared.fetch(for: character.imageURL)  // SwiftUI 셀에서 직접 요청을 수행
                    }
            }
            Spacer()
            Text(character.name)
            Spacer()
        }
        .padding()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;해당 방식을 사용했을 때 이미지를 패치하고 있는 도중에 셀을 스크롤하게 되면 패치 요청이 취소되는 문제가 발생하였습니다.&lt;/p&gt;
&lt;h2&gt;원인&lt;/h2&gt;
&lt;p&gt;문제의 원인은 SwiftUI의 .task modifier의 동작 방식에 있었습니다.&lt;br&gt;&lt;img src=&quot;https://i.imgur.com/cgd3Hl0.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;task 메서드에 대한 공식 문서에 &lt;code&gt;뷰가 사라지고 난 다음 자동으로 task가 취소된다.&lt;/code&gt; 라고 나와있었고, 이를 통해 &lt;strong&gt;셀이 화면에 등장할 때마다 실행되며, 셀이 사라지는 경우 해당 작업도 같이 취소된다&lt;/strong&gt;는 것을 알았습니다.&lt;/p&gt;
&lt;p&gt;또한, 이미지 캐싱은 비동기 작업이므로 &lt;strong&gt;셀 뷰의 생명주기에 의존하지 않는 위치&lt;/strong&gt;에서 관리되어야 안정성이 보장되어 다른 방법을 생각해보았습니다.&lt;/p&gt;
&lt;h2&gt;해결방안&lt;/h2&gt;
&lt;p&gt;해당 문제를 해결하기 위해서 SwiftUI 셀에서 요청을 보내는 방법 대신, 셀 외부에서 비동기적으로 요청을 보내고, 이미지를 패치하여 변경사항이 생길 때 이를 반영하는 식으로 수정해주고자 하였습니다.&lt;/p&gt;
&lt;p&gt;즉, 기존처럼 캐릭터 정보를 먼저 받아온 후, viewModel을 하나 두어 각 캐릭터들에 대한 요청을 ViewModel에서 수행하도록 하였습니다.&lt;br&gt;이미지 요청은 글로벌 스레드에서 동작하고, UI 업데이트만 메인 스레드에서 처리하도록 하였습니다.&lt;/p&gt;
&lt;h2&gt;변경 후 구조&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;// ViewModel
@MainActor
final class CharacterListViewModel: ObservableObject {
    @Published private(set) var characterImages: [Int: UIImage] = [:]

    func loadImage(for character: Character) {
        Task {
            if let image = await ImageCacheManager.shared.fetch(for: character.imageURL) {
                characterImages[character.id] = image
            }
        }
    }

    func image(for character: Character) -&amp;gt; UIImage? {
        return characterImages[character.id]
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;// CharacterListViewController
private func fetchCharacters() {
        Task {
            do {
                let fetchedCharacters: [Character] = try await supabaseManager.fetchCharacter()
                characters = fetchedCharacters
                updateSnapshot()

                for character in characters {
                    characterViewModel.loadImage(for: character)
                }
            } catch {
                NSLog(&amp;quot;❌ Error fetching characters: \(error)&amp;quot;)
            }
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;// CharacterCell
struct CharacterCell: View, ReuseIdentifiable {
    var character: Character
    @ObservedObject var viewModel: CharacterListViewModel

    var body: some View {
        HStack(alignment: .center) {
            if let image = viewModel.image(for: character) {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFit()
                    .frame(height: 80)
                    .clipShape(.rect(cornerRadius: 15))
            } else {
                Image(&amp;quot;mokujin&amp;quot;)
                    .resizable()
                    .scaledToFit()
                    .frame(height: 80)
                    .background(Color.gray.opacity(0.3))
                    .clipShape(.rect(cornerRadius: 15))
            }
            Text(character.name)
                .font(.title2)
                .padding(.leading, 5)
            Spacer()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ViewModel을 ObservableObject를 채택하게 만들어, 이미지가 저장되었는지를 관리하는 딕셔너리의 값이 변경하면 셀을 다시 그리게 하는 방식을 채택하였습니다.&lt;/p&gt;</description>
      <category>사이드 프로젝트</category>
      <category>SWIFT</category>
      <category>SwiftUI</category>
      <category>UIKit</category>
      <author>문군_</author>
      <guid isPermaLink="true">https://moongoon72.tistory.com/35</guid>
      <comments>https://moongoon72.tistory.com/35#entry35comment</comments>
      <pubDate>Tue, 8 Apr 2025 17:24:49 +0900</pubDate>
    </item>
    <item>
      <title>[Swift] iOS 앱의 라이프 사이클 관리</title>
      <link>https://moongoon72.tistory.com/34</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 제가 iOS 앱의 라이프 사이클 관리에 대해 공부하기 위해 공식 문서를 참고하면서 정리한 부분을 포스팅하려고 합니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;앱의 현재 상태란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱의 현재 상태에 따라 앱이 언제든지 수행할 수 있는 작업과 수행할 수 없는 작업을 결정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 포그라운드 앱은 사용자의 주의를 끌기 때문에 CPU를 비롯한 시스템 리소스보다 우선순위를 가집니다. 반면에 백그라운드 앱은 화면 밖에 있기 때문에 가능한 한 &lt;b&gt;작업을 적게 수행&lt;/b&gt;해야 하며, 가급적 &lt;b&gt;아무것도 수행하지 않는 것&lt;/b&gt;이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱의 상태가 변경되면 UIKit은 적절한 델리게이트 객체의 메서드를 호출하여 사용자에게 알립니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;iOS 13 이상: Scene 기반 앱의 생명 주기 이벤트에 응답하기 위해 UISceneDelegate 객체를 사용합니다.&lt;/li&gt;
&lt;li&gt;iOS 12 이전: UIApplicationDelegate 객체를 사용하여 생명 주기 이벤트에 응답합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생명주기(Life Cycle)란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱의 최초 실행부터 앱이 완전히 종료되기 까지 앱이 가지는 상태와 그 상태들 사이의 전이를 생명주기라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱의 상태는 앱이 현재 어떠한 것을 할 수 있는가를 결정합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;앱의 생명주기(App&amp;rsquo;s Life Cycle)란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱의 실행 / 종료 및 앱이 Foreground / Background에 있을 때, 시스템이 발생시키는 이벤트에 의해 앱의 상태가 전환되는 일련의 과정&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;앱의 상태(App state)&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/0GiUaT5.png&quot; alt=&quot;&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;NotRunning
&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;/li&gt;
&lt;li&gt;Foreground - Inactive
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Inactive는 앱이 실행중이지만 사용자로부터 이벤트를 받을 수 없는 상태. 멀티태스킹 윈도우로 진입하거나 앱 실행중 전화, 알림 등에 의해 앱을 사용할 수 없게 되는 경우 이 상태로 진입합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Foreground - Active
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Active는 앱이 실제 실행중이고 사용자 이벤트를 받아서 상호작용할 수 있는 상태이다. 바로 Active 상태는 될 수 없으며 Inactive 상태를 거쳐 Active 상태가 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Background - Running
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Background는 홈 화면으로 나가거나 다른 앱으로 전환되어 현재 앱이 실질적인 동작을 하지 않는 상태입니다.&lt;/li&gt;
&lt;li&gt;예를 들어 뮤직 앱을 닫아도 재생한 음악이 계속 실행되는 경우, 사용자가 앱을 사용하지 않는 동안 서버와 데이터를 동기화 하거나 타이머가 실행되어야 하는 등의 작업을 이 상태에서 할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Background -Suspended
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Suspended는 앱을 다시 실행했을 때 최근 작업을 빠르게 로드하기 위해서 메모리에 관련 데이터만 저장되어 있는 상태입니다.&lt;/li&gt;
&lt;li&gt;앱이 Background 상태에 진입했을 때 다른 작업을 하지 않으면 Suspended 상태로 진입하게 됩니다. (보통 2-3초 이내)&lt;/li&gt;
&lt;/ol&gt;
&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;Respond to Scene-Based Life Cycle&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✔️iOS 13 버전 이상: Scene 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이 Scene을 지원하는 경우 UIKit은 각각의 Scene에 대해 별도의 생명 주기 이벤트를 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Scene은 기기에서 실행되는 앱 UI의 한 인스턴스를 나타냅니다. 사용자는 각 앱에 대해 여러 개의 Scene을 생성하고 이를 개별적으로 표시하거나 숨길 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Scene은 고유한 생명 주기가 있으므로 각 장면은 서로 다른 실행 상태에 있을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 한 Scene은 foreground에 있는 반면 다른 Scene은 background에 있거나 일시 중단된 상태(suspended)일 수 있습니다.&lt;/p&gt;
&lt;aside&gt; 
&lt;p data-ke-size=&quot;size16&quot;&gt;Scene 지원은 옵트인(opt-in) 기능입니다. 기본 지원을 사용하려면 앱의 Info.plist 파일에 UIApplicationSceneManifest 키를 추가합니다.&lt;/p&gt;
&lt;/aside&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/ENl7VzD.png&quot; alt=&quot;Scene의 상태 전환&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Scene의 상태 전환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림은 Scene의 상태 전환을 보여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 또는 시스템이 앱에서 &lt;b&gt;새 Scene을 요청&lt;/b&gt;하면 UIKit은 이를 &lt;b&gt;생성하고&lt;/b&gt; &lt;b&gt;연결되지 않은 상태(Unattached)&lt;/b&gt;로 전환합니다. 사용자가 요청한 Scene은 빠르게 &lt;b&gt;foreground로 이동&lt;/b&gt;하여 화면에 표시됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시스템 요청 Scene&lt;/b&gt;은 일반적으로 이벤트를 처리할 수 있도록 &lt;b&gt;background로 이동&lt;/b&gt;합니다. 예를 들어, 시스템이 위치 이벤트를 처리하기 위해 background에서 Scene을 실행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사용자가 앱의 UI를 종료&lt;/b&gt;하면 UIKit은 관련 &lt;b&gt;Scene&lt;/b&gt;을 &lt;b&gt;background 상태로 이동&lt;/b&gt;한 다음 &lt;b&gt;일시 중단된(suspended)&lt;/b&gt; 상태로 &lt;b&gt;전환&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UIKit은 언제든지 background 또는 일시 중단된(suspended) Scene의 연결을 끊어 &lt;b&gt;리소스를 회수&lt;/b&gt;하여 해당 Scene을 연결되지 않은 상태로 되돌릴 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&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;사용자가 새 Scene 요청 &amp;rarr; Scene 생성 후 연결되지 않은 상태(unattached)로 전환
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 요청한 Scene은 빠르게 foreground로 이동하여 화면 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;시스템 요청 Scene &amp;rarr; 이벤트 처리를 위해 background에서 처리&lt;/li&gt;
&lt;li&gt;사용자가 UI 종료 &amp;rarr; 관련 Scene을 background로 이동 &amp;rarr; 일시 중단된 상태 (suspended)로 전환&lt;/li&gt;
&lt;li&gt;리소스 회수: background 또는 suspended Scene의 연결을 끊어서 리소스 회수 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;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;code&gt;application(_:didFinishLaunchingWithOptions:)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;앱이 실행되고 앱을 화면에 띄우기 위한 모든 설정이 완료된 뒤, 실제로 화면에 나타나기 직전에 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Scene 연결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이 실행되면 UIKit에 Scene을 연결해야 합니다. Scene이 연결되고 화면에 나타나기까지 과정에서 다음과 같은 순서대로 메서드 들이 호출됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;application(_:configurationForConnecting:options:)&lt;/code&gt;여기서 Configuration은 Scene의 delegation 객체를 지정하는 등 Scene을 연결하기 위한 정보가 들어있는 UISceneConfiguration 객체를 의미합니다. 일반적으로 info.plist에 추가된 기본값을 사용해서 생성합니다.&lt;/li&gt;
&lt;li&gt;새로운 Scene을 만들고 UIKit과 연결하기 위한 configuration을 지정합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scene(_:willConnectTo:options:)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Scene이 연결될 것임을 delegate에 알려줍니다. 기존에 &lt;code&gt;application(_:didFinishLaunchingWithOptions:)&lt;/code&gt; 에서 했던 UIWindow 생성 작업을 이 메서드에서 할 수 있습니다. 스토리보드를 사용한다면 스토리보드에서 entry point를 찾아 내부적으로 UIWindow를 생성합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sceneDidBecomeActive(_:)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;앱이 &lt;b&gt;Inactive&lt;/b&gt; 상태에서 &lt;b&gt;Active&lt;/b&gt; 상태로 전환되었을 때 호출합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. To Background(offscreen): Active - Inactive - Background ( - Suspend)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 실행후 홈으로 나가게 되면 Active &amp;rarr; Inactive &amp;rarr; Background 상태로 상태가 전환됩니다. 이 때, SceneDelegate는 Scene에 다음 순서로 method를 호출하도록 요청합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;sceneWillResignActive(_:)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;앱이 Active에서 Inactive 상태로 전환될 때 호출됩니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sceneDidEnterBackground(_:)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;앱이 Background 상태로 전환되었을 때 호출합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. To Foreground(onscreen): Background - Inactive - Active&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Background 상태에 있는 앱을 다시 실행하면 Background &amp;rarr; Inactive &amp;rarr; Active 상태로 전환됩니다. 이 때, SceneDelegate는 시스템에 다음 순서로 메서드를 호출하도록 요청합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;sceneWillEnterForeground(_:)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;앱이 Background에서 Inactive 상태로 전환될 때 호출됩니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sceneDidBecomeActive(_:)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;앱이 Inactive에서 Active 상태로 전환될 때 호출됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. Scene 연결 해제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 &lt;code&gt;App-Based Life Cycle&lt;/code&gt;은 멀티태스킹 창에서 swipe up 제스처를 사용하여 앱을 종료시켰습니다. 하지만 Scene을 사용할 때는 multi window를 지원하기 때문에, 앱이 둘 이상의 scene window를 가진다면 swipe up 제스처는 앱을 종료시키지 않고 Scene을 해제시키게 됩니다. 만약 모든 Scene의 연결이 해제된다면 앱이 종료됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Scene 연결이 해제될 때는 다음과 같은 순서로 메서드가 호출됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;sceneDidDisconnected(_:)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;delegate에 UIKit에 연결된 Scene의 연결을 해제할 것을 요청합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;application(_:didDiscardSceneSessions:)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;사용자가 멀티태스킹 화면(app switcher)에서 한 개 이상의 Scene을 종료시켰을 때 호출됩니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;applicationWillTerminate(_:)&lt;/code&gt;&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;Respond to App-Based Life Cycle&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS 12 이전 또는 Scene을 사용하지 않는다면 모든 Process / UI Life Cycle event는 &lt;b&gt;AppDelegate&lt;/b&gt;로 전달됩니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/goKDW5l.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 앱 실행: Not Running - Inactive - Active&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이 실행되면 Not Running &amp;rarr; Inactive &amp;rarr; Active 로 상태가 전환됩니다. 이 때, AppDelegate는 시스템에 다음 메서드를 호출하도록 요청합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;application(_:didFinishLaunchingWithOptions:)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;앱이 실행되고 앱을 화면에 띄우기 위한 모든 설정이 완료된 뒤, &lt;b&gt;실제로 화면에 나타나기 직전&lt;/b&gt;에 호출됩니다. UIWindow를 생성하는 등의 작업을 할 수 있습니다. Storyboard를 사용한다면 storyboard에서 entry point를 찾아 내부적으로 UIWindow를 생성합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;applicationDidBecomeActive(_:)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;앱이 Inactive에서 Active 상태로 전환되었을 때 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. To Background(offscreen): Active - Inactive - Background ( - Suspended)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 실행후 홈으로 나가게 되면 Active &amp;rarr; Inactive &amp;rarr; Background 상태로 상태가 전환됩니다. 이 때, AppDelegate는 Scene에 다음 순서로 method를 호출하도록 요청합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;applicationWillResignActive(_:)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;앱이 Active에서 Inactive 상태로 전환될 때 호출됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이 Active에서 Inactive 상태로 전환될 때 호출됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;applicationDidEnterBackground(_:)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;앱이 Background 상태로 전환되었을 때 호출합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. To Foreground(onscreen): Background - Inactive - Active&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Background 상태에 있는 앱을 다시 실행하면 Background &amp;rarr; Inactive &amp;rarr; Active 상태로 전환됩니다. 이 때, AppDelegate는 시스템에 다음 순서로 메서드를 호출하도록 요청합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;applicationWillEnterForeground(_:)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;앱이 Background에서 Inactive 상태로 전환될 때 호출됩니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;applicationDidBecomeActive(_:)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;앱이 Inactive에서 Active 상태로 전환될 때 호출됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 앱 종료: (some States) - Background or Suspended - Not Running&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 직접 앱을 종료하는 경우, 앱은 다시 Not Running 상태로 돌아갑니다. 이 때, AppDelegate는 시스템에 다음과 같은 메서드를 호출하도록 요청합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;applicationWillTerminate(_:)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;앱이 사용자에 의해 종료될 때 호출됩니다. 시스템에 의해 예기치 못한 상황에서 종료될 때는 호출하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Application vs. Scene&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 두 방식의 차이점은 UI Life Cycle의 책임이 Scene으로 옮겨감의 차이입니다. AppDelegate의 메서드 이름이 application &amp;rarr; scene으로 바뀌었을 뿐 호출 순서는 동일합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Scene의 연결 유무의 차이가 존재하여 앱 실행후 scene 연결, 앱 종료는 scene은 scene 연결 해제를 먼저 해준다는 것의 차이가 존재합니다.&lt;/p&gt;</description>
      <category>Swift</category>
      <author>문군_</author>
      <guid isPermaLink="true">https://moongoon72.tistory.com/34</guid>
      <comments>https://moongoon72.tistory.com/34#entry34comment</comments>
      <pubDate>Tue, 18 Mar 2025 21:24:47 +0900</pubDate>
    </item>
    <item>
      <title>[Supabase] API 요청은 제대로 이루어졌으나, 응답 데이터는 비어있는 문제</title>
      <link>https://moongoon72.tistory.com/33</link>
      <description>&lt;h2&gt;문제&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;let client = Supabaseclient(supabaseURL, supabaseKey)
client.from(&amp;quot;Character&amp;quot;)
.select()
.exectue()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 코드를 실행했을 때 응답 코드는 200으로 문제없이 응답은 왔으나, Table의 데이터는 들어오지 않는 문제가 발생하였습니다.&lt;/p&gt;
&lt;h2&gt;원인과 해결 과정&lt;/h2&gt;
&lt;p&gt;원인은 몇 가지가 복합적으로 존재했는데요! 문제는 다음과 같았습니다.&lt;/p&gt;
&lt;h3&gt;칼럼 네임 실수&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;CodingKeys로 키를 맞춰줄 때 이름을 실수하는 문제가 있었습니다!&lt;h3&gt;RLS (Row Level Security)&lt;/h3&gt;
&lt;/li&gt;
&lt;li&gt;데이터베이스에 특정 사용자나 그룹이 특정 행에만 접근할 수 있도록 하는 보안 매커니즘&lt;/li&gt;
&lt;li&gt;Supabase의 DB에 접근하기 위해서는 &lt;code&gt;Project URL&lt;/code&gt;과 &lt;code&gt;API Key&lt;/code&gt;가 필요한데, &lt;code&gt;API Key&lt;/code&gt;에는 두 가지 키가 존재합니다.&lt;br&gt;&lt;img src=&quot;https://i.imgur.com/OllSk52.png&quot; alt=&quot;&quot;&gt;&lt;/li&gt;
&lt;li&gt;secret key의 경우 Row Level Security를 우회하여 동작하기에 secret key를 사용하는 경우 문제 없이 데이터를 받아올 수 있지만, 해당 키는 노출되었을 때 DB의 모든 것을 조작할 수 있기 때문에 클라이언트 쪽에서는 사용하지 않는 것이 바람직하다고 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그래서 해당 문제를 해결하기 위해서 &lt;strong&gt;public key를 사용하면서 RLS 정책을 추가&lt;/strong&gt;하는 방식을 사용하였습니다.&lt;/p&gt;
&lt;h4&gt;해결 과정&lt;/h4&gt;
&lt;p&gt;Supabase 관리 페이지에서, Authentication -&amp;gt; Policies 에서 각 테이블에 대한 RLS 설정을 할 수 있습니다!&lt;br&gt;&lt;img src=&quot;https://i.imgur.com/GQ0EmNz.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;테이블 별로 두 개의 버튼으로 쿼리문을 직접 작성하지 않아도 테이블 관련 설정을 할 수가 있었는데, Disable RLS를 할 경우 public key로도 값을 잘 받아오게 할 수 있었지만, 이 경우 읽기 뿐 아니라 모든 접근 및 조작을 공개키로도 가능하게 되어 보안상의 문제가 있습니다.&lt;/p&gt;
&lt;p&gt;따라서 Create policy 를 눌러서 우측 Template에서 원하는 권한을 추가해주면 RLS를 킨 상태에서 특정 권한만 public key로 접근이 가능하도록 할 수 있었습니다!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/D1BCvxb.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/bJx7x75.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</description>
      <category>사이드 프로젝트</category>
      <category>supabase</category>
      <category>SWIFT</category>
      <author>문군_</author>
      <guid isPermaLink="true">https://moongoon72.tistory.com/33</guid>
      <comments>https://moongoon72.tistory.com/33#entry33comment</comments>
      <pubDate>Tue, 11 Mar 2025 12:54:03 +0900</pubDate>
    </item>
    <item>
      <title>[Swift] CoreData를 CloudKit에 연동하기</title>
      <link>https://moongoon72.tistory.com/29</link>
      <description>&lt;h2&gt;기존 Local CoreData 영구 저장소를 iCloud에 연동 가능하도록 수정함.&lt;/h2&gt;
&lt;p&gt;이는 기존에 &lt;code&gt;NSPersistentContainer&lt;/code&gt;를 &lt;code&gt;NSPersistentCloudKitContainer&lt;/code&gt;로 변경해주면 자연스럽게 CloudKit으로 사용이 가능합니다.&lt;br&gt;&lt;img src=&quot;https://i.imgur.com/3VPorcw.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;NSPersistentCloudKitContainer는 NSPersistentContainer를 상속받기 때문에 교체해도 아무런 문제 없이 사용이 가능합니다.&lt;/p&gt;
&lt;h2&gt;Local CoreData로 사용하던 것을 iCloud 동기화 sync 기능을 추가&lt;/h2&gt;
&lt;p&gt;현재까지 알아낸 바로는, 유저가 iCloud 연동을 원하는지 여부를 bool 값으로 저장하고 있다는 가정하에 (UserDefaults에 저장) 이 값을 바탕으로&lt;br&gt;&lt;code&gt;persistentContainer(type: NSPersistentCloudKitContainer).persistentDescriptions.first&lt;/code&gt;로 &lt;code&gt;NSPersistentDescription&lt;/code&gt;을 가져옵니다.&lt;br&gt;해당 description의 &lt;code&gt;.cloudKitContainerOptions&lt;/code&gt;를 nil로 초기화 해주면 해당 영구 저장소는 iCloud를 사용하지 않습니다.&lt;br&gt;그게 아니라면 자동으로 iCloud 연동 기능을 사용하게 됩니다.&lt;/p&gt;
&lt;h2&gt;remote-notification error&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/isVwTFn.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;cloudKit에서의 background update를 위해서는 remote notification 기능을 켜줘야 합니다.&lt;br&gt;&lt;img src=&quot;https://i.imgur.com/3rit2yE.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Target -&amp;gt; Signing &amp;amp; Capabilities -&amp;gt; + -&amp;gt; Background Mode 추가 한 뒤 &lt;code&gt;remote notification&lt;/code&gt;에 체크를 해주면 됩니다.&lt;/p&gt;
&lt;p&gt;CloudKit 추가도 마찬가지로 Target -&amp;gt; Signing &amp;amp; Capabilities -&amp;gt; + -&amp;gt; iCloud 추가하면 됩니다.&lt;/p&gt;
&lt;h2&gt;iCloud 연동할 store 선택&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/0FHp8gd.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;저희 앱은 Default 1개의 Configuration만 사용하기 때문에 해당 Configuration을 선택한 뒤 우측 inspector에서 Used with CloudKit을 선택해주면 동기화하게 됩니다!&lt;/p&gt;
&lt;p&gt;만약 Configurations가 없다면 &lt;code&gt;NSPersistentCloudKitContainer&lt;/code&gt;는 &lt;code&gt;첫 번째 StoreDescription&lt;/code&gt;과  &lt;code&gt;entitlements&lt;/code&gt;의 &lt;code&gt;첫 번째 CloudKit Container identifier&lt;/code&gt;를 일치시킵니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;guard let description = persistentContainer.persistentStoreDescriptions.first
    else { throw Error.storeSetUpFailed }
    if !isSync {
        description.cloudKitContainerOptions = nil
    } else {
        description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: &amp;quot;iCloud.kr.codesquad.boostcamp9.RetsTalk&amp;quot;)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/GyMmFpm.png&quot; alt=&quot;entitlements 파일&quot;&gt;&lt;br&gt;&lt;img src=&quot;https://i.imgur.com/zfQ6HVp.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;CoreData와 CloudKit 연동은 어떻게 자연스레 되는가?&lt;/h2&gt;
&lt;h3&gt;CoreData -&amp;gt; CloudKit&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/pCZcHOp.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;(추측으로는) 앞서 &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;persistentContainer = NSPersistentCloudKitContainer(name: name)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;를 만들어 주었고, 이로부터 CoreData는 반드시 생성이 됩니다. (올바른 name이라면)&lt;/p&gt;
&lt;p&gt;그래서 CloudKit을 껐다가 켰을 때 container를 재생성하고 cloudKitContaierOptions를 할당해주지만,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: &amp;quot;iCloud.kr.codesquad.boostcamp9.RetsTalk&amp;quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;기존의 CoreData가 바뀌는 것은 아니기 때문에, 이 경우에는 Background task로 CloudKit에 전달이 되게 됩니다.&lt;/p&gt;
&lt;h3&gt;CloudKit -&amp;gt; CoreData&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/KSHTudS.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;위 경우는 &lt;code&gt;CloudKit&lt;/code&gt;에서는 사용자의 다른 계정에 주기적으로 &lt;code&gt;Push Notification&lt;/code&gt;을 보냅니다. 그 다음 각 장치에서 시스템은  마지막 패치 이후 변경된 모든 레코드를 다운로드하고, &lt;code&gt;NSManagedObject&lt;/code&gt;의 인스턴스로 변환하는 백그라운드 작업을 만듭니다.&lt;br&gt;CoreData는 이러한 managedObject를 local store에 저장합니다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.apple.com/documentation/coredata/mirroring_a_core_data_store_with_cloudkit/syncing_a_core_data_store_with_cloudkit&quot;&gt;Syncing a Core Data Store with CloudKit&lt;/a&gt;&lt;/p&gt;</description>
      <category>Swift</category>
      <category>CloudKit</category>
      <category>CoreData</category>
      <category>SWIFT</category>
      <author>문군_</author>
      <guid isPermaLink="true">https://moongoon72.tistory.com/29</guid>
      <comments>https://moongoon72.tistory.com/29#entry29comment</comments>
      <pubDate>Tue, 18 Feb 2025 17:04:00 +0900</pubDate>
    </item>
    <item>
      <title>최소 스패닝(신장) 트리 (Minimum Spanning Tree)</title>
      <link>https://moongoon72.tistory.com/28</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spanning Tree&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래프 내의 모든 정점을 포함하는 트리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  최소 신장 트리는 주어진 그래프의 모든 정점들을 연결하는 부분 그래프 중에서 그 &lt;b&gt;가중치의 합이 최소인 트리&lt;/b&gt;를 말한다.&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;사이클이 만들어지지 않아야 한다.
&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;/li&gt;
&lt;li&gt;전체 노드(n) -1 개의 간선으로 구성된다.&lt;/li&gt;
&lt;li&gt;하나의 그래프에 여러 개의 최소 신장 트리가 존재할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최소 신장 트리의 대표적인 알고리즘으로는 &lt;b&gt;크루스칼(Kruskal)&lt;/b&gt; 알고리즘과 &lt;b&gt;프림(Prim)&lt;/b&gt; 알고리즘이 존재한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;크루스칼 알고리즘 (Kruskal Algorithm)&lt;/h2&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;/li&gt;
&lt;li&gt;사이클이 없는 경우에만 트리에 추가&lt;/li&gt;
&lt;li&gt;총 v-1 (정점 개수 -1)개의 간선이 선택될 때까지 반복&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구성한 스패닝 트리에 사이클 발생 여부를 판단하기 위해, 분리 집합 (Disjoint Set)을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;b&gt;Union-Find&lt;/b&gt; 알고리즘 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 어떤 간선을 선택했을 때, 그 간선의 두 정점이 같은 최상위 부모를 가지면 사이클이 발생함을 알 수 있다.&lt;/p&gt;</description>
      <category>알고리즘</category>
      <author>문군_</author>
      <guid isPermaLink="true">https://moongoon72.tistory.com/28</guid>
      <comments>https://moongoon72.tistory.com/28#entry28comment</comments>
      <pubDate>Fri, 14 Feb 2025 15:09:10 +0900</pubDate>
    </item>
  </channel>
</rss>