<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Ryan-Son.devlog</title>
        <link>https://velog.io/</link>
        <description>문제의 원인을 깊이 파고드는 일을 좋아합니다.</description>
        <lastBuildDate>Mon, 26 Oct 2026 20:11:34 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Ryan-Son.devlog</title>
            <url>https://velog.velcdn.com/images/ryan-son/profile/a8e3caf8-0f93-486d-8c2b-f34d6f211cc0/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Ryan-Son.devlog. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ryan-son" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[TCA Path에 화면을 추가했을 때 크래시가 난 이유]]></title>
            <link>https://velog.io/@ryan-son/TCA-Path%EC%97%90-%ED%99%94%EB%A9%B4%EC%9D%84-%EC%B6%94%EA%B0%80%ED%96%88%EC%9D%84-%EB%95%8C-%ED%81%AC%EB%9E%98%EC%8B%9C%EA%B0%80-%EB%82%9C-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@ryan-son/TCA-Path%EC%97%90-%ED%99%94%EB%A9%B4%EC%9D%84-%EC%B6%94%EA%B0%80%ED%96%88%EC%9D%84-%EB%95%8C-%ED%81%AC%EB%9E%98%EC%8B%9C%EA%B0%80-%EB%82%9C-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Mon, 26 Oct 2026 20:11:34 GMT</pubDate>
            <description><![CDATA[<p>TCA에서 여러 종류의 화면을 하나의 내비게이션 스택으로 관리할 때 <code>Path</code> enum에 화면별 리듀서를 선언합니다. 새로운 화면을 추가하려면 이 enum에 case를 추가하죠.</p>
<p>저희도 이렇게 Path에 화면을 추가하다 크래시를 겪은 적이 있습니다. 시뮬레이터에서는 실행되던 앱이 실기기에서는 크래시가 났습니다. 원인을 조사하는 과정에서 TCA 커뮤니티에도 비슷한 사례가 있다는 것을 알게 됐습니다. <a href="#ref-1">[1]</a></p>
<p>이번 글을 준비하면서 TCA 1.26.2에서도 같은 증상이 나타나는지 확인해 봤습니다. 당시 앱 대신 작은 프로젝트를 만들고 Path의 case 수와 <code>StackState</code>의 원소 수를 따로 바꿨습니다. 화면 상태를 하나만 넣어도 크래시가 났고, 경로를 비운 채 부모 액션을 처리해도 같은 문제가 나타났습니다.</p>
<p>경로에 아무 상태도 넣지 않았는데 왜 문제가 생겼을까요? 호출 스택을 살펴보니 액션 처리에 필요한 리듀서를 구성하는 중에 스택 공간이 부족해지고 있었습니다.</p>
<h2 id="화면을-열지-않고-부모-액션만-처리해도-크래시가-발생합니다">화면을 열지 않고 부모 액션만 처리해도 크래시가 발생합니다</h2>
<p>알림이나 딥링크로 여러 종류의 화면에 진입해야 하면 Path에 선언하는 case도 늘어납니다. 하지만 선언한 화면 종류 수와 실제로 경로에 넣은 상태 수는 별개입니다. 두 조건의 영향을 나누려고 각 case에는 서로 다른 리듀서 타입을 넣되 상태와 처리 로직은 같은 구조로 만들었습니다.</p>
<p>다음은 빈 경로에서 부모 액션을 처리한 실험의 구조입니다. 실제로는 Path에 128개 case를 선언했으며, 아래에는 세 개만 남겼습니다. 화면별 리듀서 구현과 측정용 출력은 생략했습니다.</p>
<pre><code class="language-swift">@Reducer
enum Path {
  case boardList(BoardList)
  case postDetail(PostDetail)
  case payment(Payment)
  // 실험에서는 총 128개 case
}

@Reducer
struct Host {
  @ObservableState
  struct State {
    var path = StackState&lt;Path.State&gt;()
  }
  enum Action {
    case ping
    case path(StackActionOf&lt;Path&gt;)
  }
  var body: some ReducerOf&lt;Self&gt; {
    Reduce { _, _ in .none }
      .forEach(\.path, action: \.path)
  }
}

var state = Host.State()
// append 없이 부모 액션만 처리합니다.
_ = Host()._reduce(into: &amp;state, action: .ping)</code></pre>
<p><code>_reduce</code>는 이번 실험에서 리듀서를 직접 실행하기 위해 사용했습니다. 실제 UI나 Store는 만들지 않았습니다. 부모 상태와 리듀서 인스턴스를 생성하는 것만으로는 크래시가 나지 않았지만, 위처럼 빈 상태에서 <code>.ping</code>을 처리하자 크래시가 났습니다. 부모 <code>body</code>나 <code>Path.body</code>만 평가해도 같은 결과였습니다.</p>
<p>비교할 때 고정한 것은 함수 실행에 쓰는 메모리인 <strong>호출 스택</strong>의 크기입니다. 화면 이동 기록인 <strong>내비게이션 경로</strong>의 길이와는 별개입니다. macOS에서 별도 스레드를 만들고 시작 전에 <code>stackSize</code>를 1MiB(1,048,576바이트)로 지정해 조건마다 같은 크기로 비교했습니다. <a href="#ref-2">[2]</a></p>
<p>Apple의 <em>Threading Programming Guide</em>에도 iOS 메인 스레드의 스택 크기가 다음과 같이 나옵니다. <a href="#ref-7">[7]</a></p>
<blockquote>
<p>1 MB (iOS main thread)</p>
</blockquote>
<p>다만 이번에 실행한 것은 macOS의 별도 스레드입니다. 스택 크기를 비슷하게 맞춰도 툴체인과 실행 중 사용하는 공간이 다르므로, 아래의 case 수를 iOS에서도 크래시가 발생하는 기준으로 볼 수는 없습니다.</p>
<p>빈 경로 실험에서는 최적화를 끈 <code>-Onone</code>과 요청 스택 1MiB 조건에서 부모 액션 처리와 두 body 평가가 각각 세 번 모두 크래시가 났습니다. 스택을 8MiB로 늘리거나 같은 소스와 의존성을 유지한 채 <code>-O</code>로 컴파일하면 모두 정상 종료됐습니다. 먼저 빈 경로의 부모 액션이 어디서 멈췄는지 LLDB로 살펴봤습니다.</p>
<h2 id="부모-액션을-처리하기-전에-pathbody를-구성했습니다">부모 액션을 처리하기 전에 Path.body를 구성했습니다</h2>
<p>부모의 <code>.ping</code> 처리 부분에 넣은 출력도 크래시가 난 실행에서는 남지 않았습니다. 다음은 빈 경로에서 <code>.ping</code>을 보냈을 때의 호출 경로입니다. 중간의 클로저와 프로토콜 호출 프레임은 생략했습니다.</p>
<p><img src="https://velog.velcdn.com/images/ryan-son/post/62f56e6d-7a51-444a-8d60-222df1450321/image.png" alt="빈 경로의 부모 액션에서 Path.body를 구성하다 발생한 크래시"></p>
<p>여기서 <code>body</code>는 화면을 그리는 SwiftUI의 <code>body</code>가 아니라, 액션을 처리할 리듀서를 만드는 코드입니다. 부모의 <code>body</code>에 있는 <code>forEach</code>는 경로용 리듀서를 구성하면서 <code>Path.body</code>를 호출합니다. TCA 소스에서도 enum용 <code>forEach</code>가 <code>Path.body</code>를 제공하고, 이를 받는 <code>forEach</code>가 <code>destination()</code>을 호출하는 것을 확인했습니다. <a href="#ref-3">[3]</a> <a href="#ref-6">[6]</a></p>
<p>이 구성은 경로의 원소를 찾아 자식 액션을 처리하기 전에 일어납니다. 그래서 경로가 비어 있고 액션이 부모의 <code>.ping</code>이어도 <code>Path.body</code>를 실행했던 것입니다.</p>
<p>스택 공간을 비교한 기존 실험에서도 같은 위치에서 멈췄습니다. 그 실험은 경로에 원소 하나를 넣고 해당 원소에 액션을 보낸 조건입니다. 아래의 어셈블리와 case 수별 비교는 이 조건에서 얻은 결과입니다.</p>
<p><code>___chkstk_darwin</code>은 큰 스택 공간을 확보할 때 접근할 페이지를 검사하는 코드입니다. 검사 중 크래시가 났기 때문에 <code>Path.body</code>가 얼마나 큰 공간을 확보하려 했는지 어셈블리를 확인했습니다.</p>
<p>스택 검사 뒤에는 스택 포인터를 이동하는 두 명령이 있었습니다.</p>
<pre><code class="language-asm">sub sp, sp, #0x206, lsl #12
sub sp, sp, #0x140</code></pre>
<p>첫 명령은 스택 포인터 <code>sp</code>에서 <code>0x206 &lt;&lt; 12</code>만큼, 두 번째 명령은 <code>0x140</code>만큼 뺍니다. 합하면 약 2.02MiB입니다. <code>Path.body</code> 하나를 실행하는 데 확보하려는 공간이 스레드에 지정한 약 1MiB보다 컸던 것이죠.</p>
<p>스택 검사에서 접근한 주소도 읽고 쓸 수 없는 보호 영역에 속했습니다. 공간 부족 때문인지 확인하려고 같은 실행 파일에서 스택 크기만 8MiB로 늘렸더니, 이번에는 자식 리듀서까지 실행됐습니다.</p>
<p>그렇다면 짧은 enum 선언에서 만들어진 <code>Path.body</code>가 왜 이렇게 많은 공간을 필요로 했을까요?</p>
<h2 id="매크로는-path의-리듀서들을-하나로-조합합니다">매크로는 Path의 리듀서들을 하나로 조합합니다</h2>
<p><code>@Reducer</code>는 enum에 적힌 case를 바탕으로 상태와 액션, 리듀서 구성을 생성합니다. TCA 1.26.2의 매크로 소스를 보면 화면마다 <code>ifCaseLet</code>을 붙이고, 이를 <code>Reduce</code>로 감싼 <code>body</code>를 만듭니다. <a href="#ref-4">[4]</a></p>
<p>앞의 세 화면에 해당하는 조합 부분을 단순화하면 다음과 같습니다. 모듈 접두사와 생성되는 다른 선언은 생략했습니다.</p>
<pre><code class="language-swift">Reduce(
  EmptyReducer&lt;State, Action&gt;()
    .ifCaseLet(
      \State.Cases.boardList,
      action: \Action.Cases.boardList
    ) {
      BoardList()
    }
    .ifCaseLet(
      \State.Cases.postDetail,
      action: \Action.Cases.postDetail
    ) {
      PostDetail()
    }
    .ifCaseLet(
      \State.Cases.payment,
      action: \Action.Cases.payment
    ) {
      Payment()
    }
)</code></pre>
<p>각 <code>ifCaseLet</code>은 앞서 조합한 리듀서에 해당 화면의 리듀서를 덧붙입니다. case가 늘면 조합도 길어지고, <code>Path.body</code>는 이 조합을 구성한 뒤에야 액션을 처리할 리듀서를 반환합니다. <code>StackState</code>가 비어 있어도 <code>Path.body</code>를 평가하면 이 조합을 구성합니다.</p>
<p><img src="https://velog.velcdn.com/images/ryan-son/post/f01ba23c-2611-4a64-b25b-e7c412201a5f/image.png" alt="매크로의 코드 생성 과정과 실행 중 리듀서 구성 단계를 구분한 도식"></p>
<p><em>이번 Debug 크래시는 자식 화면의 액션을 처리하기 전, 리듀서를 구성하는 중에 발생했습니다.</em></p>
<p>원소 하나를 둔 실험에서는 화면 종류가 83개일 때 정상 종료됐고 84개부터 Debug 크래시가 발생했습니다. 첫 case와 마지막 case에 각각 세 번씩 액션을 보내도 결과는 같았습니다. 같은 조건의 Release에서는 확인한 128개까지 모두 정상 종료됐습니다.</p>
<p>이 조합이 길어지면 <code>Path.body</code>에 필요한 공간도 늘어났습니다. 함수 진입 때 확보하는 공간은 화면 종류가 41개일 때 약 218KiB, 83개일 때 약 877KiB, 128개일 때 약 2.02MiB였습니다. 다만 이 값에는 이후 다른 함수를 호출할 때 쓰는 공간은 포함되지 않습니다.</p>
<p>84개에서는 함수 진입의 스택 검사를 통과했는데도 크래시가 났습니다. 호출 스택을 보니 Swift 런타임이 리듀서의 타입 정보를 구성하는 함수들을 연이어 호출하고 있었습니다. <code>ifCaseLet</code>으로 중첩한 제네릭 타입을 다루는 과정에서 추가로 쓴 공간까지 합쳐 스택 한도를 넘은 것입니다.</p>
<p>128개에서는 <code>Path.body</code>에 필요한 공간을 확보하기 전에 멈췄고, 84개에서는 그 공간을 확보한 뒤 타입 정보를 구성하다 멈춘 것이죠. 두 경우 모두 자식 리듀서의 액션 처리에는 도달하지 못했습니다.</p>
<h2 id="release에서는-리듀서를-구성하는-데-필요한-스택-공간이-줄었습니다">Release에서는 리듀서를 구성하는 데 필요한 스택 공간이 줄었습니다</h2>
<p>그런데 같은 128개 case를 둔 Release 빌드는 왜 정상 실행됐을까요? Debug에서 크래시가 났던 <code>Path.body</code>부터 비교했습니다.</p>
<p>Release에서도 부모의 <code>forEach</code>는 <code>Path.body</code>를 호출했습니다. 다만 실제 처리는 컴파일러가 최적화한 함수로 이어졌고, LLDB에서는 <code>specialized … PathA_128.body.getter</code>로 표시됐습니다. 따라서 이 함수가 확보하는 스택 공간을 비교했습니다.</p>
<p>함수 진입부에는 다음 두 명령이 있었습니다.</p>
<pre><code class="language-asm">sub sp, sp, #0x56, lsl #12
sub sp, sp, #0x6a0</code></pre>
<p>두 감소량을 합하면 <code>0x566a0</code>바이트, 약 346KiB입니다. Debug의 약 2.02MiB보다 훨씬 작습니다. 두 값 모두 해당 명령으로 확보하는 공간이며, 레지스터 저장과 이후 함수 호출에 쓰는 공간은 제외했습니다.</p>
<p>요청 스택 1MiB에서 실행 결과도 비교했습니다. 경로 원소 하나를 두고 첫 case와 마지막 case를 각각 세 번 실행하자 Debug는 모두 크래시가 났고 Release는 모두 액션 처리를 마쳤습니다. Release에서는 함수 진입에 필요한 공간이 줄면서 자식 리듀서까지 실행할 수 있었습니다.</p>
<h2 id="임시값들이-같은-스택-공간을-재사용하고-있었습니다">임시값들이 같은 스택 공간을 재사용하고 있었습니다</h2>
<p>Debug와 Release는 최적화 외의 설정도 다를 수 있습니다. 최적화만으로 이 차이가 생기는지 확인하려고 같은 소스와 <code>DEBUG</code> 조건, 이미 빌드한 의존성을 유지했습니다. 이 코드의 옵션만 <code>-Onone</code>에서 <code>-O</code>로 바꿔도 확보하는 공간은 약 2.02MiB에서 346KiB로 줄었습니다.</p>
<p>컴파일러가 무엇을 바꿨는지 보려고 중간 단계의 코드도 출력했습니다. Swift 컴파일러는 소스를 SIL로 변환하고, 이후 LLVM IR을 거쳐 기계어를 만듭니다. <a href="#ref-8">[8]</a> 먼저 최적화하지 않은 SIL에서 리듀서를 조합하는 부분을 살펴봤습니다.</p>
<p><code>ifCaseLet</code>을 이어 붙이면 앞서 조합한 리듀서를 포함하는 중간 값들이 만들어집니다. 최적화하지 않은 SIL에는 이 값들을 스택에 담는 <code>alloc_stack</code> 명령들이 있었습니다.</p>
<p>최적화하면 이런 저장 공간이 모두 없어지는 걸까요? 최적화한 LLVM IR에도 스택 공간을 확보하는 <code>alloca</code>가 남아 있었습니다. 함께 보이는 <code>llvm.lifetime.start</code>와 <code>llvm.lifetime.end</code>는 그 공간을 사용하는 구간을 표시합니다.</p>
<p>값이 모두 같은 시점에 필요한 것은 아니므로, 사용을 마친 공간을 다른 값이 다시 쓸 수 있습니다. LLVM의 <strong>stack coloring</strong>은 이런 사용 구간을 분석해 임시값들이 같은 스택 공간을 공유하도록 하는 최적화입니다. <a href="#ref-9">[9]</a> 따라서 임시값의 개수만으로 최종 스택 크기를 설명할 수는 없습니다.</p>
<p>이 최적화의 영향을 확인하려고 <code>-O</code>를 유지한 채 <code>-Xllvm -no-stack-coloring</code>을 추가했습니다. stack coloring만 끄자 같은 함수가 확보하는 공간이 약 <strong>346KiB에서 2.81MiB로 늘었습니다</strong>.</p>
<p>실행 결과도 달라졌습니다. 요청 스택 1MiB에서 첫 case와 마지막 case를 각각 세 번 실행했을 때, <code>-O</code>는 모두 정상 종료됐지만 stack coloring을 끈 실행은 모두 크래시가 났습니다. 임시 저장 공간을 재사용하는 최적화가 이번 함수의 스택 크기를 줄이는 데 중요한 역할을 했음을 확인한 것입니다.</p>
<p>최적화로 공간이 줄어든 이유는 확인했지만 Debug에서는 여전히 크래시가 났습니다. 그래서 비교한 또 다른 방법은 한 <code>body</code>에서 조합하는 리듀서 수를 줄이는 것이었습니다.</p>
<h2 id="path를-나눠-한-번에-조합하는-리듀서-수를-줄였습니다">Path를 나눠 한 번에 조합하는 리듀서 수를 줄였습니다</h2>
<p>현재 저희 앱은 최상위 Path에 41개의 기능 영역을 두고 각 영역의 하위 Path에 화면을 나누고 있습니다. 직접 연결된 하위 Path의 case를 합하면 112개지만 가장 큰 하위 Path는 10개입니다. 모든 화면을 하나의 <code>body</code>에서 조합하는 대신, 상위 Path는 기능 영역을 연결하고 하위 Path는 그 영역의 화면들을 조합합니다.</p>
<p>이 구조를 표준 매크로로 비교할 때는 하위 Path를 연결하는 Reducer 어댑터를 뒀습니다. 하위 enum의 <code>body</code>를 상위 Path에서 인스턴스로 연결하기 위한 코드입니다. 실험 코드의 이름을 게시판 예시에 맞추면 다음과 같습니다. 화면별 리듀서와 다른 기능의 선언은 생략했습니다.</p>
<pre><code class="language-swift">@Reducer
enum BoardPath {
  case list(BoardList)
  case postDetail(PostDetail)
}

struct BoardRouter: Reducer {
  typealias State = BoardPath.State
  typealias Action = BoardPath.Action

  var body: some ReducerOf&lt;Self&gt; {
    BoardPath.body
  }
}

@Reducer
enum AppPath {
  case board(BoardRouter)
}</code></pre>
<p><code>BoardRouter</code>는 <code>BoardPath</code>의 상태와 액션을 그대로 사용하고 처리는 <code>BoardPath.body</code>에 맡깁니다. 상위 <code>AppPath.body</code>에서는 각 화면을 직접 나열하는 대신 이 어댑터를 조합합니다.</p>
<p>각 Path의 case 수를 앱과 같게 구성한 테스트 프로젝트에서, 위와 같은 연결 방식으로 표준 매크로를 다시 실행했습니다. 화면 상태와 로직은 앞의 테스트용 구현을 사용했습니다. 경로 원소 수를 1개, 8개, 48개로 바꿔도 Debug와 Release에서 모두 크래시 없이 실행됐습니다.</p>
<p>이 결과는 커스텀 매크로만이 대안은 아니라는 점을 보여줬습니다. 표준 매크로를 유지하면서 한 Path에서 조합하는 리듀서 수를 줄이는 방법도 검토할 수 있었습니다.</p>
<h2 id="switch로-바꾸면-effect-취소는-어떻게-될까요">switch로 바꾸면 effect 취소는 어떻게 될까요</h2>
<p>저희 앱은 Path를 나누는 것 외에 별도의 매크로로 조합 방식도 바꿨습니다. 이 매크로는 <code>ifCaseLet</code>을 이어 붙이는 대신 상태와 액션을 <code>switch</code>로 분기해 해당 자식 리듀서를 직접 호출합니다.</p>
<p>이번 실험에서는 직접 작성한 단순 switch와 앱의 매크로가 생성한 코드를 따로 비교했습니다. 두 구현 모두 하나의 Path에 128개 case를 둔 Debug 조건에서 크래시 없이 실행됐습니다.</p>
<p>다만 <code>ifCaseLet</code>이 하던 취소 처리도 남아 있는지 확인해야 했습니다. <code>ifCaseLet</code>은 case가 바뀌면 이전 자식의 effect를 취소합니다. <a href="#ref-5">[5]</a> 앱의 매크로는 이 처리를 생성하지 않지만, 경로를 처리하는 바깥의 <code>forEach</code>는 그대로 사용합니다. <code>forEach</code>는 경로에서 제거된 원소의 ID를 기준으로 effect를 취소합니다. <a href="#ref-6">[6]</a></p>
<p>취소 조건이 다르므로 테스트도 둘로 나눴습니다. 앱의 매크로와 경로 편집 함수를 연결하고, effect가 나중에 응답하도록 만든 뒤 기존 원소를 제거하거나 같은 ID의 상태를 다른 화면 종류로 바꿨습니다.</p>
<p>기존 원소를 제거하고 새 ID로 추가했을 때는 이전 effect가 취소됐습니다. 같은 ID를 유지한 채 화면 종류만 바꾸자 이전 응답이 도착했습니다. 이 경우에는 원소의 ID가 그대로 남아 있어 <code>forEach</code>의 취소 조건에 해당하지 않았던 것이죠.</p>
<p>도착한 액션과 새 상태가 맞지 않아 불일치 진단이 남았고 새 상태는 바뀌지 않았지만, effect 자체를 취소한 결과는 아니었습니다.</p>
<p>제거 시점도 확인했습니다. 앱에는 새 화면을 먼저 추가하고 전환이 끝난 뒤 이전 원소를 제거하는 코드가 있습니다. 테스트에서도 이전 원소가 남아 있는 동안에는 응답을 받았고, 전환 완료 액션으로 그 원소를 제거한 뒤에는 effect가 취소됐습니다. 전환 완료는 테스트에서 직접 알렸으며 실제 화면 애니메이션은 실행하지 않았습니다.</p>
<p>이처럼 원소를 제거하는 동작은 취소까지 이어졌지만, 같은 ID로 화면 종류만 교체하는 동작은 그렇지 않았습니다. 크래시 없이 실행됐다는 결과와 기존 취소 동작을 유지했다는 결과를 따로 확인해야 했던 이유입니다.</p>
<h2 id="실험-환경">실험 환경</h2>
<ul>
<li>Xcode 27.1, Apple Swift 6.4, macOS 27.0.1 arm64, TCA 1.26.2를 사용했습니다. TCA와 SwiftSyntax 602.0.0을 포함한 의존성은 비교 구현에서 동일하게 고정했습니다.</li>
<li>macOS 별도 스레드에 요청한 스택 크기는 1,048,576바이트이며 시스템 보고값은 1,060,864바이트였습니다. 매 실행은 새 프로세스에서 진행해 최초 타입 정보 구성도 포함했습니다.</li>
<li>빈 경로 실험은 128개 case에서 인스턴스 생성, 부모 액션 처리, 부모 body 평가, Path.body 평가를 각각 세 번 비교했습니다. <code>-Onone</code>·<code>-O</code>와 요청 스택 1MiB·8MiB를 조합한 48회 실행이며 <code>DEBUG</code> 조건과 Debug 의존성은 고정했습니다.</li>
<li>case 수별 경계와 프레임 크기를 비교한 실험에서는 경로 원소 수를 1로 고정하고 첫 case와 마지막 case를 각각 검사했습니다. Debug 경계 비교와 같은 조건의 Release 비교는 조합별로 세 번 반복했습니다.</li>
<li>계층 비교는 41개 기능 영역과 하위 case 합계 112개의 분포를 재현했습니다. 표준 구현에는 하위 enum의 <code>body</code>를 연결하는 상태 없는 Reducer 어댑터를 사용했습니다. 실제 앱의 상태 크기와 화면 로직을 가져온 실험은 아닙니다.</li>
</ul>
<h2 id="참고자료">참고자료</h2>
<p><a id="ref-1"></a></p>
<ol>
<li><a href="https://github.com/pointfreeco/swift-composable-architecture/discussions/3162">TCA 커뮤니티의 내비게이션 크래시 논의 #3162</a>. 원인을 조사하며 참고한 유사 사례입니다. Path의 case 증가에 따른 실기기 크래시와 Debug·Release 차이를 다룹니다.
<a id="ref-2"></a></li>
<li><a href="https://developer.apple.com/documentation/foundation/thread/stacksize">Apple Thread.stackSize</a>. 스레드의 스택 크기를 지정하는 속성입니다. 실제 스택 크기에 반영하려면 스레드 시작 전에 설정해야 합니다.
<a id="ref-3"></a></li>
<li><a href="https://github.com/pointfreeco/swift-composable-architecture/blob/377da4061db10d26337a71bb279c506bb951f50f/Sources/ComposableArchitecture/CaseReducer.swift">TCA 1.26.2 CaseReducer.swift</a>. StackState용 <code>forEach</code>가 <code>DestinationState.StateReducer.body</code>를 사용하는 구현입니다.
<a id="ref-4"></a></li>
<li><a href="https://github.com/pointfreeco/swift-composable-architecture/blob/377da4061db10d26337a71bb279c506bb951f50f/Sources/ComposableArchitectureMacros/ReducerMacro.swift">TCA 1.26.2 ReducerMacro.swift</a>. enum의 case별 <code>ifCaseLet</code> 조합과 static <code>body</code> 생성 부분입니다.
<a id="ref-5"></a></li>
<li><a href="https://github.com/pointfreeco/swift-composable-architecture/blob/377da4061db10d26337a71bb279c506bb951f50f/Sources/ComposableArchitecture/Reducer/Reducers/IfCaseLetReducer.swift">TCA 1.26.2 IfCaseLetReducer.swift</a>. case 전환 전후의 내비게이션 ID를 비교하고 이전 자식의 effect를 취소하는 구현입니다.
<a id="ref-6"></a></li>
<li><a href="https://github.com/pointfreeco/swift-composable-architecture/blob/377da4061db10d26337a71bb279c506bb951f50f/Sources/ComposableArchitecture/Reducer/Reducers/StackReducer.swift">TCA 1.26.2 StackReducer.swift</a>. 액션 처리 전후의 경로 ID를 비교하고 제거된 원소의 effect를 취소하는 구현입니다.
<a id="ref-7"></a></li>
<li><a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/CreatingThreads/CreatingThreads.html">Apple Threading Programming Guide — Thread Management</a>. 보관된 문서의 Table 2-1은 iOS 메인 스레드의 스택 공간을 “1 MB”로 기재합니다. 본문 인용은 표의 해당 항목이며 최신 기기의 실측값은 아닙니다.
<a id="ref-8"></a></li>
<li><a href="https://www.swift.org/documentation/swift-compiler/">Swift Compiler — Swift.org</a>. Swift 소스에서 SIL, LLVM IR과 기계어로 이어지는 컴파일 단계를 설명합니다.
<a id="ref-9"></a></li>
<li><a href="https://github.com/llvm/llvm-project/blob/main/llvm/lib/CodeGen/StackColoring.cpp">LLVM StackColoring.cpp</a>. 스택 공간의 사용 구간을 분석해 공간을 공유하는 구현과 <code>no-stack-coloring</code> 옵션입니다. 공개 구현은 동작 설명에 참고했고, 본문의 수치는 Xcode 27.1에 포함된 컴파일러에서 해당 옵션을 켜고 끈 결과입니다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[파일은 같은데, 빌드는 왜 다르게 판단할까]]></title>
            <link>https://velog.io/@ryan-son/%ED%8C%8C%EC%9D%BC%EC%9D%80-%EA%B0%99%EC%9D%80%EB%8D%B0-%EB%B9%8C%EB%93%9C%EB%8A%94-%EC%99%9C-%EB%8B%A4%EB%A5%B4%EA%B2%8C-%ED%8C%90%EB%8B%A8%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@ryan-son/%ED%8C%8C%EC%9D%BC%EC%9D%80-%EA%B0%99%EC%9D%80%EB%8D%B0-%EB%B9%8C%EB%93%9C%EB%8A%94-%EC%99%9C-%EB%8B%A4%EB%A5%B4%EA%B2%8C-%ED%8C%90%EB%8B%A8%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Mon, 12 Oct 2026 19:02:27 GMT</pubDate>
            <description><![CDATA[<p>이전 글에서는 코드를 바꾸지 않았는데도 Xcode 26.3이 빌드 계획을 매번 새로 만드는 문제를 살펴봤습니다. 최소 재현 프로젝트에서는 파일 복사 등 일부 작업만 반복됐고 Swift 파일은 다시 컴파일하지 않았습니다. 그런데 같은 Xcode 버전으로 빌드한 실제 앱의 로그에는 컴파일 작업까지 반복해서 나타났습니다.</p>
<p>계획을 새로 만든다는 사실만으로는 두 결과의 차이를 설명할 수 없었습니다. 이번에는 소스 코드와 드라이버에 전달한 명령이 같아도 드라이버 작업이 반복될 수 있는 이유를 입력 목록에서 찾아봅니다. 이어서 캐시 동작을 살펴보며, 로그에 나타난 컴파일 작업과 실제 재컴파일을 구분하려면 무엇을 확인해야 하는지 알아보겠습니다.</p>
<h2 id="소스-코드도-드라이버에-전달한-명령도-같았습니다">소스 코드도 드라이버에 전달한 명령도 같았습니다</h2>
<p>빌드를 다시 요청해도 모든 작업이 다시 실행되는 것은 아닙니다. 빌드 시스템은 필요한 개별 작업을 골라 실행합니다. 이 글에서 다루는 Swift 드라이버 작업도 그중 하나로, 어떤 파일을 컴파일할지 정하고 컴파일러에 전달할 작업을 구성합니다. 컴파일 작업이 반복된 이유를 찾기 위해, 로그에 나타난 드라이버 작업에 무엇을 전달했는지부터 비교했습니다.</p>
<p>두 빌드의 드라이버 커맨드라인 인자는 모두 같았습니다. 명령이 읽는 소스 파일 목록 등 보조 파일의 내용도 같았고, 두 빌드에 사용한 소스 코드 사본도 같았습니다.</p>
<p>빌드 시스템은 작업에 필요한 파일을 <strong>입력</strong>으로 등록해 실행 여부를 판단할 때 사용합니다. 명령이 같아도 등록된 파일은 다를 수 있으므로, 이번에는 입력 목록을 비교했습니다.</p>
<h2 id="같은-입력-파일이-다른-순서로-기록됐습니다">같은 입력 파일이 다른 순서로 기록됐습니다</h2>
<p>입력 목록은 <strong>빌드 manifest</strong>에서 확인할 수 있습니다. 각 작업의 명령과 입력·출력 파일을 기록한 파일입니다. 작업을 실행하는 엔진인 llbuild는 이 기록과 작업 사이의 의존 관계를 따라 빌드를 진행합니다.</p>
<p>실제 앱은 Swift 매크로를 사용하므로, 드라이버 작업의 입력에는 소스 파일 외에 매크로를 실행하는 플러그인 파일도 포함됩니다.</p>
<p>앱 타깃의 <code>SwiftDriver Compilation</code> 작업을 비교하니, 두 manifest에서 작업을 식별하는 키는 같았지만 입력 목록의 순서가 달랐습니다. 1,272개 항목 중 위치가 바뀐 여섯 파일은 모두 매크로 플러그인이었습니다.</p>
<p>아래 번호는 목록 안의 위치입니다. 공통 경로는 생략하고 사내 플러그인 두 개는 공개용 이름으로 바꿨습니다.</p>
<pre><code class="language-diff"># 입력 위치: 앞선 빌드 → 다음 빌드
- 1250: PerceptionMacros
+ 1250: SwiftNavigationMacros
- 1252: DependenciesMacrosPlugin
+ 1252: InHouseMacrosA
- 1253: SwiftNavigationMacros
+ 1253: PerceptionMacros
- 1254: InHouseMacrosB
+ 1254: CasePathsMacros
- 1255: CasePathsMacros
+ 1255: DependenciesMacrosPlugin
- 1256: InHouseMacrosA
+ 1256: InHouseMacrosB</code></pre>
<p>파일이 추가되거나 빠지지 않았는데도 순서만 달라졌습니다. 빌드 시스템은 이 차이도 작업의 변화로 판단할까요?</p>
<h2 id="입력-순서가-드라이버-작업-실행에-영향을-주는-이유">입력 순서가 드라이버 작업 실행에 영향을 주는 이유</h2>
<p>llbuild는 작업 이름과 입력·출력 이름 등을 결합한 <strong>작업 서명</strong>을 비교합니다. 이 값이 이전 실행 때와 달라지면 작업이 바뀌었다고 판단합니다. 앞 글에서는 계획 전체를 재사용할 때 쓰는 서명을 살펴봤습니다. 여기서는 개별 작업의 실행 여부를 판단하는 서명을 살펴봅니다.</p>
<p><code>ExternalCommand::getSignature()</code>의 반복문을 보면 입력 순서가 서명에 어떻게 반영되는지 알 수 있습니다. <a href="#ref-1">[1]</a></p>
<pre><code class="language-cpp">CommandSignature code(getName());
for (const auto* input: inputs) {
  code = code.combine(
      input-&gt;getName());
}</code></pre>
<p><em>작업 서명을 계산하는 코드 중 입력을 처리하는 부분입니다. 이후 출력 이름과 플래그도 결합합니다.</em></p>
<p>반복문은 입력 이름을 목록 순서대로 읽어 서명에 반영합니다. 다음 이름을 처리할 때는 앞서 계산한 값을 사용하므로 순서도 계산 결과에 영향을 줍니다. 입력을 정렬하는 처리가 없기 때문에, 같은 파일들이어도 나열된 순서가 달라지면 서명이 달라질 수 있습니다.</p>
<p><code>BuildEngine</code>은 현재 작업의 서명과 이전 실행 때 저장한 서명을 비교합니다. 서로 다르면 상태를 <code>NeedsToRun</code>으로 바꿔 해당 작업을 다시 실행하도록 결정합니다. <a href="#ref-2">[2]</a></p>
<pre><code class="language-cpp">if (ruleInfo.rule-&gt;signature !=
    ruleInfo.result.signature) {
  ruleInfo.state =
    RuleInfo::StateKind::NeedsToRun;
  // 실행 이유 알림 생략
  return true;
}</code></pre>
<p><em>실행 판정에 필요한 부분을 발췌하고 줄바꿈을 조정했습니다. 생략한 코드에서는 해당 작업을 다시 실행하는 이유를 <code>SignatureChanged</code>로 알립니다.</em></p>
<p>드라이버 작업의 서명에도 입력 정보가 포함됩니다. <a href="#ref-3">[3]</a> 앞서 두 manifest에서 작업의 키는 같았지만, 키는 어떤 작업인지 식별하는 값입니다. <strong>같은 작업이라도 입력 순서 때문에 서명이 달라지면 다시 실행할 수 있습니다.</strong></p>
<h2 id="매크로-입력-목록은-정렬되지-않았습니다">매크로 입력 목록은 정렬되지 않았습니다</h2>
<p>그렇다면 같은 매크로 파일들이 왜 다른 순서로 기록됐을까요? swift-build에 바로 이 차이를 다룬 PR #1203이 있었습니다. <a href="#ref-4">[4]</a></p>
<p>수정 전 코드는 순서를 보장하지 않는 <code>Set</code>에서 매크로 정보를 가져왔습니다. 아래의 명령 인자(<code>args</code>)와 추가 입력 파일(<code>extraInputPaths</code>)을 만드는 코드를 비교하면, 명령 인자에만 <code>.sorted()</code>가 적용돼 있습니다.</p>
<pre><code class="language-swift">args.append(
    contentsOf: macroDescriptors
        .sorted()
        .flatMap(\.compilerFlags)
)
extraInputPaths.append(
    contentsOf: macroDescriptors
        .map(\.path)
)</code></pre>
<p>PR은 먼저 정렬한 목록을 만든 뒤 두 곳에서 함께 사용하도록 바꿨습니다.</p>
<pre><code class="language-swift">let sortedDescriptors =
    macroDescriptors.sorted()

args.append(
    contentsOf: sortedDescriptors
        .flatMap(\.compilerFlags)
)
extraInputPaths.append(
    contentsOf: sortedDescriptors
        .map(\.path)
)</code></pre>
<p><em>PR에서 발췌한 코드를 수정 전후로 나누고 줄바꿈을 조정했습니다.</em></p>
<p>명령 인자는 정렬하고 입력 파일은 정렬하지 않는 구조라면, 커맨드라인이 같아도 입력 순서는 달라질 수 있습니다. 다만 이 수정이 특정 Xcode 버전에 포함됐는지까지 확인한 것은 아닙니다.</p>
<p>입력 순서가 드라이버 작업의 재실행에 영향을 줄 수 있다는 점은 확인했습니다. <strong>그렇다면 드라이버를 다시 실행하면 Swift 파일도 다시 컴파일할까요?</strong></p>
<h2 id="드라이버-작업은-반복됐지만-swiftcompile-로그는-없었습니다">드라이버 작업은 반복됐지만 SwiftCompile 로그는 없었습니다</h2>
<p>실제 앱에는 다른 요인도 많으므로, 매크로 플러그인 네 개를 사용하는 작은 Swift 패키지에서 확인했습니다. macOS 대상으로 빌드했고, 첫 편에서 다룬 문제가 함께 나타나지 않도록 동기화 폴더는 사용하지 않았습니다.</p>
<p>앞서 본 매크로 입력 목록은 빌드 계획을 구성하는 코드에서 만들어집니다. 기존 계획을 재사용하는 조건과 목록을 새로 구성하는 조건을 비교하려면, 계획을 다시 만들 방법이 필요했습니다. 소스를 수정하면 그 변경 때문에 컴파일할 수 있으므로 소스는 그대로 두기로 했습니다.</p>
<p>소스를 건드리지 않고 바꿀 수 있는 조건으로 환경 변수를 사용했습니다. Swift Build 구현을 보면, 빌드 계획의 서명에는 <code>xcodebuild</code>가 전달받은 환경 정보도 포함됩니다. 따라서 프로젝트에서 사용하지 않는 변수라도 서명에 포함되는 값이라면, 그 값을 바꿨을 때 기존 계획을 재사용하지 못할 수 있습니다. <a href="#ref-6">[6]</a></p>
<p>관련 이슈 #1149에서도 CI 실행마다 달라지는 환경 변수 때문에 계획을 재사용하지 못하는 현상을 보고했습니다. PR #1441은 일부 셸 환경 변수를 서명에서 제외하도록 수정했습니다. 환경 변수 전체를 제외한 것은 아니므로, 실험에 사용할 변수도 같은 영향을 주는지는 빌드 로그로 확인했습니다.</p>
<p>실험용 변수의 이름은 <code>PIF_REPLAN_NOISE</code>로 정했습니다. Xcode가 제공하는 옵션이 아니라 수집 스크립트에서 붙인 이름입니다. Python 스크립트가 이 변수를 <code>xcodebuild</code> 프로세스의 환경에 추가했으며, 같은 전달 방식을 셸 명령으로 표현하면 다음과 같습니다.</p>
<pre><code class="language-sh"># 같은 값을 유지하며 반복
PIF_REPLAN_NOISE=fixed \
  xcodebuild build ...

# 다음 실행마다 값 변경
PIF_REPLAN_NOISE=noise-1 \
  xcodebuild build ...
PIF_REPLAN_NOISE=noise-2 \
  xcodebuild build ...</code></pre>
<p><em>실제 실행한 전체 명령을 옮긴 것은 아닙니다. <code>...</code>는 동일하게 유지한 빌드 인자를 생략한 표시입니다. 명령 앞의 변수 할당은 해당 프로세스에 환경 변수를 전달합니다.</em></p>
<p>소스와 DerivedData를 유지하고 변수 값을 고정한 조건과 매번 바꾼 조건으로 빌드했습니다. 계획을 새로 만들었는지는 각 로그의 <code>Build description signature:</code> 기록으로 확인했습니다. Xcode 26.3에서 값을 고정하면 기존 계획을 재사용했고, 값을 바꾸면 매번 새 계획을 만들었습니다.</p>
<table>
<thead>
<tr>
<th>확인한 작업</th>
<th>기존 계획 사용</th>
<th>매번 새 계획 생성</th>
</tr>
</thead>
<tbody><tr>
<td>새 빌드 계획 생성</td>
<td>0/9회</td>
<td>12/12회</td>
</tr>
<tr>
<td>드라이버 작업 실행</td>
<td>0/9회</td>
<td>12/12회</td>
</tr>
<tr>
<td><code>SwiftCompile</code> 로그</td>
<td>없음</td>
<td>없음</td>
</tr>
</tbody></table>
<p>새 계획을 만드는 쪽에서는 드라이버 작업이 매번 실행됐지만, 두 조건 모두 <code>SwiftCompile</code> 로그는 없었습니다. 적어도 이 실험에서는 드라이버 작업의 반복이 컴파일 작업의 실행 기록으로 이어지지 않았습니다.</p>
<p>이 실험에서는 환경 변수 값을 의도적으로 바꿔 새 계획을 만들었습니다. 첫 편의 문제처럼 같은 조건에서 서명이 저절로 달라진 현상을 재현한 것은 아닙니다. 또한 입력 순서만 바꾼 대조 실험은 아니므로, 실제 앱의 모든 작업이 반복된 원인을 이 결과만으로 확정할 수는 없습니다. 구체적인 반복 횟수와 환경은 <a href="#experiment-conditions">실험 조건</a>에 정리했습니다.</p>
<p>반면 실제 앱에는 <code>SwiftCompile</code> 로그도 나타났습니다. 드라이버 작업뿐 아니라 컴파일 작업의 실행까지 기록됐다면, 소스를 다시 컴파일했다고 볼 수 있을까요?</p>
<h2 id="컴파일-작업도-캐시를-사용할-수-있습니다">컴파일 작업도 캐시를 사용할 수 있습니다</h2>
<p>같은 빌드에는 일부 작업이 이전 결과를 가져왔다는 <code>Cache hit</code> 기록도 있었습니다. 컴파일 작업 역시 캐시를 사용하고 끝날 수 있는지 확인할 필요가 있었습니다.</p>
<p>이 가능성을 확인하려고 Swift Build에서 컴파일 작업을 처리하는 <code>SwiftDriverJobTaskAction</code>을 살펴봤습니다. 캐시를 사용할 수 있는 경우에는 컴파일 프로세스를 실행하기 전에 이전 결과를 가져오고, 재사용에 성공하면 바로 작업을 마칩니다. <a href="#ref-5">[5]</a></p>
<pre><code class="language-swift">if let db = cas,
  let casOpts = payload.casOptions,
  try await Self.replayCachedCommand(
      /* 인자 생략 */
  ) {
    // 완료를 알리는 처리 생략
    return .succeeded
}
// 재사용하지 못하면 컴파일 실행</code></pre>
<p><em>캐시 재사용 경로를 발췌했습니다. 인자와 일부 처리를 생략했으며 독립 실행용 예제는 아닙니다.</em></p>
<p>따라서 <code>SwiftCompile</code> 로그를 모두 재컴파일로 세면 캐시를 재사용한 작업까지 포함할 수 있습니다. 캐시를 사용하더라도 작업을 구성하고 처리하는 비용은 남습니다.</p>
<p><img src="https://velog.velcdn.com/images/ryan-son/post/12a02663-0206-40bd-b57c-eef25e8020cc/image.png" alt="드라이버 작업을 다시 실행한 뒤 컴파일 작업이 실행되지 않을 수 있으며, 컴파일 작업이 실행돼도 캐시 재사용 여부에 따라 실제 컴파일 프로세스 실행이 달라지는 흐름"></p>
<p><em>‘SwiftCompile 로그 없음’은 최소 재현 프로젝트에서 확인한 결과입니다. 서명에 따른 드라이버 작업 실행과 컴파일 작업의 캐시 분기는 공개 소스에서 확인한 동작을 연결했습니다.</em></p>
<p>실제 앱 로그에는 <code>SwiftCompile</code>과 <code>Cache hit</code>가 모두 있었지만, 두 기록을 작업별로 연결해 확인하지는 않았습니다. 따라서 이 집계만으로는 어느 소스가 다시 컴파일됐는지 알 수 없습니다. 이를 확인하려면 반복된 컴파일 작업마다 캐시 사용 여부를 살펴봐야 합니다. 다음은 프로젝트에서 이 차이를 확인할 때 살펴볼 순서입니다.</p>
<h2 id="우리-프로젝트에서는-무엇부터-확인할까">우리 프로젝트에서는 무엇부터 확인할까?</h2>
<p><strong>1. 같은 조건으로 빌드하고 실행 기록을 남깁니다.</strong></p>
<p>DerivedData를 유지한 채 같은 명령으로 빌드하고 로그를 각각 저장합니다. 첫 빌드가 만든 결과를 다음 빌드에서 재사용할 수 있도록 소스와 설정도 유지합니다. 두 번째 로그에서 다시 실행된 작업을 찾고, <code>Build description signature:</code>가 나타났다면 계획을 새로 만든 이유도 확인합니다.</p>
<p><strong>2. 반복된 작업의 명령과 입력 목록을 비교합니다.</strong></p>
<p>명령이 같다면 manifest에서 입력·출력 목록과 순서를 확인합니다. 이번 조사에서는 DerivedData의 <code>Build/Intermediates.noindex/XCBuildData</code> 아래에서 <code>manifest.json</code>을 찾았습니다. 다음은 파일 위치를 확인하는 명령입니다. 경로와 형식은 Xcode 버전에 따라 달라질 수 있습니다.</p>
<pre><code class="language-sh"># 사용 중인 DerivedData 경로
pif_dd=&quot;/tmp/pif-blog-dd&quot;
find &quot;$pif_dd/Build&quot; -type f \
  -path &#39;*/XCBuildData/*&#39; \
  -name manifest.json -print</code></pre>
<p>파일이 여러 개라면 로그의 <code>Build description path:</code>에 기록된 디렉터리를 확인합니다. 다음 빌드에서 파일이 덮어써지거나 오래된 계획이 정리될 수 있으므로 각 빌드 직후 사본을 남겨둡니다.</p>
<p>manifest의 <code>commands</code>에서 로그에 나온 작업 설명(<code>description</code>)을 찾은 뒤, 해당 작업의 <code>inputs</code>와 <code>outputs</code>를 비교합니다. 파일이 추가되거나 빠졌는지뿐 아니라 나열된 순서도 확인합니다. 내부 경로와 설정이 담겨 있으므로 외부에 공유할 때는 필요한 부분만 발췌합니다.</p>
<p><strong>3. 로그로 확인한 범위와 추가 조사가 필요한 작업을 구분합니다.</strong></p>
<p>먼저 <code>SwiftDriver</code>만 반복됐는지 <code>SwiftCompile</code>도 나타났는지 확인합니다. 드라이버 작업만 반복됐다면 이를 재컴파일 횟수로 세지 않습니다. 컴파일 작업도 나타났다면 캐시 기록을 함께 살펴봅니다. <code>Cache hit</code>가 어느 컴파일 작업의 기록인지 알 수 없다면, 해당 작업의 재컴파일 여부는 아직 확인하지 못한 상태로 남겨야 합니다.</p>
<p>실제 지연 원인까지 좁히려면 각 컴파일 작업에 대해 캐시 재사용 여부, 컴파일 프로세스 실행 여부와 소요 시간을 연결한 기록이 더 필요합니다.</p>
<p>이번 조사에서는 드라이버에 전달한 명령이 같아도, 별도로 등록된 입력 목록의 순서는 달랐습니다. 그 순서가 서명과 실행 판정에 영향을 주는 코드를 따라가자, 같은 명령으로도 작업이 반복될 수 있는 이유가 드러났습니다. <strong>드라이버 작업이 왜 반복됐는지와 컴파일 작업이 실제로 무엇을 했는지를 나눠 확인해야, 로그에 나타난 작업 수를 재컴파일 횟수로 오해하지 않을 수 있습니다.</strong></p>
<p><a id="experiment-conditions"></a></p>
<h3 id="실험-조건">실험 조건</h3>
<p>매크로 플러그인 네 개를 사용하는 macOS용 Swift 패키지에서 최초 빌드를 마친 뒤 소스와 DerivedData를 유지했습니다. 본문의 두 조건을 각각 세 차례 측정했습니다. 컴파일 캐시 설정은 추가하지 않았습니다.</p>
<p>각 차례에는 먼저 환경 변수 값을 <code>fixed</code>로 고정해 네 번 빌드하고, 이어서 매번 값을 바꿔 네 번 빌드했습니다. 값을 고정한 뒤 첫 빌드는 이전 조건에서 계획이 바뀌는 영향을 제외하려고 집계에서 뺐습니다. 따라서 기존 계획을 사용하는 조건은 3회씩 총 9회, 새 계획을 만드는 조건은 4회씩 총 12회입니다.</p>
<p>환경 변수 변경은 다른 작업에도 영향을 줄 수 있으므로 측정한 시간 차이를 매크로 입력 순서만의 비용으로 제시하지 않았습니다.</p>
<p>Xcode 27.0에서는 새로 만든 빌드 계획 두 개의 manifest를 비교해도 드라이버 작업의 키와 명령·입력·출력 기록이 같았습니다. 기존 계획을 사용하는 조건과 새 계획을 만드는 조건 모두 <code>SwiftCompile</code> 로그가 나타나지 않았습니다. 서로 다른 머신에서 측정했으므로 버전 간 속도 차이는 비교하지 않았습니다.</p>
<h2 id="참고자료">참고자료</h2>
<p><a id="ref-1"></a></p>
<ol>
<li>swiftlang/swift-llbuild, <a href="https://github.com/swiftlang/swift-llbuild/blob/0a9584962ff829b9e58e90d5f09c21da1e79cef7/lib/BuildSystem/ExternalCommand.cpp#L36-L48">ExternalCommand.cpp</a>. 고정 커밋 <code>0a958496</code>의 작업 서명 계산. 발췌 라이선스: Apache-2.0. 공개 소스와 측정한 Xcode 번들의 구현이 완전히 같다고 확인한 것은 아닙니다.</li>
</ol>
<p><a id="ref-2"></a></p>
<ol start="2">
<li>swiftlang/swift-llbuild, <a href="https://github.com/swiftlang/swift-llbuild/blob/0a9584962ff829b9e58e90d5f09c21da1e79cef7/lib/Core/BuildEngine.cpp#L464-L479">BuildEngine.cpp</a>. 작업 서명이 달라진 규칙을 재실행 대상으로 판단하는 경로. 코드 발췌의 추적 로그 처리는 생략했습니다.</li>
</ol>
<p><a id="ref-3"></a></p>
<ol start="3">
<li>swiftlang/swift-llbuild, <a href="https://github.com/swiftlang/swift-llbuild/blob/0a9584962ff829b9e58e90d5f09c21da1e79cef7/products/libllbuild/BuildSystem-C-API.cpp#L1308-L1320">BuildSystem-C-API.cpp</a>. 커스텀 명령이 기본 작업 서명에 추가 정보를 결합하는 경로.</li>
</ol>
<p><a id="ref-4"></a></p>
<ol start="4">
<li>swiftlang/swift-build, <a href="https://github.com/swiftlang/swift-build/pull/1203">PR #1203</a>, 최종 <a href="https://github.com/swiftlang/swift-build/blob/05e9e1651cdd81db1ab05b2ab8e26ed9e50c12f1/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift">SwiftCompiler.swift</a>. 매크로 플러그인 인자와 추가 입력 경로에 같은 정렬을 적용한 수정. 이 PR이 특정 Xcode 버전에 포함됐다는 판단과는 구분합니다.</li>
</ol>
<p><a id="ref-5"></a></p>
<ol start="5">
<li>swiftlang/swift-build, <a href="https://github.com/swiftlang/swift-build/blob/2187330e13e7f7cfba1401a2c7e70c827a56a061/Sources/SWBTaskExecution/TaskActions/SwiftDriverJobTaskAction.swift#L517-L533">SwiftDriverJobTaskAction.swift</a>. 작업 액션에서 캐시 결과를 재사용하고 성공을 반환하는 경로. 캐시 재사용에 성공하면 컴파일 프로세스를 실행하지 않고 작업을 마칩니다.</li>
</ol>
<p><a id="ref-6"></a></p>
<ol start="6">
<li>swiftlang/swift-build, <a href="https://github.com/swiftlang/swift-build/blob/2187330e13e7f7cfba1401a2c7e70c827a56a061/Sources/SWBTaskExecution/BuildDescriptionSignature.swift">BuildDescriptionSignature.swift</a>, <a href="https://github.com/swiftlang/swift-build/issues/1149">이슈 #1149</a>, <a href="https://github.com/swiftlang/swift-build/pull/1441">PR #1441</a>. 계획 서명은 사용자 정보(<code>UserInfo</code>)의 환경 정보를 포함합니다. 관련 이슈는 CI 환경 변수 변경에 따른 계획 재사용 실패를 보고합니다. 공개 구현에는 일부 환경 변수를 서명에서 제외하는 처리도 있으므로 모든 변수와 모든 Xcode 버전에 같은 동작을 일반화하지 않습니다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[코드는 그대로인데, 빌드는 왜 느릴까]]></title>
            <link>https://velog.io/@ryan-son/%EC%BD%94%EB%93%9C%EB%8A%94-%EA%B7%B8%EB%8C%80%EB%A1%9C%EC%9D%B8%EB%8D%B0-%EB%B9%8C%EB%93%9C%EB%8A%94-%EC%99%9C-%EB%8A%90%EB%A6%B4%EA%B9%8C</link>
            <guid>https://velog.io/@ryan-son/%EC%BD%94%EB%93%9C%EB%8A%94-%EA%B7%B8%EB%8C%80%EB%A1%9C%EC%9D%B8%EB%8D%B0-%EB%B9%8C%EB%93%9C%EB%8A%94-%EC%99%9C-%EB%8A%90%EB%A6%B4%EA%B9%8C</guid>
            <pubDate>Mon, 12 Oct 2026 16:46:41 GMT</pubDate>
            <description><![CDATA[<p>Xcode 26.3으로 사내 iOS 앱을 개발하던 중, 코드를 바꾸지 않았는데도 다시 빌드하는 데 수십 초가 걸렸습니다. 빌드 설정을 바꾸거나 DerivedData를 지운 것도 아니었습니다. 방금 빌드한 결과가 남아 있는데 왜 다시 이렇게 오래 걸리는지 의문이 들었습니다.</p>
<p>증분 빌드는 이전 빌드의 결과를 재사용해 불필요한 작업을 줄입니다. 예를 들어 컴파일에 쓰이는 파일과 설정이 바뀌지 않았고 앞서 만든 오브젝트 파일도 남아 있다면, 같은 파일을 다시 컴파일할 필요가 없습니다. <a href="#ref-1">[1]</a> 이번 빌드도 이전 결과를 재사용하고 있는지 알아보려고, 로그를 비교해 어떤 작업을 다시 실행하는지 확인했습니다.</p>
<p>이 글에서는 Swift 파일 하나로 만든 재현 프로젝트와 Swift Build의 소스를 함께 살펴봅니다. 코드는 그대로인데 무엇이 달라졌기에 일부 작업을 다시 실행했는지 추적한 과정입니다.</p>
<h2 id="작은-프로젝트에서도-같은-일이-일어날까">작은 프로젝트에서도 같은 일이 일어날까?</h2>
<p>실제 앱에는 외부 패키지와 매크로, 빌드 스크립트가 함께 있어 반복 실행의 원인을 구분하기 어려웠습니다. 다른 요인을 줄이기 위해 Swift 파일 하나만 있는 프로젝트에서 비교했습니다.</p>
<p>동기화 폴더를 쓰면 코드를 수정하지 않아도 일부 빌드 작업이 반복된다는 선행 재현 자료 <code>build-flapping-demo</code>가 있었습니다. <a href="#ref-2">[2]</a> 이를 참고해 같은 소스와 빌드 설정을 사용하는 두 프로젝트를 준비하고, 한쪽에는 일반 그룹(Group), 다른 쪽에는 동기화 폴더(Folder)를 사용했습니다.</p>
<p>여기서 동기화 폴더는 <code>PBXFileSystemSynchronizedRootGroup</code>으로 관리하는 폴더를 뜻합니다. 두 프로젝트를 같은 명령으로 빌드해 어느 쪽에서 작업이 반복되는지 확인했습니다.</p>
<p>각 프로젝트를 한 번 빌드한 뒤 파일·설정·DerivedData를 그대로 두고 다시 빌드했습니다. 매번 새 <code>xcodebuild</code> 프로세스를 실행했으며, 구체적인 반복 횟수와 비교 조건은 뒤의 버전별 결과에서 설명합니다.</p>
<p>두 프로젝트 모두 Swift 파일을 다시 컴파일하지 않았습니다. 하지만 동기화 폴더를 사용한 쪽에서는 <code>ProcessInfoPlistFile</code>과 <code>CopySwiftLibs</code> 작업이 반복됐습니다. 로그에는 <code>Build description signature:</code>도 매번 나타났습니다.</p>
<p>Xcode 26.3에서 같은 머신으로 각각 10회 측정한 빌드 시간의 중앙값은 일반 그룹 0.81초, 동기화 폴더 0.96초였습니다. 이 작은 차이로 실제 앱의 수십 초 전체를 설명할 수는 없습니다. 다만 파일 하나만 있는 프로젝트에서도 일부 작업을 다시 실행하고 있었습니다.</p>
<p>왜 그 작업들을 반복하는지 알아보려면 로그의 의미부터 확인해야 했습니다. Xcode가 사용하는 빌드 엔진인 Swift Build의 소스에서 <code>Build description signature:</code>를 출력하는 위치를 찾아봤습니다. <a href="#ref-3">[3]</a></p>
<h2 id="컴파일-전에-실행할-작업을-구성합니다">컴파일 전에 실행할 작업을 구성합니다</h2>
<p>앱을 만들려면 컴파일뿐 아니라 리소스 처리, 링크, 파일 복사와 코드 서명도 필요합니다. 빌드 시스템은 각 작업에 사용할 도구와 설정을 정하고, 작업을 어떤 순서로 실행할지도 결정합니다.</p>
<p>예를 들어 링커는 컴파일러가 만든 오브젝트 파일을 사용합니다. 따라서 필요한 파일의 컴파일이 끝나야 링크를 시작할 수 있습니다. 빌드 시스템은 이런 의존 관계를 작업 그래프로 구성합니다. WWDC22의 <em>Demystify parallelization in Xcode builds</em>도 컴파일과 링크의 관계로 이 동작을 설명합니다. <a href="#ref-1">[1]</a></p>
<p>Swift Build는 실행할 작업과 그 의존 관계를 <code>BuildDescription</code>으로 관리합니다. 이것이 이 글에서 말하는 <strong>빌드 계획</strong>입니다. 기존 계획을 그대로 쓸 수 있다면 매번 다시 만들 필요가 없습니다.</p>
<p>공개된 소스의 <code>BuildDescriptionManager</code>는 먼저 메모리와 디스크에 저장한 계획을 확인합니다. 쓸 수 있는 계획을 찾지 못하면 새로 만드는데, 앞서 본 <code>Build description signature:</code>는 이 경로에서 출력됩니다. <a href="#ref-4">[4]</a></p>
<p><img src="https://velog.velcdn.com/images/ryan-son/post/8dac3327-0f55-4271-9c79-eee8d09a695f/image.png" alt="컴파일·링크의 의존 관계를 정한 계획을 재사용하거나 새로 만든 뒤 작업을 실행하는 흐름"></p>
<p><em>빌드 계획을 다시 만들었는지와 컴파일 작업을 다시 실행했는지는 따로 확인해야 합니다.</em></p>
<p>소스와 로그를 대조하니 동기화 폴더 프로젝트에서는 <strong>기존 계획을 쓰지 못하고 매번 새로 만들고 있었습니다.</strong> 그렇다면 파일과 설정을 바꾸지 않았는데 왜 기존 계획을 쓰지 못했을까요?</p>
<h2 id="기존-계획을-찾는-서명이-달라졌습니다">기존 계획을 찾는 서명이 달라졌습니다</h2>
<p>기존 계획을 쓰려면 이번 빌드에도 맞는 계획인지 확인해야 합니다. 빌드할 타깃이나 SDK가 달라졌다면, 앞서 정한 작업과 설정도 다시 확인해야 하기 때문입니다.</p>
<p>Swift Build는 타깃·SDK·빌드 설정 등의 정보를 모아 해시를 계산합니다. 이 해시가 <strong>빌드 계획의 서명</strong>이며, 저장된 계획을 찾는 키로 사용됩니다. 같은 서명의 계획을 찾은 뒤에는 실제로 재사용할 수 있는지도 검사합니다. 여기서 서명은 앱 배포에 사용하는 코드 서명과는 다릅니다.</p>
<p>어떤 정보가 달라졌는지 찾기 위해 <code>BuildDescriptionSignature</code>의 코드를 살펴봤습니다. 여기에는 워크스페이스와 타깃의 서명을 가져오는 부분이 있습니다. 이 두 서명은 빌드 계획의 서명을 계산하는 데 쓰이는 값입니다. <a href="#ref-5">[5]</a></p>
<pre><code class="language-swift">workspaceSignature = request.workspaceContext.workspace.signature
// 다른 필드 생략
targets = request.buildGraph.allTargets.map {
    TargetMetadata(
        name: $0.target.name,
        signature: $0.target.signature,
        // 나머지 인자 생략
    )
}</code></pre>
<p>이렇게 모은 값을 JSON으로 직렬화하고 해시를 계산합니다. 아래의 <code>humanReadableString</code>에는 그 JSON이 담겨 있습니다. <a href="#ref-5">[5]</a></p>
<pre><code class="language-swift">let hashContext = InsecureHashContext()
hashContext.add(bytes: humanReadableString)
return hashContext.signature</code></pre>
<p>해시가 달라졌다는 사실만으로는 어떤 값이 바뀌었는지 알 수 없습니다. 원인을 찾으려면 <strong>해시를 계산하기 전의 정보</strong>를 비교해야 합니다.</p>
<p>Swift Build는 디버그 로그를 켜면 이 정보를 <code>description.signature</code> 파일에 저장합니다. 두 실행 모두 <code>EnableDebugActivityLogs=YES</code>를 환경 변수로 지정해 수집했습니다. <a href="#ref-5">[5]</a></p>
<p>두 파일을 비교하니 타깃 목록 첫 항목의 <code>signature</code>와 워크스페이스의 <code>workspaceSignature</code>만 달랐습니다. 아래는 실제 비교 결과에서 긴 문자열의 일부를 줄인 것입니다.</p>
<pre><code class="language-diff"># targets[0].signature
- TARGET@v11_hash=3a3d…4b0b
+ TARGET@v11_hash=58c5…10ef

# workspaceSignature
- WORKSPACE@v11_hash=(null)_subobjects=1bef…a6a7
+ WORKSPACE@v11_hash=(null)_subobjects=fb40…d314</code></pre>
<p>일반 그룹 프로젝트에서는 이 값들이 그대로였습니다. 동기화 폴더 프로젝트는 같은 파일과 설정으로 빌드했는데도 타깃과 워크스페이스의 서명이 달라졌습니다. 이 값들은 빌드 계획의 서명을 계산할 때 쓰이므로, 기존 계획을 찾는 키도 달라집니다.</p>
<p>그런데 타깃과 워크스페이스의 서명은 왜 바뀌었을까요? 이를 확인하려고 Xcode가 빌드 서비스에 전달한 프로젝트 정보를 비교했습니다.</p>
<h2 id="파일은-같았지만-pif의-식별자는-달랐습니다">파일은 같았지만 PIF의 식별자는 달랐습니다</h2>
<p>Xcode는 프로젝트 정보를 <strong>PIF(Project Interchange Format)</strong> 형식으로 빌드 서비스에 전달합니다. PIF에는 워크스페이스·프로젝트·타깃의 정보와 파일 참조, 각 객체의 서명이 들어 있습니다. <a href="#ref-6">[6]</a></p>
<p>PIF에서 파일을 참조할 때는 GUID라는 식별자를 사용합니다. 두 프로젝트에서 Xcode가 같은 파일에 부여한 GUID를 비교했습니다.</p>
<p>일반 그룹을 사용하면 <code>.pbxproj</code>에 파일별 참조가 저장돼 있어 Xcode가 다음 빌드에서도 이를 읽을 수 있습니다. 동기화 폴더는 개별 파일의 참조 대신 폴더 경로를 기록합니다. 덕분에 폴더 안에서 파일을 추가하거나 삭제해도 프로젝트 파일을 일일이 수정하지 않아도 됩니다. <a href="#ref-7">[7]</a> Xcode는 이 폴더의 파일을 찾아 PIF에 필요한 파일 참조와 GUID를 만듭니다. 두 방식은 파일 목록을 얻는 과정이 다르지만, 같은 파일을 가리키는 PIF의 GUID는 다음 실행에서도 같아야 합니다.</p>
<p>같은 재현 프로젝트를 두 번 빌드하고 PIF를 비교했습니다. 소스는 수정하지 않았고 <code>App.swift</code>의 경로도 같았는데, GUID는 달랐습니다. 같은 파일을 가리키는 빌드 항목의 <code>fileReference</code>도 함께 바뀌었습니다.</p>
<p>아래는 실제 diff에서 해당 부분만 뽑았습니다. 긴 GUID의 공통 접두부는 <code>…</code>로 줄였습니다.</p>
<pre><code class="language-diff">  &quot;path&quot;: &quot;App.swift&quot;
- &quot;guid&quot;: &quot;…d6eedcf646588d17f894c326c2ff5242&quot;
+ &quot;guid&quot;: &quot;…146da9c56ebe445f2f199cc8dd68f76d&quot;

- &quot;fileReference&quot;: &quot;…d6eedcf646588d17f894c326c2ff5242&quot;
+ &quot;fileReference&quot;: &quot;…146da9c56ebe445f2f199cc8dd68f76d&quot;</code></pre>
<p><em>표시한 두 항목은 원래 PIF의 서로 다른 위치에 있습니다. 읽기 쉽게 모았으며 완전한 JSON은 아닙니다.</em></p>
<p>PIF를 비교하니 파일 GUID뿐 아니라 프로젝트·타깃·워크스페이스의 서명도 달라져 있었습니다. 이 변화와 앞 절에서 확인한 빌드 계획의 서명을 연결하면 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/ryan-son/post/9b5f8a72-c0fd-4f4c-aefe-039211fde3e2/image.png" alt="같은 App.swift를 표현하는 GUID가 달라져 타깃·워크스페이스 서명과 빌드 계획 서명이 바뀌는 경로"></p>
<p>PIF 명세는 같은 객체에 일관된 GUID를 부여해야 한다고 설명합니다.</p>
<blockquote>
<p>“Stable, such that clients can generate the same GUID consistently for an object …” <a href="#ref-6">[6]</a></p>
</blockquote>
<p>이번 재현에서는 같은 파일에 부여한 GUID가 이 요구를 지키지 못했습니다. 소스와 설정이 같아도 PIF의 식별자가 달라지면, 이를 사용하는 서명도 달라질 수 있습니다. <strong>기존 계획을 찾는 키가 달라졌기 때문에 코드를 수정하지 않아도 새 계획을 만들었던 것입니다.</strong></p>
<h2 id="빌드-없이-출력한-pif에서도-식별자가-달랐습니다">빌드 없이 출력한 PIF에서도 식별자가 달랐습니다</h2>
<p>이번에는 컴파일이나 파일 복사 작업을 실행하지 않고도 GUID가 바뀌는지 확인했습니다. <code>xcodebuild -dumpPIF</code>로 프로젝트의 PIF만 출력해 두 결과를 비교했습니다. 이 옵션은 확인한 버전의 <code>xcodebuild -help</code>에 안내돼 있지 않습니다.</p>
<pre><code class="language-sh"># Xcode 설치 경로를 자신의 환경에 맞게 바꿉니다.
export DEVELOPER_DIR=&quot;/Applications/Xcode-26.3.0.app/Contents/Developer&quot;

# 재현 프로젝트의 동기화 폴더 디렉터리에서 실행합니다.
xcodebuild -dumpPIF /tmp/pif-first.json -project MinApp.xcodeproj
xcodebuild -dumpPIF /tmp/pif-second.json -project MinApp.xcodeproj
diff -u /tmp/pif-first.json /tmp/pif-second.json</code></pre>
<p>Xcode 26.3·26.6에서는 PIF만 출력해도 동기화 폴더 안 파일의 GUID가 달라졌습니다. 일반 그룹에서는 같은 GUID가 나왔습니다.</p>
<p>다른 프로젝트에서 비교할 때도 GUID와 이를 가리키는 참조가 달라졌는지 확인해야 합니다. 배열의 순서만 바뀌는 등 다른 차이가 있을 수 있으므로 diff가 있다는 사실만으로 원인을 확정할 수는 없습니다. PIF에는 프로젝트 경로와 빌드 설정도 담겨 있어, 공유할 때는 필요한 부분만 발췌하는 편이 좋습니다.</p>
<h2 id="xcode-270는-기존-계획을-재사용합니다">Xcode 27.0는 기존 계획을 재사용합니다</h2>
<p>같은 프로젝트를 Xcode 27.0에서도 빌드해봤습니다. 동기화 폴더를 사용해도 파일 GUID가 같았고, 다음 빌드에서는 기존 계획을 사용했습니다.</p>
<table>
<thead>
<tr>
<th>동기화 폴더 프로젝트의 관측</th>
<th>Xcode 26.3</th>
<th>Xcode 26.6</th>
<th>Xcode 27.0</th>
</tr>
</thead>
<tbody><tr>
<td>캐시를 유지한 반복 빌드에서 새 계획을 만든 횟수</td>
<td>10/10회</td>
<td>10/10회</td>
<td>0/10회</td>
</tr>
<tr>
<td>같은 10회에서 Swift 컴파일 작업 실행</td>
<td>없음</td>
<td>없음</td>
<td>없음</td>
</tr>
<tr>
<td>같은 소스 파일의 GUID</td>
<td>실행마다 달라짐</td>
<td>실행마다 달라짐</td>
<td>유지됨</td>
</tr>
</tbody></table>
<p>버전별로 최초 빌드 후 파일·설정·DerivedData를 그대로 두고 5회 더 빌드했으며, 이 실험을 두 번 진행한 총 10회의 결과입니다. 각 실험은 새 DerivedData에서 시작했고, 두 번째 실험에서는 일반 그룹과 동기화 폴더의 빌드 순서를 바꿨습니다. 별도로 PIFCache만 옮기고 실행한 5회에서도 버전별 차이는 같았습니다.</p>
<p>Xcode 26.3·26.6은 M3 Pro, macOS 26.4 환경에서, Xcode 27.0은 M2 Max와 macOS 27.0.1 환경에서 측정했습니다. 머신과 OS가 다르므로 어느 버전이 몇 배 빨라졌는지 비교하는 것은 의미가 크게 없으니, <strong>같은 파일에 같은 GUID를 부여하는지, 기존 계획을 다시 사용하는지</strong>를 비교했습니다.</p>
<p>재현 프로젝트에서는 Xcode 27.0에서 문제가 나타나지 않았습니다. Xcode 내부의 어떤 수정으로 동작이 달라졌는지와 GUID를 계산하는 정확한 방법은 확인하지 못했습니다.</p>
<h2 id="내우리-프로젝트는">내(우리) 프로젝트는?</h2>
<p>원인을 조사할 때는 캐시를 지우기 전에 같은 명령으로 다시 빌드해보는 편이 좋습니다. 캐시를 지우면 이전 결과가 없어 다시 실행하는 작업과, 결과가 남아 있는데도 다시 실행하는 작업을 구분하기 어렵습니다. 파일·설정·실행 환경을 바꾸지 않고 DerivedData도 그대로 두고 비교합니다.</p>
<p>앞에서 Xcode를 선택한 터미널에서 다음처럼 두 번 빌드하고 로그를 따로 저장할 수 있습니다. 실제 프로젝트에서는 프로젝트·스킴·빌드 대상과 DerivedData 경로를 바꿉니다. 두 번 실행하는 동안에는 이 값을 바꾸지 않습니다.</p>
<pre><code class="language-sh">for run in 1 2; do
  xcodebuild build \
    -project MinApp.xcodeproj -scheme MinApp \
    -destination &#39;generic/platform=iOS Simulator&#39; \
    -derivedDataPath /tmp/pif-blog-dd \
    CODE_SIGNING_ALLOWED=NO &gt; &quot;/tmp/pif-build-$run.log&quot; 2&gt;&amp;1 || break
done</code></pre>
<p>두 번째 로그에서 다시 실행한 작업을 확인합니다. <code>Build description signature:</code>가 또 나타났다면 계획도 다시 만든 것입니다. 설정이나 환경이 달라졌는지 확인하고, 해시 계산 전의 정보를 비교해 바뀐 값을 찾습니다. 동기화 폴더를 사용한다면 PIF의 GUID도 비교할 수 있습니다. 다른 이유로도 계획을 다시 만들 수 있으므로 로그 한 줄만으로 동기화 폴더 문제라고 결론 내리지는 않습니다.</p>
<p>이번 실험에서는 일반 그룹을 사용했을 때 GUID가 바뀌지 않았고 기존 계획도 다시 사용했습니다. 같은 문제가 확인된 프로젝트라면 사본에서 동기화 폴더를 일반 그룹으로 바꾼 뒤 비교해볼 수 있습니다. 폴더 안의 파일이 자동으로 반영되는 방식도 바뀌므로, 타깃에 포함할 파일과 리소스 처리는 함께 확인해야 합니다. Xcode 27.0으로 전환할 수 있다면 파일과 설정을 그대로 두고 문제가 사라지는지도 확인할 수 있습니다.</p>
<p>Xcode 앱에서 빌드했을 때는 실행 방식에 따른 차이가 있었습니다. 26.3에서 재현 프로젝트를 다시 열면 첫 빌드에서는 새 계획을 만들었지만, 프로젝트를 열어둔 채 다시 빌드하면 그 계획을 사용했습니다. 따라서 Xcode 앱에서 빌드할 때와 매번 새 <code>xcodebuild</code> 프로세스로 빌드할 때 겪는 지연은 다를 수 있습니다.</p>
<p>파일 하나로 만든 프로젝트에서는 계획을 다시 만들고 일부 작업을 반복했지만, Swift 파일을 다시 컴파일하지는 않았습니다. 실제 앱에서는 컴파일 작업까지 다시 실행됐습니다. <strong>계획을 새로 만드는 것만으로는 이 차이를 설명할 수 없어, 각 컴파일 작업이 사용하는 파일과 설정을 비교했습니다.</strong> 다음 글에서는 컴파일 명령은 같은데 매크로 플러그인 파일의 순서가 달라져 작업을 다시 실행한 과정을 다루겠습니다.</p>
<h2 id="참고자료">참고자료</h2>
<p><a id="ref-1"></a></p>
<ol>
<li>Apple, <a href="https://developer.apple.com/videos/play/wwdc2022/110364/">Demystify parallelization in Xcode builds</a>, WWDC22. 작업의 입력·출력과 의존 관계 설명, 증분 빌드 설명(04:19 부근).</li>
</ol>
<p><a id="ref-2"></a></p>
<ol start="2">
<li>protonjohn, <a href="https://github.com/protonjohn/build-flapping-demo">build-flapping-demo</a>, Xcode 26.2에서 동기화 폴더와 반복 계획 생성의 관계를 보고한 선행 재현 자료.</li>
</ol>
<p><a id="ref-3"></a></p>
<ol start="3">
<li>Swift.org, <a href="https://www.swift.org/blog/the-next-chapter-in-swift-build-technologies/">The Next Chapter in Swift Build Technologies</a>. Swift Build의 역할과 오픈소스 공개 배경. SwiftPM 명령 <code>swift build</code>와 빌드 엔진 저장소 <code>swift-build</code>는 구분합니다.</li>
</ol>
<p><a id="ref-4"></a></p>
<ol start="4">
<li>swiftlang/swift-build, <a href="https://github.com/swiftlang/swift-build/blob/2187330e13e7f7cfba1401a2c7e70c827a56a061/Sources/SWBTaskExecution/BuildDescriptionManager.swift#L372-L632">BuildDescriptionManager.swift</a>, 커밋 <code>2187330e</code>. 기존 계획 조회와 새 계획 생성·로그 경로.</li>
</ol>
<p><a id="ref-5"></a></p>
<ol start="5">
<li>swiftlang/swift-build, <a href="https://github.com/swiftlang/swift-build/blob/2187330e13e7f7cfba1401a2c7e70c827a56a061/Sources/SWBTaskExecution/BuildDescriptionSignature.swift#L46-L205">BuildDescriptionSignature.swift</a>, 같은 커밋. 입력 구성·직렬화·해시 계산·디버그 출력 경로. 본문의 소스 발췌는 이 고정 커밋을 기준으로 하며, 측정한 Xcode 번들의 소스와 완전히 같다는 뜻은 아닙니다. 코드 발췌 라이선스: Apache-2.0.</li>
</ol>
<p><a id="ref-6"></a></p>
<ol start="6">
<li>swiftlang/swift-build, <a href="https://github.com/swiftlang/swift-build/blob/2187330e13e7f7cfba1401a2c7e70c827a56a061/SwiftBuild.docc/Core/project-interchange-format.md">Project Interchange Format</a>, 같은 커밋. PIF의 모델·객체 서명·GUID 정의.</li>
</ol>
<p><a id="ref-7"></a></p>
<ol start="7">
<li>Apple, <a href="https://developer.apple.com/documentation/xcode-release-notes/xcode-16-release-notes">Xcode 16 Release Notes</a>, Project Management의 buildable folder 설명(123729918). <a href="https://developer.apple.com/documentation/xcode/managing-files-and-folders-in-your-xcode-project">Managing files and folders in your Xcode project</a>도 참고할 수 있습니다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[TaskLocal을 직접 쓰지 않았는데 왜 TaskLocal에서 크래시가 날까?]]></title>
            <link>https://velog.io/@ryan-son/TaskLocal%EC%9D%84-%EC%A7%81%EC%A0%91-%EC%93%B0%EC%A7%80-%EC%95%8A%EC%95%98%EB%8A%94%EB%8D%B0-%EC%99%9C-TaskLocal%EC%97%90%EC%84%9C-%ED%81%AC%EB%9E%98%EC%8B%9C%EA%B0%80-%EB%82%A0%EA%B9%8C</link>
            <guid>https://velog.io/@ryan-son/TaskLocal%EC%9D%84-%EC%A7%81%EC%A0%91-%EC%93%B0%EC%A7%80-%EC%95%8A%EC%95%98%EB%8A%94%EB%8D%B0-%EC%99%9C-TaskLocal%EC%97%90%EC%84%9C-%ED%81%AC%EB%9E%98%EC%8B%9C%EA%B0%80-%EB%82%A0%EA%B9%8C</guid>
            <pubDate>Sun, 11 Oct 2026 17:29:17 GMT</pubDate>
            <description><![CDATA[<p>개발하던 iOS 앱에서 여러 API의 응답을 모아 보여주는 탭에 처음 들어가면 앱이 비정상 종료되는 문제가 발생했습니다. 당시 장애 기록에서는 iOS 16·17에서 발생했고 iOS 18 이상에서는 같은 증상이 나타나지 않았습니다. 문제가 된 코드는 <code>withTaskGroup</code>으로 요청을 병렬 실행하고, <code>waitForAll()</code>로 기다린 뒤 TCA 액션을 보내는 부분이었습니다.</p>
<p>자식 작업이 끝나도 <code>withTaskGroup</code> 본문은 아직 실행 중이었고, 그 안에서 액션을 보내는 과정이 구형 Swift 런타임의 task-local 바인딩 검사에 걸렸습니다. 액션 전송을 그룹 밖으로 옮겨 수정한 뒤, 런타임의 검사 조건과 다른 호출 형태를 조사하고 같은 위험을 개발 중 찾는 정적 검사를 추가했습니다.</p>
<h2 id="액션을-보냈는데-스택에는-tasklocal이-있었습니다">액션을 보냈는데 스택에는 TaskLocal이 있었습니다</h2>
<p>앱 코드에서는 <code>TaskLocal</code>을 직접 호출하지 않았지만 크래시 스택에는 <code>withDependencies</code>, <code>TaskLocal.withValue</code>, <code>swift_task_reportIllegalTaskLocalBindingWithinWithTaskGroupImpl</code>이 있었습니다. 당시 액션 전송 경로에서 TCA가 의존성 문맥을 복원하며 task-local 값을 바인딩한 것입니다.</p>
<p>이 조건을 살펴보기 위해 앱 코드 대신, 여러 작업을 기다린 뒤 완료를 기록하는 예제를 사용하겠습니다. <code>process(item:)</code>은 항목 하나를 처리하는 비동기 함수라고 가정합니다. 완료 기록 함수는 로그에 사용할 문맥을 task-local로 바인딩합니다.</p>
<pre><code class="language-swift">enum Trace {
    @TaskLocal static var id: String?
}

func reportCompletion() {
    Trace.$id.withValue(&quot;batch-completed&quot;) {
        print(&quot;완료: \(Trace.id ?? &quot;none&quot;)&quot;)
    }
}

func processBatch() async {
    await withTaskGroup(of: Void.self) { group in
        for item in 0..&lt;3 {
            group.addTask { await process(item: item) }
        }
        await group.waitForAll()
        reportCompletion() // 구형 런타임: 내부 withValue에서 종료
    }
}</code></pre>
<p><code>reportCompletion()</code>은 일반적인 함수 호출처럼 보이지만 내부에서 <code>withValue</code>를 실행합니다. 실제 사건의 <code>send</code>도 호출 내부에서 바인딩이 일어났다는 점에서 같은 조건에 해당합니다. 이 예제의 실패 설명은 당시 검사 코드가 있던 구형 런타임을 기준으로 합니다.</p>
<p>Task-local은 실행 중인 태스크의 문맥에 값을 연결하는 기능입니다. <code>withValue</code>는 클로저가 실행되는 동안 사용할 값을 바인딩합니다. 따라서 호출한 함수 내부에서 <code>withValue</code>를 사용하면, 호출부에 <code>TaskLocal</code>이 없어도 그 함수가 실행되는 태스크에서 바인딩이 일어납니다.[1]</p>
<p>확인해야 할 것은 <code>send</code>라는 함수 이름보다 <strong>그 바인딩이 어느 태스크에서, 어떤 스코프가 살아 있을 때 실행됐는가</strong>였습니다.</p>
<h2 id="waitforall은-그룹을-종료하지-않습니다">waitForAll은 그룹을 종료하지 않습니다</h2>
<p>Swift 5.9 릴리스 소스의 <code>TaskLocal.withValue</code>는 값을 넣기 전에 <code>_checkIllegalTaskLocalBindingWithinWithTaskGroup</code>을 호출합니다. 이 검사는 현재 태스크에 TaskGroup 상태 기록이 있는지 확인하고, 있으면 오류를 보고하며 프로세스를 종료합니다. 실행 중인 자식의 수나 자식이 실제로 값을 읽었는지를 검사하는 코드가 아닙니다.[2]</p>
<p><code>waitForAll()</code>은 자식 작업이 끝날 때까지 기다립니다. 그러나 그 다음 줄도 여전히 그룹 본문에서 실행됩니다. 따라서 자식이 모두 끝났더라도 <code>reportCompletion()</code> 내부의 바인딩은 검사에 걸릴 수 있었습니다.</p>
<p>완료 기록은 <code>waitForAll()</code> 뒤가 아니라 <code>withTaskGroup</code>이 반환한 뒤로 옮겨야 했습니다.</p>
<h2 id="그룹-안의-바인딩을-왜-제한했을까">그룹 안의 바인딩을 왜 제한했을까</h2>
<p>이전 task-local 설계에서는 자식 태스크가 부모의 바인딩을 참조할 수 있었습니다. 이번에는 바인딩 안에서 자식을 만드는 예를 보겠습니다. 그룹 본문 안에서 값을 바인딩하고 자식을 만들면 자식이 끝나기 전에 바인딩이 종료될 수 있습니다.[1]</p>
<pre><code class="language-swift">// 앞서 정의한 Trace를 사용
// 구형 런타임에서 금지된 위치
await withTaskGroup(of: Void.self) { group in
    Trace.$id.withValue(&quot;request&quot;) {
        group.addTask {
            print(Trace.id ?? &quot;none&quot;)
        }
    } // 검사가 없다면 바인딩이 자식보다 먼저 끝날 수 있음
}</code></pre>
<p>그룹은 자신이 반환하기 전에 자식이 끝나도록 보장합니다. 하지만 그룹 본문 안의 더 짧은 <code>withValue</code> 스코프까지 자식의 수명을 감싸 주지는 않습니다. 자식이 참조할 바인딩이 먼저 끝나는 상황을 막기 위해 이전 구현은 그룹 본문에서의 바인딩을 제한했습니다.</p>
<p>자식 전체가 같은 값을 사용해야 한다면 바인딩으로 그룹 전체를 감쌉니다. <code>withTaskGroup</code>이 모든 자식을 기다리고 반환한 뒤 바인딩이 끝나므로 수명이 맞습니다.</p>
<pre><code class="language-swift">await Trace.$id.withValue(&quot;request&quot;) {
    await withTaskGroup(of: Void.self) { group in
        group.addTask { print(Trace.id ?? &quot;none&quot;) }
    } // 자식 완료 후 그룹 반환
}     // 그 뒤 바인딩 종료</code></pre>
<p>특정 자식에만 값이 필요하다면 그 자식 안에서 바인딩할 수도 있습니다. 이 예제의 <code>withValue</code>는 그룹 본문을 실행하는 부모가 아닌 자식 태스크에서 실행됩니다.</p>
<pre><code class="language-swift">await withTaskGroup(of: Void.self) { group in
    group.addTask {
        Trace.$id.withValue(&quot;child&quot;) {
            print(Trace.id ?? &quot;none&quot;)
        }
    }
}</code></pre>
<p>처음 살펴본 코드에서는 이미 자식을 모두 기다렸지만 이 검사 조건은 같았습니다. 실제로 바인딩이 자식보다 먼저 끝나는지를 개별 판정하는 대신, 현재 태스크에 그룹 상태 기록이 있으면 막는 방식이었기 때문입니다.</p>
<h3 id="새-런타임에서는-무엇이-달라졌나">새 런타임에서는 무엇이 달라졌나</h3>
<p>Swift 6.0 릴리스 소스에서는 해당 검사 호출이 제거됐고, 그룹 안에서 만든 바인딩을 자식이 상속할 때 필요한 항목을 복사하는 처리가 들어 있습니다. 그룹 바깥에서 바인딩해 자식이 끝날 때까지 유지되는 값은 부모를 계속 참조할 수 있습니다.[3]</p>
<p>당시 장애의 OS별 차이는 이러한 검사·상속 방식의 변화와 부합합니다. 다만 공개 소스의 릴리스 태그만으로 각 iOS 세부 버전의 구현까지 확정할 수는 없습니다. 앱에서는 지원 대상인 iOS 16·17의 검사 조건을 피하면서 기존 화면 동작을 유지해야 했습니다.</p>
<h2 id="그룹이-반환한-뒤-완료를-기록합니다">그룹이 반환한 뒤 완료를 기록합니다</h2>
<p>예제에서는 <code>reportCompletion()</code>을 그룹 밖으로 옮기면 됩니다. <code>withTaskGroup</code>은 자식 작업이 모두 끝난 뒤 반환하므로, 반환 다음 줄에서 완료를 기록해도 모든 항목의 처리가 끝난 상태입니다.[^4]</p>
<pre><code class="language-swift">func processBatch() async {
    await withTaskGroup(of: Void.self) { group in
        for item in 0..&lt;3 {
            group.addTask { await process(item: item) }
        }
    } // 모든 자식 작업이 끝난 뒤 반환

    reportCompletion() // 그룹을 종료한 상태에서 바인딩
}</code></pre>
<p>이 위치에서는 별도의 <code>waitForAll()</code>이 필요하지 않습니다. 항목 처리 완료 → 그룹 반환 → 완료 기록 순서로 실행되므로, 기존 작업 순서를 유지하면서 바인딩 위치를 바꿀 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/ryan-son/post/a0ea18e4-ae50-4d38-8a42-72f2bdf576bf/image.png" alt="변경 전에는 자식 작업을 기다린 뒤에도 그룹 본문 안에서 완료를 기록합니다. 변경 후에는 그룹이 반환한 뒤 완료를 기록하므로 task-local 바인딩도 그룹 밖에서 실행됩니다"></p>
<p>실제 앱에서도 같은 원리로 액션 전송을 그룹 밖으로 옮겼습니다. 이때 화면을 구성한 뒤 로딩을 종료하는 기존 순서가 바뀌지 않도록 종료 처리의 위치도 함께 조정했고, 이 수정은 릴리스에 반영됐습니다.</p>
<h2 id="의존성-캡처에서도-같은-문제가-발생했습니다">의존성 캡처에서도 같은 문제가 발생했습니다</h2>
<p>이후에는 액션을 보내지 않는 코드에서도 같은 크래시가 발생했습니다. <code>@Dependency</code>로 선언한 <code>clock</code>을 자식 태스크의 캡처 목록에서 읽는 코드였습니다. 아래는 같은 조건을 일정 시간 기다리는 작업으로 구성한 예제입니다.</p>
<pre><code class="language-swift">// 타입에 선언한 의존성 프로퍼티
@Dependency(\.continuousClock) var clock

// 다음은 메서드 본문: 캡처 시 프로퍼티 래퍼를 읽음
try await withThrowingTaskGroup(of: Void.self) { group in
    group.addTask { [clock] in // 캡처 값은 부모 태스크에서 평가
        try await clock.sleep(for: .seconds(1))
    }
    try await group.waitForAll()
}</code></pre>
<p><code>[clock]</code>은 자식 태스크가 실행되며 평가하는 코드가 아닙니다. 클로저를 만드는 시점, 즉 그룹 본문을 실행하는 부모 태스크에서 평가됩니다.[5] 또 당시 확인한 swift-dependencies의 구현에서는 <code>@Dependency</code>를 읽는 과정에 <code>TaskLocal.withValue</code> 호출이 있었습니다. 일반적인 task-local 값 읽기와 달리, 이 프로퍼티 래퍼의 읽기는 내부에서 바인딩도 수행합니다.[6]</p>
<p>수정은 의존성 값을 그룹 진입 전에 꺼내는 것이었습니다. 이후 캡처 목록에서는 프로퍼티 래퍼가 아닌 지역 상수를 읽습니다.</p>
<pre><code class="language-swift">let clock = self.clock // 그룹 진입 전에 의존성을 읽음
try await withThrowingTaskGroup(of: Void.self) { group in
    group.addTask { [clock] in
        try await clock.sleep(for: .seconds(1))
    }
    try await group.waitForAll()
}</code></pre>
<p>여기서는 자식 작업의 오류를 전달받기 위해 <code>try await group.waitForAll()</code>을 유지했습니다. 앞의 예제와 달리 자식 작업이 오류를 던질 수 있기 때문입니다. 바꾼 것은 기다리는 방식이 아니라 의존성 값을 읽는 위치입니다.</p>
<p>첫 문제를 “그룹 본문에서 <code>send</code>를 부르지 않는다”로만 정리했다면 이 형태는 놓쳤을 것입니다. 두 코드의 공통점은 <strong>그룹 본문을 실행하는 태스크에서 task-local 바인딩이 일어난다</strong>는 데 있었습니다.</p>
<h2 id="참고자료">참고자료</h2>
<p>[1]: Swift Evolution, <a href="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0311-task-locals.md">SE-0311: Task Local Values</a>. Task-local 바인딩, 상속과 TaskGroup 안의 수명 제약에 대한 초기 설계 설명. 현재 구현 전체를 설명하는 문서로 사용하지 않았다.
[2]: Swift 5.9 릴리스, <a href="https://github.com/swiftlang/swift/blob/swift-5.9-RELEASE/stdlib/public/Concurrency/TaskLocal.swift#L173">TaskLocal.swift</a>, <a href="https://github.com/swiftlang/swift/blob/swift-5.9-RELEASE/stdlib/public/Concurrency/TaskStatus.cpp#L499">TaskStatus.cpp</a>. 바인딩 전 검사와 현재 태스크의 그룹 상태 기록 확인.
[3]: Swift 6.0 릴리스, <a href="https://github.com/swiftlang/swift/blob/swift-6.0-RELEASE/stdlib/public/Concurrency/TaskLocal.swift">TaskLocal.swift</a>, <a href="https://github.com/swiftlang/swift/blob/swift-6.0-RELEASE/stdlib/public/Concurrency/TaskLocal.cpp">TaskLocal.cpp</a>. 검사 호출 제거와 <code>copyToOnlyOnlyFromCurrentGroup</code>의 복사·참조 처리.
[4]: Swift 6.0 릴리스, <a href="https://github.com/swiftlang/swift/blob/swift-6.0-RELEASE/stdlib/public/Concurrency/TaskGroup.swift">TaskGroup.swift</a>. <code>withTaskGroup</code>은 본문 실행 후 남은 자식을 기다리고 그룹을 정리한 뒤 반환한다.
[5]: The Swift Programming Language, <a href="https://docs.swift.org/swift-book/documentation/the-swift-programming-language/expressions/#Capture-Lists">Expressions — Capture Lists</a>. 캡처 목록은 클로저가 만들어질 때 초기화된다.
[6]: swift-dependencies, <a href="https://github.com/pointfreeco/swift-dependencies/blob/a10f9feeb214bc72b5337b6ef6d5a029360db4cc/Sources/Dependencies/Dependency.swift#L206">Dependency.swift</a>. 최초 수정 시점의 앱 의존성 잠금 파일에 기록된 1.10.0 리비전으로 대조. <code>_wrappedValue</code>의 Debug·Release 경로에서 <code>withValue</code>를 사용한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Service UUID가 있어도 iOS 백그라운드에서 BLE 기기를 찾지 못한 이유]]></title>
            <link>https://velog.io/@ryan-son/Service-UUID%EA%B0%80-%EC%9E%88%EC%96%B4%EB%8F%84-iOS-%EB%B0%B1%EA%B7%B8%EB%9D%BC%EC%9A%B4%EB%93%9C%EC%97%90%EC%84%9C-BLE-%EA%B8%B0%EA%B8%B0%EB%A5%BC-%EC%B0%BE%EC%A7%80-%EB%AA%BB%ED%95%9C-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@ryan-son/Service-UUID%EA%B0%80-%EC%9E%88%EC%96%B4%EB%8F%84-iOS-%EB%B0%B1%EA%B7%B8%EB%9D%BC%EC%9A%B4%EB%93%9C%EC%97%90%EC%84%9C-BLE-%EA%B8%B0%EA%B8%B0%EB%A5%BC-%EC%B0%BE%EC%A7%80-%EB%AA%BB%ED%95%9C-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Sun, 11 Oct 2026 16:56:10 GMT</pubDate>
            <description><![CDATA[<h1 id="앱을-열어야-발견되던-공동현관-출입-기기">앱을 열어야 발견되던 공동현관 출입 기기</h1>
<p>공동현관 출입 서비스를 개발하면서 사용자가 앱을 열지 않아도 문 앞에서 출입할 수 있도록 하는 것이 목표였습니다. 제조사 SDK를 연동하던 중 iPhone 11 Pro의 iOS 16.5에서 문제가 나타났습니다. 앱을 켜면 발견되던 기기를 백그라운드에서는 찾지 못해, 출입하려면 앱을 열어야 했습니다.</p>
<p>Apple의 액세서리 설계 가이드와 실제 블루투스 패킷을 대조해, 발견에 필요한 Service UUID가 스캔 응답에만 들어 있다는 것을 확인했습니다. 제조사에 UUID를 광고 패킷으로 옮기도록 요청했고, 펌웨어 수정 후 백그라운드에서 실제 출입이 동작하는 것을 확인했습니다.</p>
<h2 id="백그라운드에서는-스캔-응답을-받지-못했습니다">백그라운드에서는 스캔 응답을 받지 못했습니다</h2>
<p>기기를 발견하지 못하면 연결과 인증을 거쳐 문을 여는 단계로 넘어갈 수 없습니다. 발견 단계의 차이를 설명하려면 기기가 보내는 정보 중 백그라운드에서 받을 수 있는 정보를 구분해야 했습니다.</p>
<p>BLE 기기는 자신이 제공하는 서비스를 Service UUID로 알릴 수 있습니다. 스캐너는 이 값을 이용해 대상 서비스를 제공하는 기기를 찾습니다. 그런데 UUID가 담긴 패킷에 따라 이 정보를 얻는 과정이 달라집니다.</p>
<p>당시 기기가 사용한 레거시 BLE 광고에서 <code>ADV_IND</code>는 기기가 보내는 광고 패킷입니다. 능동 스캔 중인 스캐너는 이 광고를 받은 뒤 <code>SCAN_REQ</code>로 추가 정보를 요청하고, 기기는 <code>SCAN_RSP</code>로 응답합니다. 수동 스캔에서는 추가 요청을 보내지 않습니다.</p>
<p>당시 백그라운드 출입에 사용하려던 스캔 조건에서는 이 스캔 응답을 받지 못했습니다. 따라서 발견에 필요한 Service UUID가 광고 패킷에 있는지, 추가 요청이 필요한 스캔 응답에만 있는지 확인해야 했습니다.</p>
<p>Apple의 액세서리 설계 가이드도 기기 상태에 따라 능동 스캔을 하지 않을 수 있다고 설명하며 다음과 같이 권고합니다.</p>
<blockquote>
<p>The primary services should always be advertised in the advertising PDU.
<em>- Accessory Design Guidelines for Apple Devices</em></p>
</blockquote>
<p>주요 서비스는 항상 광고 PDU에 포함해야 한다는 권고입니다. 여기서 광고 PDU는 앞서 설명한 광고 패킷을 가리킵니다. 이 권고를 기준으로 실제 기기가 Service UUID를 어디에 담는지 살폈습니다.</p>
<h2 id="발견에-필요한-uuid가-스캔-응답에만-있었습니다">발견에 필요한 UUID가 스캔 응답에만 있었습니다</h2>
<p>PacketLogger로 광고와 스캔 응답을 나눠 확인하니, 대상 Service UUID는 <code>ADV_IND</code>에 없고 <code>SCAN_RSP</code>에만 있었습니다. 포그라운드에서 얻을 수 있던 UUID가 백그라운드에서는 받지 못하는 응답에 담겨 있었던 것입니다. 이 배치로는 해당 UUID를 기준으로 백그라운드에서 기기를 발견할 수 없었습니다.</p>
<p>앱의 파싱 코드를 바꾸더라도 수신하지 못한 응답에서 UUID를 읽을 수는 없습니다. 백그라운드 출입을 지원하려면 추가 스캔 응답 없이도 발견에 필요한 정보를 얻을 수 있도록 기기의 광고 구성을 바꿔야 했습니다.</p>
<h2 id="광고-패킷에-uuid를-담도록-요청했습니다">광고 패킷에 UUID를 담도록 요청했습니다</h2>
<p>저는 iOS의 스캔 조건과 Apple의 권고, 패킷 분석 결과를 근거로 제조사에 Service UUID를 <code>ADV_IND</code>로 옮기도록 요청했습니다.</p>
<p><img src="https://velog.velcdn.com/images/ryan-son/post/39b0daad-89ca-4c60-82c4-16244b9ee99c/image.png" alt=""></p>
<p>제조사가 펌웨어를 수정한 뒤 백그라운드에서 실제 문이 열리는 것을 확인했습니다.</p>
<h2 id="현재-ios에-적용할-때-확인할-조건">현재 iOS에 적용할 때 확인할 조건</h2>
<p>이 사례에서는 앱의 실행 조건과 Service UUID의 패킷 위치를 함께 확인한 것이 해결의 근거가 됐습니다. 비슷한 증상을 조사할 때도 기기 발견과 연결 이후의 실패를 먼저 구분하면, 패킷 배치를 확인해야 할 문제인지 범위를 좁힐 수 있습니다.</p>
<p>iOS의 스캔 조건은 버전과 실행 상태에 따라 달라질 수 있습니다. Apple의 2026년 설명에는 iOS 26에서 Live Activity가 있을 때 일부 스캔 제한이 완화되지만, 잠금 화면이 꺼지면 다시 제한되는 조건도 나옵니다. 따라서 포그라운드·백그라운드라는 구분과 함께 화면 상태와 사용 중인 기능도 확인해야 합니다.</p>
<h2 id="참고자료">참고자료</h2>
<ol>
<li>Bluetooth SIG, <a href="https://www.bluetooth.com/bluetooth-le-primer/">The Bluetooth Low Energy Primer</a>. 레거시 광고 PDU와 능동·수동 스캔 설명.</li>
<li>Apple, <a href="https://developer.apple.com/accessories/Accessory-Design-Guidelines.pdf">Accessory Design Guidelines for Apple Devices</a>, 2026-09-21판, 58.4 Advertising Data, 350–351쪽.</li>
<li>Apple Developer Forums, <a href="https://developer.apple.com/forums/thread/815189">Background scanning for Bluetooth advertisements with LiveActivities on ios26</a>, Apple 엔지니어 답변, 2026년 2월.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[WWDC17] Behind the Scenes of the Xcode Build Process]]></title>
            <link>https://velog.io/@ryan-son/WWDC17-Behind-the-Scenes-of-the-Xcode-Build-Process</link>
            <guid>https://velog.io/@ryan-son/WWDC17-Behind-the-Scenes-of-the-Xcode-Build-Process</guid>
            <pubDate>Sun, 10 Oct 2021 07:43:20 GMT</pubDate>
            <description><![CDATA[<p>Xcode의 빌드 과정을 알아보겠습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/2070f0ec-4add-40a1-9128-c017643aec33/image.png" alt=""></p>
<h1 id="다룰-내용">다룰 내용</h1>
<ol>
<li>빌드 과정의 구조</li>
</ol>
<ul>
<li>Xcode가 빌드 과정을 모델링하고 조직하기 위해 프로젝트 파일 정보를 사용하는 방법</li>
</ul>
<ol start="2">
<li>컴파일러 영역</li>
</ol>
<ul>
<li>Clang과 Swift가 소스 코드를 오브젝트 파일로 빌드하는 방법</li>
<li>헤더와 모듈의 작동 방법</li>
<li>Swift 컴파일 모델이 기초적으로 C, C++, Objective-C와 다른 점</li>
</ul>
<ol start="3">
<li>링커</li>
</ol>
<ul>
<li>심볼들이 작성된 소스코드와 어떻게 연관되어 있는지</li>
<li>링커가 컴파일러가 생성한 오브젝트 파일을 받아 애플리케이션 또는 프레임워크와 같이 최종적으로 실행가능하게 이어 붙이는 방법</li>
</ul>
<p><code>PetWell</code>이라는 샘플 앱을 사용하여 이 과정을 알아봅니다. 이 앱은 애완동물 사진을 보여주는 앱입니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/e5622455-58cb-4439-bf84-d3a1cc96b22b/image.png" alt=""></p>
<h1 id="build-process">Build process</h1>
<p>아래 <code>PetWall</code> 프로젝트에는 앱 타겟, 프레임워크와 Swift 및 Objective-C로 작성된 소스코드 파일이 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/3d73c789-5ede-4d70-9e7c-9ffa0bdce6f4/image.png" alt=""></p>
<p>앱을 빌드할 때 소스 크드와 리소스에서 사용자에게 배포하기 위해 앱 스토어에 업로드하는 패키지로 변환하기까지 여러 단계를 거쳐야합니다. </p>
<ol>
<li>소스코드를 컴파일하고 링크</li>
<li>Headers, asset catalogues, storyboards와 같은 리소스들을 복사하고 처리</li>
<li>코드사인 또는 API 문서화, 코드 린팅, 검증 도구 사용을 위해 shell script 작업</li>
</ol>
<p><img src="https://images.velog.io/images/ryan-son/post/5a22ae39-b666-4c02-84b7-ee0aea715975/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/ryan-son/post/485ef008-db01-40c3-af63-7f6491383bd9/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/ryan-son/post/02e06cce-3fb5-4dbe-8a60-5855efe22a76/image.png" alt=""></p>
<p>대부분의 이러한 작업들 Clang, LD, AC tool, IB tool, Code sign 등과 같은 커맨드 라인 도구들이 수행합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/dc3d527a-554d-41c3-aac8-4066acc95753/image.png" alt=""></p>
<h2 id="빌드-과정">빌드 과정</h2>
<blockquote>
<p>빌드 작업들은 종속성 순 (dependency order)으로 실행된다.</p>
</blockquote>
<p>이러한 작업들은 Xcode 프로젝트에 따라 특정한 순서로 처리되어야 하지만, 빌드 시스템이 빌드를 수행할 때마다 조직화 작업을 자동으로 수행해줍니다.</p>
<p>빌드 작업이 실행되는 순서는 작업이 소비하는 인풋들과 작업이 생산하는 아웃풋들과 같이 작업의 종속정 정보들로 결정됩니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/f83fd1a9-fde7-4af4-b767-1accd6d1610f/image.png" alt=""></p>
<p>예를 들어, 컴파일 작업은 <code>PetController.m</code>과 같은 소스코드 파일을 소비하여 <code>PetController.o</code>와 같은 오브젝트 파일을 아웃풋으로 만듭니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/201ec1c4-06a4-41e1-959f-a16904a28b8d/image.png" alt=""></p>
<p>유사하게 링커 작업은 이전 작업으로 컴파일러가 만든 다수의 오브젝트 파일을 소비하여 .app 번들에 들어갈 <code>PetWall</code> 과 같은 실행 가능한 파일 (executable)이나 라이브러리를 아웃풋으로 만듭니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/1785ddb7-e199-4be4-91e4-2b533a75d8ba/image.png" alt=""></p>
<p>여기에서 패턴이 등장하는 것을 알 수 있습니다. 궁극적인 실행 순서를 나타내는 아래의 그래프 구조를 통해 종속성 정보가 흐르는 방법을 확인하실 수 있습니다. </p>
<p>이렇게 그래프에 나타낸 컴파일 작업은 차선과 유사합니다. 컴파일 작업들이 그들의 차선 안에서 완전히 독립적이기에 병렬적으로 실행될 수 있음을 알 수 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/4a00d40c-a3e8-4bbe-846d-3fceb549d73e/image.png" alt=""></p>
<p>그리고 링커 작업이 다른 모든 것들을 인풋으로써 받아들이기에 마지막에 와야하는 것도 알 수 있습니다. 이렇게 빌드 시스템이 종속성 정보를 통해 작업이 실행되어야 하는 순서와 어떤 작업들이 병렬적으로 실행되어야 하는지 결정하는 것을 종속성 순서 (dependency order)라 합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/53d27283-3ee9-46ad-af06-b6135a111387/image.png" alt=""></p>
<h2 id="빌드-과정의-작동-방법">빌드 과정의 작동 방법</h2>
<p>빌드를 누르면 Xcode 프로젝트 파일로부터 빌드 상세 (build description)을 받아 구문 해석 (parse)하고 프로젝트의 모든 파일, 타겟들, 종속성 관계, 빌드 설정들을 고려합니다. 그리고는 이를 directed graph라 부르는 트리와 유사한 구조로 변환합니다. Directed graph는 프로젝트에 있는 인풋과 아웃풋 파일들의 종속성들과 이들을 처리하기 위해 실행될 작업들을 나타냅니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/3c3c333b-ac87-462f-b577-bada38cbad81/image.png" alt=""></p>
<p>다음으로 Low-level execution engine이 이 그래프를 처리하여 종속성 상세사항 (dependency specifications)을 살펴보고 실행할 작업을 알아냅니다. 실행될 순서나 병렬적으로 실행될 작업들을 파악하여 이들을 실행합니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/77ab4b14-85c7-4c58-8bb5-0c6b0e21a0cd/image.png" alt=""></p>
<p>빌드 시스템의 Low-level build execution engine은 <code>llbuild</code>라 하며, 오픈 소스 프로젝트이기에 GitHub에서 소스코드를 확인하실 수 있습니다.</p>
<h2 id="discovered-dependencies">Discovered dependencies</h2>
<p>현재로써는 종속성 정보가 많지는 않기에 빌드 시스템이 작업 실행 과정 동안 더 많은 정보들을 찾아야 합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/803e8100-1a68-4c5f-a514-41e8b3b15af9/image.png" alt=""></p>
<p>예를 들어, Clang이 Objective-C 파일을 컴파일하면 예상하는대로 오브젝트 파일을 만듭니다. 하지만 해당 소스 파일에 포함된 헤더 파일 목록이 포함된 다른 파일을 생성할 수도 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/20b70657-a698-4d47-8650-5d686f43b468/image.png" alt=""></p>
<p>그러면 다음에 빌드할 때 빌드 시스템은 이 파일의 정보를 사용하여 포함된 헤더 파일이 변경되었는지 확인하여 필요하면 소스 파일을 다시 컴파일합니다. </p>
<p>그리고 <code>PetController.h</code>, <code>PetController.d</code>, <code>.n</code>을 통해 <code>.o</code> 파일 까지의 종속성 경로를 볼 수 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/bdc2b800-7cb1-4985-908c-a8fa9e6d47a3/image.png" alt=""></p>
<h2 id="증분-빌드-incremental-builds">증분 빌드 (Incremental Builds)</h2>
<p>지금까지 빌드 시스템의 주요 업무인 작업을 실행하는 방법에 대해 다루었습니다. 당연히 프로젝트가 커질수록 빌드 과정이 더 길어집니다. 하지만 빌드할 때마다 이런 작업들을 수행하지는 않습니다.</p>
<p>대신에 빌드 시스템은 그래프 상의 작업들의 일부만을 실행합니다. 이전 빌드로부터 변경한 사항에 따라 빌드가 실행되는 것이죠. 이를 증분 빌드 (Incremental build)라 하는데, 증분 빌드가 올바르고 효율적으로 작동하기 위해서는 정확한 종속성 정보를 가지고 있는 것이 매우 중요합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/a19dc71f-9dc4-4cdc-ac25-c0f71bf5dda7/image.png" alt=""></p>
<h3 id="change-detections-and-task-signatures">Change Detections and Task Signatures</h3>
<ul>
<li>빌드 그래프의 각 작업은 signature를 가지고 있음 (해시값)</li>
<li>signature은 인풋들의 상태 정보 (파일 경로, 수정 시간)와 기타 작업 메타데이터 (사용된 컴파일러의 버전) 로부터 계산됨</li>
<li>빌드 시스템은 작업의 현재와 이전 빌드의 시그니처를 추적</li>
<li>빌드가 수행될 때 재수행 여부를 결정 (signature가 다르면 재수행, 같으면 건너뜀)</li>
</ul>
<h2 id="빌드-시스템을-돕기-위한-방법">빌드 시스템을 돕기 위한 방법</h2>
<p>빌드 프로세스는 특정한 순서로 이루어진다고 했지만 이 순서를 조직하는 일은 빌드 시스템이 할 일이므로 관심 대상에서 제외하겠습니다.</p>
<p>대신에 개발자로서 작업들 간의 종속성을 고려하여 빌드 시스템이 그래프의 구조에 따라 최선의 방법으로 실행하는 방법을 알려주는데 집중해봅시다. 이를 통해 빌드 시스템이 작업의 순서를 올바르게 결정하고 멀티코어 하드웨어를 최대한 활용하기 위해 작업을 병렬화할 수 있을 것입니다.</p>
<h3 id="종속성">종속성</h3>
<ol>
<li><p>빌드 시스템 내에서의 빌드 방식 (Built in)
빌드 시스템은 컴파일러, 링커, 애셋 카탈로그 및 스토리보드 프로서스 등에 대한 규칙을 가지고 있습니다. 그리고 이러한 규칙은 어떤 종류의 파일이 인풋으로 허용되고 어떤 출력이 생성되는지 정의합니다.
<img src="https://images.velog.io/images/ryan-son/post/9427d737-e4a5-4eb7-8f94-e95f4c901a61/image.png" alt=""></p>
</li>
<li><p>타겟 종속성 (Target dependencies)
타겟 종속성은 타겟들이 빌드될 대략적인 순서를 결정합니다. 어떤 경우에 빌드 시스템은 다른 타겟들과 병렬 소스들을 컴파일할 수 있습니다. 이전에 Xcode에서 타겟이 빌드될 때 전체 종속 대상의 컴파일을 완료해야 시작될 수 있었던 점과 대조됩니다.</p>
</li>
</ol>
<p>현재의 Xcode 빌드 시스템에서 타겟은 더 빨리 빌드를 시작할 수 있습니다. 이는 소스 컴파일 단계가 비용 없이 일부 병렬화를 제공하기 위해 더 일찍 시작할 수 있음을 의미합니다. 하지만 실행 스크립트 단계 (run script phases)를 사용하는 경우 이 병렬화가 적용되기 전에 해당 스크립트 단계를 완료합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/c3049fff-98f8-4867-bf3b-1f8c49c16d23/image.png" alt=""></p>
<ol start="3">
<li><p>암시적 종속성 (Implicit dependencies)
암시적 종속성은 타겟 종속성과 다소 관련이 있습니다. 예를 들어, 바이너리 빌드 단계가 있는 링크 라이브러리의 타겟을 나열하고 스키마 편집기에서 암시적 종속성이 활성화된 경우 (기본값이 활성화 상태), 빌드 시스템은 타겟 종속성에 나열되지 않은 경우에도 해당 타겟에 대한 암시적 종속성을 설정합니다.</p>
</li>
<li><p>빌드 단계 종속성 (Build phase dependencies)
타겟 편집기에는 헤더 복사, 소스 컴파일, 번들 리소스 카피 등과 같이 수많은 빌드 단계가 있습니다. </p>
</li>
</ol>
<p><img src="https://images.velog.io/images/ryan-son/post/615cf4a2-6edc-4477-8079-f1b35ef799bd/image.png" alt=""></p>
<p>이러한 단계와 관련된 작업은 일반적으로 단계가 나열된 순서에 따라 실행되는 그룹입니다. 그러나 빌드 시스템이 더 잘 알고 있는 경우 해당 순서를 무시할 수 있습니다. <code>Link Binary With Libraries</code>를 <code>Compile Sources</code> 전에 위치시킨다든지 하는 경우에요. 빌드 단계 순서가 잘못되면 빌드 문제나 실패가 발생할 수 있으므로 종속성을 이해하고 빌드 단계가 올바른 순서가 되어 있는지 확인해야 합니다.</p>
<ol start="5">
<li>스킴 순서 종속성 (Scheme order dependencies)
스킴 설정에서 병렬 빌드 체크 박스를 활성화하면 더 나은 빌드 성능을 얻을 수 있으며 스킴에 있는 타겟 순서는 중요하지 않게 됩니다. 그러나 병렬 빌드를 비활성화하면 Xcode는 스킴의 빌드 작업에 나열된 순서대로 대상을 하나씩 빌드합니다. 타겟 종속성은 먼저 빌드할 대상을 결정할 때 여전히 더 높은 우선 순위를 갖고 있습니다. 그러나 그렇지 않으면 Xcode는 그 순서를 존중합니다. 이제 종속성을 올바르게 설정하지 않은 경우에도 예측 가능한 빌드 순서를 제공하므로 이를 사용하기를 희망할 수 있습니다. 그러나 이렇게하면 병렬화가 되지 않기에 빌드 속도가 느려집니다. 따라서 빌드 병렬화 체크 발스를 활성화된 상태로 두고 타겟 종속성을 올바르게 설정하여 순서에 의존하지 않게 만드는 것이 좋습니다.</li>
</ol>
<p><img src="https://images.velog.io/images/ryan-son/post/2875982d-a198-4005-a7d7-3fc64cb3390b/image.png" alt=""></p>
<ol start="6">
<li>커스텀 빌드 스크립트 단계
마지막 종속성 정보는 개발자가 설정한 내용이 있을 수 있습니다. 커스텀 셸 스크립트 빌드 단계나 빌드 규칙을 만든다면 빌드 시스템에게 인풋과 아웃풋이 무엇인지 알려주어야 합니다. 이는 빌드 시스템이 불필요한 작업 스크립트를 재실행하는 것을 피할 수 있도록 해주고 올바른 순서로 실행했는지를 판단하게 해줄 것입니다. 이러한 파일들의 경로는 스크립트에서 환경 변수로 사용할 수 있습니다.</li>
</ol>
<p><img src="https://images.velog.io/images/ryan-son/post/19b62c8b-eb24-4e7e-891e-1c538f99b7ad/image.png" alt=""></p>
<hr>
<h2 id="avoid-auto-link-for-project-dependencies">Avoid Auto-Link for Project Dependencies</h2>
<p>프로젝트의 타겟 종속성 auto-link에 의존하지 마세요. 이 설정을 사용하면 링크 라이브러리의 빌드 단계에서 명시적으로 링크하지 않고도 가져온 모듈에 해당하는 프레임워크에 컴파일러가 자동으로 링크할 수 있습니다. 그러나 자동 링크는 빌드 시스템 수준에서 해당 프레임워크에 대한 종속성을 설정하지 않는다는 점에 유의하여야 합니다. 따라서 연결을 시도하기 전에 의존하는 대상이 실제로 구축된다는 보장은 없습니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/0a15f7b1-b8b4-4932-823c-852832a2ad39/image.png" alt=""></p>
<h2 id="add-explicit-dependencies">Add Explicit Dependencies</h2>
<p>따라서 플랫폼 SDK의 프레임워크에 대해서만 이 기능에 의존해야 합니다. Foundation 및 UIKit과 마찬가지로 빌드가 시작되기 전에 이미 존재한다는 것을 알고 있기 때문입니다. 자체 프로젝트의 타겟에 대해 명시적으로 라이브러리의 종속성을 추가해야 합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/0b82bbef-3597-40b3-9a82-d3f438998d66/image.png" alt=""></p>
<h2 id="create-workspace-and-project-references">Create Workspace and Project References</h2>
<p>의존하는 다른 프로젝트의 타겟을 나타내기 위해 다른 Xcode 프로젝트를 프로젝트의 파일 탐색기로 끌어다 놓아 프로젝트 참조를 생성할 수도 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/db75add4-691a-4c5a-b3b6-618c1fd97884/image.png" alt=""></p>
<p>결론적으로, 정확한 종속성 정보를 사용하면 빌드 시스템이 빌드를 더 잘 병렬화할 수 있고 매번 일관된 결과를 얻을 수 있으므로 빌드 시간을 줄이고 개발에 더 많은 시간을 할애할 수 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/633d09b5-afad-4bfd-b500-ee0a38e5ffb0/image.png" alt=""></p>
<h1 id="clang-builds">Clang Builds</h1>
<p>살펴볼 두 가지 기능들:</p>
<ol>
<li>Header maps (Xcode 빌드 시스템과 Clang 컴파일러 간의 정보 소통을 위해 사용)</li>
<li>Clang modules (빌드 가속을 위해 사용)</li>
</ol>
<p><img src="https://images.velog.io/images/ryan-son/post/612c03b6-684b-4414-8663-a40296a16597/image.png" alt=""></p>
<p>현재 Swift만 사용하시는 분들도 계시겠지만, Swift는 내부적으로 Clang을 사용합니다. </p>
<h2 id="clang">Clang</h2>
<p>Clang은 Apple의 공식 C 컴파일러이자 C, C++ 및 대부분의 프레임워크에 사용되는 Objective-C와 같은 C 언어군을 위한 컴파일러입니다. 앞서 말씀드린 바와 같이 컴파일러는 여러 인풋 파일을 받아 하나의 출력 파일을 생성한 다음 추후 링커가 이를 사용합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/9780d40b-5dbd-4182-9e56-5fd7f723d660/image.png" alt=""></p>
<p>iOS API들에 접근하거나 구현한 코드에 접근하고자 한다면 헤더 파일을 포함해야 합니다. 헤더 파일은 약속이라고 할 수 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/e0c3a56d-936e-4903-b3c6-c4792fa706f3/image.png" alt=""></p>
<p>어딘가에 이것의 구현부가 존재한다는 것을 약속하는 것입니다. 물론 구현부 파일만 업데이트하고 헤더 파일을 업데이트하지 않는다면 약속을 깨뜨리는 것입니다. 컴파일러가 약속을 신뢰하기 때문에 컴파일 시에는 break가 일어나지 않습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/3fbe3328-adf4-4b6b-a368-4dc965ef40da/image.png" alt=""></p>
<p>이는 링크 중에 break가 일어납니다. 컴파일러는 하나 이상의 헤더 파일을 가지고 있으며 이는 모든 컴파일러의 호출을 야기합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/df6c6e25-08a7-42d0-8274-e15a48e272a1/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/ryan-son/post/65240bf0-c52f-4730-b5ca-033dbda82aa0/image.png" alt=""></p>
<p>예시 앱을 통해 헤더 파일을 다루는 방법을 알아보겠습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/5c5359d0-4ae7-43e7-b9e2-986b14efc0d0/image.png" alt=""></p>
<p>PetWall은 여러 언어가 공존하는 애플리케이션입니다. 애플리케이션 자체는 Swift로 작성되어 있으며 Objective-C로 작성된 프레임워크를 사용하고 있습니다. 그리고 C++로 작성된 support library를 가지고 있습니다. 시간이 지나 애플리케이션이 성장하여 파일을 찾기 용이하도록 다시 조직하였다고 해봅시다. cat과 연관된 파일들을 모두 하위 폴더에 옮기는거죠. 그래도 구현부 파일을 변경할 필요는 없습니다.</p>
<h3 id="clang이-헤더-파일을-찾는-방법">Clang이 헤더 파일을 찾는 방법</h3>
<p>Clang은 헤더 파일을 어떻게 찾을까요? 간단한 예시를 보시겠습니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/97611e53-d6e7-4e7e-b31d-80300e9fcda9/image.png" alt=""></p>
<p>아래 코드는 구현부 파일 중 하나로 <code>cat.h</code>라 부르는 헤더 파일을 포함하였습니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/2403af8b-f23a-4cc7-a6ab-d54bdef34db4/image.png" alt=""></p>
<p>Clang이 무엇을 하는지 파악하려면 빌드 로그를 살펴보면 됩니다. 빌드 로그를 살펴보면 특정한 파일을 컴파일할 때 Xcode 빌드 시스템이 무엇을 하였는지 알 수 있습니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/052487e4-0e7f-4b13-a641-69de58fad0ca/image.png" alt=""></p>
<p>이를 verbose를 의미하는 <code>-v</code> 옵션을 붙여 터미널에 붙여넣으면 Clang이 많은 정보를 알려줄 것입니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/16d3badd-262c-4d3c-90e8-3617aea42db9/image.png" alt=""></p>
<p>이제 헤더맵(headermaps)을 살펴보겠습니다.</p>
<p>헤더맵은 헤더 파일들의 위치를 알기 위해 Xcode 빌드 시스템이 사용하는 것입니다. 이제 가장 중요한 두 헤더맵 파일을 보겠습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/80b9acd2-264d-45fa-ae3c-3c95eb0dd2fa/image.png" alt=""></p>
<p>처음 두 엔트리들은 프레임워크 이름을 헤더에 넣어줍니다. 이 두 헤더들은 공개 (public) 헤더들입니다. 이 기능에 의존하면 안됩니다. 그 이유는 기존 프로젝트가 계속 작동하도록 유지하고 있을 때 Clang 모듈에 문제가 있을 수 있으므로 자체 프레임워크에서 공개 또는 비공개 헤더 파일을 포함할 때 항상 프레임워크 이름을 지정하는 것이 좋습니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/c087eca8-2484-4c90-96f6-c045753a34af/image.png" alt=""></p>
<p>세 번째 엔트리는 프로젝트 헤더입니다. 이 경우에 필수는 아닙니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/88062360-9516-4499-817f-5e34110a11d2/image.png" alt=""></p>
<p>헤더맵의 목적은 소스코드로 포인트 백 (point back) 해주는 것이니까요. 공개 및 비공개 헤더들로 동일한 작업을 수행합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/44d8e5c9-bf41-42e1-9f3a-1d886b81a4cf/image.png" alt=""></p>
<p>Clang이 소스코드 경로에 있는 파일들이 유용한 에러와 경고 메시지들을 만들고 빌드 디렉토리의 다른 위치에 있을 수 있는 잠재적 복사본이 아닌 파일을 생성할 수 있도록 소스코드로 포인트 백 시킵니다.</p>
<h3 id="헤더맵과-관련된-흔한-프로젝트-이슈">헤더맵과 관련된 흔한 프로젝트 이슈</h3>
<ul>
<li>프로젝트에 헤더를 넣지 않음 (헤더는 프로젝트의 일부가 아님)<ul>
<li>이는 소스 경로에 있지만 프로젝트 자체는 아니라 프로젝트에 추가하여야 함</li>
</ul>
</li>
<li>헤더를 같은 이름으로 지음<ul>
<li>같은 이름이면 서로를 가리게 되므로 겹치지 않는 고유의 이름을 지어야 함 </li>
</ul>
</li>
</ul>
<p>이는 시스템 헤더에도 적용됩니다. 프로젝트에 시스템 헤더와 동일한 이름을 가진 로컬 헤더가 있으면 시스템 헤더를 가리게 되므로 이 상황을 피해야 합니다.</p>
<h3 id="시스템-헤더를-찾는-방법">시스템 헤더를 찾는 방법</h3>
<p>아래는 PetWall의 다른 예시인데, 여기에 SDK에 있는 <code>Foundation.h</code> 헤더 파일을 넣었습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/40f5ddab-0a68-4019-8e12-e0f4ab0f6924/image.png" alt=""></p>
<p>자신의 헤더 파일을 찾을 때 이전에 했던 것과 같은 일을 할 수 있지만, 현재 우리는 시스템 헤더를 찾고 있습니다. 헤더맵은 자체적인 헤더만을 대상으로 하기에 이를 무시할 수 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/1b1f18df-436f-445c-8435-62c104f71e2a/image.png" alt=""></p>
<p>포함하고 있는 경로에 초점을 맞춰보면, 기본적으로 SDK에 있는 두 개의 경로를 볼 수 있습니다. 첫 번째는 사용자가 넣은 것, 시스템 라이브러리 프레임워크가 넣은 것입니다. 이는 일반적인 포함 경로입니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/ba178ce0-9e0f-4642-912f-82c46a17470c/image.png" alt=""></p>
<p>사용자측에 <code>Foundation/Foundation.h</code> search term을 넣어서는 결과가 없어 찾을 수 없습니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/f7f695f1-c96b-49f2-a428-dc8e5e462b14/image.png" alt=""></p>
<p>시스템 라이브러리 프레임워크에서는 작동 방식이 사용자측과 달리 프레임워크의 종류와 존재 여부를 식별한 후 헤더 파일의 헤더 경로를 찾습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/fb3db105-f9cf-47b0-afcf-228e8bd11677/image.png" alt=""></p>
<p>이 때 헤더 파일이 존재하지 않는다면 비공개 헤더 경로에서 헤더 파일을 찾습니다. Apple은 SDK에 비공개 헤더를 넣지 않지만, 다른 프레임워크들은 공개 및 비공개 헤더들을 넣을 수 있습니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/9c405aee-fe96-4038-8d61-6870e7a6406b/image.png" alt=""></p>
<p>헤더 파일을 찾는데 실패했지만 더 이상 경로를 탐색하게 하지 않습니다. 프레임워크를 이미 찾은 상태이기 때문이죠. 프레임워크를 찾았으므로 프레임워크 경로에 헤더가 있을 것이라 예상합니다. 하지만 이를 찾지 못하면 탐색을 중지합니다. </p>
<p>모든 헤더들이 넣어지고 전처리가 된 후 구현부 파일의 생김새가 궁금하다면 Xcode가 전처리 파일을 만들게 하면 됩니다. 이는 매우 큰 아웃풋 파일을 만들 것입니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/61c51e3b-a501-432e-a4ef-e02b75950d23/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/ryan-son/post/b4ff0ca6-51e4-4b9d-971f-290bcd1a4f74/image.png" alt=""></p>
<p><code>Foundation.h</code>는 시스템에서 매우 기초적인 헤더입니다. 직접적으로 넣거나 다른 헤더 파일을 통해 간접적으로 넣는 방식으로 많이 적용되고 있습니다. 이는 컴파일러가 매번 이 헤더를 찾기 위해 호출한다는 것을 의미합니다.</p>
<p>Clang은 한 줄의 include 구문을 위해 800 개 이상의 헤더 파일을 탐색하여 처리해야 합니다. 이는 구문 분석되고 검증되어야할 소스코드가 9 MB가 넘는다는 것입니다. 그리고 이러한 일이 컴파일러가 호출될 때마다 일어난다는 것이죠. 이는 많은 일이며 불필요한 일입니다.</p>
<p>이를 개선시킬 수 있는 방법 중 하나는 미리 컴파일된 (precompiled) 헤더 파일을 사용하는 것입니다. 하지만 더 좋은 것이 있는데, 이것이 Clang modules입니다.</p>
<h2 id="clang-modules">Clang Modules</h2>
<p>Clang 모듈은 프레임워크당 헤더를 한 번 찾아 구문분석한 후 이 정보를 디스크에 저장하여 캐시된 상태로 재사용될 수 있도록 만들어 빌드 시간을 개선합니다.</p>
<p>이를 위해 Clang 모듈은 특정 속성을 가져야 합니다. </p>
<h3 id="context-free">Context-free</h3>
<p>그 중 하나가 context-free입니다. 아래 두 코드들에 <code>PetKit</code> 모듈을 임포트하였지만 그 전에 두 가지 다른 매크로 정의가 넣었습니다. 이 헤더들을 임포트하는데 기존 모델을 사용했다면 이들이 포함되었다는 것을 의미합니다. 전처리기는 이 정의를 따라 헤더 파일을 적용할 것입니다. 하지만 이렇게 하면 모듈들은 각 헤더 케이스에 따라 다를 것이므로 재사용할 수 없습니다. 그래서 모듈들을 사용하려면 이렇게 해서는 안됩니다. 대신에 모듈이 context와 관련된 모든 정보들을 무시하도록하여 모든 구현부 파일에 재사용될 수 있도록 합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/97ee1a82-b048-45c8-a006-b8e63a3052ae/image.png" alt=""></p>
<h3 id="self-contained">Self-contained</h3>
<p>또 다른 하나의 요건은 모듈이 self-contained하여야 한다는 것입니다. 이는 모든 종속성을 나타내야 한다는 것입니다. 임포트 작업을 위해 어떠한 추가 헤더 파일도 추가할 걱정을 하지 않아도 되며 한 번 모듈을 임포트하면 작동한다는 의미입니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/f62266a0-705f-406f-b5ef-5690006dcba3/image.png" alt=""></p>
<p>그럼 Clang은 모듈을 빌드 여부를 어떻게 알까요? <code>NSString.h</code>의 사례를 살펴봅시다. 먼저 Clang은 프레임워크에서 특정 헤더를 찾습니다. <code>Foundation.framwork</code> 경로죠. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/f3a69355-1855-4bc2-b731-55fffa68397a/image.png" alt=""></p>
<p>다음으로 Clang 컴파일러는 헤더의 경로에 상대적인 모듈 경로와 모듈 맵을 찾습니다. 모듈 맵은 특정 헤더 파일들의 집합이 모듈로 변환되는 방식을 나타냅니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/83529cc7-2779-4324-8ed5-3bcc941b3ca5/image.png" alt=""></p>
<p>Foundation이라는 모듈 이름과 어떤 헤더들이 이 모듈의 일부인지 나타내고 있습니다. 여기에는 <code>Foundation.h</code> 헤더라는 하나의 모듈만 가지고 있는데 이는 <code>umbrella</code> 키워드를 가진 특수한 헤더입니다. 이는 <code>NSString.h</code>이 모듈의 일부인지 알아내기 위해 Clang이 이 특정한 헤더 파일을 살펴보아야 한다는 것입니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/b40906c3-4ddd-4c17-8f24-36ccfacffae1/image.png" alt=""></p>
<p>이제 <code>NSString.h</code>가 foundation 모듈의 일부임을 알았습니다. Clang은 텍스트 임포트를 모듈 임포트로 업그레이드할 수 있으며 이를 위해 foundation 모듈을 빌드해야 합니다. 그렇다면 Foundation 모듈을 어떻게 빌드할까요? 우선 이를 위해 별도의 Clang 위치를 만듭니다. 그리고 이 Clang 위치는 foundation 모듈의 모든 헤더 파일들을 가지고 있습니다. 우리는 원래 (original) 컴파일러 호출에서 기존 context를 전달하지 않았으므로 context-free입니다.</p>
<p>실제로 전달해야할 것은 Clang에 전달한 커맨드 라인 전달인자입니다. foundation 모듈을 빌드하는 동안 프레임워크 자체는 추가적인 프레임워크를 포함하므로 이 모듈 또한 빌드해야 합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/97db6e9c-7a5c-419a-aff2-37181a452e9e/image.png" alt=""></p>
<p>하지만 여기에서 이점을 찾을 수 있습니다. 임포트한 것들 중 일부는 동일할 수 있으므로 해당 모듈을 재사용하면 되는 것입니다. 이러한 모듈은 모듈 캐시라고 하는 디스크에 저장됩니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/0e5758e2-a2b5-44ec-be1b-2fd44e220a97/image.png" alt=""></p>
<p>앞서 언급하였듯이 커맨드 라인 전달인자들은 모듈을 만들 때 전달되는데, 이는 이 전달인자들이 모듈의 내용에 영향을 미칠 수 있다는 것을 의미합니다.</p>
<p>결과적으로 이 모든 전달인자들을 해시하고 이러한 특정 컴파일러 호출을 위해 생성한 모듈을 해당 해시와 일치하는 경로에 저장해야 합니다. 다른 제한 파일에 대한 컴파일러 전달인자를 변경하는 경우, 예를 들어 enable cat이라고 하면 이는 다른 해시이며, Clang이 해당 해시와 일치하는 해당 디렉토리에 모든 모듈의 인풋들을 다시 빌드해야 합니다. 따라서 모듈 캐시를 최대한 재사용하기 위해서는 전달인자를 동일하게 유지해야 합니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/d958fe58-db2a-40b1-919d-2801003be98c/image.png" alt=""></p>
<p>시스템 프레임워크의 모듈을 찾아 빌드하는 방법은 알았는데 자체적인 프레임워크는 어떨까요? 기존 cat 예시로 돌아가봅시다. 이번에는 모듈을 켰습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/b2878057-102e-4c9d-a862-263f6e6a77b7/image.png" alt=""></p>
<p>만약 헤더맵을 다시 사용한다면, 헤더맵은 소스 경로로 포인트 백 시킬 것입니다. 하지만 소스 경로를 보면 문제가 있습니다. 모듈 경로가 없거든요. 프레임워크 같아 보이지도 않기 때문에 Clang은 이 경우 어떻게 해야할지 모를 것입니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/9207a0ca-f563-4284-bd7a-1a9686a3e849/image.png" alt=""></p>
<p>이 문제를 해결하기 위해 Clang의 Virtual File System이라 부르는 새로운 개념을 소개합니다. 이는 Clang이 모듈을 빌드할 수 있도록 프레임워크의 가상 추상화결과물을 만듭니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/04595ade-3824-437e-b324-dadb7c885292/image.png" alt=""></p>
<p>하지만 추상화 결과물은 경로로 파일을 포인팅시키기 때문에 다시 Clang은 소스코드에 대한 에러를 만들 것입니다. 앞서 말씀 드렸듯이 프레임워크 이름을 명시하지 않으면 이슈가 발생할 수 있습니다. 어떤 문제가 발생할 수 있는지 예시를 보여 드리겠습니다. 두 개의 임포트가 있습니다. 첫 번째는 <code>PetKit</code> 모듈이며 두 번째는 <code>PetKit</code> 모듈의 일부이지만 프레임워크 이름을 명시하지 않았기에 Clang은 알아채지 못할 것입니다. 이 경우 중복 정의 에러가 발생할 수 있습니다. 동일한 헤더를 임포트하면 발생하는 것이죠.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/5877208e-e417-4acf-80bc-c03845b54b1d/image.png" alt=""></p>
<p>Clang은 이러한 이슈를 해결하기 위해 내부적으로 바쁘게 작동합니다. 하지만 고칠 수 없죠. 이제 context를 바꾸는 조그마한 변화를 주어봅시다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/3bc003ba-1396-4361-8388-e31ee4231f89/image.png" alt=""></p>
<p>모듈 임포트는 context를 무시하기 때문에 이에 영향받지 않을 것입니다. cat 임포트는 여전히 이 변경 사항을 관찰할 헤더의 텍스트 임포트일 뿐입니다. </p>
<p>중복 정의가 없을 수 있지만 이제 모순된 정의를 가질 수 있습니다. 이 문제는 Clang이 해결할 수 없는 일이죠. 그래서 앞서 말씀 드렸듯이 항상 공개 또는 비공개 헤더를 임포트할 때 프레임워크 이름을 명시하기를 권장 드립니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/e3def3be-3121-457d-a88e-6ec58ed930cf/image.png" alt=""></p>
<h1 id="swift-builds">Swift Builds</h1>
<p>Swift와 빌드 시스템이 프로젝트에서 선언한 것들을 찾기 위해 협업하는 모습을 살펴보겠습니다.</p>
<p>이전 내용을 잠시 돌이켜보면, Clang은 각 Objective-C 파일을 따로 컴파일한다고 했습니다. 이 말은 다른 파일에 있는 클래스를 참조하고 싶다면 해당 클래스가 선언된 헤더를 임포트 해야 한다는 것입니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/e708bbac-caf6-4735-9e7f-bd7c768d901a/image.png" alt=""></p>
<p>하지만 Swift는 헤더를 작성할 필요가 없도록 설계되었습니다. 이는 초보자들이 언어를 시작하는 것을 용이하게 해주고 별도의 파일에 동일한 선언을 반복하는 것을 방지하게 해주죠. 하지만 이는 컴파일러가 추가적으로 장부 관리 작업 (bookkepping)을 수행해야 한다는 것이기도 합니다. 이제 장부 관리 작업이 이루어지는 방식을 살펴보도록 하겠습니다.</p>
<p><code>PetWall</code> 앱으로 돌아가 봅시다. 앱에는 ViewController에 Swift로 작성된 view, Objective-C AppDelegate, Swift unit test들이 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/8674cded-0266-430e-92cd-31afadf9def7/image.png" alt=""></p>
<p><code>PetViewController</code>라는 최상단에 위치한 파일을 컴파일하기 위해 컴파일러는 네 개의 다른 작업을 수행하여야 합니다.</p>
<ul>
<li>Swift 타겟과 Objective-C로부터 오는 두 가지에서 선언문들을 찾아야 합니다. </li>
<li>Objective-C와 다른 Swift 타겟들에서 선언문들을 찾아 사용할 수 있게끔 파일의 내용을 설명하는 인터페이스를 만들어야 합니다.</li>
</ul>
<p><img src="https://images.velog.io/images/ryan-son/post/377c8906-5a33-48d4-ab0f-88318aa51ca6/image.png" alt=""></p>
<p>예시를 통해 더 자세히 살펴보겠습니다. <code>PetViewController.swift</code>를 컴파일할 때, 컴파일러는 호출 가능 여부를 판단하기 위해 PetView의 이니셜라이저의 타입을 탐색합니다. 하지만 이 작업을 하기 전에 이니셜라이저 선언이 제대로 되어 있는지 확인하기 위해<code>PetView.swift</code>를 구문 분석하여 검증할 필요가 있습니다. 컴파일러는 이니셜라이저의 구현부 (body)까지 확인할 필요는 없다는 것을 알기에 이 부분을 체크하지는 않지만 파일의 인터페이스 부분을 처리하기 위해 몇 가지 작업을 더 수행하여야 합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/be9f1cbf-6c5b-4308-962c-67c76e83e1a8/image.png" alt=""></p>
<p>Clang과는 달리 Swift 파일을 컴파일할 때 컴파일러는 타겟 안의 모든 Swift 파일을 구문 분석합니다. 이 파일들 중 일부가 인터페이스와 관련이 있는지를 판단하기 위해서죠.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/b1250286-eadc-4904-9389-342456d4ce58/image.png" alt=""></p>
<p>Xcode 9에서는 컴파일러가 각 파일을 따로 컴파일하여 증분 빌드 시 빌드를 반복했을 때 반복되는 작업이 있었습니다. 이는 파일들이 병렬적으로 컴파일 될 수 있도록 했지만 컴파일러가 반복적으로 각 파일을 파싱하도록 강요했습니다. 구현한 것으로 <code>.o</code>를 만들려고 하면 선언문을 찾기 위해 여러 번 인터페이스를 찾아야 했죠.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/6e1293e7-861d-43ad-b78a-287198a3a6ef/image.png" alt=""></p>
<p>Xcode 10에서는 이러한 간접 비용 (overhead)를 줄였습니다. 가능한 한 많은 작업을 공유하는 그룹으로 파일을 결합하여 수행합니다. 최대한의 병렬 처리도 여전히 사용할 수 있습니다. 이는 그룹 내에서 파싱 결과를 재사용하고 그룹 간에서만 반복 작업이 일어납니다. 그룹의 수는 일반적, 상대적으로 적으므로 증분 디버그 빌드를 상당히 빠르게 만들어 줄 수 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/0255a850-27ea-4125-ad9c-11cd2016a9dd/image.png" alt=""></p>
<p>이제 Swift 코드는 Swift 코드만을 호출하지 않습니다. Objective-C도 호출할 수 있죠. PetWall 예시 앱으로 돌아가보면, Objective-C로 작성된 UIKit과 같은 시스템 프레임워크도 있으니까요.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/b1d01cb0-21a6-476b-8f18-cbd83722467c/image.png" alt=""></p>
<p>Swift는 다른 많은 언어들과는 다른 접근 방식을 취합니다. 외부의 다른 함수 인터페이스를 제공할 필요도 없죠. 예를 들어 Objective-C API를 사용하려면 일반적으로 이에 대한 Swift 선언을 작성해야 하지만 Swift 컴파일러인 swiftc는 내부에 Clang의 많은 부분을 포함하고 이를 라이브러리로 사용하고 있기 때문에 이를 통해 Objective-C 프레임워크를 직접 가져올 수 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/ce55fb61-3e7f-4346-bf3d-2b1bd4d85246/image.png" alt=""></p>
<p>그럼 Objective-C 선언은 어디에서 올까요? 아래와 같이 임포터가 타겟의 타입에 따라 헤더를 찾습니다.</p>
<ul>
<li>어떤 타겟이라도 Objective-C 프레임워크를 임포트하면 해당 프레임워크의 Clang 모듈 맵을 통해 드러난 선언문을 찾습니다.</li>
<li>Swift와 Objective-C 코드가 혼합된 프레임워크에서는 임포터는 umbrella 헤더에서 선언문을 찾습니다.<ul>
<li>Umbrella 헤더는 공개 인터페이스를 정의합니다. 이 방식으로 프레임워크 내부의 Swift 코드는 동일한 프레임워크의 공개된 Objective-C 코드를 호출할 수 있습니다.</li>
</ul>
</li>
<li>애플리케이션과 unit test 번들에서는 타겟의 브리징 헤더 (bridging header)에 임포트들을 추가하여 선언이 Swift로 호출되도록 합니다. 이제 임포터가 선언을 가져오면 이들을 더 관용적으로 만들기 위해 변경합니다.</li>
</ul>
<p>예를 들어, Swift의 내장 에러 처리 언어 기능을 사용하기 위해 쓰로잉 메서드들로 <code>NSError</code>를 사용하는 Objective-C 메서드를 임포트할 수 있습니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/7a78fe4e-746a-441e-93c6-ed7359041bc7/image.png" alt=""></p>
<p>이 과정에서 동사와 전치사 다음에 오는 매개변수 타입 이름을 제거합니다. 예를 들어, <code>drawPet atPoint</code> 메서드에서는 pet이라는 단어가 있습니다. 동사 draw 다음에 오는 Pet 타입의 매개변수, 그리고 마찬가지로 전치사 at 다음에 오는 CGPoint 타입의 매개변수에 대한 단어 point가 있습니다. 이 메서드를 Swift로 가져올 때 단순히 <code>draw(_:at:)</code>으로 생략되어 표현됩니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/88b97d2a-6472-4dae-b913-2185e74a6456/image.png" alt=""></p>
<p>이것은 어떻게 작동할까요? 컴파일러가 흔한 영어 동사와 전치사 목록을 가지고 있다는 것을 아시면 놀라실지도 모르겠습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/fe5990e4-a96d-4945-9563-ff3904476a6f/image.png" alt=""></p>
<p>하드 코딩된 목록이며 인간이 사용하는 언어이기에 지저분하여 종종 단어를 놓칠 때도 있습니다. 더욱이 Swift의 명명 규칙 (컨벤션)에 맞추기 위해 임포터는 품사를 기반으로 단어를 제거하여 메서드의 이름도 바꿉니다. 종종 목록에 단어가 없어 놓치는 경우도 있는데, 이 경우에는 <code>NS_SWIFT_NAME</code> 어노테이션을 이용하여 컴파일러에게 원하는 메서드를 임포트 해달라고 할 수 있습니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/39a01e38-af9b-4a13-b800-66e0d71ff89b/image.png" alt=""></p>
<p>Objective-C 헤더가 Swift에 임포트 되는 방법을 알고 싶다면 Xcode의 related items 팝업을 보시면 됩니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/c514a0e0-f061-4a31-ab8a-8d8a5e83c90c/image.png" alt=""></p>
<p>이건 좌측 상단에서 찾을 수 있는데, generated interfaces를 선택하시면 다른 Swift 버전에서의 인터페이스를 볼 수 있습니다.</p>
<p>반대로 Objective-C가 Swift를 임포트하는 상황에서는 Swift가 임포트한 Objective-C의 헤더를 만듭니다. 이 방법으로 Swift에서 클래스를 작성한 것을 Objective-C가 호출해서 사용할 수 있는 것이죠.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/8c7b6c6a-c581-4662-9a8e-0630c0682306/image.png" alt=""></p>
<ul>
<li>컴파일러는 <code>NSObject</code>를 확장한 Swift 클래스와 <code>@objc</code> 어노테이션이 있는 요소들로부터 Objective-C 선언을 만듭니다.</li>
<li>유닛 테스트에 있는 앱의 경우 헤더는 공용 및 internal 선언 모두 포함하고 있게 됩니다. 이것이 앱의 Objective-C 부분으로부터 Swift를 사용할 수 있게 해줍니다.</li>
<li>프레임워크는 빌드 프로덕트에 포함되어 있고 프레임워크의 공용 인터페이스 부분이기 때문에 생성된 헤더가 공용 선언문만을 제공합니다.</li>
</ul>
<p><img src="https://images.velog.io/images/ryan-son/post/994ca01d-4b5a-4a55-a788-ba7071e8102a/image.png" alt=""></p>
<p>오른쪽에서는 컴파일러가 모듈 이름인 <code>PetWall</code>을 포함하는 Swift 클래스에 약간 변형된 이름에 Objective-C 클래스를 바인딩하는 것을 볼 수 있습니다. 
두 모듈이 동일한 이름의 클래스를 정의했을 때 런타임 충돌을 방지하기 위해 <code>@objc</code> 어노테이션을 통해 Objective-C 클래스에 다른 이름을 사용하도록 Swift에게 요청할 수 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/3af4a1e2-d22d-4d16-8e05-8d9fc649c33f/image.png" alt=""></p>
<p>이름이 충돌되지 않도록 하는 것은 개발자의 책임입니다. <code>@objc(name)</code>처럼 새로운 이름을 전달하여 충돌을 방지할 수 있습니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/d7e4969b-cdec-49a9-833b-41edb19c0bad/image.png" alt=""></p>
<p>컴파일러는 다른 Swift 타겟들의 인터페이스를 만들 때도 유사한 접근 방식을 취합니다.</p>
<p>Swift에서 모듈은 배포 가능한 선언 단위를 의미합니다. 그리고 이러한 선언문을 이용하려면 모듈을 임포트해야 합니다. 예를 들어, Objective-C 모듈이나 XCTest 모듈을 가져올 수 있겠죠. </p>
<p>Xcode에서 각 Swift 타겟은 앱 타겟을 포함하여 별도의 모듈을 생성합니다. 이것이 유닛 테스트에서 테스트하기 위해 앱의 메인 모듈을 임포트해야하는 이유입니다. 모듈을 가져올 때 컴파일러는 사용할 때 타입을 확인하기 위해 특수한 Swift 모듈 파일을 역직렬화(deserialize)합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/f9251d8d-3e08-48b6-b8c6-5a821d2d4b4c/image.png" alt=""></p>
<p>예를 들어 아래의 유닛 테스트에서 컴파일러는 컨트롤러를 제대로 생성하는지 확인하기 위해 <code>PetWall</code> Swift 모듈의 일부인 <code>PetViewController</code>를 로드할 것입니다. 이것은 앞서 보여 드렸던 컴파일러가 타겟 내 선언문을 찾는 방식과 유사합니다. 컴파일러는 Swift 파일을 직접 파싱하지 않고 모듈을 요약하는 파일을 로드합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/800563e1-8491-465a-bc47-2f41faea120b/image.png" alt=""></p>
<p>컴파일러는 생성된 Objective-C 헤더처럼 많은 Swift 모듈 파일을 생성합니다. 하지만 텍스트가 아니라 바이너리 표현 방식을 사용합니다. 여기에는 Objective-C의 정적 인라인 함수 또는 C++의 헤더 구현과 같은 인라인이 가능한(inlineable) 함수의 구현부 (bodies)가 포함됩니다. 하지만 이는 비공개 선언문들의 이름과 타입을 포함하고 있습니다. 이렇게 하면 디버거에서 참조할 수 있어 정말 편리하지만 비공개 변수를 아무렇게나 지어서는 안된다는 것을 시사합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/d215eb8e-f163-4c29-99eb-c41b22a1514b/image.png" alt=""></p>
<p>증분 빌드의 경우 컴파일러는 부분적인 Swift 모듈 파일을 생성한 다음 전체 모듈의 내용을 나타내는 단일 파일로 병합합니다. 이 병합 프로세스를 통해 단일 Objective-C 헤더를 생성할 수도 있습니다. 이는 링커가 개체 파일을 단일 실행 파일로 합칠 때 수행하는 작업과 유사합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/9a240e82-19bf-49ad-910a-1f6dd4afc86b/image.png" alt=""></p>
<h1 id="linking">Linking</h1>
<p>Xcode 빌드 프로세스의 마지막 단계인 링커를 살펴봅시다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/95280922-179d-494b-8131-fc298def9f94/image.png" alt=""></p>
<h2 id="링커">링커</h2>
<ul>
<li>실행가능한 Mach-O를 빌드하는 마지막 작업</li>
<li>모든 컴파일러의 결과물(<code>.o</code> 파일)을 합쳐 하나의 파일로 만듦<ul>
<li>컴파일러가 생성한 코드를 이동하거나 수정함</li>
</ul>
</li>
<li>두 종류의 인풋 파일을 받음<ul>
<li>오브젝트 파일 (<code>.o</code>)</li>
<li>라이브러리 (<code>.dylib</code>, <code>.tbd</code>, <code>.a</code>)</li>
</ul>
</li>
</ul>
<h2 id="심볼">심볼</h2>
<ul>
<li>코드나 데이터의 조각을 참조하기 위한 이름</li>
<li>코드 조각들은 다른 심볼들을 참조할 수 있음 (다른 함수를 부르는 함수를 작성 등)</li>
<li>심볼은 링커의 작동 방식에 영향을 주는 속성을 가질 수 있음 (weak, availability symbol 등)</li>
<li>언어들은 데이터를 &quot;mangling&quot;하여 심볼로 인코딩함 (C++, Swift)</li>
</ul>
<h2 id="오브젝트-파일">오브젝트 파일</h2>
<ul>
<li>개별 컴파일러 액션의 결과물</li>
<li>코드와 데이터 조각을 가진 실행할 수 없는 Mach-O 파일 (missing된 일부분을 링커가 이어 붙여 고쳐야 함)<ul>
<li>각 조각은 심볼로 표현됨</li>
<li>조각들은 &quot;정의되지 않은&quot; 심볼들을 참조할 수 있음 (한 <code>.o</code> 파일의 함수가 다른 <code>.o</code> 파일의 함수를 참조할 때)</li>
</ul>
</li>
</ul>
<h2 id="라이브러리">라이브러리</h2>
<ul>
<li>타겟의 일부로서 빌드되지 않은 심볼들을 정의<ul>
<li>Dylibs: 동적 라이브러리<ul>
<li>실행 파일이 사용할 코드와 데이터 조각을 노출하는 Mach-O 파일</li>
</ul>
</li>
<li>TBDs: 텍스트 기반 동적 라이브러리 (Text Based Dylib Stubs)<ul>
<li>심볼만 가지고 있음</li>
</ul>
</li>
<li>정적 아카이브<ul>
<li>&quot;AR&quot; 툴으로 빌드된 .o 파일의 모음</li>
<li>참조하는 심볼이 있는 .o 파일만 앱에 포함됨</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="예시">예시</h2>
<p>아래 그림에서 AAC 사운드 파일이 될 &quot;purr.aac&quot;를 찾아볼 수 있지만 <code>purrFile</code>은 나타나지 않음. 이는 static이기 때문입니다 (nonexported name).</p>
<p><img src="https://images.velog.io/images/ryan-son/post/bf1e3c18-3cf9-4acf-bd7b-cba2e0713b77/image.png" alt=""></p>
<p>Cat purr는 아래와 같이 심볼이 만들어집니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/af367c78-7a4e-4e3b-820a-ea88309bb2c2/image.png" alt=""></p>
<p>이제 변수를 <code>playSound(_:)</code>에 전달합니다. 어셈블리어의 명령어가 두 개인데, 이는 &quot;purr.aac&quot;는 문자열 이름이라 실제 주소를 가지고 있지 않기 때문에 이후에 찾기 위함입니다. 링커가 들어와서 이후에 고칠 심볼릭 오프셋, 심볼릭 값 페이지와 페이지 오프를 남겨둡니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/0b62b06b-a6cf-4db1-85e7-5ba608567311/image.png" alt=""></p>
<p>마지막으로 이 문자열을 x0에 로드했으므로 <code>playSound(_:)</code>를 호출할 수 있습니다. <code>playSound(_:)</code>를 호출하지 않고 <code>__Z9playSoundPKc</code>를 호출하죠. 이는 &quot;mangle&quot;된 심볼을 나타내는건데, Objective-C++이면서 C++에도 동일한 playSound 함수가 있기 때문입니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/83ca3b09-3837-4b0b-8ad7-d2fcd38336ae/image.png" alt=""></p>
<p>이제 <code>.o</code> 파일이 있습니다. 이를 통해 빌드 시스템은 모든 <code>.o</code> 파일들을 링커에 인풋으로 전달할 것입니다. 그리고 링커는 파일을 생성하기 시작합니다. 이 경우 <code>PetWall</code> 내부에 포함된 프레임워크인 <code>PetKit</code>을 빌드합니다. Text segment라 부르는 것을 만들 것입니다. Text segment는 애플리에키션에 대한 모든 코드를 보관하는 곳인데, 이번에는 <code>cat.o</code>를 가져와서 복사합니다. 두 부분으로 나눌 건데, 하나는 해당 문자열용이고, 또 다른 하나는 실행 코드용입니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/2121a061-e477-4b98-b88f-23d0aec5aa99/image.png" alt=""></p>
<p>이제 이들의 절대 주소를 알고 있으므로 링커가 특정 오프셋에서 로드하도록 <code>cat.o</code>를 다시 작성할 수 있습니다. 명령어를 삭제하거나 만들 수 는 없기 때문에 두 번째 명령어를 아무것도 하지 않는 null로 대체합니다. 그런 다음 분기합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/7a45e693-eb38-4d86-bdcc-27e907dca0b1/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/ryan-son/post/ea544a25-efdf-4ed3-b7e5-16453aa6026c/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/ryan-son/post/b03c5566-27e9-442a-b775-f7e8cb39a8e3/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/ryan-son/post/7d66c889-ec42-44ed-91c2-ad7723910d7a/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/ryan-son/post/ebe3bb23-ca4c-48bf-bf53-d7b53e32d534/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[multipart/form-data 알아보기 (vs application/json)]]></title>
            <link>https://velog.io/@ryan-son/multipartform-data-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-vs-applicationjson</link>
            <guid>https://velog.io/@ryan-son/multipartform-data-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-vs-applicationjson</guid>
            <pubDate>Sun, 26 Sep 2021 12:03:55 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/c0cc4062-217c-49d4-8744-6d83b161f4b1/image.png" alt=""></p>
<h1 id="overview">Overview</h1>
<blockquote>
<p>*&quot;multipart/form-data&quot; content type은 파일, ASCII 형식이 아닌 데이터와 바이너리 데이터 제출(submit)에 사용된다.* - HTML 4.01 Specification (W3C)</p>
</blockquote>
<p><a href="https://www.ietf.org/rfc/rfc2045.txt">RFC2045</a>에 소개된 모든 multipart Multipurpose Internet Mail Extensions (MIME) 데이터 스트림의 규칙을 준수합니다.</p>
<p>하나의 multipart/form-data 메시지는 <a href="https://www.w3.org/TR/html401/interact/forms.html#successful-controls">successful control</a>이라 표현하는 여러 파트로 이루어져 있으며 각 파트는 기본값을 <code>text/plain</code>으로하는 <code>Content-Type</code>을 가질 수 있습니다.</p>
<p>각 파트는 아래 내용을 가져야 합니다.</p>
<ol>
<li><code>&quot;form-data&quot;</code> 값을 가지는 <code>&quot;Content-Dispoisition&quot;</code> 헤더</li>
<li>상응하는 control을 지칭하기 위한 control name</li>
</ol>
<pre><code class="language-swift">Content-Disposition: form-data; name=&quot;mycontrol&quot;</code></pre>
<h1 id="예시">예시</h1>
<p>HTML 4.01 Specification에서 설명한 내용은 어떻게 이루어져 있는지 아래 예시를 통해 확인하실 수 있습니다.</p>
<p>배경지식으로 각 파트에서 등장하는 <code>\r\n</code>는 Carriage Return (CR), Line Feed (LF)를 의미하는<code>CRLF</code>이며 아래를 의미합니다.</p>
<ol>
<li>Carriage Return (CR, <code>\r</code>): 현재 라인에서 가장 앞자리로 위치 이동</li>
<li>Line Feed(LF, <code>\n</code>): 커서의 위치를 바꾸지 않은 상태로 종이를 한 라인 위로 올림</li>
</ol>
<h2 id="initial-and-encapsulated-part">Initial and encapsulated part</h2>
<p>multipart/form-data의 첫 파트와 중간 파트는 고유 메시지임을 나타내는 <code>boundary</code>와 control name을 가진 <code>content-disposition</code>, 그리고 값을 가지고 있습니다.</p>
<pre><code class="language-swift">--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA\r\n
Content-Disposition: form-data; name=\&quot;price\&quot;\r\n\r\n

12000\r\n</code></pre>
<h2 id="final-part">Final part</h2>
<p>마지막 파트는 메시지의 종료를 나타내기 위해 <code>--</code> 문자로 끝을 냅니다.</p>
<pre><code class="language-swift">--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA--\r\n</code></pre>
<h2 id="entire-message">Entire message</h2>
<p>전체 메시지는 아래와 같이 구성되어 있습니다.</p>
<pre><code class="language-swift">--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA\r\n
Content-Disposition: form-data; name=\&quot;price\&quot;\r\n\r\n

12000\r\n
--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA\r\n
Content-Disposition: form-data; name=\&quot;currency\&quot;\r\n\r\n

KRW\r\n
--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA\r\n
Content-Disposition: form-data; name=\&quot;descriptions\&quot;\r\n\r\n

상품 상세 정보입니당~\r\n
--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA\r\n
Content-Disposition: form-data; name=\&quot;discounted_price\&quot;\r\n\r\n

10000\r\n
--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA\r\n
Content-Disposition: form-data; name=\&quot;password\&quot;\r\n\r\n

1234\r\n--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA\r\n
Content-Disposition: form-data; name=\&quot;images[]\&quot;; filename=\&quot;BD9B2CA6-F70D-46C5-AD02-540528441574.jpeg\&quot;\r\n
Content-Type: image/jpeg\r\n\r\n

(data)\r\n
--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA\r\n
Content-Disposition: form-data; name=\&quot;images[]\&quot;; filename=\&quot;1CA0A2CF-BBEC-49AC-946A-D9A0D4014DE2.webp\&quot;\r\n
Content-Type: image/webp\r\n\r\n

(data)\r\n
--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA\r\nContent-Disposition: form-data; name=\&quot;title\&quot;\r\n\r\n

상품 이름이에요~!\r\n
--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA\r\n
Content-Disposition: form-data; name=\&quot;stock\&quot;\r\n\r\n

100\r\n
--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA--\r\n
</code></pre>
<h1 id="vs-applicationjson">vs <code>application/json</code></h1>
<p><code>multipart/form-data</code>를 이용하는 주된 이유는 가장 먼저 말씀드린 바와 같이 <em>파일, ASCII 형식이 아닌 데이터와 바이너리 데이터</em>를 업로드할 수 있는 형태이기 때문인 것으로 판단됩니다. 그렇다면 주로 활용되는 <code>application/json</code> 형식으로는 파일을 업로드할 수 없을까요?</p>
<p>파일의 컨텐츠를 <code>base64</code> 인코딩한 후 아래와 같이 구성하여 메시지를 보내면 업로드할 수 있습니다.</p>
<pre><code class="language-json">EXAMPLE 9: Files
&lt;form enctype=&#39;application/json&#39;&gt;
  &lt;input type=&#39;file&#39; name=&#39;file&#39; multiple&gt;
&lt;/form&gt;

// assuming the user has selected two text files, produces:
{
    &quot;file&quot;: [
        {
            &quot;type&quot;: &quot;text/plain&quot;,
            &quot;name&quot;: &quot;dahut.txt&quot;,
            &quot;body&quot;: &quot;REFBQUFBQUFIVVVVVVVVVVVVVCEhIQo=&quot;
        },
        {
            &quot;type&quot;: &quot;text/plain&quot;,
            &quot;name&quot;: &quot;litany.txt&quot;,
            &quot;body&quot;: &quot;SSBtdXN0IG5vdCBmZWFyLlxuRmVhciBpcyB0aGUgbWluZC1raWxsZXIuCg==&quot;
        }
    ]
}</code></pre>
<h2 id="applicationjson"><code>application/json</code></h2>
<ul>
<li>장점:<ul>
<li>파일과 함께 전달된 데이터의 연관 관계를 표현하기 용이하다.</li>
</ul>
</li>
<li>단점: <ul>
<li>인코딩 방식의 특성으로 인해 바이너리 데이터보다 33-37 % 많은 용량을 차지한다.
(base64 인코딩 방식이 인코딩 시 바이트 당 8 비트를 차지하는 바이너리 방식과 달리 6 비트를 차지하여 이론상 8/6 = 1.33, 약 33 % 많은 용량을 차지하지만 실제로는 패딩 등의 이유로 3~4 % 더 차지함)</li>
<li>서버는 디코딩, 클라이언트는 인코딩을 위한 프로세싱 오버헤드가 부가된다.</li>
<li>인코딩한 파일 컨텐츠의 문자열이 길어져 서버측에서 파일을 읽지 못하는 경우가 발생할 수 있다.</li>
</ul>
</li>
</ul>
<h2 id="multipartform-data"><code>multipart/form-data</code></h2>
<ul>
<li>장점:<ul>
<li>파일 업로드 시 <code>application/json</code> 방식에서 발생하는 단점을 감수하지 않을 수 있다.</li>
</ul>
</li>
<li>단점:<ul>
<li>파일과 함께 전달된 데이터의 연관 관계 표현이 제한된다.</li>
</ul>
</li>
</ul>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2">HTML 4.01 Specification</a> - W3C</li>
<li><a href="https://stackoverflow.com/questions/4526273/what-does-enctype-multipart-form-data-mean">What does enctype=&#39;multipart/form-data&#39; mean?</a> - Stack overflow</li>
<li><a href="https://www.w3.org/TR/html-json-forms/">W3C HTML JSON form submission</a> - W3C Working Group Note</li>
<li><a href="https://lena-chamna.netlify.app/post/http_multipart_form-data/">HTTP multipart/form-data 이해하기</a> - 레나 참나 블로그</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Storyboard의 컴파일 과정 알아보기]]></title>
            <link>https://velog.io/@ryan-son/Storyboard-%EC%BB%B4%ED%8C%8C%EC%9D%BC-%EA%B3%BC%EC%A0%95-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@ryan-son/Storyboard-%EC%BB%B4%ED%8C%8C%EC%9D%BC-%EA%B3%BC%EC%A0%95-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sun, 26 Sep 2021 11:57:19 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/ba06c8e3-0c25-41ec-b769-f365e12bd4b4/image.png" alt=""></p>
<blockquote>
<ul>
<li>ibtool이 storyboard 파일을 storyboardc 파일로 컴파일 (c: compiled).</li>
<li>storyboardc 파일은 view와 view의 관계 (IBAction, Segue 등)가 정의된 스토리보드를 바이너리 파일인 nib로 컴파일하며 스토리보드 내 view들의 고유 NIB 식별자 등의 정보를 Information Property List에 기록</li>
</ul>
</blockquote>
<h2 id="스토리보드-소스-파일">스토리보드 소스 파일</h2>
<p>Xcode를 통해 확인할 수 있는 스토리보드 파일의 구성은 아래와 같습니다.</p>
<h3 id="interface-builder---storyboard">Interface Builder - Storyboard</h3>
<p>스토리보드 파일을 클릭하면 볼 수 있는 흔한 화면 구성입니다. 화면과 화면 간의 관계를 시각적으로 표현하고 있어 화면 구성을 이해하기 용이합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/c21395b9-595e-42f1-a9d9-c3efb8e7bde6/image.png" alt=""></p>
<h3 id="source-code">Source Code</h3>
<p>스토리보드 파일을 우클릭하여 <code>Open As</code>, <code>Source Code</code>을 순차적으로 클릭하며 진입하면 다음과 같이 <code>XML</code> 형식으로 표현된 스토리보드를 확인하실 수 있습니다. 스토리보드의 내용을 수정하여 커밋하면 이 소스코드가 변경된다는 것을 gif diff 명령을 통해 확인하신 적이 있으실 것입니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/4d1e07f3-f155-4662-87d5-fbd8da5d50e7/image.png" alt=""></p>
<h3 id="번외---xib">번외 - XIB</h3>
<p><code>XML Interface builder</code> 또는 <code>Xcode Interface builder</code>라고 불리는 XIB 파일도 마찬가지로 <code>XML</code> 형식으로 작성되어 있습니다. 스토리보드의 document type이 <code>&quot;com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB&quot;</code>, XIB 파일의 document type이 <code>&quot;com.apple.InterfaceBuilder3.CocoaTouch.XIB&quot;</code>으로 둘 모두 유사한 파일 형식임을 유추할 수 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/7b83389b-896d-40fe-b29a-a1fa55e2ed2d/image.png" alt=""></p>
<h2 id="build-phase">Build phase</h2>
<p>빌드 완료 시 <code>XIB</code> 파일은 바이너리 형식으로 변환되어 Next step Interface Builder를 의미하는 <code>NIB</code> 파일로 컴파일됩니다. <code>NIB</code> 형식으로는 사람이 읽고 이해할 수 있는 형태를 벗어나서 git과 같은 방식으로 소스 컨트롤을 하기 제한되어 <code>XML</code> 형식의 <code>XIB</code>가 도입되었다고 하네요.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/076e775b-86dd-4bd8-b3f8-cfe52a3cf2ba/image.png" alt=""></p>
<p>출처: <a href="https://dalgonakit.tistory.com/82">달망이 개발일기</a></p>
<p>빌드 후 앱 번들 패키지를 열어보시면 아래와 같이 구성되어 있는 것을 확인하실 수 있습니다. 개별 XIB로 작업한 내용은 스토리보드와 관계없이 NIB 파일로 컴파일되어 앱 번들 최상위 폴더에서 확인할 수 있네요.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/171c3096-da0e-459c-8c68-68e5a01c8faa/image.png" alt=""></p>
<p>다음은 메인 소스가 위치할 것으로 예상되는 <code>Base.lproj</code>를 열었을 때 확인할 수 있는 구성입니다. 스토리보드 파일이었던 <code>LaunchScreen</code>과 <code>Main</code>이 있네요.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/fafedbb8-9305-4186-b4ed-29804a57913e/image.png" alt=""></p>
<p>미리 말씀드렸듯이 컴파일된 <code>storyboardc</code> 패키지는 view와 view의 관계 (IBAction, Segue 등)로 이루어진 NIB 파일들을 가지고 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/db398ff4-4c53-4f9c-8d22-03ca09c22d08/image.png" alt=""></p>
<p>의미 없어보이는 <code>BYZ-38-t0r-view-8bC-Xf-vdC</code>와 같은 문자열도 의미가 있습니다. 다음 코드 일부를 통해 특정 ViewController의 최상위 view의 id를 지칭하는 것임을 파악할 수 있습니다.</p>
<p>마찬가지로 스토리보드의 요소인 <code>UINavigationController</code>, <code>UIViewController</code>들도 모두 무작위처럼 보이는 <code>id</code>를 키값을 가지고있으며 이러한 내용이 <code>storyboardc</code> 파일에 동봉된 <code>info.plist</code> 프로퍼티 리스트를 통해 확인할 수 있습니다.</p>
<pre><code class="language-swift">// Main.storyboard
...
&lt;viewController id=&quot;BYZ-38-t0r&quot; customClass=&quot;OpenMarketItemListViewController&quot; customModule=&quot;OpenMarket&quot; customModuleProvider=&quot;target&quot; sceneMemberID=&quot;viewController&quot;&gt;
    &lt;view key=&quot;view&quot; contentMode=&quot;scaleToFill&quot; id=&quot;8bC-Xf-vdC&quot;&gt;
...</code></pre>
<p><img src="https://images.velog.io/images/ryan-son/post/b95aeace-9be6-4552-86c4-b03644bac92e/image.png" alt=""></p>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://stackoverflow.com/questions/42043047/what-does-ibtool-do-in-the-compiling-and-linking-storyboards-phase-and-how-the-r">What does ibtool do in the compiling and linking storyboards phase and how the result of linked storyboards work with executable?</a> - Stack overflow</li>
<li><a href="https://dalgonakit.tistory.com/82">iOS의 화면을 그리기 위해선 이것이 필요합니다! XIB, NIB, Storyboard</a> - 달망이 개발일기</li>
<li><a href="https://soooprmx.com/nib-%ED%8C%8C%EC%9D%BC%EB%A1%9C%EB%B6%80%ED%84%B0-ui-%EA%B4%80%EB%A0%A8-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EB%A1%9C%EB%94%A9%ED%95%98%EA%B8%B0/#fn-7226-2-2">Nib 파일로부터 UI 관련 객체를 로딩하기</a> - Wireframe blog</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[nib과 xib의 차이]]></title>
            <link>https://velog.io/@ryan-son/nib%EA%B3%BC-xib%EC%9D%98-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@ryan-son/nib%EA%B3%BC-xib%EC%9D%98-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Sun, 26 Sep 2021 11:51:43 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/215799ef-5a85-42ed-b21f-34e96830c059/image.png" alt=""></p>
<blockquote>
<p>XIB를 바이너리 형식으로 컴파일하여 파일로 만든 것을 NIB라고 합니다.</p>
</blockquote>
<p>빌드 완료 시 <code>XIB</code> 파일은 바이너리 형식으로 변환되어 Next step Interface Builder를 의미하는 <code>NIB</code> 파일로 컴파일됩니다. 사람이 읽고 이해할 수 있는 형태가 아니라 <code>NIB</code> 형식으로는 git과 같은 방식으로 소스 컨트롤을 하기 제한된다는 단점이 있어 <code>XML</code> 형식의 <code>XIB</code>가 도입되었다고 하네요.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/076e775b-86dd-4bd8-b3f8-cfe52a3cf2ba/image.png" alt=""></p>
<p>이미지 출처: <a href="https://dalgonakit.tistory.com/82">https://dalgonakit.tistory.com/82</a></p>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://dalgonakit.tistory.com/82">iOS의 화면을 그리기 위해선 이것이 필요합니다! XIB, NIB, Storyboard</a> - 달망이 개발일기</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Safe area 정리]]></title>
            <link>https://velog.io/@ryan-son/Safe-area-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@ryan-son/Safe-area-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sun, 26 Sep 2021 11:41:21 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/85083f5e-d478-4db9-a9fe-dadccba6f01c/image.png" alt="">
이미지 출처: <a href="https://wit.nts-corp.com/2019/10/24/5731">https://wit.nts-corp.com/2019/10/24/5731</a></p>
<blockquote>
<p>View controller의 view를 가리는 내비게이션 바, 탭 바, 툴바, 기타 요소를 제외한 영역</p>
</blockquote>
<h1 id="소개">소개</h1>
<p><code>Safe area</code>는 모든 인터페이스의 가시적인 부분에 view를 위치시키는 것을 도와줍니다. <code>UIKit</code>이 정의한 <code>View controller</code>는 컨텐츠 위에 내비게이션 컨트롤러의 내비게이션 바와 같이 특별한 view들을 컨텐츠 위에 위치시킬 수 있습니다. 이러한 view들이 일부 투명할 수 있지만, 그래도 view 아래에 일부 컨텐츠들을 가립니다. tvOS에서는 <code>safe area</code>가 화면 베젤을 의미하는 <code>overscan inset</code>을 포함합니다.</p>
<p>컨텐츠의 레이아웃을 결정할 때 각 view에서 접근할 수 있는 <code>safeAreaLayoutGuide</code>를 통해 <code>safe area</code>의 도움을 받을 수 있습니다. AutoLayout을 통해 제약을 설정하는 상황이 아니라면 <code>safeAreaInsets</code> 프로퍼티를 통해 raw inset values를 얻을 수 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/9dc18a39-6470-4f5d-af2e-5ead191872e5/image.png" alt=""></p>
<h1 id="custom-view를-포함하기-위해-safe-area를-확장하기">Custom View를 포함하기 위해 safe area를 확장하기</h1>
<p><code>Container view controller</code>는 <code>embedded child view controller</code>의 view를 통해 컨텐츠를 보여줄 수 있습니다. 이 상황에서 <code>container view</code>의 컨텐츠 view들이 이미 커버하고 있는 영역을 <code>child view controller</code>의 <code>safe area</code>에서 제외해주기 위해 업데이트하여야 하는데, UIKit <code>container view controller</code>는 컨텐츠 view들을 고려한 <code>child view controller</code>의 <code>safe area</code>를 미리 조정합니다. 예를 들어, 내비게이션 컨트롤러는 내비게이션 바를 고려하여 <code>child view controller</code>의 <code>safe area</code>을 확장합니다.</p>
<p><code>Embedded child view controller</code>의 <code>safe area</code>를 확장하려면 <code>additionalSafeAreaInsets</code> 프로퍼티를 수정하면 됩니다. 아래 그림과 같이 화면 아래와 오른쪽 끝에 커스텀 view들을 보여주겠다고 정의하였다면 <code>child view controller</code>의 컨텐츠가 커스텀 view들 아래에 위치하기 때문에 해당 view들을 고려하여 <code>child view controller</code>의 <code>safe area</code> 아래와 오른쪽의 inset을 설정하여야 합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/733617d1-1d98-4a87-afed-5e974cea5b82/image.png" alt=""></p>
<p>아래 코드를 통해 <code>container view controller</code>의 <code>viewDidAppear(_:)</code> 메서드에서 커스텀 view들을 고려하여 <code>child view controller</code>의 <code>safe area</code>를 늘리는 방법을 확인하실 수 있습니다. view 계층에 view가 추가되기 까지 <code>safe area insets</code>가 정확하지 않으니 해당 메서드에서 수정 작업이 이루어져야 합니다.</p>
<pre><code class="language-swift">override func viewDidAppear(_ animated: Bool) {
   var newSafeArea = UIEdgeInsets()
   // Adjust the safe area to accommodate 
   //  the width of the side view.
   if let sideViewWidth = sideView?.bounds.size.width {
      newSafeArea.right += sideViewWidth
   }
   // Adjust the safe area to accommodate 
   //  the height of the bottom view.
   if let bottomViewHeight = bottomView?.bounds.size.height {
      newSafeArea.bottom += bottomViewHeight
   }
   // Adjust the safe area insets of the 
   //  embedded child view controller.
   let child = self.childViewControllers[0]
   child.additionalSafeAreaInsets = newSafeArea
}</code></pre>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://developer.apple.com/documentation/uikit/uiview/positioning_content_relative_to_the_safe_area">Positioning Content Relative to the Safe Area</a> - Apple Developer</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Design patterns in Swift]]></title>
            <link>https://velog.io/@ryan-son/Design-patterns-in-Swift</link>
            <guid>https://velog.io/@ryan-son/Design-patterns-in-Swift</guid>
            <pubDate>Mon, 20 Sep 2021 07:50:56 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/8296ec55-037b-41df-9cf6-d40e1a55721a/image.png" alt=""></p>
<p>Swift로 작성한 예시를 통해 디자인 패턴 적용 방법을 알아봅니다.</p>
<h1 id="contents">Contents</h1>
<table>
<thead>
<tr>
<th>Behavioral patterns</th>
<th>Creational patterns</th>
<th>Structural patterns</th>
</tr>
</thead>
<tbody><tr>
<td><a href="https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Strategy-pattern-in-Swift">Strategy</a></td>
<td><a href="https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Factory-pattern-in-Swift">Factory</a></td>
<td><a href="https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Adapter-pattern-in-Swift">Adaptor</a></td>
</tr>
<tr>
<td><a href="https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Template-method-pattern-in-Swift">Template Method</a></td>
<td></td>
<td></td>
</tr>
</tbody></table>
<h1 id="참고자료">참고자료</h1>
<ol>
<li><a href="https://github.com/ochococo/Design-Patterns-In-Swift">Design Patterns in Swift</a> - ochococo GitHub Repository</li>
<li><a href="https://www.youtube.com/watch?v=lJES5TQTTWE">객체지향 디자인패턴 1</a> - 얄팍한 코딩사전</li>
<li><a href="https://www.youtube.com/watch?v=q3_WXP9pPUQ">객체지향 디자인패턴 2</a> - 얄팍한 코딩사전</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[디자인 패턴] Factory pattern in Swift]]></title>
            <link>https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Factory-pattern-in-Swift</link>
            <guid>https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Factory-pattern-in-Swift</guid>
            <pubDate>Mon, 20 Sep 2021 07:39:04 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/0f474073-80a3-40a0-aaf5-afeb6bf084b7/image.png" alt=""></p>
