<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>안녕 젤리, iOS</title>
    <link>https://jellabean.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 11 Apr 2026 12:12:28 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>젤라빈</managingEditor>
    <image>
      <title>안녕 젤리, iOS</title>
      <url>https://tistory1.daumcdn.net/tistory/3144305/attach/4fe70c190d6b4197a5ee07cd334efa3d</url>
      <link>https://jellabean.tistory.com</link>
    </image>
    <item>
      <title>구독할 때마다 달라지는 Observable? Cold vs Hot 개념 정리</title>
      <link>https://jellabean.tistory.com/30</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;RxSwift의 Cold vs Hot 개념 정리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS 개발을 하다 보면 비동기 작업을 다룰 일이 많다.&lt;br /&gt;예를 들어, 네트워크 요청을 하거나, 사용자의 버튼 탭 이벤트를 처리하고,&lt;br /&gt;그에 따른 화면 UI를 업데이트하는 작업은 거의 매일 마주치는 일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때 자주 쓰게 되는 것이 Observable이다.&lt;br /&gt;그런데 개발을 하다 보면 다음과 같은 의문이 생길 때가 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;같은 Observable인데 구독할 때마다 동작이 다르네?&amp;rdquo;&lt;br /&gt;&amp;ldquo;subscribe만 했을 뿐인데 왜 두 번 요청이 나가지?&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 현상은 대부분 &lt;b&gt;Observable이 Cold인지 Hot인지&lt;/b&gt;에 따라 달라지는 동작 때문이다.&lt;br /&gt;이번 글에서는 RxSwift의 Cold Observable과 Hot Observable의 차이를 명확하게 짚어보고,&lt;br /&gt;실제 iOS 프로젝트에서 어떻게 활용하면 좋을지도 함께 정리해보자.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Cold Observable: 구독할 때마다 새롭게 시작된다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cold Observable은 말 그대로 &lt;b&gt;&amp;lsquo;차가운&amp;rsquo;, 아직 시작되지 않은 상태&lt;/b&gt;를 의미한다.&lt;br /&gt;이 옵저버블은 &lt;b&gt;누군가가 구독하기 전까지는 아무 일도 하지 않는다.&lt;/b&gt;&lt;br /&gt;그리고 구독이 발생하면 그때 비로소 동작을 시작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말하면, &lt;b&gt;녹화 방송&lt;/b&gt;을 틀어주는 것과 같다.&lt;br /&gt;각 구독자는 자기만의 타이밍으로 방송을 처음부터 보기 시작한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예제&lt;/h3&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;let coldObservable = Observable&amp;lt;Int&amp;gt;.create { observer in
    print(&quot;  새로운 스트림 시작&quot;)
    observer.onNext(Int.random(in: 1...100))
    observer.onCompleted()
    return Disposables.create()
}

coldObservable.subscribe(onNext: { print(&quot;  구독자 A: \($0)&quot;) })
coldObservable.subscribe(onNext: { print(&quot;  구독자 B: \($0)&quot;) })&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;출력 예시&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;  새로운 스트림 시작
  구독자 A: 42
  새로운 스트림 시작
  구독자 B: 91
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;subscribe가 발생할 때마다 새로운 데이터 스트림이 만들어진다.&lt;/li&gt;
&lt;li&gt;네트워크 요청, 파일 읽기, 데이터베이스 쿼리 등 대부분의 작업은 Cold Observable로 표현된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hot Observable: 이미 방송 중인 상태&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hot Observable은 &lt;b&gt;누군가가 구독하기 전에도 이미 데이터가 흘러가고 있는 상태&lt;/b&gt;다.&lt;br /&gt;즉, &lt;b&gt;실시간 생방송&lt;/b&gt;을 생각하면 된다.&lt;br /&gt;방송이 시작된 후에 들어오면, 그 시점 이후의 데이터만 받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hot Observable을 직접 만들기 위해서는 보통 Subject를 사용한다.&lt;br /&gt;가장 기본적인 예는 PublishSubject이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예제&lt;/h3&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;let subject = PublishSubject&amp;lt;String&amp;gt;()

subject.onNext(&quot;  방송 시작 전&quot;) // 구독자가 없기 때문에 무시된다

subject.subscribe(onNext: { print(&quot;  A: \($0)&quot;) })
subject.onNext(&quot;  방송 1&quot;)

