<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>240-coding.log</title>
        <link>https://velog.io/</link>
        <description>나의 내일은 파래 🐳</description>
        <lastBuildDate>Sun, 28 Apr 2024 07:21:47 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>240-coding.log</title>
            <url>https://velog.velcdn.com/images/240-coding/profile/d58ac225-53ed-480d-a700-f9be2343ef49/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 240-coding.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/240-coding" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Combine 스터디 (5) - Error Handling]]></title>
            <link>https://velog.io/@240-coding/Combine-%EC%8A%A4%ED%84%B0%EB%94%94-5-Error-Handling</link>
            <guid>https://velog.io/@240-coding/Combine-%EC%8A%A4%ED%84%B0%EB%94%94-5-Error-Handling</guid>
            <pubDate>Sun, 28 Apr 2024 07:21:47 GMT</pubDate>
            <description><![CDATA[<h2 id="combine에서-에러-핸들링-특징">Combine에서 에러 핸들링 특징</h2>
<p>지금까지 봤던 다양한 Publisher들에서 알 수 있듯, Combine은 스트림에서 에러 타입을 꼭 정의해주어야 한다.</p>
<pre><code class="language-swift">struct AnyPublisher&lt;Output, Failure&gt; where Failure : Error</code></pre>
<p>Publisher에서 Operator, Subscriber로 흘러가는 동안 에러는 언제 어디서든 발생할 수 있다. 따라서 Combine을 효과적이고 안전하게 사용하기 위해서 에러를 적절하게 처리하는 것은 필수적이다! (사실 Combine뿐 아니라 모든 reactive programming에 다 해당되는 일,,)</p>
<p>Combine에서의 에러 핸들링 방법을 알아보자!✨</p>
<h1 id="never">Never</h1>
<p>본격적인 에러 핸들링 방법을 공부하기 전, <code>Never</code> 에 대해 잠깐 짚고 넘어가보자.</p>
<p>Failure 타입이 <code>Never</code> 인 Publisher는 절대 실패하지 않는다.</p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/107255b3-982e-4032-8da5-21de3f9d9e6b/image.png" alt=""></p>
<p>따라서 Publisher가 값을 소비하는 데에만 집중할 수 있다.</p>
<h1 id="절대-실패하지-않는-publisher를-위한-메소드">절대 실패하지 않는 Publisher를 위한 메소드</h1>
<p>Combine에서는 절대 실패하지 않을 때만 사용할 수 있는 메소드들이 있다.</p>
<h2 id="sinkreceivevalue">sink(receiveValue:)</h2>
<pre><code class="language-swift">func sink(receiveValue: @escaping ((Self.Output) -&gt; Void)) -&gt; AnyCancellable</code></pre>
<pre><code class="language-swift">let integers = (0...3)
integers.publisher
    .sink { print(&quot;Received \($0)&quot;) }

// Prints:
//  Received 0
//  Received 1
//  Received 2
//  Received 3</code></pre>
<p>이 메소드는 subscriber를 생성하고, <strong>subscriber를 리턴하기 전</strong>에 바로 값을 무한대로 요청한다.</p>
<p>리턴되는 값은 유지되어야 하고, 그렇지 않으면 스트림이 취소된다.</p>
<h2 id="assigntoon">assign(to:on:)</h2>
<pre><code class="language-swift">func assign&lt;Root&gt;(
    to keyPath: ReferenceWritableKeyPath&lt;Root, Self.Output&gt;,
    on object: Root
) -&gt; AnyCancellable</code></pre>
<p>publisher로부터 받은 값을 object의 프로퍼티에 할당해주는 메소드이다.</p>
<p><em>이전 스터디 주차에서 공부했었던 메소드라 자세한 설명은 생략!!</em></p>
<h2 id="setfailuretype">setFailureType</h2>
<pre><code class="language-swift">func setFailureType&lt;E&gt;(to failureType: E.Type) -&gt; Publishers.SetFailureType&lt;Self, E&gt; where E : Error</code></pre>
<p>실패하지 않는 publisher를 실패할 수 있는 publisher로 변환해야 하는 경우가 있을 것이다.</p>
<p><code>setFailureType</code> operator를 사용하면 upstream publisher의 failure type을 바꿀 수 있다.</p>
<p>대신 upstream publisher의 failure type은 <code>Never</code> 여야 한다!</p>
<p>(실패할 수 있는 upstream의 에러 타입을 바꾸고 싶다면 <code>mapError(_:)</code> 를 사용하자)</p>
<pre><code class="language-swift">let pub1 = [0, 1, 2, 3, 4, 5].publisher
let pub2 = CurrentValueSubject&lt;Int, Error&gt;(0)
let cancellable = pub1
    .setFailureType(to: Error.self)
    .combineLatest(pub2)
    .sink(
        receiveCompletion: { print (&quot;completed: \($0)&quot;) },
        receiveValue: { print (&quot;value: \($0)&quot;)}
     )

// Prints: &quot;value: (5, 0)&quot;.</code></pre>
<p><code>combineLatest</code> 는 두 publisher의 Failure 타입이 같아야 하기 때문에, 위 예시에서는 <code>setFailureType</code> 으로 에러 타입을 맞춰주었다.</p>
<p>또한 <code>setFailureType</code> 은 타입에만 영향을 미치기 때문에 실제 에러는 발생하지 않는다.</p>
<h2 id="assertnofailure">assertNoFailure</h2>
<pre><code class="language-swift">func assertNoFailure(
    _ prefix: String = &quot;&quot;,
    file: StaticString = #file,
    line: UInt = #line
) -&gt; Publishers.AssertNoFailure&lt;Self&gt;</code></pre>
<p>upstream publisher가 실패하면 fatal error를 일으키고, 아니면 받은 input 값을 모두 다시 publish하는 메소드</p>
<ul>
<li><p><code>prefix</code> : fatal error 메시지 앞에 적을 string</p>
</li>
<li><p><code>file</code> : 에러 메시지에서 사용할 파일명</p>
</li>
<li><p><code>line</code> : 에러 메시지에서 사용할 라인 넘버</p>
</li>
<li><p>테스트 중 내부 무결성을 확인하고 싶을 때 사용</p>
<p>  → publisher가 실패로 종료될 수 없음을 확인할 때 유용! </p>
</li>
<li><p>개발, 테스트뿐 아니라 배포 버전에서도 fatal error를 일으키니 주의!!</p>
</li>
</ul>
<pre><code class="language-swift">public enum SubjectError: Error {
    case genericSubjectError
}

let subject = CurrentValueSubject&lt;String, Error&gt;(&quot;initial value&quot;)
subject
    .assertNoFailure()
    .sink(receiveCompletion: { print (&quot;completion: \($0)&quot;) },
          receiveValue: { print (&quot;value: \($0).&quot;) }
    )

subject.send(&quot;second value&quot;)
subject.send(completion: Subscribers.Completion&lt;Error&gt;.failure(SubjectError.genericSubjectError))

// Prints:
//  value: initial value.
//  value: second value.
//  The process then terminates in the debugger as the assertNoFailure operator catches the genericSubjectError.</code></pre>
<p><code>subject</code> 가 세 번째로 <code>genericSubjectError</code> 라는 에러를 보냈고, fatal exception이 발생해서 프로세스가 중단됐다.</p>
<h1 id="try-친구들">try~ 친구들</h1>
<p>Combine 은 에러를 발생시킬 수 있는 operator 와 그렇지 않은 operator 사이에 구분을 제공한다.</p>
<p>이 operator들은 publisher의 <code>Failure</code> 를 Swift의 <code>Error</code> 타입으로 바꾼다.</p>
<ul>
<li>map / tryMap</li>
<li>scan / tryScan</li>
<li>filter / tryFilter</li>
<li>compactMap / tryCompactMap</li>
<li>removeDuplicates / tryRemoveDuplicates</li>
<li>reduce / tryReduce</li>
<li>drop / tryDrop</li>
<li>…</li>
</ul>
<p>그외 너무 많다.. 그때그때 try operator가 있는지 찾아보면서 개발하자</p>
<h1 id="mapping-errors">Mapping errors</h1>
<p><code>map</code> 은 에러를 던질  수 없는 non-throwing 메소드이다. 에러를 던지고 싶을 때는 <code>tryMap</code> 을 사용하자.</p>
<h2 id="trymap">tryMap</h2>
<pre><code class="language-swift">func tryMap&lt;T&gt;(_ transform: @escaping (Self.Output) throws -&gt; T) -&gt; Publishers.TryMap&lt;Self, T&gt;</code></pre>
<pre><code class="language-swift">struct ParseError: Error {}
func romanNumeral(from:Int) throws -&gt; String {
    let romanNumeralDict: [Int : String] =
        [1:&quot;I&quot;, 2:&quot;II&quot;, 3:&quot;III&quot;, 4:&quot;IV&quot;, 5:&quot;V&quot;]
    guard let numeral = romanNumeralDict[from] else {
        throw ParseError()
    }
    return numeral
}
let numbers = [5, 4, 3, 2, 1, 0]
cancellable = numbers.publisher
    .tryMap { try romanNumeral(from: $0) }
    .sink(
        receiveCompletion: { print (&quot;completion: \($0)&quot;) },
        receiveValue: { print (&quot;\($0)&quot;, terminator: &quot; &quot;) }
     )

// Prints: &quot;V IV III II I completion: failure(ParseError())&quot;</code></pre>
<h2 id="maperror">mapError</h2>
<pre><code class="language-swift">func mapError&lt;E&gt;(_ transform: @escaping (Self.Failure) -&gt; E) -&gt; Publishers.MapError&lt;Self, E&gt; where E : Error</code></pre>
<pre><code class="language-swift">struct DivisionByZeroError: Error {}
struct MyGenericError: Error { var wrappedError: Error }

func myDivide(_ dividend: Double, _ divisor: Double) throws -&gt; Double {
       guard divisor != 0 else { throw DivisionByZeroError() }
       return dividend / divisor
   }

let divisors: [Double] = [5, 4, 3, 2, 1, 0]
divisors.publisher
    .tryMap { try myDivide(1, $0) }
    .mapError { MyGenericError(wrappedError: $0) }
    .sink(
        receiveCompletion: { print (&quot;completion: \($0)&quot;) ,
        receiveValue: { print (&quot;value: \($0)&quot;, terminator: &quot; &quot;) }
     )

// Prints: &quot;0.2 0.25 0.3333333333333333 0.5 1.0 completion: failure(MyGenericError(wrappedError: DivisionByZeroError()))&quot;</code></pre>
<h1 id="catching-and-retrying">Catching and retrying</h1>
<h2 id="replaceerror">replaceError</h2>
<pre><code class="language-swift">func replaceError(with output: Self.Output) -&gt; Publishers.ReplaceError&lt;Self&gt;</code></pre>
<p>스트림에서 받은 에러를 특정한 값으로 대체(replace)하는 operator</p>
<pre><code class="language-swift">struct MyError: Error {}
let fail = Fail&lt;String, MyError&gt;(error: MyError())
cancellable = fail
    .replaceError(with: &quot;(replacement element)&quot;)
    .sink(
        receiveCompletion: { print (&quot;\($0)&quot;) },
        receiveValue: { print (&quot;\($0)&quot;, terminator: &quot; &quot;) }
    )

// Prints: &quot;(replacement element) finished&quot;.</code></pre>
<p>에러를 다시 감싸고 downstream subscriber로 받은 값을 다시 넘겨주고 싶을 때는 <code>catch(_:)</code> 사용</p>
<h2 id="catch">catch</h2>
<pre><code class="language-swift">func `catch`&lt;P&gt;(_ handler: @escaping (Self.Failure) -&gt; P) -&gt; Publishers.Catch&lt;Self, P&gt; where P : Publisher, Self.Output == P.Output</code></pre>
<p>upstream publisher로부터 받은 에러를 다른 publisher로 교체하는 메소드</p>
<h3 id="catch-vs-trycatch">catch vs tryCatch</h3>
<h3 id="catch-vs-replaceerror">catch vs replaceError</h3>
<p><code>Publishers.Catch</code>가 상위 Publisher가 에러를 낼 때 다른 Publisher로 교체하는 동작을 제공한다면, <code>Publishers.ReplaceError</code>는 상위 Publisher가 에러를 낼 때 특정 요소로 교체하는 동작을 제공한다.</p>
<h2 id="retry">retry</h2>
<pre><code class="language-swift">func retry(_ retries: Int) -&gt; Publishers.Retry&lt;Self&gt;</code></pre>
<p>upstream publisher를 이용해서 인자로 전달한 값까지 failed subscription을 다시 만든다.</p>
<ul>
<li><code>retries</code> : 재시도할 횟수</li>
<li>publisher 반환</li>
</ul>
<pre><code class="language-swift">struct WebSiteData: Codable {
    var rawHTML: String
}

let myURL = URL(string: &quot;https://www.example.com&quot;)

cancellable = URLSession.shared.dataTaskPublisher(for: myURL!)
    .retry(3)
    .map({ (page) -&gt; WebSiteData in
        return WebSiteData(rawHTML: String(decoding: page.data, as: UTF8.self))
    })
    .catch { error in
        return Just(WebSiteData(rawHTML: &quot;&lt;HTML&gt;Unable to load page - timed out.&lt;/HTML&gt;&quot;))
}
.sink(receiveCompletion: { print (&quot;completion: \($0)&quot;) },
      receiveValue: { print (&quot;value: \($0)&quot;) }
 )

// Prints: The HTML content from the remote URL upon a successful connection,
//         or returns &quot;&lt;HTML&gt;Unable to load page - timed out.&lt;/HTML&gt;&quot; if the number of retries exceeds the specified value.</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Combine 스터디 (4) - Combining Operators]]></title>
            <link>https://velog.io/@240-coding/Combine-%EC%8A%A4%ED%84%B0%EB%94%94-4-Combining-Operators</link>
            <guid>https://velog.io/@240-coding/Combine-%EC%8A%A4%ED%84%B0%EB%94%94-4-Combining-Operators</guid>
            <pubDate>Thu, 21 Mar 2024 11:18:31 GMT</pubDate>
            <description><![CDATA[<h1 id="prepending">Prepending</h1>
<blockquote>
<p><strong>prepend</strong>
add (something) to the beginning of something else.</p>
</blockquote>
<h2 id="prependoutput"><strong>prepend(Output)</strong></h2>
<pre><code class="language-swift">func prepend(_ elements: Self.Output...) -&gt; Publishers.Concatenate&lt;Publishers.Sequence&lt;[Self.Output], Self.Failure&gt;, Self&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/878e2d9c-11de-4a6c-af65-0011af1bea76/image.png" alt=""></p>
<p>publisher의 output 앞에 특정한 값을 붙이는 operator</p>
<ul>
<li>elements: publisher의 항목 전에 publish될 항목<ul>
<li>항목의 개수는 제한 없음!</li>
</ul>
</li>
</ul>
<pre><code class="language-swift">let dataElements = (0...10)
cancellable = dataElements.publisher
    .prepend(0, 1, 255)
    .sink { print(&quot;\($0)&quot;, terminator: &quot; &quot;) }

// Prints: &quot;0 1 255 0 1 2 3 4 5 6 7 8 9 10&quot;</code></pre>
<ul>
<li><code>0, 1, 255</code> 가 먼저 publish 되고 <code>dataElements</code> 의 항목들이 publish된다.</li>
</ul>
<pre><code class="language-swift">let publisher = [3, 4].publisher

publisher
  .prepend(1, 2) 
  .prepend(-1, 0)
  .sink(receiveValue: { print($0) })

/*
 -1
 0
 1
 2
 3
 4
*/</code></pre>
<p>마지막에 연결된 <code>prepend</code> 가 가장 앞에 온다.</p>
<p>→ <code>prepend(1, 2)</code> 를 실행해서 1, 2가 붙어서 새로 publish 되고, 그 다음 <code>prepend(-1, 0)</code> 가 실행되어서 그 앞에 또 -1, 0이 붙어서 다시 publish 되니까?!</p>
<h3 id="publishersconcatenate"><strong>Publishers.Concatenate</strong></h3>
<pre><code class="language-swift">struct Concatenate&lt;Prefix, Suffix&gt; where Prefix : Publisher, Suffix : Publisher, Prefix.Failure == Suffix.Failure, Prefix.Output == Suffix.Output</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/86fa232d-ba43-4366-bea5-f123adca2209/image.png" alt=""></p>
<p>한 publisher의 <strong>모든 값</strong>을 다른 publisher 전에 방출하는 publisher</p>
<ul>
<li>prefix: <code>suffix</code> 가 값을 다시 방출하기 전에 모든 값을 다시 publish</li>
<li>suffix: prefix가 종료된 후에 다시 값을 publish 하는 publisher</li>
</ul>
<h2 id="prependsequence">prepend(Sequence)</h2>
<p><img src="https://velog.velcdn.com/images/240-coding/post/fbbe90a2-2fc7-40ea-a21a-78ea6aeb5597/image.png" alt=""></p>
<p>파라미터로 sequence를 받을 수도 있다.</p>
<h3 id="잠깐">잠깐,,</h3>
<ul>
<li><p>Sequence가 뭐지?</p>
<p>  <img src="https://velog.velcdn.com/images/240-coding/post/d0c1559f-9b7c-4972-ab03-1a7cd7c1e124/image.png" alt=""></p>
</li>
</ul>
<pre><code>배열이나 Range 등을 publisher로 변환하면 `Publishers.Sequence` 타입이 된다.

Publishers.Sequence는 여러 항목을 연속으로 publish하는 publisher이다.

![](https://velog.velcdn.com/images/240-coding/post/996f9bf7-a80c-41d1-96ca-b1f51ec04b57/image.png)


이 Sequence는 생성할 때 `Elements` 를 파라미터로 받는데, 이 친구는 `Sequence` 라는 프로토콜을 준수하는 타입이어야 한다.

Swift의 Array, Set, Dictionary 등은 `Collection` 프로토콜을 준수하고 있는데, `Collection` 은 `Sequence` 를 준수하고 있다.


![](https://velog.velcdn.com/images/240-coding/post/025a8d4b-db63-4018-b69c-732a22d4c161/image.png)

![](https://velog.velcdn.com/images/240-coding/post/185ab81c-3ef7-4891-b7b6-9b986ce7540e/image.png)</code></pre><p>그래서 Array 같은 Collection 타입들을 publisher로 변환하면 <code>Publishers.Sequence</code> 라는 타입이 되었던 것!</p>
<pre><code class="language-swift">// 1
let publisher = [5, 6, 7].publisher

// 2
publisher
  .prepend([3, 4])
  .prepend(Set(1...2))
    .prepend(stride(from: 6, to: 11, by: 2))
  .sink(receiveValue: { print($0) })
  .store(in: &amp;subscriptions)

/*
 6
 8
 10
 1
 2
 3
 4
 5
 6
 7
*/</code></pre>
<ul>
<li><code>Set</code> 은 순서를 보장하지 않기 때문에 위처럼 <code>1, 2</code> 가 아닌 <code>2, 1</code> 처럼 출력될 수 있다.</li>
<li><code>stride</code> 처럼 다양한 Sequence 타입을 전달할 수 있다.</li>
</ul>
<h2 id="prependpublisher">prepend(Publisher)</h2>
<blockquote>
</blockquote>
<p>👀 combining operator라면서 ,, 단순한 값이 아니라 다른 publisher의 값도 붙일 수는 없나?</p>
<blockquote>
</blockquote>
<pre><code class="language-swift">func prepend&lt;P&gt;(_ publisher: P) -&gt; Publishers.Concatenate&lt;P, Self&gt; where P : Publisher, Self.Failure == P.Failure, Self.Output == P.Output</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/9891a65c-b89b-461c-9df1-563512f6d43e/image.png" alt=""></p>
<p>publisher의 output 앞에 매개변수로 받은 publisher가 방출한 항목을 붙이는 operator</p>
<ul>
<li>어떤 publisher의 항목을 다른 publisher의 항목 앞에 붙이고 싶을 때 사용한다.</li>
</ul>
<pre><code class="language-swift">let prefixValues = [0, 1, 255]
let dataElements = (0...10)
cancellable = dataElements.publisher
    .prepend(prefixValues.publisher)
    .sink { print(&quot;\($0)&quot;, terminator: &quot; &quot;) }

// Prints: &quot;0 1 255 0 1 2 3 4 5 6 7 8 9 10&quot;</code></pre>
<p> <code>dataLements</code> 가 값을 publish 하기 전에 <code>prefixValues</code> 가 값을 publish 한다.</p>
<pre><code class="language-swift">// 1
let publisher1 = [3, 4].publisher
let publisher2 = PassthroughSubject&lt;Int, Never&gt;()

// 2
publisher1
  .prepend(publisher2)
  .sink(receiveValue: { print($0) })
  .store(in: &amp;subscriptions)

// 3
publisher2.send(1)
publisher2.send(2)

/*
1
2
*/</code></pre>
<p>먼저 진행된 publisher가 반드시 완료된 후에 다음 publisher가 값을 방출할 수 있다.</p>
<pre><code class="language-swift">publisher2.send(completion: .finished)</code></pre>
<p><code>publisher2</code> 를 종료시키면 <code>publisher1</code> 도 값을 방출할 수 있다.</p>
<pre><code class="language-swift">1
2
3
4</code></pre>
<h1 id="appending">Appending</h1>
<p>prepend와 비슷하다. 대신 publisher의 뒤에 값을 추가해줌!</p>
<h2 id="appendoutput">append(Output…)</h2>
<pre><code class="language-swift">func append(_ elements: Self.Output...) -&gt; Publishers.Concatenate&lt;Self, Publishers.Sequence&lt;[Self.Output], Self.Failure&gt;&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/6a7b87aa-4460-4733-b987-b937539c50b4/image.png" alt=""></p>
<p>publisher의 output 뒤에 특정한 값들을 추가하는 operator</p>
<pre><code class="language-swift">let dataElements = (0...10)
cancellable = dataElements.publisher
    .append(0, 1, 255)
    .sink { print(&quot;\($0)&quot;, terminator: &quot; &quot;) }

// Prints: &quot;0 1 2 3 4 5 6 7 8 9 10 0 1 255&quot;</code></pre>
<p><code>dataElements</code> 의 모든 값들을 다시 publish 한 다음에 파라미터로 받은 값들을 publish 한다.</p>
<h3 id="prepend와-차이점">prepend와 차이점</h3>
<p>append와 prepend의 가장 큰 차이점은 <strong>원래 publisher가 <code>.finished</code> 로 종료된 후</strong>에 작동한다는 것이다.</p>
<p>append()의 리턴 타입이 <code>Publishers.Concatenate&lt;Self, Publishers.Sequence&lt;[Self.Output], Self.Failure&gt;&gt;</code> 었고, Concatenate의 suffix는 preffix가 종료된 후에 다시 publish 된다고 했기 때문</p>
<pre><code class="language-swift">// 1
let publisher = PassthroughSubject&lt;Int, Never&gt;()

publisher
    .append(3, 4)
    .append(5)
    .sink(receiveValue: { print($0) })

// 2
publisher.send(1)
publisher.send(2)

/*
1
2
*/</code></pre>
<p><code>publisher</code> 가 종료되지 않았기 때문에 <code>append()</code> 가 아무런 영향을 주지 않는다.</p>
<pre><code class="language-swift">publisher.send(completion: .finished)</code></pre>
<p>publisher를 종료시키면 원하는 결과를 얻을 수 있다.</p>
<pre><code class="language-swift">1
2
3
4
5</code></pre>
<p><code>Publishers.Concatenate&lt;Prefix, Suffix&gt;</code> 는 항상 앞에 애가 finish 되어야 뒤에 애의 값을 republish 할 수 있다는 걸 기억하자!!</p>
<h2 id="appendsequence">append(Sequence)</h2>
<p><img src="https://velog.velcdn.com/images/240-coding/post/551289ba-d169-448d-8fd3-539c49aed625/image.png" alt=""></p>
<p>Sequence를 파라미터로 받는 append operator</p>
<pre><code class="language-swift">// 1
let publisher  = [1, 2, 3].publisher

publisher
    .append([4, 5])    //  2
    .append(Set([6, 7]))    // 3
    .append(stride(from: 8, to: 11, by: 2))    // 4
    .sink(receiveValue: { print($0) })
    .store(in: &amp; subscriptions)

/*
1
2
3
4
5
7    // 순서가 바뀔 수 있음
6    // 순서가 바뀔 수 있음
8
10
*/</code></pre>
<p>prepend와 마찬가지로 다양한 종류의 Sequence를 append 할 수 있다.</p>
<h2 id="appendpublisher">append(Publisher)</h2>
<p><img src="https://velog.velcdn.com/images/240-coding/post/30737cd2-7558-49fc-96f4-1c4f2a102528/image.png" alt=""></p>
<p>Publisher를 파라미터로 받는 append operator</p>
<pre><code class="language-swift"> // 1
 let publisher1 = [1, 2].publisher
 let publisher2 = [3, 4].publisher

 // 2
 publisher1
     .append(publisher2)
     .sink(receiveValue: { print($0) })

/*
1
2
3
4
*/</code></pre>
<h1 id="고급-combining-operator">고급 Combining Operator</h1>
<h2 id="switchtolatest">switchToLatest()</h2>
<p><img src="https://velog.velcdn.com/images/240-coding/post/93b21641-4340-45b7-a8c6-5839ae677f41/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/29cc676e-a757-4d4e-9807-462dd60e3c11/image.png" alt=""></p>
<p>가장 최신의 publisher가 값을 방출하면 기존 publisher의 구독을 취소하고 최신 publisher로 구독을 전환(switching)하는 operator</p>
<pre><code class="language-swift">// 1
let publisher1 = PassthroughSubject&lt;Int, Never&gt;()
let publisher2 = PassthroughSubject&lt;Int, Never&gt;()
let publisher3 = PassthroughSubject&lt;Int, Never&gt;()

// 2
let publishers = PassthroughSubject&lt;PassthroughSubject&lt;Int, Never&gt;, Never&gt;()

// 3
publishers
    .switchToLatest()
    .sink(receiveCompletion: { _ in print(&quot;Completed!&quot;) },
        receiveValue: { print($0) })
    .store(in: &amp;subscriptions)

// 4
publishers.send(publisher1)
publisher1.send(1)
publisher1.send(2)

// 5
publishers.send(publisher2)
publisher1.send(3)
publisher2.send(4)
publisher2.send(5)

// 6
publishers.send(publisher3)
publisher2.send(6)
publisher3.send(7)
publisher3.send(8)
publisher3.send(9)

// 7
publisher3.send(completion: .finished)
publishers.send(completion: .finished)</code></pre>
<ol>
<li><p>3개의 <code>PassthroughSubjects</code> 를 만듦</p>
</li>
<li><p><code>PassthroughSubjects</code> 를 받는 <code>PassthroughSubjects</code> 를 만듦</p>
</li>
<li><p><code>publishers</code> 에 <code>switchToLatest()</code> 적용</p>
</li>
<li><p><code>publisher1</code> 을 <code>publishers</code> 로 보내고 1, 2 값을 보냄</p>
<p> → 현재 <code>publishers</code> 는 <code>publisher1</code> 구독!</p>
</li>
<li><p><code>publishers</code> 에 <code>publisher2</code> 를 보냄</p>
<p> → <code>publishers</code> 가 <code>publisher2</code> 를 구독해서 <code>publisher1</code> 의 값은 무시됨</p>
</li>
<li><p><code>publishers</code> 에 <code>publisher3</code> 을 보냄</p>
<p> → <code>publishers</code> 가 <code>publisher3</code> 을 구독해서 <code>publisher2</code> 의 값은 무시됨</p>
</li>
<li><p><code>publisher3</code> 과 <code>publishers</code> 을 종료해서 활성화된 모든 구독을 완료함</p>
</li>
</ol>
<pre><code class="language-swift"> 1
 2
 4
 5
 7
 8
 9
 completed!</code></pre>
<h3 id="publishersswitchtolatest"><strong>Publishers.SwitchToLatest</strong></h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/7e620d2c-26c9-4a57-9ab0-0dca496dc3a4/image.png" alt=""></p>
<pre><code class="language-swift">struct SwitchToLatest&lt;P, Upstream&gt; where P : Publisher, P == Upstream.Output, Upstream : Publisher, P.Failure == Upstream.Failure</code></pre>
<blockquote>
<p>🤔 위 예시에서 <code>publishers</code> 의 Output 타입은 <code>PassthroughSubject</code> 인데.. 왜 Int 값이 출력되지?</p>
</blockquote>
<p>⇒ switchToLatest()는 publisher들을 납작하게 (flatten) 만든 publisher를 리턴하기 때문이다!</p>
<ul>
<li><p>switchToLatest()를 적용할 publisher는 publisher를 방출해야 한다.</p>
</li>
<li><p>여러 publisher들이 값을 방출하지만, downstream subscriber들에게는 하나의 publisher로부터 연속적인 데이터 스트림을 구독하는 것처럼 보인다!</p>
</li>
<li><p>upstream publisher로부터 새 publisher를 받으면 이전 구독은 취소된다.</p>
</li>
<li><p>앞에 있는 publisher가 불필요한 작업을 하는 것을 막을 때 사용할 수 있다.</p>
<p>  예: 자주 업데이트 되는 UI publisher로부터 네트워크 요청을 하는 publisher를 만드는 경우</p>
<pre><code class="language-swift">   let url = URL(string: &quot;https://source.unsplash.com/random&quot;)!

   // 1
   func getImage() -&gt; AnyPublisher&lt;UIImage?, Never&gt; {
       return URLSession.shared
           .dataTaskPublisher(for: url)
           .map { data, _ in UIImage(data: data) }
           .print(&quot;image&quot;)
           .replaceError(with: nil)
           .eraseToAnyPublisher()
   }

   // 2
   let taps = PassthroughSubject&lt;Void, Never&gt;()

   taps
       .map { _ in getImage() } // 3
       .switchToLatest() // 4
       .sink(receiveValue: { _ in })
       .store(in: &amp;subscriptions)

   // 5
   taps.send()

   DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
       taps.send()
   }
   DispatchQueue.main.asyncAfter(deadline: .now() + 3.1) {
       taps.send()
   }</code></pre>
<ol>
<li><p>랜덤 이미지를 가져오는 API를 호출하는 메소드 <code>getImage()</code> 작성</p>
</li>
<li><p>사용자가 버튼을 터치하는 이벤트를 방출하는 <code>PassthroughSubject</code> 선언</p>
</li>
<li><p>버튼을 터치하면 <code>getImage()</code> 가 호출되고, switchToLatest()도 적용</p>
</li>
<li><p><code>taps</code> 에 이벤트를 3번 보냄</p>
<pre><code class="language-swift">image: receive subscription: (DataTaskPublisher)
image: request unlimited
image: receive value: (Optional(&lt;UIImage:0x600000364120 anonymous {1080, 720}&gt;))
image: receive finished
image: receive subscription: (DataTaskPublisher)
image: request unlimited
image: receive cancel
image: receive subscription: (DataTaskPublisher)
image: request unlimited
image: receive value: (Optional(&lt;UIImage:0x600000378d80 anonymous {1080, 1620}&gt;))
image: receive finished</code></pre>
<p>실제로는 2개의 이미지만 가져와진다. 두 번째 send 0.1초 뒤에 다음 send가 불려서 두 번째 구독은 취소되었기 때문!</p>
</li>
</ol>
</li>
</ul>
<h2 id="mergewith">merge(with:)</h2>
<pre><code class="language-swift">func merge(with other: Self) -&gt; Publishers.MergeMany&lt;Self&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/feaafff4-57ed-4cc3-a08e-043cf58c9a23/image.png" alt=""></p>
<p>같은 타입의 다른 publisher와 합쳐서 값들을 상호 교차해서 전달해주는 operator</p>
<ul>
<li><p>upstream publisher들이 이벤트를 방출하면 리턴되는 publisher가 이벤트를 방출한다.</p>
</li>
<li><p>최대 8개의 publisher들을 합칠 수 있다</p>
<p>  <img src="https://velog.velcdn.com/images/240-coding/post/32fa4bd0-49a0-4a13-9be7-0102caddab41/image.png" alt=""></p>
</li>
</ul>
<pre><code class="language-swift"> // 1
 let publisher1 = PassthroughSubject&lt;Int, Never&gt;()
 let publisher2 = PassthroughSubject&lt;Int, Never&gt;()

 // 2
 publisher1
     .merge(with: publisher2)
     .sink(receiveCompletion: { _ in print(&quot;Completed&quot;) },
               receiveValue: { print($0) })
     .store(in: &amp;subscriptions)

 // 3
 publisher1.send(1)
 publisher1.send(2)

 publisher2.send(3)

 publisher1.send(4)

 publisher2.send(5)

 // 4
 publisher1.send(completion: .finished)
 publisher2.send(completion: .finished)

/*
1
2
3
4
5
Completed
*/</code></pre>
<h2 id="combinelatest">combineLatest</h2>
<pre><code class="language-swift">func combineLatest&lt;P&gt;(_ other: P) -&gt; Publishers.CombineLatest&lt;Self, P&gt; where P : Publisher, Self.Failure == P.Failure</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/cb495644-4b0a-4977-9c9d-6eb70f60af39/image.png" alt=""></p>
<p>다른 publisher를 구독하고, publisher들로부터 <strong>마지막으로 받은 output의 튜플</strong>을 publish하는 operator</p>
<ul>
<li><p><strong>다른 타입</strong>의 publisher도 결합할 수 있다!</p>
<p>  단, Failure 타입은 같아야 함</p>
</li>
<li><p>각 upstream마다 버퍼 크기 1을 사용해서 가장 최근 값을 저장한다.</p>
</li>
<li><p>최대 4개의 publisher 결합 가능</p>
<p>  <img src="https://velog.velcdn.com/images/240-coding/post/8d11fe55-fb76-4e27-b44e-30f1b9c6c280/image.png" alt=""></p>
</li>
</ul>
<pre><code class="language-swift">let pub1 = PassthroughSubject&lt;Int, Never&gt;()
let pub2 = PassthroughSubject&lt;Int, Never&gt;()

cancellable = pub1
    .combineLatest(pub2)
    .sink { print(&quot;Result: \($0).&quot;) }

pub1.send(1)
pub1.send(2)
pub2.send(2)
pub1.send(3)
pub1.send(45)
pub2.send(22)

// Prints:
//    Result: (2, 2).    // pub1 latest = 2, pub2 latest = 2
//    Result: (3, 2).    // pub1 latest = 3, pub2 latest = 2
//    Result: (45, 2).   // pub1 latest = 45, pub2 latest = 2
//    Result: (45, 22).  // pub1 latest = 45, pub2 latest = 22</code></pre>
<p>모든 upstream publisher들이 종료되면 이 publisher도 종료된다.</p>
<p>upstream publisher가 값을 publish하지 않으면 이 publisher도 절대 종료되지 않는다.</p>
<p>→ combineLatest로 결합된 모든 publisher들은 최소 한 번 이상 값을 방출해야 한다.</p>
<h2 id="zip">zip</h2>
<pre><code class="language-swift">func zip&lt;P&gt;(_ other: P) -&gt; Publishers.Zip&lt;Self, P&gt; where P : Publisher, Self.Failure == P.Failure</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/1eb88946-bb85-4ae1-89aa-668fca45cdf9/image.png" alt=""></p>
<p>다른 publisher와 결합하고, 그 값들의 쌍을 튜플 형태로 전달하는 operator</p>
<ul>
<li><p>각 publisher들이 마지막으로 방출한 값을 튜플 형태로 downstream에 방출한다.</p>
</li>
<li><p>리턴되는 publisher는 <strong>모든 publisher가 이벤트를 방출할 때까지 기다린다</strong>. 그 후 가장 마지막으로 소비되지 않은 이벤트를 subscriber에게 넘겨준다.</p>
<p>  ⇒ 다시말해, 같은 인덱스 상에 있는 값들을 튜플 형태로 조합해서 방출한다. 지퍼를 잠그는 느낌,,</p>
</li>
</ul>
<pre><code class="language-swift"> let numbersPub = PassthroughSubject&lt;Int, Never&gt;()
 let lettersPub = PassthroughSubject&lt;String, Never&gt;()

 cancellable = numbersPub
     .zip(lettersPub)
     .sink { print(&quot;\($0)&quot;) }
 numbersPub.send(1)    // numbersPub: 1      lettersPub:        zip output: &lt;none&gt;
 numbersPub.send(2)    // numbersPub: 1,2    lettersPub:        zip output: &lt;none&gt;
 letters.send(&quot;A&quot;)     // numbers: 1,2       letters:&quot;A&quot;        zip output: &lt;none&gt;
 numbers.send(3)       // numbers: 1,2,3     letters:           zip output: (1,&quot;A&quot;)
 letters.send(&quot;B&quot;)     // numbers: 1,2,3     letters: &quot;B&quot;       zip output: (2,&quot;B&quot;)

 // Prints:
 //  (1, &quot;A&quot;)
 //  (2, &quot;B&quot;)</code></pre>
<p>upstream publisher 중 하나라도 정상적으로 종료되거나 실패하면 zip 된 publisher도 똑같이 종료된다.</p>
<h2 id="merge-vs-combinelatest-vs-zip">merge vs combineLatest vs zip</h2>
<ul>
<li>여러 Publisher로부터 가장 마지막 값 하나를 받고 싶을 때는 <code>merge</code></li>
<li>여러 publisher들이 마지막으로 방출한 값들을 묶어서 받고 싶을 때는 <code>combineLatest</code></li>
<li>여러 publisher들이 방출한 값을 쌍을 맞춰서 받고 싶을 때는 <code>zip</code></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Combine 스터디 (3) - Filtering Operators]]></title>
            <link>https://velog.io/@240-coding/Combine-%EC%8A%A4%ED%84%B0%EB%94%94-3-Filtering-Operators</link>
            <guid>https://velog.io/@240-coding/Combine-%EC%8A%A4%ED%84%B0%EB%94%94-3-Filtering-Operators</guid>
            <pubDate>Sun, 03 Mar 2024 13:23:48 GMT</pubDate>
            <description><![CDATA[<ul>
<li>upstream publisher가 방출한 값을 downstream과 다른 operator에게 보낼 수 있다</li>
</ul>
<h1 id="filtering-elements">Filtering Elements</h1>
<h2 id="filter_">filter(_:)</h2>
<pre><code class="language-swift">func filter(_ isIncluded: @escaping (Self.Output) -&gt; Bool) -&gt; Publishers.Filter&lt;Self&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/a6298fa2-1a0e-49e3-b19f-008a0b78b0ea/image.png" alt=""></p>
<p>클로저에 맞는 항목만 다시 publish 하는 operator</p>
<ul>
<li>isIncluded: 한 항목을 파라미터로 받고 항목을 다시 publish 할지에 대한 Boolean 값을 리턴하는 클로저</li>
</ul>
<pre><code class="language-swift">let numbers: [Int] = [1, 2, 3, 4, 5]
cancellable = numbers.publisher
    .filter { $0 % 2 == 0 }
    .sink { print(&quot;\($0)&quot;, terminator: &quot; &quot;) }

// Prints: &quot;2 4&quot;</code></pre>
<h2 id="tryfilter_"><strong>tryFilter(_:)</strong></h2>
<pre><code class="language-swift">func tryFilter(_ isIncluded: @escaping (Self.Output) throws -&gt; Bool) -&gt; Publishers.TryFilter&lt;Self&gt;</code></pre>
<p><code>isIncluded</code> 클로저가 항목을 다시 publish 할지 여부를 리턴하거나 에러를 던짐.</p>
<p>클로저가 에러를 던지면 publisher는 에러와 함께 실패한다.</p>
<pre><code class="language-swift">struct ZeroError: Error {}

let numbers: [Int] = [1, 2, 3, 4, 0, 5]
cancellable = numbers.publisher
    .tryFilter{
        if $0 == 0 {
            throw ZeroError()
        } else {
            return $0 % 2 == 0
        }
    }
    .sink(
        receiveCompletion: { print (&quot;\($0)&quot;) },
        receiveValue: { print (&quot;\($0)&quot;, terminator: &quot; &quot;) }
     )

// Prints: &quot;2 4 failure(DivisionByZeroError())&quot;.</code></pre>
<h2 id="removeduplicatesby"><strong>removeDuplicates(by:)</strong></h2>
<pre><code class="language-swift">func removeDuplicates(by predicate: @escaping (Self.Output, Self.Output) -&gt; Bool) -&gt; Publishers.RemoveDuplicates&lt;Self&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/e7bd9a3b-3f0f-40f3-af71-eebf5ecfcf77/image.png" alt=""></p>
<p>이전 항목과 다른 항목만 publish 하는 operator</p>
<ul>
<li><code>predicate</code> : 두 항목이 같은지 비교하는 클로저<ul>
<li>필터링 하기 위함</li>
<li>두 번째 항목이 첫 번째 항목과 같으면 <code>true</code> 를 반환함</li>
</ul>
</li>
<li>중복된 항목을 publish 하는 게 아니라 consume 하는 publisher를 반환한다.</li>
</ul>
<h3 id="언제-쓰면-좋을까">언제 쓰면 좋을까?</h3>
<p>사용자가 연속으로 같은 문자를 입력했을 때, 중복된 문자를 무시하고 싶을 때 유용할 듯</p>
<ul>
<li>upstream publisher에서 중복되는 항목을 지우고 싶을 때 사용<ul>
<li>직전에 publish된 항목과 현재 publish된 항목을 비교함</li>
</ul>
</li>
<li><code>Equatable</code> 을 구현하지 않은 타입들을 비교하거나 <code>Equatable</code> 을 구현한 것과 다르게 값을 비교하고 싶을 때 사용</li>
</ul>
<pre><code class="language-swift">struct Point {
    let x: Int
    let y: Int
}

let points = [Point(x: 0, y: 0), Point(x: 0, y: 1),
              Point(x: 1, y: 1), Point(x: 2, y: 1)]
cancellable = points.publisher
    .removeDuplicates { prev, current in
        // Considers points to be duplicate if the x coordinate
        // is equal, and ignores the y coordinate
        prev.x == current.x
    }
    .sink { print(&quot;\($0)&quot;, terminator: &quot; &quot;) }

// Prints: Point(x: 0, y: 0) Point(x: 1, y: 1) Point(x: 2, y: 1)</code></pre>
<h3 id="publishersremoveduplicates"><strong>Publishers.RemoveDuplicates</strong></h3>
<pre><code class="language-swift">struct RemoveDuplicates&lt;Upstream&gt; where Upstream : Publisher</code></pre>
<p><strong>이전 항목과 일치하지 않는 항목</strong>들만 publish하는 publisher</p>
<h2 id="tryremoveduplicatesby"><strong>tryRemoveDuplicates(by:)</strong></h2>
<pre><code class="language-swift">func tryRemoveDuplicates(by predicate: @escaping (Self.Output, Self.Output) throws -&gt; Bool) -&gt; Publishers.TryRemoveDuplicates&lt;Self&gt;</code></pre>
<ul>
<li><code>predicate</code> 클로저가 Boolean 값을 반환하거나 에러를 던짐.<ul>
<li>클로저가 에러를 던지면 publisher는 그 에러와 함께 종료함</li>
</ul>
</li>
</ul>
<pre><code class="language-swift">struct BadValuesError: Error {}
let numbers = [0, 0, 0, 0, 1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
cancellable = numbers.publisher
    .tryRemoveDuplicates { first, second -&gt; Bool in
        if (first == 4 &amp;&amp; second == 4) {
            throw BadValuesError()
        }
        return first == second
    }
    .sink(
        receiveCompletion: { print (&quot;\($0)&quot;) },
        receiveValue: { print (&quot;\($0)&quot;, terminator: &quot; &quot;) }
     )

 // Prints: &quot;0 1 2 3 4 failure(BadValuesError()&quot;</code></pre>
<h1 id="reducing-elements">Reducing Elements</h1>
<h2 id="ignoreoutput">ignoreOutput()</h2>
<pre><code class="language-swift">func ignoreOutput() -&gt; Publishers.IgnoreOutput&lt;Self&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/46b750af-397a-4ba8-aaf2-fd25080949b7/image.png" alt=""></p>
<p>모든 upstream 항목들을 무시하지만 upstream publisher의 completion 상태(finished or failed)는 전달해주는 operator</p>
<ul>
<li>모든 upstream 항목들을 무시하는 publisher를 반환함</li>
<li>publisher가 정상적으로 완료하거나 실패하는지 결정할 때 사용</li>
</ul>
<pre><code class="language-swift">struct NoZeroValuesAllowedError: Error {}
let numbers = [1, 2, 3, 4, 5, 0, 6, 7, 8, 9]
cancellable = numbers.publisher
    .tryFilter({ anInt in
        guard anInt != 0 else { throw NoZeroValuesAllowedError() }
        return anInt &lt; 20
    })
    .ignoreOutput()
    .sink(receiveCompletion: {print(&quot;completion: \($0)&quot;)},
          receiveValue: {print(&quot;value \($0)&quot;)})

// Prints: &quot;completion: failure(NoZeroValuesAllowedError())&quot;</code></pre>
<p>위 예시에서 <code>ignoreOutput()</code> operator는 1~5까지는 정상적으로 전달받지만 downstream으로 publish되지 않는다.</p>
<p><code>0</code> 이 publish 되었을 때는 <code>NoZeroValuesAllowedError</code> 와 함께 stream이 종료된다.</p>
<h3 id="publishersignoreoutputself">Publishers.IgnoreOutput&lt;Self&gt;</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/b9e69465-6f4c-4b7f-9fdc-409a481dc81e/image.png" alt=""></p>
<p>이 Publisher의 Output은 <code>Never</code> 이다.</p>
<p>절대 항목을 방출하지 않기 때문!</p>
<h1 id="selecting-specific-elements">Selecting Specific Elements</h1>
<h2 id="first"><strong>first()</strong></h2>
<pre><code class="language-swift">func first() -&gt; Publishers.First&lt;Self&gt;</code></pre>
<p>스트림의 첫 번째 항목을 publish하고 종료하는 operator</p>
<ul>
<li>downstream이 하나 이상의 항목을 요청하면 <code>first()</code> operator가 바로 upstream으로부터 <code>unlimited</code> 를 요청함</li>
<li><code>first()</code> 가 값을 받기 전에 upstream이 종료되면 어떤 값도 방출하지 않고 종료된다.</li>
</ul>
<pre><code class="language-swift">let numbers = (-10...10)
cancellable = numbers.publisher
    .first()
    .sink { print(&quot;\($0)&quot;) }

// Print: &quot;-10&quot;</code></pre>
<h2 id="firstwhere"><strong>first(where:)</strong></h2>
<pre><code class="language-swift">func first(where predicate: @escaping (Self.Output) -&gt; Bool) -&gt; Publishers.FirstWhere&lt;Self&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/57c1afba-5f8f-4854-96ec-0b8b1a79ecda/image.png" alt=""></p>
<p>클로저 안의 내용을 만족하는 스트림의 첫 번째 항목을 publish하고 종료하는 operator</p>
<ul>
<li><p><code>predicate</code> : 한 항목을 파라미터로 받고, 그 항목을 publish 할지 여부를 리턴하는 클로저</p>
</li>
<li><p>첫 번째 항목을 publish 한 후에는 다른 항목들은 모두 무시하고 정상 종료한다.</p>
<ul>
<li><p>“모두 무시”라는 표현 때문에 이후 값도 다 받긴 하지만 그냥 무시하는 건가 싶었는데 첫 번째 값을 찾으면 바로 종료되는 것 같다.</p>
<pre><code class="language-swift">——— Example of: first(where:) ———
numbers: receive subscription: (1...9)
numbers: request unlimited
numbers: receive value: (1)
numbers: receive value: (2)
numbers: receive cancel
2
Completed with: finished</code></pre>
</li>
</ul>
</li>
<li><p>아무 항목도 받지 않았다면 아무 것도 publish 하지 않고 종료한다.</p>
</li>
</ul>
<pre><code class="language-swift">let numbers = (-10...10)
cancellable = numbers.publisher
    .first { $0 &gt; 0 }
    .sink { print(&quot;\($0)&quot;) }

// Prints: &quot;1&quot;</code></pre>
<h2 id="tryfirstwhere">tryFirst(where:)</h2>
<pre><code class="language-swift">func tryFirst(where predicate: @escaping (Self.Output) throws -&gt; Bool) -&gt; Publishers.TryFirstWhere&lt;Self&gt;</code></pre>
<p><code>predicate</code> 클로저가 Boolean 값을 반환하거나 에러를 던질 수 있다. 클로저가 에러를 던지면 publisher는 실패한다.</p>
<pre><code class="language-swift">let numberRange: ClosedRange&lt;Int&gt; = (-1...50)
numberRange.publisher
    .tryFirst {
        guard $0 &lt; 99 else {throw RangeError()}
        return true
    }
    .sink(
        receiveCompletion: { print (&quot;completion: \($0)&quot;, terminator: &quot; &quot;) },
        receiveValue: { print (&quot;\($0)&quot;, terminator: &quot; &quot;) }
     )

// Prints: &quot;-1 completion: finished&quot;
// If instead the number range were ClosedRange&lt;Int&gt; = (100...200), the tryFirst operator would terminate publishing with a RangeError.</code></pre>
<h2 id="last">last()</h2>
<pre><code class="language-swift">func last() -&gt; Publishers.Last&lt;Self&gt;</code></pre>
<p>스트림이 종료된 후에 스트림의 마지막 항목을 publish 한다.</p>
<ul>
<li>upstream publisher의 마지막 항목을 방출하고 싶을 때 사용한다</li>
</ul>
<pre><code class="language-swift">let numbers = (-10...10)
cancellable = numbers.publisher
    .last()
    .sink { print(&quot;\($0)&quot;) }

// Prints: &quot;10&quot;</code></pre>
<h3 id="publisherslast">Publishers.Last</h3>
<pre><code class="language-swift">struct Last&lt;Upstream&gt; where Upstream : Publisher</code></pre>
<p>스트림이 종료될 때까지 기다렸다가 마지막 항목을 publish하는 publisher</p>
<p>→ 그래서 upstream publisher는 반드시 언젠가 종료되는 publisher여야 한다!</p>
<h2 id="lastwhere"><strong>last(where:)</strong></h2>
<pre><code class="language-swift">func last(where predicate: @escaping (Self.Output) -&gt; Bool) -&gt; Publishers.LastWhere&lt;Self&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/dd2aac29-d824-41c4-b301-32d6a5abc5f6/image.png" alt=""></p>
<p>upstream이 종료된 후에 클로저 내의 조건을 만족하는 스트림의 마지막 항목을 publish하는 operator</p>
<ul>
<li>predicate: 한 항목을 파라미터로 받고 publish 할지 여부를 반환하는 클로저</li>
</ul>
<pre><code class="language-swift">let numbers = (-10...10)
cancellable = numbers.publisher
    .last { $0 &lt; 6 }
    .sink { print(&quot;\($0)&quot;) }

// Prints: &quot;5&quot;</code></pre>
<h2 id="trylastwhere"><strong>tryLast(where:)</strong></h2>
<pre><code class="language-swift">func tryLast(where predicate: @escaping (Self.Output) throws -&gt; Bool) -&gt; Publishers.TryLastWhere&lt;Self&gt;</code></pre>
<ul>
<li><code>predicate</code> 클로저가 Boolean 값을 리턴하거나 에러를 던진다.</li>
<li>클로저가 에러를 던지면 publisher도 실패한다.</li>
</ul>
<pre><code class="language-swift">struct RangeError: Error {}

let numbers = [-62, 1, 6, 10, 9, 22, 41, -1, 5]
cancellable = numbers.publisher
    .tryLast {
        guard 0 != 0  else {throw RangeError()}
        return true
    }
    .sink(
        receiveCompletion: { print (&quot;completion: \($0)&quot;, terminator: &quot; &quot;) },
        receiveValue: { print (&quot;\($0)&quot;, terminator: &quot; &quot;) }
    )
// Prints: &quot;5 completion: finished&quot;
// If instead the numbers array had contained a `0`, the `tryLast` operator would terminate publishing with a RangeError.&quot;</code></pre>
<h1 id="applying-sequence-operations-to-elements">Applying Sequence Operations to Elements</h1>
<h2 id="dropuntiloutputfrom">drop(untilOutputFrom:)</h2>
<pre><code class="language-swift">func drop&lt;P&gt;(untilOutputFrom publisher: P) -&gt; Publishers.DropUntilOutput&lt;Self, P&gt; where P : Publisher, Self.Failure == P.Failure</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/c67a31ff-7672-4c19-a618-bd4e470e7f4e/image.png" alt=""></p>
<p>파라미터로 받은 publisher로부터 항목을 받기 전까지는 upstream publisher의 항목을 무시하는 operator</p>
<p>operator를 호출한 publisher를 A, 파라미터로 받은 publisher를 B라고 하면..</p>
<ul>
<li>B가 항목을 생성하기 전까지는 upstream publisher(A)의 항목을 drop 하는 publisher를 반환한다.</li>
<li>A는 B에게 하나의 값을 요청하고, B가 값을 생성하기 전까지는 upstream publisher(A)의 모든 항목들을 무시(drop)한다.</li>
<li>B가 항목을 생성하면 이 operator는 B의 구독을 취소하고 A의 이벤트가 지나가도록 허용한다.</li>
<li>operator가 A로부터 구독을 받으면 downstream에서 upstream publisher로 backpressure request(?)을 통과한다.<ul>
<li>Backpressure: subscriber가 항목을 받을 준비가 되어 있다는 신호를 보내서 흐름을 제어하는 것 <a href="https://developer.apple.com/documentation/combine/processing-published-elements-with-subscribers#Apply-Back-Pressure-with-a-Custom-Subscriber">(공식 문서)</a></li>
<li><a href="https://tanaschita.com/20211205-back-pressure-in-combine/">Backpressure in Combine</a><ul>
<li>subscriber가 받는 항목들을 제한하는 것</li>
<li>데이터를 consume하는 속도보다 publish하는 속도가 더 빠를 때 메모리 이슈 등이 발생할 수 있기 때문</li>
</ul>
</li>
</ul>
</li>
<li>B가 항목을 생성하기 전 upstream publisher가 이 요청에 대해 동작하면, operator는 upstream publisher로부터 받은 항목을 drop한다.</li>
</ul>
<blockquote>
<p>After this publisher receives a subscription from the upstream publisher, it passes through backpressure requests from downstream to the upstream publisher. If the upstream publisher acts on those requests before the other publisher produces an item, this publisher drops the elements it receives from the upstream publisher.</p>
</blockquote>
<pre><code class="language-swift">let upstream = PassthroughSubject&lt;Int,Never&gt;()
let second = PassthroughSubject&lt;String,Never&gt;()
cancellable = upstream
    .drop(untilOutputFrom: second)
    .sink { print(&quot;\($0)&quot;, terminator: &quot; &quot;) }

upstream.send(1)
upstream.send(2)
second.send(&quot;A&quot;)
upstream.send(3)
upstream.send(4)
// Prints &quot;3 4&quot;</code></pre>
<h3 id="publishersdropuntiloutput"><strong>Publishers.DropUntilOutput</strong></h3>
<pre><code class="language-swift">struct DropUntilOutput&lt;Upstream, Other&gt; where Upstream : Publisher, Other : Publisher, Upstream.Failure == Other.Failure</code></pre>
<p>두 번째 publisher의 항목을 받기 전까지는 upstream publisher의 항목을 무시하는 publisher</p>
<h2 id="dropfirst">dropFirst()</h2>
<pre><code class="language-swift">func dropFirst(_ count: Int = 1) -&gt; Publishers.Drop&lt;Self&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/8f480cc6-6a88-4929-acd7-150eac058ef8/image.png" alt=""></p>
<p>특정 개수의 항목들을 drop 한 후에 항목들을 publish 하는 operator</p>
<ul>
<li><code>count</code> 의 기본값은 1</li>
<li><code>count</code> 로 전달받은 개수만큼의 항목은 publish하지 않는 publisher를 리턴한다.</li>
</ul>
<pre><code class="language-swift">let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
cancellable = numbers.publisher
    .dropFirst(5)
    .sink { print(&quot;\($0)&quot;, terminator: &quot; &quot;) }

// Prints: &quot;6 7 8 9 10 &quot;</code></pre>
<h2 id="dropwhile"><strong>drop(while:)</strong></h2>
<pre><code class="language-swift">func drop(while predicate: @escaping (Self.Output) -&gt; Bool) -&gt; Publishers.DropWhile&lt;Self&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/4d98559a-71f1-4746-9e68-d02ec4b51ab9/image.png" alt=""></p>
<p>클로저가 false를 리턴할 때까지 upstream publisher의 값을 무시하고, 나머지 항목들을 publish하는 operator</p>
<ul>
<li><code>predicate</code> : publisher로부터 받은 항목을 drop 할지 여부를 반환하는 클로저</li>
</ul>
<pre><code class="language-swift">let numbers = [-62, -1, 0, 10, 0, 22, 41, -1, 5]
cancellable = numbers.publisher
    .drop { $0 &lt;= 0 }
    .sink { print(&quot;\($0)&quot;) }

// Prints: &quot;10 0, 22 41 -1 5&quot;</code></pre>
<p>한 번 클로저가 false를 반환하고 나면 그 이후 모든 값들을 publish한다.</p>
<h2 id="trydropwhile"><strong>tryDrop(while:)</strong></h2>
<pre><code class="language-swift">func tryDrop(while predicate: @escaping (Self.Output) throws -&gt; Bool) -&gt; Publishers.TryDropWhile&lt;Self&gt;</code></pre>
<ul>
<li><p>클로저가 Boolean 값을 리턴하거나 에러를 던진다.</p>
</li>
<li><p>클로저가 에러를 던지면 publisher는 에러와 함께 실패한다.</p>
<ul>
<li><p><strong>이때는 아무 값도 방출되지 않음!</strong></p>
<p>  <img src="https://velog.velcdn.com/images/240-coding/post/23351e2d-3cbe-4478-8964-0c63a686dc2b/image.png" alt=""></p>
</li>
</ul>
</li>
</ul>
<pre><code>- 에러를 던지기 전에 클로저가 `false` 를 반환하면 에러도 던지지 않고 무조건 publish 한다.

    → `filter` 는 조건에 만족하는 항목을 만난 후에도 계속 클로저의 조건을 확인하지만, `drop(while:)` 은 한 번 클로저의 조건을 만족하면 그 이후에는 조건을 다시 확인하지 않는다.

    ![](https://velog.velcdn.com/images/240-coding/post/846f0a7f-e278-4da7-bfce-6e39c34d7a69/image.png)</code></pre><pre><code class="language-swift">struct RangeError: Error {}
var numbers = [1, 2, 3, 4, 5, 6, -1, 7, 8, 9, 10]
let range: CountableClosedRange&lt;Int&gt; = (1...100)
cancellable = numbers.publisher
    .tryDrop {
        guard $0 != 0 else { throw RangeError() }
        return range.contains($0)
    }
    .sink(
        receiveCompletion: { print (&quot;completion: \($0)&quot;) },
        receiveValue: { print (&quot;value: \($0)&quot;) }
    )

// Prints: &quot;-1 7 8 9 10 completion: finished&quot;
// If instead numbers was [1, 2, 3, 4, 5, 6, 0, -1, 7, 8, 9, 10], tryDrop(while:) would fail with a RangeError.</code></pre>
<h2 id="prefix_"><strong>prefix(_:)</strong></h2>
<pre><code class="language-swift">func prefix(_ maxLength: Int) -&gt; Publishers.Output&lt;Self&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/41ed6851-4df9-483d-ae41-e6d3ccd67bca/image.png" alt=""></p>
<p>파라미터로 전달한 개수만큼의 항목을 publish한다.</p>
<p>⇒ upstream publisher가 방출할 수 있는 값의 개수를 제어</p>
<p>prefix라는 이름 그대로 먼저 항목을 publish 한 후에 완료한다. dropFirst와 반대되는 개념!</p>
<ul>
<li><code>maxLength</code> : publish할 항목의 최대 개수</li>
<li>downstream subscriber로 publish할 항목의 개수를 제한할 때 사용한다.</li>
<li>설정한 개수만큼 publish한 후에는 바로 종료된다.</li>
</ul>
<pre><code class="language-swift">let numbers = (0...10)
cancellable = numbers.publisher
    .prefix(2)
    .sink { print(&quot;\($0)&quot;, terminator: &quot; &quot;) }

// Prints: &quot;0 1&quot;</code></pre>
<h2 id="prefixwhile"><strong>prefix(while:)</strong></h2>
<pre><code class="language-swift">func prefix(while predicate: @escaping (Self.Output) -&gt; Bool) -&gt; Publishers.PrefixWhile&lt;Self&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/875464fa-09d2-4047-9048-014c78ad6ee2/image.png" alt=""></p>
<p>클로저의 조건을 만족하는 값을 방출할 때까지만 다시 publish하는 operator</p>
<ul>
<li><code>predicate</code> : 하나의 항목을 파라미터로 받고 계속 publish 해야 하는지 여부를 반환하는 클로저</li>
<li>upsteram publisher로부터 받은 항목이 조건을 만족하는 동안만 값을 방출할 때 사용한다.</li>
<li>클로저가 <code>false</code> 를 리턴하면 publisher는 종료된다.</li>
</ul>
<pre><code class="language-swift">let numbers = (0...10)
numbers.publisher
    .prefix { $0 &lt; 5 }
    .sink { print(&quot;\($0)&quot;, terminator: &quot; &quot;) }

// Prints: &quot;0 1 2 3 4&quot;</code></pre>
<h2 id="tryprefixwhile"><strong>tryPrefix(while:)</strong></h2>
<pre><code class="language-swift">func tryPrefix(while predicate: @escaping (Self.Output) throws -&gt; Bool) -&gt; Publishers.TryPrefixWhile&lt;Self&gt;</code></pre>
<ul>
<li>클로저가 Boolean 값을 반환하거나 에러를 던진다.<ul>
<li>클로저가 에러를 던지면 publisher는 에러와 함께 실패한다.</li>
</ul>
</li>
</ul>
<pre><code class="language-swift">struct OutOfRangeError: Error {}

let numbers = (0...10).reversed()
cancellable = numbers.publisher
    .tryPrefix {
        guard $0 != 0 else {throw OutOfRangeError()}
        return $0 &lt;= numbers.max()!
    }
    .sink(
        receiveCompletion: { print (&quot;completion: \($0)&quot;, terminator: &quot; &quot;) },
        receiveValue: { print (&quot;\($0)&quot;, terminator: &quot; &quot;) }
    )

// Prints: &quot;10 9 8 7 6 5 4 3 2 1 completion: failure(OutOfRangeError()) &quot;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Combine 스터디 (2) - Transforming Operators]]></title>
            <link>https://velog.io/@240-coding/Combine-%EC%8A%A4%ED%84%B0%EB%94%94-2-Transforming-Operators</link>
            <guid>https://velog.io/@240-coding/Combine-%EC%8A%A4%ED%84%B0%EB%94%94-2-Transforming-Operators</guid>
            <pubDate>Sun, 03 Mar 2024 13:17:26 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/240-coding/post/3fd479f9-b428-4005-836d-a5857103ffb5/image.png" alt=""></p>
<p>다른 값이나 시퀀스로 변형할 수 있는 operator</p>
<h1 id="들어가기-전에">들어가기 전에</h1>
<h2 id="operator는-publisher다">Operator는 Publisher다!</h2>
<blockquote>
<p>1주차 스터디에서 스터디원이 <code>sink() 라는 메소드는 왜 있는 건가요??</code> 라고 던진 질문에 <code>Publisher 안에 자체적으로 구현되어 있는 게 아닐까요?😅</code> 라고만 답했었는데...</p>
</blockquote>
<p>애초에 Publisher Operators 라는 메소드들이 공식적으로 정의가 되어 있었다!</p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/d3435ad1-4d17-49f8-8c55-17393e81f757/image.png" alt=""></p>
<p>받은 항목들을 가지고 downstream publisher나 subscriber를 만드는 메소드들을 Publisher Operators라고 한다.</p>
<p><a href="https://developer.apple.com/documentation/combine/publishers-collect-publisher-operators">Publisher Operators | Apple Developer Documentation</a></p>
<p>이 operator들은 publisher를 리턴한다.</p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/aa54b6c9-9d06-4e8b-81bb-8b3ee5d8c57c/image.png" alt=""></p>
<p>이런 식으로..</p>
<ul>
<li>upstream value를 받는다 → 데이터를 조작한다 → 이 값을 downstream으로 보낸다</li>
</ul>
<h1 id="collecting-values">Collecting Values</h1>
<h2 id="collect">collect()</h2>
<pre><code class="language-swift">func collect() -&gt; Publishers.Collect&lt;Self&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/cd699e27-0603-41e6-bc44-5b8cc90371a5/image.png" alt=""></p>
<ul>
<li><p>받은 항목들을 모아서 publisher가 종료될 때 하나의 배열로 방출함</p>
</li>
<li><p>publisher가 에러를 방출하면 receiver에게 그대로 에러를 전달함</p>
</li>
<li><p>publisher로부터 무한대로 항목을 받고, 받은 값들을 저장하기 위해 <strong>메모리를 무제한으로 사용</strong>함</p>
<p>  → 메모리 관리에 주의해야 함</p>
</li>
</ul>
<pre><code class="language-swift">let numbers = (0...10)
cancellable = numbers.publisher
    .collect()
    .sink { print(&quot;\($0)&quot;) }

// Prints: &quot;[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]&quot;</code></pre>
<p>특정 개수만큼 항목들을 묶어서 방출하고 싶으면 파라미터로 숫자를 전달한다.</p>
<pre><code class="language-swift">func collect(_ count: Int) -&gt; Publishers.CollectByCount&lt;Self&gt;</code></pre>
<pre><code class="language-swift">let numbers = (0...10)
cancellable = numbers.publisher
    .collect(5)
    .sink { print(&quot;\($0), terminator: &quot; &quot;&quot;) }

// Prints &quot;[0, 1, 2, 3, 4] [5, 6, 7, 8, 9] [10] &quot;</code></pre>
<p>→ 파라미터는 최댓값이기 때문에 파라미터로 받은 숫자보다 더 적은 개수의 항목이 묶일 수도 있음</p>
<h3 id="publisherscollect">Publishers.Collect</h3>
<pre><code class="language-swift">struct Collect&lt;Upstream&gt; where Upstream : Publisher</code></pre>
<h3 id="publisherscollectbycount">Publishers.CollectByCount</h3>
<pre><code class="language-swift">struct CollectByCount&lt;Upstream&gt; where Upstream : Publisher</code></pre>
<h1 id="mapping-values">Mapping Values</h1>
<h2 id="map_"><strong>map(_:)</strong></h2>
<pre><code class="language-swift">func map&lt;T&gt;(_ transform: @escaping (Self.Output) -&gt; T) -&gt; Publishers.Map&lt;Self, T&gt;</code></pre>
<p>publisher로부터 받은 값들을 변형하는 operator</p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/b4ae9cae-6f36-4742-b2c9-4e5569850452/image.png" alt=""></p>
<pre><code class="language-swift">let numbers = [5, 4, 3, 2, 1, 0]
let romanNumeralDict: [Int : String] =
   [1:&quot;I&quot;, 2:&quot;II&quot;, 3:&quot;III&quot;, 4:&quot;IV&quot;, 5:&quot;V&quot;]
cancellable = numbers.publisher
    .map { romanNumeralDict[$0] ?? &quot;(unknown)&quot; }
    .sink { print(&quot;\($0)&quot;, terminator: &quot; &quot;) }

// Prints: &quot;V IV III II I (unknown)&quot;</code></pre>
<h3 id="publishersmap"><strong>Publishers.Map</strong></h3>
<pre><code class="language-swift">struct Map&lt;Upstream, Output&gt; where Upstream : Publisher</code></pre>
<p>upstream publisher로부터 받은 항목들을 전달받은 클로저를 사용해서 변형하는 publisher</p>
<h2 id="trymap_"><strong>tryMap(_:)</strong></h2>
<pre><code class="language-swift">func tryMap&lt;T&gt;(_ transform: @escaping (Self.Output) throws -&gt; T) -&gt; Publishers.TryMap&lt;Self, T&gt;</code></pre>
<p>upstream publisher로부터 받은 항목들을 에러를 던지는 클로저를 이용해서 변형하는 operator</p>
<ul>
<li><code>transform</code> : 한 항목을 파라미터로 받고, 새 항목을 리턴하는 클로저. 클로저가 에러를 던지면 publisher는 에러와 함께 실패한다.</li>
</ul>
<pre><code class="language-swift">struct ParseError: Error {}
func romanNumeral(from:Int) throws -&gt; String {
    let romanNumeralDict: [Int : String] =
        [1:&quot;I&quot;, 2:&quot;II&quot;, 3:&quot;III&quot;, 4:&quot;IV&quot;, 5:&quot;V&quot;]
    guard let numeral = romanNumeralDict[from] else {
        throw ParseError()
    }
    return numeral
}
let numbers = [5, 4, 3, 2, 1, 0]
cancellable = numbers.publisher
    .tryMap { try romanNumeral(from: $0) }
    .sink(
        receiveCompletion: { print (&quot;completion: \($0)&quot;) },
        receiveValue: { print (&quot;\($0)&quot;, terminator: &quot; &quot;) }
     )

// Prints: &quot;V IV III II I completion: failure(ParseError())&quot;</code></pre>
<p>에러 처리가 필요하면 <code>tryMap(_:)</code> 을, 아니면 <code>map(_:)</code> 을 사용하자</p>
<h3 id="publisherstrymap"><strong>Publishers.TryMap</strong></h3>
<pre><code class="language-swift">struct TryMap&lt;Upstream, Output&gt; where Upstream : Publisher</code></pre>
<p>upstream publisher로부터 받은 항목들을 에러를 던지는 클로저를 이용해서 변형하는 pubilsher</p>
<h1 id="flattening-publishers">Flattening Publishers</h1>
<h2 id="flatmapmaxpublishers_"><strong>flatMap(maxPublishers:_:)</strong></h2>
<pre><code class="language-swift">func flatMap&lt;T, P&gt;(
    maxPublishers: Subscribers.Demand = .unlimited,
    _ transform: @escaping (Self.Output) -&gt; P
) -&gt; Publishers.FlatMap&lt;P, Self&gt; where T == P.Output, P : Publisher, Self.Failure == P.Failure</code></pre>
<blockquote>
<p><strong>Republishing Elements by Subscribing to New Publishers</strong>
새 publisher를 구독해서 항목을 새로 publish 하는 것</p>
</blockquote>
<p>publisher로부터 받은 항목들을 새 publisher로 변형하는 operator</p>
<ul>
<li><code>maxPublishers</code> : 동시에 존재하는 publisher subscription의 최댓값. 기본값은 unlimited</li>
<li><code>transform</code> : 항목을 파라미터로 받고, 같은 타입의 항목을 만드는 publisher를 반환하는 클로저</li>
</ul>
<p><img src="https://velog.velcdn.com/images/240-coding/post/b856e06e-2b6a-423e-95de-2bb6b14aa118/image.png" alt=""></p>
<p>maxPublisher를 2로 설정했을 때 예시</p>
<ul>
<li><p>upstream publisher로부터 받은 값을 가지고 새 이벤트를 만들고 싶을 때 사용</p>
</li>
<li><p>클로저는 받은 값을 가지고 새 Publisher를 만든다</p>
<ul>
<li>이 Publisher는 하나 이상의 이벤트를 방출할 수 있다</li>
<li>이 Publisher가 정상적으로 종료되어도 전체 스트림이 완료되지 않는다</li>
<li>하지만 실패할 때는 전체 스트림도 같이 실패한다!</li>
</ul>
</li>
<li><p>flatMap은 각 output을 하나의 publisher로 변형하는데, 이 publisher들을 downstream으로 방출되는 publisher를 업데이트하기 위해 이 publisher들을 버퍼에 가지고 있는다.</p>
<p>  → 메모리 관리에 주의해야 한다!</p>
</li>
</ul>
<pre><code class="language-swift">public struct WeatherStation {
    public let stationID: String
}

var weatherPublisher = PassthroughSubject&lt;WeatherStation, URLError&gt;()

cancellable = weatherPublisher.flatMap { station -&gt; URLSession.DataTaskPublisher in
    let url = URL(string:&quot;https://weatherapi.example.com/stations/\(station.stationID)/observations/latest&quot;)!
    return URLSession.shared.dataTaskPublisher(for: url)
}
.sink(
    receiveCompletion: { completion in
        // Handle publisher completion (normal or error).
    },
    receiveValue: {
        // Process the received data.
    }
 )

weatherPublisher.send(WeatherStation(stationID: &quot;KSFO&quot;)) // San Francisco, CA
weatherPublisher.send(WeatherStation(stationID: &quot;EGLC&quot;)) // London, UK
weatherPublisher.send(WeatherStation(stationID: &quot;ZBBB&quot;)) // Beijing, CN </code></pre>
<h3 id="publishersflatmap"><strong>Publishers.FlatMap</strong></h3>
<pre><code class="language-swift">struct FlatMap&lt;NewPublisher, Upstream&gt; where NewPublisher : Publisher, Upstream : Publisher, NewPublisher.Failure == Upstream.Failure</code></pre>
<p>upstream publisher로부터 받은 항목을 새 publisher로 변형하는 publisher</p>
<h3 id="🤯-map-flatmap">🤯 map? flatMap?</h3>
<pre><code class="language-swift">[1, 2, 3].publisher.flatMap({ int in
  return (0..&lt;int).publisher
  }).sink(receiveCompletion: { _ in }, receiveValue: { value in
    print(&quot;value: \(value)&quot;)
  })

/* Result:
value: 0
value: 0
value: 1
value: 0
value: 1
value: 2
*/</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/ded4218e-9c03-40ec-beaa-de2da79e6369/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/0c736700-e78b-40d7-ab22-f7df7d561d4d/image.png" alt=""></p>
<pre><code class="language-swift">let tmp1 = [1, 2, 3].publisher.flatMap({ int in
  return (0..&lt;int).publisher
  })

let tmp2 = [1, 2, 3].publisher.map { int in
    return (0..&lt;int).publisher
}

/*
value: Sequence&lt;Range&lt;Int&gt;, Never&gt;(sequence: Range(0..&lt;1))
value: Sequence&lt;Range&lt;Int&gt;, Never&gt;(sequence: Range(0..&lt;2))
value: Sequence&lt;Range&lt;Int&gt;, Never&gt;(sequence: Range(0..&lt;3))
*/</code></pre>
<ul>
<li><strong>Publishers.Map</strong>은 변형된 upstream publisher의 output을 output으로 가지는 publisher</li>
<li><strong>Publishers.FlatMap</strong>은 upstream publisher의 output을 요리조리 변형해서 새 publisher를 만드는 publisher<ul>
<li>그리고 애초에 flatMap은 publisher를 리턴한다.</li>
</ul>
</li>
</ul>
<p>그래서 map을 사용해서 publisher로 변형하면 output이 publisher인 publiser가 만들어지는 것!! (<code>Publisher&lt;NewPublisher, Error&gt;</code> 같은 느낌..)</p>
<p>하지만 flatMap을 사용하면 각 항목들을 변형한 하나의 publisher가 예쁘게 만들어진다.</p>
<h1 id="upstream-output-바꾸기">Upstream Output 바꾸기</h1>
<h2 id="replacenilwith"><strong>replaceNil(with:)</strong></h2>
<pre><code class="language-swift">func replaceNil&lt;T&gt;(with output: T) -&gt; Publishers.Map&lt;Self, T&gt; where Self.Output == T?</code></pre>
<p>스트림에 있는 nil 항목을 특정 항목으로 교체하는 operator</p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/00b2f6c3-25da-4290-8adc-84431a1c5b14/image.png" alt=""></p>
<pre><code class="language-swift">let numbers: [Double?] = [1.0, 2.0, nil, 3.0]
numbers.publisher
    .replaceNil(with: 0.0)
    .sink { print(&quot;\($0)&quot;, terminator: &quot; &quot;) }

// Prints: &quot;Optional(1.0) Optional(2.0) Optional(0.0) Optional(3.0)&quot;</code></pre>
<h2 id="replaceemptywith">replaceEmpty(with:)</h2>
<pre><code class="language-swift">func replaceEmpty(with output: Self.Output) -&gt; Publishers.ReplaceEmpty&lt;Self&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/e7c545e9-1640-4d24-bdbc-1e9c396d550d/image.png" alt=""></p>
<p>빈 스트림을 전달된 항목으로 바꾸는 operator</p>
<ul>
<li><code>output</code> : upstream publisher가 아무 값도 방출되지 않고 종료되면 방출되는 항목</li>
</ul>
<pre><code class="language-swift">let numbers: [Double] = []
cancellable = numbers.publisher
    .replaceEmpty(with: Double.nan)
    .sink { print(&quot;\($0)&quot;, terminator: &quot; &quot;) }

// Prints &quot;(nan)&quot;.</code></pre>
<p><code>Double.nan</code> 을 방출하고 정상 종료됨</p>
<pre><code class="language-swift">let otherNumbers: [Double] = [1.0, 2.0, 3.0]
cancellable2 = otherNumbers.publisher
    .replaceEmpty(with: Double.nan)
    .sink { print(&quot;\($0)&quot;, terminator: &quot; &quot;) }

// Prints: 1.0 2.0 3.0</code></pre>
<p>방출되는 값이 있으면 replaceEmpty 로 전달한 값은 방출되지 않는다.</p>
<h3 id="publishersreplaceempty"><strong>Publishers.ReplaceEmpty</strong></h3>
<pre><code class="language-swift">struct ReplaceEmpty&lt;Upstream&gt; where Upstream : Publisher</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/eb89cba4-87c7-49d3-9705-5b3069d5868d/image.png" alt=""></p>
<h1 id="incrementally-transforming-output"><strong>Incrementally transforming output</strong></h1>
<h2 id="scan">scan(<em>:</em>:)</h2>
<pre><code class="language-swift">func scan&lt;T&gt;(
    _ initialResult: T,
    _ nextPartialResult: @escaping (T, Self.Output) -&gt; T
) -&gt; Publishers.Scan&lt;Self, T&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/e7089bd5-f3a7-4555-98ec-8250e4b11006/image.png" alt=""></p>
<p>클로저한테 마지막으로 전달받은 값과 현재 항목을 가 직고 sptream publisher로부터 받은 항목들을 변형하는 operator</p>
<ul>
<li><code>initialResult</code> : <code>nextPartialResult</code> 로부터 리턴받은 이전 결과</li>
<li><code>nextPartialResult</code> : 클로저로부터 리턴받은 이전 값과 upstream publisher로부터 방출받을 다음 항목을 인자로 받는 클로저</li>
</ul>
<blockquote>
<p>이전에 publish된 값들을 모아서 하나의 값으로 만들고 싶을 때 사용한다.
→ <code>reduce()</code> 랑 비슷한 느낌?</p>
</blockquote>
<pre><code class="language-swift">let range = (0...5)
cancellable = range.publisher
    .scan(0) { return $0 + $1 }
    .sink { print (&quot;\($0)&quot;, terminator: &quot; &quot;) }
 // Prints: &quot;0 1 3 6 10 15 &quot;.</code></pre>
<h3 id="publishersscan"><strong>Publishers.Scan</strong></h3>
<pre><code class="language-swift">struct Scan&lt;Upstream, Output&gt; where Upstream : Publisher</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/177dba10-6767-4e00-b153-6b984b484fb1/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Combine 스터디 (1) - Publisher, Subscriber]]></title>
            <link>https://velog.io/@240-coding/Combine-%EC%8A%A4%ED%84%B0%EB%94%94-1-Publisher-Subscriber</link>
            <guid>https://velog.io/@240-coding/Combine-%EC%8A%A4%ED%84%B0%EB%94%94-1-Publisher-Subscriber</guid>
            <pubDate>Sun, 03 Mar 2024 13:09:50 GMT</pubDate>
            <description><![CDATA[<p>저번 달부터 회사 동기들이랑 컴바인 스터디를 하고 있다.
나도 정리도 나름 열심히 해가고 도움도 많이 되고 있어서 블로그에도 기록용으로 남겨본다✌🏻</p>
<h1 id="combine">Combine?</h1>
<ul>
<li><p>비동기 이벤트를 처리하는 애플의 프레임워크.</p>
</li>
<li><p>시간이 지남에 따라 값을 처리하기 위한 선언형 API들을 제공한다.</p>
</li>
<li><p>Publisher: 시간에 따라 변할 수 있는 값을 노출</p>
</li>
<li><p>Subscriber: Publisher로부터 값을 받고 그에 대한 액션을 취함</p>
</li>
<li><p>여러 publisher들의 값들을 결합하고, 상호작용을 조절할 수 있음</p>
</li>
</ul>
<h3 id="combine-사용의-장점">Combine 사용의 장점</h3>
<ul>
<li>이벤트를 처리하는 코드를 중앙화함으로써 가독성, 유지보수</li>
<li>중첩 클로저, convention-based callbacks 등보다 사용하기 편함<ul>
<li>‘협약에-기초한 콜백 (convention-based callbacks)’ 은 이벤트를 ‘콜백 (callback) 함수’ 로 처리하려면 양쪽 사이에 서로 ‘협의해서 약속한 (convention)’ 정보가 있어야 함을 의미한다..고 한다.</li>
</ul>
</li>
</ul>
<h1 id="publisher">Publisher</h1>
<pre><code class="language-swift">protocol Publisher&lt;Output, Failure&gt;</code></pre>
<ul>
<li><p>하나 이상의 Subscriber에게 값을 전달한다.</p>
</li>
<li><p>Subscriber의 Input, Failure 타입과 Publisher의 Output, Failure 타입이 일치해야 함</p>
</li>
<li><p><code>receive(_:)</code> 메소드를 호출한 다음에 subscriber의 <code>receive</code> 메소드를 호출할 수 있음.</p>
<ul>
<li><p>얘네를 사용해서 subscriber와 커뮤니케이션(?) 할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/422278ed-7ff9-48dc-93de-cad838fbc360/image.png" alt=""></p>
</li>
</ul>
</li>
</ul>
<pre><code>- `receive` 메소드 종류는 3가지!

    → 위의 세 메소드들은 Subscriber 프로토콜 안에 있는 메소드들임. </code></pre><p><img src="https://velog.velcdn.com/images/240-coding/post/d73a2291-713e-4553-93ec-32688824bc2a/image.png" alt=""></p>
<ul>
<li>시간에 따라 값을 방출한다.</li>
<li>에러 상황을 감지하기 위해 에러도 함께 방출한다.</li>
<li>내가 방출할 값과 에러(만약 있으면)의 종류를 정의해야 한다.</li>
</ul>
<p>Publisher는 프로토콜이기 때문에 원래는 직접 구현해야 한다. 하지만 Combine에서는 다양한 Publisher 타입(?)을 제공한다.</p>
<ul>
<li><p>Subject의 구체적인 하위클래스 사용하기 (ex: PassthroughSubject)</p>
<p>  <code>send()</code> 메소드 호출해서 실시간으로 값을 publish 할 수 있다</p>
</li>
<li><p>CurrentValueSubject</p>
<ul>
<li>subject의 기본값을 업데이트 할 때마다 값을 publish 할 때 사용</li>
</ul>
</li>
<li><p>프로퍼티에 <code>@Published</code> 어노테이션 붙이기</p>
<ul>
<li>프로퍼티의 값이 바뀔 때마다 이벤트를 방출하는 퍼블리셔를 가지게 됨</li>
</ul>
</li>
</ul>
<h3 id="publishers">Publishers..????</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/6ec2d2f3-333c-4a47-8b5b-8c00aa86b6cb/image.png" alt=""></p>
<p>*<em>Publisher..? Publishers..?? 넌 뭐냐?? 🤯
*</em></p>
<ul>
<li>Publisher 역할을 하는 타입을 모아둔 enum</li>
<li>Publisher의 extension으로 정의된 Operator들은 <code>Publishers</code> 를 상속(??)하는 클래스나 구조체로 기능을 구현한다.<ul>
<li>예: contains(_:) Operator는 <code>Publishers.Contains</code> 인스턴스를 반환한다.</li>
</ul>
</li>
</ul>
<h2 id="publisher의-종류">Publisher의 종류</h2>
<p><img src="https://velog.velcdn.com/images/240-coding/post/46ee70af-c9f3-4b44-a9c2-840ebf3e146a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/852d80b2-45f3-42f1-a076-ecef2d05f1b2/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/718de9cf-f1a9-4f02-a0e1-2b41ff0822df/image.png" alt=""></p>
<ul>
<li>Just<ul>
<li>가장 기본적인 Publisher</li>
<li>하나의 값만 방출하고, 절대 실패하지 않는다</li>
</ul>
</li>
</ul>
<pre><code class="language-swift">[&quot;Pepperoni&quot;, &quot;Mushrooms&quot;, &quot;Onions&quot;, &quot;Sausage&quot;, &quot;Bacon&quot;, &quot;Extra cheese&quot;, &quot;Black olives&quot;, &quot;Green peppers&quot;].publisher</code></pre>
<p>나는 값을 여러 번 방출하고 싶어!! → publisher로 손쉽게 변환할 수 있다.</p>
<p>여러 개의 값을 방출하고 종료함.</p>
<p><code>.publisher</code> 라는 키워드를 붙여서 publisher를 만들 수 있다</p>
<h3 id="예시---notificationcenter-사용">예시 - NotificationCenter 사용</h3>
<pre><code class="language-swift">// create the order
let pizzaOrder = Order()
let pizzaOrderPublisher = NotificationCenter
  .default
  .publisher(for: .didUpdateOrderStatus, object: pizzaOrder)</code></pre>
<ul>
<li>pizzaOrderPublisher: NotificationCenter가 pizzaOrder에 didUpdateOrderStatus 이라는 노티피케이션을 보낼 때마다 이벤트를 방출하는 Publisher</li>
</ul>
<p><img src="https://velog.velcdn.com/images/240-coding/post/5f9efa3a-f577-4d2e-8d15-625309b14c29/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/876bcbef-f8cc-4a23-b599-8a23b98b75d6/image.png" alt=""></p>
<pre><code class="language-swift">// once the user is ready to place the order
NotificationCenter
  .default
  .post(name: .didUpdateOrderStatus,
        object: pizzaOrderPublisher,
        userInfo: [&quot;status&quot;: OrderStatus.processing])</code></pre>
<ul>
<li>NotificationCenter에 데이터를 보내고 post → publisher가 값을 방출함</li>
<li>근데 아직 이 publisher를 구독한 subscriber가 없어서 아무 일도 일어나지 않는다.</li>
</ul>
<blockquote>
<p><strong>❓ 궁금한 점..</strong>
notification center를 반드시 사용해야 하는가?
어떤 경우에서 사용하는 건가? 라고 생각했었는데
애플 공식문서에 있던 예시는 macOS를 개발하는 AppKit에서 사용하는 방법을 알려주는 예시였다.
나중에 <a href="https://medium.com/hcleedev/swift-notificationcenter%EC%99%80-%EC%82%AC%EC%9A%A9%EB%B2%95-6eb4490aac88">Swift: NotificationCenter와 사용법</a> 를 읽어봐도 좋을 듯!</p>
</blockquote>
<h1 id="subscriber">Subscriber</h1>
<pre><code class="language-swift">protocol Subscriber&lt;Input, Failure&gt; : CustomCombineIdentifierConvertible</code></pre>
<ul>
<li>Publisher로부터 요소의 스트림을 받는다.<ul>
<li>라이프사이클 이벤트에 따라..</li>
</ul>
</li>
<li>Publisher와 타입이 일치해야 한다는 거 유의</li>
<li>Publisher의 <code>subscribe(_:)</code> 메소드 호출해서 subscriber와 publisher를 연결할 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/240-coding/post/f1a3210e-20b0-4b4d-8cce-c2d1d567e1d7/image.png" alt=""></p>
<ul>
<li>publisher를 구독하고 얘네가 방출한 값을 받음</li>
<li>각 subscriber는 어떤 타입의 값과 에러를 받을 건지 정의해야 한다.</li>
</ul>
<h2 id="publisher와-subsciber-연결-과정">Publisher와 Subsciber 연결 과정</h2>
<ol>
<li><p>Publisher의 <code>subscribe(_:)</code> 메소드 호출</p>
<pre><code class="language-swift"> func subscribe&lt;S&gt;(_ subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input</code></pre>
<p> Apple 왈,, <code>receive(subscriber:)</code> 대신 얘를 호출하라고 함. 이 함수 안에서 <code>receive(subscriber:)</code> 를 호출할 거임.</p>
</li>
<li><p>그러면 publisher가 subscriber의 <code>receive(subscription:)</code> 메소드를 호출하게 됨</p>
</li>
<li><p>subscriber에게 <code>Subscription</code> 인스턴스를 전달해줌</p>
<ul>
<li>Publisher에게 요소를 달라고 요구하는 역할</li>
<li>아니면 구독을 취소할 수도 있음</li>
</ul>
</li>
<li><p>subscriber가 처음으로 Demand를 생성함</p>
</li>
<li><p>publisher가 subscriber의 <code>receive(_:)</code> 메소드를 호출해서 새로 publish된 요소를 전달함.</p>
</li>
<li><p>publisher가 publish를 중단하면 <code>receive(completion:)</code> 메소드를 호출함</p>
<ul>
<li><code>Subscriber.Completion</code> 타입의 파라미터 사용</li>
<li>퍼블리싱이 정상적으로 끝났는지 아니면 에러를 뱉었는지 나타내기 위함<h2 id="❓-receive-메소드를-정리해보자"><strong>❓ receive 메소드를 정리해보자</strong></h2>
</li>
</ul>
</li>
</ol>
<blockquote>
<p>🤯 아니..  receive 메소드가 publisher꺼야 subcriber꺼야;; 겁나 헷갈리네</p>
</blockquote>
<h3 id="💫--publisher">💫  Publisher</h3>
<p><strong>1️⃣ receive(subscriber:)</strong></p>
<pre><code class="language-swift">func receive&lt;S&gt;(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input</code></pre>
<p>publisher에게 subscriber를 붙이는 메소드</p>
<p>직접 이 메소드를 호출하지 않고 <code>subscribe(_:)</code> 를 호출하면 얘를 호출해준다.</p>
<h3 id="💫-subscriber">💫 Subscriber</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/e82d8a6c-b6c7-4ad7-8dde-9f88a52db581/image.png" alt=""></p>
<p>값을 받는 것 / 생애주기 이벤트를 받는 것으로 나뉨</p>
<p><strong>1️⃣ receive(_:)</strong></p>
<pre><code class="language-swift">func receive(_ input: Self.Input) -&gt; Subscribers.Demand</code></pre>
<p>subscriber에게 publisher가 항목을 만들어냈음을 알려주는 메소드</p>
<ul>
<li><code>input</code> : publish된 요소</li>
<li><code>Subscribers.Demand</code> : subscriber가 얼마나 많은 요소들을 더 받을 것으로 예상하는지를 나타내는 인스턴스</li>
</ul>
<p><strong>2️⃣ receive()</strong></p>
<pre><code class="language-swift">func receive() -&gt; Subscribers.Demand</code></pre>
<p>void 요소의 publisher가 다음 요청을 받을 준비가 되었음을 알려주는 메소드</p>
<p>이벤트가 발생했다는 것만 알려주고 싶을 때 <code>Void</code> input / output을 사용</p>
<p><strong>3️⃣ receive(subscription:)</strong></p>
<pre><code class="language-swift">func receive(subscription: any Subscription)</code></pre>
<p>subscriber에게 publisher를 성공적으로 구독했고, 항목을 요청할 수 있음을 알려주는 메소드</p>
<ul>
<li><p><strong>subscription?</strong></p>
<p>  publisher와 subscriber 사이의 연결을 나타냄. publisher에게 항목을 요청할 때 매개변수로 받은 subscription을 사용함.</p>
</li>
</ul>
<p><strong>4️⃣ receive(completion:)</strong></p>
<pre><code class="language-swift">func receive(completion: Subscribers.Completion&lt;Self.Failure&gt;)</code></pre>
<p>subscriber에게 publisher의 퍼블리싱이 끝났음을 알려주는 메소드</p>
<ul>
<li><code>Subscriber.Completion</code> : 퍼블리싱이 정상적으로 끝났는지 or 에러를 뱉었는지 알려줌</li>
</ul>
<h3 id="combine의-주요-subscriber">Combine의 주요 subscriber</h3>
<p>Combine은 Publisher 타입에 있는 operator 형태의 subscriber를 제공한다.</p>
<ol>
<li><strong>sink</strong><ul>
<li>completion 시그널을 받고 새 항목을 받을 때 클로저를 실행</li>
</ul>
</li>
<li><strong>assign</strong><ul>
<li>받은 값을 다른 프로퍼티나 publisher에게 할당</li>
</ul>
</li>
</ol>
<h2 id="subscribers">Subscribers</h2>
<pre><code class="language-swift">enum Subscribers</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/aef4646a-603e-43ca-92d0-63b62f8f76b7/image.png" alt=""></p>
<p>Publisher와 마찬가지로 subscriber 역할을 하는 타입들을 모아둔 네임스페이스가 있다.</p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/728118f4-e56d-44cb-89a6-3c7dd4318ff7/image.png" alt=""></p>
<h3 id="demand">Demand</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/6da6340a-b600-4357-a5da-513bdb630231/image.png" alt=""></p>
<ul>
<li>요청한 항목의 개수</li>
<li>subscription을 통해 subscriber → publisher에게 전달됨</li>
</ul>
<p><strong>max(_:)</strong></p>
<ul>
<li>publisher에게 몇 번 더 값을 달라고 요청을 보낼 것인지</li>
<li>음수를 전달하면 fatal error가 발생함</li>
</ul>
<p><strong>unlimited</strong></p>
<ul>
<li>publisher가 만들 수 있는 만큼 다 요청을 보낸다</li>
</ul>
<p><strong>none</strong></p>
<ul>
<li>publisher로부터 어떤 값도 요청하지 않는다.</li>
</ul>
<h3 id="completion">Completion</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/a5d3b8fa-3316-4377-8dff-c7f5381721ae/image.png" alt=""></p>
<p>publisher가 정상적인 종료 or 에러 발생에 의해 더이상 값을 만들어내지 않음을 알려주는 신호</p>
<p><strong>finished</strong></p>
<ul>
<li>정상적으로 종료됨</li>
</ul>
<p><strong>failure(Failure)</strong></p>
<ul>
<li>에러에 의해 publisher가 퍼블리싱을 중단함</li>
</ul>
<h1 id="convenience-publishers">Convenience Publishers</h1>
<p><img src="https://velog.velcdn.com/images/240-coding/post/e5d8782f-597a-46ea-974a-243deefc76b2/image.png" alt=""></p>
<h2 id="just">Just</h2>
<pre><code class="language-swift">struct Just&lt;Output&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/6f610fd1-0e0f-400a-8040-33f3f04f4685/image.png" alt=""></p>
<ul>
<li><p>각각의 subscriber에게 output을 <strong>한 번 방출하고 종료</strong>되는 Publisher</p>
</li>
<li><p>반드시 값을 하나 생성하고, 에러를 생성하지 않는다.</p>
<p>  → Failure의 타입이 <code>Never</code> 로 정의되어 있기 때문</p>
</li>
</ul>
<h3 id="기존-swift에서의-never">기존 Swift에서의 Never?</h3>
<blockquote>
<p>Never 는 코드에서 실행되면 안되는 부분을 명시적으로 나타냅니다.</p>
</blockquote>
<p>Never 를 이용해서 코드를 볼 때 ‘아 이부분은 실행이 되면 안되는 부분이구나’ 라고 판단할 수 있을 것이다. 앱을 강제 종료 해야 한다거나, UIEvent 에 Error 를 써야 할 때 활용할 수 있을 것 같다. 고 한다..</p>
<p><a href="https://medium.com/@b9d9/swift-%EC%97%90%EC%84%9C-never-%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C-89b2a47f1c17">Swift 에서 Never 란 무엇일까?</a></p>
<p><a href="https://www.avanderlee.com/swift/never-keyword/">Never keyword in Swift: return type explained with code examples</a></p>
<p>Publisher가 에러를 뱉지 않으면 <code>Never</code> 를 사용함</p>
<ul>
<li>publisher의 체인을 시작할 때 사용할 수 있다고 한다.</li>
</ul>
<h2 id="future">Future</h2>
<pre><code class="language-swift">final class Future&lt;Output, Failure&gt; where Failure : Error</code></pre>
<p>하나의 값을 만들고 결과를 보내는 (종료 or 실패) publisher</p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/475ad760-1e52-4eac-a1a4-b7e5d17d40fd/image.png" alt=""></p>
<ul>
<li><p>하나의 값을 <strong>비동기로</strong> publish 해야 할 때 사용</p>
<p>  → escaping closure로 구현되어 있다</p>
</li>
</ul>
<p><strong>Future.Promise</strong></p>
<pre><code class="language-swift">typealias Promise = (Result&lt;Output, Failure&gt;) -&gt; Void</code></pre>
<ul>
<li>Future 안에서 호출되는 클로저의 typealias</li>
<li><code>Result&lt;Output, Failure&gt;</code> : Future가 publish한 값 or 에러</li>
</ul>
<pre><code class="language-swift">public init(_ attemptToFulfill: @escaping (@escaping (Result&lt;Output, Failure&gt;) -&gt; Void) -&gt; Void)</code></pre>
<p>사실상 이렇게 정의되어 있는 것</p>
<pre><code class="language-swift">func generateAsyncRandomNumberFromFuture() -&gt; Future &lt;Int, Never&gt; {
    return Future() { promise in
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            let number = Int.random(in: 1...10)
            promise(Result.success(number))
        }
    }
}</code></pre>
<p>Future는 Swift Concurrency로 대체 가능하다고 하는데,, 나중에 알아보는 걸로~</p>
<h2 id="deferred">Deferred</h2>
<blockquote>
<p>Defer: 미루다, 연기하다</p>
</blockquote>
<pre><code class="language-swift">struct Deferred&lt;DeferredPublisher&gt; where DeferredPublisher : Publisher</code></pre>
<p>새로운 Subscriber의 Publisher를 만들기 위해 클로저를 실행하기 전 Subscription을 기다리는 Publisher</p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/f3529aaa-ea7b-4895-8162-d9ef61b4091e/image.png" alt=""></p>
<ul>
<li>init의 <code>createPublisher</code> 는 <code>subscribe(_:)</code> 가 호출될 때 실행된다. 이름 그대로 나중에 호출될 Publisher를 새로 만드는 클로저</li>
</ul>
<h3 id="createpublisher"><strong>createPublisher</strong></h3>
<pre><code class="language-swift">let createPublisher: () -&gt; DeferredPublisher</code></pre>
<p><code>createPublisher</code> 에 의해 리턴된 publisher는 subscription을 바로 받는다.</p>
<h2 id="empty">Empty</h2>
<pre><code class="language-swift">struct Empty&lt;Output, Failure&gt; where Failure : Error</code></pre>
<p>아무 값도 publish 하지 않고, 즉시 종료될 수 있는 publisher</p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/764230df-415e-44d3-987f-de9bbfafbd4c/image.png" alt=""></p>
<ul>
<li><code>Empty(completeImmediately: false)</code> : 어떤 값도 보내지 않고, 결과(종료 or 실패)도 보내지 않는 “Never” publisher</li>
<li><code>completeImmediately</code> : Publisher가 바로 completion을 보낼지 여부<ul>
<li>true면 subscriber에게 subscription을 보내고 바로 종료</li>
<li>false면 절대 완료되지 않음</li>
</ul>
</li>
<li>init(completeImmediately:outputType:failureType:)<ul>
<li>empty publisher를 subscriber나 특정한 output과 failure 타입이 정해진 publisher와 연결할 때 사용하는 생성자</li>
</ul>
</li>
</ul>
<h2 id="fail">Fail</h2>
<pre><code class="language-swift">struct Fail&lt;Output, Failure&gt; where Failure : Error</code></pre>
<p>특정한 에러와 함께 바로 종료되는 publisher</p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/57c5272f-4cba-4040-8190-f49eac954a3f/image.png" alt=""></p>
<ul>
<li>init(outputType:failure:)<ul>
<li>특정한 output 타입을 요구하는 다른 subscriber나 publisher와 연결하고 싶을 때 사용하는 생성자</li>
</ul>
</li>
</ul>
<h2 id="record">Record</h2>
<pre><code class="language-swift">struct Record&lt;Output, Failure&gt; where Failure : Error</code></pre>
<p>여러 개의 input들과 하나의 completion을 기록하고, 나중에 subscriber에게 전달해주는 publisher</p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/22320fe9-6866-44d1-94a7-0e13e112c56e/image.png" alt=""></p>
<ul>
<li>Record Publisher를 생성할 때 output들을 함께 배열 형태로 매개변수로 전달한다.</li>
<li>기존에 있던 <code>recording</code> 을 가지고도 새로 생성 가능</li>
</ul>
<h3 id="recording">Recording</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/7e2d3a0e-81b7-4c9f-b308-bd93c19ef4e1/image.png" alt=""></p>
<p><code>Record</code>가 기록한 결과를 보관해두는 역할을 하는 구조체</p>
<ul>
<li>기록된 output들의 시퀀스</li>
<li>Subscriber에게 보낼 output과 completion을 설정함</li>
<li><code>receive(_:)</code> , <code>receive(completion:)</code> 로 각각 새 output과 completion을 저장함<ul>
<li>output은 completion이 추가된 후에는 생성 불가능</li>
<li>completion은 하나만 추가 가능</li>
</ul>
</li>
</ul>
<h2 id="anypublisher">AnyPublisher</h2>
<pre><code class="language-swift">@frozen
struct AnyPublisher&lt;Output, Failure&gt; where Failure : Error</code></pre>
<p>다른 publisher를 감싸서 타입을 지우는 publisher</p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/926d9636-5600-4688-b881-5a3d0cc32a0a/image.png" alt=""></p>
<p><strong>언제 사용하지?</strong></p>
<ul>
<li><p>자세한 타입을 감추고 싶을 때</p>
</li>
<li><p><code>Subject</code> 의 <code>send(_:)</code> 메소드가 호출되는 걸 막고 싶을 때</p>
<p>  → 값 변경을 막음</p>
</li>
</ul>
<p><code>eraseToAnyPublisher()</code> 를 사용해서 publisher를 AnyPublisher로 감쌀 수 있다</p>
<h1 id="convenience-subscribers">Convenience Subscribers</h1>
<h2 id="sink">Sink</h2>
<pre><code class="language-swift">final class Sink&lt;Input, Failure&gt; where Failure : Error</code></pre>
<p>무한정으로 값들을 요청하는 subscriber</p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/f44a0441-39ae-41fd-8d5b-a0986b3514c6/image.png" alt=""></p>
<ul>
<li>receiveCompletion: completion에서 실행될 클로저</li>
<li>receiveValue: 값을 받았을 때 실행할 클로저</li>
</ul>
<h2 id="assign">Assign</h2>
<pre><code class="language-swift">final class Assign&lt;Root, Input&gt;</code></pre>
<p>받은 항목을 key path를 사용해서 프로퍼티에 할당하는 subscriber</p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/de160c7d-a8f1-4f43-a1a2-0ae8eb82a802/image.png" alt=""></p>
<ul>
<li><p>Assign의 Failure 타입은 <code>Never</code> 이다.</p>
</li>
<li><p>init(object:keyPath:)</p>
<ul>
<li><p>subscriber는 새 값을 받을 때마다 <code>object</code> 의 프로퍼티에 값을 할당한다.</p>
</li>
<li><p>할당해줄 프로퍼티는 <code>keyPath</code> 를 사용해서 나타낸다.</p>
<p>  (key-path expression은 <a href="https://docs.swift.org/swift-book/documentation/the-swift-programming-language/expressions/#Key-Path-Expression">여기</a>를 참고하자..)</p>
</li>
</ul>
</li>
</ul>
<pre><code class="language-swift">class SampleObject {
    var intValue: Int {
        didSet {
            print(&quot;intValue Changed: \(intValue)&quot;)
        }
    }

    init(intValue: Int) {
        self.intValue = intValue
    }

    deinit {
        print(&quot;sample object deinit&quot;)
    }
}

let myObject = SampleObject(intValue: 5)

let assign = Subscribers.Assign&lt;SampleObject, Int&gt;(object: myObject, keyPath: \.intValue)

let intArrayPublisher = [6,7,8,9].publisher
intArrayPublisher.subscribe(assign)</code></pre>
<h2 id="anysubscriber"><strong>AnySubscriber</strong></h2>
<pre><code class="language-swift">@frozen
struct AnySubscriber&lt;Input, Failure&gt; where Failure : Error</code></pre>
<ul>
<li><p>타입을 지운 subscriber</p>
</li>
<li><p>AnyPublisher와 마찬가지로 자세한 타입을 보여주고 싶지 않을 때 사용</p>
</li>
<li><p><code>Subscriber</code> 를 직접 구현하지 않고, 프로토콜 안에 정의된 메소드를 위한 클로저를 사용해서 커스텀 subscriber를 만들고 싶을 때도 사용한다고 한다..</p>
<p>  → 많이 사용할 일은 없지 않을까?</p>
</li>
</ul>
<h2 id="convenience-publishers와-비교">Convenience Publishers와 비교?</h2>
<ul>
<li>Publisher들은 <code>Publisher</code> 프로토콜을 채택한 구조체 (Future 제외)였음</li>
<li>Subscriber들은 <code>Subscribers</code> 안에 정의되어 있고, class 타입이다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/240-coding/post/9ce927d0-3f4a-484d-afd2-ad37d2d35416/image.png" alt=""></p>
<h1 id="cancellable">Cancellable</h1>
<h2 id="cancellable-1">Cancellable</h2>
<pre><code class="language-swift">protocol Cancellable</code></pre>
<p>어떤 작업에 대한 취소가 가능함을 나타내는 프로토콜</p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/b1d39e98-920b-411a-af9e-0c30bb85dfc9/image.png" alt=""></p>
<p><code>cancel()</code></p>
<ul>
<li>할당된 자원을 모두 해제함</li>
<li>타이머, 네트워크 접근, 디스크 I/O로 발생하는 사이드이펙트를 막을 수 있다</li>
</ul>
<p><code>store(in:)</code></p>
<ul>
<li><p>cancellable 인스턴스를 저장하는 메소드</p>
<p>  → 파라미터로 받은 곳에 저장함</p>
</li>
</ul>
<p><code>Subscription</code> 프로토콜이 <code>Cancellable</code> 을 채택</p>
<p>→ thread-safe 하기 위해 </p>
<h2 id="anycancellable">AnyCancellable</h2>
<pre><code class="language-swift">final class AnyCancellable</code></pre>
<p><img src="https://velog.velcdn.com/images/240-coding/post/42b71c8e-473b-4416-aa09-9edaa99ac25d/image.png" alt=""></p>
<p>취소되었을 때 클로저를 실행하는 type-erasing cancellable 객체</p>
<ul>
<li><p>Subscriber는 “cancellation token”을 갖기 위해 이 타입을 사용할 수 있다</p>
<p>  → caller가 publisher를 cancel하기 위해 사용</p>
</li>
<li><p>deinit 되었을 때 자동으로 <code>cancel()</code> 을 호출한다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[3. MVVM: Model-View-ViewModel (4)]]></title>
            <link>https://velog.io/@240-coding/3.-MVVM-Model-View-ViewModel-4</link>
            <guid>https://velog.io/@240-coding/3.-MVVM-Model-View-ViewModel-4</guid>
            <pubDate>Sat, 03 Feb 2024 15:27:11 GMT</pubDate>
            <description><![CDATA[<h2 id="add-list-screen">Add List Screen</h2>
<p>할 일 목록을 추가하는 화면을 만들어보자.</p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/e6bb6b21-7abb-4041-8465-8a354e4d64b7/image.png" alt=""></p>
<h2 id="addlistviewcontroller">AddListViewController</h2>
<p>홈 뷰컨과 같이 AddList 뷰컨도 ViewModel을 생성하고 이를 View에게 전달하는 작업과 화면 전환 로직만을 담당한다</p>
<pre><code class="language-swift">class AddListViewController: UIViewController {
      private var addListView: AddListView!
      ...
      private function setupAddListView() {
          let viewModel = AddListViewModel(tasksListService: TasksListService())
          addListView = AddListView(viewmodel: viewmodel)
          addListView.delegate = self
          self.view = addListView
      }
}</code></pre>
<h2 id="addlistview">AddListView</h2>
<p>할일 추가 화면에 있는 UI 컴포넌트들은 다음과 같다.</p>
<ul>
<li>UITextField (제목 입력)</li>
<li>UIButton (추가 버튼, 뒤로 가기 버튼)</li>
<li>IconSelector</li>
</ul>
<p><code>bindViewToModel</code> 메소드에서 위 컴포넌트들과 ViewModel을 바인딩해준다.</p>
<p>먼저 <code>titleTextField</code> 를 바인딩해보자.</p>
<pre><code class="language-swift">titleTextfield
    .rx.text
    .map({ !($0?.isEmpty)! })
    .bind(to: addListButton.rx.isEnabled)
    .disposed(by: disposeBag)
titleTextfield.rx.text
    .map({ $0! })
    .bind(to: viewModel.input.title )
    .disposed(by: disposeBag)</code></pre>
<p><code>map</code> 메소드를 사용해서 텍스트 필드에 값이 입력되어 있는지 여부를 방출한다.</p>
<p>또한 UIButton의 tap 이벤트도 ViewModel과 바인딩해준다.</p>
<pre><code class="language-swift">addListButton.rx.tap
    .bind(to: viewModel.input.addList)
    .disposed(by: disposeBag)
backButton.rx.tap
    .bind(to: viewModel.input.dismiss)
    .disposed(by: disposeBag)
viewModel.output.dismiss
    .drive(onNext: { [self] _ in
        delegate?.navigateBack()
    })</code></pre>
<p><code>iconSelectorView</code> 에는 선택한 아이콘의 이름을 방출하는 BehaviorRelay를 추가한다.</p>
<pre><code class="language-swift">var selectedIcon = BehaviorRelay&lt;String&gt;(value: &quot;checkmark.seal.fill&quot;)</code></pre>
<p>그리고 <code>selectedIcon</code> 과 ViewModel을 바인딩해준다. </p>
<pre><code class="language-swift">iconSelectorView.selectedIcon
    .bind(to: viewModel.input.icon)
    .disposed(by: disposeBag)</code></pre>
<h2 id="addlistviewmodel">AddListViewModel</h2>
<p>이 ViewModel에서는 Model과 상호작용 할 수 있게 <code>TaskListService</code> 에 레퍼런스를 전달하고 <code>TaskListModel</code> 객체를 초기화한다.</p>
<pre><code class="language-swift">class AddListViewModel {
    var output: Output!
    var input: Input!
    struct Input {
        let icon: PublishRelay&lt;String&gt;
        let title: PublishRelay&lt;String&gt;
        let addList: PublishRelay&lt;Void&gt;
        let dismiss: PublishRelay&lt;Void&gt;
    }
    struct Output {
        let dismiss: Driver&lt;Void&gt;
    }
    ...
}</code></pre>
<pre><code class="language-swift">class AddListViewModel {
    ...
    private var tasksListService: TasksListServiceProtocol!
    private(set) var list: TasksListModel!
    private let dismiss = BehaviorRelay&lt;Void&gt;(value: ())
    init(tasksListService: TasksListServiceProtocol) {
        self.tasksListService = tasksListService
        self.list = TasksListModel(id: ProcessInfo().globallyUniqueString, icon: &quot;checkmark.seal.fill&quot;,
        createdAt: Date())
        // Inputs
        let icon = PublishRelay&lt;String&gt;()
        _ = icon.subscribe(onNext: { [self] newIcon in
            list.icon = newIcon
        })
        let title = PublishRelay&lt;String&gt;()
        _ = title.subscribe(onNext: { [self] newTitle in
            list.title = newTitle
        })
        let addList = PublishRelay&lt;Void&gt;()
        _ = addList.subscribe(onNext: { [self] _ in
            tasksListService.saveTasksList(list)
            dismiss.accept(())
        })
        let dismissView = PublishRelay&lt;Void&gt;()
        _ = dismissView.subscribe(onNext: { [self] _ in
            dismiss.accept(())
        })
        input = Input(icon: icon, title: title, addList: addList, dismiss: dismissView)
        // Outputs
        let backNavigation = dismiss.asDriver()
        output = Output(dismiss: backNavigation)
       ...
    }
}</code></pre>
<h1 id="tasks-list-screen">Tasks List Screen</h1>
<p>이 화면은 목록 안에 있는 태스크를 보여주고, 완료 표시를 하고, 태스크를 추가하거나 삭제할 수 있는 화면이다.</p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/695fd079-6be2-41c3-9422-90d5258e6596/image.png" alt=""></p>
<h2 id="tasklistviewcontroller"><strong>TaskListViewController</strong></h2>
<p>이 뷰컨이 수행하는 역할은 다음과 같다.</p>
<ul>
<li>TaskListViewModel 인스턴스 초기화</li>
<li>View에게 ViewModel 넘겨주기</li>
<li>태스크 추가 화면/홈 화면으로의 내비게이션 관리</li>
</ul>
<pre><code class="language-swift">class TaskListViewController: UIViewController {
    private var taskListView: TaskListView!
    private var tasksListModel: TasksListModel!
    init(tasksListModel: TasksListModel) {
        **self.tasksListModel = tasksListModel**
        super.init(nibName: nil, bundle: nil)
    }
    ...
    private func setupTaskListView() {
        **let viewModel = TaskListViewModel(tasksListModel: tasksListModel, taskService: TaskService(), tasksListService: TasksListService())
        taskListView = TaskListView(viewModel: viewModel)
        taskListView.delegate = self
        self.view = taskListView**
    }
}
extension TaskListViewController: TaskListViewControllerDelegate {
    func addTask() {
        let addTaskViewController = AddTaskViewController(tasksListModel: tasksListModel)
        addTaskViewController.modalPresentationStyle = .pageSheet
        present(addTaskViewController, animated: true)
    }
}
extension TaskListViewController: BackButtonDelegate {
    func navigateBack() {
        navigationController?.popViewController(animated: true)
    }
}</code></pre>
<p>이 화면에서는 홈 화면에서 선택한 리스트에 있는 태스크들을 보여주기 때문에 초기화를 할 때 <code>tasksListModel</code>  을 전달해야 한다. 그리고 이 ViewModel은 다시 <code>TaskListViewModel</code> 에 전달된다.</p>
<h2 id="tasklistview">TaskListView</h2>
<p>TaskListView는 HomeView와 유사한 컴포넌트들을 가지고 있다.</p>
<ul>
<li>태스크들의 목록을 보여주는 UITableView</li>
<li>태스크를 삭제/추가하는 버튼</li>
<li>태스크 목록 제목</li>
<li>HomeView로 돌아가는 버튼</li>
</ul>
<p>그외 태스크를 수정하거나 상태를 변경할 수 있는 기능들이 포함될 수 있다.</p>
<p>이런 기능들을 고려하면서 먼저 UITableView와 ViewModel을 바인딩해보자!</p>
<pre><code class="language-swift">tableView.rx
    .setDelegate(self)
    .disposed(by: disposeBag)
tableView.rx.itemDeleted
    .bind(to: viewModel.input.deleteRow)
    .disposed(by: disposeBag)
viewModel.output.tasks
    .drive(tableView.rx.items(cellIdentifier: TaskCell.reuseId, cellType: TaskCell.self)) { (index, task, cell) in
        cell.setParametersForTask(task, at: index)
        cell.checkButton.rx.tap
            .map({ IndexPath(row: cell.cellIndex, section: 0) })
            .bind(to: viewModel.input.updateRow)
            .disposed(by: cell.disposeBag)
    }
    .disposed(by: disposeBag)</code></pre>
<pre><code class="language-swift">addTaskButton.rx.tap
    .asDriver()
    .drive(onNext: { [self] in
        delegate?.addTask()
    })
    .disposed(by: disposeBag)
backButton.rx.tap
    .asDriver()
    .drive(onNext: { [self] in
        delegate?.navigateBack()
    })
    .disposed(by: disposeBag)</code></pre>
<p>이 버튼들을 누르면 delegate를 통해 뷰컨의 메소드를 호출한다.</p>
<pre><code class="language-swift">viewModel.output.pageTitle
    .drive(pageTitle.rx.text)
    .disposed(by: disposeBag)
viewModel.output.hideEmptyState
    .drive(emptyState.rx.isHidden)
    .disposed(by: disposeBag)</code></pre>
<p>페이지 제목과 빈 화면 여부도 바인딩해준다.</p>
<pre><code class="language-swift">viewModel.input.reload.accept(())</code></pre>
<p>마지막으로 reload를 accept한다. accept하면 ViewModel이 데이터베이스를 호출해서 생성된 태스크 목록을 불러오고 이를 테이블 뷰에 보여준다.</p>
<h2 id="taskslistviewmodel"><strong>TasksListViewModel</strong></h2>
<pre><code class="language-swift">class TaskListViewModel {
    var output: Output!
    var input: Input!
    struct Input {
        let reload: PublishRelay&lt;Void&gt;
        let deleteRow: PublishRelay&lt;IndexPath&gt;
        let updateRow: PublishRelay&lt;IndexPath&gt;
    }
    struct Output {
        let hideEmptyState: Driver&lt;Bool&gt;
        let tasks: Driver&lt;[TaskModel]&gt;
        let pageTitle: Driver&lt;String&gt;
    }
    ...
}</code></pre>
<p>TaskListViewModel의 Input과 Output은 다음과 같다. 이제 이 속성들을 초기화해보자.</p>
<pre><code class="language-swift">class TaskListViewModel {
    ...
    init(tasksListModel: TasksListModel,
         taskService: TaskServiceProtocol,
         tasksListService: TasksListServiceProtocol) {
        self.tasksListModel = tasksListModel
        self.taskService = taskService
        self.tasksListService = tasksListService
        // Inputs
        let reload = PublishRelay&lt;Void&gt;()
        _ = reload.subscribe(onNext: { [self] _ in
            fetchTasks()
        })
        let deleteRow = PublishRelay&lt;IndexPath&gt;()
        _ = deleteRow.subscribe(onNext: { [self] indexPath in
            deleteTaskAt(indexPath: indexPath)
        })
        let updateRow = PublishRelay&lt;IndexPath&gt;()
        _ = updateRow.subscribe(onNext: { [self] indexPath in
            updateTaskAt(indexPath: indexPath)
        })
        input = Input(reload: reload, deleteRow: deleteRow, updateRow: updateRow)
        // Outputs
        let items = tasks
            .asDriver(onErrorJustReturn: [])
        let hideEmptyState = tasks
            .map({ items in
                return !items.isEmpty
            })
            .asDriver(onErrorJustReturn: false)
        let pageTitle = pageTitle
            .asDriver(onErrorJustReturn: &quot;&quot;)
        output = Output(hideEmptyState: hideEmptyState, tasks: items, pageTitle: pageTitle)
        ...
    }
    ...
}</code></pre>
<p>개인적으로 ViewModel의 Output 역할이 항상 헷갈렸기 때문에 Output 속성들의 역할만 정리하자면</p>
<ul>
<li>hideEmptyState: 태스크들이 존재하는지 여부를 Bool값으로 리턴한다.</li>
<li>tasks: 태스크들이 담긴 배열을 방출한다. 이 태스크의 타입은 TaskModel이고 데이터베이스에서 받아오는 값이다.</li>
<li>pageTitle: 태스크 목록의 제목</li>
</ul>
<p>이제 데이터베이스 (Model)에 접근하는 메소드를 작성하자.</p>
<pre><code class="language-swift">class TaskListViewModel {
    ...
    private var tasksListModel: TasksListModel!
    private var taskService: TaskServiceProtocol!
    private var tasksListService: TasksListServiceProtocol!
    let tasks = BehaviorRelay&lt;[TaskModel]&gt;(value: [])
    let pageTitle = BehaviorRelay&lt;String&gt;(value: &quot;&quot;)
    init(tasksListModel: TasksListModel,
         taskService: TaskServiceProtocol,
         tasksListService: TasksListServiceProtocol) {
        ...
        NotificationCenter.default.addObserver(self,
                           selector: #selector(contextObjectsDidChange),
                           name: NSNotification.Name.NSManagedObjectContextObjectsDidChange,
                           object: CoreDataManager.shared.mainContext)
    }
    @objc func contextObjectsDidChange() {
        fetchTasks()
    }
    func fetchTasks() {
        guard let list = tasksListService.fetchListWithId(tasksListModel.id) else { return }
        let orderedTasks = list.tasks.sorted(by: { $0.createdAt.compare($1.createdAt) == .orderedDescending })
        tasks.accept(orderedTasks)
        pageTitle.accept(list.title)
    }
    func deleteTaskAt(indexPath: IndexPath) {
        taskService.deleteTask(tasks.value[indexPath.row])
    }
    func updateTaskAt(indexPath: IndexPath) {
        var taskToUpdate = tasks.value[indexPath.row]
        taskToUpdate.done.toggle()
        taskService.updateTask(taskToUpdate)
    }
}</code></pre>
<h1 id="add-task-screen"><strong>Add Task Screen</strong></h1>
<p><img src="https://velog.velcdn.com/images/240-coding/post/44735dd8-2dde-49a0-8ade-61ba52b24f4f/image.png" alt=""></p>
<h2 id="addtaskviewcontroller"><strong>AddTaskViewController</strong></h2>
<p>초기화될 때 <code>tasksListModel</code> 을 받고 <code>AddTaskViewModel</code> 을 생성한 후 이를 View에 전달한다.****</p>
<pre><code class="language-swift">class AddTaskViewController: UIViewController {
    private var addTaskView: AddTaskView!
    private var tasksListModel: TasksListModel!
    init(tasksListModel: TasksListModel) {
        super.init(nibName: nil, bundle: nil)
        self.tasksListModel = tasksListModel
    }
    ...
    private func setupAddTaskView() {
        let viewModel = AddTaskViewModel(tasksListModel: tasksListModel, taskService: TaskService())
        addTaskView = AddTaskView(viewModel: viewModel)
        addTaskView.delegate = self
        self.view = addTaskView
    }
}</code></pre>
<h2 id="addtaskview"><strong>AddTaskView</strong></h2>
<p>이 View의 코드는 <code>AddListView</code> 와 유사하다.</p>
<pre><code class="language-swift">func bindViewToModel(_ viewModel: AddTaskViewModel) {
    titleTextfield.rx.text
        .map({!($0?.isEmpty)!})
        .bind(to: addTaskButton.rx.isEnabled)
        .disposed(by: disposeBag)
    titleTextfield.rx.text
        .map({ $0! })
        .bind(to: viewModel.input.title )
        .disposed(by: disposeBag)
    addTaskButton.rx.tap
        .bind(to: viewModel.input.addTask)
        .disposed(by: disposeBag)
    iconSelectorView.selectedIcon
        .bind(to: viewModel.input.icon)
        .disposed(by: disposeBag)
    viewModel.output.dismiss
        .skip(1)
        .drive(onNext: { [self] in
            delegate?.addedTask()
        })
        .disposed(by: disposeBag)
}</code></pre>
<h2 id="addtaskviewmodel"><strong>AddTaskViewModel</strong></h2>
<p><code>AddTaskViewModel</code> 은 태스크를 생성한 태스크를 데이터베이스에 저장하는 역할을 담당한다.</p>
<pre><code class="language-swift">class AddTaskViewModel {
    var output: Output!
    var input: Input!
    struct Input {
        let icon: PublishRelay&lt;String&gt;
        let title: PublishRelay&lt;String&gt;
        let addTask: PublishRelay&lt;Void&gt;
    }
    struct Output {
        let dismiss: Driver&lt;Void&gt;
    }
    ...
}</code></pre>
<pre><code class="language-swift">class AddTaskViewModel {
    ...
    private var tasksListModel: TasksListModel!
    private var taskService: TaskServiceProtocol!
    private(set) var task: TaskModel!
    let dismiss = BehaviorRelay&lt;Void&gt;(value: ())
    init(tasksListModel: TasksListModel,
         taskService: TaskServiceProtocol) {
        self.tasksListModel = tasksListModel
        self.taskService = taskService
        self.task = TaskModel(id: ProcessInfo().globallyUniqueString, 
                                                  icon: &quot;checkmark.seal.fill&quot;,
                                                  done: false,
                                                  createdAt: Date())
        // Inputs
        let icon = PublishRelay&lt;String&gt;()
        _ = icon.subscribe(onNext: { [self] newIcon in
            task.icon = newIcon
        })
        let title = PublishRelay&lt;String&gt;()
        _ = title.subscribe(onNext: { [self] newTitle in
            task.title = newTitle
        })
        let addTask = PublishRelay&lt;Void&gt;()
        _ = addTask.subscribe(onNext: { [self] _ in
            taskService.saveTask(task, in: tasksListModel)
            dismiss.accept(())
        })
        input = Input(icon: icon, title: title, addTask: addTask)
        // Outputs
        let dismissView = dismiss.asDriver()
        output = Output(dismiss: dismissView)
    }
}</code></pre>
<p><code>input.addTask</code>  는 데이터베이스에 새 태스크를 추가하는 메소드를 실행한 후 <code>dismiss</code>  메소드를 호출한다.</p>
<hr>
<p>드디어 기능 개발 부분은 다 읽었다..!!!
여러 번 구현 과정을 보고, 회사에서도 개발을 하니까 확실히 혼자 공부할 때보다 이해가 훨씬 잘 된다!
다음은 이제 테스트 방법... 상반기 안에 이 책 꼭 완독해야지..👀</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[3. MVVM: Model-View-ViewModel (3)]]></title>
            <link>https://velog.io/@240-coding/3.-MVVM-Model-View-ViewModel-3</link>
            <guid>https://velog.io/@240-coding/3.-MVVM-Model-View-ViewModel-3</guid>
            <pubDate>Fri, 12 Jan 2024 14:32:04 GMT</pubDate>
            <description><![CDATA[<h1 id="mytodos-application-screens">MyToDos Application Screens</h1>
<p>이제 MVVM을 활용해서 투두 애플리케이션을 만들어보자.</p>
<p>MVVM은 MVP 아키텍처와 비슷하다. ViewModel이 Presenter와 비슷한 기능을 하기 때문이다.</p>
<p>MVP는 View와 Presenter의 결합도가 높다는 단점이 있다. 즉, View와 Presenter가 서로를 참조해야 한다.</p>
<p>하지만 데이터 바인딩을 사용하면 이 결합도를 낮출 수 있다!</p>
<h2 id="appdelegate-and-scenedelegate">AppDelegate and SceneDelegate</h2>
<p>AppDelegate, SceneDelegate에서 첫 화면으로 쓰일 ViewController 객체를 생성한다. 이때 뷰컨 객체에 서비스에 대한 dependency를 전달하지 않는다. ViewModel 안에서 dependency를 주입할 것이기 때문!</p>
<h2 id="home-screen">Home Screen</h2>
<p>Home Screen의 메인 컴포넌트는 <code>HomeViewModel</code> 이다. 이 뷰모델은 <code>HomeView</code> 에 바인딩되어 있다.</p>
<h2 id="homeviewcontroller">HomeViewController</h2>
<p>뷰컨에서 <code>HomeViewModel</code> 의 객체를 생성하고, 이를 View에게 전달한다.</p>
<p>또한 화면 전환 역시 뷰컨이 담당한다.</p>
<pre><code class="language-swift">extension HomeViewController: HomeViewControllerDelegate {
    func addList() {
        navigationController?.pushViewController(AddListViewController(), animated: true)
    }
    func selectedList(_ list: TasksListModel) {
        let taskViewController = TaskListViewController(tasksListModel: list)
        navigationController?.pushViewController(taskViewController, animated: true)
    }
}</code></pre>
<p>Coordinator 패턴을 사용하면 뷰컨에서 화면전환 로직을 제거할 수 있다.</p>
<h2 id="homeview">HomeView</h2>
<p>MVVM의 View에서 특히 주목해야 할 것은 <strong>데이터 바인딩을 사용해서 ViewModel의 상태 변경을 감지하는 방법</strong>이다.</p>
<p>RxSwift를 사용하면 쉽게 View의 컴포넌트들과 Model을 연결할 수 있다.</p>
<pre><code class="language-swift">class HomeView: UIView {
    ...
    private let viewModel: HomeViewModel!
    private let disposeBag = DisposeBag()
    init(frame: CGRect = .zero, viewModel: HomeViewModel) {
        self.viewModel = viewModel
        ...
        bindViewToModel(viewModel)
    }
}
private extension HomeView {
    ...
    func bindViewToModel(_ viewModel: HomeViewModel) {
        ...
    }
}</code></pre>
<h3 id="view의-컴포넌트와-viewmodel-바인딩하기">View의 컴포넌트와 ViewModel 바인딩하기</h3>
<p>먼저 <code>UITableView</code> 를 바인딩해보자. 일단 테이블 뷰의 Delegate를 설정한다.</p>
<pre><code class="language-swift">tableView.rx
         .setDelegate(self)
         .disposed(by: disposeBag)</code></pre>
<p>그 다음 섹션, 행, 항목의 수에 대한 정보를 전달한다.</p>
<pre><code class="language-swift">viewModel.output.lists
    .drive(tableView.rx.items(cellIdentifier: ToDoListCell.reuseId, cellType: ToDoListCell.self)) { (_, list, cell) in
        cell.setCellParametersForList(list)
    }
.disposed(by: disposeBag)</code></pre>
<p>Input/Output 접근법에 따르면, 태스크(할 일)의 목록이 ViewModel의 output이 된다. <code>tableView.rx.items()</code> 메소드를 사용해서 이 데이터와 테이블 뷰 cell들을 바인딩한다.</p>
<p>다음으로 사용자가 셀을 선택하는 액션을 구독한다. 이때는 <code>input.selectRow</code> 파라미터를 사용한다.</p>
<pre><code class="language-swift">tableView.rx.itemSelected
    .bind(to: viewModel.input.selectRow)
    .disposed(by: disposeBag)
viewModel.output.selectedList
    .drive(onNext: { [self] list in
        delegate?.selectedList(list)
    })
    .disposed(by: disposeBag)</code></pre>
<ol>
<li>사용자가 table view의 cell을 선택한다.</li>
<li>사용자가 선택한 cell의 indexPath를 ViewModel에 전달한다.<ol>
<li><code>itemSelected</code> 는 선택한 셀의 indexPath를 반환해준다.</li>
</ol>
</li>
<li>이에 대한 <code>TaskListModel</code> 을 output으로 방출한다.</li>
</ol>
<pre><code class="language-swift">addListButton.rx.tap
    .asDriver()
    .drive(onNext: { [self] in
        delegate?.addList()
    })
    .disposed(by: disposeBag)</code></pre>
<p>버튼을 탭했을 때 실행될 로직은 위와 같이 작성한다. rx를 사용하면 버튼 이벤트를 <code>tap</code> 이라는 메소드로 간단하게 처리할 수 있다.</p>
<p>마지막으로 table view를 reload하는 코드를 추가한다.</p>
<pre><code class="language-swift">viewModel.input.reload.accept(())</code></pre>
<h2 id="homeviewmodel">HomeViewModel</h2>
<p>ViewModel은 Model로부터 데이터를 얻은 데이터를 View에게 전달하고, View의 비즈니스 로직을 관리한다.</p>
<pre><code class="language-swift">class HomeViewModel {
    var output: Output!
    var input: Input!
    struct Input {
        let reload: PublishRelay&lt;Void&gt;
        let deleteRow: PublishRelay&lt;IndexPath&gt;
        let selectRow: PublishRelay&lt;IndexPath&gt;
    }
    struct Output {
        let hideEmptyState: Driver&lt;Bool&gt;
        let lists: Driver&lt;[TasksListModel]&gt;
        let selectedList: Driver&lt;TasksListModel&gt;
    }
    ...
}</code></pre>
<p><code>HomeViewModel</code> 의 Input과 Output은 위와 같이 구성되어 있다.</p>
<p>Input과 Output을 정의한 다음에는 ViewModel의 생성자 안에서 동작을 정의한다.</p>
<pre><code class="language-swift">    init(taskListService: TasksListServiceProtocol) {
        ...
              // Inputs
        let reload = PublishRelay&lt;Void&gt;()
        _ = reload.subscribe(onNext: { [self] _ in
            fetchTasksLists()
        })
        let deleteRow = PublishRelay&lt;IndexPath&gt;()
        _ = deleteRow.subscribe(onNext: { [self] indexPath in
            tasksListService.deleteList(listAtIndexPath(indexPath))
        })
        let selectRow = PublishRelay&lt;IndexPath&gt;()
        _ = selectRow.subscribe(onNext: { [self] indexPath in
        taskList.accept(listAtIndexPath(indexPath))
        })
        self.input = Input(reload: reload, deleteRow: deleteRow, selectRow: selectRow)
        // Outputs
        let items = lists
            .asDriver(onErrorJustReturn: [])
        let hideEmptyState = lists
            .map({ items in
                return !items.isEmpty
            })
            .asDriver(onErrorJustReturn: false)
             let selectedList = taskList.asDriver()
        output = Output(hideEmptyState: hideEmptyState, lists: items, selectedList: selectedList)
        ...</code></pre>
<p>다시 또 정리해보자..</p>
<ul>
<li>Input: 사용자와 인터랙션이 발생하면 이를 Input 속성에게 전달(바인딩)한다. ViewModel 내에서 Input 속성들을 구독하고 있기 때문에 인터랙션이 일어난 후에 ViewModel에서 관련된 처리를 해줄 수 있다.</li>
<li>Output: Output 속성을 구독하고 무언가 처리하는 건 View이기 때문에, ViewModel에서는 <code>asDriver()</code> 메소드를 사용해서 각 Output들을 driver로 변환만 해준다.</li>
</ul>
<p>그리고 Model에 접근하는 메소드 역시 ViewModel에서 작성한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[3. MVVM: Model-View-ViewModel (2)]]></title>
            <link>https://velog.io/@240-coding/3.-MVVM-Model-View-ViewModel-2</link>
            <guid>https://velog.io/@240-coding/3.-MVVM-Model-View-ViewModel-2</guid>
            <pubDate>Sat, 02 Dec 2023 03:23:08 GMT</pubDate>
            <description><![CDATA[<h1 id="mvvm-application">MVVM Application</h1>
<p>이제 MVVM 패턴을 적용해서 투두 앱을 만들어보자! 이전 글과 마찬가지로 새롭게 알게 된 내용이나 기억하고 싶은 내용 위주로만 작성할 예정이다.</p>
<h2 id="mvvm-layer">MVVM Layer</h2>
<h3 id="model">Model</h3>
<ul>
<li>비즈니스 로직과 관련된 모든 것들</li>
<li>모델 안에서 일어난 변화를 ViewModel이 알아야 한다. 이를 위해 ViewModel의 <code>init</code> 메소드 안에서 옵저버를 등록한다.</li>
</ul>
<p>Model 디렉토리 안에 있는 하위 디렉토리들은 아래와 같다.</p>
<p>📂 <strong>Core Data</strong></p>
<ul>
<li>CoreDataManager.swift 파일이 있는 곳</li>
</ul>
<p>📂 <strong>Models</strong></p>
<ul>
<li>데이터베이스 엔티티를 모델로 변환하는 모델들을 포함한다. 이를 구현할 때 필요한 프로토콜도 포함한다.</li>
</ul>
<p>📂 <strong>Services</strong></p>
<ul>
<li>데이터베이스에 데이터를 업로드하는 로직과 관련된 클래스들을 포함한다.</li>
<li>그외에도 데이터베이스로부터 데이터를 받는 클래스, 이를 모델로 변환하는 클래스들도 포함한다.</li>
</ul>
<p>📂 <strong>Extensions</strong></p>
<ul>
<li>이 프로젝트에서는 UIColor, NSManagedObject 익스텐션들을 포함한다.<ul>
<li>NSManagedObject는 테스트를 할 때 사용됨</li>
</ul>
</li>
</ul>
<p><strong>📂 Constants</strong></p>
<ul>
<li>앱 내에서 사용되는 상수 파라미터들을 포함한다.</li>
</ul>
<h3 id="view">View</h3>
<ul>
<li>View와 ViewController 파일들이 있는 곳</li>
<li>MVVM에서 컨트롤러는 화면간 내비게이션과 데이터 전달(델리게이트 패턴 등) 기능만을 가지고 있다는 것 잊지 말기!</li>
</ul>
<h3 id="viewmodel">ViewModel</h3>
<ul>
<li>Model과 View를 연결하는 ViewModel들만을 포함한다.</li>
</ul>
<h2 id="rxswift-잠깐-복습하고-넘어가기">RxSwift 잠깐 복습하고 넘어가기</h2>
<p>RxSwift는 비동기적으로 코드를 작성할 수 있게 도와주는 라이브러리다. 새로운 데이터를 받으면 동작할 코드를 연속적이고 독립적으로 간단하게 작성할 수 있다.</p>
<h3 id="observables-and-observers">Observables and Observers</h3>
<ul>
<li><strong>Observable</strong>: 변경이 생기면 알림을 방출한다.</li>
<li><strong>Observer</strong>: Observable을 구독해서 알림을 받는다.</li>
</ul>
<p>Observable은 한 개 이상의 Observer를 가질 수 있다.</p>
<h2 id="inputoutput-approach">Input/Output Approach</h2>
<p>Input/Output 접근법은 서로 다른 컴포넌트와 이벤트를 바인딩하고 RxSwift를 더 체계적으로 사용하기 위한 방법이다.</p>
<ul>
<li><strong>Input</strong>: View에서 일어나는 모든 이벤트와 인터랙션. ViewModel에 영향을 주는 것들이다. (텍스트 입력, 버튼 터치 등)</li>
<li><strong>Output</strong>: 모델에서 일어나는 변화들. 얘네들은 반드시 View에 반영되어야 한다.</li>
</ul>
<pre><code class="language-swift">class ExampleViewModel {
    var output: Output!
    var input: Input!

    struct Input {
        let text: PublishRelay&lt;String&gt;
    }

    struct Output {
        let title: Driver&lt;String&gt;
    }

    init() {
        let text = PublishRelay&lt;String&gt;()

        let capsTitle = text
            .map({
                return text.uppercased()
            })
            .asDriver(onErrorJustReturn: &quot;&quot;)
        input = Input(text: test)
        output = Output(title: capsTitle)
    }
}</code></pre>
<p>Input/Output 접근법을 사용해서 ViewModel을 구현한 예시이다.</p>
<ul>
<li><p><strong>PublishRelay</strong></p>
<ul>
<li>구독한 이후 발생하는 값들을 방출한다.</li>
<li>예제에서는 <code>UITextField</code> 에서 입력한 문자열 값이 방출될 것이다.</li>
</ul>
</li>
<li><p><strong>Driver</strong></p>
<ul>
<li><p>메인 스레드에서 동작하는 Observable이다.</p>
<p>  → 기본적으로 Observable은 백그라운드 스레드에서 동작한다고 한다. 그래서 View를 업데이트하기 위해 Driver를 사용하는 것..!!!</p>
</li>
<li><p>예제에서는 <code>UITextField</code> 에서 받아온 값을 UI 컴포넌트에 전달한다.</p>
</li>
</ul>
</li>
</ul>
<blockquote>
<p>나중에 알게 되었는데,
ViewModel 초기화 시점에 Input의 스트림을 생성하지 않고 View에게 Input을 전달받고 Output 스트림을 생성해서 리턴하는 메소드를 정의하는 방법도 있다고 한다.
<a href="https://coding-idiot.tistory.com/7">참고1</a>, <a href="https://mildwhale.github.io/2020-04-16-mvvm-with-input-output/">참고2</a></p>
</blockquote>
<p>그럼 ViewModel과 View는 어떻게 바인딩해줄까?</p>
<pre><code class="language-swift">class ExampleView {
    func bind() {
        textfield.rx.text
            .bind(to: viewModel.input.text)
            .disposed(by: disposeBag)

        viewModel.output.title
            .drive(titleLabel.rx.text)
            .disposed(by: disposeBag)
    }
}</code></pre>
<ul>
<li><code>UITextField</code> 의 <code>text</code> 파라미터를 ViewModel의 text 변수(Input)와 바인딩한다. 사용자가 텍스트필드에 텍스트를 입력하면 ViewModel이 이를 받게 된다.</li>
<li>ViewModel Output의 title 변수를 <code>UILabel</code> 의 <code>text</code> 파라미터와 바인딩한다. 사용자가 타이핑을 하면 ViewModel의 text가 대문자로 변환되고, View에 전달되어서 화면에 표시된다.</li>
</ul>
<p>흠<strong>…🤔</strong></p>
<ul>
<li><p>Subscribe / Bind / Drive에 대한 차이점 정리는 <a href="https://suvera.tistory.com/35">요기</a>로.. Rx에 익숙해지면 나도 더 자세히 공부해봐야지!!!</p>
</li>
<li><p>bind(to:)는 새 구독을 생성하고 항목들을 Observer들에게 보내주는 메소드이다.</p>
</li>
<li><p><code>drive</code> 는 <code>Drive</code> 에서만 정의되며, <code>bind(to:)</code> 대신 사용할 수 있다고 한다..</p>
<p>  → 그니까 텍스트필드의 텍스트를 구독해서 뷰모델의 input에 알려주고, 뷰모델의 output을 구독해서 타이틀라벨의 텍스트에게 알려주는…..</p>
</li>
</ul>
<h3 id="🤯-너무-헷갈린다--그래서-누가-누굴-구독하는-거야">🤯 <strong>너무 헷갈린다..  그래서 누가 누굴 구독하는 거야?</strong></h3>
<ol>
<li><p><code>bind(to:)</code> 메소드는 다음과 같은 형태로 작성되어 있다.</p>
<pre><code class="language-swift"> public func bind(to relays: PublishRelay&lt;Element&gt;...) -&gt; Disposable {
     bind(to: relays)
 }</code></pre>
<p> 이 메소드는 새 구독을 생성하고 항목을 publish relay들에게 보내는 메소드라고 한다.</p>
<p> 안에 있는 <code>bind</code> 메소드가 어떻게 구현되어 있는지 더 뜯어보자.</p>
<pre><code class="language-swift"> private func bind(to relays: [PublishRelay&lt;Element&gt;]) -&gt; Disposable {
     subscribe { e in
         switch e {
         case let .next(element):
              relays.forEach {
                 $0.accept(element)
             }
         case let .error(error):
             rxFatalErrorInDebug(&quot;Binding error to publish relay: \(error)&quot;)
         case .completed:
             break
         }
     }
 }</code></pre>
<p> 이 <code>bind</code> 메소드를 호출하는 Observable을 구독하고, 새 항목이 방출되면 파라미터로 받은 relay에게 그 항목을 전달하는 것 같다!</p>
<p> <code>to</code> 라는 파라미터 이름을 방출된 항목을 받는 친구를 가리키는 거라고 생각해야겠다.</p>
</li>
<li><p><code>drive()</code> 는 종류가 다양하다. Observer와 Relay 모두 파라미터로 받을 수 있다.</p>
<pre><code class="language-swift"> public func drive&lt;Observer: ObserverType&gt;(_ observers: Observer...) -&gt; Disposable where Observer.Element == Element {
     MainScheduler.ensureRunningOnMainThread(errorMessage: errorMessage)
     return self.asSharedSequence()
                .asObservable()
                .subscribe { e in
                 observers.forEach { $0.on(e) }
                }
     }</code></pre>
<p> 이 메소드도 새 구독을 생성하고 항목을 observer에게 보내주는 메소드라고 한다. 대신 메인 스레드에서만 호출된다.</p>
</li>
</ol>
<p>결론은 <code>bind(to:)</code> 와 <code>drive()</code> 모두 메소드를 호출하는 Observable을 구독하는 것이고, 파라미터로 받은 Observer가 변경된 값을 전달받는 듯 하다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[3. MVVM: Model-View-ViewModel (1)]]></title>
            <link>https://velog.io/@240-coding/3.-MVVM-Model-View-ViewModel-1</link>
            <guid>https://velog.io/@240-coding/3.-MVVM-Model-View-ViewModel-1</guid>
            <pubDate>Sat, 02 Dec 2023 03:22:02 GMT</pubDate>
            <description><![CDATA[<h2 id="what-is-mvvm">What is MVVM?</h2>
<ul>
<li><p>MVVM은 웹 인터페이스의 이벤트 기반 프로그래밍을 단순화하기 위해 고안된 패턴이다.</p>
</li>
<li><p>MVP 패턴에서 발전된 패턴으로, Presenter 레이어가 ViewModel 레이어로 대체되었다.</p>
<p>  → View와 Presenter 사이의 결합도를 낮추고자 한 패턴이다. </p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/240-coding/post/182bd1fb-84a8-4e82-83b0-57f409b00603/image.png" alt=""></p>
<h2 id="components-in-mvvm">Components in MVVM</h2>
<h3 id="model">Model</h3>
<p>비즈니스 로직과 데이터를 저장, 조작, 접근하는 컴포넌트이다. Model에 포함되는 요소들은 다음과 같다.</p>
<ul>
<li>데이터 저장과 관련된 클래스</li>
<li>애플리케이션의 통신을 제어하는 클래스</li>
<li>외부에서 받은 정보를 모델 객체로 변환하는 책임을 가짐</li>
<li>익스텐션, 상수들</li>
</ul>
<p>Model 레이어는 ViewModel 레이어와만 소통할 수 있다. Model은 View의 존재를 몰라야 한다!</p>
<h3 id="view">View</h3>
<p>UIView와 UIViewController를 모두 포함한다.</p>
<ul>
<li>사용자에게 업데이트된 정보를 항상 보여주어야 한다. 이때 정보는 ViewModel이 전해줌!</li>
<li>View는 어떤 로직도 바꾸지 않는다.</li>
<li>ViewModel에 대한 여러 레퍼런스를 가질 수 있다.</li>
</ul>
<p>Controller는 코디네이션이나 라우팅하는 함수, 화면간의 내비게이션, 델리게이트 패턴을 통해 받은 정보를 전달하는 로직만을 갖게 된다.</p>
<h3 id="viewmodel">ViewModel</h3>
<p>ViewModel은 MVVM의 중심이 되는 레이어이다. MVC의 컨트롤러나 MVP의 프레젠터 같은 느낌.</p>
<ul>
<li>ViewModel은 항상 View의 현재 상태를 보여주어야 한다.</li>
<li>View를 제어하는 Input/Output 정보를 가공한다.</li>
<li><strong>View는 ViewModel을 소유하고, ViewModel은 Model을 소유한다.</strong><ul>
<li>View ← ViewModel ← Model</li>
</ul>
</li>
</ul>
<p>ViewModel은 데이터 바인딩을 통해 View에 연결된다. 이를 통해 ViewModel이 관리하는 정보의 상태가 변하면 View가 자동으로 업데이트 될 수 있다.</p>
<h3 id="data-binding">Data Binding</h3>
<p>View와 ViewModel 사이에 데이터 바인딩을 하는 방법은 여러 가지가 있다.</p>
<ul>
<li>RxSwift, Bond, Combine 같은 라이브러리 사용</li>
<li>KVO (Key-Value observing) 패턴 사용</li>
<li>Delegate 패턴, 클로져 사용</li>
</ul>
<h2 id="advantages-and-disadvantages-of-mvvm">Advantages and Disadvantages of MVVM</h2>
<h3 id="advantages">Advantages</h3>
<ul>
<li><p>책임이 더욱 명확하게 분리된다.</p>
<p>  → 유지가 쉽다.</p>
</li>
<li><p>테스트를 하기 쉽다.</p>
<p>  → View를 고려하지 않고 ViewModel의 비즈니스 로직을 테스트할 수 있다. 또한 MVC와 다르게 컨트롤러가 Model에 의존하지 않기 때문에 컨트롤러도 테스트하기 용이함!</p>
</li>
<li><p>MVC 아키텍처에서 마이그레이션 할 때 자주 사용된다.</p>
</li>
</ul>
<h3 id="disadvantages">Disadvantages</h3>
<ul>
<li>외부 라이브러리를 사용하는 경우 고려해야 될 점들<ul>
<li>앱의 용량이 증가한다.</li>
<li>앱의 성능에도 영향을 준다.</li>
<li>외부 라이브러리를 정확하게 사용하기 위한 러닝 커브</li>
</ul>
</li>
<li>데이터 바인딩은 선언형 패러다임으로, 프로그램이 어떻게 동작할지가 아닌 무엇을 해야 할지에 대한 코드를 작성한다. 이는 디버깅을 어렵게 만들고, UIKit보다 SwiftUI에 대한 이해를 더욱 필요로 한다.</li>
<li>아키텍처를 제대로 적용하지 않으면 ViewModel에 해당하지 않는 함수를 오버로딩 하게 될 수 있다.</li>
<li>처음 시작할 때는 복잡하다. 😅</li>
</ul>
<hr>
<p>MVC 패턴을 공부한지 9개월이 지나고 나서 다음 공부하는 나란 사람..ㅎㅎ;;</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RxSwift: Operators (2) - Transforming, Combining Operators]]></title>
            <link>https://velog.io/@240-coding/RxSwift-Operators-2-Transforming-Combining-Operators</link>
            <guid>https://velog.io/@240-coding/RxSwift-Operators-2-Transforming-Combining-Operators</guid>
            <pubDate>Wed, 15 Nov 2023 16:11:39 GMT</pubDate>
            <description><![CDATA[<h2 id="transforming-operators">Transforming Operators</h2>
<p>관찰 가능한 데이터를 새로운 시퀀스로 변형하는 operator이다.</p>
<h3 id="toarray">ToArray</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/cd57ba13-6262-4f00-bf7d-e8f0d214ccbc/image.png" alt=""></p>
<p>여러 개의 항목들을 하나의 배열로 변환해주는 operator이다.</p>
<pre><code class="language-swift">let disposeBag = DisposeBag()

Observable.of(1, 2, 3, 4, 5)
    .toArray()
    .subscribe({
        print($0)
    }).disposed(by: disposeBag)

// 실행결과
// success([1, 2, 3, 4, 5])</code></pre>
<p><code>toArray()</code> 의 반환 타입이 <code>Single&lt;[Element]&gt;</code> 인데, 그래서 subscribe에 <code>onNext</code> 가 없는 것 같다. 나중에 업데이트 된 것 같은데 더 알아봐야 할 듯</p>
<h3 id="map">Map</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/688ee73e-436f-4bfc-83bc-aeda49ca9214/image.png" alt=""></p>
<p>시퀀스의 각 항목들에 함수를 적용하는 함수. Swift의 map과 유사하다.</p>
<pre><code class="language-swift">let disposeBag = DisposeBag()

Observable.of(1, 2, 3, 4, 5)
    .map {
        return $0 * 2
    }.subscribe(onNext: {
        print($0)
    }).disposed(by: disposeBag)

/* 실행 결과
2
4
6
8
10
*/</code></pre>
<h3 id="flatmap">FlatMap</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/44979547-5518-4899-86e4-fd15c3311bb9/image.png" alt=""></p>
<p>Observable에 의해 방출된 항목을 다시 <strong>Observable</strong>로 변형하고 방출된 항목을 다시 <strong>하나의 Observable</strong>로 평면화한다.</p>
<pre><code class="language-swift">let disposeBag = DisposeBag()

struct Student {
    var score: BehaviorRelay&lt;Int&gt;
}

let john = Student(score: BehaviorRelay(value: 75))
let mary = Student(score: BehaviorRelay(value: 95))

let student = PublishSubject&lt;Student&gt;()
    .flatMap { $0.score.asObservable() }
    .subscribe(onNext: {
        print($0)
    }).disposed(by: disposeBag)

student.onNext(john)
john.score.accept(100)
student.onNext(mary)

// 실행결과
// 75
// 100
// 95</code></pre>
<p>john의 score 값을 변경하려면 같이 <code>accept</code> 메소드를 사용해야 한다.</p>
<p>위 예시처럼 내부에 있는 Observable을 변형하고 평면화할 때 유용하다고 한다.</p>
<p>Map은 단순한 값을 반환하지만 flatMap은 Observable을 반환하는 것이 가장 큰 차이인 것 같다. 아직은 둘의 차이점이 완전히 와닿지는 않음 ㅠ-ㅠ</p>
<p><a href="https://limjs-dev.tistory.com/139">[RxSwift] map vs flatMap</a></p>
<h3 id="flatmaplatest">FlatMapLatest</h3>
<pre><code class="language-swift">let disposeBag = DisposeBag()

struct Student {
    var score: BehaviorRelay&lt;Int&gt;
}

let john = Student(score: BehaviorRelay(value: 75))
let mary = Student(score: BehaviorRelay(value: 95))

let student = PublishSubject&lt;Student&gt;()
    .flatMapLatest { $0.score.asObservable() }
    .subscribe(onNext: {
        print($0)
    }).disposed(by: disposeBag)

student.onNext(john)
john.score.accept(100)

student.onNext(mary)
john.score.accept(45)

/* 실행 결과
75
100
95
*/</code></pre>
<p>flatMap에서 발생하는 이벤트 중 가장 최신 이벤트만 관찰하고 싶을 때 사용하는 operator이다.</p>
<p>위 예시에서는 마지막으로 mary를 observe 하고 있기 때문에 john의 score 값을 바꿔도 이벤트가 무시되는 것을 볼 수 있다.</p>
<h2 id="combining-operators">Combining Operators</h2>
<h3 id="startwith">StartWith</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/1dbd0764-0811-4edd-82e5-3284731fb8b1/image.png" alt=""></p>
<p>Observable의 항목들을 방출하기 전 특정 시퀀스를 먼저 방출한다.</p>
<pre><code class="language-swift">let disposeBag = DisposeBag()

let numbers = Observable.of(2, 3, 4)

let observable = numbers.startWith(1)
observable.subscribe(onNext: {
    print($0)
}).disposed(by: disposeBag)

/* 실행 결과
1
2
3
4
*/</code></pre>
<h3 id="concat">Concat</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/b53363c5-248c-4b3d-ae18-cb645589dce6/image.png" alt=""></p>
<p>둘 이상의 Observable을 하나로 합쳐주는 Operator이다.</p>
<pre><code class="language-swift">let disposeBag = DisposeBag()

let first = Observable.of(1, 2, 3)
let second = Observable.of(4, 5, 6)

let observable = Observable.concat([first, second])

observable.subscribe(onNext: {
    print($0)
}).disposed(by: disposeBag)

/* 실행 결과
1
2
3
4
5
6
*/</code></pre>
<h3 id="merge">Merge</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/98ed9b89-5fa3-45ac-9078-4a1412ee936f/image.png" alt=""></p>
<p>여러 개의 Observable들을 하나로 합치는 operator이다. <code>concat</code> operator와 달리 각 값들의 도착 시간을 기준으로 순서가 정해진다.</p>
<pre><code class="language-swift">let disposeBag = DisposeBag()

let left = PublishSubject&lt;Int&gt;()
let right = PublishSubject&lt;Int&gt;()

let source = Observable.of(left.asObservable(), right.asObservable())

let observable = source.merge()
observable.subscribe(onNext: {
    print($0)
}).disposed(by: disposeBag)

left.onNext(5)
left.onNext(3)
right.onNext(2)
right.onNext(1)
left.onNext(99)

/* 실행 결과
5
3
2
1
99
*/</code></pre>
<p><code>left</code> 와 <code>right</code> 를 observe 하는 Observable을 만들고, 여기에 <code>merge</code> operator를 적용한다.</p>
<h3 id="combinelatest">CombineLatest</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/fa70a925-c2a2-442f-831c-daa67bd569ec/image.png" alt=""></p>
<p>두 Observable의 <strong>최신</strong> 항목들을 합쳐서 새로운 결과를 만드는 operator이다.</p>
<pre><code class="language-swift">let disposeBag = DisposeBag()

let left = PublishSubject&lt;Int&gt;()
let right = PublishSubject&lt;Int&gt;()

let observable = Observable.combineLatest(left, right, resultSelector: {
    lastLeft, lastRight in
    &quot;\(lastLeft) \(lastRight)&quot;
})

let disposable = observable.subscribe(onNext: { value in
    print(value)
})

left.onNext(45)
right.onNext(1)
left.onNext(30)
right.onNext(1)
right.onNext(2)

/* 실행 결과
45 1
30 1
30 1
30 2
*/</code></pre>
<p><code>combineLast</code> operator는 <code>resultSelector</code> 라는 파라미터를 가진다!</p>
<h3 id="withlatestfrom">WithLatestFrom</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/06f81f15-535f-4acb-9b9b-1e2fafe1de26/image.png" alt=""></p>
<p>source Observable을 다른 Observable들과 결합해서 새로운 observable을 만든다. <strong>source Observable에서 항목이 방출될 때만</strong> 다른 observable들의 최신 값들과 source observable의 값이 합쳐진다.</p>
<pre><code class="language-swift">let disposeBag = DisposeBag()

let button = PublishSubject&lt;Void&gt;()
let textField = PublishSubject&lt;String&gt;()

let observable = button.withLatestFrom(textField)
let disposable = observable.subscribe(onNext: {
    print($0)
})

textField.onNext(&quot;Sw&quot;)
textField.onNext(&quot;Swif&quot;)
textField.onNext(&quot;Swift&quot;)

button.onNext(())
button.onNext(())

/* 실행 결과
Swift
Swift
*/</code></pre>
<p>위 예제에서는 <code>button</code> 이 source observable이자 트리거 역할을 한다.</p>
<p><a href="https://coding-time.co/rx-combinelatest-vs-withlatestfrom/">Rx: combineLatest vs withLatestFrom</a></p>
<h3 id="reduce">Reduce</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/89a53f4b-3977-4260-a0f2-f9579db7c416/image.png" alt=""></p>
<p>Observable이 방출하는 항목들마다 함수를 적용하고 최종 값을 마지막으로 방출한다. 기존에 Swift에 있는 <code>reduce</code> 와 유사하다.</p>
<pre><code class="language-swift">let disposeBag = DisposeBag()

let source = Observable.of(1, 2, 3)

// 방법 1
source.reduce(0, accumulator: +)
    .subscribe(onNext: {
        print($0)
    }).disposed(by: disposeBag)

// 방법 2
source.reduce(0, accumulator: {
    summary, newValue in
    return summary + newValue
}).subscribe(onNext: {
    print($0)
}).disposed(by: disposeBag)

/* 실행 결과
6
*/</code></pre>
<h3 id="scan">Scan</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/2e92a15b-1b83-4659-875d-d15f3749f05b/image.png" alt=""></p>
<p>reduce와 유사한데 함수가 적용된 결과값을 계산 과정 중에 계속 방출해준다는 차이점이 있다.</p>
<p>이름 그대로 계산 과정을 스캔하는 느낌 👀</p>
<pre><code class="language-swift">let disposeBag = DisposeBag()

let source = Observable.of(1, 2, 3, 5, 6)

source.scan(0, accumulator: +)
    .subscribe(onNext: {
        print($0)
    }).disposed(by: disposeBag)

/* 실행 결과
1
3
6
11
17
*/</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[RxSwift: Operators (1) - Filtering Operators]]></title>
            <link>https://velog.io/@240-coding/RxSwift-Operators-1-Filtering-Operators</link>
            <guid>https://velog.io/@240-coding/RxSwift-Operators-1-Filtering-Operators</guid>
            <pubDate>Sun, 05 Nov 2023 13:55:31 GMT</pubDate>
            <description><![CDATA[<p>Operator는 Observable에서 받은 이벤트들을 변환하고 처리할 수 있도록 해준다.</p>
<h3 id="ignoring-operator">Ignoring Operator</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/f55c7664-22db-4dd4-be84-1f580e7a6637/image.png" alt=""></p>
<p>1, 2, 3이라는 세 가지 값을 가지고 있는 시퀀스가 있다고 해보자. <code>ignoreElements()</code> 라는 ignoring operator를 사용하면 시퀀스의 모든 원소들을 무시한다. 그런데 이때도 completed 이벤트는 발생하게 된다.</p>
<pre><code class="language-swift">let strikes = PublishSubject&lt;String&gt;()

let disposeBag = DisposeBag()

strikes
    .ignoreElements()
    .subscribe { _ in
        print(&quot;[Subscription is called]&quot;)
    }.disposed(by: disposeBag)

strikes.onNext(&quot;A&quot;)
strikes.onNext(&quot;B&quot;)
strikes.onNext(&quot;C&quot;)</code></pre>
<p>위 코드를 실행해보면 아무 것도 출력되지 않는다. <code>ignoreElements()</code> 라는 operator를 실행했기 때문!</p>
<p>이 subscription은 <code>onCompleted</code> 라는 completed 이벤트가 발생했을 때만 호출된다.</p>
<pre><code class="language-swift">strikes.onCompleted()</code></pre>
<p>위 코드를 추가해주면 <code>[Subscription is called]</code> 라는 문구가 잘 출력된다.</p>
<p>Ignoring Operator는 실제 값은 상관없고 observation이 완료되었는지만 구독받고 싶을 때 사용하면 좋다.</p>
<h3 id="elementat">elementAt()</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/08facb1a-d938-4d81-9aee-dd53c23420d0/image.png" alt=""></p>
<p>어떤 시퀀스가 있을 때 <code>elementAt()</code> 메소드를 사용하면 인자로 전달된 인덱스에 있는 값을 얻을 수 있다.</p>
<pre><code class="language-swift">let strikes = PublishSubject&lt;String&gt;()
let disposeBag = DisposeBag()

strikes.element(at: 2)
    .subscribe(onNext: { _ in
        print(&quot;You are out!&quot;)
    }).disposed(by: disposeBag)

strikes.onNext(&quot;X&quot;)
strikes.onNext(&quot;X&quot;)
strikes.onNext(&quot;X&quot;) // You are out!</code></pre>
<p><code>strikes</code> 에서 <code>elementAt()</code> 메소드를 호출하고 구독했다. 이후 세 번째 값이 방출되었을 때 이벤트가 실행이 된다. 두 번째 값까지만 방출하면 이벤트는 실행되지 않는다.</p>
<h2 id="filter-operator">Filter Operator</h2>
<p><img src="https://velog.velcdn.com/images/240-coding/post/886244d5-e3e7-4d28-87ba-ad7d98cbddda/image.png" alt=""></p>
<p>조건에 맞는 값만 방출하는 operator이다.</p>
<pre><code class="language-swift">let strikes = PublishSubject&lt;String&gt;()
let disposeBag = DisposeBag()

Observable.of(1, 2, 3, 4, 5, 6, 7)
    .filter { $0 % 2 == 0 }
    .subscribe(onNext: {
        print($0)
    }).disposed(by: disposeBag)

// 출력 결과
// 2
// 4
// 6</code></pre>
<p>위 Observable에서 <code>filter()</code> 메소드를 호출하여 짝수 값이 방출될 때만 이벤트가 호출되는 것을 볼 수 있다.</p>
<h2 id="skip-operator">Skip Operator</h2>
<h3 id="skip">Skip</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/01ae25b8-7c7d-4176-a313-d3af1fb368e3/image.png" alt=""></p>
<p>파라미터로 받은 숫자만큼 방출된 항목들을 건너뛰는 operator이다.</p>
<pre><code class="language-swift">let strikes = PublishSubject&lt;String&gt;()
let disposeBag = DisposeBag()

Observable.of(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;, &quot;D&quot;, &quot;E&quot;, &quot;F&quot;)
    .skip(3)
    .subscribe(onNext: {
        print($0)
    }).disposed(by: disposeBag)

// 실행 결과
// D
// E
// F</code></pre>
<h3 id="skipwhile">SkipWhile</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/f7d6aef4-8c8d-4893-9922-27465648211d/image.png" alt=""></p>
<p>조건식을 만족할 때까지 계속 건너뛴다. 한 번 조건식이 <code>false</code> 가 되면 이후 다른 값들은 <strong>모두</strong> 시퀀스의 항목에 포함된다.</p>
<pre><code class="language-swift">let strikes = PublishSubject&lt;String&gt;()
let disposeBag = DisposeBag()

Observable.of(2, 2, 3, 4, 4)
    .skip(while: { $0 % 2 == 0 })
    .subscribe(onNext: {
        print($0)
    }).disposed(by: disposeBag)

/* 실행 결과:
3
4
4
*/</code></pre>
<h3 id="skipuntil">SkipUntil</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/3a4186d6-5f2e-44f9-8827-55ffd5a60dca/image.png" alt=""></p>
<p><strong>트리거 역할을 하는 Operator</strong>가 이벤트를 방출하기 전까지 항목을 건너뛴다.</p>
<p>다른 observable을 기반으로 하기 때문에 동적으로 요소들을 필터링 할 수 있다는 장점이 있다!</p>
<pre><code class="language-swift">let strikes = PublishSubject&lt;String&gt;()
let disposeBag = DisposeBag()

let subject = PublishSubject&lt;String&gt;()
let trigger = PublishSubject&lt;String&gt;()

subject.skip(until: trigger)
    .subscribe(onNext: {
        print($0)
    }).disposed(by: disposeBag)

subject.onNext(&quot;A&quot;)
subject.onNext(&quot;B&quot;)

trigger.onNext(&quot;X&quot;)

subject.onNext(&quot;C&quot;)

/* 실행 결과
C
*/</code></pre>
<p><code>trigger</code> 가 항목을 방출한 후에야 <code>subject</code> 의 C라는 항목이 방출된 것을 볼 수 있다.</p>
<h2 id="taking-operator">Taking Operator</h2>
<p>Skip Operator와 유사하지만 정반대로 동작하는 operator이다.</p>
<h3 id="take">Take</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/57263ccd-7128-41c1-85fe-f6dedfc42301/image.png" alt=""></p>
<p>처음부터 n개의 항목들만 방출한다.</p>
<pre><code class="language-swift">let strikes = PublishSubject&lt;String&gt;()
let disposeBag = DisposeBag()

Observable.of(1, 2, 3, 4, 5, 6)
    .take(3)
    .subscribe(onNext: {
        print($0)
    }).disposed(by: disposeBag)

/* 실행 결과:
1
2
3
*/</code></pre>
<h3 id="takewhile">TakeWhile</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/1858480a-cd85-47a2-8836-4abe137d6b4a/image.png" alt=""></p>
<p>조건식을 만족하는 동안 항목들을 방출한다. 조건식이 <code>false</code> 가 되면 더 이상 방출을 하지 않는다.</p>
<pre><code class="language-swift">let strikes = PublishSubject&lt;String&gt;()
let disposeBag = DisposeBag()

Observable.of(2, 4, 6, 7, 8, 10)
    .take(while: {
        $0 % 2 == 0
    }).subscribe(onNext: {
        print($0)
    }).disposed(by: disposeBag)

/* 실행 결과:
2
4
6
*/</code></pre>
<h3 id="takeuntil">TakeUntil</h3>
<p><img src="https://velog.velcdn.com/images/240-coding/post/905a2797-a130-40ba-b4b3-763e4afe8634/image.png" alt=""></p>
<p>트리거 역할을 하는 observable이 항목을 방출하거나 종료되기 전까지 항목들을 방출한다.</p>
<pre><code class="language-swift">let strikes = PublishSubject&lt;String&gt;()
let disposeBag = DisposeBag()

let subject = PublishSubject&lt;String&gt;()
let trigger = PublishSubject&lt;String&gt;()

subject.take(until: trigger)
    .subscribe(onNext: {
        print($0)
    }).disposed(by: disposeBag)

subject.onNext(&quot;1&quot;)
subject.onNext(&quot;2&quot;)

trigger.onNext(&quot;X&quot;)

subject.onNext(&quot;3&quot;)

/* 실행 결과:
1
2
*/</code></pre>
<p><code>trigger</code> 가 X라는 항목을 방출하기 전까지만 <code>subject</code> 의 항목이 출력되는 것을 볼 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RxSwift - Subject]]></title>
            <link>https://velog.io/@240-coding/RxSwift-Subject</link>
            <guid>https://velog.io/@240-coding/RxSwift-Subject</guid>
            <pubDate>Fri, 27 Oct 2023 14:02:06 GMT</pubDate>
            <description><![CDATA[<h2 id="subject란">Subject란?</h2>
<p>Subject는 Observable이자 Observer이다.</p>
<p>이벤트를 받으면 그 이벤트를 다시 구독자들에게 전달하는 역할을 한다.</p>
<p>→ 값을 방출할 수 있을 뿐 아니라 Observe도 가능하다!!</p>
<p>Subject에는 Publish Subject, Behavior Subject, Replay Subject, BehaviorRelay등 다양한 종류가 있다.</p>
<p><a href="https://asong-study-record.tistory.com/143">[RxSwift] Subject의 종류</a></p>
<h3 id="publish-subject-써보기">Publish Subject 써보기</h3>
<pre><code class="language-swift">let subject = PublishSubject&lt;String&gt;()

subject.onNext(&quot;Issue 1&quot;)

subject.subscribe { event in
    print(event)
}

subject.onNext(&quot;Issue 2&quot;)
subject.onNext(&quot;Issue 3&quot;)

subject.dispose()

subject.onCompleted()

subject.onNext(&quot;Issue 4&quot;)</code></pre>
<pre><code>// 실행 결과
next(Issue 2)
next(Issue 3)</code></pre><p>Publish Subject는 초깃값 없이 생성되고, 구독한 이후 방출되는 이벤트만 전달받는다.</p>
<ul>
<li><code>onNext()</code> 메소드를 사용해서 값을 방출한다.</li>
<li>Issue1은 subscribe를 하기 전 방출되었기 때문에 이벤트가 출력되지 않는다.</li>
<li><code>dispose()</code> 메소드를 호출한 후에 호출된 메소드들의 결과는 적용되지 않는다.</li>
<li><code>dispose()</code> 를 호출하지 않고 <code>onCompleted()</code> 만 호출하면 completed가 출력된다.</li>
</ul>
<h3 id="behavior-subject">Behavior Subject</h3>
<p>Behavior Subject는 Publish Subject와 비슷하다. 대신 초기화를 할 때 초깃값을 전달해주어야 한다. 구독을 하면 가장 최신 이벤트를 전달받는다.</p>
<pre><code class="language-swift">let disposeBag = DisposeBag()

let subject = BehaviorSubject(value: &quot;Initial Value&quot;)

subject.onNext(&quot;Last Issue&quot;)

subject.subscribe { event in
    print(event)
}

subject.onNext(&quot;Issue 1&quot;)</code></pre>
<pre><code>// 실행 결과
next(Last Issue)
next(Issue 1)</code></pre><p>초깃값을 설정한 후 방출된 최신 값이 <code>Last Issue</code> 이므로 Last Issue를 방출하고, 그 다음 <code>Issue 1</code> 이 방출되었기 때문에 Issue 1도 출력된다.</p>
<h3 id="replay-subject">Replay Subject</h3>
<p>설정한 버퍼 크기만큼의 이벤트를 저장하고, Observer가 구독을 시작하면 <strong>버퍼에 있는 모든 이벤트를 전달</strong>한다.</p>
<pre><code class="language-swift">let subject = ReplaySubject&lt;String&gt;.create(bufferSize: 2)

subject.onNext(&quot;Issue 1&quot;)
subject.onNext(&quot;Issue 2&quot;)
subject.onNext(&quot;Issue 3&quot;)

subject.subscribe {
    print($0)
}</code></pre>
<pre><code>// 실행 결과
next(Issue 2)
next(Issue 3)</code></pre><p>버퍼 크기를 2로 설정했기 때문에 가장 마지막으로 방출된 두 개의 이벤트가 전달된 것을 볼 수 있다.</p>
<pre><code class="language-swift">let subject = ReplaySubject&lt;String&gt;.create(bufferSize: 2)

subject.onNext(&quot;Issue 1&quot;)
subject.onNext(&quot;Issue 2&quot;)
subject.onNext(&quot;Issue 3&quot;)

subject.subscribe {
    print($0)
}

subject.onNext(&quot;Issue 4&quot;)
subject.onNext(&quot;Issue 5&quot;)
subject.onNext(&quot;Issue 6&quot;)

print(&quot;[Subscription 2]&quot;)
subject.subscribe {
    print($0)
}</code></pre>
<p>첫 번째 subscribe 밑에 3개의 이벤트와 새로운 subscribe 메소드를 하나 추가하고 코드를 실행시키면 아래와 같은 실행결과가 나온다.</p>
<pre><code>next(Issue 2)
next(Issue 3)
next(Issue 4)
next(Issue 5)
next(Issue 6)
[Subscription 2]
next(Issue 5)
next(Issue 6)</code></pre><p>두 번째 subscription은 가장 마지막에 방출된 이벤트인 Issue5와 Issue6을 출력한다.</p>
<h3 id="variable-deprecated">Variable (*deprecated)</h3>
<p>Variable은 behavior subject를 감싸고 있으며, 현재 값의 상태를 저장한다. 이 값은 <code>value</code> 프로퍼티를 사용해서 접근할 수 있다.</p>
<pre><code class="language-swift">let variable = Variable(&quot;Initial Value&quot;)

variable.value = &quot;Hello world&quot;

variable.asObservable()
    .subscribe {
        print($0)
}

// next(Hello World)</code></pre>
<blockquote>
<p>Variable은 RxSwift에서 공식적으로 deprecated되었다고 한다 😱 대신 <code>BehaviorRelay</code> 나 <code>BehaviorSubject</code> 를 사용해야 한다. (<a href="https://freak4pc.medium.com/whats-new-in-rxswift-5-f7a5c8ee48e7">참고: What’s New in RxSwift5</a>)</p>
</blockquote>
<h3 id="behavior-relay">Behavior Relay</h3>
<p>BehaviorRelay는 <code>RxCocoa</code> 를 import해야 사용할 수 있다.</p>
<pre><code class="language-swift">let relay = BehaviorRelay(value: &quot;Initial Value&quot;)

relay.asObservable()
    .subscribe {
        print($0)
    }

relay.value = &quot;Hello World&quot; // Error!

relay.accept(&quot;Hello World&quot;)</code></pre>
<p>BehaviorRelay의 <code>value</code> 는 읽기 전용이기 때문에 값을 직접 수정할 수 없다. 대신 <code>accept()</code> 라는 메소드를 사용해서 새 값을 넣어줄 수 있다.</p>
<pre><code>// 실행 결과
next(Initial Value)
next(Hello World)</code></pre><p>BehaviorRelay의 값이 배열이라면 나중에 값을 어떻게 변경해줄 수 있을까?</p>
<pre><code class="language-swift">let relay = BehaviorRelay(value: [&quot;Item 1&quot;])

// 방법 1
var value = relay.value
value.append(&quot;Item 2&quot;)
value.append(&quot;Item 3&quot;)

relay.accept(value)

// 방법 2
relay.accept(relay.value + [&quot;Item 2&quot;, &quot;Item 3&quot;])

relay.asObservable()
    .subscribe {
        print($0)
    }</code></pre>
<p><code>value</code> 의 값을 직접 변경해줄 수 없기 때문에 새 변수에 값을 담아서 append 하거나 <code>value</code> 에 저장된 배열과 새 값들을 저장한 배열을 합쳐서 accept 해주는 식으로 저장해주어야 한다.</p>
<pre><code>next([&quot;Item 1&quot;, &quot;Item 2&quot;, &quot;Item 3&quot;])</code></pre><p>BehaviorRelay는 UI에서 사용하기 좋도록 변형된 Subject라고 한다. 더 자세한 차이점 및 사용 방법은 나중에 공부해야 할 듯!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RxSwift - Observables]]></title>
            <link>https://velog.io/@240-coding/RxSwift-Observables</link>
            <guid>https://velog.io/@240-coding/RxSwift-Observables</guid>
            <pubDate>Fri, 27 Oct 2023 14:00:23 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/240-coding/post/62dae9f2-a698-4ae7-b3d8-84dacd309f2a/image.png" alt="">
Observable은 RxSwift의 기본이 되는 개념으로, sequence라고도 한다.</p>
<p>예를 들어 iOS 애플리케이션에서 사용자가 탭을 세 번 하면 세 개의 이벤트가 순차적으로 발생될 것이고, 이 이벤트는 언젠가 종료될 것이다.</p>
<p>하지만 어느 순간 에러가 발생해서 이벤트가 완전히 종료되기 전에 프로그램이 종료된다면..??!! 😱</p>
<p>그래서 이벤트들을 구독하자!!는 것이 Observable의 개념이다. Observable을 구독하면 이벤트들의 값이 변할 때마다 이 사실을 알 수 있게 된다.</p>
<h2 id="observable-써보기">Observable 써보기</h2>
<pre><code class="language-swift">let observable = Observable.just(1)

let observable2 = Observable.of(1, 2, 3) // Observable&lt;Int&gt;

let observable3 = Observable.of([1, 2, 3]) // Observable&lt;[Int]&gt;

let observable4 = Observable.from([1, 2, 3, 4, 5]) // Observable&lt;[Int]&gt;</code></pre>
<ul>
<li>Observable.just() : 하나의 값을 방출할 때 사용</li>
<li>Observable.of() : 여러 개의 값을 방출할 때 사용. 여기서 <code>observable2</code> 와 <code>observable3</code> 의 자료형 차이 주목!! observable2는 개별적인 값, observable3은 배열을 저장하고 있음</li>
<li>Observable.from() :  하나의 배열을 받아서 배열의 요소들을 순서대로 방출할 때 사용<ul>
<li><code>of()</code> 에 배열을 넣으면 <strong>배열 자체</strong>가 하나의 항목으로 방출되지만, <code>from()</code> 을 사용하면 <strong>배열의 요소</strong>들이 순서대로 방출된다!!</li>
</ul>
</li>
</ul>
<h2 id="subscription-구현해보기">Subscription 구현해보기</h2>
<pre><code class="language-swift">observable4.subscribe { event in
    print(event)
}</code></pre>
<p>Observable을 구독(subscribe)해서 Observable이 가지고 있는 값에 접근할 수 있다.</p>
<p><code>subscribe</code> 는 실제 값을 가지고 있는 게 아니라 이벤트를 전달해준다. (이벤트, 다음 이벤트, 다음 값, 그 다음 이벤트, 그 다음 값…)</p>
<pre><code>next(1)
next(2)
next(3)
next(4)
next(5)
completed</code></pre><p>위 코드를 실행해보면 다음과 같은 결과가 나온다. 실제 저장된 값이 아닌 이벤트가 출력되는 것을 볼 수 있다!</p>
<p>또한 Observable의 모든 값을 출력하고 나면 <code>completed</code> 라는 이벤트가 발생된다.</p>
<h3 id="전-실제-값을-얻고-싶은데요-🤔">전 실제 값을 얻고 싶은데요..? 🤔</h3>
<pre><code class="language-swift">observable4.subscribe { event in
    if let element = event.element {
        print(element)
    }
}</code></pre>
<p><code>element</code> 라는 프로퍼티를 사용하면 실제 값에 접근할 수 있다.</p>
<pre><code class="language-swift">observable.subscribe(onNext: { element in
    print(element)
})</code></pre>
<p>위 방법은 옵셔널 언래핑도 해줘야 하고 귀찮으니까..!!! <code>onNext</code> 를 사용하면 더 쉽게 값에 접근할 수 있다.</p>
<pre><code>// 결과
1
2
3
4
5</code></pre><h2 id="disposing-and-terminating">Disposing and Terminating</h2>
<p>subscription을 생성하면 subscriber가 리턴되는데, 이 subscriber는 특정 시퀀스를 관찰(observe)하고 있다.</p>
<p>언젠가 이 subscriber를 사용하지 않게 되면 이 구독을 해제해주어야 한다. 이를 dispose라고 함! 구독을 해제해주지 않으면 <strong>메모리 누수</strong> 같은 문제가 발생할 수 있기 때문에 꼭 dispose를 해주어야 한다. 🤯</p>
<pre><code class="language-swift">let subscription = observable.subscribe(onNext: { element in
    print(element)
})

subscription.dispose()</code></pre>
<p><code>dispose()</code> 라는 메소드를 사용해서 dispose를 해줄 수 있다. 하지만 위와 같이 subscription과 dispose를 따로따로 해주면 어느 시점에 dispose를 해줘야 할지 알기 어렵다.</p>
<pre><code class="language-swift">let disposeBag = DisposeBag()

Observable.of(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;)
    .subscribe {
        print($0)
    }
    .disposed(by: disposeBag)</code></pre>
<p>그래서 위처럼 subscribe와 disposed 메소드를 이어서 작성해주면  observable가 종료되는 시점에 dispose가 잘 실행될 수 있다.</p>
<p>여기서 <code>DisposeBag</code> 은 이름 그대로 disposable을 담은 가방이다. Dispose 해야 하는 리소스들이 많으면 일일이 <code>dispose()</code> 메소드를 호출해서 구독 해제를 시켜줘야 하는데 너무 귀찮고 번거로우니까!! DisposeBag에 담아주면 이 DisposeBag이 알아서 <code>dispose()</code> 메소드를 호출해준다.</p>
<p><a href="https://babbab2.tistory.com/186">RxSwift) Dispose /Disposable / DisposeBag 이해하기</a></p>
<h3 id="create로-observable를-생성하기">create로 Observable를 생성하기</h3>
<pre><code class="language-swift">Observable&lt;String&gt;.create { observer in
    observer.onNext(&quot;A&quot;)
    observer.onCompleted()
    observer.onNext(&quot;?&quot;)
    return Disposables.create()
}.subscribe(onNext: { print($0) }, onError: { print($0) }, onCompleted: { print(&quot;Completed&quot;) }, onDisposed: { print(&quot;Disposed&quot;) })
    .disposed(by: disposeBag)</code></pre>
<p><code>create</code> 메소드를 사용하면 <code>onNext</code> , <code>onCompleted</code> , <code>onError</code> 같은 메소드를 클로저 안에서 직접 호출할 수 있다.</p>
<pre><code>A
Completed
Disposed</code></pre><p>이때 onCompleted나 onError가 호출되면 Observable이 dispose 된다. 그래서 위 코드를 실행해보면 onCompleted 다음에 있는 <code>observer.onNext(&quot;?&quot;)</code> 는 방출이 되지 않는다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RxSwift - Introduction]]></title>
            <link>https://velog.io/@240-coding/RxSwift-Introduction</link>
            <guid>https://velog.io/@240-coding/RxSwift-Introduction</guid>
            <pubDate>Sun, 22 Oct 2023 14:12:16 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/240-coding/post/f01f1a58-f616-4d1d-b769-c02b6dc98f6c/image.png" alt="">
(Udemy의 Mastering RxSwift in iOS 강의를 듣고 정리한 글입니다.)</p>
<p>회사에서 개발하려면 MVVM이랑 RxSwift는 모르면 안 될 것 같아서 RxSwift 공부 시작..!! ^-ㅠ</p>
<h2 id="함수형-프로그래밍이란">함수형 프로그래밍이란?</h2>
<p>함수형 프로그래밍에서 모든 변수는 상수이기 때문에 값을 변경할 수 없다.</p>
<h3 id="mutable-state">mutable state</h3>
<p>mutable state는 누구나 값을 마음대로 변경할 수 있다는 것</p>
<p>⇒ concurrency (동시성) 관련 작업을 할 때 데드락과 같은 문제가 일어날 수 있다.</p>
<h3 id="immutable-state">Immutable state</h3>
<p>함수형 프로그래밍에서는 모든 것이 immutable state이기 때문에 값을 변경할 수 없다.</p>
<p>⇒ 데드락 등의 문제도 일어나지 않고, 다른 것들에 대한 의존 관계도 가지지 않는다!</p>
<h3 id="first-class-and-higher-order-function">First-class and higher-order function</h3>
<p><strong>고차함수(Higher-Order Function, HOF)</strong>는 아래 조건 중 하나 이상을 만족하는 함수를 말한다.</p>
<ul>
<li>하나 이상의 함수를 인자로 받는다.</li>
<li>함수를 결과로 반환한다.</li>
</ul>
<p>즉, <strong>함수를 다루는 함수</strong>라고 할 수 있음!</p>
<p><a href="https://medium.com/@la.place/higher-order-function-%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-1c61e0bea79"></a></p>
<h3 id="pure-function">Pure Function</h3>
<ul>
<li>같은 Input 값을 넣으면 항상 같은 Output 값이 나오는 함수</li>
<li>주어진 Input을 가지고 계산하는 것 이외에 프로그램에 다른 영향(side effect)를 미치면 안 된다.</li>
</ul>
<p>함수형 프로그래밍에서 모든 것은 HOF이고 Pure Function임!</p>
<h2 id="rxswift란">RxSwift란?</h2>
<p><img src="https://velog.velcdn.com/images/240-coding/post/bf4e46c8-c8fb-4fef-b96c-c880f8a8d576/image.png" alt=""></p>
<p>테이블 뷰, 탭 바, 정렬/필터 버튼이 있는 앱을 생각해보자</p>
<p>이 앱에는 이미지 다운로드, 탭 등등 다양한 action들이 있을 거고, 이런 액션들은 모두 비동기적으로 접근할 수 있어야 한다. 이때 Notification Center, 델리게이트 패턴, GCD, 클로저를 사용해야 하는데 얘네는 사실 <strong>비동기를 위해 완전히 설계된 것이 아님!!</strong> 그래서 <code>async</code> 같은 코드들을 더 추가해줘야 한다는 문제가 있다.</p>
<p>RxSwift를 사용하면 비동기 코드를 더욱 간결하고 유지하기 쉽게 작성할 수 있다 😎</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[iOS에서 데이터를 저장하는 다양한 방법들]]></title>
            <link>https://velog.io/@240-coding/iOS%EC%97%90%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EC%A0%80%EC%9E%A5%ED%95%98%EB%8A%94-%EB%8B%A4%EC%96%91%ED%95%9C-%EB%B0%A9%EB%B2%95%EB%93%A4</link>
            <guid>https://velog.io/@240-coding/iOS%EC%97%90%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EC%A0%80%EC%9E%A5%ED%95%98%EB%8A%94-%EB%8B%A4%EC%96%91%ED%95%9C-%EB%B0%A9%EB%B2%95%EB%93%A4</guid>
            <pubDate>Tue, 16 May 2023 11:22:35 GMT</pubDate>
            <description><![CDATA[<p>어느덧 졸업프로젝트가 막바지를 향하고 있다 🤯 스타트 때 열심히 주제를 고민하던 때가 엊그제 같은데..</p>
<p>다시 한 번 우리 팀의 프로젝트를 간단하게 소개하자면, 사용자가 영어로 말한 것을 STT로 변환하고 이를 기반으로 사용자의 영어 말하기를 분석해주는 앱이다.
<img src="https://velog.velcdn.com/images/240-coding/post/e352c26d-80e2-4c2a-af6f-0e81a19f3336/image.png" alt=""></p>
<p>프로젝트 개발 단계에서 가장 많이 신경쓰고 고민했던 부분은 앱 내에서 데이터를 저장하고 관리하는 것이었다.</p>
<p>열심히 UI들을 만들고 보니, 사용자 정보도 저장해야 하고, 이미지나 음성 파일들도 저장해야 하고… 생각보다도 데이터를 저장해야 하는 경우가 정말 다양했다.</p>
<p>실제로 iOS에서 데이터를 저장하는 방법은 굉장히 다양하다. 우리 프로젝트에서도 여러 방법을 사용해서 데이터를 저장했었다. 그래서 이 글에서 iOS에서 데이터를 저장하는 방법들을 우리 프로젝트에서 적용한 방식과 함께 소개하고자 한다. 👀</p>
<h2 id="1-사용자-정보를-저장할-때---userdefaults">1. 사용자 정보를 저장할 때 - UserDefaults</h2>
<p>우리 앱에서는 홈 화면뿐만 아니라 스피킹 분석 화면 등 사용자의 닉네임을 필요로 하는 화면이 여기저기에 있다.
<img src="https://velog.velcdn.com/images/240-coding/post/a80e535b-8141-467e-b9c2-40ab3683126e/image.png" alt=""></p>
<p>닉네임, 이메일, 자기소개 같은 사용자의 데이터를 저장하기 위해서 <code>UserDefaults</code> 를 사용하였다.</p>
<h3 id="userdefaults가-뭐지">UserDefaults가 뭐지?</h3>
<p>UserDefaults는 사용자의 기본 데이터베이스에 대한 인터페이스이다. 키-값 쌍으로 저장되며, 앱을 실행하면 영구적으로 데이터가 저장된다.</p>
<p>주로 간단한 사용자의 정보나 설정 등을 저장할 때 사용된다.</p>
<p> UserDefaults 클래스의 메소드를 사용해서  <code>Bool</code>, <code>Float</code>, <code>Double</code>, <code>Int</code>, <code>String</code>, <code>URL</code> 같은 기본 자료형 데이터를 쉽게 저장할 수 있다.</p>
<h3 id="데이터-저장하기">데이터 저장하기</h3>
<p>UserDefaults를 사용하기 위해서는 먼저 UserDefaults의 인스턴스를 생성해주어야 한다.</p>
<pre><code class="language-swift">let defaults = UserDefaults.standard</code></pre>
<p>그 다음 <code>set</code> 메소드를 사용하여 데이터와 데이터에 대한 고유한 키값을 함께 저장해주면 된다.</p>
<pre><code class="language-swift">defaults.set(25, forKey: &quot;Age&quot;)
defaults.set(true, forKey: &quot;UseTouchID&quot;)
defaults.set(CGFloat.pi, forKey: &quot;Pi&quot;)</code></pre>
<h3 id="데이터-가져오기">데이터 가져오기</h3>
<p>그럼 UserDefaults에 저장된 데이터는 다시 어떻게 가져올 수 있을까? 데이터를 가져오는 것도 역시 아주 간단하다. 단, 데이터를 읽을 때는 저장된 데이터의 타입과 맞는 메소드를 호출해야 한다.</p>
<p>대표적인 메소드들은 다음과 같다.</p>
<ul>
<li><code>integer(forKey:)</code><ul>
<li>key 값이 존재하면 데이터를 반환하고, 그렇지 않으면 0을 반환한다.</li>
</ul>
</li>
<li><code>bool(forKey:)</code><ul>
<li>key 값이 존재하면 데이터를 반환하고, 그렇지 않으면 false를 반환한다.</li>
</ul>
</li>
<li><code>float(forKey:)</code><ul>
<li>key 값이 존재하면 데이터를 반환하고, 그렇지 않으면 0.0을 반환한다.</li>
</ul>
</li>
<li><code>double(forKey:)</code><ul>
<li>key 값이 존재하면 데이터를 반환하고, 그렇지 않으면 0.0을 반환한다.</li>
</ul>
</li>
<li><code>object(forKey:)</code><ul>
<li><code>Any?</code> 를 반환하여 원하는 데이터 타입으로 형변환하여 사용할 수 있다.</li>
</ul>
</li>
</ul>
<h3 id="speaking에서-사용-방법">SpeaKing에서 사용 방법</h3>
<p>위 메소드들만 사용해도 문제는 없지만 약간의 불편한 점이 있다.</p>
<ol>
<li>UserDefaults를 사용할 때마다 <code>UserDefaults.standard</code> 에 접근해야 한다.</li>
<li>데이터의 키값을 기억하고 있어야 하고, 일일이 타이핑 해야 한다. 오타가 날 확률도 있다. 🤯</li>
</ol>
<p>그래서 우리 프로젝트에서는 더 편리하게 UserDefaults에 접근할 수 있도록 <code>UserDefaultsManager</code> 라는 커스텀 클래스를 만들어서 사용하였다.</p>
<pre><code class="language-swift">// UserDefaultsManager.swift

class UserDefaultsManager {
    enum UserDefaultsKeys: String, CaseIterable {
        case email
        case nickname
        case intro
    }

    static func setData&lt;T&gt;(value: T, key: UserDefaultsKeys) {
        let defaults = UserDefaults.standard
        defaults.set(value, forKey: key.rawValue)
    }

    static func getData&lt;T&gt;(type: T.Type, forKey: UserDefaultsKeys) -&gt; T? {
        let defaults = UserDefaults.standard
        let value = defaults.object(forKey: forKey.rawValue) as? T
        return value
    }

    static func removeData(key: UserDefaultsKeys) {
        let defaults = UserDefaults.standard
        defaults.removeObject(forKey: key.rawValue)
    }
}</code></pre>
<ul>
<li>클래스 내에서 <code>UserDefaultsKeys</code> 라는 열거형을 정의하여 필요한 키값을 일일이 타이핑하거나 기억할 필요 없이 편리하게 사용할 수 있도록 하였다.</li>
<li>데이터를 생성하고, 읽고, 삭제하는 메소드를 구현하여 부가적인 설정 없이 바로바로 데이터를 관리할 수 있도록 하였다.</li>
</ul>
<p><code>UserDefaultsManager</code> 클래스를 사용한 실제 예시는 다음과 같다.</p>
<pre><code class="language-swift">AF.request(url, method: .get, headers: headers)
            .validate()
            .responseDecodable(of: ProfileModel.self) { response in
                switch response.result {
                case .success(let response):
                    UserDefaultsManager.setData(value: response.result.email, key: .email)
                    UserDefaultsManager.setData(value: response.result.nickname, key: .nickname)
                    UserDefaultsManager.setData(value: response.result.intro, key: .intro)
                    completion()
                case .failure(let error):
                    debugPrint(error)
                }
            }</code></pre>
<p>⬆️ 서버에서 사용자 정보를 받아온 후 UserDefaults에 저장</p>
<pre><code class="language-swift">let nickname = UserDefaultsManager.getData(type: String.self, forKey: .nickname) ?? &quot;사용자&quot;</code></pre>
<p>⬆️ 홈 화면에서 사용자의 닉네임을 표시하기 위해 UserDefaults에 저장된 사용자 닉네임 데이터를 가져옴</p>
<h2 id="2-민감한-정보를-저장할-때---keychain">2. 민감한 정보를 저장할 때 - Keychain</h2>
<p>SpeaKing 서버의 API들은 호출할 때 사용자의 JWT 토큰을 request header에 넣어주도록 구현되어 있다. 따라서 앱에서 SpeaKing 서버 API를 호출하기 위해 사용자의 JWT 토큰을 어딘가에 저장해두어야 한다.</p>
<blockquote>
<p>🤔 토큰도 그냥 UserDefaults에 저장하면 되지 않을까?</p>
<p><strong>→ Noooo!!!!!!!!</strong></p>
</blockquote>
<p>UserDefaults의 데이터들은 property list 문서 (.plist)에 저장된다. 사용자가 특정 툴 등을 사용하면 UserDefaults에 쉽게 접근해서 이 데이터를 확인하고 수정할 수 있다.</p>
<p>따라서 UserDefaults에 사용자의 비밀번호나 토큰 등 민감한 데이터들을 저장하는 것은 보안상 좋은 선택이 아니다.</p>
<h3 id="keychain">Keychain?</h3>
<p>Keychain은 비밀번호나 암호키 등 작은 개인정보 등을 안전하게 저장할 수 있는 곳이다. keychain service의 API를 사용해서 키체인 항목을 추가, 삭제, 수정할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/a9825af0-83b9-4d92-8651-3c1efa320565/image.png" alt=""></p>
<p>데이터는 다음과 같은 과정으로 암호화되어 Keychain에 저장된다.</p>
<p><img src="https://velog.velcdn.com/images/240-coding/post/ee576ae1-a3df-49ee-b126-f8aaf8a512d0/image.png" alt=""></p>
<p>Keychain에 저장할 데이터를 키체인 아이템으로 감싼다. 그리고 아이템의 접근을 제어하고, 아이템을 검색할 수 있도록 공개적으로 볼 수 있는 속성 값들을 함께 정의한다. </p>
<p>Keychain service는 Keychain에서 데이터를 암호화하고 저장한다. 이때 Keychain은 디스크 내에 암호화되어 저장된 데이터베이스이다. 이후 아이템을 찾고 데이터를 복호화할 때에도 keychain service가 사용된다.</p>
<h3 id="keychain에-데이터-저장하기">Keychain에 데이터 저장하기</h3>
<p>이제 Keychain에 실제로 비밀번호를 저장하는 과정을 보겠다.</p>
<p><strong>1. 쿼리 생성하기</strong></p>
<pre><code class="language-swift">var query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
                            kSecAttrAccount as String: account,
                            kSecAttrServer as String: server,
                            kSecValueData as String: password]</code></pre>
<p>Keychain service를 사용할 때는 항상 먼저 쿼리를 생성해야 한다. 이때 쿼리는 현재 보고 있는 데이터에 대해 설명해주는 역할을 한다.</p>
<ul>
<li>kSecClass: keychain service에게 데이터의 종류를 알려주는 역할. 여기서 저장할 키체인 아이템은 인터넷 비밀번호이다.</li>
<li>kSecAttrAccount: 사용자 이름</li>
<li>kSecAttrServer: 이 계정 정보를 사용하는 서버의 도메인명</li>
<li>kSecValueData: 사용자 비밀번호</li>
</ul>
<p><strong>2. 키체인 아이템 추가하기</strong></p>
<p>위 쿼리를 사용해서 <code>SecItemAdd(_:_:)</code> 메소드를 호출하면 Keychain에 항목이 추가된다.</p>
<pre><code class="language-swift">let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) }</code></pre>
<p>이 메소드를 호출하면 keychain에 저장이 잘 되었는지 알려주는 결과 코드가 리턴된다. 이를 사용해서 정상적으로 저장되었는지 여부를 확인할 수 있다.</p>
<h3 id="데이터-수정삭제하기">데이터 수정/삭제하기</h3>
<p>키체인 항목을 수정하려면 먼저 키체인에 저장된 항목을 찾아야 한다. 따라서 데이터를 찾기 위한 쿼리를 생성한다.</p>
<pre><code class="language-swift">let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
                            kSecAttrServer as String: server]</code></pre>
<p>또한 새로 업데이트할 데이터의 내용에 대한 attribute도 함께 작성한다.</p>
<pre><code class="language-swift">let account = credentials.username
let password = credentials.password.data(using: String.Encoding.utf8)!
let attributes: [String: Any] = [kSecAttrAccount as String: account,
                                 kSecValueData as String: password]</code></pre>
<p>이 둘을 가지고 <code>SecItemUpdate(_:_:)</code> 메소드를 호출하면 키체인 아이템을 업데이트 할 수 있다.</p>
<pre><code class="language-swift">let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
guard status != errSecItemNotFound else { throw KeychainError.noPassword }
guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) }</code></pre>
<p>아이템을 추가할 때와 마찬가지로 메소드의 리턴 값을 사용해서 업데이트 성공 여부를 확인한다.</p>
<p>아이템을 삭제할 때는 검색 쿼리를 가지고 <code>SecItemDelete(_:)</code> 메소드를 호출하면 된다.</p>
<pre><code class="language-swift">let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) }</code></pre>
<h3 id="speaking에서-사용-방법-1">SpeaKing에서 사용 방법</h3>
<p>Keychain도 마찬가지로 더 쉽게 데이터를 관리하기 위해 <code>KeychainManager</code> 클래스를 만들어서 사용하였다.</p>
<pre><code class="language-swift">// KeychainManager.swift

class KeychainManager {
    enum KeychainError: Error {
        case duplicateEntry
        case noToken
        case unknown(OSStatus)
    }

    static func save(userId: String, token: Data) throws {
        let query: [String: AnyObject] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: userId as AnyObject,
            kSecValueData as String: token as AnyObject,
        ]

        let status = SecItemAdd(
            query as CFDictionary,
            nil
        )

        guard status != errSecDuplicateItem else {
            throw KeychainError.duplicateEntry
        }

        guard status == errSecSuccess else {
            throw KeychainError.unknown(status)
        }
    }

    static func get() -&gt; (userId: String, token: String)? {
        let query: [String: AnyObject] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecReturnAttributes as String: kCFBooleanTrue,
            kSecReturnData as String: kCFBooleanTrue,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]

        var result: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &amp;result)

        guard let data = result as? [String: AnyObject],
              let tokenData = data[kSecValueData as String] as? Data,
              let token = String(data: tokenData, encoding: String.Encoding.utf8),
              let userId = data[kSecAttrAccount as String] as? String
        else {
            return nil
        }

        return (userId, token)
    }

    static func delete() throws {
        let query: [String: AnyObject] = [
            kSecClass as String: kSecClassGenericPassword,
        ]

        let status = SecItemDelete(query as CFDictionary)

        guard status != errSecItemNotFound else { throw KeychainError.noToken }
        guard status == errSecSuccess else { throw KeychainError.unknown(status) }

    }
}</code></pre>
<p>우리 앱에서는 서버에서 지정한 사용자 ID와 JWT 토큰을 함께 저장해주었다.</p>
<pre><code class="language-swift">AF.request(url, method: .post, parameters: userInfo, encoder: JSONParameterEncoder.default)
            .validate()
            .responseDecodable(of: LoginResponseModel.self) { response in
                switch response.result {
                case .success(let response):
                    do {
                        try KeychainManager.save(
                            userId: &quot;\(response.result.userId)&quot;,
                            token: response.result.token.data(using: .utf8) ?? Data())
                    }
                    catch {
                        print(error)
                    }
                    completion(response)
                case .failure(let error):
                    print(debugPrint(error))
                }
            }</code></pre>
<p>⬆️ 로그인 API 호출 후 반환받은 사용자 ID와 JWT 토큰을 저장하는 예시</p>
<pre><code class="language-swift">// SceneDelegate.swift

if let token = KeychainManager.get()?.token {
    AuthService().authenticateToken(token) { isSuccess in
        if isSuccess {
            print(token)
            navigationController = UINavigationController(rootViewController: HomeViewController(profileService: ProfileService()))
        } else {
            do {
                try KeychainManager.delete()
            } catch {
                debugPrint(error)
            }
            navigationController = UINavigationController(rootViewController: LoginViewController(loginService: AuthService()))
        }
        window.rootViewController = navigationController
        window.makeKeyAndVisible()
        self.window = window
    }
} else {
    navigationController = UINavigationController(rootViewController: LoginViewController(loginService: AuthService()))
    window.rootViewController = navigationController
    window.makeKeyAndVisible()
    self.window = window
}
</code></pre>
<p>⬆️ SceneDelegate에서 JWT 토큰 여부에 따른 첫 화면을 설정하는 예시. 유효한 토큰이 Keychain 내에 저장되어 있으면 홈 화면을 띄우고, 그렇지 않으면 로그인 화면을 띄운다.</p>
<h2 id="3-파일을-저장할-때는-filemanager">3. 파일을 저장할 때는 FileManager</h2>
<p>우리 앱에서는 간단한 데이터를 넘어서 음성 녹음 파일도 저장해야 한다. 
iOS 앱은 각자 디바이스 내에 자신들만의 공간을 가지고 있다. 이 공간은 FileManager 클래스를 사용해서 접근할 수 있다.</p>
<h3 id="filemanager를-사용해서-새-파일-저장하기">FileManager를 사용해서 새 파일 저장하기</h3>
<p><strong>1. 파일을 저장할 경로에 접근한다.</strong></p>
<pre><code class="language-swift">let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)</code></pre>
<p>위 경로는 사용자의 문서 경로를 의미한다.</p>
<p><strong>2. 해당 경로에 파일을 추가하기 위해 새로운 경로를 추가한다.</strong></p>
<pre><code class="language-swift">let audioFile = path.appendingPathComponent(&quot;recording.wav&quot;)</code></pre>
<p><code>appendingPathComponent</code> 메소드를 사용하여 경로를 새롭게 추가한다.</p>
<p>음성 녹음의 경우는 <code>AVAudioRecorder</code> 의 <code>stop()</code> 메소드를 호출하여 선택한 경로에 음성 파일이 저장할 수 있다.</p>
<h2 id="4-음성-파일의-메타데이터-저장하기---realm">4. 음성 파일의 메타데이터 저장하기 - Realm</h2>
<p>음성 파일들을 재생하려면 음성 파일들의 경로를 알고 있어야 한다. 이런 경로들은 어떻게 저장할 수 있을까?</p>
<p>UserDefaults를 사용해서도 경로를 저장할 수는 있다. 하지만 우리 앱에서는 UserDefaults에서는 간단한 사용자 정보만을 저장하고, 음성 파일의 메타데이터는 다른 데이터베이스를 사용하여 저장하기로 하였다.</p>
<p>iOS에서 사용 가능한 로컬 데이터베이스는 Core Data, SQLite, Realm 등 굉장히 다양한데, 우리 프로젝트에서는 Realm을 사용하였다.</p>
<h3 id="realm이-뭐지">Realm이 뭐지?</h3>
<p>Realm은 모바일 환경에 최적화된 데이터베이스이다. Realm을 사용하면 iOS 디바이스 내에 데이터를 영구적으로 저장할 수 있다. 추가로 설치해야 한다는 부담이 있지만, Core Data, SQLite보다 작업 속도가 빠르고 사용법이 간단하기 때문에 이를 사용하기로 하였다.</p>
<h3 id="realm-사용해보기">Realm 사용해보기</h3>
<p>참고로 Realm을 사용하려면 먼저 Realm을 설치해야 한다. <a href="https://www.mongodb.com/docs/realm/sdk/swift/install/">Realm 공식 문서</a>에서 설치 방법을 확인할 수 있다.</p>
<p><strong>1. 데이터 모델 정의하기</strong></p>
<p>Realm을 설치한 다음에는 데이터베이스에 저장하기 위한 모델을 정의한다.</p>
<pre><code class="language-swift">import Foundation
import RealmSwift

class Audio: Object {
    @Persisted var id: String
    @Persisted var url: String = &quot;&quot;

    override class func primaryKey() -&gt; String? {
        return &quot;id&quot;
    }

    convenience init(id: String, url: String) {
        self.init()
        self.id = id
        self.url = url
    }
}</code></pre>
<p>Realm 데이터베이스에 데이터를 저장할 때는 각 데이터마다 고유한 primary key도 함께 저장되어야 한다. 여기서는 <code>id</code> 를 primary key로 사용하기 위해 <code>primaryKey()</code> 메소드를 사용하였다.</p>
<p><strong>2. 데이터 추가하기</strong></p>
<pre><code class="language-swift">let audioData = Audio(id: audioId, url: audioRecorder.url.absoluteString)

try! realm.write {
    realm.add(audioData)
}</code></pre>
<p>⬆️ 음성 파일의 고유 ID와 파일 경로를 저장하는 예시</p>
<p>추가할 데이터에 대한 객체를 만들고 realm의 <code>add()</code> 메소드를 사용하여 데이터를 추가한다. 이때 파일을 쓰는 작업은 <code>realm</code> 의 권한으로 접근해야 한다. 따라서 <code>add()</code> 메소드 호출 전에 <code>realm.write{ }</code> 을 호출한다.</p>
<p><strong>3. 데이터 읽기</strong></p>
<pre><code class="language-swift">realm.objects(Audio.self)</code></pre>
<p>⬆️ Realm에 저장된 모든 Audio 타입의 데이터를 불러오는 예시</p>
<h2 id="비교해보자">비교해보자!</h2>
<p>이렇게 iOS에서의 다양한 데이터 저장 방법을 알아보았다.</p>
<p>마지막으로, 다시 한 번 각 방법들을 비교해보자.</p>
<ul>
<li><strong>UserDefault vs Keychain</strong><ul>
<li>앱을 제거해도 Keychain의 정보는 유지된다. 반면 UserDefaults의 값들을 지워진다.</li>
</ul>
</li>
<li><strong>Realm vs Core Data</strong><ul>
<li>Realm은 설치가 쉽고 무제한으로 사용할 수 있다.</li>
<li>Realm이 실행 속도가 더 빠르다. 하지만 서드 파티이기 때문에 Core Data보다 앱의 크기가 커진다.</li>
</ul>
</li>
</ul>
<p>이번 졸업 프로젝트 덕분에 그동안 이름만 들어봤던 다양한 데이터 저장 방법들을 마음껏 활용해볼 수 있어서 좋았다. 또한, 데이터 저장 방식을 고민하고 선택하는 과정도 재미있었다. 프로젝트도 마지막까지 잘 마무리 하고 싶다. 파이팅! 💪</p>
<hr>
<h2 id="✏️-참고-자료">✏️ 참고 자료</h2>
<p><a href="https://developer.apple.com/documentation/foundation/userdefaults">https://developer.apple.com/documentation/foundation/userdefaults</a></p>
<p><a href="https://developer.apple.com/documentation/security/keychain_services/keychain_items">https://developer.apple.com/documentation/security/keychain_services/keychain_items</a></p>
<p><a href="https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/storing_keys_in_the_keychain">https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/storing_keys_in_the_keychain</a></p>
<p><a href="https://developer.apple.com/documentation/security/keychain_services/keychain_items/updating_and_deleting_keychain_items">https://developer.apple.com/documentation/security/keychain_services/keychain_items/updating_and_deleting_keychain_items</a></p>
<p><a href="https://developer.apple.com/documentation/foundation/filemanager">https://developer.apple.com/documentation/foundation/filemanager</a></p>
<p><a href="https://realm.io/realm-swift/">https://realm.io/realm-swift/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로그래머스 - 단어 변환]]></title>
            <link>https://velog.io/@240-coding/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%8B%A8%EC%96%B4-%EB%B3%80%ED%99%98</link>
            <guid>https://velog.io/@240-coding/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%8B%A8%EC%96%B4-%EB%B3%80%ED%99%98</guid>
            <pubDate>Fri, 31 Mar 2023 12:18:47 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-swift">import Foundation

func solution(_ begin:String, _ target:String, _ words:[String]) -&gt; Int {    
    if !words.contains(target) {
        return 0
    }

    var answer = 500
    var visited = Set&lt;String&gt;()

    func isClose(_ string1: String, _ string2: String) -&gt; Bool {        
        let a = Array(string1), b = Array(string2)
        let diffCount = (0..&lt;string1.count).filter {a[$0] != b[$0] }.count

        return diffCount == 1
    }

    func dfs(_ now: String, _ count: Int) {
        visited.insert(now)

        for next in words {
            if isClose(now, next) &amp;&amp; !visited.contains(next) {
                if next == target {
                    answer = min(answer, count)
                    return
                }
                dfs(next, count + 1)
            }
        }
    }

    dfs(begin, 1)

    return answer
}</code></pre>
<ul>
<li>DFS를 사용해서 현재 단어와 한 글자만 다른 단어를 계속해서 탐색하고, <code>target</code> 단어에 도달하면 탐색을 종료한다.<ul>
<li>단어의 길이와 개수의 수가 작기 때문에 완전 탐색을 이용해서 한 글자만 다른 단어들을 찾아도 된다.</li>
</ul>
</li>
<li>단어를 한 글자씩 쪼개서 탐색해야 할 줄 알고 고민했었는데.. 입력값의 크기가 작으면 완전 탐색을 사용해도 된다!! 까먹지 말자!!!</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로그래머스 - 네트워크]]></title>
            <link>https://velog.io/@240-coding/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC</link>
            <guid>https://velog.io/@240-coding/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC</guid>
            <pubDate>Thu, 30 Mar 2023 16:06:02 GMT</pubDate>
            <description><![CDATA[<h3 id="인접-리스트를-사용한-풀이">인접 리스트를 사용한 풀이</h3>
<pre><code class="language-swift">import Foundation

func solution(_ n:Int, _ computers:[[Int]]) -&gt; Int {
    var answer = 0
    var graph: [[Int]] = Array(repeating: [], count: n)
    var visited = Array(repeating: false, count: n)

    // 인접 리스트 만들기
    for i in 0..&lt;n {
        for j in 0..&lt;n {
            if computers[i][j] == 1 &amp;&amp; i != j {
                graph[i].append(j)
                graph[j].append(i)
            }
        }
    }

    func dfs(_ num: Int) {
        visited[num] = true

        for next in graph[num] {
            if !visited[next] {
                dfs(next)
            }
        }
    }

    for i in 0..&lt;n {
        if !visited[i] {
            answer += 1
            dfs(i)
        }
    }

    return answer
}</code></pre>
<h3 id="인접-행렬을-사용한-풀이">인접 행렬을 사용한 풀이</h3>
<pre><code class="language-swift">import Foundation

func solution(_ n:Int, _ computers:[[Int]]) -&gt; Int {
    var answer = 0
    var visited = Array(repeating: false, count: n)

    func dfs(_ num: Int) {
        visited[num] = true

        for i in 0..&lt;n {
            if computers[num][i] == 1 &amp;&amp; !visited[i] {
                dfs(i)
            }
        }
    }

    for i in 0..&lt;n {
        if !visited[i] {
            answer += 1
            dfs(i)
        }
    }

    return answer
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로그래머스 - 모음사전]]></title>
            <link>https://velog.io/@240-coding/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%AA%A8%EC%9D%8C%EC%82%AC%EC%A0%84</link>
            <guid>https://velog.io/@240-coding/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%AA%A8%EC%9D%8C%EC%82%AC%EC%A0%84</guid>
            <pubDate>Thu, 23 Mar 2023 15:59:25 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-swift">import Foundation

func solution(_ word:String) -&gt; Int {
    let wordIndex = [&quot;A&quot;: 0, &quot;E&quot;: 1, &quot;I&quot;: 2, &quot;O&quot;: 3, &quot;U&quot;: 4]
    var rate = [1]
    var answer = word.count


    // 각 자리 증가율 계산 및 저장
    (1...4).forEach { rate.append(rate[$0 - 1] * 5 + 1) }
    rate.reverse()

    // 단어 차례 계산
    for (i, ch) in word.enumerated() {
        answer += rate[i] * wordIndex[String(ch)]!
    }

    return answer
}</code></pre>
<p><a href="https://seongho96.tistory.com/50">이 블로그 풀이</a>를 참고해서 풀었다.</p>
<p>입력 값의 수가 적을 때는 완전탐색을 쓰되, 차례대로 하나씩 쓰면서 규칙을 찾아보자..!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 - 탑 (2493)]]></title>
            <link>https://velog.io/@240-coding/%EB%B0%B1%EC%A4%80-%ED%83%91-2493</link>
            <guid>https://velog.io/@240-coding/%EB%B0%B1%EC%A4%80-%ED%83%91-2493</guid>
            <pubDate>Thu, 23 Mar 2023 13:56:04 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-swift">let N = Int(readLine()!)!