<p><a href="https://velog.io/@ryan-son/Design-patterns-in-Swift">목차로 돌아가기</a></p>
<h1 id="소개">소개</h1>
<p>팩토리 또는 팩토리 메서드 패턴 (Factory pattern or factory method pattern)은 생성 관점의 디자인 패턴 (Creational design patterns)의 일종으로, 인스턴스 생성을 팩토리 타입의 메서드에 위임하는 방식으로 개발자들에게 이용하는 타입에 대한 정보를 숨기고 인스턴스 생성을 용이하게 해줍니다.</p>
<h1 id="example">Example</h1>
<h2 id="가정">가정</h2>
<p>현재 개발 중인 앱에는 자주 사용되는 UI 요소들을 커스터마이징한 커스텀 UI 타입이 있습니다.</p>
<h2 id="패턴을-적용하지-않은-경우">패턴을 적용하지 않은 경우</h2>
<pre><code class="language-swift">import UIKit

final class MyAppLabel: UILabel {

    init(textColor: CGColor = UIColor.label.cgColor, backgroundColor: UIColor = .systemBackground) {
        self.textColor = textColor
        self.backgroundColor = backgroundColor
    }
}


final class MyAppButton: UIButton {

    init(textColor: CGColor, backgroundColor: UIColor = .systemBackground) {
        self.textColor = textColor
        self.backgroundColor = backgroundColor
    }
}

