<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Deving</title>
    <link>https://snowee.tistory.com/</link>
    <description>얕게 말고 깊게</description>
    <language>ko</language>
    <pubDate>Mon, 3 Aug 2026 22:32:24 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>snowe</managingEditor>
    <image>
      <title>Deving</title>
      <url>https://tistory1.daumcdn.net/tistory/4304983/attach/2801cbb6dd18491180fede40808d15e2</url>
      <link>https://snowee.tistory.com</link>
    </image>
    <item>
      <title>iOS) RIBs에서의 ViewDidLoad() 호출 시점 - init()에서 ViewDidLoad()가 호출되는 것에 관하여</title>
      <link>https://snowee.tistory.com/64</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 RIBs라는 아키텍쳐를 사용해서 개발을 하고 있다. RIBs에는 Router, Interactor, Builder, View, (Presenter)라는 개념이 등장한다. VIPER패턴에 익숙한 사람이라면 이런 개념들 또한 익숙할 것이다.&lt;br&gt; &lt;br&gt;무튼 주제와 가까워져 보자면, 이슈사항은 RIBs와 Reacorkit을 함께 사용하면서 발생했다.&lt;br&gt; &lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RIBs에서 ViewDidLoad(), didBecomeActive()의 호출 순서에 대한 고찰&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;그리고 나의 이슈트래킹&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;br&gt;ReactorKit을 사용하기 위해서는 View에서도 ReactorKit의 존재를 알아야했는데, 이를 막기 위해 RIBs의 PresentableListener를 사용하였다.&lt;/p&gt;
&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;protocol TransactionDetailPresentableListener: AnyObject {
	typealias Action = TransactionDetailAction
	typealias State = TransactionDetailState
	
	var state: Observable&amp;lt;State&amp;gt; { get }
	var currentState: State { get }
	
	func sendAction(_ action: Action)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;br&gt;그리고 기존에는 Reactor를 통해서 bind가 이루어져야했지만 위와같이 구현하면서 View는 ReactorKit의 존재를 모르기 때문에 별도로 위 Listener를 통한 bind메서드를 구현해주었다.&lt;/p&gt;
&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;init() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super.init(nibName: nil, bundle: nil)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;bind(to: listener)
}

// MARK: Reactor

func bind(to listener: TransactionDetailPresentableListener?) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;guard let listener = listener else { return }
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;bindActions(to: listener)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;bindStates(from: listener)
}

func bindActions(to listener: TransactionDetailPresentableListener) {}

func bindStates(from listener: TransactionDetailPresentableListener) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;listener.state.map(\.transactionInfo)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.distinctUntilChanged()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.bind(with: self, onNext: {(owner, response) in
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;owner.responseBindToCell(with: response)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}).disposed(by: disposeBag)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;br&gt;이러면 이제 뷰컨은 PresentableListener의 sendAction(_ :)을 통해 액션을 전달하고, 비즈니스 로직을 담당하는 Interactor에서 액션에 대한 Mutate, Reduce가 이루어진 뒤, 최종적으로 State를 반환한다. bind메서드로 listener의 state를 구독해놓았기 때문에 변화된 상태가 반환되면 bindStates내의 코드가 동작하게 된다.&lt;br&gt; &lt;br&gt;문제는 이때 발생했다. init() 내에 작성해둔 bind메서드가 제대로 동작하지 않아 새로운 State가 반환되어도 뷰컨은 아무런 업데이트도 일어나지 않았다. init()시점에 뷰컨에 작성해둔 bind의 파라미터로 들어가야 할 listener가 nil인것이 이유였다.&lt;br&gt;&lt;br&gt;이때부터 혼란이 찾아왔다. 프로젝트에는 리액터 킷을 사용하고 있는 RIB이 3개 더 있었는데 현재 작업 중인 RIB에서만 ViewController에서 listener에 nil이 들어왔다.. 그래서 답답한 나머지 리액터킷을 사용중인 RIB들의 생명주기 호출 순서 로그를 다 찍어보았다.&lt;br&gt; &lt;br&gt;앞서 언급한 문제없이 잘 되는(리액터킷을 사용중인 3개의) RIB에서는 didBecomeActive()가 viewDidLoad()보다 먼저 불렸고, 대부분의 RIB에서는 viewDidLoad()가 didBecomeActive()보다 먼저 불렸다. 내가 알고있던 것과 달랐다. RIBs 튜토리얼에서도 didBecomeActive() 이후에 viewDidLoad()가 불렸고, 어디를 찾아봐도 이 순서가 디폴트였기 때문이다.&lt;br&gt; &lt;br&gt;그래서 당시 패스트캠퍼스 강의로 RIBs를 기반으로 한 슈퍼앱 강의가 있었는데 그 강사분께 메일로 질문을 드려 답변을 받았다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
  그 강의에서 진행 중인 프로젝트에서도 내가 알고있던 것과 다르게 viewDidLoad() -&amp;gt; didBecomeActive() 순으로 호출이 이루어지고 있었다. 
&lt;/blockquote&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cr6gSD/btrwGiJlKET/6ROFEyphzR2CRvFriynyfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cr6gSD/btrwGiJlKET/6ROFEyphzR2CRvFriynyfK/img.png&quot; data-origin-width=&quot;2715&quot; data-origin-height=&quot;1886&quot; style=&quot;width: 49.041275%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cr6gSD/btrwGiJlKET/6ROFEyphzR2CRvFriynyfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcr6gSD%2FbtrwGiJlKET%2F6ROFEyphzR2CRvFriynyfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2715&quot; height=&quot;1886&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baLat8/btrwIgR3IUG/golmuxKsKmKKvb31cOURP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baLat8/btrwIgR3IUG/golmuxKsKmKKvb31cOURP0/img.png&quot; data-origin-width=&quot;2672&quot; data-origin-height=&quot;1828&quot; style=&quot;width: 49.795933%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baLat8/btrwIgR3IUG/golmuxKsKmKKvb31cOURP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaLat8%2FbtrwIgR3IUG%2FgolmuxKsKmKKvb31cOURP0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2672&quot; height=&quot;1828&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;친절하게 답변해주신 노수진님께 감사드립니다 :)&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;메일을 통해 얻은 가장 큰 수확은 &lt;b&gt;달라질 수 있는 viewDidLoad()의 호출 시점&lt;/b&gt;이다.&lt;br&gt; &lt;br&gt;아까 말했듯 RIBs는 Router, Interactor, Builder, View로 이루어져있다. 이들은 각자의 생명주기가 있고, 이는 RIBs 깃헙에 나와있는 튜토리얼을 진행하다보면 자연스레 알게된다. 튜토리얼을 마친 뒤 나는 무의식적으로 Interactor의 didBecomeActive()가 먼저 호출되고 View의 ViewController()가 이후에 호출된다는 것을 당연하게 생각하고 있었다. 이 순서는 무조건 보장된다고 생각했다. 그래서 더 깊게 알아보지 않았던게 화근이 되었다.&lt;br&gt; &lt;br&gt;각 RIB의 build메서드에서는 본인의 뷰컨 인스턴스 및 자식 RIB들의 builder를 만들고 이를 라우팅의 인스턴스 파라미터로 전달한다. 라우터에서는 이들을 가지고 attach, detach를 할 때 사용한다.&lt;/p&gt;
&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;func build(withListener listener: HomeListener) -&amp;gt; HomeRouting {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;let component = HomeComponent(dependency: dependency)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;let viewController = HomeViewController()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;let interactor = HomeInteractor(dependency: component, presenter: viewController)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;let transactionDetailBuilder = TransactionDetailBuilder(dependency: component)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;interactor.listener = listener

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return HomeRouter(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;interactor: interactor,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;viewController: viewController,	
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;transactionDetailBuilder: transactionDetailBuilder
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;br&gt;&lt;b&gt;내가 만약 addSubView와 같은 view에 접근하는 코드를 뷰컨의 viewDidLoad()에 작성해두었다면, 이 메서드에서는 아무런 생명주기도 호출되지 않는다. 하지만 이슈를 겪었던 나처럼 init()에 view에 접근하는 코드를 작성해두었다면 build메서드가 호출되면서 viewController가 인스턴스화 될 때 viewDidLoad()가 호출된다.&lt;/b&gt;&lt;br&gt; &lt;br&gt;init()에서 뷰에 접근하지 않았다면 attach될 때 didBecomeActive가 호출 된 이후 뷰가 처음으로 접근 될 때 loadView()가 호출되고 viewDidLoad()가 호출될 것이다. &lt;a href=&quot;https://stackoverflow.com/questions/16849189/viewdidload-and-loadview-is-not-fired-after-the-view-controller-is-pushed-into&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;push, present함으로써 무조건 view에 접근하는 것은 아니다. &lt;/span&gt;&lt;/a&gt;&lt;br&gt; &lt;br&gt;암튼 그래서 내가 겪은 이슈는 해결을 하였다. init()에 view접근을 함으로써 listener가 들어오기 전에 뷰가 그려진 것이다.&lt;br&gt;나는 지금까지 얻은 정보들을 팀원에게 공유했다. 그리고 팀원들 중 한명이&lt;b&gt; &quot;init()에서 뷰에 접근하는 것이 괜찮을까?&quot;&lt;/b&gt; 라는 궁금증을 갖게 해주었다.&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ViewDidLoad()가 init() 시점에 호출되어도 괜찮을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁금증은 정확하게 &lt;b&gt;&quot;init()에서 뷰에 접근하여 viewDidLoad()가 호출되는 것이 성능상의 비효율을 가져오지는 않을까?&quot;&lt;/b&gt; 였다.&lt;br&gt;질문을 드렸던 강사님께서 init()에 view에 접근하는 방식을 자주 사용한다고 말씀하셔서 재차 질문을 드려보았다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;1456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/U0D2P/btrwA7hoCiz/EwnkDK9gJhiugZhmvvKUD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/U0D2P/btrwA7hoCiz/EwnkDK9gJhiugZhmvvKUD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/U0D2P/btrwA7hoCiz/EwnkDK9gJhiugZhmvvKUD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FU0D2P%2FbtrwA7hoCiz%2FEwnkDK9gJhiugZhmvvKUD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;501&quot; height=&quot;411&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;1456&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;내가 잘못 생각하고 있던 점도 있었지만, 전체적으로는 고민해 볼 만한 이슈였다고 생각한다. &lt;br&gt;결론적으로 이 질문에 대한 답변은 두 가지 로 나눌 수 있을 것 같다. &lt;br&gt; &lt;br&gt;&lt;b&gt;1. RIBs처럼 비즈니스 로직의 시작이 view가 되어서는 안되는 아키텍쳐&lt;/b&gt;&lt;br&gt;&lt;b&gt;2. &lt;/b&gt;&lt;b&gt;MVC처럼 비즈니스 로직의 시작이 view가 될수도 있는 아키텍쳐&lt;/b&gt;&lt;br&gt; &lt;br&gt;RIBs를 사용한다면 모든 비즈니스 로직의 시작은 Interactor에서 이루어져야 하며, view는 최대한 바보같이 Interactor가 시키는 대로만 하는 수동적인 객체로 만들어야한다. 따라서 내가 이슈를 만나게 된 계기인 bind(listener: )의 호출 또한 Interactor의 active시점에 맞추어 &lt;b&gt;Interactor가 호출시켜주는 것&lt;/b&gt;이 옳은 방법이라고 생각이 들었다. viewDidLoad()의 호출 시점에 의해 이후 비즈니스로직이 좌지우지 되서는 안된다고 생각했기 때문이다.&lt;br&gt; &lt;br&gt;내가 겪은 이슈 외에 다른 발생가능한 성능상의 이슈를 생각해보았지만, RIBs 아키텍쳐에서 view의 viewDidLoad()나 init()에서는 뷰를 그리는 일 이외에 다른 일이 일어나서는 안된다. 그것이 아키텍쳐의 장점 중 하나이다. 따라서 성능상의 비효율을 일으킬 수 있는 것은 view를 미리 로드하는 정도일 것이다. RIBs에서는 build메서드에서 뷰컨을 인스턴스화 한다. 그리고 이 build메서드는 Router에서 attach시점이 되어서야 present/push를 하기 위해 호출된다. attach와 present는 거의 셋트로 동작하기 때문에 무엇이 먼저 호출되던 성능상의 비효율을 야기할 정도는 아닐 것이라고 생각한다.&lt;br&gt; &lt;br&gt;하지만 만약 비즈니스 로직이 viewDidLoad()에도 포함될 수 있는 MVC와 같은 아키텍쳐에서는 어떨까? &lt;br&gt;MVC에서는 아래와 같은 코드가 가능해진다. &lt;/p&gt;
&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;override func viewDidLoad() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super.viewDidLoad()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;configureUI()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;callUserInfoAPI()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;callPermissionAPI()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 말인 즉슨, 탭바와 같이 뷰컨을 인스턴스로 만들어서 가지고 있어야하는 경우 불필요한 시점에 view를 포함한 서비스 호출까지 이루어질 수 있음을 의미한다. 이게 점점 많아질 경우 성능상의 비효율은 충분히 일어날 수 있다고 생각이된다.&lt;br&gt; &lt;br&gt;결론에 와서 생각해보면 RIBs구조에 맞지 않는 코드를 작성해서(bind 호출을 viewDidLoad()에게 맡겨버린) 이와같은 이슈를 만나게 되었다. 하지만 이를 통해 평소에 생각하지 못했던 것들을 생각하게 되었고, 앞으로 더 좋은 코드를 작성할 수 있게되었기에 좋은 경험이었던 것 같다 :)&lt;/p&gt;</description>
      <category>iOS</category>
      <author>snowe</author>
      <guid isPermaLink="true">https://snowee.tistory.com/64</guid>
      <comments>https://snowee.tistory.com/64#entry64comment</comments>
      <pubDate>Mon, 21 Mar 2022 20:56:57 +0900</pubDate>
    </item>
    <item>
      <title>iOS) PinLayout</title>
      <link>https://snowee.tistory.com/63</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://github.com/layoutBox/PinLayout&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/layoutBox/PinLayout&lt;/a&gt;&amp;nbsp;을 참고했습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PinLayout&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;761&quot; data-origin-height=&quot;240&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ca5VAl/btrscN0zIMZ/ZQ7zEZ7XPT2kw4w3wYtLHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ca5VAl/btrscN0zIMZ/ZQ7zEZ7XPT2kw4w3wYtLHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ca5VAl/btrscN0zIMZ/ZQ7zEZ7XPT2kw4w3wYtLHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fca5VAl%2FbtrscN0zIMZ%2FZQ7zEZ7XPT2kw4w3wYtLHk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;222&quot; height=&quot;70&quot; data-origin-width=&quot;761&quot; data-origin-height=&quot;240&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Luc Dion이라는 개발자가 만든 프레임워크이다. layoutBox라는 Swift를 사용한 layout 오픈 소스 organization 관리자인 이분은 PinLayout뿐만 아니라 FlexLayout을 만든 사람이기도 하다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PinLayout은 기본적으로 &lt;b&gt;AutoLayout을 사용하지 않고 레이아웃을 구성&lt;/b&gt;할 수 있도록 도와주는 프레임워크이다. AutoLayout을 사용하지 않음으로서 &lt;b&gt;렌더링 성능&lt;/b&gt;이 훨씬 빠르다고 한다(8~12배).&amp;nbsp;PinLayout은 수동으로 view를 layout하는데 때문에 Manual Layout과 속도가 비슷하거나 더 빠르다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;525&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NAwNa/btrr5Vluh7y/1iJyRTaZgrpHKPoIcXJLu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NAwNa/btrr5Vluh7y/1iJyRTaZgrpHKPoIcXJLu0/img.png&quot; data-alt=&quot;https://github.com/layoutBox/PinLayout/blob/master/docs/Benchmark.md&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NAwNa/btrr5Vluh7y/1iJyRTaZgrpHKPoIcXJLu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNAwNa%2Fbtrr5Vluh7y%2F1iJyRTaZgrpHKPoIcXJLu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1106&quot; height=&quot;525&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;525&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://github.com/layoutBox/PinLayout/blob/master/docs/Benchmark.md&lt;/figcaption&gt;
&lt;/figure&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;SnapKit&lt;/b&gt;이 &lt;b&gt;AutoLayout&lt;/b&gt;을 좀 더 직관적으로 잡을 수 있도록 도와준 프레임워크라면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PinLayout&lt;/b&gt;은 &lt;b&gt;ManualLayout&lt;/b&gt;을 직관적으로 잡을 수 있게 도와주는 프레임워크이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(그렇다고 PinLayout을 사용하면 무조건 AutoLayout을 사용하면 안되는 것은 아니고 부분적으로도 적용할 수 있다.)&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;유의사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PinLayout은 auto layout constrainst를 사용하지 않는다, 수동적으로 view들의 layout을 잡는 프레임워크이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이유때문에 우리는 &lt;b&gt;디바이스의 회전을 포함한 layout의 업데이트&lt;/b&gt;를 &lt;b&gt;UIView.layoutSubviews()&lt;/b&gt; 혹은 &lt;b&gt;UIViewController.viewDidLayoutSubViews()&lt;/b&gt;에서 작성해주어야한다.&lt;/p&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PinLayout 의 철칙&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Manual layouting (auto layout에 의존하지 않는다).&lt;/li&gt;
&lt;li&gt;PinLayout은 가능한 한 간단하고 빠른 상태로 존재한다. 앞서 말했듯, manual layout만큼 빠르다. &lt;a href=&quot;https://github.com/layoutBox/PinLayout#performance&quot;&gt;performance results below.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Full control: You're in the middle of the layout process, no magic black box(음 내가 다 관리할 수 있다 정도..?)&lt;/li&gt;
&lt;li&gt;한번에 하나의 뷰를 layout한다. 코드와 디버깅을 간단하게 만들어준다.&lt;/li&gt;
&lt;li&gt;간결한 구문을 가짐으로써 대부분의 뷰를 한 줄로 layout할 수 있다.&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;Edges Layout&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;401&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q1Ndd/btrr3kd6G99/lUtH9CkAMOplk1DSHkslsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q1Ndd/btrr3kd6G99/lUtH9CkAMOplk1DSHkslsk/img.png&quot; data-alt=&quot;Superview에게 모든 edge를 10만큼 떨어뜨려서 레이아웃 잡기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q1Ndd/btrr3kd6G99/lUtH9CkAMOplk1DSHkslsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq1Ndd%2Fbtrr3kd6G99%2FlUtH9CkAMOplk1DSHkslsk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;401&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;401&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Superview에게 모든 edge를 10만큼 떨어뜨려서 레이아웃 잡기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빠르게 위의 레이아웃을 잡기위한 코드부터 보자면&lt;/p&gt;
&lt;pre id=&quot;code_1643712063865&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;viewA.pin.top(10).bottom(10).left(10).right(10) // 방법 1
view.pin.all(10) // 방법 2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 사용한다.&amp;nbsp;snp 대신 pin을 쓰는 방식이라고 생각하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 다른 점이 있다면 snapkit의 경우에는 superView와 safeArea 모두 명시를 해주어야 했지만 pinLayout은 좀 다르다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;1224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MOlJM/btrsi9ILTOT/OadzimzKggZG9i7V0v1m40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MOlJM/btrsi9ILTOT/OadzimzKggZG9i7V0v1m40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MOlJM/btrsi9ILTOT/OadzimzKggZG9i7V0v1m40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMOlJM%2Fbtrsi9ILTOT%2FOadzimzKggZG9i7V0v1m40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;258&quot; height=&quot;471&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;1224&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;default가 superView이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;따라서 기본적으로 top, bottom, left, right가&amp;nbsp;&lt;/span&gt;&lt;b&gt;superview의 edge를 기준으로 한다.&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;그래서 safeArea만 따로 처리해주면 된다.&lt;/span&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;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;edge(top, left, right, bottom)&lt;/b&gt;의 layout을 잡는 방법에는 &lt;b&gt;4가지&lt;/b&gt;가 있다.&lt;/span&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;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;1. top(_&amp;nbsp;offset:&amp;nbsp;CGFloat),&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;span&gt;2. top()&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;일반적인 사용. 띄우고 싶은 offset을 넣는다. 아예 안넣으면 0으로 인지&lt;/span&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;span style=&quot;background-color: #ffffff;&quot;&gt;2. top(_&amp;nbsp;offset:&amp;nbsp;Percent)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;%로 넣을 수 있는건데..음 잘 쓰일지는 의문이다.&lt;/span&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;span style=&quot;background-color: #ffffff;&quot;&gt;3. top(_&amp;nbsp;margin:&amp;nbsp;UIEdgeInsets)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;UIEdgeInsets을 사용하기 때문에 safeArea Layout을 잡을 때 유용하다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1643722436759&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;testView.pin.top(self.view.pin.safeArea.top)&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;center&lt;/b&gt;의 layout을 잡기 위한 속성은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.&lt;b&gt;&amp;nbsp;vCenter(vertical)&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;1224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/doarH9/btrr4QYbTru/Zt8ASQX59Uiofb8XF087s1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/doarH9/btrr4QYbTru/Zt8ASQX59Uiofb8XF087s1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/doarH9/btrr4QYbTru/Zt8ASQX59Uiofb8XF087s1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdoarH9%2Fbtrr4QYbTru%2FZt8ASQX59Uiofb8XF087s1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;110&quot; height=&quot;201&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;1224&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;(_offset: CGFloat), (_offset: Percent), ()&lt;/blockquote&gt;
&lt;pre id=&quot;code_1643722683468&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// left, right를 superView에 맞추고 
// y축 중앙(vertical)에 
// 높이 100 크기의 뷰를 정렬한다.
label.pin.left().right().vCenter().height(100)&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;2. &lt;b&gt;hCenter(horizontal)&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;1224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eATYZR/btrshyB8cNV/HukHA6eGCFWdIlaIpKCQTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eATYZR/btrshyB8cNV/HukHA6eGCFWdIlaIpKCQTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eATYZR/btrshyB8cNV/HukHA6eGCFWdIlaIpKCQTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeATYZR%2FbtrshyB8cNV%2FHukHA6eGCFWdIlaIpKCQTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;109&quot; height=&quot;199&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;1224&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;(_offset: CGFloat), (_offset: Percent), ()&lt;/blockquote&gt;
&lt;pre id=&quot;code_1643722870899&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// top, bottom을 superView에 
// x축(horizontal) 중앙에 맞추고
// 너비를 100을 준다
label.pin().top().bottom().hCenter().width(100)&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;/p&gt;
&lt;pre id=&quot;code_1643724108480&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;   view.pin.top(20).bottom(20)   // The view has a top margin and a bottom margin of 20 pixels 
   view.pin.top().left()         // The view is pinned directly on its parent top and left edge
   view.pin.all()                // The view fill completely its parent (horizontally and vertically)
   view.pin.all(pin.safeArea)    // The view fill completely its parent safeArea 
   view.pin.top(25%).hCenter()   // The view is centered horizontally with a top margin of 25%
   view.pin.left(12).vCenter()   // The view is centered vertically
   view.pin.start(20).end(20)    // Support right-to-left languages.
   view.pin.horizontally(20)     // The view is filling its parent width with a left and right margin.
   view.pin.top().horizontally() // The view is pinned at the top edge of its parent and fill it horizontally.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;Layout multiple edges relative to superview&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pinLayout은 topLeft, topCenter 등 좀 더 직관적인 네이밍으로 코드를 간결하게 줄여준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;1340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wFJEg/btrr3kebsI0/zBxBtDEyYjKmWb10XNC2h1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wFJEg/btrr3kebsI0/zBxBtDEyYjKmWb10XNC2h1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wFJEg/btrr3kebsI0/zBxBtDEyYjKmWb10XNC2h1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwFJEg%2Fbtrr3kebsI0%2FzBxBtDEyYjKmWb10XNC2h1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;151&quot; height=&quot;280&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;1340&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;(_ offset: CGFloat)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 우측 상단으로 뷰를 붙이고 싶다면 이렇게 코드를 작성해야한다.&lt;/p&gt;