let height = readLine()!.split(separator: &quot; &quot;).map { Int(String($0))! }
var stack = [0]
var answer = &quot;0 &quot;

for i in 1..&lt;N {
    while !stack.isEmpty &amp;&amp; height[stack.last!] &lt; height[i] {
        stack.removeLast()
    }
    answer += &quot;\(stack.isEmpty ? 0 : stack.last! + 1) &quot;
    stack.append(i)
}

print(answer)</code></pre>
<ul>
<li>왼쪽부터 각 탑들의 높이를 확인한다.<ul>
<li>지금까지 봤던 탑 중 가장 오른쪽에 있는 탑보다 높이가 낮다면 다음에 이 탑이 신호를 수신 받을 가능성이 있다.</li>
<li>지금까지 봤던 탑들보다 높이가 더 크다면, 이전에 있던 낮은 탑들은 더 이상 신호를 수신받을 수 없다.<ul>
<li>따라서 이 탑보다 높이가 작은 탑들은 스택에서 모두 pop해준다.</li>
</ul>
</li>
</ul>
</li>
<li>위 과정을 거친 후 스택의 top에 저장된 탑이 신호를 수신받는 탑이 된다. 만약 스택이 비어있다면 신호를 수신받는 탑이 없는 것이다.<ul>
<li>이 값을 정답 변수에 저장한 후 현재 보고 있는 탑을 스택에 push한다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 - MVP 다이아몬드 (Easy) (20413)]]></title>
            <link>https://velog.io/@240-coding/%EB%B0%B1%EC%A4%80-MVP-%EB%8B%A4%EC%9D%B4%EC%95%84%EB%AA%AC%EB%93%9C-Easy-20413</link>
            <guid>https://velog.io/@240-coding/%EB%B0%B1%EC%A4%80-MVP-%EB%8B%A4%EC%9D%B4%EC%95%84%EB%AA%AC%EB%93%9C-Easy-20413</guid>
            <pubDate>Wed, 22 Mar 2023 10:28:33 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-swift">let grade = [&quot;B&quot;: 0, &quot;S&quot;: 1, &quot;G&quot;: 2, &quot;P&quot;: 3, &quot;D&quot;: 4]
let N = Int(readLine()!)!
var answer = 0

var money = [0] + readLine()!.split(separator: &quot; &quot;).map { Int(String($0))! }
// 각 등급 최대 가능한 과금액 저장
for i in 0..&lt;4 {
    money[i] = money[i + 1] - 1
}

let input = Array(readLine()!)
var maxMoney = Array(repeating: 0, count: N + 1) // 상민이 각 달 최대 과금액 저장

for i in 1...N {
    let nowGrade = grade[String(input[i - 1])]!
    maxMoney[i] = nowGrade != 4 ? money[nowGrade] - maxMoney[i - 1] : money[nowGrade]
    answer += maxMoney[i]
}

print(answer)</code></pre>
<p>다이아몬드 등급을 제외한 모든 등급마다 최대로 과금 가능한 금액 (다음 등급 - 1)을 저장한다.</p>
<p>그리고 모든 달마다 <code>현재 등급 - 이전 달 최대 과금 금액</code> 을 계산하고, 이 값을 정답 변수의 값에 더해준다.</p>
]]></description>
        </item>
    </channel>
</rss>