final class MyAppTextField: UITextField {

    init(textColor: CGColor = UIColor.label.cgColor, backgroundColor: UIColor) {
        self.textColor = textColor
        self.backgroundColor = backgroundColor
    }
}</code></pre>
<p>각 타입에는 이니셜라이저를 통해 기본으로 설정된 값도 있고, 그렇지 않은 경우도 있습니다. 개발자들은 UI를 개발하는 동안 모든 커스텀 타입에 대해 이러한 세부 사항을 기억하고 있어야 합니다. 현재는 <code>textColor</code>와 <code>backgroundColor</code>에 대한 사항만을 가정하고 있지만, 실제로는 크기 또는 여백 등 더 많은 커스터마이징 요소들이 포함되어 있을 수 있습니다. 팩토리 패턴이 적용되지 않은 현재는 아래와 같이 직접 UI 요소들을 초기화하여 사용하고 있습니다.</p>
<pre><code class="language-swift">let label = MyAppLabel()
let button = MyAppButton(textColor: UIColor.systemPink.cgColor)
let textField = MyAppTextField(backgroundColor: .systemTeal)</code></pre>
<p>하지만 개발자들이 모든 커스텀 타입에 대해 이러한 세부 사항을 기억하는 것은 쉽지 않은 일일 것입니다.</p>
<h2 id="패턴을-적용한-경우">패턴을 적용한 경우</h2>
<p>그래서 각 UI 요소들을 열거형을 통해 케이스를 정의하고, 프로토콜을 통해 외부에 제공해야할 인터페이스를 정의하도록 만들어 케이스별로 특정한 UI 요소를 지칭할 수 있는 환경을 만들어줍니다.</p>
<pre><code class="language-swift">import UIKit