&lt;pre id=&quot;code_1643724506599&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;viewA.pin.top().right().size(100)&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;하지만 pinLayout에서 제공하는 키워드를 이용하면 좀 더 간결한 코드 작성이 가능해진다.&lt;/p&gt;
&lt;pre id=&quot;code_1643725472322&quot; class=&quot;css&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;viewA.pin.topRight().size(100)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9tK4f/btrr5VMJvQf/WzVRVUKp4UTdMu4kkLDRI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9tK4f/btrr5VMJvQf/WzVRVUKp4UTdMu4kkLDRI0/img.png&quot; data-alt=&quot;모두 같은 방식으로 사용이 가능하다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9tK4f/btrr5VMJvQf/WzVRVUKp4UTdMu4kkLDRI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9tK4f%2Fbtrr5VMJvQf%2FWzVRVUKp4UTdMu4kkLDRI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;592&quot; height=&quot;269&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;모두 같은 방식으로 사용이 가능하다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 얘는 percent나 UIEdgeInset 적용은 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얘네도 있음! 비슷한 친구들~&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;harr;️ &amp;lt;&amp;lt; 요 표시는 AutoLayout의 leading, trailing과 비슷한 개념이 적용된다는 뜻인데&lt;br /&gt;우리나라는 글을 읽는 방향이 left to right라서 topStart와 topLeft가 똑같지만&lt;br /&gt;글을 읽는 방향이 right to left인 나라에서는 topStart == topRight가 된다.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;topStart(_ offset: CGFloat)&amp;nbsp;/&amp;nbsp;topStart() &amp;harr;️&lt;/li&gt;
&lt;li&gt;topEnd(_ offset: CGFloat)&lt;span&gt;&amp;nbsp;&lt;/span&gt;/&lt;span&gt;&amp;nbsp;&lt;/span&gt;topEnd()&lt;span&gt; &amp;harr;️&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;bottomStart(_ offset: CGFloat)&lt;span&gt;&amp;nbsp;&lt;/span&gt;/&lt;span&gt;&amp;nbsp;&lt;/span&gt;bottomStart() &amp;harr;️&lt;/li&gt;
&lt;li&gt;bottomEnd(_ offset: CGFloat)&lt;span&gt;&amp;nbsp;&lt;/span&gt;/&lt;span&gt;&amp;nbsp;&lt;/span&gt;bottomEnd() &amp;harr;️&lt;/li&gt;
&lt;li&gt;centerStart(_ offset: CGFloat)&lt;span&gt;&amp;nbsp;&lt;/span&gt;/&lt;span&gt;&amp;nbsp;&lt;/span&gt;centerStart() &amp;harr;️&lt;/li&gt;
&lt;li&gt;centerEnd(_ offset: CGFloat)&lt;span&gt;&amp;nbsp;&lt;/span&gt;/&lt;span&gt;&amp;nbsp;&lt;/span&gt;centerEnd()&lt;span&gt; &amp;harr;️&lt;/span&gt;&lt;/li&gt;
&lt;/ul&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;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Relative Edges layout&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pinLayout은 다른 뷰들과 상대적인 포지션을 잡는 메서드들이 존재한다. 그래서 pinLayout을 사용하면 view가 다른 view 옆에 위치해야하거나 위에 위치해야 할 때 보다 편리하게 하나의 키워드로 작성이 가능하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;snapkit을 사용했다면 하나씩 잡아줘야해서 코드가 길어졌을 것이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Methods: 이런 메소드가 존재한다.&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;above(of: UIView)&lt;span&gt;&amp;nbsp;&lt;/span&gt;/&lt;span&gt;&amp;nbsp;&lt;/span&gt;above(of: [UIView])&lt;/li&gt;
&lt;li&gt;below(of: UIView)&lt;span&gt;&amp;nbsp;&lt;/span&gt;/&lt;span&gt;&amp;nbsp;&lt;/span&gt;below(of: [UIView])&lt;/li&gt;
&lt;li&gt;before(of: UIView)&lt;span&gt;&amp;nbsp;&lt;/span&gt;/&lt;span&gt;&amp;nbsp;&lt;/span&gt;before(of: [UIView])&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;harr;️&lt;/li&gt;
&lt;li&gt;after(of: UIView)&lt;span&gt;&amp;nbsp;&lt;/span&gt;/&lt;span&gt;&amp;nbsp;&lt;/span&gt;after(of: [UIView])&amp;nbsp;&amp;harr;️&lt;/li&gt;
&lt;li&gt;left(of: UIView)&lt;span&gt;&amp;nbsp;&lt;/span&gt;/&lt;span&gt;&amp;nbsp;&lt;/span&gt;left(of: [UIView])&lt;/li&gt;
&lt;li&gt;right(of: UIView)&lt;span&gt;&amp;nbsp;&lt;/span&gt;/&lt;span&gt;&amp;nbsp;&lt;/span&gt;right(of: [UIView])&lt;/li&gt;
&lt;/ul&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;span style=&quot;background-color: #dddddd;&quot;&gt;[Example 1]&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;1340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b07upr/btrshyPI2kF/ZnAHBzttHa9fwBQZOirtHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b07upr/btrshyPI2kF/ZnAHBzttHa9fwBQZOirtHK/img.png&quot; data-alt=&quot;이런 레이아웃을 잡으려면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b07upr/btrshyPI2kF/ZnAHBzttHa9fwBQZOirtHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb07upr%2FbtrshyPI2kF%2FZnAHBzttHa9fwBQZOirtHK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;256&quot; height=&quot;475&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;1340&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이런 레이아웃을 잡으려면&lt;/figcaption&gt;
&lt;/figure&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1643726777098&quot; class=&quot;css&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;oneView.pin.topLeft().size(200)
twoView.pin.after(of: self.oneView).size(100)
threeView.pin.after(of: self.oneView).below(of: self.twoView).size(50)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;[Example 2]&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;379&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhV4Cw/btrsi8J0yqx/q9ZeCdMkayIqlbwvmLI5HK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhV4Cw/btrsi8J0yqx/q9ZeCdMkayIqlbwvmLI5HK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhV4Cw/btrsi8J0yqx/q9ZeCdMkayIqlbwvmLI5HK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhV4Cw%2Fbtrsi8J0yqx%2Fq9ZeCdMkayIqlbwvmLI5HK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;379&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;379&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 다른 예시로, C라는 뷰가 오른쪽처럼 되려면 아래의 세가지 스타일의 방법이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;세번째 줄의 코드에 horizontallyBetween에 주목하자 저렇게 쓰면 코드 길이가 또 한번 줄어든다&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;같은 방식으로 verticallyBetween도 있다.&lt;br /&gt;horizontallyBetween(:UIView, and: UIView,&amp;nbsp;aligned: VerticalAlign)&lt;br /&gt;verticallyBetween(:UIView, and: UIView,&amp;nbsp;aligned: HorizontalAlign)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1643728106310&quot; class=&quot;css&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;viewC.pin.top().right(of: viewA).left(of: viewB).margin(10)

viewC.pin.top().left(to: viewA.edge.right).right(to: viewB.edge.left). margin(10)

viewC.pin.horizontallyBetween(viewA, and: viewB, aligned: .top).marginHorizontal(10)&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;alignment&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;HorizontalAlignment(.left / .center / .right / .start &amp;harr;️/ .end &amp;harr;️)&lt;/li&gt;
&lt;li&gt;VerticalAlignment(.top / .center / .bottom)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;Methods:&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;above(of: UIView, aligned: HorizontalAlignment)&lt;br /&gt;above(of: [UIView], aligned: HorizontalAlignment)&lt;/li&gt;
&lt;li&gt;below(of: UIView, aligned: HorizontalAlignment)&lt;br /&gt;below(of: [UIView], aligned: HorizontalAlignment)&lt;/li&gt;
&lt;li&gt;before(of: UIView, aligned: HorizontalAlignment)&amp;harr;️&lt;br /&gt;before(of: [UIView], aligned: HorizontalAlignment)&amp;harr;️&lt;/li&gt;
&lt;li&gt;after(of: UIView, aligned: HorizontalAlignment)&amp;harr;️&lt;br /&gt;after(of: [UIView], aligned: HorizontalAlignment)&amp;harr;️&lt;/li&gt;
&lt;li&gt;left(of: UIView, aligned: VerticalAlignment)&lt;br /&gt;left(of: [UIView], aligned: HorizontalAlignment)&lt;/li&gt;
&lt;li&gt;right(of: UIView, aligned: VerticalAlignment)&lt;br /&gt;right(of: [UIView], aligned: HorizontalAlignment)&lt;/li&gt;
&lt;/ul&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;span style=&quot;background-color: #dddddd;&quot;&gt;[Example 3]&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;1340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5674w/btrsktNV1KE/p45FKUBCazV83ivXdvAYJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5674w/btrsktNV1KE/p45FKUBCazV83ivXdvAYJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5674w/btrsktNV1KE/p45FKUBCazV83ivXdvAYJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5674w%2FbtrsktNV1KE%2Fp45FKUBCazV83ivXdvAYJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;270&quot; height=&quot;501&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;1340&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1643728739314&quot; class=&quot;css&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;oneView.pin.topLeft().size(200)
twoView.pin.after(of: self.oneView, aligned: .bottom).size(100)
threeView.pin.below(of: self.twoView, aligned: .right).size(50)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Edge&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cngqOj/btrslV4Jg9T/QkBhtMRmItgUua75E2N5v0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cngqOj/btrslV4Jg9T/QkBhtMRmItgUua75E2N5v0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cngqOj/btrslV4Jg9T/QkBhtMRmItgUua75E2N5v0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcngqOj%2FbtrslV4Jg9T%2FQkBhtMRmItgUua75E2N5v0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;412&quot; height=&quot;171&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;530&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;지금 뷰를 다른 뷰의 오른쪽에 맞추고싶어, 왼쪽에 맞추고싶어&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Edges(top, vCenter, bottom, left, hCenter, right, start, end)를 이용해서 레이아웃 잡기&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;Methods:&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;top(to edge: ViewEdge)&lt;/li&gt;
&lt;li&gt;vCenter(to edge: ViewEdge)&lt;/li&gt;
&lt;li&gt;bottom(to edge: ViewEdge)&lt;/li&gt;
&lt;li&gt;left(to: edge: ViewEdge)&lt;/li&gt;
&lt;li&gt;hCenter(to: edge: ViewEdge)&lt;/li&gt;
&lt;li&gt;right(to: edge: ViewEdge)&lt;/li&gt;
&lt;li&gt;start(to: edge: ViewEdge)&amp;harr;️&lt;/li&gt;
&lt;li&gt;end(to: edge: ViewEdge)&amp;harr;️&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;[Example 1]&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;235&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVwi1H/btrr9gQCupp/weuHgQ8i00Aey7GHuUNJA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVwi1H/btrr9gQCupp/weuHgQ8i00Aey7GHuUNJA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVwi1H/btrr9gQCupp/weuHgQ8i00Aey7GHuUNJA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVwi1H%2Fbtrr9gQCupp%2FweuHgQ8i00Aey7GHuUNJA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;431&quot; height=&quot;120&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;235&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1643769740051&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;aView.pin
    .top(to: bView.edge.top)
    .hCenter(to: bView.edge.hCenter)
    .marginTop(10)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Anchors&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PSn8d/btrr9gXljAw/BBGTWkbeebOEFlfID6K15k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PSn8d/btrr9gXljAw/BBGTWkbeebOEFlfID6K15k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PSn8d/btrr9gXljAw/BBGTWkbeebOEFlfID6K15k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPSn8d%2Fbtrr9gXljAw%2FBBGTWkbeebOEFlfID6K15k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;367&quot; height=&quot;167&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;지금 뷰를 다른 뷰의 오른쪽위, 왼쪽아래에 맞추고싶어&quot;&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;Methods:&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;topLeft(to anchor: Anchor)&lt;/li&gt;
&lt;li&gt;topCenter(to anchor: Anchor)&lt;/li&gt;
&lt;li&gt;topRight(to anchor: Anchor)&lt;/li&gt;
&lt;li&gt;topStart(to anchor: Anchor)&amp;harr;️&lt;/li&gt;
&lt;li&gt;topEnd(to anchor: Anchor)&amp;harr;️&lt;/li&gt;
&lt;li&gt;centerLeft(to anchor: Anchor)&lt;/li&gt;
&lt;li&gt;center(to anchor: Anchor)&lt;/li&gt;
&lt;li&gt;centerRight(to anchor: Anchor)&lt;/li&gt;
&lt;li&gt;centerStart(to anchor: Anchor)&amp;harr;️&lt;/li&gt;
&lt;li&gt;centerEnd(to anchor: Anchor)&amp;harr;️&lt;/li&gt;
&lt;li&gt;bottomLeft(to anchor: Anchor)&lt;/li&gt;
&lt;li&gt;bottomCenter(to anchor: Anchor)&lt;/li&gt;
&lt;li&gt;bottomRight(to anchor: Anchor)&lt;/li&gt;
&lt;li&gt;bottomStart(to anchor: Anchor)&amp;harr;️&lt;/li&gt;
&lt;li&gt;bottomEnd(to anchor: Anchor)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;[Example 1]&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1643770028172&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	viewB.pin.center(to: viewA.anchor.topRight)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;386&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUoRvD/btrr3j7tB1B/EekTb1X5QjO8vkjHoJMDQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUoRvD/btrr3j7tB1B/EekTb1X5QjO8vkjHoJMDQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUoRvD/btrr3j7tB1B/EekTb1X5QjO8vkjHoJMDQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUoRvD%2Fbtrr3j7tB1B%2FEekTb1X5QjO8vkjHoJMDQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;620&quot; height=&quot;386&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;386&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Width, Height&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;너비, 높이를 지정하고 싶어&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Basic&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;width(:CGFloat)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;/&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;width(:Percent) /&amp;nbsp;&lt;/b&gt;&lt;b&gt;width(of: UIView)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;height(:CGFloat)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;/&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;height(:Percent) /&amp;nbsp;&lt;/b&gt;&lt;b&gt;height(of: UIView)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;size(:CGSize)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;/&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;size(:Percent)&lt;/b&gt;&amp;nbsp;/&amp;nbsp;&lt;b&gt;size(_ sideLength: CGFloat)&lt;/b&gt;&amp;nbsp;/&amp;nbsp;&lt;b&gt;size(of: UIView)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;[Example]&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1643771810534&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;view.pin.width(100)
view.pin.width(50%)
view.pin.width(of: view1)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Max/Min&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;minWidth(:CGFloat)&lt;/b&gt;&amp;nbsp;/&amp;nbsp;&lt;b&gt;minWidth(:Percent)&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;maxWidth(:CGFloat)&lt;/b&gt;&amp;nbsp;/&amp;nbsp;&lt;b&gt;maxWidth(:Percent)&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;minHeight(:CGFloat)&lt;/b&gt;&amp;nbsp;/&amp;nbsp;&lt;b&gt;minHeight(:Percent)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;maxHeight(:CGFloat)&lt;/b&gt;&amp;nbsp;/&amp;nbsp;&lt;b&gt;maxHeight(:Percent)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;[Example]&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;375&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DebC1/btrr7p8as39/edspR390tNOXRKUmRQw4Z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DebC1/btrr7p8as39/edspR390tNOXRKUmRQw4Z1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DebC1/btrr7p8as39/edspR390tNOXRKUmRQw4Z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDebC1%2Fbtrr7p8as39%2FedspR390tNOXRKUmRQw4Z1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;670&quot; height=&quot;375&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;375&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1643772136356&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;viewA.pin.top(20).hCenter().width(100%).maxWidth(200)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;justify()라는 키워드를 사용해도 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;- justify(_ : HorizontalAlign)&lt;br /&gt;- align(_ : VerticalAlign)&lt;/blockquote&gt;
&lt;pre id=&quot;code_1643772147324&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;viewA.pin.top(20).horizontally().maxWidth(200).justify(.center)&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Adjusting Size&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;내용에 맞게 size를 유동적으로 조절하고싶어&quot;&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;sizeToFit()&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;sizeToFit(: FitType)&lt;/b&gt;&lt;br /&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;&lt;b&gt;Parameter&lt;span&gt;&amp;nbsp;&lt;/span&gt;fitType&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;.width&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;.height&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;.widthFlexible&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;.heightFlexible&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;[Example]&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1643771958969&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; // Adjust the view's size based on the result of `UIView.sizeToFit()` and center it.
 view.pin.center().sizeToFit()

 // Adjust the view's size based on a width of 100 pixels.
 // The resulting width will always match the pinned property `width(100)`.
 view.pin.width(100).sizeToFit(.width)

 // Adjust the view's size based on view's current width.
 // The resulting width will always match the view's original width.
 // The resulting height will never be bigger than the specified `maxHeight`.
 view.pin.sizeToFit(.width).maxHeight(100)

 // Adjust the view's size based on 100% of the superview's height.
 // The resulting height will always match the pinned property `height(100%)`.
 view.pin.height(100%).sizeToFit(.height)

// Adjust the view's size based on view's current height.
// The resulting width will always match the view's original height.
view.pin.sizeToFit(.height)

// Since `.widthFlexible` has been specified, its possible that the resulting
// width will be smaller or bigger than 100 pixels, based of the label's sizeThatFits()
// method result.
label.pin.width(100).sizeToFit(.widthFlexible)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;[Example 2]&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;303&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byTngX/btrskEBWvBT/QGC518sIXfJXzWQHSloeR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byTngX/btrskEBWvBT/QGC518sIXfJXzWQHSloeR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byTngX/btrskEBWvBT/QGC518sIXfJXzWQHSloeR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyTngX%2FbtrskEBWvBT%2FQGC518sIXfJXzWQHSloeR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;303&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;303&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1643772025566&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	label.pin.after(of: image, aligned: .top).right().marginHorizontal(10).sizeToFit(.width)&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Aspect Ratio&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;너비나 높이만 지정하고 비율로 사이즈를 잡고싶어&quot;&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;aspectRatio(_ ratio: CGFloat)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;aspectRatio(of view: UIView)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;aspectRatio()&lt;/b&gt;:&lt;br /&gt;UIImageView에서 사용했을 때에만 image의 dimension을 사용하여 비율을 잡아준다. 다른 타입의 view들에서 이 메서드는 영향이 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;[Example]&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;809&quot; data-origin-height=&quot;232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KUdNe/btrsaSaT39E/A9SfucVwz3ASdZnhNWrsLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KUdNe/btrsaSaT39E/A9SfucVwz3ASdZnhNWrsLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KUdNe/btrsaSaT39E/A9SfucVwz3ASdZnhNWrsLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKUdNe%2FbtrsaSaT39E%2FA9SfucVwz3ASdZnhNWrsLk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;317&quot; height=&quot;91&quot; data-origin-width=&quot;809&quot; data-origin-height=&quot;232&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1643772372458&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;imageView.pin.top().hCenter().width(50%).aspectRatio()&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;safeArea, readable and layout margins&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UIKit은 3 종류의 area/guide가 존재하는데 PinLayout은 이걸 다음과 같이 표현한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;UIView.pin.safeArea&lt;/b&gt;: UIKit's&lt;span&gt;&amp;nbsp;&lt;/span&gt;UIView.safeAreaInsets&lt;span&gt;&amp;nbsp;&lt;/span&gt;/&lt;span&gt;&amp;nbsp;&lt;/span&gt;UIView.safeAreaLayoutGuide.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UIView.pin.readableMargins&lt;/b&gt;: UIKit's&lt;span&gt;&amp;nbsp;&lt;/span&gt;UIView.readableContentGuide.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UIView.pin.layoutMargins&lt;/b&gt;: UIKit's&lt;span&gt;&amp;nbsp;&lt;/span&gt;UIView.layoutMargins&lt;span&gt;&amp;nbsp;&lt;/span&gt;/&lt;span&gt;&amp;nbsp;&lt;/span&gt;UIView.layoutMarginsGuide.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1023&quot; data-origin-height=&quot;770&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wWebx/btrsaTARbMX/afKOT1eHI5QlPMraOks4lK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wWebx/btrsaTARbMX/afKOT1eHI5QlPMraOks4lK/img.png&quot; data-alt=&quot;ipad에서 나타낸 영역과 가이드들&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wWebx/btrsaTARbMX/afKOT1eHI5QlPMraOks4lK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwWebx%2FbtrsaTARbMX%2FafKOT1eHI5QlPMraOks4lK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;684&quot; height=&quot;515&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1023&quot; data-origin-height=&quot;770&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ipad에서 나타낸 영역과 가이드들&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UIView's transforms&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지는 .pin이라는 키워드를 사용했지만 .pinFrame이라는 키워드도 존재합니다.&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;아까 글의 서두에서 유의사항을 다룰 때 PinLayout은 수동으로 레이아웃을 잡기 때문에 회전과 같은 상황에서도 직접 처리를 해주어야 한다는 것을 암시했어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.pinFrame은 &lt;b&gt;뷰의 변환이 일어나는 상황&lt;/b&gt;(UIView.transform, scailing, rotation(회전))에서 사용됩니다.&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;[pin]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변화가 일어나지 않는 뷰의 사이즈와 위치를 잡는다. pin을 사용하면 변화가 일어나기 전에 레이아웃이 적용된다. 따라서 만약 자체적인 레이아웃의 수정 없이 transform을 사용하여 애니메이션을 주고 싶은 상황에서는 유용하게 사용될 수 있다.&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;[pinFrame]&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;[Example]&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;475&quot; data-origin-height=&quot;365&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxXpwB/btrsaR4ebIj/RZZSHEdZ0B1I9foxepkKyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxXpwB/btrsaR4ebIj/RZZSHEdZ0B1I9foxepkKyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxXpwB/btrsaR4ebIj/RZZSHEdZ0B1I9foxepkKyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxXpwB%2FbtrsaR4ebIj%2FRZZSHEdZ0B1I9foxepkKyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;160&quot; height=&quot;365&quot; data-origin-width=&quot;475&quot; data-origin-height=&quot;365&quot;/&gt;&lt;/span&gt;&lt;/figure&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;&amp;nbsp;&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;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;.pin을 사용하면?&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;  view.transform = .init(rotationAngle: CGFloat.pi / 2)
  view.pin.center().width(100).height(50)&lt;/code&gt;&lt;/pre&gt;
&lt;figure class=&quot;imageblock floatLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;475&quot; data-origin-height=&quot;365&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxuzIL/btrskvd9IcY/KrzUyTCGitM2PaYL1iWqYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxuzIL/btrskvd9IcY/KrzUyTCGitM2PaYL1iWqYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxuzIL/btrskvd9IcY/KrzUyTCGitM2PaYL1iWqYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxuzIL%2Fbtrskvd9IcY%2FKrzUyTCGitM2PaYL1iWqYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;160&quot; height=&quot;365&quot; data-origin-width=&quot;475&quot; data-origin-height=&quot;365&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;뷰의 레이아웃이 잡히고 나서 transform이 이루어지기 때문에 처음 잡았던 레이아웃이 유지된채로 회전이 일어난다.&amp;nbsp;&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;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;.pinFrame을 사용하면?&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;  view.transform = .init(rotationAngle: CGFloat.pi / 2)
  view.pinFrame.center().width(100).height(50)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;475&quot; data-origin-height=&quot;365&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SWCZ6/btrskvkYuOS/KDfT7pO7FkbbPO5PgLTkb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SWCZ6/btrskvkYuOS/KDfT7pO7FkbbPO5PgLTkb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SWCZ6/btrskvkYuOS/KDfT7pO7FkbbPO5PgLTkb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSWCZ6%2FbtrskvkYuOS%2FKDfT7pO7FkbbPO5PgLTkb0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;160&quot; height=&quot;365&quot; data-origin-width=&quot;475&quot; data-origin-height=&quot;365&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;transform이 일어난 뒤에 뷰의 레이아웃이 잡히기 때문에 회전에 따라서 뷰의 레이아웃도 변경된다.&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;&amp;nbsp;&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;&amp;nbsp;&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;p&gt;&lt;figure class=&quot;imageblock floatLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;484&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/epOOZh/btrr6Ano1kr/DhBKpneWR6bLTvI2D5wYgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/epOOZh/btrr6Ano1kr/DhBKpneWR6bLTvI2D5wYgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/epOOZh/btrr6Ano1kr/DhBKpneWR6bLTvI2D5wYgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FepOOZh%2Fbtrr6Ano1kr%2FDhBKpneWR6bLTvI2D5wYgK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;160&quot; height=&quot;374&quot; data-origin-width=&quot;484&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1643776246304&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  aView.transform = .init(scaleX: 1.5, y: 1.5)
  aView.pin.width(100).height(50)
  bView.pin.below(of: aView, aligned: .left)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;viewB가 변환 전인 A의 below에 레이아웃되었따.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;239&quot; data-origin-height=&quot;185&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d22jcF/btrslU5RGaH/tFqNcnv87350BlPPAnkn50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d22jcF/btrslU5RGaH/tFqNcnv87350BlPPAnkn50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d22jcF/btrslU5RGaH/tFqNcnv87350BlPPAnkn50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd22jcF%2FbtrslU5RGaH%2FtFqNcnv87350BlPPAnkn50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;160&quot; height=&quot;185&quot; data-origin-width=&quot;239&quot; data-origin-height=&quot;185&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1643776288546&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;aView.transform = .init(scaleX: 1.5, y: 1.5)
