<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>lsh424</title>
    <link>https://lsh424.tistory.com/</link>
    <description>iOS와 머신러닝에 관심이 많습니다 :)</description>
    <language>ko</language>
    <pubDate>Thu, 16 Apr 2026 21:28:26 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>lsh424</managingEditor>
    <image>
      <title>lsh424</title>
      <url>https://tistory1.daumcdn.net/tistory/3635623/attach/79596a8816414267ac78ac8599dc189b</url>
      <link>https://lsh424.tistory.com</link>
    </image>
    <item>
      <title>[Swift] Swift Testing (Xcode16)</title>
      <link>https://lsh424.tistory.com/101</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;해당 글은 &lt;a href=&quot;https://developer.apple.com/videos/play/wwdc2024/10179/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;WWDC24 Meet Swift Testing&lt;/a&gt;을 토대로 작성했습니다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Swift Testing이란?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swift Testing은 Swift로 작성된 코드를 테스트하기 위한 &lt;a href=&quot;https://github.com/apple/swift-testing&quot;&gt;&lt;span&gt;새로운 오픈 소스 패키지&lt;/span&gt;&lt;/a&gt;입니다. 기존 XCTest에 비해 테스트를 더 쉽게 작성하고 관리할 수 있도록 도와줍니다. (Xcode16부터 사용 가능하며, 템플릿 default 설정은 XCTest가 아닌 Swift Testing이 됩니다.)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Swift Testing의 구성 요소&lt;/b&gt;&lt;/h4&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;Swift Testing에서는 기존 테스트 함수가 &quot;test&quot;로 시작했던 것과 다르게 @Test 속성을 사용해 테스트 함수임을 명시적으로 표현합니다.&lt;/li&gt;
&lt;li&gt;전역 함수, 정적 함수, 인스턴스 메서드 등 다양한 형태의 함수를 지원합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Expectations&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 통과 &amp;amp; 실패 유무 확인은 기존 XCTAssert api를 사용하는 것이 아닌, #expect와 #require 두 가지 매크로를 사용합니다.&lt;/li&gt;
&lt;li&gt;일반적인 표현식과 연산자를 사용하여 조건을 검증하기 때문에 테스트 작성이 직관적입니다.&lt;/li&gt;
&lt;li&gt;#require 매크로의 경우 실패 시 테스트를 중단시킵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Traits&lt;/h4&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;조건부 실행, 테스트 비활성화, 버그 참조 등 다양한 trait을 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Suites&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트를 포함하는 타입으로 테스트 함수나 다른 스위트를 구조화한 것을 Suites라 합니다. (즉, 테스트 케이스 그룹화)&lt;/li&gt;
&lt;li&gt;구조체, 액터, 클래스등을 스위트로 사용 가능합니다.&lt;/li&gt;
&lt;li&gt;@Suite 속성을 사용해 스위트임을 명시적으로 표현할 수 있습니다.&lt;/li&gt;
&lt;li&gt;프로퍼티를 가질 수 있고, 테스트 전후 로직 수행을 위해 init, deinit 사용이 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Swift Testing의 장점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;병렬 실행: Swift 동시성을 사용하여 테스트 병렬 실행이 가능하고 그에 따라 더 빠른 테스트 결과 도출이 가능합니다.&lt;/li&gt;
&lt;li&gt;매개변수화된 테스트: 여러 인수로 테스트 반복 실행이 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;사용 예 (Building blocks)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드들은 &lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://developer.apple.com/videos/play/wwdc2024/10179/&quot;&gt;WWDC24 Meet Swift Testing&lt;/a&gt; 영상에서 예시로 나오는 코드들로, DestinationVideo 모듈에서 비디오 파일의 메타데이터가 기대하는 메타데이터와 동일한지 테스트 하는 상황을 가정합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722174251149&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Testing
@testable import DestinationVideo