enum UIComponent {
    case label, button, textField
}

protocol MyAppUIComponent {

    var component: UIComponent { get }
    var textColor: CGColor { get set }
    var backgroundColor: UIColor { get set }
}


final class MyAppLabel: UILabel, MyAppUIComponent {

    var component: UIComponent { .label }

    init(textColor: CGColor = UIColor.label.cgColor, backgroundColor: UIColor = .systemBackground) {
        self.textColor = textColor
        self.backgroundColor = backgroundColor
    }
}


final class MyAppButton: UIButton, MyAppUIComponent {

    var component: UIComponent { .button }

    init(textColor: CGColor, backgroundColor: UIColor = .systemBackground) {
        self.textColor = textColor
        self.backgroundColor = backgroundColor
    }
}

final class MyAppTextField: UITextField, MyAppUIComponent {

    var component: UIComponent { .textField }

    init(textColor: CGColor = UIColor.label.cgColor, backgroundColor: UIColor) {
        self.textColor = textColor
        self.backgroundColor = backgroundColor
    }
}</code></pre>
<p>계속해서 정의한 커스텀 UI 요소의 생성을 전문으로하는 팩토리 타입을 만듭니다.</p>
<pre><code class="language-swift">struct ComponentFactory {

    func make(_ component: UIComponent) -&gt; MyAppUIComponent {
        switch component {
        case .label:
            return MyAppLabel()
        case .button:
            return MyAppButton(textColor: UIColor.systemPink.cgColor)
        case .textField:
            return MyAppTextField(backgroundColor: .systemTeal)
        }
    }
}</code></pre>
<p>이제 팩토리 타입을 이용해 원하는 UI 요소를 케이스별로 생성하여 사용할 수 있게 되었습니다. 개발자들이 UI 요소를 생성할 때 많은 것을 알 필요가 없게 되었네요. </p>
<pre><code class="language-swift">let factory = ComponentFactory()

