<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Swift.rgb</title>
        <link>https://velog.io/</link>
        <description>Swift 를 공부합니다</description>
        <lastBuildDate>Sat, 06 Jan 2024 12:24:29 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Swift.rgb</title>
            <url>https://velog.velcdn.com/images/x_0o0/profile/48d2e9f7-0b02-4b70-a936-81844ffd1587/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Swift.rgb. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/x_0o0" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[스위프트 패키지를 코코아팟에 배포하기]]></title>
            <link>https://velog.io/@x_0o0/deploy-package-to-cocoapods</link>
            <guid>https://velog.io/@x_0o0/deploy-package-to-cocoapods</guid>
            <pubDate>Sat, 06 Jan 2024 12:24:29 GMT</pubDate>
            <description><![CDATA[<table>
<thead>
<tr>
<th>대상</th>
<th>난이도</th>
</tr>
</thead>
<tbody><tr>
<td>스위프트 패키지를 코코아팟에 배포하고 싶은 분</td>
<td>🫐 Blueprint (참고하면서 개발하기 좋음)</td>
</tr>
</tbody></table>
<h1 id="스위프트-패키지를-코코아팟에-배포하기">스위프트 패키지를 코코아팟에 배포하기</h1>
<p>스위프트 패키지를 코코아팟(CocoaPods) 의존성 관리도구(Dependency Manager) 에 배포하는 과정</p>
<h2 id="1-배경-및-목적">1. 배경 및 목적</h2>
<p>Xcode 에서 개발한 Framework 를 코코아팟에 배포하고 XCFramework 를 추출해서 스위프트 패키지 매니저에도 배포는 수차례 해보았지만
스위프트 패키지 매니저에 배포하던 스위프트 패키지를 코코아팟에 배포하는 것은 경험이 없었다.</p>
<p>CocoaPods의 매니페스트(Manifest) 설정을 담당하는 Podspec 를 가볍게 수정하는 것으로 스위프트 패키지를 코코아팟에 배포할 수 있다.</p>
<p>이 문서에서는 <code>SevenSegmentUI</code> 라는 스위프트 패키지에 <code>.podspec</code> 파일을 생성하고 코코아팟 트렁크(Trunk) 에 푸시하여 배포하는 과정을 다뤄보겠습니다.</p>
<h2 id="2-코코아팟-배포-과정">2. 코코아팟 배포 과정</h2>
<h3 id="21-podspec-생성-및-수정">2.1. Podspec 생성 및 수정</h3>
<blockquote>
<p><strong>중요</strong></p>
<p><code>pod</code> 커맨드라인 도구 설치가 사전에 필요합니다.</p>
</blockquote>
<pre><code class="language-bash">pod spec create SevenSegmentUI # 패키지 이름</code></pre>
<p><code>spec create</code> 는 <code>.podspec</code> 파일을 생성하는 명령어 입니다.<sup><a href="#footnote_1">1</a></sup></p>
<p>명령어 실행 후 <code>package-seven-segments</code> 폴더의 구성은 다음과 같게 됩니다.</p>
<pre><code>package-seven-segments
|--- Package.swift
|--- SevenSegmentUI.podspec
|--- Sources
     |--- SevenSegmentUI</code></pre><p><code>.podspec</code> 을 열고 아래와 같이 팟의 스펙 정보를 작성했습니다.</p>
<pre><code class="language-podspec">Pod::Spec.new do |spec|
  spec.name         = &quot;SevenSegmentUI&quot;
  spec.version      = &quot;0.0.1&quot;
  spec.summary      = &quot;한 줄 설명&quot;

  spec.description  = &lt;&lt;-DESC
    라이브러리에 대한 자세한 소개 문구가 들어갑니다.
                   DESC

  spec.license      = { :type =&gt; &quot;MIT&quot;, :file =&gt; &quot;LICENSE&quot; }

  spec.author             = { &quot;Jaesung Lee&quot; =&gt; &quot;이메일주소&quot; }

  spec.ios.deployment_target = &quot;16.0&quot;
  spec.swift_version = &quot;5.9&quot;

  spec.source       = {
    :git =&gt; &quot;https://github.com/jaesung-0o0/package-seven-segments.git&quot;,
    :tag =&gt; &quot;#{spec.version}&quot;
  }

  spec.source_files  = &quot;Sources/SevenSegmentUI/**/*&quot;

end</code></pre>
<p>스펙에 들어가야 하는 정보는 다음과 같습니다. 여기서 가장 중요한 것은 <code>spec.source_files</code> 입니다. Sources 하위 폴더 중 팟으로 제공하고자 하는 소스의 폴더를 명시해줘야 합니다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>name</code></td>
<td>pod 이름</td>
<td><code>spec.name = &quot;MyPackage&quot;</code></td>
</tr>
<tr>
<td><code>version</code></td>
<td>버전. 아래의 <code>source</code>에서 태그(<code>:tag</code>)를 버전으로 사용하게 세팅합니다.</td>
<td><code>spec.version = &quot;1.0.0&quot;</code></td>
</tr>
<tr>
<td><code>summary</code></td>
<td>한 줄 요약</td>
<td><code>spec.summary = &quot;Swift Package that provides OOO features&quot;</code></td>
</tr>
<tr>
<td><code>description</code></td>
<td>자세한 소개 문구</td>
<td>위의 코드 블럭 참고</td>
</tr>
<tr>
<td><code>license</code></td>
<td>라이센스. 오픈소스 라이센스라면 <code>:type</code> 과 <code>:file</code> 을 사용하면 된다.</td>
<td><code>spec.license = { :type =&gt; &quot;MIT&quot;, :file =&gt; &quot;LICENSE&quot; }</code></td>
</tr>
<tr>
<td><code>author</code></td>
<td>작성자. 코코아팟 트렁크에 등록(<code>pod trunk register</code>)된 이메일과 이름이어야 합니다.</td>
<td><code>spec.author = { &quot;Jaesung Lee&quot; =&gt; &quot;이메일주소&quot; }</code></td>
</tr>
<tr>
<td><code>ios.deployment_target</code></td>
<td>지원하느 플랫폼 버전</td>
<td><code>spec.ios.deployment_target = &quot;16.0&quot;</code></td>
</tr>
<tr>
<td><code>swift_version</code></td>
<td>지원하는 스위프트 버전</td>
<td><code>spec.swift_version = &quot;5.9&quot;</code></td>
</tr>
<tr>
<td><code>source</code></td>
<td>소스코드 저장 위치와 버전. <code>:git</code> 에 깃 저장소 URL 주소를 기재하고, <code>:tag</code> 는 태그를 버전으로 사용할 수 있도록 <code>&quot;#{spec.version}&quot;</code> 을 값으로 사용합니다.</td>
<td><code>spec.source = { :git =&gt; &quot;https://github.com/사용자이름/레포지토리이름.git&quot;, :tag =&gt; &quot;#{spec.version}&quot;</code></td>
</tr>
<tr>
<td><code>source_files</code></td>
<td>소스코드가 위치한 Sources 의 하위폴더를 명시합니다. Package.swift 에서 타겟에 소스파일 경로를 명시하는 것과 동일합니다.</td>
<td><code>spec.source_files = &quot;Sources/MyPackage/**/*&quot;</code><sup><a href="#footnote_2">2</a></sup></td>
</tr>
</tbody></table>
<p><code>.podspec</code> 수정이 완료되면 다음과 같이 작성된 내용이 유효한 지 검증합니다.</p>
<pre><code class="language-bash">pod spec lint SevenSegmentUI.podspec</code></pre>
<h3 id="22-코코아팟에-배포">2.2. 코코아팟에 배포</h3>
<p>수정된 <code>.podspec</code> 커밋하고 원격 저장소에 푸시합니다. </p>
<pre><code class="language-bash">git add *
git commit -m &quot;[버전] 1.0.0&quot;
git push origin main</code></pre>
<p>커밋에 버전을 명시한 태그를 추가합니다. 아까 <code>.podspec</code> 에서 <code>source_files</code> 의 <code>:tag</code> 에 태그 내용을 버전으로 사용한다고 명시했기 때문에, trunk 에 배포 전 반드시 원격저장소에 태그가 먼저 추가되어 있어야 합니다.</p>
<pre><code class="language-bash">git tag &quot;1.0.0&quot;</code></pre>
<p>태그까지 추가 되었다면 트렁크에 푸시합니다.</p>
<pre><code class="language-bash">pod trunk push</code></pre>
<blockquote>
<p><strong>NOTE</strong></p>
<p><code>git.author</code> 에 기재한 작성자 정보가 트렁크에 등록되어있지 않다면 트렁크에 푸시할 수 없습니다. 푸시 전 다음 명령어를 실행하여 이메일과 이름을 트렁크에 등록해주도록 합니다.</p>
<pre><code class="language-bash">pod trunk register 이메일주소 이름 # 띄어쓰기는 밑줄로 대체. 예: Jaesung_Lee</code></pre>
<p>이 명령어를 실행하면 등록을 하려는 이메일 주소로 인증메일이 발송 됩니다. 인증 메일에 있는 링크를 누르면 등록이 완료됩니다.</p>
</blockquote>
<p>푸시가 성공적으로 완료되면 다음과 같이 메세지가 나타납니다.</p>
<pre><code class="language-bash">--------------------------------------------------------------------------------
 🎉  Congrats

 🚀  SevenSegmentUI (0.0.1) successfully published
 📅  November 3rd, 04:37
 🌎  https://cocoapods.org/pods/SevenSegmentUI
 👍  Tell your friends!
--------------------------------------------------------------------------------</code></pre>
<h2 id="3-결론">3. 결론</h2>
<p><code>.podspec</code> 의 <code>source_files</code> 에 <code>Sources/소스폴더이름/**/*</code> 를 기재하여 스위프트 패키지를 코코아팟에 배포할 수 있습니다.</p>
<h2 id="4-참고-문헌">4. 참고 문헌</h2>
<p><a name="footnote_1">1</a>: <a href="https://guides.cocoapods.org/making/specs-and-specs-repo.html">https://guides.cocoapods.org/making/specs-and-specs-repo</a></p>
<p><a name="footnote_2">2</a>: <code>**</code> 는 모든 하위폴더를 의미, <code>*</code> 모든 파일을 의미</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Single Sheet Support 에러]]></title>
            <link>https://velog.io/@x_0o0/single-sheet-support</link>
            <guid>https://velog.io/@x_0o0/single-sheet-support</guid>
            <pubDate>Mon, 25 Dec 2023 11:40:19 GMT</pubDate>
            <description><![CDATA[<h1 id="single-sheet-support-에러">Single Sheet Support 에러</h1>
<h2 id="결론">결론</h2>
<p>동일한 <code>.sheet</code> 로직이 서로 뷰 계층에 중복으로 있는 지 확인해 볼것!</p>
<pre><code class="language-swift">struct ParentView: View {
    @State private var isSheetPresented = false

    var body: some View {
        VStack {
            ChildView()
                .sheet(isPresented: $isSheetPresented) {
                    SheetView()
                }
        }
        .sheet(isPresented: $isSheetPresented) {
            SheetView()
        }
    }
}</code></pre>
<h3 id="설명">설명</h3>
<p>부모 뷰와 자식뷰에 <code>.sheet</code> 를 동일한 데이터로 트리거 하는 로직이 있다면 해당 데이터가 동시에 두개 이상의 <code>sheet</code> 컨텐츠를 트리거할 수 있다. 꼭 부모-자식이 아니더라도 서로 다른 뷰 계층 구조면 해당되는 상황이다. 실제 그 상황이 발생하면 콘솔에 다음과 같이 로그가 뜨는 것을 확인할 수 있다.</p>
<blockquote>
<p>Currently, only presenting a single sheet is supported. The next sheet will be presented when the currently presented sheet gets dismissed.</p>
</blockquote>
<p>글쓴이 본인이 이 에러를 마주했을 때 코드는 아래와 같이 동일한 <code>.sheet</code> 코드가 부모 뷰와 자식뷰에 있었다.</p>
<pre><code class="language-swift">// 부모뷰: NoticesContentView
var body: some View {
    NoticeList(store: self.store) // 자식뷰
        .sheet( // 👈 자식 뷰에 동일 코드 있음
            store: self.store.scope(
                state: \.$changeDepartment,
                action: \.changeDepartment
            )
        ) { store in
            NavigationStack {
                DepartmentSelector(store: store)
            }
        }
}

// 자식뷰: NoticeList
var body: some View {
    Section {
        // ...
    }
    .sheet( // 👈 자식 뷰에 동일 코드
        store: self.store.scope(
            state: \.$changeDepartment,
            action: \.changeDepartment
        )
    ) { store in
        NavigationStack {
            DepartmentSelector(store: store)
        }
    }
}</code></pre>
<p>이 코드 하나만으로 <code>onAppear</code> 로직이 올바르게 동작하지 않아 Canvas Preview 가 실행되자 마자 에러가 나는 등 원인을 쉽게 찾을 수 없는 골치 아픈 에러들을 야기했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Row 선택 해지시키기]]></title>
            <link>https://velog.io/@x_0o0/deselect-row</link>
            <guid>https://velog.io/@x_0o0/deselect-row</guid>
            <pubDate>Tue, 28 Nov 2023 16:16:28 GMT</pubDate>
            <description><![CDATA[<h1 id="row-선택-해지시키기">Row 선택 해지시키기</h1>
<h2 id="결론">결론</h2>
<p>리스트아이템 뷰에 적용하기</p>
<pre><code class="language-swift">// Row.swift
struct Row: View {
    var body: some View {
        { ... }
            .buttonStyle(.plain)
    }
}</code></pre>
<p>또는</p>
<pre><code class="language-swift">// List.swift

List {
    ForEach(...) {
        Row()
            .buttonStyle(.plain)
    }
}</code></pre>
<h2 id="분석">분석</h2>
<pre><code class="language-swift">List {
    ForEach(...) {
        Row()
            .buttonStyle(.plain) // 👈 2
    }
}
.listStyle(.plain)
.buttonStyle(.plain) // 👈 1</code></pre>
<h3 id="1번-경우">1번 경우</h3>
<p><code>ForEach</code> 에 적용하는 경우 de-select가 다소 늦게 동작하는 현상이 있었습니다. 예를 들어 swipe action을 하는 경우 selected 상태가 유지 되다가 터치를 떼는 순간 deselect 됩니다.</p>
<h3 id="2번-경우">2번 경우</h3>
<p>기대했던 동작으로 잘 되는 것을 확인할 수 있습니다. Row 뷰에 바로 적용하는 방법을 추천합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[정사각형 사이즈 최대로 키우기]]></title>
            <link>https://velog.io/@x_0o0/maximize-square-size</link>
            <guid>https://velog.io/@x_0o0/maximize-square-size</guid>
            <pubDate>Tue, 28 Nov 2023 16:05:46 GMT</pubDate>
            <description><![CDATA[<h1 id="정사각형-사이즈-최대로-키우기">정사각형 사이즈 최대로 키우기</h1>
<h2 id="결론">결론</h2>
<p><code>.aspectRatio(_:contentMode:)</code> -&gt; <code>frame(maxWidth: .infinity)</code> 호출해주면 된다.</p>
<h3 id="설명">설명</h3>
<p>아래 사진처럼 정사각형 형태로 뷰를 최대 사이즈로 키우고 싶다면</p>
<p><code>.aspectRatio(_:contentMode:)</code> -&gt; <code>frame(maxWidth: .infinity)</code> 호출해주면 된다.</p>
<pre><code class="language-swift">Color(.tertiarySystemGroupedBackground)
    .aspectRatio(1, contentMode: .fill)
    .frame(maxWidth: .infinity)</code></pre>
<img width="260" alt="Screen Shot 2023-02-14 at 1 50 23 AM" src="https://user-images.githubusercontent.com/53814741/218520295-b9c3307d-28ae-4e22-84b6-01cc2483ba57.png">
]]></description>
        </item>
        <item>
            <title><![CDATA[새로운 뷰와 @ViewBuilder 함수 둘 중 누가 더 퍼포먼스가 좋을까?]]></title>
            <link>https://velog.io/@x_0o0/swiftui-new-view-and-viewbuilder-performance</link>
            <guid>https://velog.io/@x_0o0/swiftui-new-view-and-viewbuilder-performance</guid>
            <pubDate>Tue, 28 Nov 2023 16:00:17 GMT</pubDate>
            <description><![CDATA[<h1 id="새로운-뷰와-viewbuilder-함수-둘-중-누가-더-퍼포먼스가-좋을까">새로운 뷰와 @ViewBuilder 함수 둘 중 누가 더 퍼포먼스가 좋을까?</h1>
<h2 id="결론">결론</h2>
<p>애플의 WWDC19, 21 세션들을 보면, SwiftUI는 컴파일 시 커스텀 뷰들을 하나의 뷰 계층구조에 flatten 시키기 때문에 퍼포먼스 차이는 없습니다.</p>
<h2 id="참고">참고</h2>
<ul>
<li>Building Custom View with SwiftUI, Apple Developer, <a href="https://developer.apple.com/videos/play/wwdc2019/237">https://developer.apple.com/videos/play/wwdc2019/237</a></li>
<li>Demystify SwiftUI, Apple Developer, <a href="https://developer.apple.com/videos/play/wwdc2021/10022">https://developer.apple.com/videos/play/wwdc2021/10022</a></li>
<li>Demystify SwiftUI performance, Apple Developer, <a href="https://developer.apple.com/videos/play/wwdc2023/10160">https://developer.apple.com/videos/play/wwdc2023/10160</a></li>
<li>SwiftUI Tips and Tricks, Hacking with Swift, <a href="https://www.hackingwithswift.com/quick-start/swiftui/swiftui-tips-and-tricks">https://www.hackingwithswift.com/quick-start/swiftui/swiftui-tips-and-tricks</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[커스텀 뷰 선언하기]]></title>
            <link>https://velog.io/@x_0o0/declaring-a-custom-view</link>
            <guid>https://velog.io/@x_0o0/declaring-a-custom-view</guid>
            <pubDate>Tue, 28 Nov 2023 15:13:37 GMT</pubDate>
            <description><![CDATA[<h1 id="커스텀-뷰-선언하기">커스텀 뷰 선언하기</h1>
<p>Apple Developer Documentations 의 Declaring a custom view 문서를 정독하며 정리한 내용입니다.</p>
<h2 id="개요">개요</h2>
<blockquote>
<p><strong>참고</strong> <a href="https://developer.apple.com/documentation/swiftui/declaring-a-custom-view">https://developer.apple.com/documentation/swiftui/declaring-a-custom-view</a></p>
</blockquote>
<pre><code class="language-swift">struct MyView: View {
    // 뷰 모습을 묘사
    var body: some View {
        VStack {
            Text(&quot;안녕&quot;)
        } // 이처럼 여러개의 자식 뷰를 갖는 경우 `@ViewBuilder` 속성이 있는 클로져를 사용하면 된다.(1)
    }
}</code></pre>
<h3 id="커스텀-레이아웃">커스텀 레이아웃</h3>
<blockquote>
<p><a href="https://developer.apple.com/documentation/swiftui/composing_custom_layouts_with_swiftui">https://developer.apple.com/documentation/swiftui/composing_custom_layouts_with_swiftui</a></p>
</blockquote>
<img width="709" alt="Screenshot 2023-10-02 at 11 09 47 AM" src="https://github.com/jaesung-0o0/study-ios/assets/53814741/bf421935-3cf8-45db-8dc2-a67167eb6021">

<p><strong>리더보드는 투표수와 득표율을 보여줌 -&gt; <code>Grid</code> 를 사용한다.</strong></p>
<ul>
<li><p><code>GridRow</code> ∈ <code>ForEach</code> ∈ <code>Grid</code></p>
</li>
<li><p><code>GridRow</code> 는 Column cell 생성</p>
</li>
<li><p><code>Grid</code>  의 정렬은 모든 셀에 적용</p>
<ul>
<li><p>셀에서 <code>gridColumnAlignment(_:)</code> 을 사용해서 정렬을 오버라이드 할 수 있음</p>
<pre><code class="language-swift">Grid(alignment: .leading) {
ForEach(model.pets) { pet in
    GridRow {
        Text(...)

        ProgressView(...)

        Text(...)
          .gridColumnAlignment(.trailing)
    }

    Divider()
}
}</code></pre>
</li>
</ul>
</li>
</ul>
<p><strong>투표 버튼</strong></p>
<p>각 버튼이 같은 너비를 가지려면 <code>Layout</code> 프로토콜을 준수하는 커스텀 레이아웃을 만들어야함. -&gt; <code>MyEqualWidthHStack</code></p>
<ol>
<li><p><code>sizeThatFits(proposal:subviews:cache:)</code>: 컨테이너 사이즈와 주어진 하위뷰들 정보를 알려줌.
이 메소드는 하위뷰들간의 수평 공백과 각 방향별 가장 큰 사이즈를 결합하여 컨테이너 전체 사이즈를 찾음.</p>
<pre><code class="language-swift">func sizeThatFits(
 proposal: ProposedViewSize,
 subviews: Subviews,
 cache: inout Void
) -&gt; CGSize {
 guard !subviews.isEmpty else { return .zero }

 let maxSize = maxSize(subviews: subviews) // 가장 큰 사이즈
 let spacing = spacing(subviews: subviews) // 수평 공백
 let totalSpacing = spacing.reduce(0) { $0 + $1 }

 return CGSize(
     width: maxSize.width * CGFloat(subviews.count) + totalSpacing, // 가장 큰 사이즈 기준으로 하위뷰를 잡고 모든 공백을 더함 -&gt; 너비가 초과하진 않은가...? 
     height: maxSize.height
 )
}</code></pre>
</li>
<li><p><code>placeSubviews(in:proposal:subviews:cache:)</code>: 하위뷰들이 레이아웃 내 어디에서 떠야하는지 알려주는 용도
이 메소드는 각 하위뷰에 대한 사이즈를 제안하고 이 사이즈를 뷰의 바뀐 지점을 사용해서 버튼을 기본 공백값과 함께 수평으로 나열합니다.</p>
<pre><code class="language-swift">func placeSubviews(
 in bounds: CGRect,
 proposal: ProposedViewSize,
 subviews: Subviews,
 cache: inout Void
) {
 guard !subviews.isEmpty else { return }

 let maxSize = maxSize(subviews: subviews)
 let spacing = spacing(subviews: subviews)

 let placementProposal = ProposedViewSize(width: maxSize.width, height: maxSize.height)
 var nextX = bounds.minX + maxSize.width / 2

 for index in subviews.indices {
     subviews[index].place(
         at: CGPoint(x: nextX, y: bounds.midY),
         anchor: .center,
         proposal: placementProposal
     )
     nextX += maxSize.width + spacing[index]
 }
}</code></pre>
</li>
<li><p><code>ViewThatFits</code>
투표 버튼의 크기는 텍스트의 너비에 따라 정해집니다. <code>ViewThatFits</code> 는 사용가능한 공간에 맞게 수평으로 정렬할지, 수직으로 정렬할지 SwiftUI에 의해 정하도록 합니다.</p>
<pre><code class="language-swift">ViewThatFits {
 MyEqualWidthHStack {
     Buttons()
 }
 MyEqualWidthVStack {
     Buttons()
 }
}</code></pre>
</li>
<li><p>캐싱
<code>Layout</code> 프로토콜은 양방향 캐싱 파라미터를 가짐. 이 캐시는 특정 레이아웃 인스턴스의 모든 메소드 간 공유되는 옵셔널 저장소에 접근을 제공합니다.</p>
<pre><code class="language-swift">// storage 를 위한 타입 정의
struct CacheData {
 let maxSize: CGSize
 let spacing: [CGFloat]
 let totalSpacing: CGFloat
}</code></pre>
<p>그런 다음, <code>makeCache(subviews:)</code> 옵셔널 프로토콜 메소드를 사용해서 하위뷰들을 계산하고 위에서 정의한 타입의 값으로 리턴.</p>
<pre><code class="language-swift">func makeCache(subviews: Subviews) -&gt; CacheData {
 let maxSize = maxSize(subviews: subviews)
 let spacing = spacing(subviews: subviews)
 let totalSpacing = spacing.reduce(0) { $0 + $1 }

 return CacheData(
     maxSize: maxSize,
     spacing: spacing,
     totalSpacing: totalSpacing
 )
}</code></pre>
<p>만약 하위뷰들에 변화가 생기면, SwiftUI 는 <code>updateCache(_:subviews:)</code> 메소드를 호출합니다. 이 메소드의 기본 구현부는 <code>makeCache(subviews:)</code> 를 호출하도록 되어있고, 이는 데이터를 재계산 하게 됩니다.
그런다음 <code>sizeThatFits(proposal:subviews:cache:)</code> 와 <code>placeSubviews(in:proposal:subviews:cache:)</code> 메소드에서 <code>cache</code> 파라미터를 사용해서 데이터를 가져옵니다.</p>
<pre><code class="language-swift">// placeSubviews(in:proposal:subviews:cache:)
let maxSize = cache.maxSize
let spacing = cache.spacing</code></pre>
</li>
</ol>
<blockquote>
<p><strong>Note</strong>
대부분의 간단한 레이아웃은 캐싱 사용에서 큰 효율을 얻진 못합니다. Instruments 를 사용해서 앱을 프로파일링 하면 캐싱하면 좋은 레이아웃이 뭔지 알아낼 수 있습니다.</p>
</blockquote>
<h2 id="참고">참고</h2>
<ol>
<li><a href="https://developer.apple.com/documentation/swiftui/declaring-a-custom-view#:~:text=Views%20that%20take%20multiple%20input%20child%20views%2C%20like%20the%20stack%20in%20the%20example%20above%2C%20typically%20do%20so%20using%20a%20closure%20marked%20with%20the%20ViewBuilder%20attribute">https://developer.apple.com/documentation/swiftui/declaring-a-custom-view#:~:text=Views%20that%20take%20multiple%20input%20child%20views%2C%20like%20the%20stack%20in%20the%20example%20above%2C%20typically%20do%20so%20using%20a%20closure%20marked%20with%20the%20ViewBuilder%20attribute</a>.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[call-as-function]]></title>
            <link>https://velog.io/@x_0o0/call-as-function</link>
            <guid>https://velog.io/@x_0o0/call-as-function</guid>
            <pubDate>Mon, 27 Nov 2023 16:42:21 GMT</pubDate>
            <description><![CDATA[<table>
<thead>
<tr>
<th>대상</th>
<th>난이도</th>
</tr>
</thead>
<tbody><tr>
<td>call-as-function 에 대해 알고 싶으신 분</td>
<td>🔴 Read (가볍게 읽기 좋음)</td>
</tr>
</tbody></table>
<h1 id="call-as-function-알아보기">call-as-function 알아보기</h1>
<p>객체의 메서드를 호출하지 않고 객체 자체를 함수로 사용하는 방법.</p>
<h2 id="1-서론">1. 서론</h2>
<p>Pointfree.co 의 <a href="https://github.com/pointfreeco/swift-composable-architecture">ComposableArchitecure</a> 라이브러리에서 제공하는 <code>Send</code> 타입이 <code>CallAsFunction</code> 을 구현하고 있어 다음과 같이 쓸 수 있다고 안내되어 있습니다.<sup><a href="#footnote_1">1</a></sup></p>
<pre><code class="language-swift">return .run { send in
    send(.started)
    defer { send(.finished) }
    for await event in self.events {
        send(.event(event))
    }
}</code></pre>
<p>이 때 <code>send</code>는 함수처럼 쓰고 있지만 타입을 보면 함수 타입이 아닌 <code>Send</code> 타입의 객체입니다.
객체를 함수처럼 쓸 수 있는 이유는 <code>Send</code>가 call-as-function 을 구현하고 있기 때문입니다.</p>
<p>이 문서에서 call-as-function이 무엇인지 다뤄보겠습니다.</p>
<h2 id="2-알아보기">2. 알아보기</h2>
<h3 id="21-사용-방법">2.1. 사용 방법</h3>
<p>다음 내용은 Swift.org 의 내용을 번역하였습니다.<sup><a href="#footnote_2">2</a></sup></p>
<p>call-as-function 메서드 이름은 <code>callAsFunction()</code> 또는 <code>callAsFunction(</code> 로 시작하여 인자가 있는 형태를 사용합니다.</p>
<p>예를 들어, <code>callAsFunction(_:_:)</code> 과 <code>callAsFunction(something:)</code> 둘 다 call-as-function 메서드 이름으로 사용가능 합니다.</p>
<p>다음 함수 호출은 모두 동일합니다.</p>
<pre><code class="language-swift">struct CallableStruct {
    var value: Int
    func callAsFunction(_ number: Int, scale: Int) {
        print(scale * (number + value))
    }
}

let callable = CallableStruct(value: 100)
callable(4, scale: 2)
callable.callAsFunction(4, scale: 2)
// 둘 다 208 를 출력합니다.</code></pre>
<p>call-as-function 메서드는 다음 두 가지 사이에서 적절한 절충안을 만들어 균형을 유지시킵니다.</p>
<ul>
<li>타입 시스템에 얼마나 많은 정보를 인코딩할 지</li>
<li>런타임 동안 얼마나 많은 동적 행동이 가능할 지</li>
</ul>
<h3 id="22-왜-사용하는가">2.2. 왜 사용하는가?</h3>
<p>객체를 함수처럼 호출할 수 있는 것은 상태를 가진 계산 프로그램, 파서 또는 컴퓨팅 객체를 표현하고자 할 때 유용함을 제공합니다. 이는 복잡한 수학 연산 및 머신 러닝에서 특히 흔하며 특정 객체가 일부 상태를 유지하고 하나의 메서드만을 구현하는 경우가 있습니다.</p>
<p>donnywals.com 에서 다음과 같은 예시를 통해 설명하고 있습니다. 실제 파이썬에서 자주 볼 수 있는 패턴 중 하나인 함수에 렌더링 객체를 전달하는 패턴을 Swift 언어로 보여주며 설명하고 있습니다.</p>
<pre><code class="language-swift">protocol Route {
    associatedtype Output
}

func registerHandler&lt;R: Route&gt;(_ route: R, _ handler: (R) -&gt; R.Output) {
    return renderer(route)
}</code></pre>
<p><code>registerHandler</code> 는 <code>Route</code> 프로토콜을 준수하는 객체를 전달받고, 이를 <code>handler</code> 에서 인수로 전달합니다. 이 함수를 다음과 같이 호출할 수 있습니다.</p>
<pre><code class="language-swift">registerHandler(homeRoute) { route in
    /* 여기서 아웃풋을 생성하기 위해 수많은 작업을 수행해야 함 */

    return output
}</code></pre>
<p>간단한 <code>renderer</code>의 경우 이 방식은 문제가 없지만, 파이썬 패턴을 현재 스위프트로 보여주고 있음을 고려하여, 파이썬의 맥락에서, 이러한 코드는 웹서버에서 실행됩니다.
<code>route</code>는 사용자가 요청한 웹페이지의 URL(또는 경로)와 동등합니다. <code>registerHandler</code>의 클로저는 사용자에 의해 연결된 라우트가 요청될 때마다 호출됩니다. 처리해야할 작업이 많은 경우 간단한 클로저만으로 충분치 않을 수 있습니다. <strong><code>route</code>를 처리하는데 사용되는 객체에는 데이터베이스 연결, 캐싱, 인증 등등 여러 기능이 있어야 합니다.</strong>
이 모든 것을 클로저에 포함시키는 것은 좋은 아이디어가 아닙니다. 클로저에 포함시키는 것 대신 <code>route</code>를 처리하는 하나의 객체를 사용하는 것이 좋습니다. 이 때 call-as-function을 사용하는 것이 매우 유용합니다.</p>
<pre><code class="language-swift">struct HomeHandler {
    let database: Database
    let authenticator: Authenticator

    // ...

    func callAsFunction&lt;R: Route&gt;(_ route: R) -&gt; R.Output {
        /* 여기서 아웃풋을 생성하기 위해 수많은 작업을 수행해야 함 */

        return output
    }
}

let homeHander = HomeHandler(
    database: database, 
    authenticator: authenticator
)

registerHandler(for: homeRoute, homeHandler)</code></pre>
<p>이렇게 객체를 클로저를 받는 함수에 그대로 전달할 수 있습니다. <code>homeHandler.callAsFunction</code> 과 같이 메서드를 직접 넘길 수도 있으나 call-as-function 을 사용하기 때문에 어떤 메서드를 호출해야하는지 알 필요가 없어 사용도 쉽고, 읽기도 쉽습니다.</p>
<h2 id="3-결론">3. 결론</h2>
<p>서론에서 보았던 Pointfree.co 의 ComposableArchitecture 의 예시코드를 다시 살펴 보겠습니다.</p>
<pre><code class="language-swift">return .run { send in
    send(.started)
    defer { send(.finished) }
    for await event in self.events {
        send(.event(event))
    }
}</code></pre>
<p>이때 클로져가 전달하는 실행인자가 함수가 아니라 <code>Send</code> 라는 객체입니다. 하지만 <code>Send</code> 가 <code>callAsFunction</code> 을 구현하고 있기 때문에 아래와 같이 함수처럼 사용할 수 있습니다.</p>
<pre><code class="language-swift">send(.started)</code></pre>
<p>따라서, call-as-function 은 객체의 메서드를 호출하지 않고 객체 자체를 함수로 사용하는 방법을 제공합니다.</p>
<h2 id="4-참조">4. 참조</h2>
<h3 id="41-용어-정리">4.1 용어 정리</h3>
<table>
<thead>
<tr>
<th>국문</th>
<th>영문</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>인자, 매개변수</td>
<td>Parameter</td>
<td>함수를 정의할 때 필요한 변수 이름, 함수의 전달되는 값을 넘겨받을 때 쓰이는 변수</td>
</tr>
<tr>
<td>인수, 실행인자</td>
<td>Argument</td>
<td>함수를 호출할 때 실제로 넘어가는 변수 값</td>
</tr>
</tbody></table>
<h3 id="42-참고-문헌">4.2 참고 문헌</h3>
<p><a name="footnote_1">1<a>: <a href="https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/send">https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/send</a></p>
<p><a name="footnote_2">2<a>: <a href="https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#ID622">CallsAsFunction, swift.org</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[`xcodebuild docbuild` 명령어로 문서 빌드 배포하기]]></title>
            <link>https://velog.io/@x_0o0/using-xcodebuild-docbuild</link>
            <guid>https://velog.io/@x_0o0/using-xcodebuild-docbuild</guid>
            <pubDate>Mon, 27 Nov 2023 16:38:10 GMT</pubDate>
            <description><![CDATA[<table>
<thead>
<tr>
<th>대상</th>
<th>난이도</th>
</tr>
</thead>
<tbody><tr>
<td>xcodebuild 명령어로 docc 문서를 빌드하려는 분, docc 문서를 웹호스팅하고 싶은 분</td>
<td>🫐 Blueprint (참고하면서 개발하기 좋음)</td>
</tr>
</tbody></table>
<h1 id="xcodebuild-docbuild-명령어로-문서-빌드-배포하기"><code>xcodebuild docbuild</code> 명령어로 문서 빌드 배포하기</h1>
<p><code>xcodebuild</code> 커맨드라인 도구(Command Line Tool)의 <code>docbuild</code> 명령어로 <code>.doccarchive</code> 파일 추출 및 정적 웹 호스팅을 위한 형태로 변형하는 과정.</p>
<h2 id="1-배경-및-목적">1. 배경 및 목적</h2>
<p><code>docc-plugin</code> 의 경우 빌드 대상(build destination)이 macOS 으로 고정되어 있고 다른 대상으로 변경할 수 없습니다.
빌드 대상을 변경하고자 하는 경우 <code>xcodebuild</code> 커맨드라인 도구(Command Line Tool)의 <code>docbuild</code> 명령어를 사용해야합니다.</p>
<p>이 문서에서 <code>xcodebuild</code> 커맨드라인 도구를 사용해서 빌드 대상을 iOS 로 변경하여 DocC 프레임워크 기반 개발문서를 아카이브하고 더 나아가 정적 웹호스팅을 위한 형태로 변형하는 과정을 다뤄보겠습니다.</p>
<h2 id="2-진행-방법과-결과-데이터">2. 진행 방법과 결과 데이터</h2>
<h3 id="21-문서-아카이브-파일-생성-doccarchive">2.1. 문서 아카이브 파일 생성 (<code>.doccarchive</code>)</h3>
<pre><code class="language-bash">xcodebuild docbuild -scheme BoxOffice \
  -derivedDataPath /tmp/docbuild \
  -destination &#39;generic/platform=iOS&#39;</code></pre>
<p><code>docbuild</code> 는 문서 아카이브 (<code>.doccarchive</code>) 파일을 생성하는 명령어<sup><a href="#footnote_1">1</a></sup> 입니다. <code>docbuild</code>의 옵션은 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th>옵션</th>
<th>설명</th>
<th>예시 코드</th>
</tr>
</thead>
<tbody><tr>
<td><code>-scheme {스킴이름}</code></td>
<td>문서 빌드를 하고자하는 스킴</td>
<td><code>-scheme MyPackage</code></td>
</tr>
<tr>
<td><code>-derivedDataPath {아카이브저장위치}</code></td>
<td>저장 위치. 필수는 아니지만 이 옵션을 포함하면 <code>.doccarchive</code> 번들을 찾을 때 용이.</td>
<td><code>-derivedDataPath /tmp/docbuild</code></td>
</tr>
<tr>
<td><code>-destination {플랫폼}</code></td>
<td>빌드 destination</td>
<td><code>-destination &#39;generic/platform=iOS&#39;</code></td>
</tr>
</tbody></table>
<blockquote>
<p><strong>Tip</strong></p>
<p><code>-destination</code> 옵션 설정 시 <code>generic/platform=iOS</code> 에서 <code>generic</code> 을 제외시키면 모든 iOS 시뮬레이터로 빌드하기 때문에 긴 빌드 시간이 &gt; 소요되었습니다.  따라서 특정 시뮬레이터들을 돌려야 하는 경우가 아니면 <code>generic</code> 을 포함시키는 것이 좋습니다.</p>
</blockquote>
<h3 id="22-정적-호스팅을-위한-형태로-변형-docs">2.2. 정적 호스팅을 위한 형태로 변형 (<code>/docs</code>)</h3>
<pre><code class="language-bash">$(xcrun --find docc) process-archive \
  transform-for-static-hosting /tmp/docbuild/Build/Products/Debug-iphoneos/{타겟이름}.doccarchive \
  --hosting-base-path {레포지토리_이름} \
  --output-path {저장위치}</code></pre>
<p><code>xcrun --find docc</code> 는 <code>docc</code> 도구를 실행시키는 명령어 입니다. <code>process-archive</code> 는 아카이브를 처리하는 <code>docc</code> 명령어 입니다.</p>
<table>
<thead>
<tr>
<th>옵션</th>
<th>설명</th>
<th>예시코드</th>
</tr>
</thead>
<tbody><tr>
<td><code>transform-for-static-hosting</code><sup><a href="#footnote_2">2</a></sup></td>
<td>호스팅 형태로 바꿀 <code>.doccarchive</code> 파일 위치</td>
<td><code>transform-for-static-hosting /tmp/docbuild/Build/Products/Debug-iphoneos/BoxOffice.doccarchive</code></td>
</tr>
<tr>
<td><code>--hosting-base-path {호스팅경로}</code></td>
<td>호스팅할 주소의 base 경로 값. 깃헙페이지의 경우 레포지토리 이름.</td>
<td><code>--hosting-base-path my-repository-name</code></td>
</tr>
<tr>
<td>`--output-path {결과물저장위치}</td>
<td>결과물 저장 위치. (<code>/docs</code> 로 하는 것을 권장)</td>
<td><code>--output-path ./docs</code></td>
</tr>
</tbody></table>
<h3 id="23-빌드-결과물-처리">2.3. 빌드 결과물 처리</h3>
<p><code>/docs</code> 경로에 웹 호스팅이 가능한 파일들이 위치한 것을 알 수 있습니다. <a href="#21-%EB%AC%B8%EC%84%9C-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B8%8C-%ED%8C%8C%EC%9D%BC-%EC%83%9D%EC%84%B1-doccarchive">과정2.1</a> 과 <a href="#22-%EC%A0%95%EC%A0%81-%ED%98%B8%EC%8A%A4%ED%8C%85%EC%9D%84-%EC%9C%84%ED%95%9C-%ED%98%95%ED%83%9C%EB%A1%9C-%EB%B3%80%ED%98%95-docs">과정2.2</a> 의 명령어들을 <code>.sh</code> 스크립트 파일로 저장해두면 GitHub Actions 로 배포<sup><a href="#footnote_3">3</a></sup>할 때 용이합니다.</p>
<h2 id="3-결론">3. 결론</h2>
<p><code>xcodebuild</code> 커맨드라인 도구에도 docc 에 대한 명령어들이 있어 문서를 빌드하고, 아카이브 파일을 정적 웹호스팅을 위한 형태로 변형할 수 있습니다.</p>
<p><code>docc-plugin</code> 에서는 macOS로 고정되어 있는 빌드 대상을 변경할 수 없는 반면 <code>xcodebuild</code> 는 빌드 대상을 바꿀 수 있기 때문에 macOS 가 아닌 다른 플랫폼(예: iOS)만 지원하는 스위프트 패키지의 경우 <code>xcodebuild</code> 커맨드라인 도구를 사용하여 문서를 빌드할 수 있습니다.</p>
<h2 id="4-참고문헌">4. 참고문헌</h2>
<p><a name="footnote_1">1</a>: <a href="https://developer.apple.com/documentation/xcode/distributing-documentation-to-external-developers#:~:text=xcodebuild%20docbuild%20%2Dscheme%20SlothCreator%20%2DderivedDataPath%20~/Desktop/SlothCreatorBuild">https://developer.apple.com/documentation/xcode/distributing-documentation-to-external-developers</a>, Apple, Inc.</p>
<p><a name="footnote_2">2</a>: <a href="https://apple.github.io/swift-docc-plugin/documentation/swiftdoccplugin/generating-documentation-for-hosting-online/">https://apple.github.io/swift-docc-plugin/documentation/swiftdoccplugin</a>, Apple, Inc.</p>
<p><a name="footnote_3">3</a>: <a href="https://github.com/jaesung-0o0/package-docc-example#%EA%B9%83%ED%97%99-%EC%95%A1%EC%85%98%EC%9C%BC%EB%A1%9C-github-pages-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0">https://github.com/jaesung-0o0/package-docc-example</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[라이브러리 에러와 임베드]]></title>
            <link>https://velog.io/@x_0o0/missing-library</link>
            <guid>https://velog.io/@x_0o0/missing-library</guid>
            <pubDate>Mon, 27 Nov 2023 16:34:37 GMT</pubDate>
            <description><![CDATA[<table>
<thead>
<tr>
<th>대상</th>
<th>난이도</th>
</tr>
</thead>
<tbody><tr>
<td>정적, 동적 라이브러리에 대한 기본 개념을 알고 싶은 분, Library not loaded 에러에 대한 해결을 찾는 분</td>
<td>🧣 Read (가볍게 읽기 좋음)</td>
</tr>
</tbody></table>
<h1 id="라이브러리-에러와-임베드">라이브러리 에러와 임베드</h1>
<p>정적 라이브러리와 동적 라이브러리에 대해 알아보고 라이브러리 에러 분석하고 해결하기</p>
<h2 id="1-배경-및-목적">1. 배경 및 목적</h2>
<p>사이드 프로젝트 앱 심사 중 다음과 같은 크래시 로그 파일을 전달 받았습니다.</p>
<blockquote>
<p><em>crashlog-3BD80720-2A0C-4403-BB56-6EFC6063243F.txt</em></p>
<pre><code>&quot;termination&quot; : {&quot;code&quot;:1,&quot;flags&quot;:518,&quot;namespace&quot;:&quot;DYLD&quot;,&quot;indicator&quot;:&quot;Library missing&quot;,&quot;details&quot;:[&quot;(terminated at launch; ignore backtrace)&quot;],&quot;reasons&quot;:[&quot;Library not loaded: @rpath\/KuringSDK.framework\/KuringSDK&quot;</code></pre></blockquote>
<p>앱 크래시의 원인은 <code>&quot;termination&quot;</code> 항목에서 확인할 수 있고, 보면 <code>DYLD</code>, <code>Library missing</code>, <code>Library not loaded</code> 이러한 용어들이 나옵니다. 용어를 몰라도 대충 KuringSDK 라는 프레임워크가 로딩 되지 않아서 크래시가 났구나 하고 알 수는 있습니다. </p>
<p>이 문서에서는 동적라이브러리, 정적 라이브러리에 대한 중요한 내용들을 다뤄보면서 위의 크래시 로그가 정확히 무슨 의미인지, 그리고 어떻게 해결하는 지를 알 수 있습니다.</p>
<h2 id="2-알아보기">2. 알아보기</h2>
<h3 id="21-앱-패키지">2.1. 앱 패키지</h3>
<p>앱 타겟을 빌드하면 결과물로 다음과 같은 패키지 폴더가 생성됩니다.</p>
<img width="369" alt="스크린샷 2023-11-21 오후 3 23 22" src="https://github.com/jaesung-0o0/study-ios/assets/53814741/0f6ea399-37e6-4473-9338-650dbd61a038">

<p>여기서 실행파일은 실제 앱 구동에 사용됩니다.</p>
<h3 id="22-동적-라이브러리-그리고-정적-라이브러리">2.2. 동적 라이브러리 그리고 정적 라이브러리</h3>
<p>라이브러리의 코드가 앱의 소스 코드에 합쳐지는 것을 <strong>&quot;링크(Link)&quot;</strong> 라고 합니다. 링크 되는 방식에 따라 라이브러리는 <strong>정적 라이브러리(Static Library)</strong> 그리고 <strong>동적 라이브러리(Dynamic Library)</strong> 로 구분됩니다.</p>
<p><strong>정적 라이브러리</strong>는 앱 타겟 빌드 시, <strong>스태틱 링커</strong>(Static Linker)에 의해 실행파일에 라이브러리의 코드가 앱의 소스코드와 합쳐지게 됩니다. </p>
<img width="339" alt="스크린샷 2023-11-21 오후 3 23 54" src="https://github.com/jaesung-0o0/study-ios/assets/53814741/fbd4feec-4dcf-47e4-8ef1-53c3b9623b95">

<p><strong>동적 라이브러리</strong>는 앱 타겟 빌드 시, 실행파일에 라이브러리의 코드에 대한 참조만 포함되고 앱 구동 중에 라이브러리 코드가 필요한 시점이 오면 <strong>다이나믹 링커</strong>(Dynamic Linker, 또는 다이나믹 로더 Dynamic Loader)가 이 참조를 가지고 라이브러리를 로딩하게 됩니다.</p>
<img width="357" alt="스크린샷 2023-11-21 오후 3 24 14" src="https://github.com/jaesung-0o0/study-ios/assets/53814741/9189f7a7-5e27-4b69-b7cf-b99d7265a2e0">

<table>
<thead>
<tr>
<th>종류</th>
<th>사용하는 링커</th>
<th>링크 방법</th>
</tr>
</thead>
<tbody><tr>
<td>정적 라이브러리</td>
<td>Static Linker</td>
<td>빌드 시, 앱 실행파일에 필요한 라이브러리 코드를 합침.</td>
</tr>
<tr>
<td>동적 라이브러리</td>
<td>Dynamic Linker/Loader (DYLD)</td>
<td>빌드 시, 앱 실행파일에 라이브러리의 참조를 포함시킴.</br> 런타임에 필요한 시점에 참조를 통해 라이브러리를 로딩.</td>
</tr>
</tbody></table>
<h3 id="23-dyld">2.3. DYLD</h3>
<p>DYLD 는 Dynamic Loader 를 의미합니다. 다이나믹 링커(Dynamic Linker) 라고도 합니다. 보통 라이브러리 관련해서 dy 로 시작하는 약어는 일반적으로 Dynamic (동적) 을 의미하고 좀 더 구체적으로는 다이나믹 라이브러리에 대한 것을 의미합니다. 예를 들어, dylib 는 Dynamic Library 를 의미합니다.</p>
<p>동적 라이브러리의 경우, 앱 타겟 빌드 결과물의 실행파일에는 라이브러리에 대한 <strong>참조</strong> 가 들어있어서, 앱을 실행하면 이 참조를 가지고 필요한 라이브러리의 코드를 로딩하게 됩니다. 이 <strong>로딩 과정을 진행하는 주체</strong>가 바로 <strong>DYLD</strong> 입니다.</p>
<h3 id="24-라이브러리와-임베드">2.4. 라이브러리와 임베드</h3>
<p>임베드(Emded) 옵션은 라이브러리를 Frameworks 폴더에 포함시킬지 말지에 대한 여부를 나타내는 옵션입니다.</p>
<p>정적 라이브러리의 경우, 이미 필요한 라이브러리 코드를 앱 실행파일이 전부 갖고 있기 때문에 굳이 Frameworks 폴더에 라이브러리를 중복으로 들고 있을 필요가 없습니다. 따라서 임베드 옵션을 <code>Do Not Embed</code> 로 설정할 수 있습니다. 다만 번들의 경우 실행파일에 합쳐지는 것이 아니므로 정적 라이브러리가 번들과 같이 앱 구동 중 로딩이 필요한 요소를 갖고 있다면 <code>Embed</code> 로 설정해야 합니다.</p>
<img width="284" alt="스크린샷 2023-11-21 오후 3 24 44" src="https://github.com/jaesung-0o0/study-ios/assets/53814741/eb4a1057-ee12-488c-9e4e-a0a1b69c6964">

<p>동적 라이브러리의 경우, 앱 실행파일이 참조만 갖고 있기 때문에 이 참조를 가지고 다이나믹 로더가 라이브러리를 로딩할 수 있도록 Frameworks 폴더 안에 라이브러리가 있어야 합니다. 따라서 임베드 옵션을 <code>Embed &amp; Sign</code> 혹은 <code>Embed Without Signing</code> 으로 설정해줘야 합니다.</p>
<h2 id="3-결론">3. 결론</h2>
<p>서론에서 보았던 <code>Library not loaded</code> 에러는 문제가 되었던 라이브러리가 동적 라이브러리 였고 <strong>&quot;Do Not Embed&quot;</strong> 로 옵션이 설정되어 있었기 때문에 발생한 에러였습니다.
빌드 결과물의 실행파일에는 참조만 있고 앱 실행시 <strong>다이나믹 로더(DYLD)</strong> 가 이 참조를 가지고 라이브러리를 로드해야 하는데 임베드 옵션이 &quot;Do Not Embed&quot; 이었기 때문에, 로드할 라이브러리가 Frameworks 폴더에 없어서 크래시가 난 상황입니다.
그러므로 임베드 옵션을 <strong>&quot;Embed<del>~</del>&quot;</strong> 로 변경해주면 크래시 에러를 해결할 수 있습니다.</p>
<h2 id="4-참고문헌">4. 참고문헌</h2>
<p><a href="https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/DynamicLibraries/100-Articles/OverviewOfDynamicLibraries.html">Overview of Dynamic Libraries, developer.apple.com | Apple, Inc.</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[댓글로 깃헙 액션 돌리기]]></title>
            <link>https://velog.io/@x_0o0/github-actions-with-comments</link>
            <guid>https://velog.io/@x_0o0/github-actions-with-comments</guid>
            <pubDate>Mon, 27 Nov 2023 16:29:47 GMT</pubDate>
            <description><![CDATA[<table>
<thead>
<tr>
<th>대상</th>
<th>난이도</th>
</tr>
</thead>
<tbody><tr>
<td>PR 에서 댓글로 소통하는 깃헙 액션을 고민하는 분</td>
<td>🫐 Blueprint (참고하면서 개발하기 좋음)</td>
</tr>
</tbody></table>
<h1 id="댓글로-깃헙-액션-돌리기">댓글로 깃헙 액션 돌리기</h1>
<h2 id="1-배경-및-목적">1. 배경 및 목적</h2>
<blockquote>
<p>참고: 결과물만 보고 싶은 경우 <a href="https://github.com/ku-ring/ios-app/pull/42">PR #42</a> 링크를 확인하십시오</p>
</blockquote>
<p>2년전에 (2021년) 사이드 프로젝트에서 깃헙 액션을 작업한 적이 있었습니다.</p>
<p>그때는 PR이 올라오면 base 브랜치가 develop 일 때 깃헙액션이 돌도록 했는데 문제는 커밋을 푸시할때마다 매번 테스트가 돌다보니 깃헙 액션 사용가능 시간을 불필요하게 많이 소모하는 문제가 있었습니다.</p>
<p>규모가 큰 앱도 아니었고 실제 깃헙 액션도 10-20분 정도 걸렸기 때문에, 매달 무료로 제공되는 2000분 (약 33시간)이면 마음껏 사용해도 되지 않나하고 생각할 수 있습니다.
<strong>하지만 운영체제 버전 마다 소모하는 사용시간의 크기가 다릅니다. 이 중 macOS 는 x10 만큼 소모합니다.</strong> <a href="https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#minute-multipliers">출처 링크</a></p>
<img width="779" alt="스크린샷 2023-10-13 오후 10 22 23" src="https://github.com/jaesung-0o0/study-ios/assets/53814741/9f8b4e63-540e-4ebe-8224-d064ad1f8496">

<p>즉 테스트 한번에 100-200분을 소모하게 되고 최악의 경우 테스트 10번 돌리면 그 달의 사용가능한 깃헙액션 시간을 다 소진하게 됩니다. 실제로 전부 소진했다는 메일을 자주 받았었습다.</p>
<p>이번에 사이드 프로젝트 앱의 2.0 버전 작업을 하면서 깃헙 액션 작업도 새로 하게 되었고 매번 불필요하게 액션이 돌지 않고 필요할 때 돌릴 수 있도록 <code>issue-comment</code> 로 트리거 하는 방식을 생각했습니다.</p>
<p>커멘트로 깃헙 액션 작동시키는 스크립트 작성하는 방법은 <a href="https://github.com/cozzin">cozzin</a> 님이 작성한 <a href="https://medium.com/@hongseongho/github-action%EC%9C%BC%EB%A1%9C-comment-bot-%EB%A7%8C%EB%93%A4%EA%B8%B0-422e6e471c8e">블로그 포스트</a>을 참고하였기 때문에 이 문서에서 스크립트 작성에 대한 부분은 가볍게 다루고 마주했던 문제를 자세하게 알아보도록 하겠습니다.</p>
<h2 id="2-진행-방법과-결과물">2. 진행 방법과 결과물</h2>
<h3 id="21-스펙">2.1. 스펙</h3>
<p>구상한 스펙은 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>스펙</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>운영체제</td>
<td>macOS</td>
<td></td>
</tr>
<tr>
<td>Xcode</td>
<td>15.0</td>
<td><code>#Preview</code> 매크로를 컴파일할 수 있어야 하기 때문.</td>
</tr>
<tr>
<td>iOS</td>
<td>16.0 &amp; 17.0</td>
<td>최소 지원 버전 &amp; 최신 버전</td>
</tr>
<tr>
<td>트리거 시점</td>
<td><code>/깃헙</code> + (원하는 액션)</td>
<td>예: <code>/깃헙 ios17 앱 빌드</code></td>
</tr>
</tbody></table>
<p>자동화 하려는 항목은 다음과 같았습니다.</p>
<ul>
<li><input checked="" disabled="" type="checkbox"> iOS 최신 버전에서 앱 타겟 빌드 성공 여부 (also 최소 지원버전)</li>
<li><input checked="" disabled="" type="checkbox"> iOS 최신 버전에서 스위프트 패키지 기반 모듈들의 빌드 성공 여부 (also 최소 지원버전)</li>
<li><input checked="" disabled="" type="checkbox"> iOS 최신 버전에서 앱 타겟 테스트 성공 여부 (also 최소 지원버전)</li>
</ul>
<h3 id="22-액션-트리거-시점-설정">2.2. 액션 트리거 시점 설정</h3>
<p>이슈에 댓글을 작성했거나, 댓글을 수정했을 때 깃헙 액션이 트리거 하려면 다음과 같이 작성합니다.</p>
<pre><code class="language-yml">on:
  issue_comment:
    types: [created, edited]</code></pre>
<table>
<thead>
<tr>
<th>타입</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>created</code></td>
<td>댓글이 생성된 경우</td>
</tr>
<tr>
<td><code>edited</code></td>
<td>댓글을 수정한 경우</td>
</tr>
</tbody></table>
<h3 id="23-트리거-키워드">2.3. 트리거 키워드</h3>
<p>댓글에 특정 키워드가 포함되었을 때만 액션을 수행하도록 다음과 같이 작성합니다.</p>
<pre><code class="language-yml">jobs:
  build:
    if: github.event.issue.pull_request &amp;&amp;
      contains(github.event.comment.body, &#39;/깃헙&#39;) &amp;&amp;
      contains(github.event.comment.body, &#39;ios17 앱 빌드&#39;)</code></pre>
<table>
<thead>
<tr>
<th>조건문</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>github.event.issue.pull_request</code></td>
<td>pull request 에 대한 이벤트인가</td>
</tr>
<tr>
<td><code>contains(github.event.comment.body, &#39;/깃헙&#39;)</code></td>
<td>댓글 내용에 <code>/깃헙</code>가 포함되었는가</td>
</tr>
<tr>
<td><code>contains(github.event.comment.body, &#39;ios17 앱 빌드&#39;)</code></td>
<td>댓글 내용에 <code>ios17 앱 빌드</code> 가 포함되었는가</td>
</tr>
</tbody></table>
<h3 id="24-빌드-응답-커멘트">2.4. 빌드 응답 커멘트</h3>
<p>댓글에 키워드가 포함되어있어서 액션을 시작하는 경우 다음과 같이 <code>github-script</code> 를 사용하여 액션을 실행한다고 댓글로 알려줄 수 있습니다.</p>
<pre><code class="language-yml"># iOS17 빌드 응답 커멘트
- name: Add Build comment
  uses: actions/github-script@v5
  with:
    github-token: ${{secrets.GITHUB_TOKEN}}
    script: |
      github.rest.issues.createComment({
        issue_number: context.issue.number,
        owner: context.repo.owner,
        repo: context.repo.repo,
        body: &#39;iOS 17.0 iPhone 15 Pro 에서 앱타겟을 빌드합니다.&#39;
      })</code></pre>
<p><code>github-script</code> 를 사용 시 <code>github-token</code> 을 넣어주면 이어서 작성한 <code>script</code> 를 수행할 수 있습니다. <code>github-token</code> 에는 <code>${{secrets.GITHUB_TOKEN}}</code> 이라고 값을 넣어주면 됩니다. <code>script</code> 에는 <code>github.reset.issues</code> 의 <code>createComment</code> 명령어를 사용하여 <code>body</code> 에 댓글 내용을 작성해줄 수 있습니다.</p>
<h3 id="25-빌드-결과-커멘트">2.5. 빌드 결과 커멘트</h3>
<p>빌드에 성공했다면 <strong>2.4</strong> 와 동일한 방식으로 성공했다는 메세지를 댓글로 알려줄 수 있습니다.</p>
<p>빌드에 실패했거나, 액션 수행 중 실패가 발생한 경우 step 에 <code>if</code> 옵션에 <code>failure()</code> 를 넣어주면 실패한 경우에만 동작하는 step 을 만들 수 있습니다.</p>
<pre><code class="language-yml"># 빌드 실패시 커멘트
- name: Notify failure
  uses: actions/github-script@v5
  with:
    ...
  if: failure()</code></pre>
<h3 id="26-결과물">2.6. 결과물</h3>
<p>작업 결과물은 <a href="https://github.com/ku-ring/ios-app/pull/42">PR #42</a> 에서 확인 가능합니다.</p>
<h2 id="3-마주했던-문제들">3. 마주했던 문제들</h2>
<h3 id="31-액션이-트리거-되지-않는-현상">3.1. 액션이 트리거 되지 않는 현상</h3>
<p>분명 스크립트는 잘 작성했는데 커멘트로 트리거가 되지 않는 현상이 있었고 원인은 다음과 같았습니다.</p>
<blockquote>
<p>&quot;Note: This event will only trigger a workflow run if the workflow file is on the default branch.&quot;
<a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#public">출처 | docs.github.com</a></p>
</blockquote>
<p><code>issue-comment</code> 트리거는 <strong>default branch</strong> 기반으로 돌아가기 때문에 아직 <code>yaml</code> 파일이 <strong>default branch</strong> 에 머지되지 않은 시점에서 <code>issue-comment</code> 트리거 시도를 해도 <strong>default branch 에는 트리거할 액션이 정의되어 있지 않아서 동작하지 않습니다.</strong></p>
<p>이는 다음과 같은 방법으로 해결하였습니다.</p>
<p>테스트 할때는 PR 브랜치로 default branch를 (1)임시로 변경하고, 확인이 끝나면 팀원들에게 공유 후 default branch (2)복구</p>
<h3 id="32-xcode-버전-문제">3.2. Xcode 버전 문제</h3>
<p>스크립트에 테스트가 최신 Xcode 버전에서 돌아가도록 작성했는데, Xcode15 가 아닌, Xcode14.2 (심지어 14.3 도 아닌) 으로 돌아가는 문제가 있었습니다.</p>
<p><code>macos-latest</code> 를 사용하는 경우 맥OS 에 Applications 폴더에는 Xcode_14.2.app 까지만 설치되어 있어서 발생한 문제였습니다.</p>
<p>이는 다음과 같은 방법으로 해결하였습니다.</p>
<blockquote>
<p>Looks like you need to use macos-13, not macos-latest.
<a href="https://github.com/actions/runner-images/issues/7672#issuecomment-1597219552">link</a></p>
</blockquote>
<pre><code>build:
-  runs-on: macOS-latest
+  runs-on: macos-15
    steps:
      - name: Setup Xcode version
        uses: maxim-lobanov/setup-xcode@v1
        with:
          xcode-version: &#39;15.0&#39;</code></pre><h3 id="33-ios-시뮬레이터-버전-문제">3.3. iOS 시뮬레이터 버전 문제</h3>
<p><code>#Preview</code> 매크로 빌드를 위해 Xcode15 환경이 필요했으며 iOS 17 외에도 iOS16 버전의 시뮬레이터에서도 앱타겟을 빌드하고 싶었습니다. 하지만 iOS 16 버전으로 앱타겟을 빌드 시도하면 다음과 같은 에러와 함께 액션일 실패하였습니다.
<code>&quot;Tests must be run on a concrete device&quot;</code></p>
<p>그래서 액션이 수행될 때 사용가능한 시뮬레이터 리스트(<code>xcrun xctrace list devices</code>)를 확인해보았고 iOS17 버전의 시뮬레이터만 목록에 있음을 확인하였습니다.
액션 수행 중 하위 버전의 시뮬레이터를 설치하는 방법이 있으나 소모시간 너무 길고, 특히 macOS 는 깃헙 액션 사용시간이 10배로 적용되기 때문에 최선의 방법은 아니라고 생각하여 아직 해결하지 못한 문제로 남아있습니다.</p>
<h3 id="4-참고">4. 참고</h3>
<ul>
<li><a href="https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#minute-multipliers">docs.github.com/en/billing/managing-billing-for-github-actions</a></li>
<li><a href="https://github.com/ku-ring/ios-app/pull/42">깃헙 액션 yml 파일 결과물</a></li>
<li><a href="https://medium.com/@hongseongho/github-action%EC%9C%BC%EB%A1%9C-comment-bot-%EB%A7%8C%EB%93%A4%EA%B8%B0-422e6e471c8e">medium.com/@hongseongho/github-action으로-comment-bot-만들기</a></li>
<li><a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#public">docs.github.com</a></li>
<li><a href="https://github.com/actions/runner-images/issues/7672#issuecomment-1597219552">github.com/actions/runner-images/issues</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>