subject.subscribe(onNext: { print(&quot;  B: \($0)&quot;) })
subject.onNext(&quot;  방송 2&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;출력 예시&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;  A:   방송 1
  A:   방송 2
  B:   방송 2
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구독자 A는 방송 1부터 받았고,&lt;/li&gt;
&lt;li&gt;구독자 B는 방송 2부터 받았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 &lt;b&gt;구독 타이밍에 따라 받는 데이터가 다르다&lt;/b&gt;는 것이 Hot Observable의 특징이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Cold와 Hot, 언제 어떻게 써야 할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS 개발에서는 두 가지 타입의 Observable을 모두 자주 만나게 된다.&lt;br /&gt;중요한 건 상황에 맞게 선택하고, 필요하다면 Cold를 Hot으로 변환해서 써야 한다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상황 Cold Observable Hot Observable&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;네트워크 요청&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;버튼 탭 이벤트&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;센서 스트림, NotificationCenter&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;캐시된 데이터 재사용&lt;/td&gt;
&lt;td&gt;✅ (share 필요)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Cold를 Hot처럼 바꾸려면?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RxSwift에서는 share() 또는 multicast() 연산자를 통해&lt;br /&gt;Cold Observable을 Hot처럼 공유할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 타이머처럼 반복되는 이벤트를 여러 구독자가 함께 보려면 아래와 같이 쓴다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;let shared = Observable&amp;lt;Int&amp;gt;.interval(.seconds(1), scheduler: MainScheduler.instance)
    .take(3)
    .share()

shared.subscribe(onNext: { print(&quot;  A: \($0)&quot;) })

DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
    shared.subscribe(onNext: { print(&quot;  B: \($0)&quot;) })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;출력 예시&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;  A: 0
  A: 1
  A: 2
  B: 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;share() 덕분에 &lt;b&gt;Cold였던 Observable이 Hot처럼 공유되는&lt;/b&gt; 것을 확인할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리: 지금 내 Observable은 어떤 타입일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RxSwift를 쓰다 보면 구독자가 여러 명일 때, 의도치 않게 네트워크 요청이 중복되거나,&lt;br /&gt;이벤트를 놓치는 문제가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때 Observable이 Cold인지, Hot인지 구분하고&lt;br /&gt;필요하다면 share()나 Subject로 조정해주는 것이 중요하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 작성 중인 Observable은 구독할 때마다 새로 실행되고 있는가?&lt;br /&gt;아니면 이미 흘러가는 이벤트를 받아야 하는 상황인가?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 질문을 기준으로 Cold와 Hot을 구분해보자.&lt;br /&gt;RxSwift의 흐름이 더 명확해지고, 디버깅도 훨씬 쉬워질 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 포스팅에서는 BehaviorSubject, ReplaySubject, 그리고 share(replay:scope:)를 활용해&lt;br /&gt;&lt;b&gt;데이터 캐싱 및 이벤트 공유&lt;/b&gt;를 어떻게 구성할 수 있는지도 다뤄볼 예정이다.&lt;/p&gt;</description>
      <category>ios#rxswift#observable</category>
      <author>젤라빈</author>
      <guid isPermaLink="true">https://jellabean.tistory.com/30</guid>
      <comments>https://jellabean.tistory.com/30#entry30comment</comments>
      <pubDate>Tue, 24 Jun 2025 22:13:24 +0900</pubDate>
    </item>
    <item>
      <title>비동기 처리의 새로운 패러다임, Combine 가이드</title>
      <link>https://jellabean.tistory.com/29</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;Combine, 왜 필요한가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS 개발을 하다 보면 &lt;b&gt;비동기 작업&lt;/b&gt;을 처리해야 하는 경우가 많다.&lt;br /&gt;예를 들어, &lt;b&gt;네트워크 요청, UI 이벤트 처리, 데이터 스트림 관리&lt;/b&gt; 등이 대표적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 이를 해결하기 위해 &lt;b&gt;Completion Handler, NotificationCenter, KVO, Delegate 패턴&lt;/b&gt; 등을 사용했다.&lt;br /&gt;하지만 이 방식들은 &lt;b&gt;콜백 지옥(Callback Hell)&lt;/b&gt;을 유발하거나, 코드가 복잡해지는 문제가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Combine은 이런 문제를 해결하고, &lt;b&gt;데이터 스트림을 선언적으로 관리할 수 있도록 도와주는 프레임워크&lt;/b&gt;다.&lt;br /&gt;이번 글에서는 &lt;b&gt;Combine의 개념, 기본 사용법, 활용하는 방법&lt;/b&gt;까지 하나씩 알아보자&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Combine이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Combine은 Apple이 iOS 13부터 도입한 비동기 &amp;amp; 반응형 프로그래밍 프레임워크&lt;/b&gt;다.&lt;br /&gt;RxSwift와 같은 &lt;b&gt;리액티브 프로그래밍 방식&lt;/b&gt;을 지원하며, &lt;b&gt;데이터 스트림을 다루는 강력한 기능&lt;/b&gt;을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Combine을 활용하면:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;비동기 작업을 선언적으로 처리&lt;/b&gt;할 수 있다.&lt;br /&gt;✅ &lt;b&gt;데이터 스트림을 조합하고 변환&lt;/b&gt;하는 것이 쉬워진다.&lt;br /&gt;✅ &lt;b&gt;콜백 기반 코드의 복잡성을 줄이고, 가독성을 높일 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Combine의 핵심 개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Combine에서 가장 중요한 개념 3가지는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1️⃣ &lt;b&gt;Publisher(퍼블리셔)&lt;/b&gt; &amp;rarr; 데이터를 방출하는 역할&lt;br /&gt;2️⃣ &lt;b&gt;Subscriber(서브스크라이버)&lt;/b&gt; &amp;rarr; 데이터를 구독하여 처리&lt;br /&gt;3️⃣ &lt;b&gt;Operator(연산자)&lt;/b&gt; &amp;rarr; 데이터를 변환하거나 필터링&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 개념만 이해하면 &lt;b&gt;Combine을 쉽게 활용할 수 있다!&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Publisher(퍼블리셔)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Publisher는 &lt;b&gt;데이터 스트림을 생성하는 역할&lt;/b&gt;을 한다.&lt;br /&gt;쉽게 말해, 이벤트가 발생할 때마다 데이터를 방출하는 객체다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Combine에서 제공하는 대표적인 퍼블리셔는 다음과 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ Just (단일 값 방출)&lt;/h4&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;import Combine

let publisher = Just(&quot;Hello, Combine!&quot;)

let subscriber = publisher.sink { value in
    print(&quot;  값 수신: \(value)&quot;)
}
// 출력:   값 수신: Hello, Combine!
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Just는 &lt;b&gt;한 번만 값을 방출하고 끝나는 Publisher&lt;/b&gt;다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ Timer (반복적인 값 방출)&lt;/h4&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;import Combine

let timerPublisher = Timer.publish(every: 1.0, on: .main, in: .common)
    .autoconnect()

let subscription = timerPublisher.sink { value in
    print(&quot;⏳ 현재 시간: \(value)&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Timer를 사용하면 &lt;b&gt;1초마다 새로운 값을 방출&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;2. Subscriber(서브스크라이버)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Publisher가 데이터를 방출하면, 이를 &lt;b&gt;받아서 처리하는 역할&lt;/b&gt;을 하는 것이 &lt;b&gt;Subscriber&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Combine에서는 &lt;b&gt;두 가지 방식&lt;/b&gt;으로 값을 구독할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ sink (간단한 구독)&lt;/h4&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;import Combine

let publisher = Just(&quot;Hello, Combine!&quot;)

let subscription = publisher.sink { value in
    print(&quot;  받은 값: \(value)&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sink는 가장 간단한 구독 방법으로, &lt;b&gt;Publisher가 데이터를 방출할 때마다 클로저를 실행&lt;/b&gt;한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ assign (객체 속성에 직접 할당)&lt;/h4&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;import Combine

class ViewModel {
    @Published var message: String = &quot;&quot;
}

let viewModel = ViewModel()
let publisher = Just(&quot;Hello, Combine!&quot;)

let subscription = publisher.assign(to: &amp;amp;viewModel.$message)

print(viewModel.message) // Hello, Combine!
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;assign을 사용하면, &lt;b&gt;Publisher의 데이터를 객체의 속성에 직접 연결할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Operator(연산자)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Combine의 가장 강력한 기능 중 하나는 &lt;b&gt;Operator(연산자)&lt;/b&gt;를 활용해 데이터 스트림을 변형할 수 있다는 것이다.&lt;br /&gt;RxSwift의 map, filter, flatMap과 유사한 개념이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ map (데이터 변환)&lt;/h4&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;import Combine

let publisher = Just(10)
    .map { $0 * 2 }

let subscription = publisher.sink { value in
    print(&quot;  변환된 값: \(value)&quot;)
}
// 출력:   변환된 값: 20
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;map을 사용하면 &lt;b&gt;방출된 데이터를 변환&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ filter (조건에 맞는 값만 방출)&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;import Combine

let numbers = [1, 2, 3, 4, 5].publisher

let subscription = numbers
    .filter { $0 % 2 == 0 }
    .sink { value in
        print(&quot;✅ 짝수: \(value)&quot;)
    }
// 출력: ✅ 짝수: 2, ✅ 짝수: 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;filter는 &lt;b&gt;조건을 만족하는 데이터만 방출&lt;/b&gt;하도록 필터링한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ combineLatest (두 개의 Publisher 결합)&lt;/h4&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;import Combine

let publisher1 = PassthroughSubject&amp;lt;String, Never&amp;gt;()
let publisher2 = PassthroughSubject&amp;lt;Int, Never&amp;gt;()

let subscription = publisher1.combineLatest(publisher2)
    .sink { value in
        print(&quot;  결합된 값: \(value)&quot;)
    }

publisher1.send(&quot;A&quot;)
publisher2.send(1)
publisher1.send(&quot;B&quot;)
publisher2.send(2)

// 출력:
//   결합된 값: (&quot;A&quot;, 1)
//   결합된 값: (&quot;B&quot;, 1)
//   결합된 값: (&quot;B&quot;, 2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;combineLatest를 사용하면 &lt;b&gt;두 개의 Publisher가 방출하는 최신 값을 결합하여 제공&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Combine 활용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Combine은 &lt;b&gt;비동기 네트워크 요청&lt;/b&gt;을 처리하는 데 특히 강력하다.&lt;br /&gt;기존 URLSession과 함께 Combine을 사용하면 다음과 같이 구현할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;import Combine

struct API {
    static func fetchData() -&amp;gt; AnyPublisher&amp;lt;string, error=&quot;&quot;&amp;gt; {
        let url = URL(string: &quot;https://jsonplaceholder.typicode.com/todos/1&quot;)!
        
        return URLSession.shared.dataTaskPublisher(for: url)
            .map { String(data: $0.data, encoding: .utf8) ?? &quot;&quot; }
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
}

let subscription = API.fetchData()
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print(&quot;✅ 완료&quot;)
        case .failure(let error):
            print(&quot;❌ 오류 발생: \(error)&quot;)
        }
    }, receiveValue: { value in
        print(&quot;  받은 데이터: \(value)&quot;)
    })
&amp;lt;/string,&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ URLSession.dataTaskPublisher를 사용하면 &lt;b&gt;네트워크 요청을 Combine으로 간편하게 처리&lt;/b&gt;할 수 있다.&lt;br /&gt;✅ map, receive(on:), eraseToAnyPublisher() 등을 사용하여 &lt;b&gt;데이터를 변환하고 최적화&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 &lt;b&gt;Combine의 기본 개념과 주요 기능&lt;/b&gt;을 가볍게 살펴봤다.&lt;br /&gt;Combine은 &lt;b&gt;비동기 작업을 선언적으로 처리할 수 있도록 도와주는 강력한 프레임워크&lt;/b&gt;이며,&lt;br /&gt;이를 활용하면 &lt;b&gt;데이터 흐름을 더욱 직관적으로 관리하고, 비동기 코드의 복잡성을 줄일 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;오늘 살펴본 핵심 개념 정리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Publisher&lt;/b&gt; &amp;rarr; 데이터를 방출하는 객체&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Subscriber&lt;/b&gt; &amp;rarr; 데이터를 구독하여 처리하는 객체&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Operator&lt;/b&gt; &amp;rarr; 데이터를 변형하고 필터링하는 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, &lt;b&gt;Combine의 진짜 강점은 연산자를 활용한 데이터 조작과, 기존의 UIKit 및 SwiftUI와의 결합에 있다.&lt;/b&gt;&lt;br /&gt;이번 글에서는 기본적인 개념과 사용법을 다뤘지만, &lt;b&gt;다음에는 더 심도 있는 주제들을 다뤄봐야겠다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;지금까지 Combine을 사용해 본 적이 없다면, 간단한 프로젝트에서 직접 활용해 보자!&lt;/b&gt;&lt;/p&gt;</description>
      <category>iOS</category>
      <author>젤라빈</author>
      <guid isPermaLink="true">https://jellabean.tistory.com/29</guid>
      <comments>https://jellabean.tistory.com/29#entry29comment</comments>
      <pubDate>Tue, 18 Mar 2025 20:00:11 +0900</pubDate>
    </item>
    <item>
      <title>초저지연 실시간 스트리밍, WebRTC(Web Real-Time Communication)</title>
      <link>https://jellabean.tistory.com/28</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;WebRTC(Web Real-Time Communication)는 &lt;b&gt;브라우저, 모바일 앱 간에 직접 오디오, 비디오, 데이터 전송을 가능하게 하는 실시간 통신 기술&lt;/b&gt;이다.&lt;br /&gt;RTMP나 HLS 같은 스트리밍 프로토콜과 달리, &lt;b&gt;서버를 거치지 않고 P2P(Peer-to-Peer) 방식으로 데이터를 주고받을 수 있다는 것이 가장 큰 특징&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 WebRTC는 &lt;b&gt;화상 회의, 라이브 스트리밍, 온라인 게임, 원격 제어 등 실시간 상호작용이 필요한 서비스에서 널리 사용&lt;/b&gt;되고 있다.&lt;br /&gt;이번 글에서는 &lt;b&gt;WebRTC의 개념, 동작 방식, 그리고 기존 스트리밍 기술과의 차이점&lt;/b&gt;까지 자세히 알아보자&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WebRTC의 동작 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebRTC는 기본적으로 &lt;b&gt;P2P(피어 투 피어) 방식&lt;/b&gt;으로 작동하지만, &lt;b&gt;네트워크 환경에 따라 중계 서버를 활용하는 방식도 가능&lt;/b&gt;하다.&lt;br /&gt;기본적인 흐름은 다음과 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  WebRTC의 기본 흐름&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1️⃣ &lt;b&gt;시그널링(Signaling) 과정&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebRTC 자체에는 &lt;b&gt;P2P 연결을 설정하는 기능이 없기 때문에&lt;/b&gt;, 먼저 &lt;b&gt;서버를 통해 연결 정보를 교환해야 한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;이 과정에서 &lt;b&gt;ICE(Interactive Connectivity Establishment)&lt;/b&gt; 프로토콜을 사용하여 피어 간 연결을 설정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2️⃣ &lt;b&gt;P2P 연결(Peer-to-Peer Connection) 설정&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트 간에 &lt;b&gt;직접적인 연결(P2P)&lt;/b&gt;을 시도한다.&lt;/li&gt;
&lt;li&gt;하지만 &lt;b&gt;방화벽, NAT(Network Address Translation) 등의 문제로 직접 연결이 어려운 경우 TURN 서버를 통해 데이터를 중계할 수도 있다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3️⃣ &lt;b&gt;미디어 및 데이터 전송(Media &amp;amp; Data Transfer)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연결이 설정되면, &lt;b&gt;오디오, 비디오, 텍스트 데이터&lt;/b&gt;를 &lt;b&gt;서버 없이 직접 주고받을 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;데이터는 SRTP(Secure Real-time Transport Protocol)을 사용하여 암호화된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식 덕분에 &lt;b&gt;초저지연(1초 미만) 실시간 통신이 가능&lt;/b&gt;하며, 영상 통화, 라이브 스트리밍, 게임 음성 채팅 같은 서비스에서 강력한 장점을 가진다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WebRTC의 특징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebRTC가 &lt;b&gt;RTMP나 HLS보다 실시간 스트리밍에 더 적합한 이유&lt;/b&gt;는 다음과 같은 특징 덕분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;초저지연(ultra-low latency) 실시간 통신&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;HLS는 10&lt;s&gt;30초, RTMP는 2&lt;/s&gt;5초 정도의 딜레이가 발생하지만&lt;/b&gt;, WebRTC는 &lt;b&gt;1초 미만의 지연 시간&lt;/b&gt;을 제공한다.&lt;/li&gt;
&lt;li&gt;실시간 화상 회의, 온라인 게임, 원격 제어 등에 적합하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;P2P(Peer-to-Peer) 연결&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버를 거치지 않고 &lt;b&gt;브라우저 간 직접 통신&lt;/b&gt;이 가능하다.&lt;/li&gt;
&lt;li&gt;대역폭을 절약할 수 있으며, 서버 비용이 줄어든다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;다양한 미디어 형식 지원&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;오디오, 비디오, 텍스트 데이터를 실시간으로 주고받을 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Opus, VP8, VP9, H.264 등의 코덱을 기본 지원한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;보안 강화&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 데이터가 &lt;b&gt;DTLS(SSL 기반의 Datagram Transport Layer Security) 및 SRTP를 사용하여 암호화&lt;/b&gt;된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;E2EE(End-to-End Encryption, 종단 간 암호화) 적용 가능&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;브라우저 및 모바일 기본 지원&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebRTC는 &lt;b&gt;HTML5와 연동되어 Chrome, Firefox, Safari 등 대부분의 브라우저에서 네이티브 지원&lt;/b&gt;된다.&lt;/li&gt;
&lt;li&gt;모바일(iOS, Android)에서도 사용 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WebRTC의 장점과 단점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebRTC는 여러 장점을 가지지만, 몇 가지 단점도 존재한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ WebRTC의 장점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;초저지연 스트리밍 가능&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebRTC는 &lt;b&gt;딜레이가 1초 미만&lt;/b&gt;으로, RTMP, HLS보다 훨씬 빠른 실시간 통신이 가능하다.&lt;/li&gt;
&lt;li&gt;예: 온라인 수업, 원격 회의, 라이브 채팅, 원격 게임 스트리밍&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;서버 부하 절감&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;P2P 기반으로 동작하기 때문에 &lt;b&gt;서버 없이도 스트리밍 가능&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;트래픽이 많은 경우에도 서버 비용을 절약할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;강력한 보안 기능 제공&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 데이터는 기본적으로 암호화된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;웹 브라우저에서 기본 지원&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;별도의 플러그인 없이 HTML5에서 WebRTC를 활용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;❌ WebRTC의 단점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ &lt;b&gt;네트워크 환경에 따라 품질이 달라짐&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;P2P 방식이므로 &lt;b&gt;연결된 사용자들의 인터넷 상태에 따라 품질이 달라질 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NAT/방화벽 문제&lt;/b&gt;로 인해 직접 연결이 안 되는 경우, &lt;b&gt;TURN 서버를 이용해야 하며 추가적인 비용이 발생&lt;/b&gt;한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ &lt;b&gt;대규모 방송에는 부적합&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;1:1 또는 소규모 그룹 통신에는 최적화되어 있지만, 수천~수백만 명이 보는 방송에는 적합하지 않다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;YouTube, Netflix 같은 대규모 스트리밍 서비스에서는 HLS/CDN 기반 스트리밍을 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ &lt;b&gt;네트워크 최적화 필요&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebRTC는 네트워크 트래픽을 자동으로 조절하지만, &lt;b&gt;고품질 영상 전송을 위해서는 추가적인 네트워크 최적화가 필요하다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WebRTC의 사용 사례&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebRTC는 &lt;b&gt;초저지연 실시간 통신&lt;/b&gt;이 필요한 다양한 서비스에서 활용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;Google Meet / Zoom / Microsoft Teams&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebRTC를 사용하여 &lt;b&gt;웹 브라우저 기반 화상 회의 제공&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;Discord / Xbox Live / PlayStation Network&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebRTC를 활용한 &lt;b&gt;실시간 음성 채팅 기능&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;Facebook Messenger / WhatsApp / FaceTime&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;모바일 기반 영상 통화&lt;/b&gt;에서 WebRTC 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;온라인 교육 &amp;amp; 원격 제어&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;실시간 강의, 원격 코딩 인터뷰, 원격 데스크톱 공유&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WebRTC vs RTMP vs HLS 비교&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 260px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;특징&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;WebRTC&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;RTMP&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;HLS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;지연 시간&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;  0.1~1초 (초저지연)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;⏳ 2~5초 (낮음)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;  10~30초 (높음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;연결 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;P2P (서버 필요 없음)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;지속적인 TCP 연결&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;HTTP 기반 조각 파일 전송&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;네트워크 부하&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;낮음 (직접 연결)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;중간 (서버 연결 유지)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;높음 (대량의 HTTP 요청 발생)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;대규모 방송&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;❌ 어려움 (TURN 서버 필요)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;✅ 가능 (송출용으로 적합)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;✅ 매우 적합 (CDN 활용 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;어댑티브 스트리밍&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;❌ 지원 안 함&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;❌ 지원 안 함&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;✅ 지원 (Adaptive Bitrate)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;보안&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;✅ DTLS, SRTP 암호화 기본 제공&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;❌ 기본적으로 암호화 없음&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;✅ AES-128, DRM 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;브라우저 지원&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;✅ 기본 지원 (HTML5)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;❌ Flash 기반 (현재 대부분 미지원)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;✅ HTML5 기본 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;플랫폼 지원&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;✅ 웹, 모바일, 데스크톱&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;✅ 방송 송출 프로그램 (OBS 등)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;✅ 웹, 모바일, 스마트 TV&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;적용 예시&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;화상 회의, 게임 음성 채팅, 원격 제어&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;실시간 방송 송출 (YouTube Live, Twitch)&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;VOD, 넷플릭스, YouTube 스트리밍&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;정리:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;WebRTC&lt;/b&gt; &amp;rarr; &lt;b&gt;화상 채팅, 게임, 초저지연 실시간 커뮤니케이션&lt;/b&gt;에 적합&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RTMP&lt;/b&gt; &amp;rarr; &lt;b&gt;방송 송출(입력) 프로토콜&lt;/b&gt;로 여전히 강력한 선택지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HLS&lt;/b&gt; &amp;rarr; &lt;b&gt;대규모 스트리밍(출력) 및 다양한 디바이스 지원&lt;/b&gt;에 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebRTC는 &lt;b&gt;초저지연 실시간 스트리밍을 가능하게 하는 강력한 기술&lt;/b&gt;이다.&lt;br /&gt;&lt;b&gt;서버 없이 P2P로 데이터를 주고받을 수 있으며, 웹 브라우저에서도 기본 지원&lt;/b&gt;된다는 점에서 활용성이 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ WebRTC가 필요한 경우:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;즉각적인 반응이 중요한 서비스 (예: 화상 채팅, 원격 회의, 게임 음성 채팅)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;별도의 서버 없이 P2P 기반 실시간 데이터 전송이 필요할 때&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;현재&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;RTMP로 송출하고, HLS로 변환하여 제공하는 방식&lt;/b&gt;이 가장 일반적이지만,&lt;br /&gt;&lt;b&gt;즉각적인 반응이 중요한 경우 WebRTC를 활용하는 것이 최선&lt;/b&gt;이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebRTC를 직접 활용하여, &lt;b&gt;진정한 실시간 커뮤니케이션을 경험해 보자!&lt;/b&gt;&lt;/p&gt;</description>
      <category>영상 스트리밍</category>
      <author>젤라빈</author>
      <guid isPermaLink="true">https://jellabean.tistory.com/28</guid>
      <comments>https://jellabean.tistory.com/28#entry28comment</comments>
      <pubDate>Mon, 17 Mar 2025 20:00:26 +0900</pubDate>
    </item>
    <item>
      <title>현대 스트리밍의 표준, HLS(HTTP Live Streaming)</title>
      <link>https://jellabean.tistory.com/27</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;HLS란 무엇인가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HLS(HTTP Live Streaming)는 &lt;b&gt;Apple에서 개발한 HTTP 기반의 미디어 스트리밍 프로토콜&lt;/b&gt;이다.&lt;br /&gt;기존의 RTMP가 &lt;b&gt;저지연 스트리밍&lt;/b&gt;에 최적화되어 있다면, HLS는 &lt;b&gt;안정성, 보안, 다양한 디바이스 지원&lt;/b&gt;을 위해 설계되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 &lt;b&gt;YouTube, Netflix, Twitch, Disney+ 등 대부분의 스트리밍 플랫폼에서 HLS를 표준으로 사용&lt;/b&gt;하고 있다.&lt;br /&gt;이번 글에서는 HLS의 개념, 동작 방식, 그리고 RTMP와의 차이점까지 자세히 살펴보자!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HLS의 동작 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HLS는 RTMP처럼 &lt;b&gt;지속적인 TCP 연결을 유지하는 방식이 아니라&lt;/b&gt;, &lt;b&gt;비디오 데이터를 작은 조각(Chunks)으로 나누어 HTTP를 통해 전송&lt;/b&gt;하는 방식이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  HLS의 기본 흐름&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1️⃣ &lt;b&gt;미디어 파일을 여러 개의 작은 조각(Chunk)으로 분할&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어, &lt;b&gt;10초 길이의 비디오를 2초 단위로 나눈다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;각각의 조각은 MPEG-TS 포맷으로 저장된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2️⃣ &lt;b&gt;플레이리스트(Manifest) 파일을 생성&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;.m3u8 파일을 생성하여 &lt;b&gt;비디오 조각들의 목록을 관리&lt;/b&gt;한다.&lt;/li&gt;
&lt;li&gt;플레이어는 .m3u8 파일을 읽고, 해당 조각을 순차적으로 다운로드하여 재생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3️⃣ &lt;b&gt;클라이언트가 HTTP 요청을 통해 필요한 조각만 가져와 재생&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;플레이어는 네트워크 상태에 따라 &lt;b&gt;자동으로 품질을 조절(Adaptive Bitrate Streaming)&lt;/b&gt;하여 최적의 영상을 보여준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식을 통해 &lt;b&gt;안정적인 스트리밍을 제공&lt;/b&gt;할 수 있으며, 다양한 디바이스에서 HLS를 지원할 수 있게 된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HLS의 특징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HLS가 현재 &lt;b&gt;가장 널리 사용되는 스트리밍 방식&lt;/b&gt;이 된 이유는 다음과 같은 장점 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;HTTP 기반 전송&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;일반적인 웹 서버(Nginx, Apache)로 HLS 스트리밍을 제공 가능&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;별도의 RTMP 서버 없이, &lt;b&gt;CDN(Content Delivery Network)과 쉽게 연동 가능&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;적응형 비트레이트 스트리밍 (ABR, Adaptive Bitrate Streaming)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네트워크 상태에 따라 자동으로 &lt;b&gt;고해상도&amp;harr;저해상도 변경&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;예를 들어, 1080p &amp;rarr; 720p &amp;rarr; 480p 해상도로 유동적으로 변경 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;다양한 디바이스 지원&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;iOS, macOS, Android, Windows 등 &lt;b&gt;모든 현대적인 웹 브라우저 및 모바일 기기에서 지원&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;HTML5 기반 플레이어에서도 네이티브 지원됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;보안 및 DRM 지원&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;AES-128 암호화&lt;/b&gt;를 지원하여 &lt;b&gt;미디어 콘텐츠 보호 가능&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;DRM(Digital Rights Management)과 연동 가능 (예: Widevine, FairPlay)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;캐싱 &amp;amp; CDN 최적화&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP 기반이기 때문에 &lt;b&gt;CDN(Content Delivery Network)과 쉽게 통합 가능&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;전 세계 사용자들에게 빠르고 안정적인 미디어 제공 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HLS의 장점과 단점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HLS는 여러 장점을 가지지만, 몇 가지 단점도 존재한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ HLS의 장점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;끊김 없는 스트리밍&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP 기반으로 &lt;b&gt;서버와 지속적인 연결을 유지할 필요 없음&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;일시적인 네트워크 문제 발생 시에도 빠르게 복구 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;어댑티브 스트리밍(Adaptive Bitrate)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네트워크 상태에 따라 자동으로 해상도를 조정하여 &lt;b&gt;끊김 없이 재생 가능&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;범용적인 지원&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;iOS, Android, 웹 브라우저, 스마트 TV 등에서 기본 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;보안 기능 강화&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DRM과 암호화를 지원하여 &lt;b&gt;콘텐츠 불법 다운로드 방지 가능&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;❌ HLS의 단점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ &lt;b&gt;높은 지연 시간 (Latency)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HLS는 비디오를 조각 단위로 전송하기 때문에 &lt;b&gt;RTMP보다 지연 시간이 길다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;기본적으로 &lt;b&gt;10~30초의 딜레이&lt;/b&gt;가 발생할 수 있음.&lt;/li&gt;
&lt;li&gt;하지만 &lt;b&gt;Low Latency HLS(LL-HLS)&lt;/b&gt;가 등장하면서 이 문제를 해결하는 중이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ &lt;b&gt;실시간 인터랙션 어려움&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;라이브 방송에서 시청자와의 즉각적인 상호작용이 어렵다.&lt;/li&gt;
&lt;li&gt;예: 라이브 채팅, 게임 스트리밍, 스포츠 중계&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ &lt;b&gt;서버에서 미디어 변환 필요&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비디오를 조각으로 변환하는 과정이 필요하므로, &lt;b&gt;인코딩 부담이 크다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;실시간 스트리밍에서는 &lt;b&gt;추가적인 서버 리소스가 필요&lt;/b&gt;할 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HLS의 사용 사례&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 &lt;b&gt;대부분의 스트리밍 서비스가 HLS 기반&lt;/b&gt;으로 동작하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;YouTube &amp;amp; Netflix&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HLS를 기반으로 다양한 해상도의 영상을 제공&lt;/li&gt;
&lt;li&gt;Adaptive Bitrate Streaming 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;Twitch &amp;amp; Facebook Live&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RTMP로 송출된 영상을 &lt;b&gt;HLS로 변환하여 시청자에게 제공&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;예: 방송자는 RTMP로 송출 &amp;rarr; 서버에서 HLS로 변환 &amp;rarr; 시청자는 HLS로 재생&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;iOS &amp;amp; macOS 기본 지원&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Apple이 개발한 프로토콜이기 때문에, &lt;b&gt;iOS 및 macOS에서 기본적으로 HLS를 지원&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;HTML5 기반 웹 스트리밍&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;별도의 플러그인 없이 웹 브라우저에서 HLS 스트리밍 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  RTMP vs HLS, 어떤 차이가 있을까?&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 97px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;RTMP&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;HLS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;지연 시간&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;2~5초 (낮음)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;10~30초 (높음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;연결 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;지속적인 TCP 연결&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;HTTP 기반 조각 파일 전송&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;플레이어 지원&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;Flash 기반 (현재 대부분 미지원)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;대부분의 브라우저에서 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;적용 예시&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;실시간 방송 (게임, 스포츠)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;VOD, 방송 송출 (YouTube, Netflix)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;결론:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;RTMP는 송출(입력)에 적합&lt;/b&gt; (낮은 지연 시간)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HLS는 재생(출력)에 적합&lt;/b&gt; (다양한 디바이스 지원, CDN 최적화)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 &lt;b&gt;대부분의 서비스는 RTMP로 송출한 후, HLS로 변환하여 재생&lt;/b&gt;하는 방식으로 운영되고 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HLS는 현재 가장 널리 사용되는 &lt;b&gt;HTTP 기반의 스트리밍 프로토콜&lt;/b&gt;로, &lt;b&gt;안정성, 적응형 비트레이트, 보안성&lt;/b&gt; 측면에서 강력한 장점을 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;높은 지연 시간(Latency)&lt;/b&gt;이 단점으로 작용할 수 있으며, 이를 해결하기 위해 &lt;b&gt;Low Latency HLS(LL-HLS)&lt;/b&gt; 같은 최신 기술이 발전하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;HLS는 언제 사용하면 좋을까?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다양한 해상도로 안정적인 &lt;b&gt;VOD(다시보기), 영화 스트리밍&lt;/b&gt;을 제공하고 싶을 때&lt;/li&gt;
&lt;li&gt;CDN을 활용하여 &lt;b&gt;대규모 사용자에게 스트리밍을 제공해야 할 때&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;RTMP vs HLS, 어떻게 선택해야 할까?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;실시간 송출(입력)은 RTMP&lt;/b&gt;,&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재생(출력)은 HLS&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HLS가 대세로 자리 잡은 지금, &lt;b&gt;이 프로토콜을 활용하여 최적의 스트리밍 환경을 구축해보자&lt;/b&gt;&lt;/p&gt;</description>
      <category>영상 스트리밍</category>
      <category>HLS</category>
      <author>젤라빈</author>
      <guid isPermaLink="true">https://jellabean.tistory.com/27</guid>
      <comments>https://jellabean.tistory.com/27#entry27comment</comments>
      <pubDate>Fri, 14 Mar 2025 20:00:35 +0900</pubDate>
    </item>
    <item>
      <title>iOS에서 RTMP 기반 라이브 스트리밍 구현하기</title>
      <link>https://jellabean.tistory.com/25</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;라이브 스트리밍, 어떻게 동작할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유튜브, 트위치, 인스타그램 라이브 같은 서비스에서 실시간으로 방송을 송출하려면 &lt;b&gt;RTMP(Real-Time Messaging Protocol)&lt;/b&gt;가 필요하다.&lt;br /&gt;하지만 iOS 앱에서 직접 RTMP 스트리밍을 구현하려면 &lt;b&gt;미디어 캡처, 인코딩, 전송&lt;/b&gt; 등의 과정이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;b&gt;iOS에서 RTMP 기반 라이브 스트리밍이 동작하는 과정과 실무 적용 방법&lt;/b&gt;을 알아보자!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;iOS에서 RTMP 라이브 스트리밍의 기본 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS에서 RTMP 스트리밍을 구현하려면 기본적으로 다음 &lt;b&gt;4가지 단계&lt;/b&gt;가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1️⃣ &lt;b&gt;카메라 &amp;amp; 마이크 입력 받기&lt;/b&gt;&lt;br /&gt;2️⃣ &lt;b&gt;비디오 &amp;amp; 오디오 데이터 인코딩&lt;/b&gt;&lt;br /&gt;3️⃣ &lt;b&gt;RTMP 서버로 실시간 전송&lt;/b&gt;&lt;br /&gt;4️⃣ &lt;b&gt;뷰어(시청자)에서 재생하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 각 단계별로 자세히 살펴보자!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1️⃣ 카메라 &amp;amp; 마이크 입력 받기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, &lt;b&gt;AVCaptureSession&lt;/b&gt;을 사용하여 카메라와 마이크에서 데이터를 가져온다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import AVFoundation

class LiveStreamManager {
    private let captureSession = AVCaptureSession()
    private var videoOutput = AVCaptureVideoDataOutput()
    private var audioOutput = AVCaptureAudioDataOutput()
    
    func setupCaptureSession() {
        captureSession.sessionPreset = .hd1280x720
        
        //   카메라 입력 설정
        guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front),
              let videoInput = try? AVCaptureDeviceInput(device: camera) else { return }
        captureSession.addInput(videoInput)
        
        //   마이크 입력 설정
        guard let microphone = AVCaptureDevice.default(for: .audio),
              let audioInput = try? AVCaptureDeviceInput(device: microphone) else { return }
        captureSession.addInput(audioInput)

        //   비디오 &amp;amp; 오디오 출력 추가
        videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: &quot;videoQueue&quot;))
        audioOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: &quot;audioQueue&quot;))
        captureSession.addOutput(videoOutput)
        captureSession.addOutput(audioOutput)
        
        captureSession.startRunning()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;설명&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AVCaptureSession: 비디오 &amp;amp; 오디오 캡처 관리&lt;/li&gt;
&lt;li&gt;AVCaptureDeviceInput: 카메라 및 마이크 입력 설정&lt;/li&gt;
&lt;li&gt;AVCaptureVideoDataOutput, AVCaptureAudioDataOutput: 실시간 영상 &amp;amp; 오디오 데이터를 캡처&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2️⃣ 비디오 &amp;amp; 오디오 인코딩&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RTMP 서버로 데이터를 전송하려면 &lt;b&gt;H.264 (비디오) / AAC (오디오)&lt;/b&gt; 코덱으로 압축해야 한다.&lt;br /&gt;이 과정은 &lt;b&gt;VideoToolbox&lt;/b&gt;와 &lt;b&gt;AudioToolbox&lt;/b&gt;를 활용해 구현할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  비디오 인코딩 (H.264)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비디오 프레임을 &lt;b&gt;H.264&lt;/b&gt; 포맷으로 변환해야 한다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;import VideoToolbox

extension LiveStreamManager: AVCaptureVideoDataOutputSampleBufferDelegate {
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
        
        var encodedData: Data?
        let status = VTCompressionSessionEncodeFrame(
            compressionSession,
            imageBuffer: pixelBuffer,
            presentationTimeStamp: CMTimeMake(value: 0, timescale: 1),
            duration: CMTime.invalid,
            frameProperties: nil,
            infoFlagsOut: nil,
            outputHandler: { status, flags, sampleBuffer in
                if status == noErr, let sampleBuffer = sampleBuffer {
                    encodedData = extractEncodedData(from: sampleBuffer)
                }
            }
        )
        
        if let data = encodedData {
            sendToRTMPServer(data)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;설명&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;VTCompressionSessionEncodeFrame(): H.264 인코딩&lt;/li&gt;
&lt;li&gt;CMSampleBufferGetImageBuffer(): 프레임을 픽셀 버퍼로 변환&lt;/li&gt;
&lt;li&gt;sendToRTMPServer(): 인코딩된 데이터를 RTMP 서버로 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  오디오 인코딩 (AAC)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오디오 데이터는&lt;b&gt; AAC (Advanced Audio Codec)&lt;/b&gt;로 변환해야 한다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;import AudioToolbox

extension LiveStreamManager: AVCaptureAudioDataOutputSampleBufferDelegate {
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        guard let blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) else { return }
        
        var audioData = Data()
        CMBlockBufferCopyDataBytes(blockBuffer, atOffset: 0, dataLength: CMBlockBufferGetDataLength(blockBuffer), destination: &amp;amp;audioData)
        
        sendToRTMPServer(audioData)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;설명&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CMSampleBufferGetDataBuffer(): 오디오 데이터 추출&lt;/li&gt;
&lt;li&gt;CMBlockBufferCopyDataBytes(): 오디오 데이터를 바이트 배열로 변환&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3️⃣ RTMP 서버로 전송하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 인코딩된 데이터를 &lt;b&gt;RTMP 서버&lt;/b&gt;로 전송해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RTMP는 &lt;b&gt;FLV(Flexible Live Video) 포맷&lt;/b&gt;을 사용하여 데이터를 송출한다.&lt;br /&gt;즉, &lt;b&gt;H.264 비디오 + AAC 오디오&lt;/b&gt; 데이터를 &lt;b&gt;RTMP 메시지로 패킹하여 서버에 전송&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RTMP 스트리밍을 위해서는 라이브러리(LFLiveKit, HaishinKit) 를 사용할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ RTMP 연결 설정&lt;/h4&gt;
&lt;pre id=&quot;code_1739500808095&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Network

class RTMPSocket {
    private var connection: NWConnection?
    
    func connect(to server: String, port: UInt16) {
        connection = NWConnection(host: NWEndpoint.Host(server), port: NWEndpoint.Port(rawValue: port)!, using: .tcp)
        connection?.stateUpdateHandler = { newState in
            switch newState {
            case .ready:
                print(&quot;RTMP 서버 연결 성공&quot;)
            case .failed(let error):
                print(&quot;연결 실패: \(error)&quot;)
            default:
                break
            }
        }
        connection?.start(queue: .global())
    }
    
    func sendData(_ data: Data) {
        connection?.send(content: data, completion: .contentProcessed({ error in
            if let error = error {
                print(&quot;데이터 전송 실패: \(error)&quot;)
            }
        }))
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;4917&quot; data-start=&quot;4908&quot; data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;설명&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4996&quot; data-start=&quot;4918&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4951&quot; data-start=&quot;4918&quot;&gt;NWConnection을 사용해 RTMP 서버에 연결&lt;/li&gt;
&lt;li data-end=&quot;4996&quot; data-start=&quot;4952&quot;&gt;sendData(_:) 메서드로 인코딩된 H.264/AAC 데이터를 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ HaishinKit 라이브러리 활용 (추천)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HaishinKit&lt;/b&gt;은 Swift 기반의 RTMP 라이브러리로, 간단하게 RTMP 스트리밍을 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;Podfile 추가&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;pod 'HaishinKit'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;RTMP 스트리밍 시작&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;import HaishinKit

class LiveStreamManager {
    private let rtmpConnection = RTMPConnection()
    private let rtmpStream: RTMPStream
    
    init() {
        rtmpStream = RTMPStream(connection: rtmpConnection)
    }

    func startStreaming() {
        rtmpConnection.connect(&quot;rtmp://your-server/live&quot;)
        rtmpStream.publish(&quot;stream-key&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;설명&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RTMPConnection(): RTMP 서버와 연결&lt;/li&gt;
&lt;li&gt;rtmpStream.publish(&quot;stream-key&quot;): 스트리밍 시작&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4️⃣ 시청자가 스트림을 볼 수 있도록 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RTMP 스트림을 송출하면, 클라이언트에서 이를 &lt;b&gt;재생&lt;/b&gt;해야 한다.&lt;br /&gt;시청자가 iOS 기기에서 스트림을 볼 수 있도록, &lt;b&gt;AVPlayer&lt;/b&gt;를 사용하여 스트림을 재생하자.&lt;/p&gt;
&lt;pre id=&quot;code_1739500887830&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import AVKit

class LiveStreamPlayerViewController: UIViewController {
    private var player: AVPlayer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let streamURL = URL(string: &quot;rtmp://your-server/live/stream-key&quot;)!
        let playerItem = AVPlayerItem(url: streamURL)
        player = AVPlayer(playerItem: playerItem)
        
        let playerLayer = AVPlayerLayer(player: player)
        playerLayer.frame = view.bounds
        view.layer.addSublayer(playerLayer)
        
        player?.play()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;5668&quot; data-start=&quot;5659&quot; data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;설명&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;5772&quot; data-start=&quot;5669&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;5699&quot; data-start=&quot;5669&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;AVPlayer&lt;/span&gt;를 사용해 RTMP 스트림을 재생&lt;/li&gt;
&lt;li data-end=&quot;5739&quot; data-start=&quot;5700&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;AVPlayerItem(url:)&lt;/span&gt;을 통해 RTMP URL을 입력&lt;/li&gt;
&lt;li data-end=&quot;5772&quot; data-start=&quot;5740&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;playerLayer&lt;/span&gt;를 추가하여 스트리밍 영상 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS에서 &lt;b&gt;RTMP 스트리밍을 직접 구현&lt;/b&gt;하려면 여러 단계가 필요하지만, 핵심 흐름은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1️⃣ AVCaptureSession을 사용해 카메라 &amp;amp; 마이크 데이터 캡처&lt;br /&gt;2️⃣ VideoToolbox, AudioToolbox를 활용해 H.264 &amp;amp; AAC 인코딩&lt;br /&gt;3️⃣ NWConnection을 사용해 RTMP 서버로 데이터 전송&lt;br /&gt;4️⃣ AVPlayer를 사용해 iOS에서 스트리밍 영상 재생&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이제 iOS 앱에서 직접 RTMP 라이브 스트리밍을 구현할 수 있다!&lt;/b&gt;&lt;br /&gt;RTMP는 저지연 실시간 스트리밍에 적합하지만, WebRTC나 HLS 같은 대체 기술도 존재한다.&lt;br /&gt;프로젝트에 따라 적절한 스트리밍 방식을 선택하는 것이 중요하며, &lt;b&gt;RTMP는 특히 송출(입력) 용도로 강력한 선택지&lt;/b&gt;가 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 바로 &lt;b&gt;RTMP 서버를 설정하고, 직접 데이터를 송출하는 코드를 테스트&lt;/b&gt;해 보자&lt;br /&gt;스스로 구축한 스트리밍 시스템을 통해 실시간 방송의 원리를 깊이 이해할 수 있을 것이다.&lt;/p&gt;</description>
      <category>영상 스트리밍</category>
      <author>젤라빈</author>
      <guid isPermaLink="true">https://jellabean.tistory.com/25</guid>
      <comments>https://jellabean.tistory.com/25#entry25comment</comments>
      <pubDate>Thu, 13 Mar 2025 20:00:01 +0900</pubDate>
    </item>
    <item>
      <title>실시간 스트리밍의 핵심, RTMP(Real-Time Messaging Protocol)</title>
      <link>https://jellabean.tistory.com/26</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;RTMP란 무엇인가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브 스트리밍을 구현하다 보면&lt;b&gt; RTMP(Real-Time Messaging Protocol)&lt;/b&gt;라는 용어를 한 번쯤 들어봤을 것이다.&lt;br /&gt;RTMP는 &lt;b&gt;저지연(Low Latency) 실시간 스트리밍&lt;/b&gt;을 위해 설계된 프로토콜로, 현재도 YouTube Live, Twitch, Facebook Live 같은 플랫폼에서 &lt;b&gt;방송 송출&lt;/b&gt;에 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;b&gt;RTMP가 무엇인지, 어떻게 동작하는지&lt;/b&gt;, 그리고 &lt;b&gt;실무에서 어떻게 활용할 수 있는지&lt;/b&gt;까지 깊이 있게 알아보자!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RTMP의 동작 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RTMP는 &lt;b&gt;클라이언트(방송 송출자)&lt;/b&gt;와 &lt;b&gt;서버(스트리밍 플랫폼)&lt;/b&gt; 간의 &lt;b&gt;지속적인 TCP 연결&lt;/b&gt;을 통해 오디오 및 비디오 데이터를 전송한다.&lt;br /&gt;스트리밍이 시작되면, 다음과 같은 과정이 진행된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  RTMP의 기본 흐름&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1️⃣ &lt;b&gt;연결 설정 (Handshaking)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트(송출 프로그램)가 RTMP 서버와 TCP(포트 1935)로 연결을 수립한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2️⃣ &lt;b&gt;데이터 전송 (Data Transmission)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오디오 &amp;amp; 비디오 데이터를 &lt;b&gt;Chunk Stream&lt;/b&gt; 방식으로 분할해 실시간 전송한다.&lt;/li&gt;
&lt;li&gt;비디오는 H.264, 오디오는 AAC 코덱을 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3️⃣ &lt;b&gt;플레이백 (Playback)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시청자(클라이언트)가 RTMP 데이터를 수신하고, 이를 플레이어에서 재생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식을 통해 &lt;b&gt;라이브 방송이 지연 없이 송출&lt;/b&gt;될 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RTMP의 특징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RTMP가 실시간 스트리밍에서 많이 사용되는 이유는 다음과 같은 특징 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;저지연 스트리밍&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네트워크 지연을 최소화하여 &lt;b&gt;빠른 응답성&lt;/b&gt;을 제공한다.&lt;/li&gt;
&lt;li&gt;특히 인터랙티브한 콘텐츠(게임 스트리밍, 실시간 이벤트)에 적합하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;지속적인 연결 유지&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TCP 기반으로 안정적인 스트리밍이 가능하다.&lt;/li&gt;
&lt;li&gt;비디오, 오디오, 메타데이터를 &lt;b&gt;동기화&lt;/b&gt;하여 전송할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;데이터 압축 &amp;amp; 최적화&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;효율적인 전송을 위해 &lt;b&gt;H.264(비디오), AAC(오디오)&lt;/b&gt; 코덱을 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;다양한 활용 가능&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;라이브 방송, 영상 회의, 원격 교육 등 &lt;b&gt;실시간 데이터 전송이 필요한 곳&lt;/b&gt;에 적용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RTMP의 장점과 단점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RTMP를 사용할 때 고려해야 할 장점과 단점도 알아보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ RTMP의 장점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;낮은 지연 시간&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;라이브 방송에서 &lt;b&gt;즉각적인 반응&lt;/b&gt;이 필요한 경우 적합하다.&lt;/li&gt;
&lt;li&gt;예: e스포츠 중계, 게임 스트리밍&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;간단한 설정&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오랜 기간 사용되어 왔기 때문에 &lt;b&gt;다양한 플랫폼과 호환&lt;/b&gt;된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;효율적인 전송 방식&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Chunk Stream 방식을 사용하여 네트워크 대역폭을 최적화할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;❌ RTMP의 단점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ &lt;b&gt;Flash 기반 플레이어 필요&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;과거에는 Flash Player를 이용해 RTMP 스트리밍을 재생했지만, &lt;b&gt;현재 대부분의 브라우저에서 Flash 지원이 중단&lt;/b&gt;되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ &lt;b&gt;보안 문제&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로 암호화되지 않은 TCP 연결을 사용하기 때문에 &lt;b&gt;보안 취약점&lt;/b&gt;이 존재할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RTMPS(RTMP + TLS)&lt;/b&gt;를 사용하면 보안성을 강화할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ &lt;b&gt;HLS 등 최신 프로토콜에 밀려나는 중&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RTMP는 실시간 송출에는 강하지만, 재생(Playback)은 현재 HLS(HTTP Live Streaming)로 대체되는 추세이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RTMP의 사용 사례&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RTMP는 다음과 같은 실시간 스트리밍 서비스에서 활용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;유튜브 라이브 (YouTube Live)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RTMP URL과 &lt;b&gt;스트림 키&lt;/b&gt;를 사용하여 실시간 방송 송출&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;트위치 (Twitch)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RTMP 프로토콜을 이용해 &lt;b&gt;게임 방송을 송출&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;페이스북 라이브 (Facebook Live)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;페이스북에서 실시간 스트리밍할 때 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;OBS Studio / XSplit 등 방송 송출 프로그램&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RTMP를 이용해 &lt;b&gt;YouTube, Twitch 등으로 스트림을 전송&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 RTMP는 &lt;b&gt;대부분의 라이브 스트리밍 플랫폼에서 송출(입력) 프로토콜로 사용&lt;/b&gt;되고 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RTMP vs HLS, 어떤 차이가 있을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 RTMP는 송출용으로는 많이 사용되지만, &lt;b&gt;재생(Playback) 프로토콜은 대부분 HLS로 대체&lt;/b&gt;되고 있다.&lt;br /&gt;둘의 차이를 비교해 보자.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;RTMP&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;HLS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;지연 시간&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;2~5초 (낮음)&lt;/td&gt;
&lt;td&gt;10~30초 (높음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;연결 방식&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;지속적인 TCP 연결&lt;/td&gt;
&lt;td&gt;HTTP 기반 조각 파일 전송&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;플레이어 지원&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Flash 기반 (현재 대부분 미지원)&lt;/td&gt;
&lt;td&gt;대부분의 브라우저에서 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;적용 예시&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;실시간 방송 (게임, 인터뷰, 스포츠 중계)&lt;/td&gt;
&lt;td&gt;VOD, 방송 송출 (YouTube, Netflix)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;결론:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RTMP는 초저지연이 중요한 실시간 방송(송출)에 적합&lt;/li&gt;
&lt;li&gt;HLS는 다양한 디바이스에서 안정적인 재생(Playback)을 위한 최적의 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 현재는 &lt;b&gt;송출(입력)은 RTMP, 재생(출력)은 HLS&lt;/b&gt;로 변환하여 사용하는 경우가 많다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RTMP는 오랜 역사를 가진 실시간 스트리밍 프로토콜로, 여전히 &lt;b&gt;라이브 방송 송출(입력) 방식으로 널리 사용&lt;/b&gt;되고 있다.&lt;br /&gt;하지만 &lt;b&gt;Flash 지원 중단 및 HLS/WebRTC 같은 최신 기술의 등장으로, 단독으로 사용되기보다는 HLS 등과 조합하여 활용&lt;/b&gt;되는 추세다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;RTMP의 강점은?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저지연, 안정적인 TCP 연결, 간편한 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;RTMP의 한계는?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저에서 재생 지원 부족, 보안 문제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그럼에도 불구하고 RTMP는 실시간 스트리밍에서 여전히 강력한 선택지다.&lt;/b&gt;&lt;br /&gt;지금 사용 중인 스트리밍 시스템에서 RTMP가 어떻게 동작하는지 이해하고, 적절한 기술 스택을 조합하여 최적의 스트리밍 환경을 구축해보자!&lt;/p&gt;</description>
      <category>영상 스트리밍</category>
      <category>RTMP</category>
      <author>젤라빈</author>
      <guid isPermaLink="true">https://jellabean.tistory.com/26</guid>
      <comments>https://jellabean.tistory.com/26#entry26comment</comments>
      <pubDate>Wed, 12 Mar 2025 20:00:48 +0900</pubDate>
    </item>
    <item>
      <title>TCA(The Composable Architecture): iOS 개발의 새로운 패러다임</title>
      <link>https://jellabean.tistory.com/24</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;iOS 개발에서 &lt;b&gt;상태 관리(state management)&lt;/b&gt; 는 언제나 고민해야 할 중요한 부분이다.&lt;br /&gt;특히, SwiftUI와 함께 사용될 때 &lt;b&gt;상태(State)와 데이터 흐름을 어떻게 관리할 것인가?&lt;/b&gt; 는 더욱 중요해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하는 강력한 아키텍처 패턴 중 하나가 바로 &lt;b&gt;TCA(The Composable Architecture)&lt;/b&gt; 이다.&lt;br /&gt;TCA는 &lt;b&gt;Redux 스타일의 단방향 데이터 흐름을 기반으로, 앱의 상태와 로직을 예측 가능하게 관리할 수 있도록 해준다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 &lt;b&gt;TCA의 개념, 핵심 구조, 그리고 실무에서의 활용법&lt;/b&gt;까지 살펴보자!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1️⃣ TCA란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TCA(The Composable Architecture)&lt;/b&gt; 는 &lt;b&gt;iOS 및 SwiftUI 앱을 위한 아키텍처 패턴&lt;/b&gt;으로,&lt;br /&gt;&lt;b&gt;Point-Free&lt;/b&gt; 팀이 개발한 오픈소스 프레임워크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;TCA의 주요 특징&lt;/b&gt;&lt;br /&gt;✅ &lt;b&gt;Redux 스타일의 단방향 데이터 흐름&lt;/b&gt;을 사용&lt;br /&gt;✅ &lt;b&gt;애플리케이션 상태(State)를 중앙에서 관리&lt;/b&gt;&lt;br /&gt;✅ &lt;b&gt;Side Effect(비동기 작업) 처리가 명확함&lt;/b&gt;&lt;br /&gt;✅ &lt;b&gt;Composable(조합 가능한) 구조&lt;/b&gt;로, 모듈화가 용이&lt;br /&gt;✅ &lt;b&gt;SwiftUI와 궁합이 좋음&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;TCA는 앱의 상태와 동작을 예측 가능하게 만들고, 테스트하기 쉬운 환경을 제공하는 아키텍처 패턴&lt;/b&gt;이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2️⃣ TCA의 핵심 개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCA의 구조는 크게 &lt;b&gt;State(상태), Reducer(비즈니스 로직), Action(사용자 입력), Environment(외부 의존성)&lt;/b&gt; 로 구성된다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;[ State ]        &amp;rarr; 앱의 상태를 저장
[ Action ]       &amp;rarr; 사용자 입력 및 이벤트 처리
[ Reducer ]      &amp;rarr; 액션을 받아 상태를 변경하는 로직
[ Environment ]  &amp;rarr; 외부 서비스(API, Database 등)와의 상호작용
[ Store ]        &amp;rarr; 전체적인 데이터 흐름을 관리하는 컨테이너
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 컴포넌트를 자세히 살펴보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 1. State (상태)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱의 상태를 나타내는 구조체이다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;struct CounterState: Equatable {
    var count: Int = 0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 2. Action (사용자 입력 및 이벤트)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 버튼을 누르거나, 네트워크 요청이 완료되었을 때 발생하는 액션을 정의한다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;enum CounterAction {
    case increment
    case decrement
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 3. Reducer (비즈니스 로직)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reducer는 Action을 받아서 State를 변경하는 로직을 담당한다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;import ComposableArchitecture

let counterReducer = Reducer&amp;lt;CounterState, CounterAction, Void&amp;gt; { state, action, _ in
    switch action {
    case .increment:
        state.count += 1
        return .none
    case .decrement:
        state.count -= 1
        return .none
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 4. Store (전체 데이터 흐름 관리)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Store는 앱의 상태와 액션을 관리하는 중앙 컨테이너 역할을 한다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;let store = Store(
    initialState: CounterState(),
    reducer: counterReducer,
    environment: ()
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 5. View (UI)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI와 함께 사용되며, Store에서 상태를 구독하고 액션을 보낸다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;import SwiftUI
import ComposableArchitecture

struct CounterView: View {
    let store: Store&amp;lt;CounterState, CounterAction&amp;gt;

    var body: some View {
        WithViewStore(self.store) { viewStore in
            VStack {
                Text(&quot;Count: \(viewStore.count)&quot;)
                    .font(.largeTitle)
                HStack {
                    Button(&quot;-&quot;) { viewStore.send(.decrement) }
                    Button(&quot;+&quot;) { viewStore.send(.increment) }
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;핵심 정리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Store는 상태를 관리하고, View는 Store의 상태를 구독하며, Reducer는 액션을 받아 상태를 변경한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SwiftUI의 WithViewStore 를 사용하면 Store에서 상태를 구독하고 UI를 업데이트할 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3️⃣ TCA를&amp;nbsp; 활용하는 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCA를 실제 프로젝트에서 어떻게 활용할 수 있을까?&lt;br /&gt;기본적인 Counter 예제에서 확장하여, 네트워크 요청을 포함한 예제를 만들어보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  API 호출이 필요한 경우&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCA에서는 비동기 API 호출을 &lt;b&gt;Effect(효과)&lt;/b&gt; 를 통해 처리한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 1. State 정의&lt;/h4&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;struct UserState: Equatable {
    var user: User?
    var isLoading: Bool = false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 2. Action 정의&lt;/h4&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;enum UserAction {
    case fetchUser
    case userResponse(Result&amp;lt;User, APIError&amp;gt;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 3. Reducer에서 API 호출 처리&lt;/h4&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;let userReducer = Reducer&amp;lt;UserState, UserAction, APIClient&amp;gt; { state, action, apiClient in
    switch action {
    case .fetchUser:
        state.isLoading = true
        return apiClient.fetchUser()
            .catchToEffect()
            .map(UserAction.userResponse)

    case .userResponse(.success(let user)):
        state.isLoading = false
        state.user = user
        return .none

    case .userResponse(.failure):
        state.isLoading = false
        return .none
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 4. View에서 액션 전송&lt;/h4&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;struct UserView: View {
    let store: Store&amp;lt;UserState, UserAction&amp;gt;

    var body: some View {
        WithViewStore(self.store) { viewStore in
            VStack {
                if viewStore.isLoading {
                    ProgressView()
                } else if let user = viewStore.user {
                    Text(&quot;User: \(user.name)&quot;)
                } else {
                    Button(&quot;Load User&quot;) {
                        viewStore.send(.fetchUser)
                    }
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;이렇게 하면?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;API 요청이 필요할 때 Effect를 사용하여 비동기 작업을 실행할 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UI는 상태를 구독하고 자동으로 업데이트된다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4️⃣ TCA의 장점과 단점&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ TCA의 장점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✔ &lt;b&gt;단방향 데이터 흐름&lt;/b&gt;으로 인해 예측 가능성이 높다.&lt;br /&gt;✔ &lt;b&gt;상태와 로직을 중앙에서 관리&lt;/b&gt;하여 코드의 일관성을 유지할 수 있다.&lt;br /&gt;✔ &lt;b&gt;SwiftUI와 완벽하게 통합&lt;/b&gt;되며, Combine을 활용한 비동기 처리가 용이하다.&lt;br /&gt;✔ &lt;b&gt;테스트가 쉬운 구조&lt;/b&gt;를 제공하여 TDD(Test-Driven Development)에 적합하다.&lt;br /&gt;✔ &lt;b&gt;Composable(조합 가능한) 아키텍처&lt;/b&gt;로, 기능을 모듈화하여 재사용성이 높다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;❌ TCA의 단점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;초기 학습 비용이 높다&lt;/b&gt; (Redux 스타일의 개념을 이해해야 함).&lt;br /&gt;  &lt;b&gt;소규모 프로젝트에는 다소 과할 수 있음&lt;/b&gt; (단순한 앱에서는 불필요하게 복잡해질 가능성).&lt;br /&gt;  &lt;b&gt;성능 이슈&lt;/b&gt; (State가 클 경우 업데이트가 많아질 수 있음).&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;TCA(The Composable Architecture)는 단방향 데이터 흐름을 기반으로 한 강력한 상태 관리 아키텍처&lt;/b&gt;&lt;br /&gt;✅ &lt;b&gt;State, Action, Reducer, Store, Environment의 개념을 통해 명확한 구조를 제공&lt;/b&gt;&lt;br /&gt;✅ &lt;b&gt;Effect를 사용하여 비동기 API 호출과 같은 Side Effect를 처리할 수 있음&lt;/b&gt;&lt;br /&gt;✅ &lt;b&gt;SwiftUI와 함께 사용하면 강력한 조합을 이루며, 테스트가 쉬운 구조를 만들 수 있음&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCA는 처음에는 다소 복잡해 보일 수 있지만, &lt;b&gt;한 번 익숙해지면 확장성과 유지보수성이 뛰어난 코드를 작성할 수 있다.&lt;/b&gt;&lt;br /&gt;지금 진행 중인 프로젝트에서 작은 기능부터 TCA를 도입해보면서, 어떤 이점이 있는지 직접 경험해보자&lt;/p&gt;</description>
      <category>iOS</category>
      <category>TCA</category>
      <author>젤라빈</author>
      <guid isPermaLink="true">https://jellabean.tistory.com/24</guid>
      <comments>https://jellabean.tistory.com/24#entry24comment</comments>
      <pubDate>Tue, 11 Mar 2025 20:00:14 +0900</pubDate>
    </item>
    <item>
      <title>Clean Architecture -  Layer Separation &amp;amp; Dependency Rule</title>
      <link>https://jellabean.tistory.com/23</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;코드를 기능별로 잘 나누는 것만으로 정말 좋은 아키텍처가 될까?&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아키텍처 패턴을 고민할 때 가장 중요한 것은 &lt;b&gt;코드를 단순히 레이어별로 나누는 것&lt;/b&gt;이 아니라, &lt;b&gt;각 레이어가 올바르게 역할을 수행하고, 의존성이 잘 정리되어 있는지&lt;/b&gt;를 따지는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Clean Architecture는 &lt;b&gt;Layer Separation(레이어 분리)&lt;/b&gt; 와 &lt;b&gt;Dependency Rule(의존성 규칙)&lt;/b&gt; 을 통해 &lt;b&gt;확장성 높은 소프트웨어를 만드는 것을 목표&lt;/b&gt;로 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1️⃣ Clean Architecture란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Clean Architecture는 &lt;b&gt;로버트 C. 마틴(Uncle Bob)&lt;/b&gt; 이 제안한 소프트웨어 설계 원칙으로,&lt;br /&gt;애플리케이션을 &lt;b&gt;책임에 따라 계층(Layer)으로 분리하고, 의존성을 내부 도메인 중심으로 흐르게 하는 구조&lt;/b&gt;를 말한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 핵심 목표:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비즈니스 로직과 UI를 분리&lt;/b&gt;하여 유지보수성을 높인다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;의존성을 도메인 중심으로&lt;/b&gt; 설계하여, 외부 요소(Database, UI, Frameworks)에 영향을 덜 받도록 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트가 용이한 구조&lt;/b&gt;를 만들어 단위 테스트(Unit Test)가 쉽도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Clean Architecture는 &lt;b&gt;계층 구조&lt;/b&gt;와 &lt;b&gt;의존성 규칙(Dependency Rule)&lt;/b&gt; 을 기반으로 설계된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2️⃣ Clean Architecture의 레이어 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Clean Architecture는 아래와 같은 4개의 계층(Layer)으로 나뉜다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;[ UI Layer ]        &amp;rarr; View, ViewModel (iOS에서는 SwiftUI, UIKit 등)
[ Interface Layer ] &amp;rarr; Use Case (Interactor)
[ Domain Layer ]    &amp;rarr; Entities (비즈니스 로직)
[ Data Layer ]      &amp;rarr; Repository, Data Sources (DB, API)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;레이어별 역할 정리&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;레이어&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;역할&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;예시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;UI Layer (프레젠테이션 계층)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;사용자 입력을 받고, 화면을 구성&lt;/td&gt;
&lt;td&gt;ViewController, SwiftUI View&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Interface Layer (Use Case 계층)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;UI와 도메인 사이의 연결 역할, 비즈니스 규칙 실행&lt;/td&gt;
&lt;td&gt;UseCase, Interactor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Domain Layer (도메인 계층)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;애플리케이션의 핵심 로직, 엔티티 정의&lt;/td&gt;
&lt;td&gt;UserEntity, ProductEntity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Data Layer (데이터 계층)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;데이터 관리 (DB, API, Local Storage)&lt;/td&gt;
&lt;td&gt;Repository, NetworkService, Database&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3️⃣ Dependency Rule(의존성 규칙)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Clean Architecture에서 가장 중요한 개념은 &lt;b&gt;의존성이 바깥쪽에서 안쪽으로만 흐른다는 점&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;UI &amp;rarr; Interface(Use Case) &amp;rarr; Domain(Entity) &amp;rarr; Data
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;의존성이 내부 도메인 중심으로 흐르는 이유&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비즈니스 로직을 UI와 분리&lt;/b&gt;하여 UI 변경이 비즈니스 로직에 영향을 주지 않도록 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;외부 요소(DB, API, 프레임워크 등)에 대한 의존성을 줄여 확장성을 높인다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단위 테스트가 용이&lt;/b&gt;하도록 구조화하여 유지보수성을 높인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;즉, 핵심 비즈니스 로직은 어떤 프레임워크나 라이브러리에도 의존하지 않도록 한다!&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4️⃣ Clean Architecture를 iOS 프로젝트에 적용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Clean Architecture를 iOS 프로젝트에서 어떻게 구현할 수 있는지 살펴보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 1. Entity (Domain Layer)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 계층의 핵심 비즈니스 로직을 담당하는 엔티티(Entity)이다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;struct User {
    let id: Int
    let name: String
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 2. Use Case (Interface Layer)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 로직을 실행하는 역할을 한다.&lt;br /&gt;UI와 도메인 사이의 연결고리 역할을 하며, Repository를 사용해 데이터를 가져온다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;protocol FetchUserUseCase {
    func execute(userId: Int) async throws -&amp;gt; User
}

class FetchUserInteractor: FetchUserUseCase {
    private let repository: UserRepository

    init(repository: UserRepository) {
        self.repository = repository
    }

    func execute(userId: Int) async throws -&amp;gt; User {
        return try await repository.getUser(by: userId)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 3. Repository (Data Layer)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository는 데이터를 가져오는 인터페이스 역할을 한다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;protocol UserRepository {
    func getUser(by id: Int) async throws -&amp;gt; User
}

class UserRepositoryImpl: UserRepository {
    private let apiClient: APIClient

    init(apiClient: APIClient) {
        self.apiClient = apiClient
    }

    func getUser(by id: Int) async throws -&amp;gt; User {
        let response = try await apiClient.request(endpoint: &quot;/users/\(id)&quot;)
        return User(id: response[&quot;id&quot;] as! Int, name: response[&quot;name&quot;] as! String)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 4. ViewModel (UI Layer)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewModel은 UI에서 데이터를 받아오고 상태를 관리하는 역할을 한다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;class UserViewModel: ObservableObject {
    private let fetchUserUseCase: FetchUserUseCase
    @Published var user: User?

    init(fetchUserUseCase: FetchUserUseCase) {
        self.fetchUserUseCase = fetchUserUseCase
    }

    func loadUser(userId: Int) async {
        do {
            let user = try await fetchUserUseCase.execute(userId: userId)
            DispatchQueue.main.async {
                self.user = user
            }
        } catch {
            print(&quot;Error loading user: \(error)&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 5. View (UI Layer)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI에서 ViewModel을 사용해 UI를 업데이트한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;struct UserView: View {
    @StateObject private var viewModel = UserViewModel(fetchUserUseCase: FetchUserInteractor(repository: UserRepositoryImpl(apiClient: APIClient())))

    var body: some View {
        VStack {
            if let user = viewModel.user {
                Text(user.name)
            } else {
                ProgressView()
            }
        }
        .onAppear {
            Task {
                await viewModel.loadUser(userId: 1)
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5️⃣ Clean Architecture 적용 시 고려할 점&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 언제 적용하면 좋을까?&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;대규모 프로젝트&lt;/b&gt;에서 코드의 확장성과 유지보수성을 높이고 싶을 때&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트가 중요한 프로젝트&lt;/b&gt;에서 UI와 비즈니스 로직을 분리하고 싶을 때&lt;/li&gt;
&lt;li&gt;&lt;b&gt;팀 협업이 필요한 경우&lt;/b&gt; (각 레이어별로 역할이 나뉘므로 개발이 분리됨)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 언제 과할 수 있을까?&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작은 프로젝트에서는 &lt;b&gt;계층이 많아져서 오히려 복잡성이 증가할 수도 있다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;모든 프로젝트에서 반드시 Clean Architecture를 적용할 필요는 없으며, 프로젝트의 규모와 요구사항을 고려해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ Clean Architecture는 &lt;b&gt;비즈니스 로직과 UI를 분리하여 유지보수를 쉽게 만드는 설계 패턴&lt;/b&gt;이다.&lt;br /&gt;✅ 핵심 개념은 &lt;b&gt;Layer Separation(레이어 분리)과 Dependency Rule(의존성 규칙)&lt;/b&gt; 을 따르는 것!&lt;br /&gt;✅ &lt;b&gt;도메인 계층이 외부 요소(UI, DB, API 등)에 의존하지 않도록 설계&lt;/b&gt;해야 한다.&lt;br /&gt;✅ &lt;b&gt;iOS에서는 Entity, UseCase, Repository, ViewModel, View로 분리하여 적용할 수 있다.&lt;/b&gt;&lt;br /&gt;✅ 실무에서 적용할 때는 프로젝트 규모와 필요에 따라 적절히 활용하는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Clean Architecture는 처음에는 조금 복잡해 보일 수 있지만, &lt;b&gt;한 번 익숙해지면 유지보수성과 확장성이 뛰어난 코드를 작성할 수 있다&lt;/b&gt;.&lt;br /&gt;프로젝트에서 직접 적용해보면서 &lt;b&gt;어떤 부분이 도움이 되는지 경험해보는 것&lt;/b&gt;이 가장 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제, Clean Architecture를 적용해 더 깔끔한 iOS 앱을 만들어보자!&lt;/p&gt;</description>
      <category>프로그래밍 공부</category>
      <category>clean architecture</category>
      <author>젤라빈</author>
      <guid isPermaLink="true">https://jellabean.tistory.com/23</guid>
      <comments>https://jellabean.tistory.com/23#entry23comment</comments>
      <pubDate>Mon, 10 Mar 2025 20:00:01 +0900</pubDate>
    </item>
    <item>
      <title>MVVM(Model-View-ViewModel) 패턴 제대로 이해하기</title>
      <link>https://jellabean.tistory.com/22</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;iOS 개발을 하다 보면 &lt;b&gt;ViewController가 너무 비대해지는 문제&lt;/b&gt;를 자주 마주하게 된다.&lt;br /&gt;네트워크 요청, UI 로직, 데이터 변환 등이 한 곳에 몰리면서 &lt;b&gt;코드가 복잡해지고 유지보수가 어려워지는 문제&lt;/b&gt;가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하는 방법 중 하나가 바로 &lt;b&gt;MVVM(Model-View-ViewModel) 패턴&lt;/b&gt;이다!&lt;br /&gt;MVVM을 활용하면 &lt;b&gt;코드를 더 구조적으로 분리할 수 있고, 테스트가 쉬워지며, UI 로직을 깔끔하게 정리할 수 있다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 &lt;b&gt;MVVM 패턴의 개념, 역할별 구조, 그리고 적용 방법&lt;/b&gt;까지 정리해보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1️⃣ MVVM이란?&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ MVVM의 기본 개념&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVVM은 &lt;b&gt;Model-View-ViewModel&lt;/b&gt;의 약자로, &lt;b&gt;iOS 개발에서 ViewController의 책임을 줄이고 역할을 분리하기 위해 사용되는 패턴&lt;/b&gt;이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;컴포넌트&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; &lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;역할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Model&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;데이터 및 비즈니스 로직을 담당&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;View&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;사용자 인터페이스(UI) 및 이벤트 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;ViewModel&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;View와 Model 사이에서 데이터 변환 및 UI 업데이트를 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;MVVM의 핵심 목표:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ViewController의 역할을 최소화&lt;/b&gt;하고, UI 로직을 ViewModel에서 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UI(View)와 비즈니스 로직(Model)을 분리&lt;/b&gt;하여 코드의 재사용성을 높임&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 바인딩(Data Binding) 기술을 활용&lt;/b&gt;해 View와 ViewModel을 쉽게 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 MVVM이 왜 필요한지 예제와 함께 살펴보자!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2️⃣ MVVM이 필요한 이유 (MVC의 문제점)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;기존 MVC 패턴의 문제점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 iOS 개발에서는 &lt;b&gt;MVC(Model-View-Controller) 패턴&lt;/b&gt;을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;MVC의 가장 큰 문제는 ViewController가 너무 많은 역할을 담당하게 된다는 점&lt;/b&gt;이다.&lt;br /&gt;이를 흔히 &lt;b&gt;&quot;Massive ViewController&quot; 문제&lt;/b&gt;라고 한다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;class UserViewController: UIViewController {
    let networkService = NetworkService()
    var users: [User] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        fetchUsers()
    }

    func fetchUsers() {
        networkService.getUsers { [weak self] users in
            self?.users = users
            self?.updateUI()
        }
    }

    func updateUI() {
        // UI 업데이트 코드
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ViewController가 네트워크 요청, 데이터 변환, UI 업데이트까지 모두 담당&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트가 어렵다&lt;/b&gt; &amp;rarr; ViewController를 유닛 테스트하려면 네트워크 로직도 함께 테스트해야 함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재사용성이 낮다&lt;/b&gt; &amp;rarr; 다른 화면에서도 유사한 로직이 필요하면 중복 코드가 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;이 문제를 해결하는 방법이 바로 MVVM 패턴이다!&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3️⃣ MVVM의 역할과 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVVM을 적용하면 코드 구조가 이렇게 변경된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1️⃣ &lt;b&gt;Model&lt;/b&gt; &amp;rarr; 데이터를 나타내는 구조체 또는 클래스로, 네트워크나 데이터베이스에서 가져온 데이터를 저장&lt;br /&gt;2️⃣ &lt;b&gt;ViewModel&lt;/b&gt; &amp;rarr; Model을 가공하여 View에 전달, UI 로직을 처리&lt;br /&gt;3️⃣ &lt;b&gt;View(ViewController)&lt;/b&gt; &amp;rarr; ViewModel에서 제공하는 데이터를 UI에 표시&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;[ViewController] &amp;lt;------&amp;gt; [ViewModel] &amp;lt;------&amp;gt; [Model]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4️⃣ MVVM 적용하기&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 1. Model 만들기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Model은 데이터 구조를 정의하고, 네트워크나 데이터베이스에서 가져온 데이터를 저장하는 역할을 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;struct User {
    let id: Int
    let name: String
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 2. ViewModel 만들기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewModel은 데이터를 가공하고 UI 로직을 처리하는 역할을 한다.&lt;br /&gt;View에서 직접 Model을 다루지 않고, &lt;b&gt;ViewModel을 통해 필요한 데이터를 전달받도록 한다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;class UserViewModel {
    private let networkService = NetworkService()
    private(set) var users: [User] = []

    var onUsersUpdated: (() -&amp;gt; Void)?

    func fetchUsers() {
        networkService.getUsers { [weak self] users in
            self?.users = users
            self?.onUsersUpdated?()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;핵심 포인트&lt;/b&gt;&lt;br /&gt;✅ ViewModel은 &lt;b&gt;Model을 직접 노출하지 않고, 필요한 데이터를 가공해서 제공&lt;/b&gt;&lt;br /&gt;✅ onUsersUpdated 클로저를 통해 데이터가 변경되었을 때 UI를 업데이트하도록 함&lt;br /&gt;✅ 네트워크 로직을 ViewController에서 분리하여 &lt;b&gt;코드가 더 깔끔해짐&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 3. View (ViewController)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View는 UI만 담당하고, &lt;b&gt;ViewModel을 통해 데이터를 받아서 UI를 업데이트&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;class UserViewController: UIViewController {
    private let viewModel = UserViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.onUsersUpdated = { [weak self] in
            self?.updateUI()
        }

        viewModel.fetchUsers()
    }

    func updateUI() {
        print(&quot;UI 업데이트: \(viewModel.users)&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;핵심 포인트&lt;/b&gt;&lt;br /&gt;✅ ViewController는 &lt;b&gt;오직 UI만 담당하며, 데이터 처리는 ViewModel에 위임&lt;/b&gt;&lt;br /&gt;✅ ViewModel에서 데이터를 받아 updateUI()를 호출해 화면을 갱신&lt;br /&gt;✅ &lt;b&gt;ViewController는 가볍게 유지되어 유지보수와 테스트가 쉬워짐&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5️⃣ MVVM + Combine 활용하기 (데이터 바인딩)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVVM의 강력한 장점 중 하나는 &lt;b&gt;데이터 바인딩&lt;/b&gt;을 활용할 수 있다는 점이다.&lt;br /&gt;Combine을 사용하면 ViewModel에서 변경된 데이터를 자동으로 UI에 반영할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;import Combine

class UserViewModel {
    private let networkService = NetworkService()
    @Published private(set) var users: [User] = []

    func fetchUsers() {
        networkService.getUsers { [weak self] users in
            self?.users = users
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;class UserViewController: UIViewController {
    private let viewModel = UserViewModel()
    private var cancellables = Set&amp;lt;AnyCancellable&amp;gt;()

    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.$users
            .sink { [weak self] _ in
                self?.updateUI()
            }
            .store(in: &amp;amp;cancellables)

        viewModel.fetchUsers()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;데이터가 변경될 때마다 UI가 자동으로 업데이트됨!&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;MVVM은 ViewController의 역할을 줄이고, UI와 비즈니스 로직을 분리하는 패턴&lt;/b&gt;&lt;br /&gt;✅ &lt;b&gt;Model&lt;/b&gt; &amp;rarr; 데이터를 저장하고 관리&lt;br /&gt;✅ &lt;b&gt;ViewModel&lt;/b&gt; &amp;rarr; 데이터를 가공하고 UI 로직을 처리&lt;br /&gt;✅ &lt;b&gt;View(ViewController)&lt;/b&gt; &amp;rarr; ViewModel에서 제공하는 데이터를 UI에 표시&lt;br /&gt;✅ &lt;b&gt;MVVM을 사용하면 유지보수가 쉬워지고, 유닛 테스트가 용이해짐&lt;/b&gt;&lt;br /&gt;✅ RxSwift, Combine을 활용하면 &lt;b&gt;데이터 바인딩을 통해 UI 업데이트를 자동화할 수 있음&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;442&quot; data-start=&quot;338&quot; data-ke-size=&quot;size16&quot;&gt;이제 MVVM의 개념은 이해했지만, &lt;b&gt;진짜 중요한 것은 직접 적용해보는 것&lt;/b&gt;이다.&lt;br /&gt;이 패턴을 도입하면 프로젝트 구조가 더 깔끔해지고, 유지보수가 쉬워지는 것을 체감할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;570&quot; data-start=&quot;444&quot; data-ke-size=&quot;size16&quot;&gt;다음 단계로, 현재 진행 중인 프로젝트에서 ViewController의 역할이 과도한 부분을 찾아보자.&lt;br /&gt;그리고 해당 로직을 ViewModel로 분리해보는 것부터 시작하면 자연스럽게 MVVM 패턴을 익힐 수 있을 것이다.&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;657&quot; data-start=&quot;572&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MVVM을 적용하면 더 유연하고 테스트하기 쉬운 iOS 앱을 만들 수 있다.&lt;/b&gt;&lt;br /&gt;기존 프로젝트에서 실험해보고, 변화를 직접 경험해보는게 가장 좋은것 같다!&lt;/p&gt;</description>
      <category>프로그래밍 공부</category>
      <category>MVVM</category>
      <author>젤라빈</author>
      <guid isPermaLink="true">https://jellabean.tistory.com/22</guid>
      <comments>https://jellabean.tistory.com/22#entry22comment</comments>
      <pubDate>Fri, 7 Mar 2025 20:00:14 +0900</pubDate>
    </item>
    <item>
      <title>iOS 개발에서 의존성 주입(Dependency Injection, DI)이 중요한 이유</title>
      <link>https://jellabean.tistory.com/21</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;iOS 개발을 하다 보면 &lt;b&gt;ViewController에서 네트워크 요청을 직접 호출하거나, 데이터베이스를 직접 접근하는 코드&lt;/b&gt;를 본 적이 있을 것이다.&lt;br /&gt;이렇게 하면 간단한 기능은 빠르게 구현할 수 있지만, 코드가 복잡해질수록 유지보수가 어렵고 테스트하기도 어려워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하는 방법이 바로 &lt;b&gt;의존성 주입(Dependency Injection, DI)&lt;/b&gt; 이다!&lt;br /&gt;DI를 활용하면 &lt;b&gt;유지보수가 쉬워지고, 유닛 테스트가 편리해지며, 코드의 결합도가 낮아져 더 유연한 아키텍처를 만들 수 있다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 &lt;b&gt;DI의 개념과 활용하는 방법&lt;/b&gt;을 정리해보겠다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1️⃣ 의존성이란?&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ &quot;의존성&quot;이 뭘까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체가 다른 객체를 사용할 때, 이를 &lt;b&gt;&quot;의존성(Dependency)&quot;&lt;/b&gt; 이라고 한다.&lt;br /&gt;예를 들어, ViewController가 NetworkService를 직접 생성해서 사용한다면, ViewController는 NetworkService에 &lt;b&gt;의존&lt;/b&gt;한다고 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;class ViewController: UIViewController {
    let networkService = NetworkService() // 직접 객체 생성
    
    override func viewDidLoad() {
        super.viewDidLoad()
        networkService.fetchData()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드의 문제점은?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ViewController가 NetworkService에 강하게 결합되어 있어서 &lt;b&gt;다른 네트워크 서비스로 교체하기 어려움&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유닛 테스트가 어렵다&lt;/b&gt; (NetworkService를 실제로 호출하기 때문에 Mocking이 어려움)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제를 해결하려면 &lt;b&gt;객체를 직접 생성하는 것이 아니라, 외부에서 주입받아야 한다&lt;/b&gt;.&lt;br /&gt;이 개념이 바로 &lt;b&gt;의존성 주입(Dependency Injection, DI)&lt;/b&gt; 이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2️⃣ 의존성 주입(Dependency Injection)이란?&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ DI의 개념&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;의존성 주입(Dependency Injection)&lt;/b&gt; 은 &lt;b&gt;객체가 직접 의존성을 생성하는 것이 아니라, 외부에서 주입받도록 하는 설계 패턴&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;코드의 결합도가 낮아지고&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다른 구현체로 쉽게 변경할 수 있으며&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유닛 테스트도 쉽게 작성할 수 있다&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3️⃣ 의존성 주입의 3가지 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DI를 적용하는 방법에는 &lt;b&gt;3가지 방법&lt;/b&gt;이 있다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt; 방법 &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;설명&lt;span&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;코드&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;코드 예제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;1. 생성자 주입 (Constructor Injection)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;객체를 생성할 때, 의존성을 주입&lt;/td&gt;
&lt;td&gt;init(service: NetworkService)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;2. 프로퍼티 주입 (Property Injection)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;객체 생성 후, 외부에서 의존성을 설정&lt;/td&gt;
&lt;td&gt;vc.service = NetworkService()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;3. 메서드 주입 (Method Injection)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;특정 메서드를 호출할 때, 의존성을 주입&lt;/td&gt;
&lt;td&gt;vc.setup(service: NetworkService())&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 각각의 방법을 코드로 살펴보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 1. 생성자 주입 (Constructor Injection)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 선호되는 방법으로, &lt;b&gt;객체가 생성될 때 의존성을 주입&lt;/b&gt;하는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;protocol NetworkService {
    func fetchData()
}

class APIService: NetworkService {
    func fetchData() {
        print(&quot;네트워크에서 데이터 가져오기&quot;)
    }
}

// ✅ 생성자 주입을 사용하여 의존성 주입
class ViewController {
    let service: NetworkService
    
    init(service: NetworkService) {
        self.service = service
    }
    
    func loadData() {
        service.fetchData()
    }
}

// 사용 예제
let apiService = APIService()
let vc = ViewController(service: apiService) // 의존성을 주입
vc.loadData()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;이 방법이 좋은 이유&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ViewController가 NetworkService 프로토콜에만 의존 &amp;rarr; &lt;b&gt;의존성 역전 원칙(DIP)를 준수&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;APIService 대신 MockService를 주입하면 &lt;b&gt;유닛 테스트가 가능&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;class MockService: NetworkService {
    func fetchData() {
        print(&quot;Mock 데이터 가져오기&quot;)
    }
}

let mockService = MockService()
let testVC = ViewController(service: mockService) // 테스트에서 Mock 객체 주입
testVC.loadData()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;언제 사용하면 좋을까?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;불변(immutable) 의존성을 다룰 때&lt;/b&gt; (한 번 주입되면 변경되지 않는 경우)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;객체의 생명주기가 길 때&lt;/b&gt; (서비스 레이어, 유틸리티 클래스 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 2. 프로퍼티 주입 (Property Injection)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체를 생성한 후, &lt;b&gt;외부에서 의존성을 설정하는 방식&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;class ViewController {
    var service: NetworkService? // 처음에는 nil
    
    func loadData() {
        service?.fetchData()
    }
}

// 사용 예제
let vc = ViewController()
vc.service = APIService() // 외부에서 주입
vc.loadData()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;언제 사용하면 좋을까?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;의존성이 선택적(optional)일 때&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;뷰 컨트롤러와 같은 UI 관련 클래스에서 사용&lt;/b&gt; (예: Storyboard 사용 시)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 3. 메서드 주입 (Method Injection)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 메서드를 호출할 때 의존성을 주입하는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;class ViewController {
    func loadData(service: NetworkService) {
        service.fetchData()
    }
}

// 사용 예제
let vc = ViewController()
vc.loadData(service: APIService()) // 실행할 때마다 다른 서비스 주입 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;언제 사용하면 좋을까?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;매번 다른 의존성을 주입해야 하는 경우&lt;/b&gt; (예: 전략 패턴)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유닛 테스트에서 특정 시점에 의존성을 변경할 필요가 있을 때&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4️⃣ 의존성 주입이 실무에서 중요한 이유&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;테스트가 쉬워진다!&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 NetworkService 대신 MockService를 주입하면, 네트워크 요청 없이 테스트 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드의 유연성이 증가한다!&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네트워크 서비스 변경 시 APIService 구현체만 교체하면 됨&lt;/li&gt;
&lt;li&gt;DI가 없으면 ViewController를 수정해야 하는데, DI를 적용하면 기존 코드 수정 없이 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;아키텍처 패턴과의 궁합이 좋다!&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MVVM, Clean Architecture, TCA 등의 패턴에서는 DI가 필수적&lt;/li&gt;
&lt;li&gt;특히 Dependency Injection Container(예: Swinject)를 활용하면 &lt;b&gt;객체 생성을 더 간편하게 관리 가능&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;의존성(Dependency)이란?&lt;/b&gt; 한 객체가 다른 객체를 사용할 때의 관계&lt;br /&gt;✅ &lt;b&gt;의존성 주입(DI)이란?&lt;/b&gt; 객체가 직접 의존성을 생성하는 것이 아니라, &lt;b&gt;외부에서 주입하는 설계 패턴&lt;/b&gt;&lt;br /&gt;✅ &lt;b&gt;DI를 적용하면&lt;/b&gt; 코드의 &lt;b&gt;유지보수가 쉬워지고, 테스트가 간편해지며, 결합도가 낮아진다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;DI의 3가지 방법&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;생성자 주입&lt;/b&gt;: 객체 생성 시 의존성 주입 (가장 선호되는 방식)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로퍼티 주입&lt;/b&gt;: 객체 생성 후 의존성 설정 (UI 관련 클래스에 적합)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메서드 주입&lt;/b&gt;: 특정 메서드 실행 시 의존성 전달 (전략 패턴에 유용)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DI를 잘 활용하면 &lt;b&gt;더 유지보수하기 쉬운 iOS 앱을 만들 수 있다&lt;/b&gt;.&lt;br /&gt;이제 직접 프로젝트에서 적용해보자!&amp;nbsp;&lt;/p&gt;</description>
      <category>프로그래밍 공부</category>
      <author>젤라빈</author>
      <guid isPermaLink="true">https://jellabean.tistory.com/21</guid>
      <comments>https://jellabean.tistory.com/21#entry21comment</comments>
      <pubDate>Thu, 6 Mar 2025 20:00:18 +0900</pubDate>
    </item>
  </channel>
</rss>