let label = factory.make(.label)
let button = factory.make(.button)
let textField = factory.make(.textField)</code></pre>
<p>새로운 커스텀 UI 타입을 정의한다고 하더라도 인스턴스 생성에 대한 세부 사항은 팩토리 타입에서 정의해주면 됩니다. 열거형의 케이스로 정의되어 있기 때문에 새로운 케이스에 대한 생성 방식을 팩토리 타입의 <code>make(_:)</code> 메서드에 정의하지 않으면 컴파일 에러가 발생하여 에러 발견이 더 용이해진다는 장점도 생겼습니다.</p>
<p><a href="https://velog.io/@ryan-son/Design-patterns-in-Swift">목차로 돌아가기</a></p>
<h1 id="참고자료">참고자료</h1>
<ol>
<li><a href="https://github.com/ochococo/Design-Patterns-In-Swift">Design Patterns in Swift</a> - ochococo GitHub Repository</li>
<li><a href="https://www.youtube.com/watch?v=q3_WXP9pPUQ">객체지향 디자인패턴 2</a> - 얄팍한 코딩사전</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[디자인 패턴] Template method pattern in Swift]]></title>
            <link>https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Template-method-pattern-in-Swift</link>
            <guid>https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Template-method-pattern-in-Swift</guid>
            <pubDate>Mon, 20 Sep 2021 06:31:04 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/7cd42ff7-f7ce-4aea-9b66-24f204b633c9/image.png" alt=""></p>