aView.pin.width(100).height(50)
bView.pinFrame.below(of: aView, aligned: .left)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;viewB가 변환 이후의 A의 below에 레이아웃되었다.&lt;/p&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Warning&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PinLayout은 가능하지 않은 pin rule이 적용될 수 없거나 유효하지 않는 경우에 대해서 콘솔에 warning메시지를 띄워준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;116&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhu8EL/btrsaRXr4b5/NS5yIHrBO8kgcA0hxenk30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhu8EL/btrsaRXr4b5/NS5yIHrBO8kgcA0hxenk30/img.png&quot; data-alt=&quot;요런 로그가 찍힘&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhu8EL/btrsaRXr4b5/NS5yIHrBO8kgcA0hxenk30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbhu8EL%2FbtrsaRXr4b5%2FNS5yIHrBO8kgcA0hxenk30%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;456&quot; height=&quot;116&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;116&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;요런 로그가 찍힘&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;warning을 끌 수도 있는데 기본적으로는 나오도록 설정이 되어있다.&lt;/p&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PinLayout Guide&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가이드도 따로 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 같은 순서로 작성해야한다. 코드를 항상 같은 형식으로 유지함으로써 좀 더 이해하기 쉽게 하기위한 목적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;view.pin.[EDGE|ANCHOR|RELATIVE].[WIDTH|HEIGHT|SIZE].[pinEdges()].[MARGINS].[sizeToFit()]&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;로직도 이 순서대로 적용이 된다. pinEdges()가 margin 전에 적용되고 sizeToFit()이 그 이후에 적용된다.&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;EDGE&lt;/b&gt;도 아래의 순서를 지켜주면 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TOP, BOTTOM, LEFT, RIGHT&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PinLayout에 대해 전반적으로 훑어보는 시간을 가져보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SnapKit을 더 많은 사람들이 사용하고 있지만 PinLayout만이 가진 장점들이 존재하기 때문에 충분히 고려할 만한 선택지일 것 같다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>iOS</category>
      <author>snowe</author>
      <guid isPermaLink="true">https://snowee.tistory.com/63</guid>
      <comments>https://snowee.tistory.com/63#entry63comment</comments>
      <pubDate>Wed, 2 Feb 2022 12:30:53 +0900</pubDate>
    </item>
    <item>
      <title>2021년 회고(이달의 앱, 졸업, 취준)</title>
      <link>https://snowee.tistory.com/62</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 2021년도 역시나 코로나와 함께였다. 내 첫 회고가 2020년인데 지금이 네번째 회고임에도 아직도 코로나와 함께라니.. 올해는 꼭 종식되었으면 좋겠다.&lt;br&gt;&lt;br&gt;올해는 힘든 순간도 많았지만 이전의 힘든 순간들을 보상받은 날들도 많았던 것 같다. 그게 더 힘을 낼 수 있는 원동력이 되어주었고 그렇게 2021년에도 나름의 성과와 함께 행복한 연말을 보낼 수 있었다.&lt;br&gt;&lt;br&gt;글의 제목에 써 놓았듯 이번 회고의 키워드는 크게 3가지이다. &lt;br&gt;&lt;br&gt;&lt;b&gt;이달의 앱, 졸업, 취준&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Cherish가 가져다 준 선물, 이달의 앱&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;꽤 오래된 일이긴 한데, 2021년 9월 15일.. 무려 Apple 가을 이벤트가 열리고 있는 순간에 Cherish 단톡방이 울렸다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;3078&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ndkqk/btrr2TOhxuh/6MsQOFRAq4dzMounaMQkU0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ndkqk/btrr2TOhxuh/6MsQOFRAq4dzMounaMQkU0/img.jpg&quot; data-alt=&quot; 아직도 생생해 &quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ndkqk/btrr2TOhxuh/6MsQOFRAq4dzMounaMQkU0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fndkqk%2Fbtrr2TOhxuh%2F6MsQOFRAq4dzMounaMQkU0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;254&quot; height=&quot;668&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;3078&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt; 아직도 생생해 &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 좀 안믿겼다. 왜 올라간거지? 싶었고.. 그치만! 우리가 그만큼 열심히 했고 나는 정말 애정을 가지고 있었던 서비스였기에 뿌듯함과 자랑스러움이 몰려왔다.&lt;br&gt;&lt;br&gt;그렇게 시작을 한 Cherish는..&lt;/p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byNnjz/btrsc1keayF/kWBbuRbAOH0AwwaoD7qKoK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byNnjz/btrsc1keayF/kWBbuRbAOH0AwwaoD7qKoK/img.jpg&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;1547&quot; style=&quot;width: 25.777649%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byNnjz/btrsc1keayF/kWBbuRbAOH0AwwaoD7qKoK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyNnjz%2Fbtrsc1keayF%2FkWBbuRbAOH0AwwaoD7qKoK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1124&quot; height=&quot;1547&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tFX0j/btrsfXhjofh/thuKXJ4WEhh5jTo64f52X1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tFX0j/btrsfXhjofh/thuKXJ4WEhh5jTo64f52X1/img.png&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;1945&quot; style=&quot;width: 21.341925%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tFX0j/btrsfXhjofh/thuKXJ4WEhh5jTo64f52X1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtFX0j%2FbtrsfXhjofh%2FthuKXJ4WEhh5jTo64f52X1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1170&quot; height=&quot;1945&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lXuIQ/btrsc1LhzgM/kRbKINTsJz53uKeiS8E0xK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lXuIQ/btrsc1LhzgM/kRbKINTsJz53uKeiS8E0xK/img.png&quot; data-origin-width=&quot;2240&quot; data-origin-height=&quot;1572&quot; style=&quot;width: 50.554844%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lXuIQ/btrsc1LhzgM/kRbKINTsJz53uKeiS8E0xK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlXuIQ%2Fbtrsc1LhzgM%2FkRbKINTsJz53uKeiS8E0xK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2240&quot; height=&quot;1572&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;Cherish, 최고의 플레이&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;b&gt;장난아니게 됐다.&lt;/b&gt;&lt;br&gt;&lt;b&gt;(오늘의 앱은 심지어 두번 올라옴)&lt;/b&gt;&lt;br&gt;&lt;br&gt;처음에 우리가 앱을 출시하기로 했던 이유는 3주라는 기간동안 팀원간의 화합이 너무 좋았고, 모두 서비스를 개발하는 기간과 서비스에 대한 애정이 비례하여 늘어났기때문이다. 그래서 Cherish의 끝맺음을 앱 출시로 결정했었다.&lt;br&gt;근데 위와 같은 놀라운 일들이 연속적으로 일어났고, 이로 인해 나는 사람들이 광고를 왜 맡기는지 알게 되었다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2844&quot; data-origin-height=&quot;1314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qQzO5/btrr2WEYrqR/j59ZFq1XY2sXRy9sevJiek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qQzO5/btrr2WEYrqR/j59ZFq1XY2sXRy9sevJiek/img.png&quot; data-alt=&quot; 01.30일 현재 다운로드 수 &quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qQzO5/btrr2WEYrqR/j59ZFq1XY2sXRy9sevJiek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqQzO5%2Fbtrr2WEYrqR%2Fj59ZFq1XY2sXRy9sevJiek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2844&quot; height=&quot;1314&quot; data-origin-width=&quot;2844&quot; data-origin-height=&quot;1314&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt; 01.30일 현재 다운로드 수 &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;잔잔하다가 갑자기 송곳처럼 튀어오르는 부분이 앱스토어에 배너가 걸린 날이다.&lt;br&gt;앱스토어 덕분에 많은 사용자가 유입되었고 덕분에 아직까지도 MAU가 평균 250정도를 유지하고 있다.&lt;br&gt;&lt;br&gt;&lt;b&gt;이렇게 사랑해주시는데 가만히 있을 수 없지..&lt;/b&gt;&lt;br&gt;&lt;br&gt;바로 회의를 했고 &lt;b&gt;어떤 점을 개선해야하고 어떤 기능을 추가할지 논의하였다.&lt;/b&gt;&lt;br&gt;기존 앱에 남아있던 잔버그를 개선하였고 유저들이 보내준 피드백들을 취합하여 현재 팀 상황에서 빠르게 적용 가능한 기능들을 우선적으로 반영하였다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;1252&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dSNaif/btrr7qE5sVh/NKXqkA3s4nppkFbS09TLU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dSNaif/btrr7qE5sVh/NKXqkA3s4nppkFbS09TLU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dSNaif/btrr7qE5sVh/NKXqkA3s4nppkFbS09TLU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdSNaif%2Fbtrr7qE5sVh%2FNKXqkA3s4nppkFbS09TLU1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;334&quot; height=&quot;309&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;1252&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;이 유지보수 과정 속에서 나는 정말 값진 경험을 하였다.&lt;br&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;우리가 해왔던 git flow의 문제점&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;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt; &lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;현 아키텍쳐의 문제점&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 MVC로 개발해왔다. 이유를 묻는다면 Cherish는 내 첫 iOS 프로젝트였고, 아직 아키텍쳐를 깊게 공부할 수 있는 수준이 되지 못했었다. 앱이 잘 돌아가도록 완성하는 것이 가장 큰 목표였기에 MVC를 택해서 개발하였다. 그리고 무엇보다 다른 아키텍쳐의 필요성을 느끼고있지 못했다.&lt;br&gt;&lt;br&gt;하지만 유지보수를 하면서 정말 &lt;b&gt;M&lt;/b&gt;assive &lt;b&gt;V&lt;/b&gt;iew &lt;b&gt;C&lt;/b&gt;ontroller라는 말을 직접적으로 깨닫게 되었다. 버그가 발생했을 때 내가 예전에 쓴 코드는 정말 잘 기억이 나지 않았다. 그렇다고 테스트를 할 수도 없었다. UI와 비즈니스 로직이 너무 엮여있었기 때문에..&lt;br&gt;&lt;br&gt;그래도 난 오히려 좋았다. 공부의 필요성을 느끼고 공부를 시작하는 것과 그냥 공부하는 것은 분명히 다르기 때문이다. 요즘 올라오는 글을 보면 알겠지만 지금은 RIBs를 공부중이다.&lt;br&gt; &lt;br&gt;MVC를 하다가 새로운 아키텍쳐를 공부하니까 괜히 뭔가 더 대단해진 것 같고 그렇다..ㅋㅋ Cherish도 연습삼아 바꿔보아도 재미있을듯!&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt; &lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Git Flow의 문제점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나름 커밋 컨벤션도 만들고 했지만 어느정도 시간이 지나고 git에 대한 공부가 이루어진 뒤 보니 그때의 협업 방법이 최선이 아니라는 것을 알게 되었다. PR을 그저 merge의 수단으로만 사용했고 심지어는 PR없이 merge만 했을 때도 많았다. merge도 squash, rebase는 커녕 그냥 merge만 했었다ㅎㅎ&lt;br&gt;&lt;br&gt;시간이 지나면서 더 효율적인 협업 방법을 알게 되었고 조금은 늦었을 수 있지만 ISSUE와 PR의 코드리뷰를 적극 활용하려 노력했다. 앱잼때처럼 Cherish에 몰두할 수 없다보니 코드리뷰가 생각처럼 활발히 되지는 못해서 조금 아쉽기는 하지만 그래도 꾸준히 노력중이다.&lt;br&gt;&lt;br&gt;merge에도 rebase와 squash에 대해 공부할 수 있었고 stash 라는 아주 좋은 녀석도 알게되었다. &lt;br&gt;Git은 알면 알수록 재밌는 것 같다. 아직 모르는 것들이 많지만 점점 알아가는 것이 재미있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt; &lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;그때의 나를 욕하고 있는 지금의 나&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜 왜 코드를 저렇게 짰나 싶다. 근데 이건 어쩔 수 없는게 내가 계속 공부를 하고 있는 동안에는 어제 짠 코드가 오늘 보면 맘에 안든다.&lt;br&gt;같은 맥락에서 내가 짠 모든 코드를 갈아엎고 싶었다. 그때는 storyboard가 편했지만 지금은 code-base가 편해졌다. 이와 비슷하게 면접 준비를 하면서 여러가지를 알고나니 코드 스타일이 정말 많이 변해있었다.&lt;br&gt;&lt;br&gt;모든 코드를 갈아 엎지는 못했지만 그래도 1/3 정도를 리팩토링 하였다. 나름 Composition도 활용하고 코드 효율성도 신경 쓰면서..했는데 지금보면 또 마음에 안들겠지?&lt;br&gt; &lt;br&gt;무튼 리팩토링과 더불어 firebase 애널리틱스, fastlane 등을 적용해서 프로젝트의 방향성도 잡아보고, 귀찮은 과정을 개선하기도 했다.&lt;br&gt;&lt;br&gt;&lt;b&gt;Cherish라는 서비스를 통해 정말 많이 성장했던 것 같다. &lt;/b&gt;&lt;br&gt;&lt;b&gt;그냥 공부하는 것 만으로는 느낄 수 없는 경험도 했고, 앱 개발자로서 얻은 인사이트가 상당하다. &lt;/b&gt;&lt;br&gt;&lt;b&gt;이제 회사다니니까 시간은 좀 부족해지더라도 꾸준히..틈틈히 개선해서 서버 비용이 의미없어지는 그 날까지.. 노력해야지&lt;/b&gt;&lt;br&gt; &lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;졸업, 취준&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2021년을 끝으로 학교의 졸업요건을 모두 갖췄다. 학점도 잘 채웠고, 졸업 작품도 복전 졸업논문도 잘 통과됐고..! 그리고 취뽀도 했다.&lt;br&gt;&lt;br&gt;4학년 1학기 부터 인턴을 넣었고 코테, 면접을 보면서 꾸준히 공부했다. &lt;b&gt;떨어질 때 마다 낙담보다는 기록을 했고&lt;/b&gt; &lt;b&gt;부족한 부분을 다시 공부하려 노력했던 것 같다.&lt;/b&gt; 다른 사람들과 비교했을 때 정말 많이 넣은건 아니지만 그래도 1학기 때 6곳 2학기 때 14 곳 정도를 넣었다.&lt;/p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2050&quot; data-origin-height=&quot;890&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buquzg/btrr3nCsZib/kdfckeKDK2TJIpq7wSdwe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buquzg/btrr3nCsZib/kdfckeKDK2TJIpq7wSdwe1/img.png&quot; data-alt=&quot; 실제로 받은 질문들과 그 질문에서 나올 수 있는 꼬리질문들을 나름대로 정리했었다 &quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buquzg/btrr3nCsZib/kdfckeKDK2TJIpq7wSdwe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbuquzg%2Fbtrr3nCsZib%2FkdfckeKDK2TJIpq7wSdwe1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2050&quot; height=&quot;890&quot; data-origin-width=&quot;2050&quot; data-origin-height=&quot;890&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt; 실제로 받은 질문들과 그 질문에서 나올 수 있는 꼬리질문들을 나름대로 정리했었다 &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;취준하면서 그래도 제일 힘들었던 순간을 꼽자면 코테 떨어질 때, 면접에서 꼬이기 시작할 때 인 것 같다.&lt;br&gt;&lt;br&gt;코테부터 이야기 하자면 나는 그닥 알고리즘적인 사고가 뛰어난 건 아닌 것 같았다. 처음엔 정말 기대조차 못할정도의 결과로 떨어졌다. 1학기때는 모든 코테에서 거의 한, 두문제 밖에 못 풀었으니까..?&lt;br&gt;&lt;br&gt;그래도 계속 꾸준히 하니까 늘긴 하더라, 2학기때는 떨어지더라도 커트에 근접하게 떨어졌고, 하다보니 IT 대기업 코테에 붙을 정도의 실력이 됐다. &lt;br&gt;&lt;b&gt;역시 알고리즘은 묵묵히 꾸준히 해야하는 것인가..&lt;/b&gt;&lt;br&gt;&lt;br&gt;채용 프로세스에는 과제 전형이 있는 곳도 있었는데, 내가 짜는 코드에 대해 명확히 파악하고 있었던 것이 코드 리뷰를 하면서 큰 도움이 되었다. 내 코드의 장점, 이렇게 짠 이유에 대해 충분히 설명이 가능했고 단점에 대해서도 받아들일 수 있었다.&lt;br&gt;&lt;br&gt;iOS로 취준을 했던 나는 iOS 면접 질문을 아카이빙 해 놓은 레포를 바탕으로 혼자 하나씩 꾸준히 공부하였다.&lt;/p&gt;
&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;GitHub - snowedev/I-am-interviewee:  ‍  iOS 기술 면접을 준비하는 레포&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot; ‍  iOS 기술 면접을 준비하는 레포. Contribute to snowedev/I-am-interviewee development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/snowedev/I-am-interviewee&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bCyM9U/hyNgg8sKcS/TNV8Gx9k6c3udkjffQkJpk/img.jpg?width=800&amp;amp;height=451&amp;amp;face=238_70_534_281&quot; data-og-url=&quot;https://github.com/snowedev/I-am-interviewee&quot;&gt;
 &lt;a href=&quot;https://github.com/snowedev/I-am-interviewee&quot; target=&quot;_blank&quot; data-source-url=&quot;https://github.com/snowedev/I-am-interviewee&quot;&gt;
  &lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bCyM9U/hyNgg8sKcS/TNV8Gx9k6c3udkjffQkJpk/img.jpg?width=800&amp;amp;height=451&amp;amp;face=238_70_534_281')&quot;&gt; 
  &lt;/div&gt;
  &lt;div class=&quot;og-text&quot;&gt;
   &lt;p class=&quot;og-title&quot;&gt;GitHub - snowedev/I-am-interviewee:  ‍  iOS 기술 면접을 준비하는 레포&lt;/p&gt;
   &lt;p class=&quot;og-desc&quot;&gt; ‍  iOS 기술 면접을 준비하는 레포. Contribute to snowedev/I-am-interviewee development by creating an account on GitHub.&lt;/p&gt;
   &lt;p class=&quot;og-host&quot;&gt;github.com&lt;/p&gt;
  &lt;/div&gt;&lt;/a&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래는 여기 있는 질문들을 다 채우고 public으로 돌리려했으나.. 공부할수록 공부할 것들이 많아지다보니 이러다간 영원히 private일 것 같아.. 일단 돌려놓고 천천히 추가하려한다.&lt;br&gt;&lt;br&gt;무튼 코테를 떨어지며 다른 진로를 고민해보기도 하고, 가까스로 간 면접에서 멘탈이 부서져도 보고, 어쩔때는 칭찬도 들으면서 자신감도 얻었었다. 나는 그렇게 취준을 마무리했다.&lt;br&gt;&lt;br&gt;요즘은 새로운 스택을 공부하느라 바쁘다. RIBs, ReactorKit, Rx, Tuist, FlexLayout 등등.. 지금까지 공부한 것들이 아닌 새롭게 익혀 실무에 적용해야하는 것이 부담으로 다가올때도 있지만 그만큼 내가 성장할 수 있는 포인트가 많아지는 것 같아서 기대되기도 한다. 그래도 다행히 새로운걸 배우는 게 아직 재미있다 :)&lt;br&gt;&lt;br&gt;지금 이렇게 쓰다보니 생략된 부분이 많은 것 같긴하지만 내가 취준을 하며 몸소 느낀 취준을 앞둔 누군가에게 해줄 수 있는 말은 명확하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
 &lt;li&gt;기업마다 인재를 보는 시각은 다르다. 면접이라는 단편적인 절차로 내가 지금까지 해 온 공부를 전부 증명하기란 어려우니 떨어졌다고 너무 자신을 한없이 깎아내리지 않았으면 좋겠다. 소리 한번 지르고 털어버리자. 세상은 넓고 좋은 기업은 생각보다 많다.&lt;/li&gt;
 &lt;li&gt;망친 면접을 잊으려고 하기 보다는 그 면접을 두번이고 세번이고 복기하면서 그날의 질문을 준비해보자. 정말 내가 부족한 부분만 찝어내는 몇 없는 면접관이다.&lt;/li&gt;
 &lt;li&gt;너무 조급해 하지 않았으면 좋겠다. 나도 정말 조급한 순간이 많았지만, 조급함 때문에 오히려 아무것도 못하는 상황이 생기더라.. 약간의 여유를 갖기 위해 노력하는건 좋은 것 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;2021년 한 해도 이렇게 지나갔다.&lt;/b&gt;&lt;br&gt;&lt;b&gt;2022년에는 개발자로서 더 성장하고, 돈도 잘 모으고, 코로나 좀 끝나서 맘 편히 놀고, 주변 사람도 잘 챙기는 한 해가 되었으면 좋겠다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>회고</category>
      <author>snowe</author>
      <guid isPermaLink="true">https://snowee.tistory.com/62</guid>
      <comments>https://snowee.tistory.com/62#entry62comment</comments>
      <pubDate>Mon, 31 Jan 2022 23:51:01 +0900</pubDate>
    </item>
    <item>
      <title>iOS) RIBs Tutorial 3 - Dependency 활용</title>
      <link>https://snowee.tistory.com/61</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;RIBs Dependency Injection and Communication&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;튜토리얼2를 마쳤다는 전제 하에 진행됩니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;튜토리얼2 까지는 RIBs의 핵심 개념들에 대해서 다루었습니다. 튜토리얼3 에서는 새로운 RIB을 만들지 않고 지금까지 만들었던 RIBs를 수정하는 방식으로 진행됩니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;788&quot; data-ke-mobilestyle=&quot;alignCenter&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGFECd/btrrF6mlORT/Vk3D9atFN1jfU35WTKsXOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGFECd%2FbtrrF6mlORT%2FVk3D9atFN1jfU35WTKsXOk%2Fimg.png&quot; width=&quot;518&quot; height=&quot;375&quot; data-filename=&quot;tree.png&quot; data-ke-mobilestyle=&quot;alignCenter&quot; /&gt;