// @Test 속성을 사용해 테스트 함수임을 명시적으로 표현
@Test func videoMetadata() {
    let video = Video(fileName: &quot;By the Lake.mov&quot;)
    let expectedMetadata = Metadata(duration: .seconds(90))
    // 테스트 결과(기댓값) 확인 
    #expect(video.metadata == expectedMetadata)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-28 오후 10.52.39.png&quot; data-origin-width=&quot;1766&quot; data-origin-height=&quot;214&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cg9xAt/btsIRlsyWIV/zr8GYKxm7QuMbHW7FZseMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cg9xAt/btsIRlsyWIV/zr8GYKxm7QuMbHW7FZseMK/img.png&quot; data-alt=&quot;테스트 실행 결과 fail시, 왜 fail 인지 자세하게 확인 가능 (Show 버튼)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cg9xAt/btsIRlsyWIV/zr8GYKxm7QuMbHW7FZseMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcg9xAt%2FbtsIRlsyWIV%2Fzr8GYKxm7QuMbHW7FZseMK%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;1766&quot; height=&quot;214&quot; data-filename=&quot;스크린샷 2024-07-28 오후 10.52.39.png&quot; data-origin-width=&quot;1766&quot; data-origin-height=&quot;214&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;테스트 실행 결과 fail시, 왜 fail 인지 자세하게 확인 가능 (Show 버튼)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-28 오후 10.54.07.png&quot; data-origin-width=&quot;1752&quot; data-origin-height=&quot;328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GhMEU/btsIQJm26IB/CQ3UKgKwIm1vCEBMmTO6NK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GhMEU/btsIQJm26IB/CQ3UKgKwIm1vCEBMmTO6NK/img.png&quot; data-alt=&quot;Show 버튼 눌렀을 때&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GhMEU/btsIQJm26IB/CQ3UKgKwIm1vCEBMmTO6NK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGhMEU%2FbtsIQJm26IB%2FCQ3UKgKwIm1vCEBMmTO6NK%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;1752&quot; height=&quot;328&quot; data-filename=&quot;스크린샷 2024-07-28 오후 10.54.07.png&quot; data-origin-width=&quot;1752&quot; data-origin-height=&quot;328&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Show 버튼 눌렀을 때&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 실행 결과, 위와 같이 테스트가 실패 하고 (metadata &amp;gt; duration, resolution이 다름), 테스트 실패 원인을 파악해 보니 Video 타입이 초기화시 metadata 초기화를 누락한 것을 알게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-28 오후 10.58.54.png&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DMIW3/btsIRxsVAeN/MhLky8RPlMHmwQ9bqPCG11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DMIW3/btsIRxsVAeN/MhLky8RPlMHmwQ9bqPCG11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DMIW3/btsIRxsVAeN/MhLky8RPlMHmwQ9bqPCG11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDMIW3%2FbtsIRxsVAeN%2FMhLky8RPlMHmwQ9bqPCG11%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;1282&quot; height=&quot;282&quot; data-filename=&quot;스크린샷 2024-07-28 오후 10.58.54.png&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;282&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 initializer를 수정 후 다시 테스트 실행하면? 아래와 같이 성공하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-28 오후 10.59.48.png&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;122&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tcqjC/btsIQp3x1xS/ojO3FT5SZgqTxKk9K2qrQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tcqjC/btsIQp3x1xS/ojO3FT5SZgqTxKk9K2qrQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tcqjC/btsIQp3x1xS/ojO3FT5SZgqTxKk9K2qrQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtcqjC%2FbtsIQp3x1xS%2FojO3FT5SZgqTxKk9K2qrQk%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;1278&quot; height=&quot;122&quot; data-filename=&quot;스크린샷 2024-07-28 오후 10.59.48.png&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;122&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 설명했던 것처럼, #expect 매크로는 매우 유연하고 직관적입니다. (XCTest에서 XCTAssertEqual, XCTAssertLessThan 같이 다양한 Assertion api 들을 이해하고 사용할 필요 없이 일반적인 표현식과 연산자를 사용하면 되기 때문)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-28 오후 11.04.07.png&quot; data-origin-width=&quot;558&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cx4JFm/btsIPAdIAIZ/SDTMV9kxwfkMCKJlciplJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cx4JFm/btsIPAdIAIZ/SDTMV9kxwfkMCKJlciplJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cx4JFm/btsIPAdIAIZ/SDTMV9kxwfkMCKJlciplJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcx4JFm%2FbtsIPAdIAIZ%2FSDTMV9kxwfkMCKJlciplJ1%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;280&quot; height=&quot;235&quot; data-filename=&quot;스크린샷 2024-07-28 오후 11.04.07.png&quot; data-origin-width=&quot;558&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 실행 도중 실패시 멈추고 싶을 땐 #expect 매크로가 아닌 try #require() 매크로를 사용하면 됩니다. 그럼 테스트 실패시, 테스트를 더 진행하지 않습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722175737118&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try #require(session.isValid) // 테스트 실패시, error throw &amp;amp; 테스트 더 진행하지 않음

// 해당 라인은 첫번째 라인이 성공했을 때만 의미가 있기 때문에 테스트 실패시 테스트를 멈추도록 try #require 사용한 케이스
sesiion.invalidate()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-28 오후 11.12.18.png&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCYCU4/btsIOWO03Oi/sWwlVZyZyCUPEmHw8ngpNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCYCU4/btsIOWO03Oi/sWwlVZyZyCUPEmHw8ngpNK/img.png&quot; data-alt=&quot;이 경우도 마찬가지로, paymentMethods.first가 nil이고, 더 테스트를 진행할 필요가 없기 때문에 테스트는 fail되고 중단됨&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCYCU4/btsIOWO03Oi/sWwlVZyZyCUPEmHw8ngpNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCYCU4%2FbtsIOWO03Oi%2FsWwlVZyZyCUPEmHw8ngpNK%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;1532&quot; height=&quot;192&quot; data-filename=&quot;스크린샷 2024-07-28 오후 11.12.18.png&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;192&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이 경우도 마찬가지로, paymentMethods.first가 nil이고, 더 테스트를 진행할 필요가 없기 때문에 테스트는 fail되고 중단됨&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트의 경우 아래와 같이 custom display name을 사용할 수 있습니다. (아래 코드에서 &quot;Check video metadata&quot;)&lt;/p&gt;
&lt;pre id=&quot;code_1722176152867&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test(&quot;Check video metadata&quot;) func videoMetadata() {
    let video = Video(fileName: &quot;By the Lake.mov&quot;)
    let expectedMetadata = Metadata(duration: .seconds(90))
    #expect(video.metadata == expectedMetadata)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 테스트 목적을 조금 더 구체적으로 설명할 수 있고, 아래 이미지처럼 테스트 네비게이터에도 직접 지정한 custom display name으로 뜨게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-28 오후 11.17.08.png&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LoVGU/btsIQmFGMlq/SfiGEe6a3CPRE6YNaYTY41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LoVGU/btsIQmFGMlq/SfiGEe6a3CPRE6YNaYTY41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LoVGU/btsIQmFGMlq/SfiGEe6a3CPRE6YNaYTY41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLoVGU%2FbtsIQmFGMlq%2FSfiGEe6a3CPRE6YNaYTY41%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;1414&quot; height=&quot;468&quot; data-filename=&quot;스크린샷 2024-07-28 오후 11.17.08.png&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Test 함수와 Expectations에 대해 알아봤으므로, 그 다음으로는 Traits를 알아보도록 합시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Testing에서 제공하는 Built-in traits는 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-28 오후 11.21.47.png&quot; data-origin-width=&quot;2006&quot; data-origin-height=&quot;884&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkvsBj/btsIPYL1hm3/7k1FHrLVnVgTGVmhsrjdi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkvsBj/btsIPYL1hm3/7k1FHrLVnVgTGVmhsrjdi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkvsBj/btsIPYL1hm3/7k1FHrLVnVgTGVmhsrjdi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkvsBj%2FbtsIPYL1hm3%2F7k1FHrLVnVgTGVmhsrjdi0%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;2006&quot; height=&quot;884&quot; data-filename=&quot;스크린샷 2024-07-28 오후 11.21.47.png&quot; data-origin-width=&quot;2006&quot; data-origin-height=&quot;884&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Test(&quot;...&quot;): 테스트 디스플레이 네임 지정&lt;/li&gt;
&lt;li&gt;@Test(.bug(&quot;...&quot;, &quot;...&quot;): 버그 주석과 함께, 관련 문제 참조 URL을 전달, XCode16 테스트 보고서에서 해당 버그 특성을 보고 URL을 클릭해 열어볼 수 있음&lt;/li&gt;
&lt;li&gt;@Test(.tags(...)): 테스트 태그 지정&lt;/li&gt;
&lt;li&gt;@Test(.enabled(if: ...)): 테스트 실행 전 평가할 조건을 전달하고, 실패시 테스트는 건너뜀&lt;/li&gt;
&lt;li&gt;@Test(.disabled(&quot;...&quot;)): 테스트 비활성화 시 사용, 문자열은 왜 해당 테스트를 비활성화 하는지 설명 기입, 실행하지 않을 테스트를 주석치는 것과의 차이점은 테스트 코드가 정상적으로 컴파일 되는지 확인 가능 (실행만 안 할 뿐)&lt;/li&gt;
&lt;li&gt;@Test(...) @available(macOS 15, *): 테스트 본문 전체가 특정 OS 버전에서만 실행될 수 있는 경우&lt;/li&gt;
&lt;li&gt;@Test(.timeLimit(...): 테스트 함수 maximum timeLimit 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(해당 traits의 사용 예시는 아래 Common workflows에서 확인 가능합니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;테스트가 여러개일 때는 아래와 같이 Suites로 구조화 할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722177417424&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Testing
@testable import DestinationVideo

// 테스트 스위트 (테스트 그룹화)
struct VideoTests {
    let video = Video(fileName: &quot;By the Lake.mov&quot;)
    
    @Test(&quot;Check video metadata&quot;) func videoMetadata() {
        let expectedMetadata = Metadata(duration: .seconds(90))
        #expect(video.metadata == expectedMetadata)
    }
    
    @Test func rating() async throws {
        #expect(video.contentRating == &quot;G&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 아래와 같이 계층 구조가 테스트 네비게이터에 반영되고, 테스트를 그룹으로 실행 가능합니다. @Test 함수나, @Suites를 포함하는 모든 타입은 암묵적으로 @Suite로 간주되기 때문에 @Suite를 명시하지 않아도 상관 없습니다. (추가로 아래 이미지 속 코드에는 나오지 않지만 set-up &amp;amp; tear-down 로직이 있을 경우 init, deinit에서 수행하면 됩니다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-28 오후 11.50.23.png&quot; data-origin-width=&quot;1558&quot; data-origin-height=&quot;462&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5cpWF/btsIP7IPovp/zcWWww9KG6Qx4rOgTWRft1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5cpWF/btsIP7IPovp/zcWWww9KG6Qx4rOgTWRft1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5cpWF/btsIP7IPovp/zcWWww9KG6Qx4rOgTWRft1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5cpWF%2FbtsIP7IPovp%2FzcWWww9KG6Qx4rOgTWRft1%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;1558&quot; height=&quot;462&quot; data-filename=&quot;스크린샷 2024-07-28 오후 11.50.23.png&quot; data-origin-width=&quot;1558&quot; data-origin-height=&quot;462&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Common workflows&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;위에 설명한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Built-in traits 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 조건부 테스트&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-28 오후 11.54.15.png&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ao8Zu/btsIQ1HUbS7/1qoemcq8BdU7qAAm79tTek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ao8Zu/btsIQ1HUbS7/1qoemcq8BdU7qAAm79tTek/img.png&quot; data-alt=&quot;.enabled 조건 실패시 해당 테스트 수행 스킵&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ao8Zu/btsIQ1HUbS7/1qoemcq8BdU7qAAm79tTek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAo8Zu%2FbtsIQ1HUbS7%2F1qoemcq8BdU7qAAm79tTek%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;1514&quot; height=&quot;176&quot; data-filename=&quot;스크린샷 2024-07-28 오후 11.54.15.png&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;.enabled 조건 실패시 해당 테스트 수행 스킵&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-28 오후 11.57.37.png&quot; data-origin-width=&quot;1418&quot; data-origin-height=&quot;198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpFUmF/btsIPSypLGR/NgTg28YOQqwDp1M3u64H7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpFUmF/btsIPSypLGR/NgTg28YOQqwDp1M3u64H7k/img.png&quot; data-alt=&quot;.disabled와 .bug trait을 사용한 케이스, 왜 테스트를 수행 하지 않는지, 버그와 관련된 참조 링크 표시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpFUmF/btsIPSypLGR/NgTg28YOQqwDp1M3u64H7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpFUmF%2FbtsIPSypLGR%2FNgTg28YOQqwDp1M3u64H7k%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;1418&quot; height=&quot;198&quot; data-filename=&quot;스크린샷 2024-07-28 오후 11.57.37.png&quot; data-origin-width=&quot;1418&quot; data-origin-height=&quot;198&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;.disabled와 .bug trait을 사용한 케이스, 왜 테스트를 수행 하지 않는지, 버그와 관련된 참조 링크 표시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-28 오후 11.59.07.png&quot; data-origin-width=&quot;1490&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dtTK3J/btsIPXfey9X/Zki0eN8EWYauDRksOn35bK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dtTK3J/btsIPXfey9X/Zki0eN8EWYauDRksOn35bK/img.png&quot; data-alt=&quot;해당 테스트가 특정 OS와 버전에 의존성이 있는 경우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dtTK3J/btsIPXfey9X/Zki0eN8EWYauDRksOn35bK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdtTK3J%2FbtsIPXfey9X%2FZki0eN8EWYauDRksOn35bK%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;1490&quot; height=&quot;200&quot; data-filename=&quot;스크린샷 2024-07-28 오후 11.59.07.png&quot; data-origin-width=&quot;1490&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;해당 테스트가 특정 OS와 버전에 의존성이 있는 경우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 공통 특성을 갖는 테스트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 네비게이터에서는 아래와 같이 태그로 테스트를 구분하여 확인 및 실행이 가능하고 테스트 보고서에서 tag별 필터링이 가능합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-29 오전 12.05.08.png&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTsrTZ/btsIPXsNHxf/MAPgR4GXFH5wQRII9vUk71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTsrTZ/btsIPXsNHxf/MAPgR4GXFH5wQRII9vUk71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTsrTZ/btsIPXsNHxf/MAPgR4GXFH5wQRII9vUk71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTsrTZ%2FbtsIPXsNHxf%2FMAPgR4GXFH5wQRII9vUk71%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;1672&quot; height=&quot;624&quot; data-filename=&quot;스크린샷 2024-07-29 오전 12.05.08.png&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-29 오전 12.06.10.png&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;950&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIkM9l/btsIQ0PKLOr/Ngr824onjR1PKlWoG3t1h1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIkM9l/btsIQ0PKLOr/Ngr824onjR1PKlWoG3t1h1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIkM9l/btsIQ0PKLOr/Ngr824onjR1PKlWoG3t1h1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIkM9l%2FbtsIQ0PKLOr%2FNgr824onjR1PKlWoG3t1h1%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;1914&quot; height=&quot;950&quot; data-filename=&quot;스크린샷 2024-07-29 오전 12.06.10.png&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;950&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 반복 테스트 (Tests with different arguments)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 다른 비디오에 대해 같은 테스트를 수행한다고 가정했을 때, 아래와 같이 테스트를 구성하면 매우 비효율적이게 되는데, 이럴 경우 arguments를 사용해, 비디오이름을 인자로 받는 단일 테스트케이스를 작성할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-29 오전 12.11.13.png&quot; data-origin-width=&quot;1132&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dYw8Ds/btsIRnxabLY/UB76lyAtrTiA2GKIwzvkS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dYw8Ds/btsIRnxabLY/UB76lyAtrTiA2GKIwzvkS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dYw8Ds/btsIRnxabLY/UB76lyAtrTiA2GKIwzvkS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdYw8Ds%2FbtsIRnxabLY%2FUB76lyAtrTiA2GKIwzvkS0%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;1132&quot; height=&quot;582&quot; data-filename=&quot;스크린샷 2024-07-29 오전 12.11.13.png&quot; data-origin-width=&quot;1132&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;아래 코드는 arguments 사용해 videoName을 인자로 받는 단일 테스트 케이스입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722180283071&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct VideoContinentsTests {
    @Test(&quot;Number of mentioned continents&quot;, arguments: [
    	&quot;A Beach&quot;,
        &quot;By the Lake&quot;,
        &quot;Camping in the Woods&quot;,
        &quot;The Rolling Hills&quot;,
        &quot;Ocean Breeze&quot;,
        &quot;Scotland Coast&quot;,
        &quot;China Paddy Field&quot;
    ])
    func mentionedContinentCounts(videoName: String) async throws {
    	let videoLibrary = try await VideoLibrary()
        let video = try #require(await videoLibrary.video(named: videoName))
        #expect(!video.mentionedContinents.isEmpty)
        #expect(video.mentionedContinents.count &amp;lt;= 3)
    }
}&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;스크린샷 2024-07-29 오전 12.19.58.png&quot; data-origin-width=&quot;1622&quot; data-origin-height=&quot;742&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1hjqA/btsIRm57rNO/oLaFXPUdXKIokBJGFTQ3I1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1hjqA/btsIRm57rNO/oLaFXPUdXKIokBJGFTQ3I1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1hjqA/btsIRm57rNO/oLaFXPUdXKIokBJGFTQ3I1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1hjqA%2FbtsIRm57rNO%2FoLaFXPUdXKIokBJGFTQ3I1%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;1622&quot; height=&quot;742&quot; data-filename=&quot;스크린샷 2024-07-29 오전 12.19.58.png&quot; data-origin-width=&quot;1622&quot; data-origin-height=&quot;742&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 테스트는 단일 테스트 본문 내에서 for in loop을 사용하는 것과 유사해 보이지만 arguments를 사용한 테스트의 경우 for in loop를 사용한 테스트보다 몇가지 장점이 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;arguments를 사용한 테스트의 경우 개별 인수 세부 정보를 테스트 결과에서 확인할 수 있으며 디버깅을 위해 특정 인수만 독립적으로 실행 이 가능 합니다. 그리고, 테스팅을 병렬적으로 실행하기 때문에 테스트 실행 결과 역시 빠르게 확인해 볼 수 있다는 장점을 갖고 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;XCTest와의 차이&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swift Testing의 구성 요소와 특징들을 기존 XCTest와 비교해 보면 여러모로 정말 좋아졌다는 걸 느낄 수 있습다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-29 오전 12.34.21.png&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;656&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ct5Fe6/btsIPVhykze/FR4nDQSKTwCdeKuWNKdlS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ct5Fe6/btsIPVhykze/FR4nDQSKTwCdeKuWNKdlS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ct5Fe6/btsIPVhykze/FR4nDQSKTwCdeKuWNKdlS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fct5Fe6%2FbtsIPVhykze%2FFR4nDQSKTwCdeKuWNKdlS0%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;1516&quot; height=&quot;656&quot; data-filename=&quot;스크린샷 2024-07-29 오전 12.34.21.png&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;656&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-29 오전 12.40.41.png&quot; data-origin-width=&quot;1544&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6cYza/btsIP7aX7hy/vXfDl18FKspiihpLjfQ5hK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6cYza/btsIP7aX7hy/vXfDl18FKspiihpLjfQ5hK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6cYza/btsIP7aX7hy/vXfDl18FKspiihpLjfQ5hK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6cYza%2FbtsIP7aX7hy%2FvXfDl18FKspiihpLjfQ5hK%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;1544&quot; height=&quot;648&quot; data-filename=&quot;스크린샷 2024-07-29 오전 12.40.41.png&quot; data-origin-width=&quot;1544&quot; data-origin-height=&quot;648&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-29 오전 12.41.33.png&quot; data-origin-width=&quot;1518&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4n3YH/btsIPlA38Yv/VRmiBzzpooHqnfKzJnXnqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4n3YH/btsIPlA38Yv/VRmiBzzpooHqnfKzJnXnqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4n3YH/btsIPlA38Yv/VRmiBzzpooHqnfKzJnXnqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4n3YH%2FbtsIPlA38Yv%2FVRmiBzzpooHqnfKzJnXnqK%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;1518&quot; height=&quot;640&quot; data-filename=&quot;스크린샷 2024-07-29 오전 12.41.33.png&quot; data-origin-width=&quot;1518&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-29 오전 12.46.08.png&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DSSEr/btsIPwWEl2n/90p4B3KhvqsQjPfX3q5GE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DSSEr/btsIPwWEl2n/90p4B3KhvqsQjPfX3q5GE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DSSEr/btsIPwWEl2n/90p4B3KhvqsQjPfX3q5GE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDSSEr%2FbtsIPwWEl2n%2F90p4B3KhvqsQjPfX3q5GE0%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;1530&quot; height=&quot;624&quot; data-filename=&quot;스크린샷 2024-07-29 오전 12.46.08.png&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Swift&amp;amp;iOS/Swift</category>
      <category>ios</category>
      <category>Swift</category>
      <category>unit test</category>
      <category>UnitTest</category>
      <category>WWDC</category>
      <category>wwdc24</category>
      <category>xcode16</category>
      <author>lsh424</author>
      <guid isPermaLink="true">https://lsh424.tistory.com/101</guid>
      <comments>https://lsh424.tistory.com/101#entry101comment</comments>
      <pubDate>Mon, 29 Jul 2024 01:28:55 +0900</pubDate>
    </item>
    <item>
      <title>[책] 구글 엔지니어는 이렇게 일한다 - 4</title>
      <link>https://lsh424.tistory.com/100</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;책을 읽고 인상 깊었던 글귀 정리&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;Chapter4. 공정 사회를 위한 엔지니어링&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지금까지는 프로그래밍과 소프트웨어 엔지니어링의 차이를 알아보았습니다. 프로그래밍은 당면한 문제에 집중하여 코드를 생산합니다. 그에 반해 소프트웨어 엔지니어링은 수십 년 혹은 평생에 걸친 유동적이고 모호한 문제에 대응하기 위해 코드, 도구, 정책, 프로세스 등을 응용하는 더 폭넓은 개념입니다. 이번 장에서는 다양한 계층의 사용자를 위한 제품을 설계할 때 엔지니어가 짊어져야 할 책임에 관해 이야기합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(중략)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[4.5 단일한 접근 방식 거부하기]&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;오늘날의 개발은 많이 쓰이는 기능을 먼저 만들고 특수한 상황에 쓰이는 기능이나 개선은 나중으로 미루는 방식으로 주로 진행됩니다. 하지만 여기에는 결함이 있습니다. 기술을 접하기에 유리한 사람들에게 우선권을 줌으로써 불평등을 가중하는 방식인 것입니다. 모든 사용자층을 고려하는 일을 설계 막바지까지 미룸으로써 훌륭한 엔지니어의 기준을 낮추는 꼴입니다. 대신 시작부터 포용적으로 설계하여 개발이 지향해야 하는 기준점을 높여주세요. 그리하여 기술을 접하기 어려운 사람들도 쉽게 접할 수 있고 행복하게 활용할 수 있는 도구를 만들어주세요. 이런 식으로 우리는 모든 사용자의 경험을 개선할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[4.6 확립된 프로세스에 도전하기]&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;더 공정한 제도를 만들기란 제품을 더 포용적으로 설계하는 것 이상으로 어려운 도전입니다. 공정한 제도 개발이란 때론 잘못된 결과를 낳는 기존 프로세스에 반하는 도전을 의미합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(중략)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;/li&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;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;b&gt;[4.10 핵심 정리]&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;우리는 편견에서 벗어날 수 없습니다.&lt;/li&gt;
&lt;li&gt;다양한 사용자층을 포용하도록 설계하려면 조직 구성 측면에서도 반드시 다양성을 갖춰야 합니다.&lt;/li&gt;
&lt;li&gt;..&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>책</category>
      <category>Google</category>
      <category>구글</category>
      <category>구글 엔지니어는 이렇게 일한다</category>
      <category>소프트웨어 엔지니어링</category>
      <category>책</category>
      <author>lsh424</author>
      <guid isPermaLink="true">https://lsh424.tistory.com/100</guid>
      <comments>https://lsh424.tistory.com/100#entry100comment</comments>
      <pubDate>Sat, 29 Jun 2024 15:01:44 +0900</pubDate>
    </item>
    <item>
      <title>[책] 구글 엔지니어는 이렇게 일한다 - 3</title>
      <link>https://lsh424.tistory.com/99</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;책을 읽고 인상 깊었던 글귀 정리&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;Chapter3. 지식 공유&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;조직 내에는 질문의 답을 아는 전문가들이 필요하고 그들의 지식을 전파할 메커니즘도 필요합니다. 이것이 이번 장에서 다룰 주제입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[3.1 배움을 가로막는 장애물]&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;/li&gt;
&lt;li&gt;정보 섬 - 조직의 각 부서가 서로 소통하거나 자원을 공유하지 않아서 지식이 파편화됩니다.&lt;/li&gt;
&lt;li&gt;단일 장애점 - 중요한 정보를 한 사람이 독점하면 병목이 생깁니다. 좋은 의도에서 단일 장애점이 시작되기도 합니다. '여러분을 위해 내가 다 처리하지'같은 마음에서 말이죠. 하지만 단기 효율은 높여주는 대신('내가 하는 게 빠르지') 장기 확장성을 희생하는 꼴입니다.(그 팀은 팀으로서 필요한 일들을 어떻게 해내야 할지 전혀 배우지 못합니다.)&lt;/li&gt;
&lt;li&gt;전부 아니면 전무 전문성 - 조직 구성원이 '모든 것을 아는'사람과 '아무것도 모르는' 초심자로 나뉩니다. 중간층은 거의 사라지죠. 한번 이렇게 되면 전문가들은 항상 모든 일을 자신들이 처리하게 됩니다. 지식과 책임은 계속 이미 전문가가 된 사람들에게 집중되고, 새로운 팀원이나 초심자들은 그들만의 울타리에 갇혀 느리게 성장하게 됩니다.&lt;/li&gt;
&lt;li&gt;앵무새처럼 흉내내기 - 이해하지 못한 상태로 흉내만 내는 것을 말합니다. 이 증상에 빠진 사람은 목적을 이해하지 못하고(주로 '잘은 모르겠지만 이게 맞겠거니' 하고) 무의식적으로 기존 패턴이나 코드를 따라 합니다.&lt;/li&gt;
&lt;li&gt;유령의 묘지 - 무언가 잘못될 게 두려워서 아무도 손대지 않는 영역(주로 코드)을 말합니다.&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[3.3 판 깔아주기: 심리적 안전]&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;심리적 안전은 학습 환경을 조성하는데 매우 중요합니다. 먼저 자신이 이해하지 못한 게 있음을 인정해야 무언가를 배울 수 있습니다. 그러니 우리 모두는 타인의 무지를 탓하지 말고 그 솔직함을 반겨야 합니다. 배움에는 '무언가를 시도하다가 실패해도 안전하다'는 인식이 엄청나게 중요합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[3.4 내 지식 키우기]&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 장에서 잊지 말아야 할 단 하나의 문장을 뽑는다면 '항상 배우고 항상 질문하라!'일 것입니다. 초심자가 저지르는 가장 큰 실수는 무언가 막혔을 때 질문하지 않는 것입니다. 혼자서 극복해내고 싶다거나 '너무 기초적인' 질문이란 소리를 듣는 게 두려워서일 수 있습니다. 혹은 '도움을 청하기 전에 최대한 노력해봐야 해'라고 생각할지 모릅니다. 이 함정에 빠지지 마세요!&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;스크린샷 2024-06-09 오후 1.18.50.png&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uot5f/btsHSb6E61I/kbZ8ZY8nZAIsaOuLKAGR61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uot5f/btsHSb6E61I/kbZ8ZY8nZAIsaOuLKAGR61/img.png&quot; data-alt=&quot;학사 &amp;amp;gt; 석사 &amp;amp;gt; 박사로 갈 수록 안다고 생각하는 것이 더 적어짐&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uot5f/btsHSb6E61I/kbZ8ZY8nZAIsaOuLKAGR61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fuot5f%2FbtsHSb6E61I%2FkbZ8ZY8nZAIsaOuLKAGR61%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;594&quot; height=&quot;360&quot; data-filename=&quot;스크린샷 2024-06-09 오후 1.18.50.png&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;학사 &amp;gt; 석사 &amp;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;&lt;b&gt;[3.5 질문 확장하기: 커뮤니티에 묻기]&lt;/b&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그룹 채팅&lt;/li&gt;
&lt;li&gt;메일리 리스트&lt;/li&gt;
&lt;li&gt;스택오버플로&lt;/li&gt;
&lt;li&gt;..&lt;/li&gt;
&lt;/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;b&gt;[3.6 지식 확장하기: 누구나 가르칠 게 있다]&lt;/b&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;lt;문서자료&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서자료는 독자가 무언가를 배우도록 돕는 것을 최우선 목표로 하는 기록된 지식입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문서자료 갱신하기 - 무언가를 막 배운 순간이 기존 문서자료에서 개선점을 찾기에 가장 좋은 때입니다. 새로운 프로세스나 시스템에 익숙해지고 깊이 이해하게 되면 어떤 내용이 어려웠는지, 혹은 '시작하기' 문서에서 누락된 사소한 단계가 무엇이었는지 이미 잊었을 가능성이 큽니다. 처음 배우는 단계에서 혹시라도 문서자료의 실수나 빠진 부분을 발견한다면 곧바로 고치세요!&lt;/li&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;&amp;lt;코드&amp;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;[3.7 조직의 지식 확장하기]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지식을 공유할 때는 상냥함과 존중을 담아야 하고, 또 그래야만 가능합니다. 기술 업계에서는 '뛰어난 괴짜'를 용인하는(심지어 숭배하는) 경향이 있지만, 해로운 현상입니다. 상냥한 전문가라도 얼마든지 가능하기 때문이죠. 구글 소프트웨어 엔지니어링 직무 사다리(job ladder)에서 리더십 항목을 찾아보면 다음 내용을 명확히 밝히고 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;리더는 주변 사람들을 성장시키고, 팀의 심리적 안전을 개선하고, 팀워크와 협업 문화를 조성하고, 팀 내 긴장을 해소하고, 구글 문화의 가치를 설정하며, 구글을 더 활기차고 신나는 일터로 가꿔야 합니다. 괴짜는 좋은 리더가 아닙니다.&lt;/blockquote&gt;</description>
      <category>책</category>
      <category>Google</category>
      <category>구글</category>
      <category>구글 엔지니어는 이렇게 일한다</category>
      <category>소프트웨어 엔지니어링</category>
      <category>책</category>
      <author>lsh424</author>
      <guid isPermaLink="true">https://lsh424.tistory.com/99</guid>
      <comments>https://lsh424.tistory.com/99#entry99comment</comments>
      <pubDate>Tue, 11 Jun 2024 20:00:24 +0900</pubDate>
    </item>
    <item>
      <title>[책] 구글 엔지니어는 이렇게 일한다 - 2</title>
      <link>https://lsh424.tistory.com/98</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;책을 읽고 인상 깊었던 글귀 정리&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;Chapter2. 팀워크 이끌어내기&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&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;[2.2 천재 신화]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬은 온전히 귀도 반 로섬의 작품일까요? 그가 첫 번째 버전을 작성한 건 사실입니다. 하지만 그 후 버전들은 수천 명의 사람이 아이디어를 모으고 기능을 개발하고 버그를 수정하며 만들었습니다. 스티브 잡스는 매킨토시 제작팀을 이끌었습니다. 빌 게이츠는 초기 가정용 컴퓨터를 위한 베이식 언어 인터프리터를 작성했지만, 더 큰 업적은 MS-DOS를 중심으로 마이크로소프트라는 회사를 일구어 성공시킨 일입니다. 이들은 모두 커뮤니티를 이끌어 집단적 성과물의 상징이 되었습니다. 이렇듯 천재 신화는 팀이 이룬 성공을 단 한 사람(리더)에게 몰아주어 만들어지는 경향이 있습니다. (== 모든 훌륭한 결과물은 다수의 사람에 의해 만들어진다.)&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;[2.3 숨기는 건 해롭다]&lt;/b&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;여러분의 프로젝트에서는 필요한 지식과 노하우가 얼마나 분산되어 있나요? 프로토타입 코드의 동작 원리를 이해하는 사람이 나뿐이라면 내가 해고될 가능성은 극히 낮습니다. 하지만 내가 버스에 치인다면 프로젝트는 아마 망할 것입니다. 누군가가 결혼하거나 팀을 옮기거나 퇴사하거나 병가를 낼 수 있습니다. 최소한 각 책임 영역마다 2차 소유자(담당자)를 두고, 제대로 된 문서를 갖춰 둔다면 프로젝트의 미래를 보장하는데 도움이 됩니다.&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;[2.4 모든 건 팀에 달렸다]&lt;/b&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;겸손: 당신은 모든 것을 알지도, 완벽하지도 않습니다. 겸손한 사람은 배움에 열려 있습니다.&lt;/li&gt;
&lt;li&gt;존중: 함께 일하는 동료를 진심으로 생각합니다. 친절하게 대하고 그들의 능력과 성취에 감사해합니다.&lt;/li&gt;
&lt;li&gt;신뢰: 동료들이 유능하고 올바른 일을 하리라 믿습니다. 필요하면 그들에게 스스로 방향을 정하게 해도 좋습니다.&lt;/li&gt;
&lt;/ol&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;/li&gt;
&lt;li&gt;비평하고 비평받는 법 배우기: 건설적 비판은 프로젝트에 도움이 되며 개선을 위한 지침을 줄 수 있고, 또 주어야 합니다. 여기서 가장 중요한 점은 건설적으로 비판하는 사람은 상대방을 진심으로 생각하고 상대방의 업무가 개선되길 바라야 한다는 것입니다. 대화의 반대편으로 가서, 여러분 자신도 비평을 잘 수용할 줄 알아야 합니다. 자신의 기술에 겸손해야 함은 물론, 상대는 내 최우선 관심사를 진심으로 생각하며 절대 나를 어리석다고 생각하는 게 아님을 믿어야 합니다. 우리 자존감을 우리가 작성한 코드와 동일시해서는 안됩니다.(중략) 누군가에게 '잘못했다'라고 해서는 안됩니다. 무언가를 '고치라고 요구'해서도 안 됩니다. 마지막으로 '다른 사람들과 다르게 했다고 비난'하면 안 됩니다. 다음과 같이 이야기하면 한결 나을 것입니다. &quot;저기, 이 부분의 제어 흐름이 좀 헷갈리네요. 혹시 xyz 코드 패턴을 적용하면 더 명확해질까요? 나중에 관리하기 쉬워질지도 모르겠네요.&quot; &amp;gt; 상대가 아닌 자신을 겸손하게 낮췄음에 주목하세요. 아무것도 요구하지 않고 동료가 제안을 거부해도 부담을 느끼지 않도록 배려했습니다. 또한 누군가의 가치나 코딩 기술이 아니라 코드 자체에 집중하고 있습니다.&lt;/li&gt;
&lt;li&gt;빠르게 실패하고 반복하기: 구글에서 제가 정말 좋아하는 좌우명은 '실패는 선택이다'입니다. 구글에서는 '가끔씩 실패하지 않는다면 충분히 혁신적이지 않거나 위험을 충분히 감수하지 않는 것이다'라는 믿음이 널리 통용됩니다. 실패를 '배우고 다음 단계로 넘어갈 수 있는 절호의 기회'라고 생각하는 것이죠. 실제로 발명왕 토머스 에디슨은 이런 말을 남겼습니다. '나는 만 가지의 잘못된 방식을 찾아낸 것일 뿐 실패한 게 아니다. 잘못하고 폐기한 시도 하나하나가 다음 단계로 이끌어주기 때문에 나는 낙담하지 않는다.'&lt;/li&gt;
&lt;/ol&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;훌륭한 포스트모템에는 다음 내용이 담겨야 합니다.&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;li&gt;영향과 피해 평가&lt;/li&gt;
&lt;li&gt;문제를 즉시 해결하기 위한 조치 항목&lt;/li&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;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;소프트웨어를 작성할 때는 방어적인 태도를 고집할 이유가 없습니다. 팀원은 동반자이지 경쟁자가 아닙니다. 여러분 모두가 같은 목표를 향해 달려가는 중입니다.&lt;/p&gt;</description>
      <category>책</category>
      <category>Google</category>
      <category>구글</category>
      <category>구글 엔지니어는 이렇게 일한다</category>
      <category>소프트웨어 엔지니어링</category>
      <category>책</category>
      <author>lsh424</author>
      <guid isPermaLink="true">https://lsh424.tistory.com/98</guid>
      <comments>https://lsh424.tistory.com/98#entry98comment</comments>
      <pubDate>Mon, 10 Jun 2024 19:15:27 +0900</pubDate>
    </item>
    <item>
      <title>[책] 구글 엔지니어는 이렇게 일한다 - 1</title>
      <link>https://lsh424.tistory.com/97</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;책을 읽고 인상 깊었던 글귀 정리&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;Chapter1. 소프트웨어 엔지니어링이란?&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[1.1 시간과 변경]&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;br /&gt;&lt;br /&gt;프로젝트의 기대 수명을 생각했을 때, 그 수명이 길것으로 예상된다면 프로젝트는 외부 환경의 변화에 대비하기 시작해야 합니다. 그렇지 않으면 매우 고통스러울 수 있습니다. &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 프로젝트는 세월이 충분히 흐르면 기반 환경을 모두 교체해야 할 것입니다. 하지만 아무런 외부 의존성 없이 (혹은 POSIX처럼 아주 오랜 기간 변하지 않는 대상만 이용하도록) 작성된 순수한 C 언어 프로젝트라면 리팩터링이나 난해한 업그레이드 없이 버틸 수도 있습니다. 이런 면에서 C 언어는 상당히 안정적이며, 이런 안정성이야말로 C 언어를 선택하는 주된 이유 중 하나입니다. 하지만 대부분의 프로젝트는 기반 기술의 변화를 훨씬 많이 겪습니다. 대다수의 프로그래밍 언어와 런타임은 C 언어보다 빠르게 변합니다. 여러분의 프로젝트가 의존하는 모든 기술에는 여러분이 사용하기 시작한 후에야 발견될 심각한 버그와 보안 구멍이 존재할 위험이 도사립니다.&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;&lt;b&gt;[1.2 규모 확장과 효율성]&lt;/b&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;[1.3 트레이드오프와 비용]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;/li&gt;
&lt;li&gt;리소스 비용 (예: CPU 시간)&lt;/li&gt;
&lt;li&gt;인적 비용 (예: 엔지니어링 노력)&lt;/li&gt;
&lt;li&gt;거래 비용 (예: 조치를 취하는 비용)&lt;/li&gt;
&lt;li&gt;기회 비용 (예: 조치를 취하지 않는 비용)&lt;/li&gt;
&lt;li&gt;사회적 비용 (예: 선택이 사회 전체에 미치는 영향)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비용을 평가할 때는 앞에서 열거한 모든 비용을 염두에 두어야 합니다. 소프트웨어 엔지니어링처럼 매우 창의적이고 수익성 높은 분야에서는 금융 비용보다는 인적 비용이 제한 요소일 가능성이 큽니다. 그래서 엔지니어들이 행복을 느끼게 만들고 일에 집중하고 참여할 수 있게 해 주면 효율이 높아지죠. 집중력이 생산성에 미치는 영향은 아주 커서 10%~20%의 차이는 우습게 만들어냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 내가 두 주 동안 이 연결 리스트를 고성능 데이터 구조로 변환한다면 프로덕션 시스템의 메모리 5GB를 더 쓰지만 필요한 CPU 수를 2,000개 줄일 수 있어. 진행해야 할까? 이 물음에 답하려면 메모리와 CPU의 상대적 비용은 물론, 나아가 인건비(소프트웨어 엔지니어가 2주간 작업)와 기회비용 (2주 동안 엔지니어가 할 수 있는 다른 일)까지 고려해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전혀 과학적이지 않은 트위터 여론조사에 따르면 소프트웨어가 거대해진 오늘날에도 약 60%~70%의 개발자가 빌드를 로컬에서 실행한다고 합니다. 여러분의 조직에서는 빌드가 끝나기를 기다리느라 얼마나 많은 생산적인 시간을 낭비하고 있나요? 2000년대 중반 구글은 로컬에서 컴파일 했으며 코드베이스가 커져가며 컴파일 시간도 꾸준히 늘어났습니다. 자연스레 더 크고 강력한 로컬 머신을 구입하는데 지출이 커졌고 컴파일 시간 상승은 곧 일할 시간이 줄어든다는 뜻이라서 우회적으로 인건비를 상승시키는 효과도 있었습니다. 결국 구글은 자체 분산 빌드 시스템을 개발했습니다. 이 시스템을 개발하는데도 비용과 시간이 들어갔지만, 결과적으로 전체적으로는 절약되는 비용이 훨씬 컸음이 명백했습니다. (빌드가 빨라졌고, 엔지니어가 멍 때리는 시간이 줄어듬)&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;[1.4 소프트웨어 엔지니어링 vs 프로그래밍]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 서로 관련은 깊지만 상이한 두 용어인 '프로그래밍'과 '소프트웨어 엔지니어링'을 구별해야 한다고 생각합니다. 그 차이 대부분은 시간 흐름에 따른 코드 관리, 시간 흐름에 따른 규모 확장의 영향, 이런 관점에서의 의사결정 방식에 있습니다. 프로그래밍은 코드를 생산하는 즉각적인 행위입니다. 소프트웨어 엔지니어링은 활용 가치가 남아 있는 한 오랫동안 코드를 유용하게 관리하고 팀 간 협업을 가능케 하는 정책, 관례, 도구 모두를 아우르는 종합적인 개념입니다.&lt;/p&gt;</description>
      <category>책</category>
      <category>Google</category>
      <category>구글</category>
      <category>구글 엔지니어는 이렇게 일한다</category>
      <category>소프트웨어 엔지니어링</category>
      <category>책</category>
      <author>lsh424</author>
      <guid isPermaLink="true">https://lsh424.tistory.com/97</guid>
      <comments>https://lsh424.tistory.com/97#entry97comment</comments>
      <pubDate>Sat, 8 Jun 2024 21:13:54 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] Render Loop &amp;amp; Hitch (화면 버벅임과 끊김 개선)</title>
      <link>https://lsh424.tistory.com/96</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;빈번하게 갱신되는 화면의 경우 어떻게 성능 테스트를 할 수 있을까요? 눈으로 확인했을 때 화면이 버벅이면서 보이거나 끊김 현상이 보이면 당연히 성능이 안 좋다고 판단할 수 있겠지만, 눈으로 확인하는 데는 정확성이 부족할 수 있고 미묘한 차이를 캐치하기 어려울 수 있습니다. 아무래도 가장 좋은 건 데이터, 즉 수치로 확인하는 것이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XCode12부터 UI와 관련된 성능 이슈를 파악하는데 도움을 주는 Instruments - Animation Hitches가 도입되었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 내용은 WWDC21 Hitch 관련 세션을 보고 정리한 글입니다.&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 alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-17 오후 9.45.56.png&quot; data-origin-width=&quot;1146&quot; data-origin-height=&quot;680&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTtj69/btsn12muQGv/M5ufAh9Gde6SDP10fBpK61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTtj69/btsn12muQGv/M5ufAh9Gde6SDP10fBpK61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTtj69/btsn12muQGv/M5ufAh9Gde6SDP10fBpK61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTtj69%2Fbtsn12muQGv%2FM5ufAh9Gde6SDP10fBpK61%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;1146&quot; height=&quot;680&quot; data-filename=&quot;스크린샷 2023-07-17 오후 9.45.56.png&quot; data-origin-width=&quot;1146&quot; data-origin-height=&quot;680&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 디스플레이를 갖는 디바이스의 경우 주사율을 갖습니다. iOS 디바이스의 경우 보통 60Hz의 주사율을 갖고 이는 1초에 60번 화면이 갱신됨을 의미합니다. (물론 몇몇 pro 디바이스의 경우 promotion display라고 해서 최대 120Hz 즉 1초에 120번 화면 갱신이 될 수 있습니다.)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 주사율을 토대로 하나의 프레임이 유지되는 시간은 60Hz 기준 약 16ms, 120Hz 기준 8ms가 됩니다. 정말 찰나의 순간이죠.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Render Loop&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-17 오후 9.51.19.png&quot; data-origin-width=&quot;1540&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBme9b/btsn27A6poy/pgKHDOK0gjmTMLzw4jypK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBme9b/btsn27A6poy/pgKHDOK0gjmTMLzw4jypK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBme9b/btsn27A6poy/pgKHDOK0gjmTMLzw4jypK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBme9b%2Fbtsn27A6poy%2FpgKHDOK0gjmTMLzw4jypK0%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;1540&quot; height=&quot;648&quot; data-filename=&quot;스크린샷 2023-07-17 오후 9.51.19.png&quot; data-origin-width=&quot;1540&quot; data-origin-height=&quot;648&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS 디바이스 화면의 경우 Render Loop에 맞춰 갱신되는데요. 하드웨어는&amp;nbsp;refresh&amp;nbsp;rate에&amp;nbsp;맞춰 VSYNC라는 이벤트를 방출하고 새로운 프레임은 이 VSYNC 이벤트에 맞춰 준비되어 있어야 합니다. (만약 제때 준비되지 못하면 이전 화면이 보여 사용자에게는 같은 화면 혹은 버벅이거나 끊기는 화면으로 느껴질 수 있습니다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-17 오후 9.52.05.png&quot; data-origin-width=&quot;2844&quot; data-origin-height=&quot;1192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XZWTd/btsnZZqhw9v/6kuhKFQMxy9hOzfHBzese0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XZWTd/btsnZZqhw9v/6kuhKFQMxy9hOzfHBzese0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XZWTd/btsnZZqhw9v/6kuhKFQMxy9hOzfHBzese0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXZWTd%2FbtsnZZqhw9v%2F6kuhKFQMxy9hOzfHBzese0%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;1192&quot; data-filename=&quot;스크린샷 2023-07-17 오후 9.52.05.png&quot; data-origin-width=&quot;2844&quot; data-origin-height=&quot;1192&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Render Loop은 위 이미지와 같이 3단계 프로세스, 5개의 페이즈로 구성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 개념적으로는 Event - Commit - Render prepare - Render execute - Display가 60Hz 기준 16ms안에 완료되어야 화면 딜레이나 버벅임 없이 사용자에게 보이게 되는 것이죠. 하지만 *double buffering을 사용함으로써 화면에 디스플레이되고 있는 와중에 다음에 보일 두 개의 프레임을 병렬로 처리하고 있기 때문에 실질적으로는 16ms 보다는 시간적 여유를 가져갈 수 있게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*경우에 따라 hitch를 방지하기 위해 tripple buffering을 사용하기도 합니다. (hitch는 아래에서 설명드립니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt; 각&amp;nbsp;단계별&amp;nbsp;설명&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* App&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;*&amp;nbsp;Event:&amp;nbsp;사용자의&amp;nbsp;터치,&amp;nbsp;네트워크&amp;nbsp;콜백,&amp;nbsp;키보드&amp;nbsp;등의&amp;nbsp;이벤트를&amp;nbsp;처리하고&amp;nbsp;변경이&amp;nbsp;필요한&amp;nbsp;UI를&amp;nbsp;결정&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;*&amp;nbsp;Commit:&amp;nbsp;바뀌어야&amp;nbsp;하는&amp;nbsp;UI를&amp;nbsp;계산하고&amp;nbsp;랜더링을&amp;nbsp;위해&amp;nbsp;Renser&amp;nbsp;Server로&amp;nbsp;제출&lt;br /&gt;*&amp;nbsp;Render&amp;nbsp;Server&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;*&amp;nbsp;Render&amp;nbsp;prepare:&amp;nbsp;다음&amp;nbsp;VSYNC에서&amp;nbsp;제출본을&amp;nbsp;받고&amp;nbsp;새로운&amp;nbsp;UI를&amp;nbsp;GPU를&amp;nbsp;통해&amp;nbsp;랜더링 할&amp;nbsp;준비를&amp;nbsp;함&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;*&amp;nbsp;Render&amp;nbsp;execute:&amp;nbsp;GPU를&amp;nbsp;사용해&amp;nbsp;UI를&amp;nbsp;최종&amp;nbsp;이미지로&amp;nbsp;그림&lt;br /&gt;*&amp;nbsp;On&amp;nbsp;the&amp;nbsp;display&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;*&amp;nbsp;Display:&amp;nbsp;다음&amp;nbsp;VSYNC에서&amp;nbsp;사용자에게&amp;nbsp;최종&amp;nbsp;이미지인&amp;nbsp;frame을&amp;nbsp;디스플레이함&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App - Event Phase&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-17 오후 10.01.22.png&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJFd1V/btsnK8vJFv3/Q3rHW7jkJcHvSKPDg569t0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJFd1V/btsnK8vJFv3/Q3rHW7jkJcHvSKPDg569t0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJFd1V/btsnK8vJFv3/Q3rHW7jkJcHvSKPDg569t0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJFd1V%2FbtsnK8vJFv3%2FQ3rHW7jkJcHvSKPDg569t0%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;630&quot; height=&quot;186&quot; data-filename=&quot;스크린샷 2023-07-17 오후 10.01.22.png&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;452&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-17 오후 10.01.36.png&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVRGY7/btsnOst9aLT/jtJ9al0aVjhkyVqdskPkVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVRGY7/btsnOst9aLT/jtJ9al0aVjhkyVqdskPkVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVRGY7/btsnOst9aLT/jtJ9al0aVjhkyVqdskPkVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVRGY7%2FbtsnOst9aLT%2FjtJ9al0aVjhkyVqdskPkVk%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;630&quot; height=&quot;190&quot; data-filename=&quot;스크린샷 2023-07-17 오후 10.01.36.png&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;456&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-17 오후 10.01.50.png&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cs1wtS/btsnZCCdZy6/4bQBm3gz4XgeXvLhDesx21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cs1wtS/btsnZCCdZy6/4bQBm3gz4XgeXvLhDesx21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cs1wtS/btsnZCCdZy6/4bQBm3gz4XgeXvLhDesx21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcs1wtS%2FbtsnZCCdZy6%2F4bQBm3gz4XgeXvLhDesx21%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;630&quot; height=&quot;444&quot; data-filename=&quot;스크린샷 2023-07-17 오후 10.01.50.png&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;444&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;App - Commit Phase&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-17 오후 10.02.19.png&quot; data-origin-width=&quot;1360&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/COFuQ/btsnOf2qQsI/t8BgjRKBhLFwDQVQTfMFx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/COFuQ/btsnOf2qQsI/t8BgjRKBhLFwDQVQTfMFx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/COFuQ/btsnOf2qQsI/t8BgjRKBhLFwDQVQTfMFx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCOFuQ%2FbtsnOf2qQsI%2Ft8BgjRKBhLFwDQVQTfMFx1%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;630&quot; height=&quot;468&quot; data-filename=&quot;스크린샷 2023-07-17 오후 10.02.19.png&quot; data-origin-width=&quot;1360&quot; data-origin-height=&quot;468&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;Render Server - Rendering prepare phase&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-17 오후 10.03.03.png&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Dpnyv/btsn0FLTrYP/i3Azfiv93FcGkQoEwXS440/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Dpnyv/btsn0FLTrYP/i3Azfiv93FcGkQoEwXS440/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Dpnyv/btsn0FLTrYP/i3Azfiv93FcGkQoEwXS440/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDpnyv%2Fbtsn0FLTrYP%2Fi3Azfiv93FcGkQoEwXS440%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;630&quot; height=&quot;476&quot; data-filename=&quot;스크린샷 2023-07-17 오후 10.03.03.png&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;476&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;Render Server - Rendering execute phase&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-17 오후 10.03.41.png&quot; data-origin-width=&quot;1086&quot; data-origin-height=&quot;466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kHLFU/btsnLaf2yK8/bZdr5UhwM3p9iaoJM8CM8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kHLFU/btsnLaf2yK8/bZdr5UhwM3p9iaoJM8CM8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kHLFU/btsnLaf2yK8/bZdr5UhwM3p9iaoJM8CM8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkHLFU%2FbtsnLaf2yK8%2FbZdr5UhwM3p9iaoJM8CM8K%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;630&quot; height=&quot;466&quot; data-filename=&quot;스크린샷 2023-07-17 오후 10.03.41.png&quot; data-origin-width=&quot;1086&quot; data-origin-height=&quot;466&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;h3 data-ke-size=&quot;size23&quot;&gt;Hitch&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hitch는 프레임이 스크린에 제때 나타나지 않은 시간 (늦게 나타나는 시간)을 의미합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Hitch iOS.png&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M7Hpk/btsnZJOMzl7/ZqA701KDg6tb74HphsvVz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M7Hpk/btsnZJOMzl7/ZqA701KDg6tb74HphsvVz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M7Hpk/btsnZJOMzl7/ZqA701KDg6tb74HphsvVz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM7Hpk%2FbtsnZJOMzl7%2FZqA701KDg6tb74HphsvVz0%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;630&quot; height=&quot;290&quot; data-filename=&quot;Hitch iOS.png&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;원인에 따라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Commit hitch와 Render hitch 두 가지 타입으로 구분되는데요 자세히 살펴보면 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.&amp;nbsp;Commit&amp;nbsp;hitch&lt;br /&gt;앱 프로세스 단계에서 발생하는 경우 (== Event, Commit phase에서 오랜 시간이 걸리는 경우)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-17 오후 10.09.32.png&quot; data-origin-width=&quot;1528&quot; data-origin-height=&quot;542&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHW5ff/btsnOgmJfFK/UF8UIpoXHbKS6twAufyhZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHW5ff/btsnOgmJfFK/UF8UIpoXHbKS6twAufyhZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHW5ff/btsnOgmJfFK/UF8UIpoXHbKS6twAufyhZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHW5ff%2FbtsnOgmJfFK%2FUF8UIpoXHbKS6twAufyhZ0%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;1528&quot; height=&quot;542&quot; data-filename=&quot;스크린샷 2023-07-17 오후 10.09.32.png&quot; data-origin-width=&quot;1528&quot; data-origin-height=&quot;542&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-17 오후 10.09.50.png&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;568&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bf67iU/btsn13lqlq8/IXS8mmos0PGADnj34A1z9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bf67iU/btsn13lqlq8/IXS8mmos0PGADnj34A1z9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bf67iU/btsn13lqlq8/IXS8mmos0PGADnj34A1z9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbf67iU%2Fbtsn13lqlq8%2FIXS8mmos0PGADnj34A1z9K%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;1530&quot; height=&quot;568&quot; data-filename=&quot;스크린샷 2023-07-17 오후 10.09.50.png&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;568&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App 단계에서 delay가 되면, Render Server는 처리할 process가 없게 되고 다음 VSYNC를 기다리게 됩니다. 이에 따라 frame을 delivery 할 시간 역시 delay 되고 이 delay 된 시간을 &amp;ldquo;hitch time&amp;rdquo;이라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번&amp;nbsp;케이스&amp;nbsp;hitch&amp;nbsp;time:&amp;nbsp;16.67ms&lt;br /&gt;2번&amp;nbsp;케이스&amp;nbsp;hitch&amp;nbsp;time:&amp;nbsp;33.34ms&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.&amp;nbsp;Render&amp;nbsp;hitch&lt;br /&gt;랜더&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Render hitch.png&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/or6dP/btsnZw3f4jB/TVo89tb7qqdhtihZGLCVyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/or6dP/btsnZw3f4jB/TVo89tb7qqdhtihZGLCVyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/or6dP/btsnZw3f4jB/TVo89tb7qqdhtihZGLCVyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2For6dP%2FbtsnZw3f4jB%2FTVo89tb7qqdhtihZGLCVyK%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;1516&quot; height=&quot;530&quot; data-filename=&quot;Render hitch.png&quot; data-origin-width=&quot;1516&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;랜더 서버에서의 프로세스가 오랜 시간 소요되는 경우, 다음 VSYNC에 display 하게되고 이 역시 마찬가지로 hitch time이 발생하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hitch time: 16.67ms&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;Hitch 발생 유무 확인과 해결 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Instruments Animation Hitches를 사용하면 Render Loop을 시각화하여 볼 수 있고 hitching frame을 파악하는데 도움을 받을 수 있는데요. 이를 토대로 수집한 정보를 가지고 hitch를 개선 / 제거할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고) Instruments 실행 방법: Xcode에서 Cmd + I 로 프로파일 혹은 앱 실행 후 스포트라이트 검색으로 Instruments 별도 실행&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-17 오후 10.30.07.png&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UwAoD/btsn25J34Bg/KreSpPibFwKLRk3yUhLF91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UwAoD/btsn25J34Bg/KreSpPibFwKLRk3yUhLF91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UwAoD/btsn25J34Bg/KreSpPibFwKLRk3yUhLF91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUwAoD%2Fbtsn25J34Bg%2FKreSpPibFwKLRk3yUhLF91%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;1914&quot; height=&quot;1080&quot; data-filename=&quot;스크린샷 2023-07-17 오후 10.30.07.png&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Animation Hitches를 사용하여 Recording 하고 앱 구동 후 recording을 마치면 위와 같은 화면을 볼 수 있고 Render Loop 페이즈별 응답 트랙, Hitch duration, Acceptable latency, Hitch Type 등을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Animation hitches.png&quot; data-origin-width=&quot;1624&quot; data-origin-height=&quot;906&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNuH2B/btsnYZYWG5B/vUskHYLaQWGhm9O3SUFAj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNuH2B/btsnYZYWG5B/vUskHYLaQWGhm9O3SUFAj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNuH2B/btsnYZYWG5B/vUskHYLaQWGhm9O3SUFAj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNuH2B%2FbtsnYZYWG5B%2FvUskHYLaQWGhm9O3SUFAj0%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;1624&quot; height=&quot;906&quot; data-filename=&quot;Animation hitches.png&quot; data-origin-width=&quot;1624&quot; data-origin-height=&quot;906&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Hitches 트랙: hitch와 hitch duration 정보 표시&lt;/li&gt;
&lt;li&gt;User Events 트랙: Hitching frame과 함께 수신된 이벤트 표시&lt;/li&gt;
&lt;li&gt;commits 트랙: commit phase 표시&lt;/li&gt;
&lt;li&gt;Frame Lifetimes 트랙: 히칭 프레임의 lifetime 표시&lt;/li&gt;
&lt;li&gt;Built-In Display 트랙: VSYNC 이벤트와 디스플레이에 나타나는 모든 프레임 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hitches 트랙 하단에 표시되는 Hitch들을 보면 아래와 같이 Acceptable latency 칼럼을 확인할 수 있는데요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-17 오후 11.06.58.png&quot; data-origin-width=&quot;1610&quot; data-origin-height=&quot;728&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMpaOl/btsn0ElWamf/AZlRbgt3mO9rwn0nAWiM7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMpaOl/btsn0ElWamf/AZlRbgt3mO9rwn0nAWiM7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMpaOl/btsn0ElWamf/AZlRbgt3mO9rwn0nAWiM7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMpaOl%2Fbtsn0ElWamf%2FAZlRbgt3mO9rwn0nAWiM7K%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;1610&quot; height=&quot;728&quot; data-filename=&quot;스크린샷 2023-07-17 오후 11.06.58.png&quot; data-origin-width=&quot;1610&quot; data-origin-height=&quot;728&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Acceptable latency란 '허용 가능한 대기 시간'으로 Frame Lifetime 시작 시간 ~ Hitch Duration 시작 시간을 의미합니다. (그 이후의 시간은 hitch duration, 아래 이미지 참고)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-17 오후 10.49.44.png&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;902&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbcgU4/btsnZuYCNuG/KZKygAfnhiTffwjHbIdhfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbcgU4/btsnZuYCNuG/KZKygAfnhiTffwjHbIdhfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbcgU4/btsnZuYCNuG/KZKygAfnhiTffwjHbIdhfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbcgU4%2FbtsnZuYCNuG%2FKZKygAfnhiTffwjHbIdhfk%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;1634&quot; height=&quot;902&quot; data-filename=&quot;스크린샷 2023-07-17 오후 10.49.44.png&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;902&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;iOS Acceptable latency.png&quot; data-origin-width=&quot;1628&quot; data-origin-height=&quot;912&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vQOgP/btsnS0D6wNI/rhFcFNK1qDsGeseYg036kK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vQOgP/btsnS0D6wNI/rhFcFNK1qDsGeseYg036kK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vQOgP/btsnS0D6wNI/rhFcFNK1qDsGeseYg036kK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvQOgP%2FbtsnS0D6wNI%2FrhFcFNK1qDsGeseYg036kK%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;1628&quot; height=&quot;912&quot; data-filename=&quot;iOS Acceptable latency.png&quot; data-origin-width=&quot;1628&quot; data-origin-height=&quot;912&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수집된 데이터를 토대로 결과를 분석해 보면, 먼저 Hitch가 얼마나 발생하고 있는지 시각적으로 파악이 가능하고, Hitch duration을 통해 그 심각성을 판단할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hitch Type 칼럼을 통해서는 왜 Hitch가 발생하는지 원인에 대한 대략적인 힌트를 얻을 수 있는데요. 조금 더 자세히 들여다보기 위해 아래와 같이 해당 Hitch가 발생하는 구간에 대해서만 드래깅 하여 focusing 할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-17 오후 11.09.51.png&quot; data-origin-width=&quot;3008&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fnyp9/btsnZJaa0b4/acsdMno5O4JxtQKUKGKkxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fnyp9/btsnZJaa0b4/acsdMno5O4JxtQKUKGKkxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fnyp9/btsnZJaa0b4/acsdMno5O4JxtQKUKGKkxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFnyp9%2FbtsnZJaa0b4%2FacsdMno5O4JxtQKUKGKkxk%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;3008&quot; height=&quot;800&quot; data-filename=&quot;스크린샷 2023-07-17 오후 11.09.51.png&quot; data-origin-width=&quot;3008&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Animation Hitches에서는 Time Profiler도 포함하기 때문에 Hitch가 발생하는 해당 구간에서 main thread를 포함한 각각의 thread가 어떤 코드를 처리하고 있는지도 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Time profiler stack trace.png&quot; data-origin-width=&quot;1628&quot; data-origin-height=&quot;916&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdBPZk/btsn3j9oGZU/hpI19ju9easu1qkkgHwtu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdBPZk/btsn3j9oGZU/hpI19ju9easu1qkkgHwtu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdBPZk/btsn3j9oGZU/hpI19ju9easu1qkkgHwtu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdBPZk%2Fbtsn3j9oGZU%2FhpI19ju9easu1qkkgHwtu0%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;1628&quot; height=&quot;916&quot; data-filename=&quot;Time profiler stack trace.png&quot; data-origin-width=&quot;1628&quot; data-origin-height=&quot;916&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 해당 구간에서의 main thread stack trace를 확인해 보면, main thread에서 weight이 큰 로직이 무엇인지 확인할 수 있고, 이에 따라 어떤 부분을 개선하면 좋을지 힌트를 얻을 수 있습니다. 여기서 한 가지 Tip을 드리자면, 아래와 같이 Call Tree 체크 박스를 설정하면 디버깅이 조금 더 수월해집니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-17 오후 11.18.48.png&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buLZnU/btsnZJONRGi/pw6jVU5OyN5Ohq2z5X3BuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buLZnU/btsnZJONRGi/pw6jVU5OyN5Ohq2z5X3BuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buLZnU/btsnZJONRGi/pw6jVU5OyN5Ohq2z5X3BuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuLZnU%2FbtsnZJONRGi%2Fpw6jVU5OyN5Ohq2z5X3BuK%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;385&quot; height=&quot;232&quot; data-filename=&quot;스크린샷 2023-07-17 오후 11.18.48.png&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;350&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 Commit 트랙과, Hitch duration, Hitch Type, Time Profiler를 확인해 보면 어느 단계에서 이슈가 발생하고 있는 건지, 어떤 코드가 문제가 되는 건지 파악 할 수 있고, 이 정보들을 토대로 원인을 제거, 개선하고 다시 테스트하는 식으로 반복하다 보면 끊기거나 delay 되지 않는 화면, 버벅임 없는 화면을 사용자에게 제공할 수 있게 됩니다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/videos/play/tech-talks/10855/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.apple.com/videos/play/tech-talks/10855/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1689596914092&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Explore UI animation hitches and the render loop - Tech Talks - Videos - Apple Developer&quot; data-og-description=&quot;Explore how you can improve the performance of your app's user interface by identifying scrolling and animation hitches in your app...&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/videos/play/tech-talks/10855/&quot; data-og-url=&quot;https://developer.apple.com/videos/play/tech-talks/10855/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/KNAVj/hyTk24PMK6/fswMc86mqIkvcw54M1KCr1/img.jpg?width=500&amp;amp;height=282&amp;amp;face=0_0_500_282&quot;&gt;&lt;a href=&quot;https://developer.apple.com/videos/play/tech-talks/10855/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/videos/play/tech-talks/10855/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/KNAVj/hyTk24PMK6/fswMc86mqIkvcw54M1KCr1/img.jpg?width=500&amp;amp;height=282&amp;amp;face=0_0_500_282');&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;Explore UI animation hitches and the render loop - Tech Talks - Videos - Apple Developer&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Explore how you can improve the performance of your app's user interface by identifying scrolling and animation hitches in your app...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/videos/play/tech-talks/10856/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.apple.com/videos/play/tech-talks/10856/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1689596925090&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Find and fix hitches in the commit phase - Tech Talks - Videos - Apple Developer&quot; data-og-description=&quot;Discover how to render smoother animations in your app by troubleshooting the commit phase of your render loop. Dive into the mechanics...&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/videos/play/tech-talks/10856/&quot; data-og-url=&quot;https://developer.apple.com/videos/play/tech-talks/10856/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/emlVJe/hyTmurR73M/JPMKrmKKQY9PQbzY6vHzT0/img.jpg?width=500&amp;amp;height=282&amp;amp;face=0_0_500_282&quot;&gt;&lt;a href=&quot;https://developer.apple.com/videos/play/tech-talks/10856/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/videos/play/tech-talks/10856/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/emlVJe/hyTmurR73M/JPMKrmKKQY9PQbzY6vHzT0/img.jpg?width=500&amp;amp;height=282&amp;amp;face=0_0_500_282');&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;Find and fix hitches in the commit phase - Tech Talks - Videos - Apple Developer&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Discover how to render smoother animations in your app by troubleshooting the commit phase of your render loop. Dive into the mechanics...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Swift&amp;amp;iOS/iOS</category>
      <category>Animation Hitches</category>
      <category>hang</category>
      <category>Instruments</category>
      <category>ios</category>
      <category>rendering</category>
      <category>Swift</category>
      <category>ui</category>
      <category>버벅임</category>
      <category>성능</category>
      <category>화면 갱신</category>
      <author>lsh424</author>
      <guid isPermaLink="true">https://lsh424.tistory.com/96</guid>
      <comments>https://lsh424.tistory.com/96#entry96comment</comments>
      <pubDate>Mon, 17 Jul 2023 23:50:53 +0900</pubDate>
    </item>
    <item>
      <title>모바일 개발에서의 Clean Architecture</title>
      <link>https://lsh424.tistory.com/95</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;클린 아키텍처?&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;Robert C Martin이 제시한 소프트웨어 아키텍처로 &lt;span style=&quot;text-align: start;&quot;&gt;계층별 역할(관심사)을 분리함으로써 유지보수, 테스트 용이성의 이점을 가져갈 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;클린 아키텍처 다이어그램.png&quot; data-origin-width=&quot;1558&quot; data-origin-height=&quot;1114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daVKk8/btsfdaVY5CG/ttWKnEaIkyglxDkT02klzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daVKk8/btsfdaVY5CG/ttWKnEaIkyglxDkT02klzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daVKk8/btsfdaVY5CG/ttWKnEaIkyglxDkT02klzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdaVKk8%2FbtsfdaVY5CG%2FttWKnEaIkyglxDkT02klzk%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;1558&quot; height=&quot;1114&quot; data-filename=&quot;클린 아키텍처 다이어그램.png&quot; data-origin-width=&quot;1558&quot; data-origin-height=&quot;1114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;위 이미지를 보면 의존성의 경우 바깥쪽에서 안쪽으로 향하게 되는데, 이 의미는 안쪽 계층의 경우 바깥 계층에 대해 알 수 없다는 의미(영향을 받지 않는다는 의미)이고 안쪽으로 갈 수록 의존성이 옅어짐으로 변경 가능성이 가장 적은 비즈니스 룰이 위치하게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;여기서 비즈니스 룰이란 Domain Layer(Use Cases + Entities)에 적용되는 규칙으로 &lt;span style=&quot;text-align: start;&quot;&gt;사업의 핵심적인 서비스와 관련된, 즉 잘 변하지 않는 비즈니스 로직을 적용하는 것을 말한다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;예를 들어 일기 앱 서비스라고 가정한다면 일기 작성, 삭제등의 기능이 있을 수 있다. 이 로직들은 서비스의 핵심적인 로직이고 미래에도 잘 변하지 않게될 로직이다. 그렇기 때문에 &lt;span style=&quot;text-align: start;&quot;&gt;개발팀이 아닌 타 부서 팀원에게 &lt;span style=&quot;text-align: start;&quot;&gt;UseCase 리스트를 보여주었을 때 해당 도메인이 무슨 일들을 수행하게 되는건지 이해할 수 있어야 하는 로직들이 들어가게 된다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&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;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;[Presenters &amp;amp; Controllers] &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;앱 개발(MVVM)에서 UI, ViewModels 이 포함되는 계층&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;ex)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1684049881717&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final class DefaultMoviesListViewModel: MoviesListViewModel {

    private let searchMoviesUseCase: SearchMoviesUseCase
    private let actions: MoviesListViewModelActions?

    var currentPage: Int = 0
    var totalPageCount: Int = 1
    var hasMorePages: Bool { currentPage &amp;lt; totalPageCount }
    var nextPage: Int { hasMorePages ? currentPage + 1 : currentPage }

    private var pages: [MoviesPage] = []
    private var moviesLoadTask: Cancellable? { willSet { moviesLoadTask?.cancel() } }
    private let mainQueue: DispatchQueueType
    
    ...
    
    생략 
    
    ...
    
    private func load(movieQuery: MovieQuery, loading: MoviesListViewModelLoading) {
        self.loading.value = loading
        query.value = movieQuery.query

        moviesLoadTask = searchMoviesUseCase.execute(
            requestValue: .init(query: movieQuery, page: nextPage),
            cached: { [weak self] page in
                self?.mainQueue.async {
                    self?.appendPage(page)
                }
            },
            completion: { [weak self] result in
                self?.mainQueue.async {
                    switch result {
                    case .success(let page):
                        self?.appendPage(page)
                    case .failure(let error):
                        self?.handle(error: error)
                    }
                    self?.loading.value = .none
                }
        })
    }
}&lt;/code&gt;&lt;/pre&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;[UseCases] &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;서비스 요구사항 (애플리케이션 고유 비즈니스 규칙)을 포함,&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;아래의 예시에서는 '영화 검색'에 대한 비즈니스 로직 실행&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;ex)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1684049939707&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final class DefaultSearchMoviesUseCase: SearchMoviesUseCase {

    private let moviesRepository: MoviesRepository
    private let moviesQueriesRepository: MoviesQueriesRepository

    init(
        moviesRepository: MoviesRepository,
        moviesQueriesRepository: MoviesQueriesRepository
    ) {

        self.moviesRepository = moviesRepository
        self.moviesQueriesRepository = moviesQueriesRepository
    }

    func execute(
        requestValue: SearchMoviesUseCaseRequestValue,
        cached: @escaping (MoviesPage) -&amp;gt; Void,
        completion: @escaping (Result&amp;lt;MoviesPage, Error&amp;gt;) -&amp;gt; Void
    ) -&amp;gt; Cancellable? {

        return moviesRepository.fetchMoviesList(
            query: requestValue.query,
            page: requestValue.page,
            cached: cached,
            completion: { result in

            if case .success = result {
                self.moviesQueriesRepository.saveRecentQuery(query: requestValue.query) { _ in }
            }

            completion(result)
        })
    }
}&lt;/code&gt;&lt;/pre&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #ffffff; text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;[Entities] &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #ffffff; text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;비즈니스 모델&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000; background-color: #ffffff;&quot;&gt;ex)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1684050013265&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct Movie: Equatable, Identifiable {
    typealias Identifier = String
    enum Genre {
        case adventure
        case scienceFiction
    }
    let id: Identifier
    let title: String?
    let genre: Genre?
    let posterPath: String?
    let overview: String?
    let releaseDate: Date?
}