<p><a href="https://velog.io/@ryan-son/Design-patterns-in-Swift">목차로 돌아가기</a></p>
<h1 id="소개">소개</h1>
<p>템플릿 메서드 패턴 (Template method pattern)은 행동 관점의 디자인 패턴 (Behavioral design pattern)의 일종으로 실행 과정 전반이 동일한 타입의 세부 메서드들을 각 타입에서 구현하여 필요에 따라 유사하지만 다른 수행 로직을 가진 타입을 만드는 방식을 이릅니다.</p>
<h1 id="example">Example</h1>
<h2 id="가정">가정</h2>
<p>선택 모드에 따라 다른 지도 API를 사용하여 지도를 로드하는 경우를 살펴보겠습니다. 사용할 지도 API는 네이버, 카카오로 다르지만 지도를 불러올(초기화) 때 아래의 순서는 유지된다고 가정합니다.</p>
<ol>
<li>지도 서버 연결</li>
<li>지도를 화면에 표시</li>
<li>현재 위치로 이동</li>
</ol>
<h2 id="패턴-적용">패턴 적용</h2>
<p>지도를 초기화하는 메서드를 포함해 위에서 가정한 1, 2, 3에 맞는 기능을 외부에 제공할 인터페이스를 아래와 같이 프로토콜을 통해 정의합니다. 동시에 프로토콜 기본 구현 (Protocol default implementation)을 통해 구현할 기능이 의도한 순서대로 실행해주는 메서드를 구현합니다.</p>
<pre><code class="language-swift">protocol MapView {

    func connectMapServer()
    func showMapOnScreen()
    func moveToCurrentLocation()
    func initMap()
}

extension MapView {

    func initMap() {
        connectMapServer()
        showMapOnScreen()
        moveToCurrentLocation()
    }
}</code></pre>
<p>정의한 프로토콜을 준수하며 각 지도 API를 통해 지도를 불러올 타입을 정의합니다.</p>
<pre><code class="language-swift">struct NaverMapView: MapView {

    func connectMapServer() {
        print(&quot;네이버 지도 서버에 연결&quot;)
    }

    func showMapOnScreen() {
        print(&quot;네이버 지도 보여주기&quot;)
    }

    func moveToCurrentLocation() {
        print(&quot;네이버 지도에서 현재 좌표로 이동&quot;)
    }
}

struct KaKaoMapView: MapView {

    func connectMapServer() {
        print(&quot;카카오 지도 서버에 연결&quot;)
    }

    func showMapOnScreen() {
        print(&quot;카카오 지도 보여주기&quot;)
    }

    func moveToCurrentLocation() {
        print(&quot;카카오 지도에서 현재 좌표로 이동&quot;)
    }
}</code></pre>
<p>각 타입은 <code>MapView</code> 프로토콜이 기본 구현한 <code>initMap()</code> 메서드를 이미 가지고 있으므로 해당 메서드를 통해 지도를 불러오면 됩니다. 다른 지도 API를 통해 지도를 초기화한다고 하여도 이미 정의해둔 타입과 동일한 방식으로 <code>MapView</code> 프로토콜을 준수하는 타입을 정의하여 사용하면 됩니다.</p>
<p><a href="https://velog.io/@ryan-son/Design-patterns-in-Swift">목차로 돌아가기</a></p>
<h1 id="참고자료">참고자료</h1>
<ol>
<li><a href="https://github.com/ochococo/Design-Patterns-In-Swift">Design Patterns in Swift</a> - ochococo GitHub Repository</li>
<li><a href="https://www.youtube.com/watch?v=q3_WXP9pPUQ">객체지향 디자인패턴 2</a> - 얄팍한 코딩사전</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[디자인 패턴] Adapter pattern in Swift]]></title>
            <link>https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Adapter-pattern-in-Swift</link>
            <guid>https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Adapter-pattern-in-Swift</guid>
            <pubDate>Mon, 20 Sep 2021 05:59:09 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/050355ec-c611-43b2-ad2b-54c90a6d7e08/image.png" alt=""></p>
<p><a href="https://velog.io/@ryan-son/Design-patterns-in-Swift">목차로 돌아가기</a></p>
<h1 id="소개">소개</h1>
<p>어댑터 패턴은 구조형 디자인 패턴 (Structural design pattern)의 일종으로 인터페이스가 서로 다른 타입이 동일한 형식으로 작동할 수 있도록 만들어 줍니다.</p>
<h1 id="example">Example</h1>
<h2 id="가정">가정</h2>
<p>이전 포스트인 <a href="https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Strategy-in-Swift">전략 패턴</a>과 동일한 상황을 가정합니다. 검색 기능을 개발하고 있으며, 전체, 이미지, 뉴스, 지도 검색 기능이 이미 전략 패턴으로 구현되어 있습니다.</p>
<pre><code class="language-swift">final class SearchFeatureWithPattern {

    private var searchStrategy: SearchStrategy = SearchStrategyAll()

    func setSearchStrategy(to strategy: SearchStrategy) {
        searchStrategy = strategy
    }

    func searchButtonTapped() {
        searchStrategy.search()
    }
}

protocol SearchStrategy {
    func search()
}

struct SearchStrategyAll: SearchStrategy {
    func search() {
        print(&quot;전체 검색&quot;)
    }
}

struct SearchStrategyImage: SearchStrategy {
    func search() {
        print(&quot;이미지 검색&quot;)
    }
}

struct SearchStrategyNews: SearchStrategy {
    func search() {
        print(&quot;뉴스 검색&quot;)
    }
}

struct SearchStrategyMap: SearchStrategy {
    func search() {
        print(&quot;지도 검색&quot;)
    }
}</code></pre>
<p><a href="https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Strategy-in-Swift">전략 패턴 포스트</a>에서 확인하셨듯이 <code>SearchStrategy</code> 프로토콜을 준수하는 새로운 타입을 추가하는 방식으로 새로운 검색 전략을 추가할 수 있었습니다. 하지만 아래와 같이 다른 회사에서 작성된 검색 기능을 사용하고자 할 때는 어떻게 하여야 할까요?</p>
<pre><code class="language-swift">protocol FindAlgorithm {
    func find(global: Bool)
}

struct FindVideoAlgorithm: FindAlgorithm {
    func find(global: Bool) {
        print(&quot;\(global ? &quot;전체 &quot; : &quot;일부 &quot;)&quot; + &quot;동영상 찾기&quot;)
    }
}</code></pre>
<p>다른 회사에서 개발된 기능인 <code>FindVideoAlgorithm</code>은 해당 회사에서 사용했던 것으로 추정되는 <code>FindAlgorithm</code> 프로토콜을 따르고 있고, 전체 검색 범위에서 검색할 것인지 여부를 <code>global</code>이라는 매개변수에 <code>Bool</code> 타입의 인자를 전달하여 검색 작업을 수행합니다.</p>
<h2 id="패턴-적용">패턴 적용</h2>
<p><code>Adapter pattern</code>을 적용하면 이렇듯 기존 <code>SearchStrategy</code>와 다른 인터페이스를 가진 타입을 동일한 인터페이스를 사용하게끔 조정할 수 있습니다.</p>
<p>먼저, 콘센트 규격이 다른 해외에서 사용하는 어댑터와 같이 저희가 원하는 인터페이스를 제공해줄 수 있는 어댑터 타입을 정의합니다.</p>
<pre><code class="language-swift">struct SearchFindAdapter: SearchStrategy {
    private var findAlgorithm: FindAlgorithm

    init(findAlgorithm: FindAlgorithm) {
        self.findAlgorithm = findAlgorithm
    }

    func search() {
        // 내가 `search()`라고 말하면 너는 `find(global: true)`로 알아들으면 돼.
        findAlgorithm.find(global: true)
    }
}</code></pre>
<p>정의한 어댑터 타입은 이니셜라이저를 통해 변환 대상인 <code>FindAlgorithm</code> 타입의 인스턴스를 프로퍼티로 전달받아 <code>SearchStrategy</code> 인터페이스로 사용할 수 있도록 해줍니다.</p>
<p>어댑터를 활용하여 변환한 <code>FindVideoAlgorithm</code>은 아래와 같은 방식으로 사용할 수 있습니다.</p>
<pre><code class="language-swift">let searchFeatureWithPattern = SearchFeatureWithPattern()
let searchStrategyVideo = SearchFindAdapter(findAlgorithm: FindVideoAlgorithm())
searchFeatureWithPattern.setSearchStrategy(to: searchStrategyVideo)
searchFeatureWithPattern.searchButtonTapped()</code></pre>
<p><a href="https://velog.io/@ryan-son/Design-patterns-in-Swift">목차로 돌아가기</a></p>
<h1 id="참고자료">참고자료</h1>
<ol>
<li><a href="https://github.com/ochococo/Design-Patterns-In-Swift">Design Patterns in Swift</a> - ochococo GitHub Repository</li>
<li><a href="https://www.youtube.com/watch?v=lJES5TQTTWE">객체지향 디자인패턴 1</a> - 얄팍한 코딩사전</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[디자인 패턴] Strategy pattern in Swift]]></title>
            <link>https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Strategy-pattern-in-Swift</link>
            <guid>https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Strategy-pattern-in-Swift</guid>
            <pubDate>Sat, 18 Sep 2021 16:33:22 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/290744f3-5a7b-4871-8be7-43f324939b8e/image.png" alt=""></p>