&lt;figcaption&gt;지금까지의 RIB Tree&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Goals&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TicTacToe Game의 처음 시작 화면에 몇가지가 추가됩니다.&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;/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;Builder&lt;/b&gt;의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;build&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메서드를 통해 동적인 dependency를 하위 RIB에 넘겨주는 것&lt;/li&gt;
&lt;li&gt;Dependency Injection(DI, 의존성 주입) tree를 사용해 정적인 dependency를 넘겨주는 것
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Swfitd의 확장 기반의 의존성을 따릅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;RIB  life-cycle을 사용하여 Rx stream의 life-cycle을 관리하는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Dynamic dependencies&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;튜토리얼 1에서 우리는 로그인 형식을 만들었고 거기서 입력받은 플레이어들의 이름은 &lt;b&gt;LoggedOut&lt;/b&gt; RIB 부터 &lt;b&gt;Root&lt;/b&gt; RIB까지 전달되었습니다. 하지만 튜토리얼 2에서 우리가 새롭게 만든 뷰에서 그 데이터를 사용하지 않았어요. &lt;br /&gt;&lt;br /&gt;이번 튜토리얼에서 RIB tree를 따라서 그 데이터(플레이어 이름)를 하위 RIB인 &lt;b&gt;OffGame&lt;/b&gt;, &lt;b&gt;TicTacToe&lt;/b&gt; RIB에게 전달할거에요&lt;br /&gt;&lt;br /&gt;동적 dependency부터 시작하겠습니다!&lt;br /&gt;&lt;br /&gt;&lt;b&gt;0. LoggedInBuildable protocol&lt;/b&gt;을 아래와 같이 업데이트해서 두 플레이어의 이름을 가변 dependency로 받을 수 있도록 해주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1643156728975&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protocol LoggedInBuildable: Buildable {
    func build(withListener listener: LoggedInListener, 
               player1Name: String, 
               player2Name: String) -&amp;gt; LoggedInRouting
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.&lt;/b&gt; 방금 수정한 &lt;b&gt;LoggedInBuildable protocol&lt;/b&gt;을 준수하는 &lt;b&gt;LoggedInBuilder&lt;/b&gt;의 build메서드도 수정한대로 업데이트 해주어야겠죠?&lt;/p&gt;
&lt;pre id=&quot;code_1643156728975&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func build(withListener listener: LoggedInListener, player1Name: String, player2Name: String) -&amp;gt; LoggedInRouting {
    let component = LoggedInComponent(dependency: dependency,
                                      player1Name: player1Name,
                                      player2Name: player2Name)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 함으로써 &lt;b&gt;LoggedIn&lt;/b&gt;의 상위 RIB으로 부터 받아온 플레이어의 이름들은 &lt;b&gt;동적 dependency&lt;/b&gt;에서 &lt;b&gt;정적 dependency&lt;/b&gt;로 효율적으로 변환되고 하위 RIB에서 사용할 수 있게됩니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;2.&lt;/b&gt; 위에서 &lt;b&gt;LoggedIn&lt;/b&gt;으로 라우팅될때 플레이어의 이름을 받을 수 있도록 위에서 &lt;b&gt;LoggedInBuilder&lt;/b&gt;의 build메서드를 수정했어요&lt;br /&gt;이제 &lt;b&gt;RootRouter&lt;/b&gt; 클래스에서 &lt;b&gt;LoggedIn&lt;/b&gt;으로 라우팅해줄때 사용되는 &lt;b&gt;routeToLoggedIn&lt;/b&gt; 메서드도 플레이어 이름을 받을 수 있게 파라미터를 수정해줄게요.&lt;/p&gt;
&lt;pre id=&quot;code_1643156728976&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func routeToLoggedIn(withPlayer1Name player1Name: String, player2Name: String) {
    // Detach logged out.
    if let loggedOut = self.loggedOut {
        detachChild(loggedOut)
        viewController.dismiss(viewController: loggedOut.viewControllable)
        self.loggedOut = nil
    }

    let loggedIn = loggedInBuilder.build(withListener: interactor, player1Name: player1Name, player2Name: player2Name)
    attachChild(loggedIn)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;지금까지 진행한 수정사항들을 통해서 사용자에 의해 입력된 플레이어의 이름들은 &lt;b&gt;LoggedOut&lt;/b&gt; RIB에 의해 처리되며, 그 결과 &lt;b&gt;LoggedIn&lt;/b&gt; RIB, 그리고 그 &lt;b&gt;하위&lt;/b&gt; RIB에서 사용할 수 있게 되었어요&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Dynamic dependencies VS Static dependencies&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 LoggedIn RIB이 build될 때 플레이어 이름들을 주입하기로 결정했습니다. 이를 동적 주입이라고 합니다.&lt;br /&gt;이렇게 하지 않는다면 RootBuilder에서 build할 때 넣어주는 방법이 있는데요, 이러한 경우에는 RootBuilder가 만들어지는 시점에 플레이어 이름이 없을 수도 있기 때문에 옵셔널로 받아주어야 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;옵셔널이 생기게 되면 옵셔널 체이닝, 혹은 바인딩과 같은 추가적인 코드가 발생하게 되기 때문에 그런 상황에서 추천되지 않는 방식인 것 같다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 우리가 진행한 것처럼 적절하게 범위가 한정된 종속성은 불변 가정을 할 수 있게 하고 번거로운 옵셔널 처리를 하지 않아도 돼요.&lt;br /&gt;&lt;br /&gt;정리하자면 static으로 종속성을 넣어줄 경우 Optional Value에 대한 불필요한, 불합리한 코드를 작성하는 경우가 생긴다면 적당한 Scope를 지정하여 동적으로 종속성을 넣어주는 것도 좋은 방법이 될 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RIB's Dependencies and Components&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dependency, Component에 대한 개념을 지금까지 다루지 않았는데요. &lt;br /&gt;&lt;br /&gt;RIBs에서&lt;br /&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;&lt;b&gt;Dependency&lt;/b&gt;는 RIB이 제대로 인스턴스화 되기 위해 &lt;b&gt;상위 RIB으로부터 필요한 종속성을 나열하는 프로토콜&lt;/b&gt;입니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;&lt;b&gt;Component&lt;/b&gt;는 &lt;b&gt;Dependency 프로토콜의 수행자&lt;/b&gt;입니다. &lt;b&gt;상위 종속성을 RIB의 빌더에 제공하는것 외에도, Component는 RIB이 자신과 하위 RIB을 위해 만드는 Dependency를 소유할 책임&lt;/b&gt;이 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;&lt;b&gt;보통 상위 RIB이 하위 RIB을 인스턴스화 할 때, 자신의 Component를 하위 Builder에 생성자 dependency로 주입합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1643156728977&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func build() -&amp;gt; LaunchRouting { 		
    ...         
    let component = RootComponent(dependency: dependency,rootViewController: viewController)
    ...          
    // 보통 상위 RIB이 하위 RIB을 인스턴스화 할 때, 
    // 자신의 Component를 하위 Builder에 생성자 dependency로 주입합니다.
    let loggedOutBuilder = LoggedOutBuilder(dependency: component) 
    let loggedInBuilder = LoggedInBuilder(dependency: component)  
    
    return RootRouter(
        interactor: interactor, 
        viewController: viewController, 
        loggedOutBuilder: loggedOutBuilder,
        loggedInBuilder: loggedInBuilder)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주입된 각각의 Component는 하위 RIB에게 어떤 종속성을 노출시킬지 결정합니다.&lt;br /&gt;&lt;br /&gt;Component에 포함되어있는 종속성은 일반적으로 DI(의존성 주입) 트리 아래로 전달되어야 하는 일부 상태를 보유하거나, 구성하는 데 비용이 많이 들고, 성능상의 이유로 RIB간에 공유됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Passing the player names to the OffGame scope using the DI tree&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 공부했듯이 종속성에는 동적인 것과 정적인 것이 존재해요. 정적인 종속성을 주입할 때에는 주입하는 값이 옵셔널일 경우 비효율적인 일이 발생한다는 것까지 배웠습니다.&lt;br /&gt;&lt;br /&gt;이제 우리가 종속성을 주입할 친구는 &lt;b&gt;OffGame&lt;/b&gt; RIB이고 이 친구는 무조건 플레이어 이름을 가져야만 해요 옵셔널이 아니라는 뜻이죠?&lt;br /&gt;&lt;br /&gt;&lt;b&gt;0.&lt;/b&gt; 그래서 정적으로 종속성을 주입해줄거에요. &lt;b&gt;OffGameDependency&lt;/b&gt;를 다음과 같이 수정해주세요.&lt;br /&gt;이 정적 종속성은 상위 RIB에 의해 OffGame RIB이 이니셜라이징 될 때 무조건 담겨서 와야해요.&lt;/p&gt;
&lt;pre id=&quot;code_1643156728977&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protocol OffGameDependency: Dependency {
    var player1Name: String { get }
    var player2Name: String { get }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;1.&lt;/b&gt; 다음으로 우리는 이 종속성을 OffGame 자신의 Scope 내에서 사용할 수 있도록 해줄거에요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;fileprivate으로 선언해줌으로써 OffGameBuilder.swift 파일 내에서만 사용할 수 있어요 &lt;br /&gt;LoggedInComponent에서는 하위 RIB에게 값을 제공해주어야 해서 fileprivate을 사용하지 않았습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OffGameComponent를 다음과 같이 수정해주세요&lt;/p&gt;
&lt;pre id=&quot;code_1643156728978&quot; class=&quot;kotlin&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final class OffGameComponent: Component&amp;lt;OffGameDependency&amp;gt; {     fileprivate var player1Name: String {         return dependency.player1Name     }      fileprivate var player2Name: String {         return dependency.player2Name     } }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 작성함으로써 우리는 이제 상위 RIB으로 부터 player1Name과 player2Name을 꼭 받아야해요. 하지만 아까 LoggedIn RIB의 LoggedInComponent에서 이미 넘겨주도록 구현해놨기 때문에 별도로 진행할 필요는 없습니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;2.&lt;/b&gt;&lt;b&gt; &lt;/b&gt;이 종속성을 OffGameViewController의 생성자를 통해 &lt;b&gt;OffGameViewController&lt;/b&gt;로 넘겨줄거에요. 정석대로라면 OffGameInteractor에 이를 넘겨주고 OffGamePresentable을 통해 뷰가 이 정보를 보여줄 수 있도록 메서드를 작성해주어야 하지만 현재 플레이어 이름을 보여주는것에는 별도의 로직이 필요 없기 때문에 바로 ViewController로 넣어줍니다. &lt;br /&gt;&lt;br /&gt;view controller에 종속성을 주입하기 위해 &lt;b&gt;OffGameBuilder&lt;/b&gt;를 다음과 같이 업데이트 해주세요&lt;/p&gt;
&lt;pre id=&quot;code_1643156728978&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final class OffGameBuilder: Builder&amp;lt;OffGameDependency&amp;gt;, OffGameBuildable {
    override init(dependency: OffGameDependency) {
        super.init(dependency: dependency)
    }

    func build(withListener listener: OffGameListener) -&amp;gt; OffGameRouting {
        let component = OffGameComponent(dependency: dependency)
        let viewController = OffGameViewController(player1Name: component.player1Name,
                                                   player2Name: component.player2Name)
        let interactor = OffGameInteractor(presenter: viewController)
        interactor.listener = listener
        return OffGameRouter(interactor: interactor, viewController: viewController)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;b&gt;OffGameViewController&lt;/b&gt;이 플레이어 이름을 이니셜라이징하는 동안 받을 수 있게 수정 해주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1643156728979&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

private let player1Name: String
private let player2Name: String

init(player1Name: String, player2Name: String) {
    self.player1Name = player1Name
    self.player2Name = player2Name
    super.init(nibName: nil, bundle: nil)
}

...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이제 데이터는 다 받아왔으니 받아온 데이터를 라벨에 보여줄 수 있도록 UI를 구성해주시면 됩니다!&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;s&gt;게임에 어떤 플레이어들이 참가하는지 알고싶기에 플레이어의 이름을 표시해준다.&lt;/s&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Track scores using a Rx stream&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 게임이 끝나면 플레이어는 그냥 다시 게임을 시작할 수 있는 화면으로만 이동하게 됩니다.&lt;br /&gt;이제 Rx stream을 사용하여 점수를 관찰해서 시작화면에서 누적점수를 볼 수 있도록 하려고 합니다.&lt;br /&gt;&lt;br /&gt;RIBs를 설치하면 그 안에 Rx도 포함되어있을 정도로 Rx가 굉장히 널리 사용되는데요. 하위 RIB이 자신의 상위 RIB으로부터 동적인 데이터를 얻어야 할 때, 데이터 생산자 측에서 &lt;b&gt;Observable&lt;/b&gt;로 데이터를 감싸고 데이터를 사용해야하는 쪽에서 이를 subscribe하는 방식으로 자주 사용됩니다. &lt;br /&gt;&lt;br /&gt;우리의 경우, 게임 스코어는 게임이 진행되는 &lt;b&gt;TicTacToe&lt;/b&gt; RIB에 의해 업데이트 되어야 합니다. 그리고 그 스코어는 스코어를 보여줄 &lt;b&gt;OffGame&lt;/b&gt; RIB에 의해 사용될 것입니다. &lt;b&gt;TicTacToe&lt;/b&gt;와 &lt;b&gt;OffGame&lt;/b&gt;은 서로의 존재를 모르기 때문에 아무것도 안거치고 직접적으로 데이터를 주고받을 수는 없습니다. 하지만 두 RIB의 부모가 같기때문에 &lt;b&gt;LoggedIn&lt;/b&gt; RIB에서 score stream을 구현할것입니다. &lt;br /&gt;&lt;br /&gt;우리는 총 두가지(&lt;b&gt;ScoreStream(읽기전용)&lt;/b&gt;, &lt;b&gt;MutableScoreStream(가변버전)&lt;/b&gt;)를 Models/ScoreStream에서 만들어놨습니다. 이에 대해서는 아래에서 추가적으로 설명됩니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;LoggedInComponent&lt;/b&gt;에서 공유 &lt;b&gt;ScoreStream&lt;/b&gt; 인스턴스를 만들어주세요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;TicTacToe, OffGame 모두에 전달해야하기 때문에 LoggedInComponent에 생성. &lt;br /&gt;MutableScoreStream은 LoggedIn RIB과 그 하위 RIB에서 사용하도록 만든 싱글톤 인스턴스입니다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1643156728980&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var mutableScoreStream: MutableScoreStream {
    return shared { ScoreStreamImpl() }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Stream 부분에 대해서 좀 이해가 어려웠는데 &lt;a href=&quot;https://getbingsoo.tistory.com/65&quot; target=&quot;_self&quot;&gt;&lt;span&gt;잘 설명해주신 글&lt;/span&gt;&lt;/a&gt;이 있어서 인용해왔습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;그런데 왜 shared로 만드는가? shared의 내부 구성?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Component의 기본 구현을 보면 다음과 같은 shared라는 함수가 만들어져 있다. 여기서 __function은 shared를 부른 주체인 mutalbeScoreStream이다. 기본 Component 클래스에는&amp;nbsp;sharedInstance라는 프로퍼티가 있는데 &amp;nbsp;이 프로퍼티에 [키:밸류] 형태로 전달인자를 저장해놓는다. 만약 해당 키로 저장된게 있으면 그걸 사용하고, 없으면 새로 저장한다. component 내에서 공유되는 dependency는 shared에 저장한다.&lt;/p&gt;
&lt;pre id=&quot;code_1643156728980&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public final func shared&amp;lt;T&amp;gt;(__function: String = #function, _ factory: () -&amp;gt; T) -&amp;gt; T {
 lock.lock()   
 defer { 
         lock.unlock()
 }    

 if let instance = (sharedInstances[__function] as? T?) ?? nil {
     return instance
 } 

 let instance = factory()
 sharedInstances[__function] = instance
 return instance
}  
// MARK: - Private 
private var sharedInstances = [String: Any]()
private let lock = NSRecursiveLock()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;shared 인스턴스는 주어진 scope에 대해 생성된 싱글톤을 의미한다. (우리의 경우, scope는 LoggedIn RIB와 모든 자식을 포함한다). 스트림은 대부분의 상태 저장 객체와 마찬가지로 일반적으로 범위가 있는 싱글톤이다. 그러나 대부분의 다른 dependency는 stateless해야 하며, 따라서 공유되지 않아야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoggedInComponent에서 mutableScoreStream 프로퍼티는 fileprivate가 아니라 internal로 생성되었다는 것을 알아두자. LoggedIn의 children이 접근할 수 있어야 하기 때문에 이 프로퍼티를 파일 외부에 노출해야 한다. 이 요구 사항이 유지되지 않으면, stream을 파일 내에 캡슐화하는 것이 바람직하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, RIB에서 직접 사용되는 dependency만 component의 기본 구현에 배치되어야 하며, 플레이어 이름과 같은 동적 종속성에서 주입되는 stored property는 예외다. 그러니까 mutableScoreStream같은건 LoggedInInteractor(LoggedInRIB)에서 바로 쓰이므로 LoggedInComponent에다가 넣은 것이고, 만약에 어디에 전달되는 용도의 dependency였다면 해당 전달child를 구현하는 부분의 Extension에 넣어야 한다. 예를 들어 OffGame에 전달하는 경우에, LoggedInComponent+OffGame 파일에 넣는것이좋다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이제 mutableScoreStream을 LoggedInInteractor로 넘겨준다. 이로써 나중에 스코어를 갱신할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1643156728980&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

private let mutableScoreStream: MutableScoreStream

init(mutableScoreStream: MutableScoreStream) {
    self.mutableScoreStream = mutableScoreStream
}

...&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1643156728981&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// LoggedInBuilder  
func build(withListener listener: LoggedInListener, player1Name: String, player2Name: String) -&amp;gt; LoggedInRouting {
    let component = LoggedInComponent(dependency: dependency,
                                      player1Name: player1Name,
                                      player2Name: player2Name)
    let interactor = LoggedInInteractor(mutableScoreStream: component.mutableScoreStream)

...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Passing a read-only ScoreStream down to OffGame scope for displaying&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 우리는 읽기 버전의 ScoreStream을 OffGame RIB에서 게임이 끝나면 점수를 보여주는 데에 사용할 수 있도록(수정은 X) 내려줄거에요. &lt;b&gt;OffGameDependency&lt;/b&gt; 프로토콜 내부에 읽기버젼의 &lt;b&gt;ScoreStream&lt;/b&gt;을 선언해주세요. &lt;b&gt;OffGame&lt;/b&gt; RIB의 범위 내에서만 사용가능한(&lt;b&gt;fileprivate&lt;/b&gt;) dependency로써 선언해주셔야합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1643156728981&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protocol OffGameDependency: Dependency {
    var player1Name: String { get }
    var player2Name: String { get }
    var scoreStream: ScoreStream { get }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OffGameComponent&lt;/b&gt;에서 dependency fileprivate로 선언해주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1643156728981&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fileprivate var scoreStream: ScoreStream {
    return dependency.scoreStream
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OffGame의&lt;/b&gt; builder는 &lt;b&gt;OffGameInteractor&lt;/b&gt;가 이 데이터를 사용할 수 있도록 stream을 주입시켜주어야해요&lt;/p&gt;
&lt;pre id=&quot;code_1643156728982&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func build(withListener listener: OffGameListener) -&amp;gt; OffGameRouting {
    let component = OffGameComponent(dependency: dependency)
    let viewController = OffGameViewController(player1Name: component.player1Name,
                                               player2Name: component.player2Name)
    let interactor = OffGameInteractor(presenter: viewController,
                                       scoreStream: component.scoreStream)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이제 &lt;b&gt;OffGameInteractor&lt;/b&gt;&lt;b&gt; &lt;/b&gt;의 생성자는 score stream을 받을 수 있도록 변경되었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1643156728982&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

private let scoreStream: ScoreStream

init(presenter: OffGamePresentable,
     scoreStream: ScoreStream) {
    self.scoreStream = scoreStream
    super.init(presenter: presenter)
    presenter.listener = self
}

...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LoggedIn&lt;/b&gt; RIB에서 한것과 다르게 &lt;b&gt;OffGameComponent&lt;/b&gt;에서는 &lt;b&gt;fileprivate&lt;/b&gt;으로 stream을 선언해주었는데&lt;br /&gt;&lt;b&gt;LoggedIn&lt;/b&gt; RIB에서는 하위 RIB에서 사용이 가능하도록 하기 위해서 &lt;b&gt;fileprivate&lt;/b&gt;을 사용하지 않은것이고 &lt;b&gt;OffGame&lt;/b&gt;에서는 &lt;b&gt;OffGame&lt;/b&gt;의 하위 RIB이 이 dependency를 참조할 수 없게 하기 위해서 &lt;b&gt;fileprivate&lt;/b&gt;으로 선언하였습니다.&lt;br /&gt;&lt;br /&gt;읽기 전용 score stream은 오로지 &lt;b&gt;OffGame&lt;/b&gt; 안에서만 필요하고 그 외에 &lt;b&gt;LoggedIn&lt;/b&gt; RIB 등에서는 필요하지 않기 때문에, 제공된 소스코드를 보면 이 dependency를 &lt;b&gt;LoggedInComponent+OffGame&lt;/b&gt; extension에 위치시켜놓았습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1643156728983&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// LoggedInComponent+OffGame.swift 
extension LoggedInComponent: OffGameDependency {
    var scoreStream: ScoreStream {
        return mutableScoreStream
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Display the scores by subscribing to the score stream&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;b&gt;OffGame&lt;/b&gt; RIB은 score stream을 구독해야합니다. stream에 의해 새로운 점수 값에 대한 노티가 일어나고 나면, &lt;b&gt;OffGamePresentable&lt;/b&gt;은 이 값을 view controller에게 전달할거에요. Rx의 subscription을 통해 우리는 저장된 상태를 제거하고 데이터를 반영한 UI를 자동적으로 업데이트 합니다.&lt;br /&gt;&lt;br /&gt;점수 값을 셋팅할 수 있게 &lt;b&gt;OffGamePresentable&lt;/b&gt; 프로토콜을 업데이트 해주세요. 이 프로토콜(&lt;b&gt;OffGamePresentable&lt;/b&gt;)은 우리가 &lt;b&gt;interactor에서&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;view로&lt;/b&gt; 소통하는 경우에 사용합니다!&lt;/p&gt;
&lt;pre id=&quot;code_1643156728983&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protocol OffGamePresentable: Presentable {
    weak var listener: OffGamePresentableListener? { get set }
    func set(score: Score)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;우리는 &lt;b&gt;OffGameInteractor&lt;/b&gt;&lt;b&gt; &lt;/b&gt;클래스 내에서 subscription을 만들고 stream이 새로운 값을 내뱉을 때 &lt;b&gt;OffGamePresentable&lt;/b&gt;을 통해 새로운 값을 셋팅해줄거에요&lt;/p&gt;
&lt;pre id=&quot;code_1643156728983&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;override func didBecomeActive() {  
    super.didBecomeActive()    
    updateScore() 
}  

private func updateScore() {    
    scoreStream.score         
    .subscribe(      
        onNext: { (score: Score) in  
            self.presenter.set(score: score)
            }
        )
        .disposeOnDeactivate(interactor: self)
        // OffGameInteractor가 deactivate될때 자동적으로 dispose해준다
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;disposeOnDeactivate&lt;/b&gt;? &lt;br /&gt;Interactor의 생명주기를 기반으로 deactivate될 때 자동적으로 구독을 삭제한다. 우리는 거의 항상 이러한 Rx 수명 주기 관리 유틸리티를 활용하기 위해 interactor 또는 worker 클래스에서 Rx 구독을 활용해야한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 뷰컨에서 점수를 보여주어야하는데 이건 제공해주는 &lt;a href=&quot;https://github.com/uber/ribs/blob/assets/tutorial_assets/ios/tutorial3-rib-di-and-communication/source/source3.swift&quot; target=&quot;_self&quot;&gt;&lt;span&gt;샘플 코드&lt;/span&gt;&lt;/a&gt;를 복붙하면 된당&lt;br /&gt;아직 점수가 실시간으로 반영되어 화면에 보여지고있지는 않다.&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Updating the score stream when a game is over&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게임이 끝나고나서 &lt;b&gt;TicTacToe&lt;/b&gt; RIB은 자신의 listener를 통해 &lt;b&gt;LoggedInInteractor&lt;/b&gt;를 호출해주어야한다. 그러기 위해서 메서드를 선언하자.&lt;/p&gt;
&lt;pre id=&quot;code_1643156728984&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protocol TicTacToeListener: class {
    func gameDidEnd(withWinner winner: PlayerType?)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 게임의 승자가 결정되고 이를 뷰컨이 활용할 수 있게 하는 방법은 몇가지가 존재한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;TicTacToeInteractor&lt;/b&gt;가 승자에 대한 데이터를 지역변수로 가지고 있고 전달한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TicTacToeViewController&lt;/b&gt;가 게임진행 alert창이 닫히면 closeGame 메서드를 통해 승자를 interactor에게 넘긴다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술적으로 두 방법 다 괜찮은 방법이다. &lt;br /&gt;&lt;br /&gt;1번 방법의 이점은 모든 데이터가 &lt;b&gt;TicTacToeInteractor&lt;/b&gt;안에서 캡슐화 되어있다는 것이다. 단점으로는 로컬의 변경 가능한 상태를 유지하고 있어야 한다는 것인데, 우리가 사용중인 RIB의 범위가 넓다는 사실에 의해 조금 상쇄되는 단점입니다. 각 RIB의 로컬 상태는 잘 캡슐화 되어있고 제한되기 때문에 새로운 게임을 시작해서 새롭게 &lt;b&gt;TicTacToe&lt;/b&gt; RIB을 만들면 이전거는 새로운 지역 변수로 초기화 됩니다.&lt;br /&gt;&lt;br /&gt;두번째 방법을 사용한다면 우리는 1번에서 유의해야 할 단점은 신경쓰지 않아도 되지만 비즈니스 로직을 view controller에서 처리해야합니다. &lt;br /&gt;&lt;br /&gt;이번 튜토리얼에서는 두 방법의 이점만을 활용하기 위해서 &lt;b&gt;Swift의 클로져&lt;/b&gt;를 활용할거에요. 클로져를 활용하면 Interactor가 view controller에게 게임이 끝났음을 알릴 때, view controller가 상태를 업데이트 한 후 &lt;b&gt;completion handler&lt;/b&gt;를 호출하게 할 수 있어요.&lt;br /&gt;이렇게 되면 승자에 대한 데이터가 &lt;b&gt;TicTacToeInteractor&lt;/b&gt; 안에서 캡슐화되고 추가적인 상태 값을 저장하지 않을거에요. 또한, closeGame 메서드를 view controller의 리스터안에 불필요하게 만들 필요도 없게됩니다!&lt;br /&gt;&lt;br /&gt;&lt;b&gt;TicTacToePresentableListener &lt;/b&gt;에서 closeGame 메서드를 지우고 아래 메서드를 추가해주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1643156728985&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protocol TicTacToePresentableListener: class {
    func placeCurrentPlayerMark(atRow row: Int, col: Int)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;TicTacToeViewController&lt;/b&gt;에서 &lt;b&gt;announce&lt;/b&gt; 메서드를 수정해주세요. &lt;b&gt;completion handler를&lt;/b&gt; 인자로 받고 사용자가 alert창을 끄면 호출할 수 있도록 하는 작업입니다. &lt;b&gt;announce&lt;/b&gt; 메서드는 player중 한명이 게임의 승자가 되었을 때 interactor에의해 호출이 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1643156728985&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func announce(winner: PlayerType?, withCompletionHandler handler: @escaping () -&amp;gt; ()) {
    let winnerString: String = {
        if let winner = winner {
            switch winner {
            case .player1:
                return &quot;Red won!&quot;
            case .player2:
                return &quot;Blue won!&quot;
            }
        } else {
            return &quot;It's a draw!&quot;
        }
    }()
    let alert = UIAlertController(title: winnerString, message: nil, preferredStyle: .alert)
    let closeAction = UIAlertAction(title: &quot;Close Game&quot;, style: UIAlertActionStyle.default) { _ in
        handler()
    }
    alert.addAction(closeAction)
    present(alert, animated: true, completion: nil)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;TicTacToePresentable&lt;/b&gt; 프로토콜에서 &lt;b&gt;announce&lt;/b&gt; 메서드가 &lt;b&gt;completion handler&lt;/b&gt;를 파라미터로 받을 수 있게 수정해주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1643156728986&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protocol TicTacToePresentable: Presentable {
    ...

    func announce(winner: PlayerType?, withCompletionHandler handler: @escaping () -&amp;gt; ())
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;TicTacToeInteractor&lt;/b&gt;에서도 수정! 뷰컨에서 리스너를 통해 interactor에게 아래 메서드를 호출시킬거에요.&lt;/p&gt;
&lt;pre id=&quot;code_1643156728986&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func placeCurrentPlayerMark(atRow row: Int, col: Int) {
    guard board[row][col] == nil else {
        return
    }

    let currentPlayer = getAndFlipCurrentPlayer()
    board[row][col] = currentPlayer
    presenter.setCell(atRow: row, col: col, withPlayerType: currentPlayer)

    if let winner = checkWinner() {
        presenter.announce(winner: winner) {
            self.listener?.gameDidEnd(withWinner: winner)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;마지막으로 &lt;b&gt;LoggedInInteracor&lt;/b&gt;에서 &lt;b&gt;gameDidEnd&lt;/b&gt; 메서드의 구현부를 다음과 같이 수정해주세요.&lt;br /&gt;게임의 새로운 승자가 나오면 score stream을 업데이트 해줄거에요.&lt;/p&gt;
&lt;pre id=&quot;code_1643156728986&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func gameDidEnd(withWinner winner: PlayerType?) {
    if let winner = winner {
        mutableScoreStream.updateScore(withWinner: winner)
    }
    router?.routeToOffGame()
}&lt;/code&gt;&lt;/pre&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1300&quot; data-ke-mobilestyle=&quot;alignCenter&quot;&gt;&lt;br /&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif-3-368dec8def.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dzyLQb/btrsfWSgf59/cOJRyfwEploLfAXVhKLij1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dzyLQb/btrsfWSgf59/cOJRyfwEploLfAXVhKLij1/img.gif&quot; data-alt=&quot;완성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dzyLQb/btrsfWSgf59/cOJRyfwEploLfAXVhKLij1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/dzyLQb/btrsfWSgf59/cOJRyfwEploLfAXVhKLij1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;211&quot; height=&quot;457&quot; data-filename=&quot;ezgif-3-368dec8def.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1300&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;완성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>iOS</category>
      <author>snowe</author>
      <guid isPermaLink="true">https://snowee.tistory.com/61</guid>
      <comments>https://snowee.tistory.com/61#entry61comment</comments>
      <pubDate>Wed, 26 Jan 2022 09:25:23 +0900</pubDate>
    </item>
    <item>
      <title>iOS) RIBs Tutorial 2</title>
      <link>https://snowee.tistory.com/60</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Tutorial 1 내용 간단 요약&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Goal&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- RIB에 존재하는 Interactor, Builder, Router, Presenter등이 서로 어떻게 커뮤니케이션 하는지 이해하기&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Official Wiki&lt;/blockquote&gt;
&lt;figure id=&quot;og_1642037272696&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - uber/RIBs: Uber's cross-platform mobile architecture framework.&quot; data-og-description=&quot;Uber's cross-platform mobile architecture framework. - GitHub - uber/RIBs: Uber's cross-platform mobile architecture framework.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/uber/RIBs/wiki/iOS-Tutorial-2&quot; data-og-url=&quot;https://github.com/uber/RIBs&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/pogFD/hyM4FgWEiH/LOWFY7iNiwdSmhcg8zp4e1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/uber/RIBs/wiki/iOS-Tutorial-2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/uber/RIBs/wiki/iOS-Tutorial-2&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/pogFD/hyM4FgWEiH/LOWFY7iNiwdSmhcg8zp4e1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - uber/RIBs: Uber's cross-platform mobile architecture framework.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Uber's cross-platform mobile architecture framework. - GitHub - uber/RIBs: Uber's cross-platform mobile architecture framework.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;소스코드&lt;/blockquote&gt;
&lt;figure id=&quot;og_1642038291809&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - snowedev/Study-RIBs: Practice Architecture(RIBs)&quot; data-og-description=&quot;Practice Architecture(RIBs). Contribute to snowedev/Study-RIBs development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/snowedev/Study-RIBs/tree/master/tutorials/tutorial2&quot; data-og-url=&quot;https://github.com/snowedev/Study-RIBs&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c7Qd7T/hyM5FGqYe2/owy0gfdjH1sN5cmOTKYsk1/img.png?width=1200&amp;amp;height=600&amp;amp;face=984_142_1051_215&quot;&gt;&lt;a href=&quot;https://github.com/snowedev/Study-RIBs/tree/master/tutorials/tutorial2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/snowedev/Study-RIBs/tree/master/tutorials/tutorial2&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c7Qd7T/hyM5FGqYe2/owy0gfdjH1sN5cmOTKYsk1/img.png?width=1200&amp;amp;height=600&amp;amp;face=984_142_1051_215');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - snowedev/Study-RIBs: Practice Architecture(RIBs)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Practice Architecture(RIBs). Contribute to snowedev/Study-RIBs development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Tutorial 2&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Goal&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;상위RIB과 소통하는 하위RIB 만들기&lt;/li&gt;
&lt;li&gt;상위RIB의 Interactor가 결정하는 대로 하위RIB을 attaching / detaching 해보기&lt;/li&gt;
&lt;li&gt;view-less RIB 만들기
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;view-less RIB이 detach될 때 view의 변경 정리하기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;RIB의 life cycle 이해하기
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;상위 RIB이 처음 로드될 때 하위 RIB attach하기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;RIB unit testing&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Project Structure&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전의 튜토리얼을 완료했다면 &lt;b&gt;Root&lt;/b&gt;와 &lt;b&gt;LoggedOut&lt;/b&gt;, 두개의 RIB으로 구성된 트리가 있을거에요. 이번 튜토리얼에서는 &lt;b&gt;LoggedIn&lt;/b&gt;, &lt;b&gt;OffGame&lt;/b&gt; 그리고 &lt;b&gt;TicTacToe&lt;/b&gt; 라는 세가지 추가적인 RIB을 트리에 붙일겁니다. 완성될 트리 구조는 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;149138811-89a57f44-e5dc-47e5-907c-e91387b8e00b.png&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;788&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXuAeh/btrqBGuYWeF/k37PjLycnrWk2ECjioGcC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXuAeh/btrqBGuYWeF/k37PjLycnrWk2ECjioGcC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXuAeh/btrqBGuYWeF/k37PjLycnrWk2ECjioGcC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXuAeh%2FbtrqBGuYWeF%2Fk37PjLycnrWk2ECjioGcC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;663&quot; height=&quot;480&quot; data-filename=&quot;149138811-89a57f44-e5dc-47e5-907c-e91387b8e00b.png&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;788&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;b&gt;LoggedIn &lt;/b&gt;RIB은 view-less입니다. 이 RIB은 오로지 TicTacToe와 OffGame RIB사이의 전환에 대한 역할만을 담당합니다.&amp;nbsp;&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;나머지 RIB들은 자신만의 뷰컨과 스크린에 보여줄 뷰들을 가지고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OffGame&lt;/b&gt; RIB은 플레이어가 새로운 게임을 시작할 수 있게 해줍니다. 그래서 &quot;Start Geme&quot; 버튼을 가질 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TicTacToe&lt;/b&gt; RIB은 게임 영역을 보여줄 것이며 플레이어가 이동을 할 수 있도록 해줄 것입니다.&lt;/p&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;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Communicating with a parent RIB&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 먼저 뭘 할거냐면, 상위 RIB과 하위 RIB간의 소통을 구현해볼거에요&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;사용자가 플레이어 이름 텍스트필드에 이름을 입력한 뒤 로그인 버튼을 탭하면 게임을 시작할 수 있게 해주어야 하잖아요. 그래서 현재 뷰에서 &quot;Start Game&quot; 뷰로 이동되도록 할거에요. 이걸 위해서 현재 활성화 되어있는 &lt;b&gt;LoggedOut&lt;/b&gt; RIB은 &lt;b&gt;Root&lt;/b&gt; RIB에게 로그인 액션에 대해 알려주어야 합니다. 이를 통해 &lt;b&gt;Root&lt;/b&gt; Router는 &lt;b&gt;LoggedOut&lt;/b&gt; RIB에서 &lt;b&gt;LoggedIn&lt;/b&gt; RIB으로 전환제어를 하게 됩니다.&amp;nbsp; 이어서, view-less RIB인&amp;nbsp;&lt;b&gt;LoggedIn&lt;/b&gt; RIB은&amp;nbsp;&lt;b&gt;OffGame&lt;/b&gt; RIB을 불러올 것이고 그 뷰를 뷰 컨트롤러에 띄울거에요.&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;LoggedOut&lt;/b&gt; 과 &lt;b&gt;Root&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;Root&lt;/b&gt; RIB이 &lt;b&gt;LoggedOut&lt;/b&gt; RIB의 상위 RIB이기 때문에, &lt;b&gt;Root&lt;/b&gt;의 Router는 &lt;b&gt;LoggedOut&lt;/b&gt; Interactor의 리스너가 됩니다. 우리는 이 리스너를 통해 로그인 이벤트를 &lt;b&gt;LoggedOut&lt;/b&gt; RIB에서 &lt;b&gt;Root&lt;/b&gt; RIB으로 전달할거에요&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;LoggedOutListener&lt;/b&gt;를 다음과 같이 업데이트 해주세요&lt;/p&gt;
&lt;pre id=&quot;code_1642037861370&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protocol LoggedOutListener: class {
    func didLogin(withPlayer1Name player1Name: String, player2Name: String)
}&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;LoggedOut&lt;/b&gt; RIB의 상위 RIB은 강제적으로 didLogin을 구현해야하고&amp;nbsp; 컴파일러가 이를 감시합니다.&amp;nbsp;&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;LoggedOut&lt;/b&gt; RIB에서 login 액션이 발동됐을 때 리스너를 호출함으로써 상위 RIB에게 알려주어야 겠네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LoggedOutInteractor&lt;/b&gt; 내부에 있는 login 메서드의 구현부를 바꿔줄게요.&lt;/p&gt;
&lt;pre id=&quot;code_1642038136741&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func login(withPlayer1Name player1Name: String?, player2Name: String?) {
    let player1NameWithDefault = playerName(player1Name, withDefaultName: &quot;Player 1&quot;)
    let player2NameWithDefault = playerName(player2Name, withDefaultName: &quot;Player 2&quot;)
    listener?.didLogin(withPlayer1Name: player1NameWithDefault, player2Name: player2NameWithDefault)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 LoggedOut RIB의 리스너는 사용자가 &quot;Login&quot; 버튼을 탭하면 호출될거에요&lt;/p&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;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Routing to &lt;b&gt;LoggedIn&lt;/b&gt; RIB&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 보았던 RIB 트리에서 볼 수 있지만, 사용자가 로그인을 하고 나면(리스너를 통해 그걸 인지하고 나면) &lt;b&gt;Root&lt;/b&gt; RIB은 &lt;b&gt;LoggedOut&lt;/b&gt; RIB -&amp;gt; &lt;b&gt;LoggedIn&lt;/b&gt; RIB 으로 전환을 시켜주어야 해요.&amp;nbsp;&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;RootRouting&lt;/b&gt; 프로토콜에 &lt;b&gt;LoggedIn&lt;/b&gt; RIB으로 라우팅 해주기 위한 코드를 작성할게요&lt;/p&gt;
&lt;pre id=&quot;code_1642038596535&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// MARK: Root/RootInteractor.swift

protocol RootRouting: ViewableRouting {
    func routeToLoggedIn(withPlayer1Name player1Name: String, player2Name: String)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드를 통해 &lt;b&gt;RootInteractor&lt;/b&gt;와 &lt;b&gt;RootRouter&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;RootInteractor&lt;/b&gt;에서 LoggedOutListner 프로토콜에 생성한 메서드인 didLogin의 body를 아래와 같이 작성해줄게요.&lt;/p&gt;
&lt;pre id=&quot;code_1642038956825&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// MARK: Root/RootInteractor.swift

final class RootInteractor: PresentableInteractor&amp;lt;RootPresentable&amp;gt;, RootInteractable, RootPresentableListener {
    func didLogin(withPlayerName player1Name: String, player2Name: String) {
        router?.routeToLoggedIn(withPlayer1Name: player1Name, player2Name: player2Name)
    }
    // ...
  }&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;b&gt;LoggedOut&lt;/b&gt; RIB에서 로그인 버튼을 탭하면 &lt;b&gt;LoggedOutListener&lt;/b&gt; 프로토콜에 의해 LoggedOut의 상위 RIB(&lt;b&gt;Root&lt;/b&gt; RIB)에서 &lt;b&gt;didLogin&lt;/b&gt; 메서드가 호출이 될거고, router의 &lt;b&gt;routeToLoggedIn&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;Root&lt;/b&gt; RIB이 &lt;b&gt;LoggedIn&lt;/b&gt; RIB으로 라우팅 할 수 있게 됩니다. 하지만 우리는 아직 &lt;b&gt;LoggedIn&lt;/b&gt; RIB을 가지고 있지 않기 때문에 &lt;b&gt;Root&lt;/b&gt; RIB에서 전환이 될 수 없어요. 얼른 구현하러 갑시당&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;LoggedIn&lt;/b&gt; 그룹에서 &lt;b&gt;DELETE\_ME.swift&lt;/b&gt; 파일을 지우고, &lt;b&gt;LoggedIn&lt;/b&gt; 폴더에서 RIB 템플릿을 선택하여 &lt;b&gt;LoggedIn&lt;/b&gt; RIB을 만듭시다(&quot;Owns corresponding view&quot;는 체크 해제 해주세요 요놈은 view-less RIB이니까요).&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;그러면 LoggedInRouter, LoggedInBuilder, LoggedInInteractor 라는 세가지의 파일이 생성됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Attaching a viewless&lt;span&gt;&amp;nbsp;&lt;/span&gt;LoggedIn&lt;span&gt;&amp;nbsp;&lt;/span&gt;RIB and detaching&lt;span&gt;&amp;nbsp;&lt;/span&gt;LoggedOut&lt;span&gt;&amp;nbsp;&lt;/span&gt;RIB when the users log in&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새롭게 생성된 RIB을 attach 하기 위해서, &lt;b&gt;Root&lt;/b&gt;&lt;b&gt;Router&lt;/b&gt;는 그걸 빌드할 수 있어야 합니다. &lt;b&gt;LoggedInBuildable&lt;/b&gt; 프로토콜을 생성자 주입을 통해 &lt;b&gt;RootRouter&lt;/b&gt;에 전달하여 이를 가능하게 합니다. &lt;b&gt;RootRouter&lt;/b&gt;의 코드를 다음과 같이 수정할게요.&lt;/p&gt;
&lt;pre id=&quot;code_1642039868934&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// MARK: Root/RootRouter.swift
init(interactor: RootInteractable,
     viewController: RootViewControllable,
     loggedOutBuilder: LoggedOutBuildable,
     loggedInBuilder: LoggedInBuildable) { // new
    self.loggedOutBuilder = loggedOutBuilder
    self.loggedInBuilder = loggedInBuilder // new
    super.init(interactor: interactor, viewController: viewController)
    interactor.router = self
}

// MARK: - Private

private let loggedInBuilder: LoggedInBuilder // new

...&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;LoggedIntBuilder&lt;/b&gt; 구체 클래스를 인스턴스화 하고 그걸 &lt;b&gt;RootRouter&lt;/b&gt;에 주입할 수 있게&amp;nbsp; &lt;b&gt;RootBuilder&lt;/b&gt;를 업데이트 할게요. &lt;b&gt;RootBuilder&lt;/b&gt;의 &lt;b&gt;build()&lt;/b&gt; 메서드를 수정해주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1642040231035&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// MARK: Router/RootBuilder.swift

func build() -&amp;gt; LaunchRouting {
    let viewController = RootViewController()
    let component = RootComponent(dependency: dependency,
                                  rootViewController: viewController)
    let interactor = RootInteractor(presenter: viewController)

    let loggedOutBuilder = LoggedOutBuilder(dependency: component)
    let loggedInBuilder = LoggedInBuilder(dependency: component) // new
    return RootRouter(interactor: interactor,
                      viewController: viewController,
                      loggedOutBuilder: loggedOutBuilder,
                      loggedInBuilder: loggedInBuilder) // new
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방금 수정한 코드를 보면 생성자 주입을 사용하여 &lt;b&gt;LoggedInBuilder&lt;/b&gt;에 대한 종속성으로 &lt;b&gt;RootComponent&lt;/b&gt;를 전달하고 있어요. 이 부분에 대해서는 튜토리얼 3에서 다룬다고 하네여!&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;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;b&gt;RootRouter&lt;/b&gt;는 &lt;b&gt;LoggedInBuilder&lt;/b&gt; 클래스 대신 &lt;b&gt;LoggedInBuildable&lt;/b&gt; 프로토콜에 의존합니다.&amp;nbsp; 이를 통해 &lt;b&gt;RootRouter&lt;/b&gt;를 단위 테스트할 때 &lt;b&gt;LoggedInBuildable&lt;/b&gt;에 대한 테스트 모의를 전달할 수 있습니다. 동시에 이는 프로토콜 기반 프로그래밍 원칙을 따르므로 &lt;b&gt;RootRouter&lt;/b&gt;와 &lt;b&gt;LoggedInBuilder&lt;/b&gt;가 밀접하게 결합되지 않도록 합니다.&lt;/span&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;LoggedIn&lt;/b&gt; RIB에 대한 상용구 코드를 전부 생성했고 &lt;b&gt;Root&lt;/b&gt; RIB이 &lt;b&gt;LoggedIn&lt;/b&gt; RIB을 인스턴스화 할 수 있습니다. 이제 &lt;b&gt;RootRouter&lt;/b&gt; 안에 &lt;b&gt;routeToLoggedIn&lt;/b&gt; 메서드를 구현해야합니다.&amp;nbsp;&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;detach를 하려면 dismiss가 필요하겠네요, RootRouter에&amp;nbsp; RootViewControllable 프로토콜 내에 아래처럼 dismiss를 추가해주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1642047994524&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// MARK: Root/RootRouter.swift
protocol RootViewControllable: ViewControllable {
    func present(viewController: ViewControllable)
    func dismiss(viewController: ViewControllable)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 프로토콜을 준수하고 있는 RootViewController에서 dismiss에 대한 구현부도 작성해주어야겠죠?&lt;/p&gt;
&lt;pre id=&quot;code_1642057513314&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// MARK: Root/RootViewController
func dismiss(viewController: ViewControllable) {
    if presentedViewController === viewController.uiviewController {
        dismiss(animated: true, completion: nil)
    }
}&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;이제 본격적으로 detach/attach 부분을 작성해볼게요. RootRouter에 다음의 코드를 새롭게 추가해주세요&lt;/p&gt;
&lt;pre id=&quot;code_1642048752242&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// MARK: - RootRouting

func routeToLoggedIn(withPlayer1Name player1Name: String, player2Name: String) {
    // Detach LoggedOut RIB.
    if let loggedOut = self.loggedOut {
        detachChild(loggedOut)
        viewController.dismiss(viewController: loggedOut.viewControllable)
        self.loggedOut = nil
    }

    let loggedIn = loggedInBuilder.build(withListener: interactor)
    attachChild(loggedIn)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 볼 수 있듯 새로운 하위 RIB으로 전환하려면, 상위 RIB은 먼저 지금 붙어있는 하위 RIB을 detach 해야합니다. 그 다음에 새로운 하위 RIB을 만들고 방금 detach한 자리에 attach 해줍니다.&amp;nbsp;&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;RIB과 뷰 계층 구조 간의 일관성을 유지하는 것도 상위 RIB의 책임입니다. 만약 하위 RIB이 view controller를 가지고 있다면 상위 RIB은 하위 RIB이 detach 될 때 하위 view controller 또한 dismiss 하거나 present 해주어야합니다.&amp;nbsp;&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;routeToLoggedn&lt;/b&gt;를 통해 자신의 view controller를 가진 RIB을 어떻게 attach하는지 이해해보시면 좋습니다!&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;LoggedIn&lt;/b&gt; RIB으로부터 이벤트를 받을 수 있도록 하기 위해, &lt;b&gt;Root&lt;/b&gt; RIB은 자신의 interactor를 &lt;b&gt;LoggedIn&lt;/b&gt; RIB의 리스너로 구성합니다. 위 코드에서 &lt;b&gt;Root&lt;/b&gt; RIB이 하위 RIB을 빌드할 때 그런 일들이 일어나게 되는데, 이 시점에서 아직 루트 RIB는 &lt;b&gt;LoggedIn&lt;/b&gt; RIB의 요청에 응답할 수 있는 프로토콜을 구현하지 않습니다.&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;RIB은 프로토콜 기반이므로 리스너 인터페이스를 준수하는것에 엄격합니다. 다른 암시적 관찰 방법 대신 프로토콜을 사용하므로 부모가 자식의 모든 이벤트를 구현하지 않을때 런타임이 아닌 컴파일러에서 오류를 반환하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이제&amp;nbsp;&lt;b&gt;RootInteractable&lt;/b&gt;을&amp;nbsp;&lt;b&gt;LoggedInBuilder&lt;/b&gt;의 빌드 메서드에 대한 리스너로 전달하므로 &lt;b&gt;RootInteractabl&lt;/b&gt;은&amp;nbsp;&lt;b&gt;LoggedInListener&lt;/b&gt; 프로토콜을 준수해야 합니다. &lt;b&gt;RootInteractable&lt;/b&gt;에 해당 프로토콜을 따르도록 추가해주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1642057343880&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// LoggedInListener 추가
protocol RootInteractable: Interactable, LoggedOutListener, LoggedInListener {
    var router: RootRouting? { get set }
    var listener: RootListener? { get set }
}&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;RootRouter&lt;/b&gt;는 &lt;b&gt;routeToLoggedIn&lt;/b&gt; 메서드를 통해 &lt;b&gt;LoggedIn&lt;/b&gt; RIB으로 라우팅할 때 &lt;b&gt;LoggedOut&lt;/b&gt; RIB을 detach할 수 있고 view controller도 dismiss할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Pass in&lt;span&gt;&amp;nbsp;&lt;/span&gt;LoggedInViewControllable&lt;span&gt;&amp;nbsp;&lt;/span&gt;instead of creating it&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LoggedIn &lt;/b&gt;RIB이 자신의 view을 가지고 있지 않지만 여전히 하위 RIB의 뷰들은 보여주어야 할 필요가 있기 때문에, &lt;b&gt;LoggedIn&lt;/b&gt; RIB은 view의 조상(상위 view)로의 접근이 가능해야합니다. 튜토리얼에서 &lt;b&gt;LoggedIn&lt;/b&gt; RIB의 상위 RIB은 &lt;b&gt;Root&lt;/b&gt; RIB이기 때문에 이 조상 뷰 또한 &lt;b&gt;Root&lt;/b&gt; RIB에서 제공이 되어야합니다.&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;RootViewController에 아래의 extension을 추가해서 LoggedInViewControllable을 준수하도록 해주세요&lt;/p&gt;
&lt;pre id=&quot;code_1642058397316&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// MARK: LoggedInViewControllable

extension RootViewController: LoggedInViewControllable {
}&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;LoggedinViewControllable&lt;/b&gt; 인스턴스를 &lt;b&gt;LoggedIn&lt;/b&gt; RIB에 주입 할 필요가 있는데 얘 역시 tutorial 3에서 다룬대요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 일단 LoggedInBuilder.swift를 &lt;a href=&quot;https://github.com/uber/ribs/blob/assets/tutorial_assets/ios/tutorial2-composing-ribs/source/source1.swift&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이 코드&lt;/a&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;LoggedIn&lt;/b&gt; RIB은 &lt;b&gt;Root &lt;/b&gt;RIB에 의해 수행된 &lt;b&gt;LoggedInViewControllable&lt;/b&gt;의 메서드를 통해 하위 RIB들을 show/hide 할 수 있게 됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Attaching the&lt;span&gt;&amp;nbsp;&lt;/span&gt;OffGame&lt;span&gt;&amp;nbsp;&lt;/span&gt;RIB when the&lt;span&gt;&amp;nbsp;&lt;/span&gt;LoggedIn&lt;span&gt;&amp;nbsp;&lt;/span&gt;RIB loads&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 언급했듯 LoggedIn RIB은 view-less 하기 때문에 오로지 자신의 하위 RIB들간의 전환만 가능합니다. 이제 그 하위 RIB들을 만들어볼거에요. &quot;Start Game&quot; 버튼을 가지고있고 이를 관리하는 OffGame이라고 불리는 RIB을 만들어봅시당&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;OffGame폴더 내에 이번에는 뷰를 가진 RIB을 만들어줄게요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RIB템플릿을 선택하고 &quot;Owns corresponding view&quot;를 체크한 상태로 만들어주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;b&gt;OffGameViewController&lt;/b&gt;에 &lt;a href=&quot;https://github.com/uber/ribs/blob/assets/tutorial_assets/ios/tutorial2-composing-ribs/source/source2.swift&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이 코드&lt;/a&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;이제 OffGame RIB을 상위 RIB인 LoggedIn RIB과 연결해봅시다. LoggedIn RIB은 OffGame RIB을 자신의 하위 RIB으로서 attach할 수 있어야합니다.&amp;nbsp;&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;OffGameBuildable 인스턴스에 대한 종속성을 선언하기 위해 LoggedInRouter의 생성자를 변경해야해요.&lt;/p&gt;
&lt;pre id=&quot;code_1642059910031&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;init(interactor: LoggedInInteractable,
     viewController: LoggedInViewControllable,
     offGameBuilder: OffGameBuildable) { // new
    self.viewController = viewController
    self.offGameBuilder = offGameBuilder //new
    super.init(interactor: interactor)
    interactor.router = self
}

// MARK: - Private

...

private let offGameBuilder: OffGameBuildable // new&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제&amp;nbsp;&lt;b&gt;LoggedInBuilder&lt;/b&gt;를 수정해서 &lt;b&gt;OffGameBuilder&lt;/b&gt;&amp;nbsp;클래스를&amp;nbsp;인스턴스화하고&amp;nbsp;&lt;b&gt;LoggedInRouter&lt;/b&gt; 인스턴스에 주입합니다. 다음과 같이 빌드 기능을 수정해주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1642060257126&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// MARK: LoggedIn/LoggedInBuilder.swift

func build(withListener listener: LoggedInListener) -&amp;gt; LoggedInRouting {
    let component = LoggedInComponent(dependency: dependency)
    let interactor = LoggedInInteractor()
    interactor.listener = listener

    let offGameBuilder = OffGameBuilder(dependency: component)
    return LoggedInRouter(interactor: interactor,
                          viewController: component.loggedInViewController,
                          offGameBuilder: offGameBuilder)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지하면 에러가 하나 나올텐데.. 이걸 해결하려면 &lt;b&gt;OffGameBuilder&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;OffGameBuilder&lt;/b&gt;의 계약된 종속성을 만족하기 위해서, 우리는 &lt;b&gt;LoggedInComponent&lt;/b&gt; 클래스를 &lt;b&gt;OffGameComponent&lt;/b&gt;를 준수할 수 있게 수정해야합니다(RIB 종속성과 컴포넌트들은 튜토리얼 3에서 좀 더 자세하게 다룬대요).&lt;/p&gt;
&lt;pre id=&quot;code_1642060372823&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// MARK: LoggedIn/LoggedInComponent.swift

final class LoggedInComponent: Component&amp;lt;LoggedInDependency&amp;gt;, OffGameDependency {
    
    fileprivate var loggedInViewController: LoggedInViewControllable {
        return dependency.loggedInViewController
    }
}&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;사용자가 로그인 한 뒤 즉각적으로 OffGame RIB에 의한 시작 화면이 나오면 참 좋겠죠? 그래서 &lt;b&gt;LoggedIn&lt;/b&gt; RIB은 자신이 로드 되자마자 &lt;b&gt;OffGame&lt;/b&gt; RIB을 attach해주어야 해요.&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;LoggedInRouter&lt;/b&gt;에서 didLoad 메서드를 오버라이드 해서 &lt;b&gt;OffGame&lt;/b&gt; RIB을 바로 띄울 수 있도록 로드해주세요&lt;/p&gt;
&lt;pre id=&quot;code_1642060681901&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// MARK: LoggedIn/LoggedInRouter.swift


// MARK: - Private

...

private var currentChild: ViewableRouting?

private func attachOffGame() {
    let offGame = offGameBuilder.build(withListener: interactor)
    self.currentChild = offGame
    attachChild(offGame)
    viewController.present(viewController: offGame.viewControllable)
}

override func didLoad() {
    super.didLoad()
    attachOffGame()
}

...&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;span style=&quot;color: #ee2323;&quot;&gt;Argument type 'LoggedInInteractable' does not conform to expected type 'OffGameListener'&lt;/span&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;LoggedInInteractable&lt;/b&gt;이 &lt;b&gt;OffGameListener&lt;/b&gt; 프로토콜을 준수하도록 해줄게요. 이걸 준수해주어야 &lt;b&gt;OffGame&lt;/b&gt; RIB 이벤트를 받을 수 있게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1642061603952&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protocol LoggedInInteractable: Interactable, OffGameListener {
    weak var router: LoggedInRouting? { get set }
    weak var listener: LoggedInListener? { get set }
}&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;LoggedIn&lt;/b&gt; RIB은 로딩이 된 후에 바로 &lt;b&gt;OffGame&lt;/b&gt; RIB을 attach할 수 있고, &lt;b&gt;OffGame&lt;/b&gt;에서 오는 이벤트들을 들을 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Cleaning up the attached views when the&lt;span&gt;&amp;nbsp;&lt;/span&gt;LoggedIn&lt;span&gt;&amp;nbsp;&lt;/span&gt;RIB is detached&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LoggedIn&lt;/b&gt; RIB이 자신의 뷰를 가지고 있지 않고, 오히려 상위 계층 뷰를 수정하기 때문에 &lt;b&gt;Root&lt;/b&gt; RIB는 &lt;b&gt;LoggedIn&lt;/b&gt; RIB가 수행했을 수 있는 뷰의 수정을 자동으로 제거할 방법이 없습니다.&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;Viewless LoggedIn&lt;/b&gt; RIB을 생성하는 데 사용한 Xcode 템플릿은 &lt;b&gt;LoggedIn&lt;/b&gt; RIB이 분리될 때 뷰 수정 사항을 정리할 수 있는 후크를 이미 제공합니다.(다행이다)&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;LoggedInViewControllable&lt;/b&gt; 프로토콜에 present, dismiss 메서드를 만들어주세요&lt;/p&gt;
&lt;pre id=&quot;code_1642061881222&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protocol LoggedInViewControllable: ViewControllable {
    func present(viewController: ViewControllable)
    func dismiss(viewController: ViewControllable)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 나서 우리는 cleanupViews라는 메서드가 현재 자식RIB의 view controller를 dismiss 해줄 수 있도록 해줄거에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoggedInRouter의 cleanupViews 메서드를 다음과 같이 바꿔주세요&lt;/p&gt;
&lt;pre id=&quot;code_1642061990107&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func cleanupViews() {
    if let currentChild = currentChild {
        viewController.dismiss(viewController: currentChild.viewControllable)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;cleanupViews&lt;/b&gt; 메소드는 상위 RIB이 &lt;b&gt;LoggedIn&lt;/b&gt; RIB를 detach 할 때 &lt;b&gt;LoggedInInteractor&lt;/b&gt;에 의해 호출됩니다. &lt;b&gt;cleanupViews&lt;/b&gt;에서 현재 보여지고 있는 뷰 컨트롤러를 해제함으로써 &lt;b&gt;LoggedIn&lt;/b&gt; RIB이 분리된 후 상위 RIB의 뷰 계층 구조에 뷰를 남기지 않도록 보장합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Switching to&lt;span&gt;&amp;nbsp;&lt;/span&gt;TicTacToe&lt;span&gt;&amp;nbsp;&lt;/span&gt;RIB on tapping &quot;Start Game&quot; button&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 했던 거랑 비슷하니까 혼자 해보라는데..TicTacToe 자체의 UI는 이미 튜토리얼 프로젝트에 만들어져있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭘 해주면 될까요 &amp;nbsp;일단 우리가 방금까지 만든 &lt;b&gt;OffGame&lt;/b&gt; RIB과 같은 계층에 있는 RIB이니까 마찬가지로 &lt;b&gt;TicTacToe&lt;/b&gt; RIB의 상위 RIB은 &lt;b&gt;LoggedIn&lt;/b&gt; RIB이겠네요!&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;LoggedIn&lt;/b&gt; RIB이 로드되자마자 &lt;b&gt;OffGame&lt;/b&gt; RIB을 호출하잖아요. 그리고나서 &lt;b&gt;OffGame&lt;/b&gt; RIB에서 Start를 탭 하면 &lt;b&gt;TicTacToe&lt;/b&gt; RIB으로 전환해주어야해요.&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;1. 가장 먼저 &lt;b&gt;OffGame&lt;/b&gt; RIB의 Start버튼에 대한 TapGesture를 추가해줄게요.&lt;/p&gt;
&lt;pre id=&quot;code_1642081888703&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// MARK: OffGame/OffGameViewController.swift

// MARK: - Private
private func buildStartButton() {
	...
    
    startButton.rx.tap								
        .subscribe(onNext: { [weak self] in
            self?.listener?.startTicTacToe()
        })
        .disposed(by: disposeBag)
}

private let disposeBag = DisposeBag() 

...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. VC에서 Interactor로 전달할 수 있도록 &lt;b&gt;PresentableListener&lt;/b&gt;에 메소드를 추가해주세요&lt;/p&gt;
&lt;pre id=&quot;code_1642081038569&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// OffGameViewController.swift

protocol OffGamePresentableListener: class {
    func startTicTacToe()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Listener를 통해 상위 RIB에 이벤트를 전달할 수 있도록 할게요&lt;/p&gt;
&lt;pre id=&quot;code_1642081209787&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// OffGameInteractor.swift
...

protocol OffGameListener: AnyObject {
    func startTicTacToe()
}

final class OffGameInteractor: PresentableInteractor&amp;lt;OffGamePresentable&amp;gt;, OffGameInteractable, OffGamePresentableListener {
    
    func startTicTacToe() {
        listener?.attachTicTacToe()
    }
    
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4.&amp;nbsp;이제 상위 RIB(&lt;b&gt;LoggedIn&lt;/b&gt; RIB)에서 startTicTacToe에 대한 구현부를 작성하면 돼요&lt;/p&gt;
&lt;pre id=&quot;code_1642081422408&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// LoggedInInteractor에서 Routing 메서드 추가

protocol LoggedInRouting: Routing {
    func cleanupViews()
    // TODO: Declare methods the interactor can invoke to manage sub-tree via the router.
    func routeToTicTacToe()
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1642081608995&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// LoggedInRouter.swift

func routeToTicTacToe() {
    attachTicTacToe()
    detachCurrentChild()
}

// MARK: - Private

...

private func attachTicTacToe() {
    let tictactoe = ticTacToeBuilder.build(withListener: interactor)
    self.currentChild = tictactoe
    attachChild(tictactoe)
    viewController.present(viewController: tictactoe.viewControllable)
}

private func detachCurrentChild() {
    if let currentChild = currentChild {
        detachChild(currentChild)
        viewController.dismiss(viewController: currentChild.viewControllable)
        self.currentChild = nil
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 하위 리스너를 상위 RIB Router에 연결해준다&lt;/p&gt;
&lt;pre id=&quot;code_1642081676291&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// LoggedInInteractor.swift

protocol LoggedInInteractable: Interactable, OffGameListener, TicTacToeListener {
    var router: LoggedInRouting? { get set }
    var listener: LoggedInListener? { get set }
}&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;6. &lt;b&gt;TicTacToe&lt;/b&gt; Builder의 종속성을 &lt;b&gt;LoggedInRouter&lt;/b&gt;의 생성자에 생성해주기&lt;/p&gt;
&lt;pre id=&quot;code_1642078903417&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;init(interactor: LoggedInInteractable,
     viewController: LoggedInViewControllable,
     offGameBuilder: OffGameBuildable,
     tictactoeBuilder: TicTacToeBuildable) { 		// new

    self.viewController = viewController
    self.offGameBuilder = offGameBuilder
    self.ticTacToeBuilder = tictactoeBuilder		// new

    super.init(interactor: interactor)
    interactor.router = self
}

// MARK: - Private    
...
private let ticTacToeBuilder: TicTacToeBuildable	// new&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6-1.&amp;nbsp;&lt;b&gt;LoggedInBuilder&lt;/b&gt;를 수정해서&amp;nbsp;&lt;b&gt;TicTacToe Builder&lt;/b&gt;&amp;nbsp;클래스를&amp;nbsp;인스턴스화하고&amp;nbsp;&lt;b&gt;LoggedInRouter&lt;/b&gt;&amp;nbsp;인스턴스에 주입&lt;/p&gt;
&lt;pre id=&quot;code_1642079087210&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func build(withListener listener: LoggedInListener) -&amp;gt; LoggedInRouting {
    let component = LoggedInComponent(dependency: dependency)
    let interactor = LoggedInInteractor()
    interactor.listener = listener

    let offGameBuilder = OffGameBuilder(dependency: component)
    let tictactoeBuilder = TicTacToeBuilder(dependency: component)		// new

    return LoggedInRouter(interactor: interactor,
                          viewController: component.loggedInViewController,
                          offGameBuilder: offGameBuilder,
                          tictactoeBuilder: tictactoeBuilder)			// new
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6-2.&amp;nbsp;&lt;b&gt;TicTacToeBuilder&lt;/b&gt;의 계약된 종속성을 만족하기 위해서, 우리는&amp;nbsp;&lt;b&gt;LoggedInComponent&lt;/b&gt;&amp;nbsp;클래스를 &lt;b&gt;TicTacToeComponent&lt;/b&gt;를 준수할 수 있게 수정&lt;/p&gt;
&lt;pre id=&quot;code_1642079318275&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// MARK: LoggedIn/LoggedInBuilder.swift

final class LoggedInComponent: Component&amp;lt;LoggedInDependency&amp;gt;, OffGameDependency, TicTacToeDependency {
    
    fileprivate var loggedInViewController: LoggedInViewControllable {
        return dependency.loggedInViewController
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7. 5번에서 상위 RIB이 &lt;b&gt;TicTacToe&lt;/b&gt;의 &lt;b&gt;Listener를&lt;/b&gt;&amp;nbsp;준수하게 되면서 TicTacToe 리스너의&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;gameDidEnd&lt;span style=&quot;color: #000000;&quot;&gt;() 메서드를 구현해야하는데 이건 다음 스텝에서 하는거 같아서 그냥 주석처리 해두면 된다&lt;/span&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Simulator Screen Recording - iPhone 12 mini - 2022-01-13 at 22.57.20.gif&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2NTGq/btrqAoppHgr/aRSf5CLWmlRtVQShhzqLv1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2NTGq/btrqAoppHgr/aRSf5CLWmlRtVQShhzqLv1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2NTGq/btrqAoppHgr/aRSf5CLWmlRtVQShhzqLv1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/b2NTGq/btrqAoppHgr/aRSf5CLWmlRtVQShhzqLv1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;327&quot; height=&quot;708&quot; data-filename=&quot;Simulator Screen Recording - iPhone 12 mini - 2022-01-13 at 22.57.20.gif&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2340&quot;/&gt;&lt;/span&gt;&lt;/figure&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;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Attaching the&lt;span&gt;&amp;nbsp;&lt;/span&gt;OffGame&lt;span&gt;&amp;nbsp;&lt;/span&gt;RIB and detaching the&lt;span&gt;&amp;nbsp;&lt;/span&gt;TicTacToe&lt;span&gt;&amp;nbsp;&lt;/span&gt;RIB when we have a winner&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 gif에서 게임이 끝나면 &lt;b&gt;TicTacToe&lt;/b&gt; RIB에서 다시 게임을 시작할 수 있는 &lt;b&gt;OffGame&lt;/b&gt; RIB 으로 돌아가야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러려면 지금 attach 되어있는 &lt;b&gt;TicTacToe &lt;/b&gt;RIB를 detach하고 &lt;b&gt;OffGame&lt;/b&gt; RIB을 attach 해주어야겠네요.&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;LoggedIn&lt;/b&gt; RIB과 &amp;lt;-&amp;gt;&amp;nbsp;&lt;b&gt;OffGame&lt;/b&gt; RIB /&amp;nbsp;&lt;b&gt;TicTacToe&lt;/b&gt; RIB 사이의 attach,detach를 구현해놨고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 한것과&amp;nbsp;동일한&amp;nbsp;리스너&amp;nbsp;기반&amp;nbsp;라우팅&amp;nbsp;패턴을&amp;nbsp;사용하면 돼요.&amp;nbsp;제공된 코드의 &lt;b&gt;TicTacToe&lt;/b&gt;&amp;nbsp;RIB에는&amp;nbsp;이미&amp;nbsp;리스너가&amp;nbsp;설정되어&amp;nbsp;있습니다.&amp;nbsp;&lt;b&gt;LoggedIn&lt;/b&gt;&amp;nbsp;RIB가&amp;nbsp;TicTacToe&amp;nbsp;이벤트에&amp;nbsp;응답할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;&lt;b&gt;LoggedInInteractor&lt;/b&gt;에서&amp;nbsp;구현하기만&amp;nbsp;하면&amp;nbsp;됩니다.&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;LoggedInRouting&lt;/b&gt; 프로토콜에 &lt;b&gt;routeToOffGame&lt;/b&gt; 메서드를 선언해주세요&lt;/p&gt;
&lt;pre id=&quot;code_1642085347605&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// LoggedInInteractor.swift

protocol LoggedInRouting: Routing {
    func routeToTicTacToe()
    func routeToOffGame()
    func cleanupViews()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까 비워두었던 &lt;b&gt;gameDidEnd()&lt;/b&gt; 메서드의 바디를 작성할게요&lt;/p&gt;
&lt;pre id=&quot;code_1642087126538&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// LoggedInInteractor.swift

final class LoggedInInteractor: Interactor, LoggedInInteractable {
    ...
	
    // MARK: - TicTacToeListener
    
    func gameDidEnd() {
        router?.routeToOffGame()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 다음, &lt;b&gt;routeToOffGame&lt;/b&gt; 메서드를 &lt;b&gt;LoggedInRouter&lt;/b&gt; 클래스 안에서 구현해주세요&lt;/p&gt;
&lt;pre id=&quot;code_1642087244874&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// LoggedInRouter.swift

final class LoggedInRouter: Router&amp;lt;LoggedInInteractable&amp;gt;, LoggedInRouting {
    ...
    func routeToOffGame() {
        detachCurrentChild()
        attachOffGame()
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Simulator Screen Recording - iPhone 12 mini - 2022-01-14 at 00.22.35.gif&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/W99QM/btrqFoBSu58/PK5OmW4bZilTU6rEByIK91/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/W99QM/btrqFoBSu58/PK5OmW4bZilTU6rEByIK91/img.gif&quot; data-alt=&quot;완성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/W99QM/btrqFoBSu58/PK5OmW4bZilTU6rEByIK91/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/W99QM/btrqFoBSu58/PK5OmW4bZilTU6rEByIK91/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;337&quot; height=&quot;729&quot; data-filename=&quot;Simulator Screen Recording - iPhone 12 mini - 2022-01-14 at 00.22.35.gif&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2340&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;완성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>iOS</category>
      <author>snowe</author>
      <guid isPermaLink="true">https://snowee.tistory.com/60</guid>
      <comments>https://snowee.tistory.com/60#entry60comment</comments>
      <pubDate>Thu, 13 Jan 2022 17:29:43 +0900</pubDate>
    </item>
    <item>
      <title>iOS) RIBs Architecture</title>
      <link>https://snowee.tistory.com/59</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;https://github.com/uber/RIBs/wiki를 바탕으로 공부한 RIBs에 대해 정리를 하는 글입니다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RIBs란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RIBs는 Uber의 크로스 플랫폼 아키텍쳐 프레임워크입니다. RIBs는 다음과 같은 원칙들을 준수합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Encourage Cross-Platform Collaboration&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 서비스를 기준으로 볼 때 Android, iOS 모두 개발적인 측면에서 복잡한 부분들은 대체로 비슷합니다. 이 점을 활용하여 RIBs는 Android와 iOS 모두 비슷한 개발 패턴들을 제공합니다. 이는 iOS, Android 팀 모두 하나의 단일 아키텍쳐를 서로 공유할 수 있음을 의미하는데 이를 통해 서로의 비즈니스 로직을 점검해줄 수 있다는 장점이 생깁니다(뭔가 아키텍쳐의 틀을 딱 정해두는 것은 장점이자 단점이 될 수 있을 것 같아요).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Minimize Global States and Decisions&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전역에 걸친 변경들(Global state changes)은 개발자로 하여금 예기치 못한 행동을 유발할 수 있고, 그런 변경들이 가져올 수 있는 영향에 대해 개발자들이 완벽히 파악하기란 쉽지 않습니다. RIBs는 잘 독립된 각각의 RIB들의 상태를 deep hierarchy 내에서 캡슐화하여 그러한 이슈를 피할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Testability and Isolation&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Class들은 독립적으로 생각되어야 하고 Unit Test가 쉬워야 합니다. 각각의 RIB 클래스들은 서로 다른 책임들을 갖습니다(Routing, Business Logic, View Logic, 다른 RIB 클래스의 생성 등). 더불어, 부모 RIB 로직은 자식 RIB로직과 분리되어 있습니다. 이것이 RIB 클래스들이 테스트가 쉽고 독립적으로 생각될 수 있는 이유입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Tooling for Developer Productivity&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;Adopting non-trivial architectural patterns does not scale beyond small applications without robust tooling.&lt;/span&gt; 이건 뭔 느낌의 말인지는 알겠는데 한국어로 표현을 못하겠네요..허허&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 하고싶은 말은 다른 아키텍쳐를 사용해도 강력한 tooling 기능이 있어야 되는데 RIBs는 그걸 자체적으로 제공해서 좋다는 의미인 것 같아요. &lt;span style=&quot;background-color: #ffffff;&quot;&gt;코드 생성, 정적 분석 및 runtime&amp;nbsp;integrations에 대한 IDE 툴링을 제공한다고 합니다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;Open-Closed Principle&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자는 언제든지 현재 코드를 수정하지 않고 새로운 기능을 추가할 수 있어야 합니다.(새로운 기능을 추가하기 위해 기존 코드를 계속 건드려야하면 안좋긴 하겠죠) 이것은 RIBs를 사용하면 가능합니다. &lt;span style=&quot;background-color: #ffffff;&quot;&gt;예를들어, 부모 RIB을 거의 변경하지 않고 부모의 종속성이 필요한 복잡한 자식 RIB을 attach하거나 build할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;Structured around Business Logic&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;앱의 비즈니스 로직 구조는 UI의 구조를 엄격하게 따를 필요는 없습니다. 예를 들어, 뷰의 성능과 애니메이션을 용이하게 하기 위해서, 뷰 계층 구조는 RIB의 구조보다 더 얕을 수 있습니다. 아니면 단일 기능 RIB이 각각 다른 위치에 나타나는 세개의 뷰들을 컨트롤 할 수도 있습니다.(...?)&lt;span style=&quot;color: #9d9d9d;&quot;&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;Or, a single feature RIB may control the appearance of three views that appear at different places in the UI.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;Explicit Contracts&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;요구사항들은 &lt;/span&gt;compile-time safe contracts으로 선언되어야 합니다. &lt;span style=&quot;background-color: #ffffff;&quot;&gt;클래스 종속성과 순서(ordering) 종속성이 충족되지 않으면 클래스는 컴파일되지 않아야 합니다. 우리는&amp;nbsp;ReactiveX를&amp;nbsp;사용하여&amp;nbsp;순서&amp;nbsp;종속성을&amp;nbsp;나타내고,&amp;nbsp;유형&amp;nbsp;안전&amp;nbsp;종속성&amp;nbsp;주입(DI)&amp;nbsp;시스템을&amp;nbsp;사용하여&amp;nbsp;클래스&amp;nbsp;종속성을&amp;nbsp;나타내고,&amp;nbsp;많은&amp;nbsp;DI&amp;nbsp;범위를&amp;nbsp;사용하여&amp;nbsp;데이터&amp;nbsp;불변성&amp;nbsp;생성을&amp;nbsp;권장합니다.&lt;/span&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;span style=&quot;background-color: #ffffff;&quot;&gt;음 몇개는 아직 읽어도 모르겠는 것들이 있는데.. 일단 계속 하다보면 이해가 될거라 믿습니다..!&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RIB의 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까 위에서 &lt;b&gt;Testability and Isolation &lt;/b&gt;부분에서 RIB의 클래스들은 각각 다른 책임을 갖는다고 했어요. 여기서는 그것과 관련된 내용을 다룹니다.&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;VIPER라는 아키텍쳐에서 발전된 형태가 RIBs이기 때문에 VIPER를 기존에 사용해보신 분들은 좀 더 이해가 빠르실 것 같아요. 저는 안써봤기 때문에 찬찬히 살펴보면&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;RIBs는 보통 아래와 같은 구성요소로 이루어져 있어요. 모든 구성 요소들은 자체 클래스로 구현됩니다. 그래서 각각 자신만의 책임을 갖는 셈이 되는거죠.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;149137004-20c851df-d691-47eb-83d8-d1b39ad8eaa5.png&quot; data-origin-width=&quot;2288&quot; data-origin-height=&quot;863&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcXU5Z/btrqAb3oX1a/3qSSbsAHDHGWgg7JMl6mT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcXU5Z/btrqAb3oX1a/3qSSbsAHDHGWgg7JMl6mT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcXU5Z/btrqAb3oX1a/3qSSbsAHDHGWgg7JMl6mT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcXU5Z%2FbtrqAb3oX1a%2F3qSSbsAHDHGWgg7JMl6mT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2288&quot; height=&quot;863&quot; data-filename=&quot;149137004-20c851df-d691-47eb-83d8-d1b39ad8eaa5.png&quot; data-origin-width=&quot;2288&quot; data-origin-height=&quot;863&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Interactor&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Interactor는 비즈니스 로직이 포함됩니다.&lt;/li&gt;
&lt;li&gt;Rx로 subscription을 수행하는 곳도 이 곳이고&lt;/li&gt;
&lt;li&gt;상태 변결에 대한 결정,&lt;/li&gt;
&lt;li&gt;무슨 데이터를 어디에 저장할 것인지,&lt;/li&gt;
&lt;li&gt;어떤 다른 RIBs가 자식으로서 attached되어야 하는지 등을 결정하게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Interactor에 의해 수행되는 모든 동작들은 반드시 자신의 life cycle 내에서 동작해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RIBs에는&amp;nbsp;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;Interactor가 활성화 된 경우에만 비즈니스 로직이 실행되도록 tooling이 내장되어 있습니다. 그 결과, Interactor가 비활성화되는 시나리오는 방지되지만 subscriptions이 계속 발생하여 비즈니스 로직이나 UI상태에 원치 않는 업데이트가 발생합니다. 그래서 꼭 life cycle 내에서 동작할 수 있도록 해주어야합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Router&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Router는 Interactor가 주는걸 듣고(받고?) 그걸 자식 RIB에 attaching, detaching 행위로 변환합니다. Router는 다음과 같은 세가지 간단한 이유로 존재합니다.&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;span style=&quot;background-color: #ffffff;&quot;&gt;1. Router는 Humble&amp;nbsp;Objects의 역할을 하여, 하위 Interactor를 속이거나 그 존재에 대해 신경 쓸 필요없이 복잡한 Interactor 로직을 쉽게 테스트 할 수 있습니다&lt;/span&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;span style=&quot;background-color: #ffffff; color: #9d9d9d;&quot;&gt;* Humble Object: 테스트 가능한 객체를 감싸는 wrapper로, &amp;nbsp;테스트하기 어려운 객체의 로직을 비용 효율적인 방식으로 가져오는 방법.&lt;/span&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;2.&amp;nbsp;Router는 부모 interactor와 자식 interactor간에 추가 추상화 계층을 만듭니다. 이로 인해 interactor간의 동기 통신이 조금 더 어려워지고, RIBs간의 직접 연결 대신 reactive&amp;nbsp;communication 채택이 권장됩니다.&amp;nbsp;&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;3. Router는 interactor에 의해 수행될 수 있는 단순하고 반복적인 라우팅 로직을 포함하고있습니다. 이 상용구 코드(boilerplate&amp;nbsp;code)를 제외하면(Router로 뺌으로써) Interactor를 작게 유지하고 RIB이 제공하는 핵심 비즈니스 로직에 더 집중 할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Builder&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Builder의&amp;nbsp;책임은&amp;nbsp;모든&amp;nbsp;RIB의&amp;nbsp;구성&amp;nbsp;클래스와&amp;nbsp;각&amp;nbsp;RIB의&amp;nbsp;자식에&amp;nbsp;대한&amp;nbsp;빌더를&amp;nbsp;인스턴스화하는&amp;nbsp;것입니다.&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;Builder에서 클래스 생성 로직을 분리하면 iOS에서 모의 가능성(mockability)에 대한 지원이 추가괴고 나머지 RIB 코드는 DI구현의 세부 사항에 영향을 미치지 않습니다. Builder는 프로젝트에&amp;nbsp;사용된&amp;nbsp;DI&amp;nbsp;시스템을&amp;nbsp;인식해야&amp;nbsp;하는&amp;nbsp;RIB의&amp;nbsp;유일한&amp;nbsp;부분입니다.&amp;nbsp;다른&amp;nbsp;빌더를&amp;nbsp;구현하면&amp;nbsp;다른&amp;nbsp;DI&amp;nbsp;메커니즘을&amp;nbsp;사용하여&amp;nbsp;프로젝트에서&amp;nbsp;나머지&amp;nbsp;RIB&amp;nbsp;코드를&amp;nbsp;재사용할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Presenter&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Presenter는 Business Model을 View Model로 혹은 그 반대로 변환하는 stateless 클래스입니다. View Model 변환 테스트를 용이하게 하는데에 사용될 수 있습니다. 하지만 종종 이러한 변환이 너무 사소하면 이 클래스를 만들지 않습니다. 그러한 경우에는 view model를 변환하는 것은 View(Controller)나 Interactor의 책임이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;View(Controller)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View는 UI를 빌드하고 업데이트합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;UI컴포넌트들의 레이아웃을 잡고 인스턴스화 하는것,&lt;/li&gt;
&lt;li&gt;user interaction을 처리하는 것,&lt;/li&gt;
&lt;li&gt;UI 컴포넌트들을 받은 데이터로 채우는 것,&lt;/li&gt;
&lt;li&gt;애니메이션 등이 View가 하는 일에 포함됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View는 가능한 한 &quot;벙어리&quot;이도록 디자인되어 있습니다(가능한 한 뭔가 주도적으로 뭔가를 하기보다 시키는 것만 하라는 소리인듯). 그들은 그저 정보를 보여줍니다. 그래서 일반적으로, View는 unit test가 필요한 코드를 포함하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Component&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Component는 RIB 종속성을 관리하기 위해 사용됩니다. 애네는 RIB을 구성하는 다른 유닛들을 인스턴스화 하여 Builder를 돕습니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Component는 RIB를 구축하는 데 필요한 외부 종속성에 대한 액세스를 제공하고,&lt;/li&gt;
&lt;li&gt;RIB 자체에 의해 생성된 종속성을 소유하고 다른 RIB에서 이에 대한 액세스를 제어합니다.&lt;/li&gt;
&lt;li&gt;상위 RIB의 구성요소는 일반적으로 상위 RIB의 종속성에 대한 하위 액세스를 제공하기 위해 하위 RIB의 빌더에 주입됩니다.&lt;/li&gt;
&lt;/ul&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;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RIB의 상태 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱의 상태는 현재 RIB 트리에 연결된 RIB에 의해 대부분 관리되고 표현됩니다. 예를 들어, 사용자가 아래와 같은 트리를 갖는 차량 공유 앱을 사용한다고 했을 때 사용자가 타는 flow에 따라 앱은 RIB을 attach하고 detach합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;149136944-69d1eec7-16ed-4693-8e3d-0fcb679e2863.gif&quot; data-origin-width=&quot;1633&quot; data-origin-height=&quot;1561&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wLYUP/btrqAouNNYD/IMgVwRBAlDXM60KeTgB9Zk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wLYUP/btrqAouNNYD/IMgVwRBAlDXM60KeTgB9Zk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wLYUP/btrqAouNNYD/IMgVwRBAlDXM60KeTgB9Zk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/wLYUP/btrqAouNNYD/IMgVwRBAlDXM60KeTgB9Zk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1633&quot; height=&quot;1561&quot; data-filename=&quot;149136944-69d1eec7-16ed-4693-8e3d-0fcb679e2863.gif&quot; data-origin-width=&quot;1633&quot; data-origin-height=&quot;1561&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RIB은 그들의 범위 내에서만 상태 결정을 합니다. 예를 들어, &lt;b&gt;LoggedIn&lt;/b&gt; RIB은 &lt;b&gt;Requset &lt;/b&gt;와&amp;nbsp;&lt;b&gt;OnTrip &lt;/b&gt;과 같은 상태간 전환에 대해서만 상태 결정을 합니다. 그 말은 즉슨, 우리가 &lt;b&gt;OnTrip &lt;/b&gt;화면에 들어간 뒤 어떻게 행동할 것인지에 대한 결정은 &lt;b&gt;LoggedIn&lt;/b&gt; RIB에서는 전혀 하지 않는다는 것을 의미합니다.&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;RIB을 추가/삭제 함으로써 모든 상태를 저장할 수 있는 것은 아닙니다. 예를 들어, 사용자의 프로필 성정이 변경 될 때, 어떤 RIB도 attach 혹은 detach 되지 않습니다. &lt;span style=&quot;background-color: #ffffff;&quot;&gt;일반적으로 이 상태는 세부사항이 변경될 때 값을 다시 방출하는 불변 모델 스트림(streams of immutable models)에 저장됩니다. 예를 들어, 사용자의 이름은 LoggedIn 범위 내에 있는&amp;nbsp;&lt;b&gt;ProfileDataStream&lt;/b&gt; 에 저장 될 수 있습니다. 오직 네트워크의 응답만이 이 Stream에 쓰기 권한으로 접근할 수 있습니다. 우리는 DI 그래프 아래로 이러한 스트림에 대한 읽기 액세스를 제공하는 인터페이스를 전달합니다.&lt;/span&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;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;/span&gt;RIB은 RIB 상태에 대해 단일 정보 소스를 강제하고 있지 않습니다. 이것은 React와 같은 독단적인 프레임워크가 이미 기본적으로 제공하는 것과 대조됩니다. 각 RIB의 컨텍스트 내에서 단방향 데이터 흐름을 촉진하는 패턴을 채택하도록 선택하거나 효율적인 플랫폼 애니메이션 프레임워크를 활용하기 위해 비즈니스 상태와 보기 상태가 일시적으로 분기되도록 할 수 있습니다.&lt;/p&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RIB 간의 상호작용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Interactor가 비즈니스 로직 결정을 할 때, 또 다른 RIB에 completion과 같은 이벤트에 대해 알려주고, 데이터를 보내야 할 수도 있습니다. RIBs프레임워크에는 RIB 간에 데이터를 전달하는 단일 방법이 포함되어있지 않습니다. 그럼에도 불구하고, 몇몇의 일반적인 패턴들을 용이하게 하기 위해 만들어졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;149136661-90f3bcc4-7e89-4892-a626-643a13af1979.png&quot; data-origin-width=&quot;1357&quot; data-origin-height=&quot;599&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vw7gZ/btrqCSva7H9/BLfsVwGZ1cBvHq6ZHH2NCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vw7gZ/btrqCSva7H9/BLfsVwGZ1cBvHq6ZHH2NCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vw7gZ/btrqCSva7H9/BLfsVwGZ1cBvHq6ZHH2NCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvw7gZ%2FbtrqCSva7H9%2FBLfsVwGZ1cBvHq6ZHH2NCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;446&quot; height=&quot;197&quot; data-filename=&quot;149136661-90f3bcc4-7e89-4892-a626-643a13af1979.png&quot; data-origin-width=&quot;1357&quot; data-origin-height=&quot;599&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로, 만약 커뮤니케이션이 하위 RIB으로 하향 전달되는 경우, 우리는 이 정보를 Rx Stream으로 전달합니다. 혹은 데이터가 하위 RIB의 build() 에 파라미터로서 포함될 수 있습니다. 이러한 경우,&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이 파라미터는 하위 RIB의 life time동안 변하지 않습니다.&amp;nbsp;&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;149136665-5d96791f-522d-467c-8090-8c6f934d5297.png&quot; data-origin-width=&quot;696&quot; data-origin-height=&quot;613&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m4Lbn/btrqBHglp4t/1nHqDLsqZObRVK2Z1bhDk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m4Lbn/btrqBHglp4t/1nHqDLsqZObRVK2Z1bhDk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m4Lbn/btrqBHglp4t/1nHqDLsqZObRVK2Z1bhDk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm4Lbn%2FbtrqBHglp4t%2F1nHqDLsqZObRVK2Z1bhDk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;420&quot; height=&quot;370&quot; data-filename=&quot;149136665-5d96791f-522d-467c-8090-8c6f934d5297.png&quot; data-origin-width=&quot;696&quot; data-origin-height=&quot;613&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 커뮤니케이션이 반대로 RIB 트리에서 상위 RIB의 Interactor로 전달된다면(&lt;span style=&quot;background-color: #ffffff;&quot;&gt;상향통신)&lt;/span&gt;, 하위 RIB보다(상위 RIB이) 더 오래 남아있을 수 있기 때문에 listener 인터페이스를 통해 커뮤케이션이 진행됩니다.&amp;nbsp;&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;상위 RIB 또는 DI 그래프의 일부 객체는 listener 인터페이스를 구현하고 DI 그래프에 배치하여 하위 RIB이 호출할 수 있도록 합니다.&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;부모(상위)가 자녀(하위)의 Rx 스트림을 직접 구독하도록 하는 대신 이 패턴을 사용하여 데이터를 위쪽으로 전달하면 몇 가지 이점이 있습니다. 메모리 누수를 방지하고 부모가 어떤 자식이 연결되어 있는지 모른 채 작성, 테스트 및 유지 관리할 수 있으며 자식 RIB를 연결/분리하는 데 필요한 의식의 양을 줄입니다. 이런 식으로 자식 RIB를 연결할 때 Rx 스트림이나 수신기를 등록 취소/재등록할 필요가 없습니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 인데..음..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감이 잡히는 것도 있고 아닌 것도 있지만.. 일단 튜토리얼도 4단계로 있으니까 계속 진행해보면서 이해해보겠습니다 :)&lt;/p&gt;</description>
      <category>iOS</category>
      <author>snowe</author>
      <guid isPermaLink="true">https://snowee.tistory.com/59</guid>
      <comments>https://snowee.tistory.com/59#entry59comment</comments>
      <pubDate>Wed, 12 Jan 2022 22:53:44 +0900</pubDate>
    </item>
    <item>
      <title>ETC) 내가 했던 Mac Setting 목록</title>
      <link>https://snowee.tistory.com/58</link>
      <description>&lt;h1&gt;Basic&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;&lt;code&gt;Keyboard&lt;/code&gt; - &lt;code&gt;Text&lt;/code&gt; 우측 체크박스 전부 해제&lt;/li&gt; 
 &lt;li&gt;&lt;code&gt;Accessibility&lt;/code&gt; - &lt;code&gt;Pointer Control&lt;/code&gt; - &lt;code&gt;Trackpad Options&lt;/code&gt; - &lt;code&gt;Enable dragging&lt;/code&gt; three finger drag 로 설정(세손가락으로 드래그하기)&lt;/li&gt; 
 &lt;li&gt;&lt;code&gt;Finder&lt;/code&gt; - &lt;code&gt;Preferences&lt;/code&gt; 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;&lt;code&gt;New Finder windows show&lt;/code&gt;에서 폴더 설정해주기&lt;/li&gt; 
   &lt;li&gt;&lt;code&gt;Advanced&lt;/code&gt; - &lt;code&gt;show all filename extensions&lt;/code&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;code&gt;Downloads폴더&lt;/code&gt; - &lt;code&gt;View - View Option&lt;/code&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;Group by : Date added&lt;/li&gt; 
     &lt;li&gt;Sort by : Name&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ul&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot;&gt;
&lt;h1&gt;Useful Apps&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
 &lt;li&gt;Alfred&lt;/li&gt;
 &lt;li&gt;AppCleaner&lt;/li&gt;
 &lt;li&gt;Chromium(크로미움으로 설치하기-용량,메모리측면에서 조흠)&lt;/li&gt;
 &lt;li&gt;Hidden Bar&lt;/li&gt;
 &lt;li&gt;IINA&lt;/li&gt;
 &lt;li&gt;Itsycal&lt;/li&gt;
 &lt;li&gt;KakaoTalk&lt;/li&gt;
 &lt;li&gt;KeyboardCleanTool&lt;/li&gt;
 &lt;li&gt;Magnet&lt;/li&gt;
 &lt;li&gt;Monosnap&lt;/li&gt;
 &lt;li&gt;Notion&lt;/li&gt;
 &lt;li&gt;Keka&lt;/li&gt;
 &lt;li&gt;Spark&lt;/li&gt;
 &lt;li&gt;Unicorn Blocker&lt;/li&gt;
 &lt;li&gt;VLC&lt;/li&gt;
 &lt;li&gt;YouTube Music(크롬 바로가기 앱)&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Developer Apps&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
 &lt;li&gt;GitKraken&lt;/li&gt;
 &lt;li&gt;Figma&lt;/li&gt;
 &lt;li&gt;Google Sheets&lt;/li&gt;
 &lt;li&gt;Postman&lt;/li&gt;
 &lt;li&gt;Karabiner-Elements(한영키 전환 안버벅이게)&lt;/li&gt;
 &lt;li&gt;Karabiner-EvenViewer(한영키 전환 안버벅이게)&lt;/li&gt;
 &lt;li&gt;Sublime Text&lt;/li&gt;
 &lt;li&gt;Typora&lt;/li&gt;
 &lt;li&gt;Visual Studio Code&lt;/li&gt;
 &lt;li&gt;Xcode&lt;/li&gt;
 &lt;li&gt;D2coding 글씨체 설치해서 Xcode에 적용시키기&lt;/li&gt;
 &lt;li&gt;Zeplin&lt;/li&gt;
 &lt;li&gt;Slack&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Useful&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;항상 ₩ 대신 ` 입력하기&lt;/h4&gt;
&lt;pre class=&quot;shell&quot;&gt;&lt;code&gt;$ cd ~/Library
$ mkdir KeyBindings
$ cd KeyBindings
$ touch DefaultKeyBinding.dict
$ vi DefaultKeyBinding.dict&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;{
  &quot;₩&quot; = (&quot;insertText:&quot;, &quot;`&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;하고 재부팅&lt;/span&gt;&lt;br&gt; &lt;br&gt; &lt;/p&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;code&gt;Get Info&lt;/code&gt; - &lt;code&gt;Open with&lt;/code&gt; - &lt;code&gt;app 선택&lt;/code&gt; - &lt;code&gt;Change All&lt;/code&gt;&lt;/li&gt; 
&lt;/ul&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot;&gt;
&lt;h1&gt;Terminal Setting&lt;/h1&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Homebrew 설치&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://brew.sh/index_ko&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://brew.sh/index_ko&lt;/span&gt;&lt;/a&gt; 에서 brew 설치&lt;/p&gt;
&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;/bin/bash -c &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(M1)&lt;/b&gt;이라면 설치 이후에 아래 코드&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;추가적으로 실행&lt;/span&gt;&lt;/p&gt;
&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;eval $(/opt/homebrew/bin/brew shellenv)&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;zsh, Oh My Zsh&lt;/h4&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;// zsh
$ brew install zsh

// oh-my-zsh
$ sh -c &quot;$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Node.js, epxress&lt;/h4&gt;
&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;// node설치하면서 npm 자동 설치됨 $ npm install express $ brew install node&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;iTerm2&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;  $ brew cask install iterm2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;색상스키마(Snazzy), 테마설정(&lt;a href=&quot;https://github.com/romkatv/powerlevel10k#pure-compatibility&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://github.com/romkatv/powerlevel10k#pure-compatibility&lt;/span&gt;&lt;/a&gt;, pure테마),&lt;br&gt;username, vscode 터미널에 iterm2 적용, 폰트 적용 등등은 구글링 ㄱㄱ&lt;br&gt;&lt;a href=&quot;https://velog.io/@zlemzlem5656/Mac-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%B8%ED%8C%85-2-%ED%84%B0%EB%AF%B8%EB%84%90-%EC%BB%A4%EC%8A%A4%ED%85%80&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;iterm2 설정법 추천블로그&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;vscode &lt;code&gt;code .&lt;/code&gt;명령어&lt;br&gt;vscode - &lt;kbd&gt;⇧&lt;/kbd&gt; &lt;kbd&gt;⌘&lt;/kbd&gt; &lt;kbd&gt;P&lt;/kbd&gt; - Shell command 입력 - &lt;code&gt;Shell Command: Install 'code' command in PATH&lt;/code&gt; - Terminal 재실행&lt;/li&gt; 
&lt;/ul&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot;&gt;
&lt;h1&gt;Git&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
  기본 내장 git은 버전이 오래됐다. 
&lt;/blockquote&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;// 오버라이드 옵션
// command line tool 최소 버전 충족 못하면 설치하라뜸
$ brew install -s git

// Options에서 pcre에 체크표시 되어있는지 확인
// 없으면 설치 &quot;brew install pcre&quot;
$ brew info git

// git version 2.24.3 (Apple Git-128) 아까 확인한 옛날 버전이 뜬다면
$ git --version

// &quot;/usr/bin/git&quot; -&amp;gt; 내장 버전
$ which git
$ export PATH=/usr/local/bin:$PATH

// &quot;/usr/local/bin/git&quot; -&amp;gt; 새로 설치한 버전
$ which git

//git version 2.23.0
$ git --version&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;verilog&quot;&gt;&lt;code&gt;// 계정정보 설정
$ git config --global user.name &quot;your name&quot;
$ git config --global user.email &quot;your email&quot;

// 한글 파일명 처리를 위한 설정
$ git config --global core.precomposeunicode true
$ git config --global core.quotepath false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Git SSH &amp;amp; GPG 설정&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
 &lt;li&gt;&lt;a href=&quot;https://www.lainyzine.com/ko/article/creating-ssh-key-for-github/&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;SSH 개념 &amp;amp; 설치&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
 &lt;li&gt;&lt;a href=&quot;https://www.44bits.io/ko/post/add-signing-key-to-git-commit-by-gpg#gpggnu-privacy-guard란&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;GPG 개념&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
 &lt;li&gt;&lt;a href=&quot;https://director-joe.kr/74&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;GPG full-generate 설치&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DerivedData 폴더 지우는 단축키 만들기&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Automator - Quick Action 선택해서 하기&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
 &lt;li&gt;&lt;a href=&quot;https://eunjin3786.tistory.com/220&quot; target=&quot;_self&quot;&gt;&lt;span&gt;[Automator] DerivedData 폴더 지우는 단축어 만들기&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>ETC</category>
      <author>snowe</author>
      <guid isPermaLink="true">https://snowee.tistory.com/58</guid>
      <comments>https://snowee.tistory.com/58#entry58comment</comments>
      <pubDate>Mon, 10 Jan 2022 23:09:29 +0900</pubDate>
    </item>
    <item>
      <title>Git) rebase-merge, squash-merge</title>
      <link>https://snowee.tistory.com/57</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 merge의 종류들 중 rebase&amp;amp;merge와 squash&amp;amp;merge에 대해 알아보려고 합니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PR을 올리고 해당 PR을 merge를 할 수 있는 방식에는 총 3가지가 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/42789819/144440317-786b77a7-8f2f-4272-b069-c0ae1d704085.png&quot; width=&quot;608&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;merge&lt;/li&gt;
&lt;li&gt;Squash and merge&lt;/li&gt;
&lt;li&gt;Rebase and merge&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;merge&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 가장 일반적인 merge입니다. 아무런 설정도 하지 않았을 때 default 값이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NHN Cloud meetup자료에 잘 설명된 그림이 있어서 가져와 보았습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://image.toast.com/aaaadh/real/2017/techblog/Screen%20Shot%2020170529%20at%2012.15.48%20PM.png&quot; width=&quot;608&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 merge를 하게 될 경우에는 브랜치에서 커밋된 &lt;b&gt;a-&amp;gt;b-&amp;gt;c&lt;/b&gt; 친구들을 참조하는 새로운 커밋 노드인 &lt;b&gt;m&lt;/b&gt;이 생성됩니다. 이 &lt;b&gt;m&lt;/b&gt;이라는 커밋 노드는 parent로 &lt;b&gt;init&lt;/b&gt;과 &lt;b&gt;c&lt;/b&gt;를 가지게 됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Squash and Merge&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 Squash and Merge입니다. 음..Squash라는 단어는 &quot;밀치고 들어가다, 헤치고 들어가다&quot; 라는 뜻을 지니고 있나봐요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 동작하는 방식이 그런 단어의 의미를 잘 살리고 있는지는 모르겠지만 그림을 보면..&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://image.toast.com/aaaadh/real/2017/techblog/Screen%20Shot%2020170529%20at%2012.15.51%20PM.png&quot; width=&quot;608&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브랜치에서 커밋된 &lt;b&gt;a-&amp;gt;b-&amp;gt;c&lt;/b&gt; 친구들을 한데 모아 놓은 커밋이 새롭게 생성되며 머지가 됩니다. 이 때 이 커밋은 parent로 &lt;b&gt;init&lt;/b&gt;만을 가지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;squash는 처음 공부하는거니까 실습을 해보았어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 만약, 아래와 같이 커밋된 3개의 기록이 Squash and Merge를 통해 merge해 줄 경우&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/42789819/144443414-26ef8f3b-fe72-4367-bbad-fa973276b9b7.png&quot; alt=&quot;image&quot; width=&quot;608&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 하나의 커밋내에 3개의 커밋이 함께 기록됩니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/42789819/144443699-89aa0854-0074-4dd8-b4aa-220da50e18c5.png&quot; alt=&quot;image&quot; width=&quot;608&quot; /&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Rebase and Merge&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rebase가 그나마 익숙하죠..? 이전 글에서도 알아보았듯 rebase 는 브랜치의 시작점을 똑 떼어서 붙이는 느낌입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://image.toast.com/aaaadh/real/2017/techblog/Screen%20Shot%2020170529%20at%2012.15.55%20PM.png&quot; width=&quot;608&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브랜치에서 커밋된 &lt;b&gt;a-&amp;gt;b-&amp;gt;c&lt;/b&gt; 친구들을 그대로 옮기는 방식입니다. 그렇기 때문에 새로운 커밋이 추가되지않으며 브랜치가 없었다는 듯 linear한 history를 보실 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;그럼 각 merge를 언제 사용?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;저는 보통 git flow를 따르면서 협업을 하는데 그럴경우 &lt;b&gt;&lt;code&gt;master&lt;/code&gt;, &lt;code&gt;develop&lt;/code&gt;, &lt;code&gt;feat&lt;/code&gt;, &lt;code&gt;hotfix&lt;/code&gt;&lt;/b&gt; 브랜치를 파게 될 거에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;code&gt;&lt;b&gt;master&lt;/b&gt;&lt;/code&gt;는 정말 최최최종, &lt;b&gt;&lt;code&gt;develop&lt;/code&gt;&lt;/b&gt;은 PR을 approved받고 merge한 경우, &lt;b&gt;&lt;code&gt;feat&lt;/code&gt;&lt;/b&gt;은 실제로 개발할 때 직접적인 커밋을 위해 사용하는 브랜치, &lt;b&gt;&lt;code&gt;hotfix&lt;/code&gt;&lt;/b&gt;는 버그 잡는 용이겠죠?&amp;nbsp;이렇게 나뉘어져 있다고 가정했을 때,&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;code&gt;develop&lt;/code&gt;&lt;/b&gt; - &lt;b&gt;&lt;code&gt;feat&lt;/code&gt;&lt;/b&gt; 브랜치 간 merge
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용: Squash and Merge&lt;/li&gt;
&lt;li&gt;이유: feat 브랜치는 말그대로 실시간 개발용 브랜치이기 때문에 커밋 히스토리가 복잡한 경향이 있어요. 때문에 그러한 복잡한 커밋들을 모두 묶어 하나의 새로운 커밋으로 남기고, 그 하나만을 develop 브랜치에서 관리하는 편이 더 수월하답니다. 또한, Rebase를 사용하게 되면 어느 지점부터 새로운 브랜치의 작업인지 구분이 사라지기 때문에 피하게 되는 것 같아요.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;master&lt;/code&gt;&lt;/b&gt; - &lt;b&gt;&lt;code&gt;develop&lt;/code&gt;&lt;/b&gt; 브랜치 간 merge
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용: Rebase and Merge&lt;/li&gt;
&lt;li&gt;이유: develop의 내용을 master에 추가한다는 건 무언가 새롭게 버전업을 하기 위한 경우가 대부분이기때문에 굳이 일반 merge로 불필요한 새로운 커밋을 만들 필요가 없으며 squash로 merge된 feat의 기록들을 또 한번 squash해 줄 필요가 없을거에요.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;hotfix&lt;/code&gt;&lt;/b&gt; - &lt;b&gt;&lt;code&gt;develop&lt;/code&gt;/&lt;code&gt;master&lt;/code&gt;&lt;/b&gt; 브랜치 간 merge
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용: Merge / Squash and Merge&lt;/li&gt;
&lt;li&gt;이유: Squash를 통해 머지했다는 커밋으로 흔적을 남기는게 좋을 것 같습니다!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rebase, squash는 항상 꼭 merge랑 사용해야하는 것은 아닙니다! rebase의 심리스한 특성과 squash의 모아서 보여주는 특성은 독단적으로 사용할 때도 유효하기 때문에 이 특성을 활용하여 history를 정리할 때에도 사용된답니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 오늘은 merge의 방식에 대해 알아보았습니다. 감사합니다 :)&lt;/p&gt;</description>
      <category>Git</category>
      <author>snowe</author>
      <guid isPermaLink="true">https://snowee.tistory.com/57</guid>
      <comments>https://snowee.tistory.com/57#entry57comment</comments>
      <pubDate>Tue, 28 Dec 2021 22:35:08 +0900</pubDate>
    </item>
    <item>
      <title>iOS) URLSession + NSCache 실습(2) - NSCache</title>
      <link>https://snowee.tistory.com/56</link>
      <description>&lt;p&gt;저번 글에 이어서 오늘은 자주 사용하는 이미지 캐싱 라이브러리인 Kingfisher의 Base인 &lt;code&gt;NSCache&lt;/code&gt;에 대해서 알아보도록 할게요!  &lt;/p&gt;
&lt;h1&gt;Caching을 하는 이유&lt;/h1&gt;
&lt;p&gt;예를들어, 서버에서 어떤 API를 호출해서 이미지를 받아온다고 한다면 우리는 그 이미지 처리를 위해 비동기 코드를 작성하곤 합니다. 가장 주된 이유는 이미지를 받아오는데에 걸리는 시간이 이미지 크기에 따라 엄청 오래걸릴 수 있기 때문이죠! &lt;/p&gt;
&lt;p&gt;같은 이유로 사용자가 방금 불러온 이미지인데 잠깐 나갔다가 들어왔을 때 다시 API를 호출하고 이미지를 불러올 때 같은 시간이 소요된다면 비효율적이겠죠? 이미지의 크기가 크다면 클수록 더더욱 그럴 것 같아요.&lt;/p&gt;
&lt;p&gt;그래서 우리는 기기 내부에 존재하는 임시 저장소인 &lt;code&gt;Cache&lt;/code&gt;에 이미 불러온 이미지를 넣어두고 빠르게 꺼내 쓸 수 있게 하는거에요. 그리고 이러한 행위 자체를 &lt;code&gt;Caching&lt;/code&gt;이라고 합니다!&lt;/p&gt;
&lt;h1&gt;Cache의 종류&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;Cache&lt;/code&gt;는 두 종류인데, &lt;code&gt;Memory Cache&lt;/code&gt;, &lt;code&gt;Disk Cache&lt;/code&gt;로 나뉩니다.&lt;/p&gt;
&lt;h2&gt;Memory Cache&lt;/h2&gt;
&lt;p&gt;기기를 끄면 사라지는 &lt;code&gt;Cache&lt;/code&gt;에요. 오늘 우리가 알아볼 &lt;code&gt;NSCache&lt;/code&gt;를 통해서 구현할 수 있습니다!&lt;/p&gt;
&lt;h2&gt;Disk Cache&lt;/h2&gt;
&lt;p&gt;메모리 캐시와 다르게 기기를 꺼도 남아있어요.&lt;br&gt;두 가지 방법으로 구현할 수 있는데, UserDefault를 사용한다면 앱을 삭제했을 때 같이 사라질 것이고, 파일 경로에 이미지를 저장한다면 앱이 삭제되더라도 캐시가 남아있게 됩니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;필수는 아니지만 보통은 파일 경로에 저장하는 것 같습니다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Cache 구현&lt;/h1&gt;
&lt;h2&gt;Caching 순서도&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;API&lt;/code&gt;를 호출해서 &lt;code&gt;이미지 url&lt;/code&gt;을 받아온다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;url&lt;/code&gt;을 key값으로 &lt;code&gt;memory cache&lt;/code&gt;에 저장된 데이터가 있는지 확인한다.&lt;/li&gt;
&lt;li&gt;있으면 쓰고 없다면 &lt;code&gt;disk cache&lt;/code&gt;에서 확인해본다.&lt;/li&gt;
&lt;li&gt;거기도 없다면 &lt;code&gt;url&lt;/code&gt;로 이미지를 가져온 뒤 &lt;code&gt;memory cache&lt;/code&gt;와 &lt;code&gt;disk cache&lt;/code&gt;에 각각 저장해준다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Memory Cache 구현&lt;/h2&gt;
&lt;p&gt;아까 memory cache는 &lt;code&gt;NSCache&lt;/code&gt;를 통해 구현한다고 했죠?! 저는 &lt;code&gt;NSCache&lt;/code&gt;를 아래와 같이 싱글톤 객체로 구현해주었어요!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;class ImageCacheManager {
    // 키값으로 쓸타입과 캐시에 넣을 타입을 정해주면된다.
    // 내가 사용할 key값: Image URL String
    static let shared = NSCache&amp;lt;NSString, UIImage&amp;gt;()
    private init() {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;막간을 이용한 한가지 꿀팁&lt;/strong&gt;&lt;br&gt;&lt;a href=&quot;https://developer.apple.com/documentation/foundation/nsstring/&quot;&gt;&lt;code&gt;NSString&lt;/code&gt;&lt;/a&gt;에서 &lt;code&gt;lastPathComponent&lt;/code&gt; 속성을 사용하면 url에서 파일명만 추출할 수 있다. &lt;/p&gt;
&lt;p&gt;예를들어, &lt;a href=&quot;http://www.test.com/image/test.png&quot;&gt;www.test.com/image/test.png&lt;/a&gt; 라면 &lt;code&gt;test.jpg&lt;/code&gt;만 뽑아오는 것&lt;/p&gt;
&lt;p&gt;그래서 무튼 위에서 만든 NSCache 객체를 아래처럼 사용하여 캐싱하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;let cacheKey = NSString(string: url).lastPathComponent

// 다운로드된 이미지를 메모리 캐시에 저장
ImageCacheManager.shared.setObject(image, forKey: cacheKey as NSString)&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Disk Cache 구현&lt;/h2&gt;
&lt;p&gt;Disk Cache는 &lt;a href=&quot;https://developer.apple.com/documentation/foundation/filemanager&quot;&gt;&lt;code&gt;File Manager&lt;/code&gt;&lt;/a&gt;를 이용해서 구현할 수 있어요.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;File Manager&lt;/code&gt;는 파일 시스템과 상호작용하는 주요 수단으로써 파일 시스템의 내용에 대한 편리한 인터페이스라고 애플 독스가 설명해주고있어요&lt;/p&gt;
&lt;p&gt;암튼, 이걸로 구현을 할건데 먼저 &lt;strong&gt;저장될 디렉토리를 찾기 위해서&lt;/strong&gt; &lt;code&gt;NSSearchPathForDirectoriesInDomains&lt;/code&gt;를 사용해야해요  &lt;code&gt;NSSearchPathForDirectoriesInDomains&lt;/code&gt;는 디렉토리 검색 경로 목록을 생성해줘요.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;NSSearchPathForDirectoriesInDomains(directory: FileManager.SerachPathDirectory, domainMask: FileManager.SearchPathDomainMask, expandTilde: Bool)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 첫번째 파라미터인 &lt;code&gt;directory&lt;/code&gt;에 들어가는 기본 제공 path는 이만큼이 있어요.&lt;/p&gt;
&lt;p align=&quot;center&quot;&gt;&lt;img width=50% src=&quot;https://user-images.githubusercontent.com/42789819/134193752-1e4b821f-6a76-4a4b-892f-9ef8fe9c67d3.png&quot;&gt;&lt;/p&gt;

&lt;p&gt;그리고 &lt;code&gt;expandTilde&lt;/code&gt;를 true로 해주면 해당 경로 뒤에 문자열을 붙여서 새로운 경로를 만들 수 있어요!&lt;/p&gt;
&lt;p&gt;이정도로 하고 우리가 사용할 path를 아래처럼 구성할 수 있겠네요.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;let cacheKey = NSString(string: url).lastPathComponent

// 1. 경로로 cache폴더를 지정
guard let path = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first else { return }

// 2. 1에서 지정한 cache경로 뒤에 key값으로 폴더 생성
let filePath = URL(fileURLWithPath: path).appendingPathComponent(cacheKey)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/42789819/134198442-3c6a4f3b-65d7-40a7-b4cf-16110581aff9.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;위에가 &lt;code&gt;path&lt;/code&gt;, 아래가 &lt;code&gt;filePath&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;

&lt;p&gt;그럼 이제 &lt;code&gt;FileManager&lt;/code&gt; 객체를 만들고 그 안에 우리가 찾는 이미지가 저장되어있는지 체크해주면 끝!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;let fileManager = FileManager()

// 이 이름으로 캐싱된 데이터 있니?
if fileManager.fileExists(atPath: filePath.path) {
    guard let imageData = try? Data(contentsOf: filePath) else {
        print(&amp;quot;disk cache image data nil&amp;quot;)
        return
    }

    guard let cachedImage = UIImage(data: imageData) else {
        print(&amp;quot;disk cache image data nil&amp;quot;)
        return
    }

    DispatchQueue.main.sync {
        print(&amp;quot;CACHE&amp;gt;&amp;gt; Load Disk cache&amp;quot;)
        // disk cache에서 썼으면 다음을 위해 memory cache에도 저장해주기
        ImageCacheManager.shared.setObject(cachedImage, forKey: cacheKey as NSString)
        self.image = cachedImage
    }
    return
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;전체 코드 및 설명&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;메모리 캐시와 디스크 캐시 처리를 따로 빼서 코드를 간소화 할 수도 있겠지만 일단은 설명을 위해 쭉 순서대로 작성해보았습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;import UIKit

extension UIImageView { 
    func setImageUrl(_ url: String) {

        // 캐시에 사용될 Key 값
        let cacheKey = NSString(string: url).lastPathComponent

        // MARK: Memory Cache
        // 1. `url`을 key값으로 `memory cache`에 저장된 데이터가 있는지 확인한다.
        if let cachedImage = ImageCacheManager.shared.object(forKey: cacheKey as NSString) {
            DispatchQueue.main.sync {
                print(&amp;quot;CACHE&amp;gt;&amp;gt; Load Memory cache&amp;quot;)
                // 불러오려는 이미지가 이미 메모리 캐시에 저장되어 있으면 캐싱된 이미지를 사용
                self.image = cachedImage
            }
            return
        }

        // 메모리 캐시에 원하는 이미지가 없다면

        // MARK: Disk Cache
        // 2. disk cache(UserDefault 혹은 기기Directory에있는 file)에서 확인
        guard let path = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first else { return }

        let filePath = URL(fileURLWithPath: path).appendingPathComponent(cacheKey)
        let fileManager = FileManager()

        if fileManager.fileExists(atPath: filePath.path) {
            guard let imageData = try? Data(contentsOf: filePath) else {
                print(&amp;quot;disk cache image data nil&amp;quot;)
                return
            }

            guard let cachedImage = UIImage(data: imageData) else {
                print(&amp;quot;disk cache image data nil&amp;quot;)
                return
            }

            DispatchQueue.main.sync {
                print(&amp;quot;CACHE&amp;gt;&amp;gt; Load Disk cache&amp;quot;)
                // 있다면 다음에 더 빨리 불러오기 위해 memory cache에 추가해주고 사용한다.
                ImageCacheManager.shared.setObject(cachedImage, forKey: cacheKey as NSString)
                self.image = cachedImage
            }
            return
        }

        // 이마저도 없다면
        // 3. 서버통신을 통해서 받은 URL로 이미지를 가져온다.
        DispatchQueue.global(qos: .background).async {
            if let url = URL(string: url) {
                URLSession.shared.dataTask(with: url) { (data, res, err) in
                    if let _ = err {
                        DispatchQueue.main.async {
                            self.image = UIImage()
                        }
                        return
                    }
                    DispatchQueue.main.async {
                        if let data = data, let image = UIImage(data: data) {
                            print(&amp;quot;CACHE&amp;gt;&amp;gt; No cache I&amp;#39;ll make new&amp;quot;)
                            // URL로 이미지 불러와서 사용
                            self.image = image

                            // 다음을 위해 caching 해주기
                            // 다운로드된 이미지를 메모리 캐시에 저장
                            ImageCacheManager.shared.setObject(image, forKey: cacheKey as NSString)
                            // 다운로드된 이미지를 디스크 캐시에 저장
                            fileManager.createFile(atPath: filePath.path, contents: image.jpegData(compressionQuality: 0.4), attributes: nil)
                        }
                    }
                }.resume()
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;마치며, 주의점&lt;/h1&gt;
&lt;p&gt;순수하게 구현하려니까 직접 구현해야하는 부분이 많긴해도 재밌는 것 같아요. &lt;/p&gt;
&lt;p&gt;그리고 사실 이게 전부는 아니에요..ㅎㅎ &lt;strong&gt;만약 서버에서 이미지를 교체했지만 이미지 파일명은 그대로일 경우&lt;/strong&gt; 현재 방식대로면 파일명으로 캐시 데이터를 찾으니까 이전 이미지를 계속해서 불러오겠죠? 이러면 안되는데 말이에요  &lt;/p&gt;
&lt;p&gt;이걸 방지하기 위해서 &lt;code&gt;ETag&lt;/code&gt;라는 리소스 식별자를 사용한다고 합니다. 사용을 위해서는 서버에도 이를 처리하기 위한 개발이 되어있어야 해요.&lt;br&gt;&lt;code&gt;header&lt;/code&gt;부에 &lt;code&gt;ETag&lt;/code&gt;값을 넣고 변경/미변경의 응답코드를 다르게 줌으로서 리소스 변경이 있는지 없는지를 확인할 수 있는 방법이어서 해당 문제를 해결할 수 있다고 하네요! &lt;/p&gt;
&lt;p&gt;다음엔 이것도 공부해볼게요. 감사합니다 :)&lt;/p&gt;</description>
      <category>iOS</category>
      <author>snowe</author>
      <guid isPermaLink="true">https://snowee.tistory.com/56</guid>
      <comments>https://snowee.tistory.com/56#entry56comment</comments>
      <pubDate>Wed, 15 Sep 2021 13:57:36 +0900</pubDate>
    </item>
    <item>
      <title>iOS) URLSession + NSCache 실습(1) - URLSession</title>
      <link>https://snowee.tistory.com/55</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Kingfisher,&amp;nbsp;Alamofire,&amp;nbsp;Moya&amp;nbsp;많이들&amp;nbsp;사용하시나요?&amp;nbsp;저도&amp;nbsp;사용하고&amp;nbsp;있습니다만..&lt;br /&gt;얘네를&amp;nbsp;사용하는&amp;nbsp;이유가&amp;nbsp;URLSession,&amp;nbsp;NSCache등을&amp;nbsp;사용할&amp;nbsp;때&amp;nbsp;번거로움이&amp;nbsp;많아서잖아요?&amp;nbsp;근데&amp;nbsp;제가&amp;nbsp;그&amp;nbsp;번거로움을&amp;nbsp;못&amp;nbsp;느껴본&amp;nbsp;것&amp;nbsp;같아서&amp;nbsp;이참에&amp;nbsp;직접&amp;nbsp;구현하며&amp;nbsp;경험해보고&amp;nbsp;정말&amp;nbsp;어떤&amp;nbsp;점이&amp;nbsp;좋은&amp;nbsp;건지&amp;nbsp;공부해보려고&amp;nbsp;해요.&lt;/p&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;b&gt;URLSession을 먼저 알아볼게요&lt;/b&gt;&lt;/h3&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;URLSession?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/HTTPS를 통해 콘텐츠 및 데이터를 주고받기 위해 애플에서 만든 서버 통신 API 클래스.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/HTTPS외에도 인증, 쿠키 관리, 캐시 등도 지원한대요(사실 메서드 되게 많으니까 definition 참고하는게 더 좋을듯).&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;URLSession&lt;/b&gt;은 기본적으로 비동기로 구현이 되어있어요. 그래서 구현 할 때 별도의 비동기 처리는 필요하지 않습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 통신시 일어나는 UI처리가 있을 수 있겠죠?! UI 관련 처리는 모두 &lt;b&gt;Main Thread&lt;/b&gt;에서 이루어져야하기 때문에 DispatchQueue.main.sync 내부에서 처리해야 됨을 유의해야합니다!(사실 어차피 Xcode가 보라색 알림문구로 알려주긴함)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;URLSession을 사용하는 순서?&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;URLSessionConfiguration&lt;/b&gt;을 설정하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Session&lt;/b&gt;을 생성합니다.&lt;/li&gt;
&lt;li&gt;통신할 &lt;b&gt;URL과 Request 객체&lt;/b&gt;를 준비합니다.&lt;/li&gt;
&lt;li&gt;사용할&amp;nbsp;&lt;b&gt;Task&lt;/b&gt;를&amp;nbsp;결정하고,&amp;nbsp;&lt;b&gt;Completion Handler /&lt;/b&gt;&amp;nbsp;&lt;b&gt;Delegate 메소드&lt;/b&gt;를&amp;nbsp;작성합니다.&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;간단한 통신:&amp;nbsp;&lt;/span&gt;&lt;span&gt;Completion Handler ,&amp;nbsp;&lt;/span&gt;&lt;span&gt;Background 상태일때에도 동작:&amp;nbsp;&lt;/span&gt;&lt;span&gt;Delegate&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;패턴을 주로 사용&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;b&gt;Task&lt;/b&gt;&lt;/b&gt;를 실행합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Task&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;완료 후&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Completion Handler&lt;/b&gt;를&amp;nbsp;실행합니다.&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&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;/p&gt;
&lt;pre id=&quot;code_1631631239915&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Foundation

struct TestService {
    static let shared = CAFETIService()
    
    // MARK: - baseURL + path
    let baseURL = &quot;http://유.알.엘/&quot;
    var urlString: String { get { baseURL + &quot;패.쓰&quot; } }
    let token = &quot;토.큰&quot;
    
    func fetchTestData(answers: [Int], completion: @escaping (NetworkResult&amp;lt;모델&amp;gt;) -&amp;gt; ()) {
        if let url = URL(string: urlString) {
            // MARK: 1.URLSessionConfiguration을 설정하고, Session을 생성합니다.
            let session = URLSession(configuration: .default)
            
            // MARK: 2.통신할 URL과 Request 객체를 준비합니다.
            var requestURL = URLRequest(url: url)
            requestURL.setValue(&quot;Application/json&quot;, forHTTPHeaderField: &quot;Content-Type&quot;)
            requestURL.setValue(token, forHTTPHeaderField: &quot;token&quot;)
            requestURL.httpMethod = &quot;POST&quot;
            
            let body = [&quot;answers&quot;: answers]
            guard let bodyData = try? JSONSerialization.data(withJSONObject: body, options: []) else { return }
            requestURL.httpBody = bodyData
            
            // MARK: 3.사용할 Task 를 결정하고, 그에 맞는 Completion Handler, 혹은 Delegate 메소드를 작성합니다.
            let dataTask = session.dataTask(with: requestURL) { (data, response, error) in
                if error != nil {
                    print(error!)
                    return
                }
                
                if let bindingData = data {
                    do {
                        let decodedData = try JSONDecoder().decode(모델.self, from: bindingData)
                        // MARK: 5.Task 완료 후 Completion Handler를 실행합니다.
                        completion(.success(decodedData))
                    } catch {
                        print(error.localizedDescription)
                    }
                }
            }
            // MARK: 4.Task를 실행합니다.
            dataTask.resume()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;순서는 이렇고 이제 1번부터 생소한 용어를 짚어가면서 살펴볼게요!&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1.&lt;/b&gt; &lt;b&gt;URLSessionConfiguration&lt;/b&gt;을 설정하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Session&lt;/b&gt;을 생성한다.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는&amp;nbsp;&lt;b&gt;URL과&lt;/b&gt;&amp;nbsp;&lt;b&gt;Request객체&lt;/b&gt;를 가지고 통신을 할거에요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 통신을 위해서는 &lt;b&gt;URLSession 세션 객체&lt;/b&gt;를 생성해야합니다. 이는 &lt;b&gt;URLSessionConfiguration&lt;/b&gt;을 통해 생성할 수 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URLSessionConfiguration이 제공하는 &lt;b&gt;세션 객체의 유형&lt;/b&gt;은 아래의&lt;b&gt; &quot;3가지&quot;&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;이 한 가지 있어요. URLSession의 definition에 보면 configuration이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;a href=&quot;https://developer.apple.com/documentation/foundation/nscopying/&quot;&gt;@NSCopying&lt;/a&gt;&lt;/b&gt;을 준수하고 있는 모습을 볼 수 있는데, 이는 기능적으로 자신의 복사본을 통해 동작하는 느낌인것 같더라구요?! 그래서 한번 선언된 Session 객체를 추후에다른 유형으로 변경하고자 할 때에는 새롭게 객체를 만들어주어야한다고 합니다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;946&quot; data-origin-height=&quot;70&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMI60Z/btreY3QRkYa/hFduzjspPx7hPP9uwj6QIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMI60Z/btreY3QRkYa/hFduzjspPx7hPP9uwj6QIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMI60Z/btreY3QRkYa/hFduzjspPx7hPP9uwj6QIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMI60Z%2FbtreY3QRkYa%2FhFduzjspPx7hPP9uwj6QIk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;511&quot; height=&quot;70&quot; data-origin-width=&quot;946&quot; data-origin-height=&quot;70&quot;/&gt;&lt;/span&gt;&lt;/figure&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;i&gt;Default Session(기본)&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #000000;&quot;&gt;: 디스크가 기반&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;캐싱, 자격 증명 및 쿠키&lt;span&gt;&amp;nbsp;&lt;/span&gt;저장소 개체를 사용하는&lt;/b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;세션&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: delegate 지정이 가능하다&lt;/p&gt;
&lt;pre id=&quot;code_1631627634909&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;URLSessionConfiguration(.default)&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;i&gt;&lt;span style=&quot;background-color: #fcfcfc;&quot;&gt;Ephemeral Session (임시/하루살이 세션)&lt;/span&gt;&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000; background-color: #fcfcfc;&quot;&gt;: Default Session에서 설명한&amp;nbsp;저장소 개체를 사용하지 않는&amp;nbsp;세션, Private하다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1631627708498&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;URLSessionConfiguration(.ephemeral)&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;i&gt;Background Session(백그라운드)&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 백그라운드에서도 통신을 수행할 수 있는 세션&lt;/p&gt;
&lt;pre id=&quot;code_1631627723181&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;URLSessionConfiguration(.background)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2.&lt;/b&gt; 통신할&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;URL&lt;/b&gt;과&lt;b&gt; Request 객체&lt;/b&gt;를 준비합니다.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 통신을 할 때 필요한 baseURL+path, 그리고 통신을 위해 넣어주어야 할 Request 객체를 준비해주어야겠죠?&lt;/p&gt;
&lt;pre id=&quot;code_1631629762596&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;..code..

// 통신을 주고 받을 URL
let baseURL = &quot;http://~~~~/&quot;
var urlString: String { get { baseURL + &quot;PATH자리&quot; } }

if let url = URL(string: urlString) {
      // MARK: 1.URLSessionConfiguration을 설정하고, Session을 생성합니다.
      let session = URLSession(configuration: .default)
		
      // MARK: 2.통신할 URL과 Request 객체를 준비합니다.
      // Request 객체 생성
      var requestURL = URLRequest(url: url)
      
      // Request Header 구성
      requestURL.setValue(&quot;Application/json&quot;, forHTTPHeaderField: &quot;Content-Type&quot;)
      requestURL.setValue(token, forHTTPHeaderField: &quot;token&quot;)
      
      // HTTP Method는 기본이 &quot;GET&quot;, 그 외에는 아래처럼 별도 설정 필요
      requestURL.httpMethod = &quot;POST&quot;

      // Request Body
      let body = [&quot;answers&quot;: answers]
      guard let bodyData = try? JSONSerialization.data(withJSONObject: body, options: []) else { return }
      
      // Request 객체에 할당
      requestURL.httpBody = bodyData
      
      // ...
      // MARK: 3.사용할 Task 를 결정하고, 그에 맞는 Completion Handler, 혹은 Delegate 메소드를 작성합니다. 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3.&lt;/b&gt; 사용할&amp;nbsp;&lt;b&gt;Task&lt;/b&gt;를&amp;nbsp;결정하고,&amp;nbsp;&lt;b&gt;Completion Handler /&lt;/b&gt;&amp;nbsp;&lt;b&gt;Delegate 메소드&lt;/b&gt;를&amp;nbsp;작성합니다.&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;간단한 통신:&amp;nbsp;Completion Handler ,&amp;nbsp;Background 상태일때에도 동작:&amp;nbsp;Delegate&amp;nbsp;패턴을 주로 사용&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Task&lt;/b&gt;를 실행합니다.&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Task&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;완료 후&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Completion Handler를 실행&lt;/b&gt;합니다.&lt;/h2&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;Task객체란? &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Session객체를 통해 서버로 요청을 보내고 난 뒤, 응답을 받을 때 필요한 객체. 3가지 유형이 있다.&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;i&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;URLSessionDataTask&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666;&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;Data객체를 통해&amp;nbsp;&lt;/span&gt;데이터를 주고받은 Task 수행&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #000000;&quot;&gt;&lt;i&gt;&lt;b&gt;&lt;span&gt;URLSessionUploadTask&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;: Data를 파일로 변환 후 업로드하는 Task 수행&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #000000;&quot;&gt;&lt;span&gt;&lt;i&gt;&lt;b&gt;URLSessionDownloadTask&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;:&lt;span&gt;&amp;nbsp;Data&lt;/span&gt;&lt;span&gt;를 파일로 변환 후 다운로드하는 Task 수행&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #000000;&quot;&gt;&lt;span&gt;&lt;span&gt;: 백그라운드 다운로드도 지원하며 일시정지 또한 가능하다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1631630727780&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// MARK: 3.사용할 Task를 결정합니다.고, 그에 맞는 Completion Handler를 작성합니다.
let dataTask = session.dataTask(with: requestURL) { (data, response, error) in
    if error != nil {
        print(error!)
        return
    }
    
    // 데이터 바인딩
    if let bindingData = data {
          do {
                // 통신을 성공하여 데이터를 받았다면 JSON 타입 데이터를 사용 가능하도록 디코딩
                let decodedData = try JSONDecoder().decode(CAFETI.self, from: bindingData)
                // MARK: 5.Task 완료 후 Completion Handler를 실행합니다.
                completion(.success(decodedData))
            } catch {
                print(error.localizedDescription)
        }
      }
    }
    
    // MARK: 4.Task를 실행합니다.
    dataTask.resume()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Final. &lt;/b&gt;서비스 메서드 호출&lt;/h2&gt;
&lt;pre id=&quot;code_1631632538289&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@objc
private func tappedLoad() {
    TestService.shared.fetchTestData(answers: 데이터 넣기) { [self] response in
        switch response {
        case .success(let data):
        case .requestErr(let err):
        case .pathErr:
        case .serverErr:
        case .networkFail:
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 URLSession에 대한 전반적인 부분들을 직접 사용하면서 익혀보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 오늘 호출한 API가 제가 만든 API인데 response로 이미지가 오는 API를 가져왔기 때문에 다음엔 이걸로 이미지 캐싱을 연습해볼게요!&amp;nbsp;&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;/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;&amp;nbsp;&lt;/p&gt;</description>
      <category>iOS</category>
      <author>snowe</author>
      <guid isPermaLink="true">https://snowee.tistory.com/55</guid>
      <comments>https://snowee.tistory.com/55#entry55comment</comments>
      <pubDate>Tue, 14 Sep 2021 23:21:56 +0900</pubDate>
    </item>
  </channel>
</rss>