struct MoviesPage: Equatable {
    let page: Int
    let totalPages: Int
    let movies: [Movie]
}&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;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;# 예시에 사용된 코드는 아래 프로젝트를 인용했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://github.com/kudoleh/iOS-Clean-Architecture-MVVM&quot;&gt;https://github.com/kudoleh/iOS-Clean-Architecture-MVVM&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1684050046120&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 - kudoleh/iOS-Clean-Architecture-MVVM: Template iOS app using Clean Architecture and MVVM. Includes DIContainer, FlowCoor&quot; data-og-description=&quot;Template iOS app using Clean Architecture and MVVM. Includes DIContainer, FlowCoordinator, DTO, Response Caching and one of the views in SwiftUI - GitHub - kudoleh/iOS-Clean-Architecture-MVVM: Tem...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/kudoleh/iOS-Clean-Architecture-MVVM&quot; data-og-url=&quot;https://github.com/kudoleh/iOS-Clean-Architecture-MVVM&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cQCTlH/hySBuuiZfN/kT1HFHcawePQz3dcbFK4m0/img.png?width=1200&amp;amp;height=600&amp;amp;face=995_121_1038_167&quot;&gt;&lt;a href=&quot;https://github.com/kudoleh/iOS-Clean-Architecture-MVVM&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/kudoleh/iOS-Clean-Architecture-MVVM&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cQCTlH/hySBuuiZfN/kT1HFHcawePQz3dcbFK4m0/img.png?width=1200&amp;amp;height=600&amp;amp;face=995_121_1038_167');&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 - kudoleh/iOS-Clean-Architecture-MVVM: Template iOS app using Clean Architecture and MVVM. Includes DIContainer, FlowCoor&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Template iOS app using Clean Architecture and MVVM. Includes DIContainer, FlowCoordinator, DTO, Response Caching and one of the views in SwiftUI - GitHub - kudoleh/iOS-Clean-Architecture-MVVM: Tem...&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;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;각각의 계층에 대해 알아봤으므로, 그 안쪽 원들에 대한 의존성을 조금 더 구체적으로 나타내면 아래와 같이 나타낼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;클린 아키텍처 dependency rule.png&quot; data-origin-width=&quot;1136&quot; data-origin-height=&quot;1088&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NEpct/btsfOJn5trl/Ok6hk23rav2qMykpyLgFw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NEpct/btsfOJn5trl/Ok6hk23rav2qMykpyLgFw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NEpct/btsfOJn5trl/Ok6hk23rav2qMykpyLgFw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNEpct%2FbtsfOJn5trl%2FOk6hk23rav2qMykpyLgFw0%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;1136&quot; height=&quot;1088&quot; data-filename=&quot;클린 아키텍처 dependency rule.png&quot; data-origin-width=&quot;1136&quot; data-origin-height=&quot;1088&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;의존성 방향의 경우 Presentation Layer -&amp;gt; Domain Layer -&amp;gt; Data Repositories Layer 방향이 되지만,&amp;nbsp;Repositories의 경우 Interface(protocol)를 사용해 의존성을 역전시킨다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;역전 시킨 결과:&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Presentation Layer -&amp;gt; Domain Layer &amp;lt;- Data Repositories Layer&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;결국&amp;nbsp;Domain Layer는 의존성이 없는 Layer가 되고 그에 따라 &lt;span style=&quot;text-align: start;&quot;&gt;사업의 핵심적인 서비스가 변경되지 않는 이상 변경될 가능성이 거의 없는 Layer가 된다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;/span&gt;&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;이러한 구조를 기반으로, 특정 이벤트에 대해 다음과 같은 순서로 로직이 수행되게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;특정 이벤트 발생&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Presenter(ViewModel)은 이벤트에 따라 UseCase를 실행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;UseCase는 하나 혹은 다수의 Repository를 결합, 실행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Repository는 API Service(Network Request), DB로 부터 데이터 fetch &amp;amp; 반환&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;UI 업데이트: Repository -&amp;gt; UseCase -&amp;gt; ViewModel -&amp;gt; View(UI)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Usecase와 Repository 네이밍 룰&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;네이밍 룰은 말 그대로 규칙이기 때문에 프로젝트 별로 다르게 적용될 수 있는 부분이고 정답이라고 할 것 이 없지만, 클린 아키텍처 도입 시 많은 Usecase와 Repository 네이밍을 해야 하는 만큼 참고할 만한 네이밍 규칙이 있어서 소개하고자 한다. (아래의 규칙은&lt;/span&gt; &lt;a href=&quot;https://developer.android.com/jetpack/guide/domain-layer?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;구글 안드로이드 개발자 가이드&lt;/a&gt;&lt;span style=&quot;color: #333333;&quot;&gt;를 참고 했습니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;[UseCase 네이밍 규칙]&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;현재 시제의 동사&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&amp;nbsp;+&amp;nbsp;&lt;/span&gt;명사/대상(선택사항)&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&amp;nbsp;+&amp;nbsp;&lt;/span&gt;UseCase&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #202124;&quot;&gt;&lt;span style=&quot;caret-color: #202124; background-color: #ffffff;&quot;&gt;ex) FormatDataUseCase, LogOutUserUseCase, GetLatestNewsUseCase, MakeLoginRequestUseCase 등..&lt;/span&gt;&lt;/span&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;color: #333333;&quot;&gt;[Repository 네이밍 규칙]&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;데이터 유형&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&amp;nbsp;+ Repository&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;ex)&amp;nbsp;NewsRepository&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;,&amp;nbsp;&lt;/span&gt;MoviesRepository, PaymentsRepository 등..&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;클린 아키텍처 장단점&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이렇게 계층을 분리함으로써 생기는 장단점들이 있는데, 장단점은 아래와 같다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;장점&lt;/span&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;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;패키지와&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;폴더&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;구성이&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;계층별로&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;일목요연한&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;트리구조를&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;이루기&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;때문에&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;소스코드&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;전반에&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;대한&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;파악이&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;용이&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;특정&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;계층에&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;대한&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;수정이&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;다른&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;계층에&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;영향을&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;거의&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;주지&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;않음&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; -&amp;gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;기능&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;추가&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;수정&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;용이&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;프로젝트&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;유지보수&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;용이&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;UseCase&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;를&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;사용함으로써 ViewModel이 비대해지는 것을 일부 개선할 수 있음&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;각&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;기능을&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;명확하게&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;나누게&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;됨, 각 기능에 대한 테스트 용이&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;단점&lt;/span&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;span style=&quot;color: #333333;&quot;&gt;아키텍처에 대한 이해 필요&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;많은 부가적인 class가 추가될 수 있음&lt;/span&gt;&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: #ffffff; color: #333333; text-align: left;&quot;&gt;이러한 장, 단점 때문에 거의 모든 도메인 로직이 백엔드 서버에 있어서 딱히 비즈니스 로직이랄 것이 없는 경우 다른 아키텍처가 더 나을 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;실제 프로젝트에 적용하면서 생겼던 의문점과 느낀 점들&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #202124;&quot;&gt;&lt;span style=&quot;caret-color: #202124; background-color: #ffffff;&quot;&gt;의문 1.&amp;nbsp;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;Usecase의 역할&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #202124;&quot;&gt;&lt;span style=&quot;caret-color: #202124; background-color: #ffffff;&quot;&gt;클린 아키텍처를 학습하고, 실제로 프로젝트에 적용해 보면서 들었던 의문 중 하나는 Domain Layer의 Usecase의 역할이었다. 사용하면서 느낀 건, &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;Usecase의 역할이 단순히 Repository를 실행(래핑) 하는 역할 말고는 하는 역할이 없는 것처럼 보였다는 것이다. 그러다 보니 &quot;크게 필요 없는 Layer 아닌가?&quot;, &quot;뷰모델에서 직접 Repository를 사용해도 될 것 같은데?&quot;라는 생각이 들기도 했는데. 이와 관련해 아래의 글들이 큰 도움이 되어서 소개하고자 한다!&lt;/span&gt;&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;color: #202124;&quot;&gt;&lt;span style=&quot;caret-color: #202124; background-color: #ffffff;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;&lt;a href=&quot;https://medium.com/@justfaceit/clean-architecture는-모바일-개발을-어떻게-도와주는가-1-경계선-계층을-정의해준다-b77496744616&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://medium.com/@justfaceit/clean-architecture는-모바일-개발을-어떻게-도와주는가-1-경계선-계층을-정의해준다-b77496744616&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1684044690453&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Clean Architecture는 모바일 개발을 어떻게 도와주는가? - (1) 경계선: 계층 나누기&quot; data-og-description=&quot;How Clean Architecture Assists Mobile Development - Part 1. Boudaries: Defining Layers&quot; data-og-host=&quot;medium.com&quot; data-og-source-url=&quot;https://medium.com/@justfaceit/clean-architecture는-모바일-개발을-어떻게-도와주는가-1-경계선-계층을-정의해준다-b77496744616&quot; data-og-url=&quot;https://medium.com/@justfaceit/clean-architecture%EB%8A%94-%EB%AA%A8%EB%B0%94%EC%9D%BC-%EA%B0%9C%EB%B0%9C%EC%9D%84-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%84%EC%99%80%EC%A3%BC%EB%8A%94%EA%B0%80-1-%EA%B2%BD%EA%B3%84%EC%84%A0-%EA%B3%84%EC%B8%B5%EC%9D%84-%EC%A0%95%EC%9D%98%ED%95%B4%EC%A4%80%EB%8B%A4-b77496744616&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/uysE7/hySBvs7MJZ/LiPVypa4PBxTuCTow1OHak/img.jpg?width=1000&amp;amp;height=664&amp;amp;face=0_0_1000_664,https://scrap.kakaocdn.net/dn/dVsYUI/hySBzhZQ2o/He5k2kSpkaUhGYeItF6je1/img.jpg?width=1358&amp;amp;height=1019&amp;amp;face=0_0_1358_1019,https://scrap.kakaocdn.net/dn/bLTXTL/hySBJLIOQZ/UnS3dMfOsL2pxTsWmrUonk/img.jpg?width=1358&amp;amp;height=849&amp;amp;face=0_0_1358_849&quot;&gt;&lt;a href=&quot;https://medium.com/@justfaceit/clean-architecture는-모바일-개발을-어떻게-도와주는가-1-경계선-계층을-정의해준다-b77496744616&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://medium.com/@justfaceit/clean-architecture는-모바일-개발을-어떻게-도와주는가-1-경계선-계층을-정의해준다-b77496744616&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/uysE7/hySBvs7MJZ/LiPVypa4PBxTuCTow1OHak/img.jpg?width=1000&amp;amp;height=664&amp;amp;face=0_0_1000_664,https://scrap.kakaocdn.net/dn/dVsYUI/hySBzhZQ2o/He5k2kSpkaUhGYeItF6je1/img.jpg?width=1358&amp;amp;height=1019&amp;amp;face=0_0_1358_1019,https://scrap.kakaocdn.net/dn/bLTXTL/hySBJLIOQZ/UnS3dMfOsL2pxTsWmrUonk/img.jpg?width=1358&amp;amp;height=849&amp;amp;face=0_0_1358_849');&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;Clean Architecture는 모바일 개발을 어떻게 도와주는가? - (1) 경계선: 계층 나누기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;How Clean Architecture Assists Mobile Development - Part 1. Boudaries: Defining Layers&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;medium.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://heegs.tistory.com/58&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://heegs.tistory.com/58&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1684066473634&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Android] Clean Architecture - UseCase 란 ?&quot; data-og-description=&quot;처음 학습하면서 작성한 글입니다. 필요시 추후 내용을 수정할 예정입니다. 틀린 부분이 있으면 언제든 지적해주면 감사하겠습니다 :) Clean Architecture 를 공부하는 도중에, UseCase 라는 것을 domain l&quot; data-og-host=&quot;heegs.tistory.com&quot; data-og-source-url=&quot;https://heegs.tistory.com/58&quot; data-og-url=&quot;https://heegs.tistory.com/58&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/AQOzL/hySBG2FJd5/n73NBcqtV0nCaqimTfcoi1/img.png?width=576&amp;amp;height=436&amp;amp;face=0_0_576_436,https://scrap.kakaocdn.net/dn/grjqo/hySBAnQ9cm/kCXS3bJUTL3Gd79gbq3Je0/img.png?width=576&amp;amp;height=436&amp;amp;face=0_0_576_436,https://scrap.kakaocdn.net/dn/b0sg5B/hySByKlXFr/q6fw5KX9psCJ0BtbUhZOa1/img.png?width=1180&amp;amp;height=884&amp;amp;face=0_0_1180_884&quot;&gt;&lt;a href=&quot;https://heegs.tistory.com/58&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://heegs.tistory.com/58&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/AQOzL/hySBG2FJd5/n73NBcqtV0nCaqimTfcoi1/img.png?width=576&amp;amp;height=436&amp;amp;face=0_0_576_436,https://scrap.kakaocdn.net/dn/grjqo/hySBAnQ9cm/kCXS3bJUTL3Gd79gbq3Je0/img.png?width=576&amp;amp;height=436&amp;amp;face=0_0_576_436,https://scrap.kakaocdn.net/dn/b0sg5B/hySByKlXFr/q6fw5KX9psCJ0BtbUhZOa1/img.png?width=1180&amp;amp;height=884&amp;amp;face=0_0_1180_884');&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;[Android] Clean Architecture - UseCase 란 ?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;처음 학습하면서 작성한 글입니다. 필요시 추후 내용을 수정할 예정입니다. 틀린 부분이 있으면 언제든 지적해주면 감사하겠습니다 :) Clean Architecture 를 공부하는 도중에, UseCase 라는 것을 domain l&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;heegs.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;위 글들을 종합해서 정리해 보자면, 정답은 없지만 &amp;nbsp;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;Usecase의 역할과 장점들을 생각해 보고 자신의 프로젝트 condition과 어디에 더 가치를 두는지에 맞춰서 &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;Usecase 생략 유무를 결정하면 된다! 이다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;Usecase의 장점들은 아래와 같은데, 만약 &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;Usecase가 &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;비즈니스 로직이 거의 없어 &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;presentation Layer와 Data Layer 간의 중계 역할밖에 하지 못하고 &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;Usecase가 주는 장점을 크게 체감하지 못하겠다는 생각이 되면 UseCase를 생략하고 ViewModel에 통합시켜도 괜찮다고 생각한다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;[&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;Usecase의 장점]&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;1. Usecase를 사용하는 ViewModel이 어떤 것을 하고자 하는지 직관적으로 파악이 가능해진다. (해당 도메인이 무슨 일을 하는지 쉽게 파악 가능하기 때문)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;2. 코드 중복을 방지하고, 책임을 분할하여 뷰모델이 비대해지는 것을 방지할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;3. 재사용이 용이하고, 변경에 쉽게 대처 가능하다.&amp;nbsp;&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;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #202124;&quot;&gt;&lt;span style=&quot;caret-color: #202124; background-color: #ffffff;&quot;&gt;의문 2. 다수의 Presenter(ViewModel)에서 사용되는 공통 로직이 있을 때 이 로직을 Usecase로 빼도 괜찮을까?&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;color: #202124;&quot;&gt;&lt;span style=&quot;caret-color: #202124; background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이 부분 역시, 정답이 있는 것은 아니지만 구글 안드로이드 개발&lt;/span&gt;자 &lt;a href=&quot;https://github.com/saryongkang&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;@saryongkang&lt;/a&gt; &lt;span style=&quot;color: #333333;&quot;&gt;님의 생각을 빌리자면, 다음과 같이 말하고 있다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;앱의 특성상 각 프리젠터에서 자주 사용되는 공통의 로직이 꽤 발생하는 경우가 있습니다. 이 경우, 엄밀히 얘기하면 도메인 로직이라고 할 수는 없지만, ViewUseCase 형태의 클래스로 분리해서 프레젠테이션 계층에 추가하는 것은 좋은 방법이라고 생각합니다.&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;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;의문 3.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;Usecase가 다른 Usecase를 포함해도 될까?&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #202124;&quot;&gt;&lt;span style=&quot;caret-color: #202124; background-color: #ffffff;&quot;&gt;전혀 문제 될 게 없다. 왜 이런 의문을 가졌었는지는 정확히 기억나지 않지만(아마&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #202124;&quot;&gt;&lt;span style=&quot;caret-color: #202124; background-color: #ffffff;&quot;&gt;과녁 이미지에서 화살표가 안쪽으로 향하는 dependency 때문에 그런 것 같다.) &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;Usecase가 다른 Usecase를 의존하는 데 있어 문제 될 만한 것은 없어 보인다.&lt;/span&gt;&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;color: #202124;&quot;&gt;&lt;span style=&quot;caret-color: #202124; background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;실제로&lt;/span&gt; &lt;a href=&quot;https://developer.android.com/jetpack/guide/domain-layer?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;구글 안드로이드 개발자 가이드 문서&lt;/a&gt;&lt;span style=&quot;color: #333333;&quot;&gt;를 보면 아래와 같은 사용 사례를 소개하고 있다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Usecase는 재사용 가능한 로직을 포함하기 때문에 다른 Usecase에 의해 사용될 수도 있습니다. Domain Layer에 여러&amp;nbsp;Usecase가 있는 것은 정상입니다. 예를 들어 아래 예에 정의된 Usecase는 UI 레이어의 여러 Class가 시간대를 사용하여 화면에 적절한 메시지를 표시하는 경우&amp;nbsp;FormatDateUseCase를 사용할 수 있습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&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;스크린샷 2023-05-14 오후 8.52.03.png&quot; data-origin-width=&quot;1766&quot; data-origin-height=&quot;912&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rVDB0/btsfe8Dj0pi/I7sjZIFZ0cWMgrbXusXmD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rVDB0/btsfe8Dj0pi/I7sjZIFZ0cWMgrbXusXmD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rVDB0/btsfe8Dj0pi/I7sjZIFZ0cWMgrbXusXmD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrVDB0%2Fbtsfe8Dj0pi%2FI7sjZIFZ0cWMgrbXusXmD0%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;1766&quot; height=&quot;912&quot; data-filename=&quot;스크린샷 2023-05-14 오후 8.52.03.png&quot; data-origin-width=&quot;1766&quot; data-origin-height=&quot;912&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>공부</category>
      <category>Android</category>
      <category>Architecture</category>
      <category>Clean</category>
      <category>ios</category>
      <category>JetPack</category>
      <category>Swift</category>
      <category>모바일</category>
      <category>앱 개발</category>
      <category>클린 아키텍처</category>
      <author>lsh424</author>
      <guid isPermaLink="true">https://lsh424.tistory.com/95</guid>
      <comments>https://lsh424.tistory.com/95#entry95comment</comments>
      <pubDate>Sun, 14 May 2023 21:49:30 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] Unit Test</title>
      <link>https://lsh424.tistory.com/94</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;UnitTest?&lt;/b&gt;&lt;/h3&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;gt;&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;gt;&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;테스트 코드 작성 방법&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수(비즈니스 로직)에 대한 input case와 output case들을 정의하고 테스트를 통과하는지 검증하는 테스트 코드를 작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 input에 대해서는 동일한 output이 보장되어야만 함&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 alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-29 오후 11.25.44.png&quot; data-origin-width=&quot;1084&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QLtnu/btr6OZHs2wQ/xjmYCfq7tJ2SkBShShptQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QLtnu/btr6OZHs2wQ/xjmYCfq7tJ2SkBShShptQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QLtnu/btr6OZHs2wQ/xjmYCfq7tJ2SkBShShptQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQLtnu%2Fbtr6OZHs2wQ%2FxjmYCfq7tJ2SkBShShptQ0%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;211&quot; data-filename=&quot;스크린샷 2023-03-29 오후 11.25.44.png&quot; data-origin-width=&quot;1084&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tests file로 가보시면, 아래와 같이 처음 보실 수도 있는(?) 여러 메서드가 선언되어 있는데요. 각각 어떤 목적을 갖는 함수들인지는 밑에서 살펴보는 걸로 하고! 우리의 목적은 테스트 함수 작성이기 때문에 테스트 함수를 작성해 볼 겁니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;iOS unitTest.png&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;574&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRyadd/btr6PzICdNd/P8rySrkaDbVgwiWUB85650/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRyadd/btr6PzICdNd/P8rySrkaDbVgwiWUB85650/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRyadd/btr6PzICdNd/P8rySrkaDbVgwiWUB85650/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRyadd%2Fbtr6PzICdNd%2FP8rySrkaDbVgwiWUB85650%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;601&quot; height=&quot;268&quot; data-filename=&quot;iOS unitTest.png&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;574&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 함수는 무조건 test로 시작해야 합니다. test로 시작하는 함수를 만들면 위 이미지 행 라인 쪽 숫자대신 다이아몬드 버튼으로 바뀌게 되는데, 테스트 실행은 command + U로 전체 유닛 테스트를 진행할 수도 있고 저 다이아몬드 버튼을 눌러서 개별 비즈니스 로직을 테스트할 수도 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 돌아와서, 뷰모델의 두 비즈니스 로직 add와 minus가 문제 없는지를 테스트하기 위해 테스트 코드를 작성합니다. 테스트 코드는 given, when, then이라고 하는 구조적 유닛테스트 작성 방법이 있는데, 간단히&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;given: dependencies(주어지는 환경)&lt;/li&gt;