<p><a href="https://velog.io/@ryan-son/Design-patterns-in-Swift">목차로 돌아가기</a></p>
<h1 id="소개">소개</h1>
<p>전략 패턴은 Behavioral design pattern의 일종으로 모드가 바뀔 때마다 전략을 교체할 수 있도록 타입을 제공합니다. 전략 패턴을 적용하면 새로운 전략을 추가하더라도 기존 코드의 수정을 하지 않거나 최소화할 수 있습니다.</p>
<h1 id="example">Example</h1>
<h2 id="가정">가정</h2>
<p>검색 기능을 개발하는 상황이라 가정합니다. 먼저, 전체, 이미지, 뉴스, 지도 검색 기능을 구현하는데, 이 경우 패턴을 적용하지 않았을 때와 적용하였을 때를 비교해보겠습니다.</p>
<h2 id="패턴을-적용하지-않은-경우">패턴을 적용하지 않은 경우</h2>
<p>패턴을 고려하지 않고 검색 기능을 구현하면 아래와 같이 작성할 수 있을 것입니다. 검색 모드를 열거형의 케이스로 정의하고, 모드 변경을 위한 메서드를 만든 후, 버튼 탭 동작에 따라 검색 모드에 따라 검색 로직을 실행하는 것입니다.</p>
<pre><code class="language-swift">final class SearchFeatureWithoutPattern {

    enum Mode {
        case all, image, news, map
    }

    private var mode: Mode = .all

    func setMode(to mode: Mode) {
        self.mode = mode
    }

    func searchButtonTapped() {
        switch mode {
        case .all:
            print(&quot;전체 검색&quot;)
        case .image:
            print(&quot;이미지 검색&quot;)
        case .news:
            print(&quot;뉴스 검색&quot;)
        case .map:
            print(&quot;지도 검색&quot;)
        }
    }
}

let searchFeatureWithoutPattern = SearchFeatureWithoutPattern()
searchFeatureWithoutPattern.setMode(to: .news)
searchFeatureWithoutPattern.searchButtonTapped()</code></pre>
<p>하지만 이 경우에는 동영상 검색과 같이 새로운 검색 전략을 추가하고자 할 때 <code>Mode</code>에 새로운 케이스를 추가하고 <code>searchButtonTapped()</code> 메서드를 직접 수정하여야 합니다. 확장성이 떨어지는 것이죠. 또한 프로젝트 이해도가 떨어지는 상황에서 기존에 개발해둔 기능을 수정하려 한다면 리스크가 클 수 있습니다.</p>
<h2 id="패턴을-적용한-경우">패턴을 적용한 경우</h2>
<p>반면 전략 패턴을 적용하여 동일한 기능을 구현하면 아래와 같이 작성할 수 있습니다. 각 전략을 타입으로써 구성하고 전략 타입들이 가져야할 최소한의 인스턴스 메서드 인터페이스를 프로토콜로 정의하여 이를 준수하도록 하는 것입니다. 프로토콜에 의해 구현 상 다형성을 보장하기에 각 전략 타입의 상세 구현은 각 타입에서 해주면 됩니다. 검색 전략의 변경은 <code>setSearchStrategy(to:)</code> 메서드를 통해 어떤 검색 전략 타입을 따를지 설정해줄 수 있습니다.</p>
<pre><code class="language-swift">final class SearchFeatureWithPattern {

    private var searchStrategy: SearchStrategy = SearchStrategyAll()

    func setSearchStrategy(to strategy: SearchStrategy) {
        searchStrategy = strategy
    }

    func searchButtonTapped() {
        searchStrategy.search()
    }
}

protocol SearchStrategy {
    func search()
}

struct SearchStrategyAll: SearchStrategy {
    func search() {
        print(&quot;전체 검색&quot;)
    }
}

struct SearchStrategyImage: SearchStrategy {
    func search() {
        print(&quot;이미지 검색&quot;)
    }
}

struct SearchStrategyNews: SearchStrategy {
    func search() {
        print(&quot;뉴스 검색&quot;)
    }
}

struct SearchStrategyMap: SearchStrategy {
    func search() {
        print(&quot;지도 검색&quot;)
    }
}

let searchFeatureWithPattern = SearchFeatureWithPattern()
searchFeatureWithPattern.setSearchStrategy(to: SearchStrategyNews())
searchFeatureWithPattern.searchButtonTapped()</code></pre>
<p>이러한 상황에서 새로운 검색 전략을 추가하고자 한다면 단순히 <code>SearchStrategy</code> 타입을 준수하는 새로운 검색 전략 타입을 작성하기만 하면 됩니다. 기존에 존재했던 어떤 타입, 프로퍼티, 메서드의 구현부를 수정하지 않아도 되는 것이죠.</p>
<p><a href="https://velog.io/@ryan-son/Design-patterns-in-Swift">목차로 돌아가기</a></p>
<h1 id="참고자료">참고자료</h1>
<ol>
<li><a href="https://github.com/ochococo/Design-Patterns-In-Swift">Design Patterns in Swift</a> - ochococo GitHub Repository</li>
<li><a href="https://www.youtube.com/watch?v=lJES5TQTTWE">객체지향 디자인패턴 1</a> - 얄팍한 코딩사전</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Vapor/Swift] Vapor와 Heroku를 이용한 REST API 구성 및 배포]]></title>
            <link>https://velog.io/@ryan-son/VaporSwift-Vapor%EC%99%80-Heroku%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-REST-API-%EA%B5%AC%EC%84%B1-%EB%B0%8F-%EB%B0%B0%ED%8F%AC</link>
            <guid>https://velog.io/@ryan-son/VaporSwift-Vapor%EC%99%80-Heroku%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-REST-API-%EA%B5%AC%EC%84%B1-%EB%B0%8F-%EB%B0%B0%ED%8F%AC</guid>
            <pubDate>Wed, 07 Jul 2021 19:29:21 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/373a67c4-7f4e-4954-80db-db3e2502ff11/image.png" alt=""></p>
<blockquote>
<p>이 시리즈는 Swift로 작성된 Server-side Web Framework인 Vapor를 이용하여 REST API를 구성하고 Heroku 서비스를 통해 배포하는 과정을 다룹니다.</p>
</blockquote>
<h1 id="table-of-contents">Table of Contents</h1>
<h4 id="1-vapor를-이용하여-서버-구성하기"><a href="https://velog.io/@ryan-son/VaporSwift-Vapor%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0">1. Vapor를 이용하여 서버 구성하기</a></h4>
<h4 id="2-heroku를-이용하여-vapor-서버-배포하기"><a href="https://velog.io/@ryan-son/VaporSwift-Heroku%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-Vapor-%EC%84%9C%EB%B2%84-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0">2. Heroku를 이용하여 Vapor 서버 배포하기</a></h4>
<h4 id="3-fluent-모델-알아보기"><a href="https://velog.io/@ryan-son/VaporSwift-Fluent-%EB%AA%A8%EB%8D%B8-%ED%83%80%EC%9E%85-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0">3. Fluent 모델 알아보기</a></h4>
<h4 id="4-인코딩-및-디코딩을-위한-모델-타입-작성하기"><a href="https://velog.io/@ryan-son/VaporSwift-%EC%9D%B8%EC%BD%94%EB%94%A9-%EB%B0%8F-%EB%94%94%EC%BD%94%EB%94%A9%EC%9D%84-%EC%9C%84%ED%95%9C-%EB%AA%A8%EB%8D%B8-%ED%83%80%EC%9E%85-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0">4. 인코딩 및 디코딩을 위한 모델 타입 작성하기</a></h4>
<h4 id="5-local에서-postgresql-db-구성하기"><a href="https://velog.io/@ryan-son/VaporSwift-Local%EC%97%90%EC%84%9C-PostgreSQL-DB-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0">5. Local에서 PostgreSQL DB 구성하기</a></h4>
<h4 id="6-배포-서버의-postgresql-db-구성하기"><a href="https://velog.io/@ryan-son/VaporSwift-%EB%B0%B0%ED%8F%AC-%EC%84%9C%EB%B2%84%EC%9D%98-PostgreSQL-DB-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0">6. 배포 서버의 PostgreSQL DB 구성하기</a></h4>
<h4 id="7-모델에-dto-추가하기"><a href="https://velog.io/@ryan-son/VaporSwift-%EB%AA%A8%EB%8D%B8%EC%97%90-DTO-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0">7. 모델에 DTO 추가하기</a></h4>
<h4 id="8-crud-기능-구현하기"><a href="https://velog.io/@ryan-son/VaporSwift-CRUD-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0">8. CRUD 기능 구현하기</a></h4>
<h4 id="9-request-contents-검증하기"><a href="https://velog.io/@ryan-son/VaporSwift-Request-%EC%BB%A8%ED%85%90%EC%B8%A0-%EA%B2%80%EC%A6%9D%ED%95%98%EA%B8%B0">9. Request contents 검증하기</a></h4>
<h4 id="10-에러-커스터마이징하기"><a href="https://velog.io/@ryan-son/VaporSwift-%EC%97%90%EB%9F%AC-%EC%BB%A4%EC%8A%A4%ED%84%B0%EB%A7%88%EC%9D%B4%EC%A7%95%ED%95%98%EA%B8%B0">10. 에러 커스터마이징하기</a></h4>
<hr>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://docs.vapor.codes/4.0/">Vapor Docs</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Vapor/Swift] 에러 커스터마이징하기]]></title>
            <link>https://velog.io/@ryan-son/VaporSwift-%EC%97%90%EB%9F%AC-%EC%BB%A4%EC%8A%A4%ED%84%B0%EB%A7%88%EC%9D%B4%EC%A7%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ryan-son/VaporSwift-%EC%97%90%EB%9F%AC-%EC%BB%A4%EC%8A%A4%ED%84%B0%EB%A7%88%EC%9D%B4%EC%A7%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 07 Jul 2021 19:04:53 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/8de4d7c1-71bd-4898-bf0a-57c76ba17495/image.png" alt=""></p>
<p>이전 포스팅의 말미에 잠시 언급 드렸듯이 검증 이외에도 클라이언트 요청에 따라 서버에서 작업을 수행하다 에러가 일어나는 경우가 있습니다. 이런 경우에 서버에서 기본적으로 지정된 HTTP status code와 에러 메시지를 response로 반환하게 되지만 이해하기 어려운 경우가 많습니다. 예를 들어 <code>&quot;Something went wrong!&quot;</code>, <code>&quot;Bad Request&quot;</code>와 같은 에러 메시지를 받으면 도대체 무엇이 잘못 되었는지 알 수 없는 경우가 많습니다. 그래서 이번 시간에도 이전 포스팅의 순서와 같이 Vapor의 에러와 관련한 내용을 먼저 알아보고 적용하는 모습을 살펴보도록 하겠습니다.</p>
<h1 id="에러">에러</h1>
<p>Vapor는 에러 처리를 위해 Swift의 <code>Error</code> 프로토콜을 사용합니다. 라우트(루트) 핸들러는 에러를 <code>throw</code> 하거나 실패한 <code>EventLoopFuture</code>를 반환할 수 있습니다. Swift의 <code>Error</code>를 던지거나 반환하면 <code>500</code> 상태 코드 응답과 함께 에러가 기록되는 결과를 낳습니다. <code>AbortError</code>과 <code>DebuggableError</code>를 통해 응답과 기록을 각각 변경할 수 있습니다. 에러 처리는 <code>ErrorMiddleware</code>에 의해 수행되는데, 이 middleware는 애플리케이션에 기본적으로 추가되어 있고 원할 경우 사용자 지정 로직으로 교체할 수 있습니다.</p>
<h2 id="abort">Abort</h2>
<p>Vapor는 <code>Abort</code>라는 이름의 구조체로 기본 에러를 제공합니다. 이 구조체는 <code>AbortError</code>과 <code>DebuggableError</code> 프로토콜을 모두 준수하고 있고, 필요에 따라 HTTP 상태와 failure에 대한 원인을 이니셜라이즈 할 수 있습니다.</p>
<pre><code class="language-swift">// 404 error, default &quot;Not Found&quot; reason used.
throw Abort(.notFound)

// 401 error, custom reason used.
throw Abort(.unauthorized, reason: &quot;Invalid Credentials&quot;)</code></pre>
<p><code>flatMap</code> 클로저와 같이 throwing이 지원되지 않는 비동기 상황에서는 failed future를 반환할 수 있습니다.</p>
<pre><code class="language-swift">guard let user = user else {
    req.eventLoop.makeFailedFuture(Abort(.notFound))    
}
return user.save()</code></pre>
<p>Vapor는 옵셔널 값을 가진 future를 언래핑하기 위한 도우미 익스텐션으로 <code>unwrap(or:)</code> 메서드를 가지고 있습니다.</p>
<pre><code class="language-swift">User.find(id, on: db)
    .unwrap(or: Abort(.notFound))
    .flatMap 
{ user in
    // Non-optional User supplied to closure.
}</code></pre>
<p>만약 <code>User.find</code>가 <code>nil</code>을 반환한다면, future는 실패하며 지정된 에러의 이유를 보여줄 것입니다. 그렇지 않은 경우에는 <code>flatMap</code>에게 옵셔널이 아닌 값을 제공해줄 것입니다.</p>
<h2 id="abort-error">Abort Error</h2>
<p>기본적으로 라우트(루트) 클로저가 던지거나 반환한 모든 Swift <code>Error</code>는 <code>500 Internal Server Error</code> 응답으로 처리됩니다. 디버그 모드로 빌드된 경우에는 <code>ErrorMiddleware</code>가 에러 설명을 포함해줍니다. 이는 프로젝트가 release mode로 빌드되면 보안 상의 이유로 완전히 제거됩니다.</p>
<p>특정 에러에 대해 HTTP 응답 상태 또는 원인을 구성하고자 한다면 <code>AbortError</code>를 준수하면 됩니다.</p>
<pre><code class="language-swift">import Vapor

enum MyError {
    case userNotLoggedIn
    case invalidEmail(String)
}

extension MyError: AbortError {
    var reason: String {
        switch self {
        case .userNotLoggedIn:
            return &quot;User is not logged in.&quot;
        case .invalidEmail(let email):
            return &quot;Email address is not valid: \(email).&quot;
        }
    }

    var status: HTTPStatus {
        switch self {
        case .userNotLoggedIn:
            return .unauthorized
        case .invalidEmail:
            return .badRequest
        }
    }
}</code></pre>
<hr>
<blockquote>
<p>Skip 하셔도 되는 구간입니다.</p>
</blockquote>
<h2 id="debuggable-error">Debuggable Error</h2>
<p><code>ErrorMiddleware</code>은 루트에서 던져진 에러를 기록하기 위해 <code>Logger.report(error:)</code> 메서드를 사용합니다. 이 메서드는 읽을 수 있는 메시지를 기록하기 위해 <code>CustomStringConvertible</code> 및 <code>LocalizedError</code>의 채택 여부를 체크할 것입니다.</p>
<p>에러 기록 작업을 커스터마이징 하기를 원한다면 에러 타입이 <code>DebuggableError</code>을 준수하게 만들면 됩니다. 이 프로토콜은 고유 식별자, 소스 위치, 스택 추적과 같은 유용한 프로퍼티들을 포함하고 있습니다. 대부분의 프로퍼티들은 준수를 용이하게 만들기 위해 선택적으로 사용할 수 있도록 설정되어 있습니다.</p>
<p><code>DebuggableError</code>를 잘 준수하면 </p>
<p><code>DebuggableError</code>를 가장 잘 준수하려면 에러 타입이 구조체여야 하며 필요한 경우에는 소스 및 스택 추적 정보를 저장할 수 있어야 합니다. 아래는 구조체 및 에러 소스 정보를 캡처하고 사용하기 위해 업데이트된 <code>MyError</code> 열거 타입의 예시입니다.</p>
<pre><code class="language-swift">import Vapor

struct MyError: DebuggableError {
    enum Value {
        case userNotLoggedIn
        case invalidEmail(String)
    }

    var identifier: String {
        switch self.value {
        case .userNotLoggedIn:
            return &quot;userNotLoggedIn&quot;
        case .invalidEmail:
            return &quot;invalidEmail&quot;
        }
    }

    var reason: String {
        switch self.value {
        case .userNotLoggedIn:
            return &quot;User is not logged in.&quot;
        case .invalidEmail(let email):
            return &quot;Email address is not valid: \(email).&quot;
        }
    }

    var value: Value
    var source: ErrorSource?

    init(
        _ value: Value,
        file: String = #file,
        function: String = #function,
        line: UInt = #line,
        column: UInt = #column
    ) {
        self.value = value
        self.source = .init(
            file: file,
            function: function,
            line: line,
            column: column
        )
    }
}</code></pre>
<p><code>DebuggableError</code>는 발생한 에러의 디버그 용이성을 향상시켜줄 수 있는 <code>possibleCauses</code> 및 <code>suggestedFixes</code>와 같은 몇 가지 다른 프로퍼티들을 가지고 있습니다. 더 많은 정보는 프로토콜 자체에 대해 살펴보시면 됩니다.</p>
<h3 id="스택-추적">스택 추적</h3>
<p>Vapor에는 정상적인 Swift 오류와 충돌에 대한 스택 추적를 볼 수 있는 기능이 포함되어 있습니다.</p>
<h4 id="swift-backtrace">Swift Backtrace</h4>
<p>Vapor는 SwiftBacktrace 라이브러리를 사용하여 Linux에서 치명적인 에러 또는 assertion이 발생한 후 스택 추적을 제공합니다. 이 기능이 작동하려면 아래와 같이 컴파일 중에 앱에 디버그 기호가 포함되어 있어야 합니다.</p>
<pre><code class="language-swift">swift build -c release -Xswiftc -g</code></pre>
<h4 id="에러-추적">에러 추적</h4>
<p>기본적으로 <code>Abort</code>는 이니셜라이즈 되었을 때 현재 스택 추적을 캡처합니다. 사용자 지정 에러 타입들은 <code>DebuggableError</code>을 채택하고 <code>StackTrace.capture()</code>를 가지게 함으로써 이 작업을 수행할 수 있습니다.</p>
<pre><code class="language-swift">import Vapor

struct MyError: DebuggableError {
    var identifier: String
    var reason: String
    var stackTrace: StackTrace?

    init(
        identifier: String,
        reason: String,
        stackTrace: StackTrace? = .capture()
    ) {
        self.identifier = identifier
        self.reason = reason
        self.stackTrace = stackTrace
    }
}</code></pre>
<p>애플리케이션의 <a href="https://docs.vapor.codes/4.0/logging/#level">log level</a>이 <code>.debug</code> 이하로 설정되어 잇는 경우 에러 스택 추적은 로그 결과에 포함될 것입니다.</p>
<p>스택 추적은 log level이 <code>.debug</code> 이상이라면 캡처되지 않습니다. 이 행동을 재정의하려면 <code>configure</code>에서 <code>StackTrace.isCaptureEnabled</code>를 수동으로 설정해주세요.</p>
<pre><code class="language-swift">// Always capture stack traces, regardless of log level.
StackTrace.isCaptureEnabled = true</code></pre>
<h2 id="error-middleware">Error Middleware</h2>
<p><code>ErrorMiddleware</code>는 기본적으로 애플리케이션에 추가된 유일한 미들웨어입니다. 이 미들웨어는 루트 핸들러가 던지거나 반환한 Swift 에러들을 HTTP 응답으로 변환해줍니다. 이 미들웨어가 없으면 에러가 발생하여도 응답 없이 연결이 끊어질 것입니다.</p>
<p><code>AbortError</code>과 <code>DebuggableError</code>가 제공하는 에러처리를 커스터마이징하고 싶다면 <code>ErrorMiddleware</code>를 자체적인 에러 처리 로직으로 교체하면 됩니다. 이렇게 하려면 먼저 <code>app.middleware</code>를 빈 구성으로 설정함으로써 기본 에러 미들웨어를 제거하고, 자체 에러 처리 미들웨어를 애플리케이션의 첫 번째 미들웨어로 추가하면 됩니다.</p>
<pre><code class="language-swift">// Remove all existing middleware.
app.middleware = .init()
// Add custom error handling middleware first.
app.middleware.use(MyErrorMiddleware())</code></pre>
<blockquote>
<p>스킵 구간 끝입니다.</p>
</blockquote>
<hr>
<h1 id="적용하기">적용하기</h1>
<p>저는 아래와 같이 크게 헤더의 Content-Type 검사와 검증 실패 시 에러 <code>unwrap(or:)</code> 메서드에서 <code>nil</code>을 발견하였을 때 에러를 정의하여 적용하였습니다.</p>
<pre><code class="language-swift">import Vapor

enum HTTPError {
    case progressNotFoundInURL
    case invalidProgressInURL
    case invalidContentType
    case validationFailedWhileCreating
    case validationFailedWhileUpdating
    case invalidID
}

extension HTTPError: AbortError {

    var reason: String {
        switch self {
        case .progressNotFoundInURL:
            return &quot;This path requires progress as path parameter. Please enter \&quot;/todo\&quot;, \&quot;/doing\&quot; or \&quot;/done\&quot; additively.&quot;
        case .invalidProgressInURL:
            return &quot;Progresses are classified as todo, doing and done. Please check entered URL.&quot;
        case .invalidContentType:
            return &quot;The Content-Type of the request is not application/json.&quot;
        case .validationFailedWhileCreating:
            return &quot;Validations are failed while creating data. Please check if the content is not exceed length of 1000, progress is in todo, doing and done.&quot;
        case .validationFailedWhileUpdating:
            return &quot;Validations are failed while updating data. Please check if the content has id, not to exceed length of 1000, and the progress is in todo, doing and done.&quot;
        case .invalidID:
            return &quot;You have entered invalid projectItem ID. The database does not have such item.&quot;
        }
    }

    var status: HTTPResponseStatus {
        switch self {
        case .progressNotFoundInURL:
            return .notFound
        case .invalidProgressInURL:
            return .notFound
        case .invalidContentType:
            return .badRequest
        case .validationFailedWhileCreating:
            return .badRequest
        case .validationFailedWhileUpdating:
            return .badRequest
        case .invalidID:
            return .notFound
        }
    }
}</code></pre>
<pre><code class="language-swift">func create(req: Request) throws -&gt; EventLoopFuture&lt;ProjectItem&gt; {
    // ⭐️ Header의 Content-Type이 application/json일 것
    guard req.headers.contentType == .json else {
        throw HTTPError.invalidContentType
    }

    do {
        try PostProjectItem.validate(content: req)
    } catch {
        // 검증에 대한 커스텀 에러 
        throw HTTPError.validationFailedWhileCreating
    }

    let exist = try req.content.decode(PostProjectItem.self)
    let newProjectItem = ProjectItem(exist)
    return newProjectItem.save(on: req.db).map{ (result) -&gt; ProjectItem in
        return newProjectItem
    }
}

func update(req: Request) throws -&gt; EventLoopFuture&lt;ProjectItem&gt; {
    guard req.headers.contentType == .json else {
        throw HTTPError.invalidContentType
    }

    do {
        try PatchProjectItem.validate(content: req)
    } catch {
        throw HTTPError.validationFailedWhileUpdating
    }

    let exist = try req.content.decode(PatchProjectItem.self)

    return ProjectItem.find(exist.id, on: req.db)
        .unwrap(or: HTTPError.invalidID)
        .flatMap { item in
            if let title = exist.title { item.title = title }
            if let content = exist.content { item.content = content }
            if let progress = exist.progress { item.progress = progress }
            if let deadlineDate = exist.deadlineDate { item.deadlineDate = deadlineDate }
            if let index = exist.index { item.index = index }

            return item.update(on: req.db).map { return item }
        }
}

func delete(req: Request) throws -&gt; EventLoopFuture&lt;HTTPStatus&gt; {
    guard req.headers.contentType == .json else {
        throw HTTPError.invalidContentType
    }

    let exist = try req.content.decode(DeleteProjectItem.self)
    return ProjectItem.find(exist.id, on: req.db)
        .unwrap(or: HTTPError.invalidID)
        .flatMap { $0.delete(on: req.db) }
        .transform(to: .ok)
}</code></pre>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://docs.vapor.codes/4.0/errors/">Vapor Docs - Errors</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Vapor/Swift] Request contents 검증하기]]></title>
            <link>https://velog.io/@ryan-son/VaporSwift-Request-%EC%BB%A8%ED%85%90%EC%B8%A0-%EA%B2%80%EC%A6%9D%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ryan-son/VaporSwift-Request-%EC%BB%A8%ED%85%90%EC%B8%A0-%EA%B2%80%EC%A6%9D%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 07 Jul 2021 17:55:32 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/aebeccf7-6b95-442c-8382-aae92d880d99/image.png" alt=""></p>
<p>지난 시간까지 CRUD 기능을 구현해 보았습니다. 이번 시간에는 Request contents를 검증하는 방법을 다루어보겠습니다. </p>
<h1 id="request-contents를-왜-검증하나요">Request contents를 왜 검증하나요?</h1>
<p>클라이언트의 요청에 따라 CRUD 기능을 수행할 때, 특히 DB의 수정 작업이 이루어지는 Create, Update, Delete 작업을 수행할 때 특정 필드가 반드시 포함되어야 한다거나 해당 필드 입력값에 대한 제한이 필요한 경우 검증 (Validation)을 적용합니다.</p>
<p>Vapor Validation API를 사용하면 <code>Content</code> API를 통해 데이터를 디코딩하기 전에 받은 요청을 검증할 수 있습니다. 사실 Vapor는 Swift의 타입 안전성을 가진 <code>Codable</code> 프로토콜과 통합되어 있어 동적 타입 언어에 비해 데이터 검증에 대해 많이 걱정할 필요는 없기는 하지만, 아래 이유들로 인해 검증을 사용할 가치는 충분히 있습니다.</p>
<h2 id="에러가-발생한-이유를-쉽게-이해할-수-있다">에러가 발생한 이유를 쉽게 이해할 수 있다.</h2>
<p><code>Content</code> API를 이용하여 구조체를 디코딩하면 데이터가 적절하지 않을 경우에 에러가 발생합니다. 하지만 이런 에러 메시지는 때로는 이해하기 어려운 경우가 있죠. 예를 들어 아래 예시와 같은 상황이 있을 수 있습니다.</p>
<pre><code class="language-swift">enum Color: String, Codable {
    case red, blue, green
}</code></pre>
<p>만약 사용자가 <code>Color</code> 타입에 <code>&quot;purple&quot;</code>이라는 문자열을 전달하면 아래와 같은 에러를 받을 것입니다.</p>
<pre><code class="language-swift">Cannot initialize Color from invalid String value purple for key favoriteColor</code></pre>
<p>이러한 에러는 기술적으로는 잘못된 값을 입력받지 않음으로써 endpoint를 성공적으로 보호했지만 사용성은 떨어진다고 볼 수 있습니다. 이러한 방식 대신에 사용자에게 에러와 입력할 수 있는 옵션을 알리는 것이 더 나을 수 있습니다. Validation API를 사용하면 다음과 같은 에러를 보여줌으로써 사용자에게 안내 문구를 보여줄 수 있습니다.</p>
<pre><code class="language-swift">favoriteColor is not red, blue, or green</code></pre>
<h2 id="세부적인-내용을-검증할-수-있다">세부적인 내용을 검증할 수 있다.</h2>
<p><code>Codable</code>은 타입 검증을 잘 다루지만, 때로는 타입 검증 이상의 것을 하기를 원할 때도 있습니다. 예를 들어, 문자열의 컨텐츠를 검증하거나 정수의 크기를 검증하는 것이 있을 수 있죠. Validation API로 이메일, 문자 세트 (character sets), 정수 범위 이상의 것을 검증할 수 있습니다.</p>
<h1 id="validatable">Validatable</h1>
<p>요청을 검증하려면 <code>Validations</code> 컬렉션을 만들어야 합니다. 이는 기존의 타입에 <code>Validatable</code> 프로토콜을 준수하게 만들면 됩니다.</p>
<p>아래의 예시를 통해 <code>POST /users</code> endpoint에 검증을 추가하는 방법을 알아보겠습니다.</p>
<pre><code class="language-swift">enum Color: String, Codable {
    case red, blue, green
}

struct CreateUser: Content {
    var name: String
    var username: String
    var age: Int
    var email: String
    var favoriteColor: Color?
}

app.post(&quot;users&quot;) { req -&gt; CreateUser in
    let user = try req.content.decode(CreateUser.self)
    // Do something with user.
    return user
}</code></pre>
<h2 id="검증-추가하기">검증 추가하기</h2>
<p>첫 번째 해야할 일은 디코딩할 타입(이 경우 <code>CreateUser</code>)이 <code>Validable</code> 프로토콜을 준수하게 하는 것입니다. 이는 아래와 같이 extension을 통해 할 수 있습니다.</p>
<pre><code class="language-swift">extension CreateUser: Validatable {
    static func validations(_ validations: inout Validations) {
        // Validations go here.
    }
}</code></pre>
<p>스태틱 메서드인 <code>validations(_:)</code>는 <code>CreateUser</code>가 검증될 때 호출될 것입니다. 수행하기를 원하는 모든 검증을 <code>Validations</code> 컬렉션에 추가해주어야 합니다. 아래는 사용자가 입력한 이메일이 유효한지를 검증하는 간단한 검증을 추가하는 예시입니다.</p>
<pre><code class="language-swift">validations.add(&quot;email&quot;, as: String.self, is: .email)</code></pre>
<p>첫 번째 파라미터의 전달인자에는 값의 예상되는 키를 입력합니다. 이 경우에는 <code>&quot;email&quot;</code>입니다. 이는 검증될 타입의 프로퍼티 이름과 동일해야 합니다. 두 번째 파라미터인 <code>as</code>의 전달인자에는 예상되는 타입을 입력해야 합니다. 이 경우에는 <code>String</code>입니다. 타입은 대개는 프로퍼티의 타입과 동일하지만, 항상 그런 것은 아닙니다. 마지막으로, 세 번째 파라미터 <code>is</code>의 전달인자에는 하나 혹은 그 이상의 validator가 올 수 있습니다. 이 경우에는 입력된 값이 이메일 주소인지를 검증하는 하나의 validator만 추가한 모습을 확인하실 수 있습니다.</p>
<h2 id="요청-컨텐츠-검증하기">요청 컨텐츠 검증하기</h2>
<p>타입이 <code>Validatable</code> 프로토콜을 준수하게 만들었다면, 스태틱 함수인 <code>validate(content:)</code>를 통해 request content를 검증할 수 있습니다. 아래 내용을 라우트(루트) 핸들러의 <code>req.content.decode(CreateUser.self)</code> 이전에 위치시키면 됩니다.</p>
<pre><code class="language-swift">try CreateUser.validate(content: req)</code></pre>
<p>이제 유효하지 않은 이메일을 포함한 요청을 보내봅시다.</p>
<pre><code class="language-swift">POST /users HTTP/1.1
Content-Length: 67
Content-Type: application/json

{
    &quot;age&quot;: 4,
    &quot;email&quot;: &quot;foo&quot;,
    &quot;favoriteColor&quot;: &quot;green&quot;,
    &quot;name&quot;: &quot;Foo&quot;,
    &quot;username&quot;: &quot;foo&quot;
}</code></pre>
<p>그럼 아래와 같은 에러를 확인하실 수 있습니다.</p>
<pre><code class="language-swift">email is not a valid email address</code></pre>
<h2 id="요청-쿼리-검증하기">요청 쿼리 검증하기</h2>
<p><code>Validable</code>을 준수하는 타입에는 요청의 쿼리 문자열을 검증하는 데 사용할 수 있는 <code>validate(query:)</code>도 있습니다. 아래 코드를 라우트(루트) 핸들러에 넣으시면 됩니다.</p>
<pre><code class="language-swift">try CreateUser.validate(query: req)
req.query.decode(CreateUser.self)</code></pre>
<p>이제 아래와 같이 쿼리 문자열에 유효하지 않은 이메일을 포함해서 요청을 보내봅시다.</p>
<pre><code class="language-swift">GET /users?age=4&amp;email=foo&amp;favoriteColor=green&amp;name=Foo&amp;username=foo HTTP/1.1</code></pre>
<p>그럼 아래와 같은 메시지를 확인하실 수 있으실 것입니다.</p>
<pre><code class="language-swift">email is not a valid email address</code></pre>
<h2 id="정수-검증하기">정수 검증하기</h2>
<p>이제 <code>age</code>에 대한 검증을 추가해봅시다.</p>
<pre><code class="language-swift">validations.add(&quot;age&quot;, as: Int.self, is: .range(13...))</code></pre>
<p>age 검증은 age가 13 이상임을 요구하고 있습니다. 만약 위에서 했던 요청을 다시 시도한다면 아래와 같은 에러를 만나시게 될 것입니다.</p>
<pre><code class="language-swift">age is less than minimum of 13, email is not a valid email address</code></pre>
<h2 id="문자열-검증하기">문자열 검증하기</h2>
<p>다음으로 <code>name</code>과 <code>username</code>에 대한 검증을 추가해봅시다.</p>
<pre><code class="language-swift">validations.add(&quot;name&quot;, as: String.self, is: !.empty)
validations.add(&quot;username&quot;, as: String.self, is: .count(3...) &amp;&amp; .alphanumeric)</code></pre>
<p>name 검증은 <code>.empty</code> 검증을 반전시키기 위해 <code>!</code> 연산자를 사용했습니다. 이는 요청 시 문자열이 비어있지 않을 것을 요구하게 됩니다.</p>
<p>username 검증은 <code>&amp;&amp;</code>을 이용해 두 개의 validators를 결합하고 있습니다. 이는 문자열이 최소 3 개의 characters를 가지고 알파벳과 숫자만 포함(alphanumeric)할 것을 요구하게 됩니다.</p>
<h2 id="열거형-검증하기">열거형 검증하기</h2>
<p>마지막으로 <code>favoriteColor</code>이 유효한지 체크하기 위해 조금 더 어려운 검증을 살펴보겠습니다. </p>
<pre><code class="language-swift">validations.add(
    &quot;favoriteColor&quot;, as: String.self,
    is: .in(&quot;red&quot;, &quot;blue&quot;, &quot;green&quot;),
    required: false
)</code></pre>
<p>유효하지 않은 값으로부터 <code>Color</code>를 디코딩할 수 없기 때문에 이 검증은 <code>String</code>을 베이스 타입으로 사용합니다. 값이 유효한 옵션인 red, blue, green 중에 있는지 여부를 검증하기 위해 <code>.in</code> validator를 사용하고 있습니다. 이 값은 옵셔널이므로 요청 데이터에 키가 없더라도 검증이 실패하지 않도록 <code>required</code>가 false로 설정되어 있습니다.</p>
<p>위의 검증은 favorite color 키가 없다면 통과하지만 <code>null</code>을 넣으면 통과되지 않습니다. <code>null</code> 입력을 지원하려면 검증 타입을 <code>String?</code>으로 변경하고 <code>.nil ||</code>을 추가해주세요.</p>
<pre><code class="language-swift">validations.add(
    &quot;favoriteColor&quot;, as: String?.self,
    is: .nil || .in(&quot;red&quot;, &quot;blue&quot;, &quot;green&quot;),
    required: false
)</code></pre>
<h2 id="validators">Validators</h2>
<p>아래와 같은 Validators를 사용할 수 있습니다.</p>
<table>
<thead>
<tr>
<th>Validation</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>.ascii</code></td>
<td>ASCII 문자만 포함할 수 있음</td>
</tr>
<tr>
<td><code>.alphanumeric</code></td>
<td>알파벳 또는 숫자 문자만 포함할 수 있음</td>
</tr>
<tr>
<td><code>.characterSet(_:)</code></td>
<td><code>CharacterSet</code>에 작성된 문자들만 포함할 수 있음</td>
</tr>
<tr>
<td><code>.count(_:)</code></td>
<td>컬렉션 내 요소의 개수가 작성한 범위 내에 있어야 함</td>
</tr>
<tr>
<td><code>.email</code></td>
<td>유효한 이메일을 포함해야 함</td>
</tr>
<tr>
<td><code>.empty</code></td>
<td>컬렉션이 비어있어야 함</td>
</tr>
<tr>
<td><code>.in(_:)</code></td>
<td>값이 작성한 <code>Collection</code> 안에 있어야 함</td>
</tr>
<tr>
<td><code>.nil</code></td>
<td>값이 <code>null</code>이어야 함</td>
</tr>
<tr>
<td><code>.range(_:)</code></td>
<td>값이 제공된 <code>Range</code> 안에 있어야 함</td>
</tr>
<tr>
<td><code>.url</code></td>
<td>유효한 URL을 포함해야함</td>
</tr>
</tbody></table>
<p>Validators는 아래 연산자를 사용해서 복합적으로 검증을 구성할 수 있습니다.</p>
<table>
<thead>
<tr>
<th>Operator</th>
<th>Position</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>!</code></td>
<td>앞(prefix)</td>
<td>Validator를 뒤집어 반대의 경우를 요구함</td>
</tr>
<tr>
<td><code>&amp;&amp;</code></td>
<td>중간(infix)</td>
<td>두 개의 validators를 결합하여 두 가지 모두를 요구함</td>
</tr>
<tr>
<td>||</td>
<td>중간(infix)</td>
<td>두 개의 validators를 결합하여 두 가지 중에 하나를 요구함</td>
</tr>
</tbody></table>
<h1 id="적용하기">적용하기</h1>
<p>예시 프로젝트에는 <code>POST</code>, <code>PATCH</code> 메서드로 요청을 보낼 경우 아래와 같은 검증들을 하도록 구성했습니다.</p>
<pre><code class="language-swift">extension PostProjectItem: Validatable {
    static func validations(_ validations: inout Validations) {
        validations.add(&quot;title&quot;, as: String.self, required: true)
        validations.add(&quot;content&quot;, as: String.self, is: .count(...1000), required: true)
        validations.add(&quot;progress&quot;, as: String.self, is: .in(&quot;todo&quot;, &quot;doing&quot;, &quot;done&quot;), required: true)
        validations.add(&quot;index&quot;, as: Int.self, required: true)
        validations.add(&quot;deadlineDate&quot;, as: Date.self, required: true)
    }
}

extension PatchProjectItem: Validatable {
    static func validations(_ validations: inout Validations) {
        validations.add(&quot;id&quot;, as: String.self, required: true)
        validations.add(&quot;title&quot;, as: String.self, required: false)
        validations.add(&quot;content&quot;, as: String.self, is: .count(...1000), required: false)
        validations.add(&quot;progress&quot;, as: String.self, is: .in(&quot;todo&quot;, &quot;doing&quot;, &quot;done&quot;), required: false)
        validations.add(&quot;index&quot;, as: Int.self, required: false)
        validations.add(&quot;deadlineDate&quot;, as: Date.self, required: false)
    }
}</code></pre>
<p><code>POST</code>의 경우만 간략히 설명 드리겠습니다.</p>
<ol>
<li><code>&quot;title&quot;</code>은 <code>String</code> 타입이어야 하고 반드시 요청에 키를 포함하여야 한다.</li>
<li><code>&quot;content&quot;</code>는 <code>String</code> 타입이어야 하고 길이가 0 ~ 1000 자 사이어야 하지만 반드시 요청에 키를 포함하여야 할 필요는 없다.</li>
<li><code>&quot;progress&quot;</code>는 <code>String</code> 타입이어야 하고 <code>&quot;todo&quot;</code>, <code>&quot;doing&quot;</code>, <code>&quot;done&quot;</code> 중에 하나여야 하고, 반드시 요청에 키를 포함하여야 한다.</li>
<li><code>&quot;index&quot;</code>는 <code>Int</code> 타입이어야 하고 반드시 요청에 키를 포함하여야 한다.</li>
<li><code>&quot;deadlineDate</code>는 <code>Date</code> 타입이어야 하고 반드시 요청에 키를 포함하여야 한다.</li>
</ol>
<p>앞서 보셨듯이 위의 검증을 적용하려면 HTTP 메서드 처리에 사용되는 메서드에 request 디코딩 전에 <code>validate(content:)</code> 메서드를 추가하시면 됩니다.</p>
<pre><code class="language-swift">func create(req: Request) throws -&gt; EventLoopFuture&lt;ProjectItem&gt; {
    try PostProjectItem.validate(content: req)
    let exist = try req.content.decode(PostProjectItem.self)
    let newProjectItem = ProjectItem(exist)
    return newProjectItem.save(on: req.db).map { (result) -&gt; ProjectItem in
        return newProjectItem
    }
}

func update(req: Request) throws -&gt; EventLoopFuture&lt;ProjectItem&gt; {
    try PatchProjectItem.validate(content: req)
    let exist = try req.content.decode(PatchProjectItem.self)

    return ProjectItem.find(exist.id, on: req.db)
        .unwrap(or: HTTPError.invalidID)
        .flatMap { item in
            if let title = exist.title { item.title = title }
            if let content = exist.content { item.content = content }
            if let progress = exist.progress { item.progress = progress }
            if let deadlineDate = exist.deadlineDate { item.deadlineDate = deadlineDate }
            if let index = exist.index { item.index = index }
            return item.update(on: req.db).map { return item }
        }
}</code></pre>
<h1 id="다음은">다음은?</h1>
<p>검증을 통해 입력값에 대한 제한과 확인을 하였고, 검증 결과에 의해 요청을 받아들일 수 없는 경우 사용자에게 적절한 에러 메시지도 제공해주었습니다. 하지만 헤더의 <code>Content-Type</code>이 잘못되었다거나 기타 요청을 처리하는 도중 에러가 발생했을 때 우리 상황에 더 적절한 에러 메시지를 제공해줄 수도 있을 것입니다. 다음 시간에는 Vapor의 <code>AbortError</code> 프로토콜을 이용해 사용자가 받아보는 메시지를 커스터마이징 하는 방법에 대해 알아보겠습니다!</p>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://docs.vapor.codes/4.0/validation/">Vapor Docs - Validation</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Vapor/Swift] CRUD 기능 구현하기]]></title>
            <link>https://velog.io/@ryan-son/VaporSwift-CRUD-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ryan-son/VaporSwift-CRUD-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 07 Jul 2021 10:27:36 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/234082d7-4c6b-4ddf-8936-d7d2f8057c9d/image.png" alt=""></p>
<p>데이터베이스 마이그레이션 작업에 모델 DTO 추가까지 진행해 보았습니다. 이제 실제로 만들고(Create), 읽고(Read), 업데이트하고(Update), 삭제할(Delete) 수 있는 API를 서버에서 구현해볼까요? </p>
<p>변경사항이 일어날 때마다 배포하여 테스트하기에는 시간이 많이 소요되니 로컬 환경에서 진행하도록 하겠습니다. 로컬 환경의 DB 구성 방법은 <a href="https://velog.io/@ryan-son/VaporSwift-Local%EC%97%90%EC%84%9C-PostgreSQL-DB-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0">이 포스팅</a>을 참고해주세요. </p>
<h1 id="crud-기능-구현">CRUD 기능 구현</h1>
<h2 id="controller-파일-생성-및-타입-작성">Controller 파일 생성 및 타입 작성</h2>
<p>먼저 클라이언트측에서 Request를 보내면 이를 처리할 메인 로직을 담당해줄 <code>Controller</code> 타입을 만듭니다. <code>App</code> 폴더 안에 <code>Controllers</code>라는 폴더를 만들고 <code>&lt;SchemaName&gt;Controller.swift</code> 파일을 생성해주세요. 저는 <code>ProjectItemController.swift</code>를 만들었습니다.</p>
<p>아래와 같이 <code>Fluent</code>와 <code>Vapor</code>를 import 해주세요.</p>
<pre><code class="language-swift">import Fluent
import Vapor</code></pre>
<p>이제 파일 이름과 동일한 구조체를 만들어보겠습니다. 이 컨트롤러는 <code>routes.swift</code>를 도와 묶음으로 일을 처리해주는 타입이므로 <code>RouteCollection</code> 프로토콜을 채택해줍니다.</p>
<p><code>RouteCollection</code> 프로토콜은 아래와 같이 <code>boot(routes:)</code> 메서드 구현을 요구하는데, 이 메서드는 이후 과정에서 <code>routes.swift</code>의 <code>routes(_:)</code>가 <code>register(collection:)</code>을 실행하여 컨트롤러를 사용해줄겁니다. 구조가 이해되시죠? 컨트롤러에서 이를 구현하지 않는다면 <code>routes(_:)</code> 함수 안에 컨트롤러의 모든 업무를 정의해주어야 할 것입니다. 아주 비대한 함수를 만들게 되겠지요. 지금은 <code>boot(routes:)</code> 메서드의 body를 빈 코드블럭으로 남겨주세요.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/9d73246b-ea35-414d-b5cf-983c1e50fdf6/image.png" alt=""></p>
<p>이 과정까지 완료하시면 아래와 같은 구조가 되었을 것입니다.</p>
<pre><code class="language-swift">struct ProjectItemController: RouteCollection {
    func boot(routes: RoutesBuilder) throws { }
}</code></pre>
<h2 id="create">Create</h2>
<p>초기 구성이 끝났으니 Controller 타입 안에 HTTP <code>POST</code> 메서드에 대응하는 Create 메서드를 작성해보겠습니다. 이전 포스팅에서 작성한 이니셜라이저를 이용해서 <code>PostProjectItem</code> 타입으로 디코딩된 인스턴스로부터 <code>ProjectItem</code> 타입의 인스턴스를 만들어서 저장해주시고 해당 내용을 response body로 반환해주시면 됩니다.</p>
<pre><code class="language-swift">    func create(req: Request) throws -&gt; EventLoopFuture&lt;ProjectItem&gt; {
        let exist = try req.content.decode(PostProjectItem.self)
        let newProjectItem = ProjectItem(exist)

        return newProjectItem.save(on: req.db).map{ (result) -&gt; ProjectItem in
            return newProjectItem
        }
    }</code></pre>
<p>여기에서 반환하실 때 <code>save(on:)</code> 메서드를 <code>create(on:)</code> 메서드로 대체하실 수 있습니다. <code>save(on:)</code> 메서드는 데이터베이스에 ID가 존재하지 않을 때는 <code>create(on:)</code> 메서드로, 존재할 때는 <code>update(on:)</code> 메서드로 작동하는 메서드입니다.</p>
<p>요청에 따라 모델 인스턴스를 DB에 생성하면 <code>map(_:)</code> 메서드의 클로저를 통해 response에 담아줄 body를 반환하여 클라이언트가 정상적으로 요청이 처리되었음을 확인할 수 있도록 해줍니다.</p>
<h2 id="read">Read</h2>
<p>다음으로 HTTP <code>GET</code> 메서드에 대응하는 간단한 read용 메서드를 구현해보겠습니다.</p>
<p>먼저 조건 없이 스키마 안의 모든 모델 인스턴스를 반환하는 메서드는 아래와 같이 쿼리 결과를 모두 가져오는 방식으로 작성하시면 됩니다.</p>
<pre><code class="language-swift">func read(req: Request) throws -&gt; EventLoopFuture&lt;[ProjectItem]&gt; {
    return ProjectItem.query(on: req.db).all()
}</code></pre>
<p>저는 클라이언트의 Request에서 <code>Path parameter</code>를 이용해 요청한 parameter가 포함된 모델 인스턴스를 필터하여 제공하는 메서드를 작성해보았습니다.</p>
<pre><code class="language-swift">func read(req: Request) throws -&gt; EventLoopFuture&lt;[ProjectItem]&gt; {
    let validProgress = [&quot;todo&quot;, &quot;doing&quot;, &quot;done&quot;]

    guard let progress = req.parameters.get(&quot;progress&quot;), validProgress.contains(progress) else {
        throw Abort(.badRequest)
    }

    return ProjectItem.query(on: req.db).filter(\.$progress == progress).all()
}</code></pre>
<p>필터 작업은 위와 같은 방식으로 수행하시면 됩니다.</p>
<h2 id="update">Update</h2>
<p>Update는 HTTP <code>PUT</code>과 <code>PATCH</code> 메서드에 대응하는 메서드입니다. 이미 데이터베이스에 존재하는 모델 인스턴스를 대상으로 Request하기 때문에 요청한 내용으로부터 <code>id</code>를 식별하고 DB에서 <code>id</code>를 기준으로 해당 인스턴스를 찾아 수정한 후 업데이트 해줍니다.</p>
<pre><code class="language-swift">func update(req: Request) throws -&gt; EventLoopFuture&lt;ProjectItem&gt; {
    let exist = try req.content.decode(PatchProjectItem.self)

    return ProjectItem.find(exist.id, on: req.db)
        .unwrap(or: Abort(.notFound))
        .flatMap { item in
            if let title = exist.title { item.title = title }
            if let content = exist.content { item.content = content }
            if let progress = exist.progress { item.progress = progress }
            if let deadlineDate = exist.deadlineDate { item.deadlineDate = deadlineDate }
            if let index = exist.index { item.index = index }

            return item.update(on: req.db).map { return item }
        }
}</code></pre>
<h2 id="delete">Delete</h2>
<p>Delete는 HTTP <code>DELETE</code> 메서드에 대응하는 메서드로, 아래와 같이 데이터베이스에 있는 데이터를 삭제할 수 있도록 구성해주시면 됩니다. </p>
<pre><code class="language-swift">func delete(req: Request) throws -&gt; EventLoopFuture&lt;HTTPStatus&gt; {
    guard req.headers.contentType == .json else {
        throw HTTPError.invalidContentType
    }

    let exist = try req.content.decode(DeleteProjectItem.self)
    return ProjectItem.find(exist.id, on: req.db)
        .unwrap(or: HTTPError.invalidID)
        .flatMap { $0.delete(on: req.db) }
        .transform(to: .ok)
}</code></pre>
<h1 id="url-지정-및-crud-메서드-적용">URL 지정 및 CRUD 메서드 적용</h1>
<p>CRUD 기능을 구현했다면 이제 우리 서버의 어떤 URL에 어떤 HTTP 메서드로 접근하면 해당 작업들을 할 수 있는지를 정의해야겠죠? 최초에 Controller를 만들며 빈 코드블럭으로 남겨두었던 <code>boot(routes:)</code> 메서드를 이용하면 이 작업을 하실 수 있습니다.</p>
<p>먼저 메서드가 전달인자로 받은 <code>routes</code>는 <code>RoutesBuilder</code> 타입으로, 내부의 <code>grouped(_:)</code> 메서드로 <code>https://some-server-name.domain-name.com/somePath</code>의 형태로 경로를 지정해주실 수 있고, <code>group(_:)</code> 메서드를 통해 Path parameter를 설정해주실 수 있습니다. 아래 예시를 보시죠.</p>
<pre><code class="language-swift">func boot(routes: RoutesBuilder) throws {
    // https://some-server-name.domain-name.com/projectItems
    let projectItems = routes.grouped(&quot;projectItems&quot;)
    // https://some-server-name.domain-name.com/projectItems/:progress
    // :progress에 작성되는 내용은 path parameter 형식으로 서버에 전달됨
    projectItems.group(&quot;:progress&quot;) { projectItem in
        // 위의 URL로 get 요청 가능
        projectItem.get(use: read)
    }
    // https://some-server-name.domain-name.com/projectItem
    let projectItem = routes.grouped(&quot;projectItem&quot;)
    // 위의 URL로 post, patch, delete 요청 가능
    projectItem.post(use: create)
    projectItem.patch(use: update)
    projectItem.delete(use: delete)
}</code></pre>
<p>위의 예시를 도식으로 나타내면 아래와 같습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/d321a438-81ff-4951-b5a3-71cc6ac19a26/image.png" alt=""></p>
<h1 id="route에-controller-등록">route에 Controller 등록</h1>
<p>이제 각 HTTP 메서드를 통해 요청할 수 있는 URL을 구성(route)했으니 이 컨트롤러를 <code>routes.swift</code>에 있는 <code>routes(_:)</code> 메서드에 등록만 하면 서버가 정상 작동될 것입니다.</p>
<pre><code class="language-swift">// routes.swift
import Vapor
import Fluent

func routes(_ app: Application) throws {
    try app.register(collection: ProjectItemController())
}</code></pre>
<h1 id="기능-확인">기능 확인</h1>
<p>기능 확인을 하기 전에 아래 사항들이 잘 적용되어 있는지 확인해주세요.</p>
<h2 id="사전-작업">사전 작업</h2>
<ol>
<li>로컬 PostgreSQL을 사용하도록 DB 구성 (<code>configure.swift</code>) - <a href="https://velog.io/@ryan-son/VaporSwift-Local%EC%97%90%EC%84%9C-PostgreSQL-DB-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0">관련 포스팅</a>
해당 포스팅의 <code>등록 (Register)</code>과 같은 내용으로 작성되어 있으면  됩니다.</li>
<li>추가적인 모델 타입, 마이그레이션 타입 필드 변경이 있었다면 마이그레이션 작업 완료 - 위 관련 포스팅에서 revert 후 migration</li>
<li>Controller의 <code>boot(routes:)</code> 메서드 완성 및 <code>routes.swift</code> 내 <code>routes(_:)</code> 메서드에 컨트롤러 등록 (본 포스팅)</li>
<li>Postman과 같이 Request를 보낼 수 있는 툴 준비</li>
<li>Xcode vapor 프로젝트 Run (command + R)</li>
</ol>
<h2 id="기능-검증">기능 검증</h2>
<h3 id="create-1">Create</h3>
<ol>
<li><code>POST</code> 방식으로 요청할 수 있도록 옵션 선택 </li>
<li>Request 송신 툴의 URL에 <code>http://localhost:8080/projectItem</code>과 같이 <code>POST</code> 요청이 가능한 URL을 작성</li>
<li>Body에 JSON을 담아 보낼 수 있도록 설정</li>
<li>Body 내용 작성</li>
<li>요청 송신</li>
</ol>
<p><img src="https://images.velog.io/images/ryan-son/post/bacdedc5-5367-4b12-a6a1-28cfdd6bd690/image.png" alt=""></p>
<p>Response의 HTTP Status Code가 200 번대인지, 서버가 요청한대로 응답하였는지 확인해보세요.</p>
<h3 id="read-1">Read</h3>
<ol>
<li><code>GET</code> 방식으로 요청할 수 있도록 옵션 선택 </li>
<li>Request 송신 툴의 URL에 <code>http://localhost:8080/projectItems/doing</code>과 같이 <code>GET</code> 요청이 가능한 URL을 작성</li>
<li>요청 송신</li>
</ol>
<p><img src="https://images.velog.io/images/ryan-son/post/9e75f68e-4b80-4f6c-af98-4dd13479a5fb/image.png" alt=""></p>
<h3 id="update-1">Update</h3>
<p>Create와 동일한 방식으로 요청을 만들어주되 몇 가지 파라미터는 의도적으로 제외하고 요청을 송신해봅시다.</p>
<ol>
<li><code>PATCH</code> 방식으로 요청할 수 있도록 옵션 선택 </li>
<li>Request 송신 툴의 URL에 <code>http://localhost:8080/projectItem</code>과 같이 <code>PATCH</code> 요청이 가능한 URL을 작성</li>
<li>Body에 JSON을 담아 보낼 수 있도록 설정</li>
<li>Body 내용 작성</li>
<li>요청 송신</li>
</ol>
<p><img src="https://images.velog.io/images/ryan-son/post/55837731-bd24-40a4-9b2a-ab9967af8801/image.png" alt=""></p>
<p>POST 요청과 마찬가지로 Response의 HTTP Status Code가 200 번대인지, 서버가 요청한대로 응답하였는지 확인해보세요.</p>
<h3 id="delete-1">Delete</h3>
<p>삭제 작업은 DB에 등록된 해당 아이템의 ID만 제공해주면 삭제 작업이 가능하도록 DTO를 작성했었습니다.</p>
<ol>
<li><code>DELETE</code> 방식으로 요청할 수 있도록 옵션 선택 </li>
<li>Request 송신 툴의 URL에 <code>http://localhost:8080/projectItem</code>과 같이 <code>DELETE</code> 요청이 가능한 URL을 작성</li>
<li>Body에 JSON을 담아 보낼 수 있도록 설정</li>
<li>Body 내용 작성</li>
<li>요청 송신</li>
</ol>
<p><img src="https://images.velog.io/images/ryan-son/post/ef279455-647f-43b4-a773-24418b2a0bfa/image.png" alt=""></p>
<p>Response의 HTTP Status Code가 200 번대인지 확인해보세요.</p>
<h1 id="다음은">다음은?</h1>
<p>이번 시간에는 CRUD 기능을 구현해보았습니다. 하지만 아직 DB를 사용할 수 있게끔 인터페이스만 구성한 상황인데요, 그래서 다음 포스팅에서는 <code>Validatable</code> 프로토콜을 이용해 클라이언트가 보낸 Body 데이터를 검증하는 방법을 알아보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Vapor/Swift] 모델에 DTO 추가하기]]></title>
            <link>https://velog.io/@ryan-son/VaporSwift-%EB%AA%A8%EB%8D%B8%EC%97%90-DTO-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ryan-son/VaporSwift-%EB%AA%A8%EB%8D%B8%EC%97%90-DTO-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 07 Jul 2021 07:37:10 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/df06961e-83d0-4497-9613-a79196c1b149/image.png" alt=""></p>
<p>모델과 관련된 지난 포스팅 두 편(<a href="https://velog.io/@ryan-son/VaporSwift-Fluent-%EB%AA%A8%EB%8D%B8-%ED%83%80%EC%9E%85-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0">이론</a>, <a href="https://velog.io/@ryan-son/VaporSwift-%EC%9D%B8%EC%BD%94%EB%94%A9-%EB%B0%8F-%EB%94%94%EC%BD%94%EB%94%A9%EC%9D%84-%EC%9C%84%ED%95%9C-%EB%AA%A8%EB%8D%B8-%ED%83%80%EC%9E%85-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0">적용</a>)을 통해서 앞으로 클라이언트와 서버 간의 Body 데이터를 주고 받는 양식을 Data Transfer Object (DTO)를 통해 정의할 수 있다고 말씀 드렸었습니다! 단적으로 말해 아래와 같은 상황들이 있을 수 있겠죠.</p>
<blockquote>
<ol>
<li>클라이언트: 저희는 DB에 있는 내용을 삭제하고 싶을 뿐이에요. ID 값만 드리면 되지 않나요?</li>
<li>서버: 이 필드값은 서버에서 데이터 관리용으로 사용하는 값이라 클라이언트가 받아가는 내용에서 제외해도 되는데.</li>
</ol>
</blockquote>
<p>이런 경우에 DTO를 활용해서 주고 받을 Body 데이터의 모양을 설정하실 수 있습니다. 그럼 계속해서 Fluent  프레임워크를 이용해 DTO를 작성하는 방법을 자세히 알아봅시다.</p>
<h1 id="data-transfer-object-dto">Data Transfer Object (DTO)</h1>
<p>DTO는 인코드, 디코드하고 싶은 데이터 구조를 나타내는 별도의 <code>Codable</code> 타입을 의미합니다. 예를 들어 아래와 같이 <code>User</code>라는 모델이 있다고 가정해 보겠습니다.</p>
<pre><code class="language-swift">// Abridged user model for reference.
final class User: Model {
    @ID(key: .id)
    var id: UUID?

    @Field(key: &quot;first_name&quot;)
    var firstName: String

    @Field(key: &quot;last_name&quot;)
    var lastName: String
}</code></pre>
<p><code>PATCH</code> 요청을 처리하는 경우에 흔히 DTO를 사용하게 됩니다. <code>PATCH</code> 요청에는 업데이트가 필요한 필드에 대해서만 요청에 포함시켜 보내기 때문이죠. 이 때 <code>User</code> 모델을 디코딩 타입으로 설정하면 몇 가지의 필드가 빠졌다고 하며 요청이 실패하게 될겁니다. 아래 예시를 통해 모델을 업데이트 할 때 요청 데이터를 디코딩할 때 사용할 DTO를 확인하실 수 있습니다.</p>
<pre><code class="language-swift">// Structure of PATCH /users/:id request.
struct PatchUser: Decodable {
    var firstName: String?
    var lastName: String?
}

app.patch(&quot;users&quot;, &quot;:id&quot;) { req in 
    // Decode the request data.
    let patch = try req.content.decode(PatchUser.self)
    // Fetch the desired user from the database.
    return User.find(req.parameters.get(&quot;id&quot;), on: req.db)
        .unwrap(or: Abort(.notFound))
        .flatMap 
    { user in
        // If first name was supplied, update it.
        if let firstName = patch.firstName {
            user.firstName = firstName
        }
        // If new last name was supplied, update it.
        if let lastName = patch.lastName {
            user.lastName = lastName
        }
        // Save the user and return it.
        return user.save(on: req.db)
            .transform(to: user)
    }
}</code></pre>
<p>DTO를 사용하는 또 다른 경우 중에 하나로 위의 2 번 상황과 같이 API 응답 형식을 정의해주고 싶을 때가 있습니다. 2 번에서는 특정 내용을 제외하고 보내고 싶다고 하였지만, 아래 예시에서는 <code>firstName</code>과 <code>lastName</code>을 조합하여 <code>name</code>이라는 하나의 키 값으로 응답하고 있음을 확인하실 수 있습니다.</p>
<pre><code class="language-swift">// Structure of GET /users response.
struct GetUser: Content {
    var id: UUID
    var name: String
}

app.get(&quot;users&quot;) { req in 
    // Fetch all users from the database.
    User.query(on: req.db).all().flatMapThrowing { users in
        try users.map { user in
            // Convert each user to GET return type.
            try GetUser(
                id: user.requireID(),
                name: &quot;\(user.firstName) \(user.lastName)&quot;
            )
        }
    }
}</code></pre>
<p>비록 DTO의 구조가 모델의 <code>Codable</code> 만족에 따른 모양과 동일하더라도 별도의 타입으로 만들어 두면 대형 프로젝트를 수행할 때 깔끔한 구조를 유지하실 수 있으실 것입니다. 이렇게 구조를 마련해두시면 모델의 프로퍼티를 변경해야 하는 경우에도 앱의 공용 API를 손상시킬 염려가 없게 됩니다. API 이용자 (클라이언트)와 공유할 수 있는 별도의 패키지에 DTO를 넣는 것도 고려해 볼 수 있습니다.</p>
<p>이러한 이유들로 인해 대형 프로젝트의 경우에는 가능한 경우 DTO를 되도록이면 사용할 것을 권장 드립니다.</p>
<h1 id="적용">적용</h1>
<p>기존 프로젝트에 아래와 같이 모델 타입에 DTO들을 추가해보았습니다. 타입 이름은 HTTP 메서드 이름을 기준으로 지어주었습니다. 만든 DTO들을 타입별로 파일을 분리해준다면 프로젝트를 처음 접하는 사람들도 타입의 위치를 찾기 더 쉬울 것 같습니다. 위에서 설명드리지 않았지만 한 가지 추가된 내용은 <code>PostProjectItem</code> 타입을 통해 디코딩된 인스턴스 입력으로 <code>ProjectItem</code> 타입의 인스턴스를 생성할 수 있는 이니셜라이저를 하나 작성해 주었다는 것입니다.</p>
<pre><code class="language-swift">import Fluent
import Vapor

struct PostProjectItem: Content {
    let id: UUID?
    let title: String
    let content: String
    let deadlineDate: Date
    let progress: String
    let index: Int
}

struct PatchProjectItem: Decodable {
    let id: UUID
    let title: String?
    let content: String?
    let deadlineDate: Date?
    let progress: String?
    let index: Int?
}

struct DeleteProjectItem: Decodable {
    let id: UUID
}

final class ProjectItem: Model, Content {
    static let schema = &quot;projectItems&quot;

    @ID(key: .id)
    var id: UUID?

    @Field(key: &quot;title&quot;)
    var title: String

    @Field(key: &quot;content&quot;)
    var content: String

    @Field(key: &quot;deadlineDate&quot;)
    var deadlineDate: Date

    @Field(key: &quot;progress&quot;)
    var progress: String

    @Field(key: &quot;index&quot;)
    var index: Int

    init() { }

    init(id: UUID? = nil, title: String, content: String, deadlineDate: Date, progress: String, index: Int) {
        self.id = id
        self.title = title
        self.content = content
        self.deadlineDate = deadlineDate
        self.progress = progress
        self.index = index
    }

    init(_ projectItem: PostProjectItem) {
        self.id = projectItem.id
        self.title = projectItem.title
        self.content = projectItem.content
        self.deadlineDate = projectItem.deadlineDate
        self.progress = projectItem.progress
        self.index = projectItem.index
    }
}</code></pre>
<p>이제 담당하시는 프로젝트에서 DTO를 작성하실 수 있으시겠죠? 다음 포스팅에서는 만들어 둔 DTO를 이용해 대망의 Create, Read, Update, Delete (CRUD) 기능을 함께 구현해 보겠습니다!</p>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://docs.vapor.codes/4.0/fluent/model/#data-transfer-object">Vapor Docs - Fluent - Model</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>