&lt;li&gt;when: 테스트의 동작&lt;/li&gt;
&lt;li&gt;then: 테스트 결과&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 이해할 수 있습니다.&amp;nbsp;만약 우리가 작성한 덧셈 뺄셈 테스트의 경우에는 아래처럼 볼 수 있겠죠?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;given: 두 개의 숫 자가 주어지고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;when: 두 숫 자를 더하면(빼면)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;then: &lt;span&gt;두&lt;/span&gt; &lt;span&gt;숫자를&lt;/span&gt; &lt;span&gt;더한&lt;/span&gt; &lt;span&gt;값(뺀 값)이&lt;/span&gt; return 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 작성하면 아래처럼 작성할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680100926123&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final class CalculatorTests: XCTestCase {
    let calculatorViewModel = CalculatorViewModel()
    
    override func setUpWithError() throws { }

    override func tearDownWithError() throws { }

    func testExample() throws { }

    func testPerformanceExample() throws {
        // This is an example of a performance test case.
        self.measure {
            // Put the code you want to measure the time of here.
        }
    }
    
    func testAdd() throws {
        // given
        let inputs: [(lhs: Int, rhs: Int)] = [(1, 1), (1, 2), (10, 5), (10, 10), (99, 1)]
        let outputs = [2, 3, 15, 20, 100]

        // when
        inputs.enumerated().forEach { idx, inputs in
            let result = calculatorViewModel.add(lhs: inputs.lhs, rhs: inputs.rhs)

            // then
            XCTAssertEqual(result, outputs[idx])
        }
    }

    func testMinus() throws {
        // given
        let inputs: [(lhs: Int, rhs: Int)] = [(5, 2), (10, 3), (9, 5), (10, 10), (100, 1)]
        let outputs = [3, 7, 4, 0, 99]

        // when
        inputs.enumerated().forEach { idx, inputs in
            let result = calculatorViewModel.minus(lhs: inputs.lhs, rhs: inputs.rhs)

            // then
            XCTAssertEqual(result, outputs[idx])
        }
    }
}&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;XCTest framework는 다양한 assertion들을 제공해 주는데&amp;nbsp;XCTAssertEqual은 그중 하나입니다. 아래 문서에서. 확인할 수 있듯이 두 개의 값이 같은지 아닌지 판단하고 같으면 테스트를 pass하게 되고 그렇지 않다면 fail이 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/xctest/xctassertequal&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.apple.com/documentation/xctest/xctassertequal&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 작성을 다 마친 후 command + U를 눌러서 테스트를 실행해 보면, 아래와 같이 초록색 체크박스로 테스트를 통과하는 것을 확인할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-29 오후 11.43.29.png&quot; data-origin-width=&quot;1660&quot; data-origin-height=&quot;1176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/duG88t/btr6NYvCpYU/YxqE2VF5Vm4bbCYYRBdPc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/duG88t/btr6NYvCpYU/YxqE2VF5Vm4bbCYYRBdPc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/duG88t/btr6NYvCpYU/YxqE2VF5Vm4bbCYYRBdPc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FduG88t%2Fbtr6NYvCpYU%2FYxqE2VF5Vm4bbCYYRBdPc1%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;597&quot; height=&quot;423&quot; data-filename=&quot;스크린샷 2023-03-29 오후 11.43.29.png&quot; data-origin-width=&quot;1660&quot; data-origin-height=&quot;1176&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 testMinus의 outputs의 맨 마지막을 100으로 바꾼다면..? 계산 값 99와 결괏값 100은 서로 다르니 아래처럼 테스트에 실패하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;XCTAssertEqual.png&quot; data-origin-width=&quot;1656&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvwx03/btr6JoIrh7W/46VaLE80xkYG6J12H6wo60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvwx03/btr6JoIrh7W/46VaLE80xkYG6J12H6wo60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvwx03/btr6JoIrh7W/46VaLE80xkYG6J12H6wo60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbvwx03%2Fbtr6JoIrh7W%2F46VaLE80xkYG6J12H6wo60%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;599&quot; height=&quot;209&quot; data-filename=&quot;XCTAssertEqual.png&quot; data-origin-width=&quot;1656&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실, add와 minus 함수의 경우 로직이라고 말할게 없어서 테스트가 필요할까 싶지만, 이건 설명을 위해서 그렇고, 보통의 경우에는 비즈니스 로직이 간단하지 않은 경우가 많기 때문에 테스트코드 작성이 의미 있습니다.&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;setUpWithError()는 테스트가 실행되기&amp;nbsp;전&amp;nbsp;실행되는 메서드로 테스트&amp;nbsp;케이스를&amp;nbsp;실행하기&amp;nbsp;전&amp;nbsp;상태값&amp;nbsp;리셋등의 초기화&amp;nbsp;코드를 작성하게 됩니다. 우리가 일반적으로 init에 작성하는 코드를 작성한다고 생각하면 이해하기 쉽습니다.&lt;br /&gt;&lt;br /&gt;tearDownWithError()는 모든 테스트가 끝난 후 실행되기 때문에, 테스트 실행을 모두 마친 후 실행되어야 할 코드를 작성하는 곳 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Test Assertion의 경우 우리가 사용한 XCTAssertEqual 외에도 여러 Assertion을 제공해주고 있기 때문에 아래 문서에서 확인해 보시는 걸 추천드립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/xctest&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.apple.com/documentation/xctest&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 기본적인 몇 가지 Assertion들을 소개하자면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 값이 같은지 다른지를 판단하는&amp;nbsp;XCTAssertEqual, XCTAssertNotEqual&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 nil인지 아닌지 판단하는 XCTAssertNil, XCTAssertNotNil&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 큰지 작은지 판단하는 XCTAssertLessThan, XCTAssertGreaterThan&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;비동기 함수가 어떻게 작성되어 있느냐에 따라 테스트 방법이 조금 달라지게 되는데, 일반적인 completon Handler를 사용한 비동기 함수 라면 아래처럼 테스트해 볼 수 있습니다. (아래 코드는 wwdc 2021 async/await 영상에 나오는 코드 일부를 캡처했습니다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;wwdc async await.png&quot; data-origin-width=&quot;2862&quot; data-origin-height=&quot;708&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CX3AU/btr6QTNvWsw/s44Ya3dZGQypCcgECPz0t1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CX3AU/btr6QTNvWsw/s44Ya3dZGQypCcgECPz0t1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CX3AU/btr6QTNvWsw/s44Ya3dZGQypCcgECPz0t1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCX3AU%2Fbtr6QTNvWsw%2Fs44Ya3dZGQypCcgECPz0t1%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;2862&quot; height=&quot;708&quot; data-filename=&quot;wwdc async await.png&quot; data-origin-width=&quot;2862&quot; data-origin-height=&quot;708&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 테스트가 통과하려면 두 가지를 만족해야 합니다. 5초 안에 비동기 함수의 응답이 들어와야 하고, 들어온 응답의 결과의 사이즈가 서로 같아야만 하죠.&amp;nbsp;5초 안에 비동기 함수 응답이 들어오지 않아도 테스트는 실패이고 사이즈가 같지 않아도 실패입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 fetchThumbnail 함수가 Swift Concurrency를 사용한 비동기 함수라면 아래처럼 훨씬 더 간단하게 작성 가능 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-30 오전 12.23.47.png&quot; data-origin-width=&quot;2856&quot; data-origin-height=&quot;710&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WAu6z/btr6K3qGy7N/6GNl5PUXxVoYeQLGkAK1S0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WAu6z/btr6K3qGy7N/6GNl5PUXxVoYeQLGkAK1S0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WAu6z/btr6K3qGy7N/6GNl5PUXxVoYeQLGkAK1S0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWAu6z%2Fbtr6K3qGy7N%2F6GNl5PUXxVoYeQLGkAK1S0%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;2856&quot; height=&quot;710&quot; data-filename=&quot;스크린샷 2023-03-30 오전 12.23.47.png&quot; data-origin-width=&quot;2856&quot; data-origin-height=&quot;710&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;span&gt;Unit&amp;nbsp;Test를 이해하는데 많은 도움이 되었길 바라며..! 다음 포스팅으로 찾아오겠습니다 :)&amp;nbsp;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Swift&amp;amp;iOS/iOS</category>
      <category>ios</category>
      <category>Swift</category>
      <category>UnitTest</category>
      <category>WWDC</category>
      <category>XCTest</category>
      <category>유닛테스트</category>
      <author>lsh424</author>
      <guid isPermaLink="true">https://lsh424.tistory.com/94</guid>
      <comments>https://lsh424.tistory.com/94#entry94comment</comments>
      <pubDate>Thu, 30 Mar 2023 00:34:20 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] SwiftUI 뷰와 레이아웃 시스템에 대한 이해 (feat. WWDC)</title>
      <link>https://lsh424.tistory.com/93</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://lsh424.tistory.com/92&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Swift&amp;amp;iOS/iOS] - [iOS] SwiftUI 학습 순서 (feat. Tutorial &amp;amp; WWDC)&lt;/a&gt;에서 SwiftUI Essentials(WWDC 2019)과 Building Custom Views with SwiftUI(WWDC 2019) 세션을 보는 걸 추천한다고 했었는데, 이 포스팅은 이 세션들에 대한 정리 내용입니다 ㅎㅎ (일부 WWDC에 나오지 않는 내용들도 포함 되어 있습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;세션 링크&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/videos/play/wwdc2019/216&quot;&gt;https://developer.apple.com/videos/play/wwdc2019/216&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1669534516155&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;SwiftUI Essentials - WWDC19 - Videos - Apple Developer&quot; data-og-description=&quot;Take your first deep-dive into building an app with SwiftUI. Learn about Views and how they work. From basic controls to sophisticated...&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/videos/play/wwdc2019/216&quot; data-og-url=&quot;https://developer.apple.com/videos/play/wwdc2019/216&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bqLQBu/hyQH6hlmHY/OgJt5SCczJEOJsXNmgACuk/img.jpg?width=500&amp;amp;height=282&amp;amp;face=0_0_500_282&quot;&gt;&lt;a href=&quot;https://developer.apple.com/videos/play/wwdc2019/216&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/videos/play/wwdc2019/216&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bqLQBu/hyQH6hlmHY/OgJt5SCczJEOJsXNmgACuk/img.jpg?width=500&amp;amp;height=282&amp;amp;face=0_0_500_282');&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;SwiftUI Essentials - WWDC19 - Videos - Apple Developer&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Take your first deep-dive into building an app with SwiftUI. Learn about Views and how they work. From basic controls to sophisticated...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://developer.apple.com/videos/play/wwdc2019/237&quot;&gt;https://developer.apple.com/videos/play/wwdc2019/237&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1669534525554&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Building Custom Views with SwiftUI - WWDC19 - Videos - Apple Developer&quot; data-og-description=&quot;Learn how to build custom views and controls in SwiftUI with advanced composition, layout, graphics, and animation. See a demo of a high...&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/videos/play/wwdc2019/237&quot; data-og-url=&quot;https://developer.apple.com/videos/play/wwdc2019/237&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c3IfqD/hyQH3dP2Gl/buYM8pmhjDIKb5DknWc6X1/img.jpg?width=500&amp;amp;height=282&amp;amp;face=0_0_500_282&quot;&gt;&lt;a href=&quot;https://developer.apple.com/videos/play/wwdc2019/237&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/videos/play/wwdc2019/237&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c3IfqD/hyQH3dP2Gl/buYM8pmhjDIKb5DknWc6X1/img.jpg?width=500&amp;amp;height=282&amp;amp;face=0_0_500_282');&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;Building Custom Views with SwiftUI - WWDC19 - Videos - Apple Developer&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Learn how to build custom views and controls in SwiftUI with advanced composition, layout, graphics, and animation. See a demo of a high...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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: #333333;&quot;&gt;우리가 보는 앱의 화면은 모두 뷰로 구성되어 있습니다. 사용하는&amp;nbsp;프레임워크에 따라 다르겠지만 (UIKit은 UIView, AppKit은 NSView, SwiftUI는 View) 어쨌든, 모든 view는 piece of UI입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그럼 UIKit과 SwiftUI의 차이점은 뭘까요?&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;두 프레임워크를 사용한 코드를 봤을 때 가장 먼저 눈에 띄는 건,&amp;nbsp;UIKit의 경우 명시적으로(&lt;span style=&quot;background-color: #ffffff;&quot;&gt;imperatively 하게) 동작한다는 점이고 SwiftUI의 경우 선언적으로(&lt;span style=&quot;background-color: #ffffff;&quot;&gt;declaratively 하게) 동작한다는점 입니다.&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;color: #333333;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;View의 입장에서 보면 어떨까요?&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&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: #ffffff; color: #333333;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;UIKit의 경우 커스텀 뷰를 만들려면, UIView를 상속받아야 합니다. 그러다 보니 필요하지도 않은(사용하지 않을) UIView의 common property들에 대한 Storage를 갖게 됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ViewsInUIKit.png&quot; data-origin-width=&quot;2748&quot; data-origin-height=&quot;1446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BXFKE/btrR9MCZxJS/kzXMu7a56qre4KymVqEg3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BXFKE/btrR9MCZxJS/kzXMu7a56qre4KymVqEg3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BXFKE/btrR9MCZxJS/kzXMu7a56qre4KymVqEg3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBXFKE%2FbtrR9MCZxJS%2FkzXMu7a56qre4KymVqEg3k%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;2748&quot; height=&quot;1446&quot; data-filename=&quot;ViewsInUIKit.png&quot; data-origin-width=&quot;2748&quot; data-origin-height=&quot;1446&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;caret-color: #212529; background-color: #ffffff;&quot;&gt;반면, SwiftUI View는 프로토콜 타입이고, 커스텀 뷰를 만들기 위해 상속을 받을 필요가 없습니다.&lt;/span&gt;&lt;span style=&quot;caret-color: #212529; background-color: #ffffff;&quot;&gt; 뷰에 적용하고자 하는 특성을&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;caret-color: #212529; background-color: #ffffff;&quot;&gt;modifier를 사용해 적용하고 modifier는 고유한 뷰를 반환합니다. 그렇기 때문에&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;caret-color: #212529; background-color: #ffffff;&quot;&gt;뷰의 고유 특성에 대해서만 storage를 갖게 되고, 그 결과 매우 가볍고 효율적입니다. (UIView 상속처럼 공통 탬플릿을 제공할 필요가 없음)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;View Protocol_Swift.png&quot; data-origin-width=&quot;2770&quot; data-origin-height=&quot;1422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GyL8Z/btrTlsIIIrx/COWgoDwO9b1akyriN1e931/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GyL8Z/btrTlsIIIrx/COWgoDwO9b1akyriN1e931/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GyL8Z/btrTlsIIIrx/COWgoDwO9b1akyriN1e931/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGyL8Z%2FbtrTlsIIIrx%2FCOWgoDwO9b1akyriN1e931%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;2770&quot; height=&quot;1422&quot; data-filename=&quot;View Protocol_Swift.png&quot; data-origin-width=&quot;2770&quot; data-origin-height=&quot;1422&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;caret-color: #212529; background-color: #ffffff;&quot;&gt;그럼, View Protocol의 내부를 살펴볼까요?&lt;/span&gt;&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;934&quot; data-origin-height=&quot;366&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKNx6e/btrTig3jUgK/zT4G4DOeKdxRfNAuY8oLtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKNx6e/btrTig3jUgK/zT4G4DOeKdxRfNAuY8oLtk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKNx6e/btrTig3jUgK/zT4G4DOeKdxRfNAuY8oLtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKNx6e%2FbtrTig3jUgK%2FzT4G4DOeKdxRfNAuY8oLtk%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;934&quot; height=&quot;366&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;366&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;caret-color: #212529; background-color: #ffffff;&quot;&gt;연산 프로퍼티 body를 보면 자기 자신인 View를 반환하는 것을 볼 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;caret-color: #212529; background-color: #ffffff;&quot;&gt;어?, View의 바디가 또 자기 자신인 View를 반환 하니까 reculsive(재귀) 아니야?..&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;View body reculsive.png&quot; data-origin-width=&quot;2828&quot; data-origin-height=&quot;1240&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dNciYR/btrTjEJsyZs/zioaL6rfgDcIJoxmlnSep0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dNciYR/btrTjEJsyZs/zioaL6rfgDcIJoxmlnSep0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dNciYR/btrTjEJsyZs/zioaL6rfgDcIJoxmlnSep0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdNciYR%2FbtrTjEJsyZs%2FzioaL6rfgDcIJoxmlnSep0%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;2828&quot; height=&quot;1240&quot; data-filename=&quot;View body reculsive.png&quot; data-origin-width=&quot;2828&quot; data-origin-height=&quot;1240&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;caret-color: #212529; background-color: #ffffff;&quot;&gt;맞습니다만..! 우리도 프로그램 짤 때 재귀를 사용할 경우 재귀를 종료할 수 있는 조건을 넣어 놓잖아요?&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;caret-color: #212529; background-color: #ffffff;&quot;&gt;SwiftUI View 역시 마찬가지입니다. SwiftUI의 Primitive Views라 불리는 뷰들은 Body 타입이 View가 아닌 Never 타입이고 이 Never를 만나게 되면 재귀가 종료 되게 됩니다. &lt;span style=&quot;background-color: #ffffff;&quot;&gt;Primitive Views에는 아래와 같이 Text, Image, Color 등이 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Swift_PrimitiveViews.png&quot; data-origin-width=&quot;2778&quot; data-origin-height=&quot;1316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AWUm5/btrTjF2FANf/cxNqu1pO3URYKBVIqW1hKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AWUm5/btrTjF2FANf/cxNqu1pO3URYKBVIqW1hKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AWUm5/btrTjF2FANf/cxNqu1pO3URYKBVIqW1hKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAWUm5%2FbtrTjF2FANf%2FcxNqu1pO3URYKBVIqW1hKk%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;2778&quot; height=&quot;1316&quot; data-filename=&quot;Swift_PrimitiveViews.png&quot; data-origin-width=&quot;2778&quot; data-origin-height=&quot;1316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;caret-color: #212529; background-color: #ffffff;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;Primitive Views 중 하나인&amp;nbsp;&lt;/span&gt;Text의 body &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1670675342037&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;extension Text : View {
    public typealias Body = Never
}&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: #333333;&quot;&gt;이제 SwiftUI Layout System에 대해 알아볼 텐데요. SwiftUI Layout System을 이해하기 전에 알아야 할 지식들이 있습니다.&lt;/span&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;span style=&quot;color: #333333;&quot;&gt;모든 레이아웃의 기본 정렬은 Center&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Root View는 기본적으로 SafeArea를 제외한 영역&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;거의 대부분의 modifier, View들은 layout neutral 하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;layout neutral? -&amp;gt; 레이아웃에 대한 결정권이 없기 때문에 하위 뷰의 bounds를 따라감&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Text, Image 등 몇몇 뷰는 고유한 크기를 갖는다. (텍스트 및 이미지에 맞춰 고유한 크기를 갖게 됨, UIKit에서도 AutoLayout 제약조건을 설정할 때 label 같은 특정 뷰들의 경우 너비와 높이에 대한 제약 조건을 주지 않고 위치만 잡아주어도 문제 없었죠? 그 이유는 label 텍스트의 고유한 사이즈에 그 크기가 맞춰지기 때문입니다. SwiftUI에서 역시 Text, Image처럼 고유한 크기를 갖고 있는 뷰는 그 크기에 맞춰 사이즈가 설정됩니다.)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;위 지식을 토대로 간단한 예제를 살펴보면서 레이아웃 시스템이 어떻게 동작 하나 알아봅시다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&quot;Hello World&quot;라는 Text 뷰의 경우 레이아웃의 계층구조를 아래 이미지와 같이 나타낼 수 있는데요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;여기서 Root View, ContentView, Text의 사이즈는 각각 어떻게 될까요?&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-12-11 오후 3.58.26.png&quot; data-origin-width=&quot;2360&quot; data-origin-height=&quot;1316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcSZOm/btrTgImAgbY/h9rUUC6t2VzhxkQc1FW2G0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcSZOm/btrTgImAgbY/h9rUUC6t2VzhxkQc1FW2G0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcSZOm/btrTgImAgbY/h9rUUC6t2VzhxkQc1FW2G0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcSZOm%2FbtrTgImAgbY%2Fh9rUUC6t2VzhxkQc1FW2G0%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;2360&quot; height=&quot;1316&quot; data-filename=&quot;스크린샷 2022-12-11 오후 3.58.26.png&quot; data-origin-width=&quot;2360&quot; data-origin-height=&quot;1316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;레이아웃 프로세스는 아래와 같이 세 단계로 나타낼 수 있습니다.&lt;/span&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;span style=&quot;color: #333333;&quot;&gt;부모 뷰가 자식 뷰에게 사이즈를 제안&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;자식 뷰는 부모 뷰가 제안한 사이즈를 받아들이거나 자신만의 사이즈를 결정&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;부모 뷰는 자식 뷰를 자신의 좌표 공간 내에 자식 뷰를 배치&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;ContentView의 경우 body와 동일한 bounds를 갖기 때문에 단순히 RootView와 Text만을 놓고 생각해 본다면&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;RootView는 레이아웃 프로세스에 근거해, SafeArea만큼의 영역을 Text에게 제안할 겁니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-12-13 오전 12.47.45.png&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zFpRs/btrTwByzc4y/4qR2e9NRIM8vqpKyVa8Ra1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zFpRs/btrTwByzc4y/4qR2e9NRIM8vqpKyVa8Ra1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zFpRs/btrTwByzc4y/4qR2e9NRIM8vqpKyVa8Ra1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzFpRs%2FbtrTwByzc4y%2F4qR2e9NRIM8vqpKyVa8Ra1%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;1312&quot; height=&quot;734&quot; data-filename=&quot;스크린샷 2022-12-13 오전 12.47.45.png&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;하지만 Text는 자신만의 사이즈를 갖고 있는 애죠?, Root View에 제안한 사이즈를 사용하지 않고 내 사이즈를 사용하겠다고 전달합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-12-13 오전 12.50.15.png&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;916&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8h0aE/btrToEc5EaE/5hrdXtZdVsgKUSdR7KemvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8h0aE/btrToEc5EaE/5hrdXtZdVsgKUSdR7KemvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8h0aE/btrToEc5EaE/5hrdXtZdVsgKUSdR7KemvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8h0aE%2FbtrToEc5EaE%2F5hrdXtZdVsgKUSdR7KemvK%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;1634&quot; height=&quot;916&quot; data-filename=&quot;스크린샷 2022-12-13 오전 12.50.15.png&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;916&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Root View는 자식 뷰의 사이즈를 존중하여 받아들이고 해당 뷰를 자신의 좌표 공간 내에 배치합니다. (기본적인 alignment는 center이기 때문에 가운데에 배치합니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-12-13 오전 12.52.35.png&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;916&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uxtdf/btrTtF26zWZ/QlVew4f8ljflsWiMpWCVrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uxtdf/btrTtF26zWZ/QlVew4f8ljflsWiMpWCVrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uxtdf/btrTtF26zWZ/QlVew4f8ljflsWiMpWCVrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fuxtdf%2FbtrTtF26zWZ%2FQlVew4f8ljflsWiMpWCVrK%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;1630&quot; height=&quot;916&quot; data-filename=&quot;스크린샷 2022-12-13 오전 12.52.35.png&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;916&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;즉, Text와 ContentView의 사이즈는 같고, 이 ContentView가 SafeArea 영역 가운데에 배치되는 것이죠.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;조~~금 더 복잡한 레이아웃을 살펴보죠&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이제 Text에 padding과 background modifier가 추가된 뷰입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-12-13 오전 1.00.51.png&quot; data-origin-width=&quot;1322&quot; data-origin-height=&quot;738&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6DefB/btrTsdlKnNU/78AFJTCiZc9lUltRTsumFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6DefB/btrTsdlKnNU/78AFJTCiZc9lUltRTsumFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6DefB/btrTsdlKnNU/78AFJTCiZc9lUltRTsumFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6DefB%2FbtrTsdlKnNU%2F78AFJTCiZc9lUltRTsumFk%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;1322&quot; height=&quot;738&quot; data-filename=&quot;스크린샷 2022-12-13 오전 1.00.51.png&quot; data-origin-width=&quot;1322&quot; data-origin-height=&quot;738&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;레이아웃 계층의 경우 위와 같이 나타낼 수 있는데요. 마찬가지로 Root View는 아래와 같이 Background에게 SafeArea만큼의 영역을 제안합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Delicious Avocado Toast.png&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;738&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjZI4j/btrTvXuXVzE/t88ObudykXqczpdTBKkbTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjZI4j/btrTvXuXVzE/t88ObudykXqczpdTBKkbTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjZI4j/btrTvXuXVzE/t88ObudykXqczpdTBKkbTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjZI4j%2FbtrTvXuXVzE%2Ft88ObudykXqczpdTBKkbTk%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;1316&quot; height=&quot;738&quot; data-filename=&quot;Delicious Avocado Toast.png&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;738&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Background의 경우 Layout neutral 하기 때문에 전달받은 사이즈를 그대로 Padding 뷰에 전달합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-12-13 오전 1.08.16.png&quot; data-origin-width=&quot;1318&quot; data-origin-height=&quot;744&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwfah6/btrTtCk5qMK/z9OLRdhh8KgfT1LKsYQic1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwfah6/btrTtCk5qMK/z9OLRdhh8KgfT1LKsYQic1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwfah6/btrTtCk5qMK/z9OLRdhh8KgfT1LKsYQic1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbwfah6%2FbtrTtCk5qMK%2Fz9OLRdhh8KgfT1LKsYQic1%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;1318&quot; height=&quot;744&quot; data-filename=&quot;스크린샷 2022-12-13 오전 1.08.16.png&quot; data-origin-width=&quot;1318&quot; data-origin-height=&quot;744&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Padding 뷰는 자식 뷰 side에 10 point씩 더할 것을 알고 있기 때문에, 상하좌우 10포인트씩 적은 영역을 Text에 전달합니다. (이렇게 줄어든 영역을 전달하는 이유는, 줄어든 영역을 기준으로 레이아웃이 잡혀야 패딩을 넣을 공간이 생기기 때문입니다. 만약 Avocado Toast 텍스트가 10 point씩 줄어든 레이아웃이 아닌 RootView 레이아웃 기준으로 그려지고 그 너비가 같다면 패딩을 넣을 공간이 없잖아요? 즉, 애초에 패딩 넣을 공간을 남겨두고 영역을 전달한다고 보시면 됩니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-12-13 오전 1.12.20.png&quot; data-origin-width=&quot;1322&quot; data-origin-height=&quot;736&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQ0M5J/btrTtgJhOGe/VJF38Mq43yJi9eUv5PUDSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQ0M5J/btrTtgJhOGe/VJF38Mq43yJi9eUv5PUDSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQ0M5J/btrTtgJhOGe/VJF38Mq43yJi9eUv5PUDSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQ0M5J%2FbtrTtgJhOGe%2FVJF38Mq43yJi9eUv5PUDSK%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;1322&quot; height=&quot;736&quot; data-filename=&quot;스크린샷 2022-12-13 오전 1.12.20.png&quot; data-origin-width=&quot;1322&quot; data-origin-height=&quot;736&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;참고) 만약 패딩을 극단적으로 크게 잡으면 어떻게 될까요? -&amp;gt; 자식에게 전달해줄 영역이 없기 때문에 Text는 보이지 않고 아래처럼 패딩만 남게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-12-13 오전 1.31.09.png&quot; data-origin-width=&quot;1694&quot; data-origin-height=&quot;1012&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHDdJd/btrTu2QPNRD/2thHRHPAxtKQug93TZuR10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHDdJd/btrTu2QPNRD/2thHRHPAxtKQug93TZuR10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHDdJd/btrTu2QPNRD/2thHRHPAxtKQug93TZuR10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHDdJd%2FbtrTu2QPNRD%2F2thHRHPAxtKQug93TZuR10%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;1694&quot; height=&quot;1012&quot; data-filename=&quot;스크린샷 2022-12-13 오전 1.31.09.png&quot; data-origin-width=&quot;1694&quot; data-origin-height=&quot;1012&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;아무튼~~ Text는 자신만의 Size를 갖고 있는 녀석이기 때문에, 해당 사이즈를 Padding View에 전달하고, Padding View는 전달받은 사이즈를 기준으로 상하좌우 10 point씩 더 큰 크기를 Background View에 전달합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-12-13 오전 1.29.39.png&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;914&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tsp1w/btrTsdzjeYK/KYHGdK2ririmyAcOyTU7ak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tsp1w/btrTsdzjeYK/KYHGdK2ririmyAcOyTU7ak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tsp1w/btrTsdzjeYK/KYHGdK2ririmyAcOyTU7ak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftsp1w%2FbtrTsdzjeYK%2FKYHGdK2ririmyAcOyTU7ak%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;1630&quot; height=&quot;914&quot; data-filename=&quot;스크린샷 2022-12-13 오전 1.29.39.png&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;914&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Background View의 경우 Layout Neutral 하기 때문에 자식 뷰의 크기를 그대로 따라가고 S&lt;span style=&quot;background-color: #ffffff;&quot;&gt;econdary child인 Color View에도 해당 크기를 제안합니다. Color 역시 Layout Neutral 하기 때문에 제안받은 사이즈를 그대로 사용합니다. (즉 Text 사이즈에 패딩 10 point씩 들어간 크기를 채택)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-12-13 오전 1.35.28.png&quot; data-origin-width=&quot;1356&quot; data-origin-height=&quot;750&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mwfpC/btrTqf5dE1W/UqnsJDQoU39bOfkbGMPV0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mwfpC/btrTqf5dE1W/UqnsJDQoU39bOfkbGMPV0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mwfpC/btrTqf5dE1W/UqnsJDQoU39bOfkbGMPV0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmwfpC%2FbtrTqf5dE1W%2FUqnsJDQoU39bOfkbGMPV0k%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;1356&quot; height=&quot;750&quot; data-filename=&quot;스크린샷 2022-12-13 오전 1.35.28.png&quot; data-origin-width=&quot;1356&quot; data-origin-height=&quot;750&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-12-13 오전 1.37.58.png&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;762&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nLkyX/btrTtCrOBFY/JazBuAXMOqx4mFMxIcohjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nLkyX/btrTtCrOBFY/JazBuAXMOqx4mFMxIcohjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nLkyX/btrTtCrOBFY/JazBuAXMOqx4mFMxIcohjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnLkyX%2FbtrTtCrOBFY%2FJazBuAXMOqx4mFMxIcohjk%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;1354&quot; height=&quot;762&quot; data-filename=&quot;스크린샷 2022-12-13 오전 1.37.58.png&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;762&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Background View는 Text 사이즈에 패딩이 상하좌우 10 Point 씩 들어간 크기를 Root View로 전달하고, Root View는 이 뷰를 SafeArea영역 가운데에 배치하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-12-13 오전 1.39.57.png&quot; data-origin-width=&quot;1318&quot; data-origin-height=&quot;740&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBfchV/btrTmZIstRO/K83sL1oMWdmBRdDGOt5JG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBfchV/btrTmZIstRO/K83sL1oMWdmBRdDGOt5JG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBfchV/btrTmZIstRO/K83sL1oMWdmBRdDGOt5JG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBfchV%2FbtrTmZIstRO%2FK83sL1oMWdmBRdDGOt5JG1%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;1318&quot; height=&quot;740&quot; data-filename=&quot;스크린샷 2022-12-13 오전 1.39.57.png&quot; data-origin-width=&quot;1318&quot; data-origin-height=&quot;740&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이제 SwiftUI Layout System이 어떻게 동작하는지 조금 감이 오시나요..?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;frame의 경우는 어떨까요? frame modifier도 결국 뷰입니다. AutoLayout에서의 제약조건이 아닌 액자의 개념으로 생각해야 하는데요.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;SwiftUI Frame.png&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EQoXy/btrTulpy5nU/SF3bqaDCCfi8veulyWdY91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EQoXy/btrTulpy5nU/SF3bqaDCCfi8veulyWdY91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EQoXy/btrTulpy5nU/SF3bqaDCCfi8veulyWdY91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEQoXy%2FbtrTulpy5nU%2FSF3bqaDCCfi8veulyWdY91%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;1430&quot; height=&quot;100&quot; data-filename=&quot;SwiftUI Frame.png&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;만약 20x20 사이즈의 아보카도 이미지가 있고, 30x30의 frame을 주었다면, 아래와 같이 30x30 액자 안에 20x20 아보카도 이미지가 들어있는 모습을 생각해야 합니다. (resizable modifier를 사용하지 않았을 경우)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-12-13 오전 1.46.07.png&quot; data-origin-width=&quot;1344&quot; data-origin-height=&quot;752&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bovDac/btrTthVMuWq/MY8Zb2hizW3p6hBgrCQE5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bovDac/btrTthVMuWq/MY8Zb2hizW3p6hBgrCQE5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bovDac/btrTthVMuWq/MY8Zb2hizW3p6hBgrCQE5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbovDac%2FbtrTthVMuWq%2FMY8Zb2hizW3p6hBgrCQE5k%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;1344&quot; height=&quot;752&quot; data-filename=&quot;스크린샷 2022-12-13 오전 1.46.07.png&quot; data-origin-width=&quot;1344&quot; data-origin-height=&quot;752&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Text 역시 frame과 별개로 자신만의 크기를 갖는 것을 확인할 수 있습니다. (SwiftUI Layout System을 이해하셨다면 왜 아래처럼 UI가 그려지는지 이해할 수 있을 거예요)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-12-13 오전 1.49.02.png&quot; data-origin-width=&quot;2226&quot; data-origin-height=&quot;1004&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8FLgX/btrTtX3xz6V/UF16qqGeVv6MwF28qfts01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8FLgX/btrTtX3xz6V/UF16qqGeVv6MwF28qfts01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8FLgX/btrTtX3xz6V/UF16qqGeVv6MwF28qfts01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8FLgX%2FbtrTtX3xz6V%2FUF16qqGeVv6MwF28qfts01%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;2226&quot; height=&quot;1004&quot; data-filename=&quot;스크린샷 2022-12-13 오전 1.49.02.png&quot; data-origin-width=&quot;2226&quot; data-origin-height=&quot;1004&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;UIKit AutoLayout에는 Layout Priority를 설정할 수 있었는데, SwiftUI도 그런게 있을까요? -&amp;gt; 네, 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Text, Image 등은 자신만의 고유한 사이즈를 갖고 있는 애들인데요, 만약 이 뷰들을 Horizontal 하게 여러 개 배치한다 가정했을 때 어떻게 표현을 해야 할까요? 분명 특정 뷰는 줄어들어야 할 텐데요..&amp;nbsp;이때&lt;/span&gt;&amp;nbsp;사용할 수 있는게 &lt;a href=&quot;https://developer.apple.com/documentation/swiftui/list/layoutpriority(_:)&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;layoutPriority&lt;/a&gt; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;아래처럼, layoutPriority가 높을수록 부모 뷰가 자식에게 공간을 할당하는 데 있어 우선권을 갖게 되고, layoutPriority가 같은 뷰들은 남은 공간을 서로 나눠 갖게 됩니다. (균등 분할하지만, 특정 뷰가 균등 분할한 공간보다 더 적게 사용하면, 남는 영역만큼 다른 뷰에게 줄 수 있습니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-12-13 오전 1.55.47.png&quot; data-origin-width=&quot;1314&quot; data-origin-height=&quot;732&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CCZOW/btrTu2pLgqw/yaoQb0T7T5leYrnQz0AJ81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CCZOW/btrTu2pLgqw/yaoQb0T7T5leYrnQz0AJ81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CCZOW/btrTu2pLgqw/yaoQb0T7T5leYrnQz0AJ81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCCZOW%2FbtrTu2pLgqw%2FyaoQb0T7T5leYrnQz0AJ81%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;1314&quot; height=&quot;732&quot; data-filename=&quot;스크린샷 2022-12-13 오전 1.55.47.png&quot; data-origin-width=&quot;1314&quot; data-origin-height=&quot;732&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;alignment(정렬)에서도 단순히 center, top, bottom 뿐만이 아니라 여러 옵션을 제공해주는데요. HStack을 기준으로는 firstTextBaseLine, lastTextBaseLine 등을 제공해줍니다. (alignmentGuide를 통해 세세한 조정도 가능)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-12-13 오전 2.04.04.png&quot; data-origin-width=&quot;1328&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdhj3T/btrTuBeMIcJ/hdyWeuAWKB750y4IWl12jK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdhj3T/btrTuBeMIcJ/hdyWeuAWKB750y4IWl12jK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdhj3T/btrTuBeMIcJ/hdyWeuAWKB750y4IWl12jK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcdhj3T%2FbtrTuBeMIcJ%2FhdyWeuAWKB750y4IWl12jK%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;1328&quot; height=&quot;734&quot; data-filename=&quot;스크린샷 2022-12-13 오전 2.04.04.png&quot; data-origin-width=&quot;1328&quot; data-origin-height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-12-13 오전 2.04.49.png&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2CJav/btrTqfKUK8b/hDkzI9BauiNoxkKfaIxkg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2CJav/btrTqfKUK8b/hDkzI9BauiNoxkKfaIxkg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2CJav/btrTqfKUK8b/hDkzI9BauiNoxkKfaIxkg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2CJav%2FbtrTqfKUK8b%2FhDkzI9BauiNoxkKfaIxkg0%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;1312&quot; height=&quot;734&quot; data-filename=&quot;스크린샷 2022-12-13 오전 2.04.49.png&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;마지막으로 frame에서의 idealSize(idealWidth &amp;amp; idealHeight)는 도대체 뭐고, fixedSize modifier의 역할은 뭘까요??&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;idealSize는 UIKit에서 Intrinsic Size와 같은 개념으로 볼 수 있습니다. 즉 본질적인 크기를 의미합니다. (UIKit에서 UILabel의 경우 높이 너비 등에 대한 레이아웃 제약을 따로 설정해주지 않으면 Text 크기(본질적인 크기)를 따라가는 것처럼)&amp;nbsp;즉, 부모 뷰의 공간과 관계없이 자신의 이상적인 크기를 &amp;nbsp;의미합니다. 의미 그대로 이상적인 크기이기 때문에 ideal Size는 보통&lt;/span&gt; &lt;a href=&quot;https://developer.apple.com/documentation/swiftui/view/fixedsize()&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;fixedSize&lt;/a&gt;&lt;span style=&quot;color: #333333;&quot;&gt;와 함께 씁니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;fixedSize는 애플 문서에도 나와있듯이, idealSize를 고정하는 데 사용되는 modifier입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-12-13 오전 2.18.39.png&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btOraY/btrTtAtXLoY/BGZobcbZy3rCD1x29ZXdQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btOraY/btrTtAtXLoY/BGZobcbZy3rCD1x29ZXdQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btOraY/btrTtAtXLoY/BGZobcbZy3rCD1x29ZXdQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtOraY%2FbtrTtAtXLoY%2FBGZobcbZy3rCD1x29ZXdQ0%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;1504&quot; height=&quot;412&quot; data-filename=&quot;스크린샷 2022-12-13 오전 2.18.39.png&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;412&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;만약 frame에서 idealSize만 설정한다면, 아래처럼 전혀 frame 적용이 안되는 것을 볼 수 있는데,&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-12-13 오전 2.15.55.png&quot; data-origin-width=&quot;2230&quot; data-origin-height=&quot;1030&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbfuis/btrTuA1eLSQ/g45I3DYZWfHFx70VLAIM40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbfuis/btrTuA1eLSQ/g45I3DYZWfHFx70VLAIM40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbfuis/btrTuA1eLSQ/g45I3DYZWfHFx70VLAIM40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcbfuis%2FbtrTuA1eLSQ%2Fg45I3DYZWfHFx70VLAIM40%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;2230&quot; height=&quot;1030&quot; data-filename=&quot;스크린샷 2022-12-13 오전 2.15.55.png&quot; data-origin-width=&quot;2230&quot; data-origin-height=&quot;1030&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;fixedSize()를 사용하면, 제대로 frame이 적용되는 것 을 볼 수 있죠 ㅎㅎ&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-12-13 오전 2.20.14.png&quot; data-origin-width=&quot;2234&quot; data-origin-height=&quot;1030&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Un5S5/btrTu3WuUda/jwZFo1PPkipAHfzQ91pky1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Un5S5/btrTu3WuUda/jwZFo1PPkipAHfzQ91pky1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Un5S5/btrTu3WuUda/jwZFo1PPkipAHfzQ91pky1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUn5S5%2FbtrTu3WuUda%2FjwZFo1PPkipAHfzQ91pky1%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;2234&quot; height=&quot;1030&quot; data-filename=&quot;스크린샷 2022-12-13 오전 2.20.14.png&quot; data-origin-width=&quot;2234&quot; data-origin-height=&quot;1030&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Text 역시 마찬가지입니다. 부모 뷰의 공간과 관계없이 자신의 이상적인 크기를 모두 표현하려면 fixedSize() modifier를 사용해야 하죠&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-12-13 오전 2.22.24.png&quot; data-origin-width=&quot;1310&quot; data-origin-height=&quot;728&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnka62/btrTtCk6daw/ZlKdoEAfk9QL7HvPKbDHBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnka62/btrTtCk6daw/ZlKdoEAfk9QL7HvPKbDHBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnka62/btrTtCk6daw/ZlKdoEAfk9QL7HvPKbDHBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbnka62%2FbtrTtCk6daw%2FZlKdoEAfk9QL7HvPKbDHBK%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;1310&quot; height=&quot;728&quot; data-filename=&quot;스크린샷 2022-12-13 오전 2.22.24.png&quot; data-origin-width=&quot;1310&quot; data-origin-height=&quot;728&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;조금 길어졌는데, 여기까지가 SwiftUI Layout System에 대한 내용입니다.&amp;nbsp;회사 내에서 발표했던 내용을 기반으로 글을 쓰는 거라 생각보다 빨리 쓸 줄 알았는데 엄청 오래 걸리네요 ㅜ 아무튼.. 끝까지 읽으신 분들에게 많은 도움이 되셨길 바랍니다. 감사합니다 :)&amp;nbsp;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Swift&amp;amp;iOS/iOS</category>
      <category>autolayout</category>
      <category>SwiftUI</category>
      <category>View</category>
      <category>WWDC</category>
      <category>기초</category>
      <category>레이아웃</category>
      <category>원리</category>
      <category>이해</category>
      <author>lsh424</author>
      <guid isPermaLink="true">https://lsh424.tistory.com/93</guid>
      <comments>https://lsh424.tistory.com/93#entry93comment</comments>
      <pubDate>Tue, 13 Dec 2022 02:44:47 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] SwiftUI 학습 순서 (feat. Tutorial &amp;amp; WWDC)</title>
      <link>https://lsh424.tistory.com/92</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;현재 회사에서 진행하는 프로젝트에서 SwiftUI와 Combine을 사용하고 있는데, 최근에 드디어..! CBT 버전 개발을 완료하고 배포했습니다 ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그 포스팅을 어떤 내용으로 하면 좋을까 생각해보다가&amp;nbsp;SwiftUI로 나름(?) 대규모 서비스를 개발해 본만큼 SwiftUI 학습을 처음 시작하려는 분들에게 어떤 순서로 학습을 하면 좋을지 알려주면 좋을 것 같아서 학습 순서를 정리해보려 합니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(주의: 만약 서비스가 지원하는 OS의 최소 버전이 iOS13이라면 SwiftUI 도입을 극구 말리고 싶습니다. 최소 지원 버전이 &lt;s&gt;iOS14&lt;/s&gt; -&amp;gt; iOS15 ~ 16 일때 도입을 고려해 보세요. 정정: 성능이 중요하거나, 메인 기능의 경우 추천드리지 않습니다.)&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. SwiftUI Tutorial&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 첫 시작으로 애플이 제공하는 SwiftUI Tutorial을 따라해보면 좋을 것 같습니다.&amp;nbsp;Chapter1 ~ Chapter4까지 약 4시간 30분 분량(원어민 기준)의 튜토리얼인데, 여유롭게 1주일이면 SwiftUI로 꽤 근사한 앱을 만들 수 있습니다. 튜토리얼을 통해 SwiftUI가 제공하는 대부분의 기능을 사용해볼 수 있고 개념에 대한 설명이 같이 있기 때문에 매우 유익합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/tutorials/swiftui&quot;&gt;https://developer.apple.com/tutorials/swiftui&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1669532925626&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Apple Developer Documentation&quot; data-og-description=&quot;&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/tutorials/swiftui&quot; data-og-url=&quot;https://developer.apple.com/tutorials/swiftui&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developer.apple.com/tutorials/swiftui&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/tutorials/swiftui&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;Apple Developer Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. SwiftUI Layout System 이해하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tutorial을 통해 SwiftUI에 대한 찍먹을 끝냈으면, 이제 Layout System이 어떻게 돌아가는지 이해할 필요가 있겠죠?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 UIKit으로 앱을 개발했을 때는 AutoLayout을 사용했지만, SwiftUI에서는 더 이상 사용하지 않습니다. 우리가 AutoLayout을 사용할 때도 먼저 학습을 진행했듯이, SwiftUI 역시 Layout System을 이해해야 더 제대로 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Layout System을 이해하기 위해서 제가 추천드리는 방법은 WWDC 세션 시청입니다. (꼭 보시길 강력 추천드립니다!!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SwiftUI Essentials (WWDC 2019)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/videos/play/wwdc2019/216&quot;&gt;https://developer.apple.com/videos/play/wwdc2019/216&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1669532490024&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;SwiftUI Essentials - WWDC19 - Videos - Apple Developer&quot; data-og-description=&quot;Take your first deep-dive into building an app with SwiftUI. Learn about Views and how they work. From basic controls to sophisticated...&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/videos/play/wwdc2019/216&quot; data-og-url=&quot;https://developer.apple.com/videos/play/wwdc2019/216&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bqLQBu/hyQH6hlmHY/OgJt5SCczJEOJsXNmgACuk/img.jpg?width=500&amp;amp;height=282&amp;amp;face=0_0_500_282&quot;&gt;&lt;a href=&quot;https://developer.apple.com/videos/play/wwdc2019/216&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/videos/play/wwdc2019/216&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bqLQBu/hyQH6hlmHY/OgJt5SCczJEOJsXNmgACuk/img.jpg?width=500&amp;amp;height=282&amp;amp;face=0_0_500_282');&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;SwiftUI Essentials - WWDC19 - Videos - Apple Developer&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Take your first deep-dive into building an app with SwiftUI. Learn about Views and how they work. From basic controls to sophisticated...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;Building Custom Views with SwiftUI (WWDC 2019)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://developer.apple.com/videos/play/wwdc2019/237&quot;&gt;https://developer.apple.com/videos/play/wwdc2019/237&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1669532500446&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Building Custom Views with SwiftUI - WWDC19 - Videos - Apple Developer&quot; data-og-description=&quot;Learn how to build custom views and controls in SwiftUI with advanced composition, layout, graphics, and animation. See a demo of a high...&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/videos/play/wwdc2019/237&quot; data-og-url=&quot;https://developer.apple.com/videos/play/wwdc2019/237&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c3IfqD/hyQH3dP2Gl/buYM8pmhjDIKb5DknWc6X1/img.jpg?width=500&amp;amp;height=282&amp;amp;face=0_0_500_282&quot;&gt;&lt;a href=&quot;https://developer.apple.com/videos/play/wwdc2019/237&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/videos/play/wwdc2019/237&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c3IfqD/hyQH3dP2Gl/buYM8pmhjDIKb5DknWc6X1/img.jpg?width=500&amp;amp;height=282&amp;amp;face=0_0_500_282');&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;Building Custom Views with SwiftUI - WWDC19 - Videos - Apple Developer&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Learn how to build custom views and controls in SwiftUI with advanced composition, layout, graphics, and animation. See a demo of a high...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. SwiftUI 동작 방식 이해하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI Layout System을 이해했으면, 이제 동작 방식을 이해할 필요가 있겠죠? ㅎㅎ 여기에 대한 학습도 Apple WWDC가 짱입니다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐면&amp;nbsp;Demystify SwiftUI라는 세션에서 자세히 설명해주고 있거든요..ㅋ &amp;nbsp;(참고: Demystify == '이해하기 쉽게 설명해주다.', '분명히 설명해주다.')&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Demystify SwiftUI (WWDC 2021)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/videos/play/wwdc2021/10022/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.apple.com/videos/play/wwdc2021/10022/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1669533244122&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Demystify SwiftUI - WWDC21 - Videos - Apple Developer&quot; data-og-description=&quot;Peek behind the curtain into the core tenets of SwiftUI philosophy: Identity, Lifetime, and Dependencies. Find out about common patterns,...&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/videos/play/wwdc2021/10022/&quot; data-og-url=&quot;https://developer.apple.com/videos/play/wwdc2021/10022/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/OImMl/hyQHZCvFU5/1ydA9BWxEafKHf1gNKhr3K/img.jpg?width=500&amp;amp;height=282&amp;amp;face=0_0_500_282&quot;&gt;&lt;a href=&quot;https://developer.apple.com/videos/play/wwdc2021/10022/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/videos/play/wwdc2021/10022/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/OImMl/hyQHZCvFU5/1ydA9BWxEafKHf1gNKhr3K/img.jpg?width=500&amp;amp;height=282&amp;amp;face=0_0_500_282');&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;Demystify SwiftUI - WWDC21 - Videos - Apple Developer&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Peek behind the curtain into the core tenets of SwiftUI philosophy: Identity, Lifetime, and Dependencies. Find out about common patterns,...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 이 순서대로 학습한다면, 학습에 투입하는 시간, 속도에 따라 편차는 있겠지만 빠르면 일주일 늦어도 한 달이면 학습을 끝 마칠 수 있을 거라고 생각합니다. 그 다음 부터는 기존 UIKit 프로젝트를 SwiftUI로 리팩토링 해보거나, 새로운 프로젝트를 SwiftUI로 시작해보면 좋을 것 같습니다 ㅎㅎ&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(다들 SwiftUI로 뷰를 개발하는게 얼마나 신세계인지 직접 경험해 보시길..!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Better apps. Less code.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;123971440-017b3a00-d9f5-11eb-94ec-36a7e67735d1.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uHugZ/btrSaJFLEx1/kAy3XCAkU5q2HfVJMwLthK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uHugZ/btrSaJFLEx1/kAy3XCAkU5q2HfVJMwLthK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uHugZ/btrSaJFLEx1/kAy3XCAkU5q2HfVJMwLthK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuHugZ%2FbtrSaJFLEx1%2FkAy3XCAkU5q2HfVJMwLthK%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;1200&quot; height=&quot;630&quot; data-filename=&quot;123971440-017b3a00-d9f5-11eb-94ec-36a7e67735d1.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Swift&amp;amp;iOS/iOS</category>
      <category>ios</category>
      <category>Swift</category>
      <category>SwiftUI</category>
      <category>WWDC</category>
      <category>레이아웃</category>
      <author>lsh424</author>
      <guid isPermaLink="true">https://lsh424.tistory.com/92</guid>
      <comments>https://lsh424.tistory.com/92#entry92comment</comments>
      <pubDate>Sun, 27 Nov 2022 16:23:19 +0900</pubDate>
    </item>
  </channel>
</rss>