<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>기록하레이</title>
    <link>https://kybeen.tistory.com/</link>
    <description>공부하거나 삽질한 내용을 정리하는 블로그입니다.</description>
    <language>ko</language>
    <pubDate>Sat, 15 Aug 2026 02:55:31 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>kybeen</managingEditor>
    <image>
      <title>기록하레이</title>
      <url>https://tistory1.daumcdn.net/tistory/5319108/attach/fb0fb4918f1b422cbc5223f505f67b4b</url>
      <link>https://kybeen.tistory.com</link>
    </image>
    <item>
      <title>[ iOS ] Keychain 사용해보기</title>
      <link>https://kybeen.tistory.com/151</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Keychain&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플에서 사용자 데이터를 안전하게 저장할 수 있도록 제공하는 &lt;b&gt;암호화된 데이터베이스&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Keychain Service API&lt;/b&gt;를 사용하면 키체인 안에 사용자 데이터를 저장할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안전하게 저장해야 하는 작은 데이터를 저장할 때 사용 &lt;span style=&quot;color: #666666;&quot;&gt;- &lt;i&gt;&lt;span style=&quot;text-align: left;&quot;&gt;ex) 비밀번호, 암호키, 인증키, 메모 등&lt;/span&gt;&lt;/i&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;SandBox 밖에 데이터가 저장되기 때문에 앱을 삭제해도 데이터가 남아있음&lt;/li&gt;
&lt;li&gt;디바이스가 Lock/Unlock되면 키체인도 같이 Lock/Unlock됨&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://support.apple.com/ko-kr/guide/security/sec59b0b31ff/web&quot;&gt;Secure Enclave&lt;/a&gt;라는 암호화 기능을 사용하는 하드웨어에서 키체인 데이터를 관리함&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/xcode/configuring-keychain-sharing&quot;&gt;Keychain Sharing&lt;/a&gt;을 통해 동일한 개발자에 속한 서로 다른 앱 간 키체인 공유가 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;키체인1.png&quot; data-origin-width=&quot;951&quot; data-origin-height=&quot;503&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PViZa/btsHRmzci6E/VTg1mfkacI96tKZhxTFFsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PViZa/btsHRmzci6E/VTg1mfkacI96tKZhxTFFsk/img.png&quot; data-alt=&quot;출처 : 애플 공식 문서&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PViZa/btsHRmzci6E/VTg1mfkacI96tKZhxTFFsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPViZa%2FbtsHRmzci6E%2FVTg1mfkacI96tKZhxTFFsk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;577&quot; height=&quot;305&quot; data-filename=&quot;키체인1.png&quot; data-origin-width=&quot;951&quot; data-origin-height=&quot;503&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : 애플 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  UserDefaults와 비교&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Keychain은 &lt;a href=&quot;https://developer.apple.com/documentation/foundation/userdefaults&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;UserDefaults&lt;/a&gt;와 대체로 유사하지만 다른 점이 몇 가지 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;UserDefaults로 저장되는 데이터는 plist에 저장되기 때문에 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;보안에 취약&lt;/b&gt;&lt;/span&gt;함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;UserDefaults에 저장된 데이터는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;앱 삭제 시 같이 삭제&lt;/b&gt;&lt;/span&gt;됨&lt;/li&gt;
&lt;li&gt;UserDefaults는 앱이 시작될 때 plist 파일이 로드되어 싱글톤 형태로 접근하기 때문에, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;데이터가 클 수록 로드 시 성능 저하가 발생&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;UserDefaults는 App Group을 통해 Extension 간 데이터 공유가 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  그렇기 때문에 유저의 로그인 정보와 같이 민감한 정보들은 UserDefaults가 아닌 Keychain에 저장해주는 것이 좋습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Keychain Item&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;키체인에 데이터를 저장할 때는 &lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;Keychain Item&lt;/b&gt;&lt;/span&gt;이라는 형태로 저장하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;즉, Keychain Item은 &lt;u&gt;키체인에 저장하는 Data의 단위&lt;/u&gt;라고 할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비밀번호, 암호화 키 같은 정보를 키체인에 저장하고 싶을 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Keychain Item&lt;/b&gt;으로 패키징&lt;/li&gt;
&lt;li&gt;Keychain Item으로 패키징하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #6164c6;&quot;&gt;&lt;b&gt;데이터&amp;nbsp;자체(data)&lt;/b&gt;&lt;/span&gt; 뿐만 아니라, item의 접근성을 제어하고 검색할 수 있도록&lt;span&gt;&amp;nbsp;&lt;/span&gt;public하게 볼 수 있는 &lt;span style=&quot;color: #6164c6;&quot;&gt;&lt;b&gt;arribute&lt;/b&gt;&lt;/span&gt;들이 제공됨&lt;/li&gt;
&lt;li&gt;키체인 서비스는 키체인에서 &lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;b&gt;data, attribute&lt;/b&gt; 모두에 대해 &lt;/span&gt;데이터 암호화 및 저장을 처리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;  나중에 승인된 프로세스는 키체인 서비스를 사용하여 item을 찾고, 데이터를 복호화할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;키체인2.png&quot; data-origin-width=&quot;1058&quot; data-origin-height=&quot;319&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pMQU2/btsHQl8Q5BF/tKX5q0iH4ITgmLsfNubYwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pMQU2/btsHQl8Q5BF/tKX5q0iH4ITgmLsfNubYwK/img.png&quot; data-alt=&quot;출처 : 애플 공식 문서&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pMQU2/btsHQl8Q5BF/tKX5q0iH4ITgmLsfNubYwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpMQU2%2FbtsHQl8Q5BF%2FtKX5q0iH4ITgmLsfNubYwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;564&quot; height=&quot;170&quot; data-filename=&quot;키체인2.png&quot; data-origin-width=&quot;1058&quot; data-origin-height=&quot;319&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : 애플 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Keychain Item class의 key-value&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Keychain item은 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;i&gt;password(비밀번호), cryptographic key(암호화 키), certificate(인증서)&lt;/i&gt;&lt;span&gt;&amp;nbsp;등&lt;/span&gt;&lt;/span&gt;과 같이 저장하려는 &lt;u&gt;데이터의 종류에 따라 &lt;b&gt;다양한 class&lt;/b&gt;로&lt;/u&gt; 제공됩니다. &lt;span style=&quot;color: #9d9d9d;&quot;&gt;(Swift 문법적 의미의 class가 아님)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;item이 어떤 class인지에 따라서 &lt;span style=&quot;color: #6164c6;&quot;&gt;&lt;b&gt;적용 가능한 attribute&lt;/b&gt;&lt;/span&gt;와, &lt;span style=&quot;color: #6164c6;&quot;&gt;&lt;b&gt;시스템을 통한 데이터의 암호화 여부&lt;/b&gt;&lt;/span&gt; 등이 결정됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키체인에서 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;item에 대한&lt;/span&gt; CRUD 작업이 이루어질 때는 딕셔너리 형태로 쿼리를 만들어줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 내용은 아래에서 더 설명할 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 이 쿼리를 생성할 때 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;i&gt;&lt;b&gt;kSecClass&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; 라는 key에 대한 value로 &lt;b&gt;5가지 class 중 하나를 지정&lt;/b&gt;해줍니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿼리 딕셔너리에서 &lt;b&gt;item class를 지정하기 위한 key&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1717668955460&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// item의 class를 value로 갖는 딕셔너리 Key
let kSecClass: CFString&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿼리 딕셔너리에서&lt;b&gt; item class의 value&lt;/b&gt;로 줄 수 있는 값 (&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;i&gt;&lt;b&gt;kSecClass&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; key의 value)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1717668960545&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// generic password item을 가리키는 값
let kSecClassGenericPassword: CFString

// Internet password item을 가리키는 값
let kSecClassInternetPassword: CFString

// certificate item을 가리키는 값
let kSecClassCertificate: CFString

// cryptographic key item을 가리키는 값
let kSecClassKey: CFString

// identity item을 가리키는 값
let kSecClassIdentity: CFString&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Keychain Item attribute의 key-value&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Keychain Item에는 저장할 데이터 뿐만 아니라 &lt;b&gt;attribute&lt;/b&gt;들도 포함된다고 위에서 설명드렸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;attribute를 통해 데이터를 나중에 검색할 수 있도록 하거나, 데이터가 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;사용&amp;middot;공유되는 방식 등을 제어할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;  item class에 따라 적용 가능한 attribute가 다르기 때문에 사용하려는 class에 맞게 attribute를 구성할 수 있어야 합니다.&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/security/ksecclassgenericpassword&quot;&gt;kSecClassGenericPassword&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/security/ksecclassinternetpassword&quot;&gt;kSecClassInternetPassword&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/security/ksecclasscertificate&quot;&gt;kSecClassCertificate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/security/ksecclassidentity&quot;&gt;kSecClassIdentity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/security/ksecclasskey&quot;&gt;kSecClassKey&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Kechain CRUD 메서드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키체인에서 keychain item의 &lt;u&gt;CRUD 작업 시에는 아래 4가지 메서드를 사용&lt;/u&gt;합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1717669389864&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// MARK: - Create: 키체인에 비밀정보 추가하기
func SecItemAdd(_ attributes: CFDictionary, _ result: UnsafeMutablePointer&amp;lt;CFTypeRef?&amp;gt;?) -&amp;gt; OSStatus

// MARK: - Read: 키체인에 저장된 Keychain item 찾기
func SecItemCopyMatching(_ query: CFDictionary, _ result: UnsafeMutablePointer&amp;lt;CFTypeRef?&amp;gt;?) -&amp;gt; OSStatus

// MARK: - Update: 키체인에 저장된 정보 변경하기
func SecItemUpdate(_ query: CFDictionary, _ attributesToUpdate: CFDictionary) -&amp;gt; OSStatus

// MARK: - Delete:  키체인에 저장된 정보 삭제하기
func SecItemDelete(_ query: CFDictionary) -&amp;gt; OSStatus&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  결과 처리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 메서드들은 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;i&gt;&lt;b&gt;OSStatus&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;라는 타입으로 결과에 대한 값을 반환하는데, 해당 타입은 정수 형식으로 되어 있기 때문에 값을 그냥 출력하는 것으로는 어떤 에러인지 제대로 파악하기 힘듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;print() 시에 아래 함수를 사용하면 &lt;u&gt;유의미한 에러 메시지를 확인 가능&lt;/u&gt;합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1717669680394&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SecCopyErrorMessageString(status, nil)&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Keychain을 통해 User Secrets 정보를 관리하는 예시&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/security/keychain_services/keychain_items/using_the_keychain_to_manage_user_secrets&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;애플 공식 문서&lt;/a&gt;를 따라가며 실제로 Keychain을 어떻게 사용하여 유저의 민감한 정보를 관리할 수 있는지 확인해봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키체인 서비스는 암호화된 저장소로 쉽게 접근할 수 있도록 해주기 때문에 &lt;b&gt;최소한의 상호작용으로 좋은 사용자 경험을 제공할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감이 잘 오지 않을 수도 있으니 사용 예시로 확인해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아래 다이어그램은 &lt;i&gt;&lt;b&gt;인터넷 비밀번호를 Keychain Item으로 저장하는 경우의 플로우&lt;/b&gt;&lt;/i&gt;입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;키3.png&quot; data-origin-width=&quot;1128&quot; data-origin-height=&quot;436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/47ONQ/btsHPWPi9X0/vwkAaqmWLcm870EoZUYaLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/47ONQ/btsHPWPi9X0/vwkAaqmWLcm870EoZUYaLK/img.png&quot; data-alt=&quot;출처 : 애플 공식 문서&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/47ONQ/btsHPWPi9X0/vwkAaqmWLcm870EoZUYaLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F47ONQ%2FbtsHPWPi9X0%2FvwkAaqmWLcm870EoZUYaLK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;661&quot; height=&quot;255&quot; data-filename=&quot;키3.png&quot; data-origin-width=&quot;1128&quot; data-origin-height=&quot;436&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : 애플 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서에서 키체인 사용 시 좋은 사용자 경험을 위해 3가지를 준수하라고 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;필요할 때만 사용자를 참여시킬 것 (CREATE)&lt;/b&gt;&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;일반적인 상황에서 사용자를 괴롭히지 말 것 (READ)&lt;/b&gt;&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;변화를 깔쌈하게 처리할 것 (UPDATE, DELETE)&lt;/b&gt;&lt;/i&gt;&lt;i&gt;&lt;b&gt;&lt;/b&gt;&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각을 어떻게 준수해야 하는지 위 다이어그램의 흐름을 예시로 따라가며 설명하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  필요할 때만 사용자를 참여시킬 것&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CREATE&lt;/b&gt;와 관련된 내용입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처음에 앱이 credential(자격증명)을 필요로 할 때는 password가 키체인에 저장되어 있지 않은 상태이기 때문에 다이어그램의 오른쪽 흐름과 같이 앱이 사용자에게 인증을 요청합니다.&lt;/li&gt;
&lt;li&gt;사용자가 인증 후 credential을 제공하면 앱은 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;i&gt;&lt;b&gt;SecItemAdd(_:_:)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; 함수를 호출하여 credential을 키체인에 저장하게 됩니다.&lt;/li&gt;
&lt;li&gt;이후 앱은 정기적인 네트워크 접속을 계속하면서, 나중에 서버가 재인증을 요구하게 되는 순간 사용자 대신 키체인에서 credential을 검색하여 제공해줄 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;544&quot; data-origin-height=&quot;550&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bstWan/btsHPVQtGce/6VQrhBYZc0BdC4kRFHeY11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bstWan/btsHPVQtGce/6VQrhBYZc0BdC4kRFHeY11/img.png&quot; data-alt=&quot;다이어그램의 오른쪽 흐름&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bstWan/btsHPVQtGce/6VQrhBYZc0BdC4kRFHeY11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbstWan%2FbtsHPVQtGce%2F6VQrhBYZc0BdC4kRFHeY11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;242&quot; height=&quot;245&quot; data-origin-width=&quot;544&quot; data-origin-height=&quot;550&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;다이어그램의 오른쪽 흐름&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;일반적인 상황에서 사용자를 괴롭히지 말 것&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;READ&lt;/b&gt;와 관련된 내용입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;color: #333333;&quot;&gt;다이어그램 상에서 가장 일반적인 경로는 사용자 상호 작용이 필요하지 않은 중앙 부분입니다.&lt;/li&gt;
&lt;li style=&quot;color: #333333;&quot;&gt;예를 들면 사용자가 잠시 자리를 비운 후 앱을 다시 시작하게 될 경우, 정기적인 재인증이 필요할 수 있습니다.&lt;/li&gt;
&lt;li style=&quot;color: #333333;&quot;&gt;이런 경우 앱은 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;i&gt;&lt;b&gt;SecItemCopyMatching(_:_:)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; 함수를 사용하여 키체인에서 password를 검색하게 됩니다.&lt;/li&gt;
&lt;li style=&quot;color: #333333;&quot;&gt;키체인에서 password가 발견되어 앱이 성공적으로 인증하게 되면, 사용자가 귀찮은 재인증 과정을 직접 수행할 필요 없이 계속 앱 사용이 가능해집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;586&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dRvcrq/btsHQKUXEMc/KZc6sy1ShJ7Yrdbd41kwuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dRvcrq/btsHQKUXEMc/KZc6sy1ShJ7Yrdbd41kwuK/img.png&quot; data-alt=&quot;다이어그램의 중앙 흐름&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dRvcrq/btsHQKUXEMc/KZc6sy1ShJ7Yrdbd41kwuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdRvcrq%2FbtsHQKUXEMc%2FKZc6sy1ShJ7Yrdbd41kwuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;286&quot; height=&quot;239&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;586&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;다이어그램의 중앙 흐름&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;br /&gt;  &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;변화를 쌈@뽕&lt;span style=&quot;color: #9d9d9d;&quot;&gt;(Gracefully...의역)&lt;/span&gt;하게 처리할 것&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UPDATE&lt;/b&gt;와 관련된 내용입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떨 때는 사용자가 앱의 통제 범위 밖에서 credential을 변경하게 될 수 있습니다. &lt;i&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;ex) 앱과 동일한 서비스의 웹 인터페이스를 통한 비밀번호 변경&amp;middot;재설정&lt;/span&gt;&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;이런 상황이 발생하고 나면 앱이 keychain item을 검색했을 때, 이전 password를 가져오게 되고 인증에 실패하게 됩니다.&lt;/li&gt;
&lt;li&gt;그러면 앱은 다이어그램의 오른쪽 흐름과 같이 사용자에게 새로운 인증을 요청하게 되고, 새 credential을 검증한 뒤에 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;i&gt;&lt;b&gt;SecItemUpdate(_:_:)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; 함수를 호출하여 키체인에 있는 기존 정보를 수정하게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DELETE&lt;/b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;와 관련된 내용입니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그아웃과 같이 사용자가 네트워크 서비스와 완전히 연결을 끊기로 결정할 경우에, 앱은 로그아웃 처리와 함께 credential도 제거해줘야 합니다.&lt;/li&gt;
&lt;li&gt;이 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;i&gt;&lt;b&gt;SecItemDelete(_:)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수를 사용하여 키체인에서 password를 제거해줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;478&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Y4njm/btsHRwhtMvU/ElbJfTwHcav4vodYNyXNc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Y4njm/btsHRwhtMvU/ElbJfTwHcav4vodYNyXNc0/img.png&quot; data-alt=&quot;다이어그램의 왼쪽 흐름&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Y4njm/btsHRwhtMvU/ElbJfTwHcav4vodYNyXNc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FY4njm%2FbtsHRwhtMvU%2FElbJfTwHcav4vodYNyXNc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;293&quot; height=&quot;289&quot; data-origin-width=&quot;478&quot; data-origin-height=&quot;472&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;다이어그램의 왼쪽 흐름&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;예시 코드로 확인하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 위에서 설명했던 흐름을 코드로 작성해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Keychain에 password item 추가(저장)하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 앱 상의 메모리에 저장할 &lt;b&gt;Credential에 대한 구조체를 정의&lt;/b&gt;해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 username과 password 프로퍼티를 넣어줬습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;Credentials의 내용들을 키체인에 저장하고, 키체인에서 읽어올 예정입니다.&lt;/u&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717684765331&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct Credentials {
    var username: String
    var password: String
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키체인 접근 시 에러 처리를 위한 &lt;b&gt;에러 타입을 정의&lt;/b&gt;해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1717684890880&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;enum KeychainError: Error {
    case noPassword
    case unexpectedPasswordData
    case unhandledError(status: OSStatus)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이 사용하는 서버의 주소를 static 상수로 정의해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1717684956130&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static let server = &quot;www.example.com&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;i&gt;&lt;b&gt;  서버 주소가 필요한 이유?&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;u&gt;keychain item을 저장할 때, &lt;b&gt;account(계정)&lt;/b&gt;과 &lt;b&gt;server(서버)&lt;/b&gt;에 대한 내용을 저장할 수 있는 &lt;i&gt;&lt;b&gt;attribute&lt;/b&gt;&lt;/i&gt;가 제공&lt;/u&gt;됩니다. 해당 attribute를 쿼리에 같이 작성해줌으로써 저장할 password에 대한 부수적인 정보들도 같이 넣어줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;이러한 attribute들을 잘 추가해서 나중에 키체인 아이템을 확실하게 검색할 수 있도록 구성해주어야 합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 내용은 아래 쿼리 작성 부분에서 설명드리겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Add 쿼리 만들기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Credentials 구조체의 인스턴스와 위에서 정의해준 server 상수를 사용해서 add query를 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리는 키체인 CRUD 함수에서 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;i&gt;&lt;b&gt;CFDictionary&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; 형태로 변환되어야 하기 때문에 딕셔너리로 만들어줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 설명한 것처럼 keychain item의 여러 &lt;u&gt;attribute 중 Account와 Server에 대한 것들을 쿼리에 포함&lt;/u&gt;시켜줬습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;i&gt;&lt;b&gt;kSecAttrAccount&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; : &lt;b&gt;계정에 대한 정보&lt;/b&gt;로 &lt;b&gt;Credentials의 username&lt;/b&gt; 값을 넣어줍니다. 이렇게 하면 username과 password를 나눠서 저장할 필요가 없어집니다. 하나의 keychain item에 username과 password가 모두 들어가게 되는 것이죠.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;i&gt;&lt;b&gt;kSecAttrServer&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; : 해당 password가 관리되는 서버 정보를 명시하는데, 서버의 주소를 넣을 수 있습니다. 예시 코드에서는 실제 서버를 사용하지 않기 때문에 임의의 주소를 넣어줬습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 keychain item의 &lt;u&gt;class를 지정하기 위한 attribute&lt;/u&gt;와, &lt;u&gt;저장하고자 하는 데이터에 대한 attribute&lt;/u&gt;를 추가해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장하는 데이터는 반드시 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;i&gt;&lt;b&gt;Data&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; 타입이어야 하기 때문에 String 값을 Data로 인코딩해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1717685672537&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// KEY 역할
let account = credentials.username
// VALUE 역할 - data 타입이어야 한다.
let password = credentials.password.data(using: String.Encoding.utf8)!

// add 쿼리
let query: [String: Any] = [
    // item class 종류
    kSecClass as String: kSecClassInternetPassword,

    // 저장하고자 하는 정보
    kSecAttrAccount as String: account, // item의 계정 이름을 나타냄
    kSecAttrServer as String: server, // item의 서버를 나타냄

    // Data 인스턴스로 인코딩된 password
    kSecValueData as String: password // item의 데이터
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;i&gt;&lt;b&gt;kSecClass&lt;/b&gt;&lt;/i&gt;로 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;i&gt;&lt;b&gt;kSecClassInternetPassword&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;를 지정해줬는데, 이와 비슷한&amp;nbsp;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;i&gt;&lt;b&gt;kSecClassGenericPassword&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;도 있습니다. generic password는 internet password와 거의 비슷하지만, &lt;i&gt;&lt;b&gt;kSecAttrServer&lt;/b&gt;&lt;/i&gt; 같이 원격 액세스와 관련된 특정 attribute가 없습니다. 해당 예시에서는 &lt;i&gt;&lt;b&gt;kSecAttrServer&lt;/b&gt;&lt;/i&gt;를 키체인 아이템 검색 시 식별자처럼 사용할 예정입니다. 그래서 internet password를 사용하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;kSecAttrServer&lt;/b&gt;&lt;/i&gt;가 없어도, &lt;i&gt;&lt;b&gt;kSecAttrAccount&lt;/b&gt;&lt;/i&gt;에 저장을 위한 값이 아닌 식별자로 사용할 값을 넣어줘도 됩니다. 그렇게 할 경우네는 internet password 말고 generic password를 사용해도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 포스팅할 수도 있지만 실제 진행 중인 프로젝트에서 저는 &lt;i&gt;&lt;b&gt;kSecClassGenericPassword&lt;/b&gt;&lt;/i&gt; class의 keychain item을 저장하면서 &lt;i&gt;&lt;b&gt;kSecAttrAccount&lt;/b&gt;&lt;/i&gt;는 저장할 데이터를 나타내는 key값으로 활용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;  ex) 파이어베이스 cloud function을 통한 원격 푸시 알림 사용 시 필요한 device token 값을 저장&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717686150539&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: &quot;deviceToken&quot;,
    kSecValueData as String: deviceTokenValue
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Keychain item 추가(ADD)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성된 add 쿼리를 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;i&gt;&lt;b&gt;SecItemAdd(_:_:)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; 함수에 넣어 호출해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;쿼리는 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;i&gt;&lt;b&gt;CFDictionary&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;로 캐스팅&lt;/u&gt;해줘야 하고, &lt;u&gt;2번째 인자로 리턴 data를 참조할 수 있지만 무시해도 됩니다&lt;/u&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;함수의 반환 결과는 반드시 확인해서 작업의 성공 여부에 따른 처리를 할 수 있어야 합니다&lt;/b&gt;.&lt;/span&gt; add 작업은 쿼리와 동일한 구성의 item이 이미 존재한다면 실패할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1717686248143&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
    throw KeychainError.unhandledError(status: status)
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;br /&gt;  Keychain item 검색하기&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위에서 작성한 것과 같이 쿼리 딕셔너리를 사용해 키체인 아이템을 검색할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쿼리 딕셔너리는 키체인 서비스에 어떤 item attribute를 찾아야 하고, item이 발견되면 무엇을 반환해야 하는지 알려줄 수 있어야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 검색 쿼리 딕셔너리에는 검색 옵션을 매개변수로 지정해줄 수 있습니다. &lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;i&gt;ex) 대소문자 구분 제어, 매칭되는 항목 수 제한 등...&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Search 쿼리 만들기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;add 쿼리 때와 비슷하게 작성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;검색 옵션을 위한 attribute&lt;/b&gt;들도 추가로 구성해줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시에서는 &lt;u&gt;매칭되는 결과를 1개로 제한하는 옵션&lt;/u&gt;과, &lt;u&gt;검색된 item의 attribute와 data를 모두 받아오도록&lt;/u&gt; 설정해줬습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1717686836734&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let query: [String: Any] = [
    kSecClass as String: kSecClassInternetPassword,

    // 일종의 key의 역할 - server attribute가 매치되는 item을 찾게 됨
    kSecAttrServer as String: server,

    kSecMatchLimit as String: kSecMatchLimitOne, // result를 하나의 value로 한정짓는다.(원래 디폴트값임)

    // attribute와 data 둘 다 요청
    kSecReturnAttributes as String: true,
    kSecReturnData as String: true
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;i&gt;&lt;b&gt;kSecMatchLimit&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; 검색 파라미터로 결과를 단일 값으로 제한해줍니다. 이렇게 하면 키체인에서 첫 번째로 매칭된 아이템만 받아오게 됩니다. (1개로 제한하지 않을 경우에는 배열 형식으로 반환)&lt;/li&gt;
&lt;li&gt;이 쿼리는 위의 add 예시에서 password item을 추가할 때 사용한 &lt;u&gt;&lt;b&gt;server attribute&lt;/b&gt;와 매치되는 키체인 아이템을 찾게 됩니다&lt;/u&gt;. 위에서 설명했지만 server로 지정한 값을 검색 시 일종의 식별자로 사용하는 것이죠.&lt;/li&gt;
&lt;li&gt;마지막으로 아이템의 &lt;b&gt;attribute와 data를 모두 요청&lt;/b&gt;합니다. 왜냐하면 &lt;u&gt;&lt;b&gt;kSecAttrAccount&lt;/b&gt;attribute로 username 값도 저장했기 때문에&lt;/u&gt; 이를 불러오기 위함입니다. &lt;u&gt;data에는 password를 저장했으니 당연히 요청&lt;/u&gt;해야겠죠.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Keychain item 검색(SEARCH)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;작성된 search 쿼리를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;i&gt;&lt;b&gt;SecItemCopyMatching(_:_:)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수에 넣어 호출해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;add 때와 마찬가지로, 반환된 &lt;u&gt;status를 통해 검색 결과를 체크하여 에러를 핸들링&lt;/u&gt;해줍니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;함수의 2번째 파라미터를 통해 반환되는 결과를 참조&lt;/b&gt;할 수 있습니다.&lt;/li&gt;
&lt;li&gt;정상적으로 검색이 완료되면 &lt;b&gt;결과 Data를 통해 원하는 값을 추출&lt;/b&gt;해줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1717687214500&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &amp;amp;item)

guard status != errSecItemNotFound else {
    throw KeychainError.noPassword
}
guard status == errSecSuccess else {
    throw KeychainError.unhandledError(status: status)
}

// 검색 시 1개의 결과만 요청했으므로 단일 딕셔너리로 옴
guard let existingItem = item as? [String: Any],
      let passwordData = existingItem[kSecValueData as String] as? Data,
      let password = String(data: passwordData, encoding: .utf8),
      let account = existingItem[kSecAttrAccount as String] as? String
else {
    throw KeychainError.unexpectedPasswordData
}

let credentials = Credentials(username: account, password: password)&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;br /&gt;  Keychain item 업데이트/삭제 하기&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;keychain item을 업데이트 하려면, 먼저 검색을 해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서&amp;nbsp;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;키체인 아이템의 업데이트 작업은 암시적으로 검색을 먼저 수행&lt;/b&gt;&lt;/span&gt;하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 업데이트 시에는 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;search 쿼리&lt;/b&gt;&lt;/span&gt;와 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;update 쿼리&lt;/b&gt;&lt;/span&gt;, &lt;b&gt;총 2개의 쿼리가 필요&lt;/b&gt;합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Search 쿼리 만들기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717687636044&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 업데이트 작업 수행 시에 암시적으로 검색을 먼저 해야 하기 때문에 검색할때와 같은 형식으로 쿼리 생성
let query: [String: Any] = [
    kSecClass as String: kSecClassInternetPassword,
    kSecAttrServer as String: server
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Update 쿼리 만들기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업데이트하고자 하는 내용들을 딕셔너리로 지정해줍니다. item의 data 뿐만 아니라 attribute도 업데이트 가능합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1717687672957&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 업데이트할 내용에 대한 쿼리 생성
let account = newCredentials.username
let password = newCredentials.password.data(using: .utf8)!
let attributes: [String: Any] = [
    kSecAttrAccount as String: account,
    kSecValueData as String: password
]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Update 수행&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;검색 쿼리와 새로운 attributes를 사용해서 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;i&gt;&lt;b&gt;SecItemUpdate(_:_:)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;함수를 호출해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;함수 호출이 성공하면 제공한 attribute에 따라 일치하는 모든 item을 수정하게 됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717687815462&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
guard status != errSecItemNotFound else {
    throw KeychainError.noPassword
}
guard status == errSecSuccess else {
    throw KeychainError.unhandledError(status: status)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Delete 수행&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;item을 삭제하는 것은 검색용 쿼리 딕셔너리 하나만 필요하다는 점을 제외하면 &lt;b&gt;업데이트와 매우 비슷&lt;/b&gt;합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Update에서 사용한 것과 동일한 쿼리를 사용해서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;i&gt;&lt;b&gt;SecItemDelete(_:)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;함수를 호출해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;업데이트와 비슷하게 키체인 서비스는 검색 파라미터와 일치하는 모든 키체인 항목을 삭제합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1717687992165&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 삭제할 item을 검색하기 위한 쿼리
let query: [String: Any] = [
    kSecClass as String: kSecClassInternetPassword,
    kSecAttrServer as String: server
]

let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
    throw KeychainError.unhandledError(status: status)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  테스트 앱&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://github.com/kybeen/KeychainSample&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/kybeen/KeychainSample&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1717688221564&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - kybeen/KeychainSample: Keychain 공부용 샘플 앱&quot; data-og-description=&quot;Keychain 공부용 샘플 앱. Contribute to kybeen/KeychainSample development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/kybeen/KeychainSample&quot; data-og-url=&quot;https://github.com/kybeen/KeychainSample&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bqVXAQ/hyWgYR9yRk/KJmZV22gkzztjiibec5OWK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/kybeen/KeychainSample&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/kybeen/KeychainSample&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bqVXAQ/hyWgYR9yRk/KJmZV22gkzztjiibec5OWK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - kybeen/KeychainSample: Keychain 공부용 샘플 앱&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Keychain 공부용 샘플 앱. Contribute to kybeen/KeychainSample development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래처럼 간단하게 키체인의 CRUD 작업을 테스트할 수 있는 앱을 만들어봤습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전체 코드는 좀 길기 때문에 깃허브 링크를 참고해주세요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Simulator Screen Recording - iPhone 15 Pro - 2024-06-07 at 00.35.20.gif&quot; data-origin-width=&quot;295&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l1Ilm/btsHQ0cbnJG/zZsiC6oLRfmCorXtalJ2l1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l1Ilm/btsHQ0cbnJG/zZsiC6oLRfmCorXtalJ2l1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l1Ilm/btsHQ0cbnJG/zZsiC6oLRfmCorXtalJ2l1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/l1Ilm/btsHQ0cbnJG/zZsiC6oLRfmCorXtalJ2l1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;295&quot; height=&quot;501&quot; data-filename=&quot;Simulator Screen Recording - iPhone 15 Pro - 2024-06-07 at 00.35.20.gif&quot; data-origin-width=&quot;295&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;레퍼런스&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/foundation/userdefaults&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/documentation/foundation/userdefaults&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/security/keychain_services/keychain_items/using_the_keychain_to_manage_user_secrets&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/documentation/security/keychain_services/keychain_items/using_the_keychain_to_manage_user_secrets&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/security/keychain_services&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/documentation/security/keychain_services&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/security/keychain_services/keychain_items/adding_a_password_to_the_keychain&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/documentation/security/keychain_services/keychain_items/adding_a_password_to_the_keychain&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/security/keychain_services/keychain_items/searching_for_keychain_items&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/documentation/security/keychain_services/keychain_items/searching_for_keychain_items&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/security/keychain_services/keychain_items/updating_and_deleting_keychain_items&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/documentation/security/keychain_services/keychain_items/updating_and_deleting_keychain_items&lt;/a&gt;&lt;/p&gt;</description>
      <category>iOS</category>
      <category>ios keychain</category>
      <category>keychain</category>
      <category>keychain item</category>
      <category>swift keychain</category>
      <category>Swift 키체인</category>
      <category>userdefaults keychain</category>
      <category>키체인</category>
      <category>키체인 값 저장</category>
      <author>kybeen</author>
      <guid isPermaLink="true">https://kybeen.tistory.com/151</guid>
      <comments>https://kybeen.tistory.com/151#entry151comment</comments>
      <pubDate>Fri, 7 Jun 2024 00:38:12 +0900</pubDate>
    </item>
    <item>
      <title>[ iOS ] String에서 숫자 찾기 &amp;amp; 패턴 찾기</title>
      <link>https://kybeen.tistory.com/150</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;사이드 프로젝트 개발을 하면서 문자열 String이 숫자를 포함하고 있는지 여부를 알고 싶었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kybeen.tistory.com/149&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글&lt;/a&gt;에서 실시간 버스의 도착 정보에 대한 메세지가 총 4가지 종류로 제공된다고 했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #353638; text-align: left;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;i&gt;&lt;b&gt;1️⃣ &quot;곧 도착&quot;&lt;/b&gt;&lt;/i&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;i&gt;&lt;b&gt;2️⃣ &quot;00분00초후[0번째 전]&quot;&lt;/b&gt;&lt;/i&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;i&gt;&lt;b&gt;3️⃣ &quot;운행종료&quot;&lt;/b&gt;&lt;/i&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;i&gt;&lt;b&gt;4️⃣ &quot;출발대기&quot;&lt;/b&gt;&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 각각의 응답 메세지 형식에 따라서 아래와 같은 enum 타입으로 분류해준다고 했었죠.&lt;/p&gt;
&lt;pre id=&quot;code_1717053578111&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;enum ArrivalStatus: Comparable {
    case arriveSoon // 곧 도착
    case coming(remainingSecond: Int) // 운행중(남은시간(초))
    case waiting // 출발대기
    case finished // 운행종료
    case unknown // 알 수 없음
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 적절한 &lt;u&gt;ArrivalStatus로 변환할 수 있도록 &lt;/u&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;u&gt;String의 내용을 분석&lt;/u&gt;해줄 필요가 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2️⃣의 경우에는 유일하게 &lt;b&gt;문자열에 숫자가 포함&lt;/b&gt;됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;b&gt;숫자가 포함된다면 문자열에서 &lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;i&gt;00분00초&lt;/i&gt;&lt;/span&gt;의 패턴을 추출하여 남은 시간을 구해줘야 합니다&lt;/b&gt;. (.coming 케이스의 연관값으로 전달하기 위해)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 아래와 같이 동작하는 로직을 설계하게 되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2020&quot; data-origin-height=&quot;810&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3RVns/btsHGYUM3g0/Wqccv2O1oBkyF1Krwk6XD0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3RVns/btsHGYUM3g0/Wqccv2O1oBkyF1Krwk6XD0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3RVns/btsHGYUM3g0/Wqccv2O1oBkyF1Krwk6XD0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3RVns%2FbtsHGYUM3g0%2FWqccv2O1oBkyF1Krwk6XD0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2020&quot; height=&quot;810&quot; data-origin-width=&quot;2020&quot; data-origin-height=&quot;810&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 로직 구현이 필요한 부분은&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;숫자 포함 여부 확인&lt;/b&gt;&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;&quot;00분00초&quot; &amp;rarr; 패턴 추출&lt;/b&gt;&lt;/i&gt;&lt;i&gt;&lt;b&gt;&lt;/b&gt;&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 2가지가 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;각각의 로직을 어떻게 구현했는지 설명드리겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  String의 숫자 포함 여부 확인&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자 포함 여부 확인에 저는 String의 &lt;a href=&quot;https://developer.apple.com/documentation/foundation/nsstring/1411461-rangeofcharacter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;rangeOfCharacter&lt;span style=&quot;color: #000000;&quot;&gt;(&lt;/span&gt;from&lt;span style=&quot;color: #000000;&quot;&gt;:options:range:&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/foundation/nsstring/1411461-rangeofcharacter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;)&lt;/a&gt; 메서드를 활용했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Foundation 프레임워크를 임포트해야 사용 가능)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1616&quot; data-origin-height=&quot;664&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u6YUm/btsHIXfkYOn/We1kaXDrbAkLyR1KXlEW10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u6YUm/btsHIXfkYOn/We1kaXDrbAkLyR1KXlEW10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u6YUm/btsHIXfkYOn/We1kaXDrbAkLyR1KXlEW10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu6YUm%2FbtsHIXfkYOn%2FWe1kaXDrbAkLyR1KXlEW10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;685&quot; height=&quot;281&quot; data-origin-width=&quot;1616&quot; data-origin-height=&quot;664&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 메서드는 &lt;b&gt;파라미터로 설정한 옵션에 따라 문자열을 탐색하며, 문자열의 주어진 범위 내에서 주어진 문자 집합에 해당하는 범위를 구해줍니다.&lt;/b&gt; &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;가장 먼저 발견한 곳의 range만 반환&lt;/b&gt;&lt;/span&gt;하게 되며, 반환 타입이 옵셔널이기 때문에 매칭되는 곳이 없으면 nil을 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파라미터는 from을 제외하고는 필수가 아니며, 저는 from 파라미터만 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 파라미터 용도는 이렇습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;from&lt;/b&gt;&lt;/i&gt; : 검색하고자 하는 문자를 나타내는 CharacterSet&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;options&lt;/b&gt;&lt;/i&gt; : 문자열 검색 옵션 (검색 방향 등을 설정할 수 있습니다.)&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;range&lt;/b&gt;&lt;/i&gt; : 검색할 범위 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1618&quot; data-origin-height=&quot;654&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z27eM/btsHIcYAGG9/epviT7yDSUbPb8DYCqXys0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z27eM/btsHIcYAGG9/epviT7yDSUbPb8DYCqXys0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z27eM/btsHIcYAGG9/epviT7yDSUbPb8DYCqXys0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz27eM%2FbtsHIcYAGG9%2FepviT7yDSUbPb8DYCqXys0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1618&quot; height=&quot;654&quot; data-origin-width=&quot;1618&quot; data-origin-height=&quot;654&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어쨋든 해당 String 내부에 지정한 CharacterSet에 매칭되는 곳이 있다면 그 부분의 range값이 반환될 것이고, 저는 &lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;반환되는 값이 있는지 없는지&lt;/span&gt;만 확인하면 숫자 포함 여부를 구할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;from 파라미터로 숫자에 대한 CharacterSet을 넘겨줘야 하는데, 저는 타입 프로퍼티로 기본적으로 제공되는 &lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;i&gt;&lt;b&gt;.decimalDigits&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;를 사용했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EXv68/btsHH8oq2lm/8sUs8Z4Uh9OnhilaN27XD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EXv68/btsHH8oq2lm/8sUs8Z4Uh9OnhilaN27XD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EXv68/btsHH8oq2lm/8sUs8Z4Uh9OnhilaN27XD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEXv68%2FbtsHH8oq2lm%2F8sUs8Z4Uh9OnhilaN27XD1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;630&quot; height=&quot;63&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;138&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 사용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2028&quot; data-origin-height=&quot;302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/X1iuV/btsHHL1jqPv/srSRMf5dNL5kSDvkB9MECK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/X1iuV/btsHHL1jqPv/srSRMf5dNL5kSDvkB9MECK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/X1iuV/btsHHL1jqPv/srSRMf5dNL5kSDvkB9MECK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FX1iuV%2FbtsHHL1jqPv%2FsrSRMf5dNL5kSDvkB9MECK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;705&quot; height=&quot;105&quot; data-origin-width=&quot;2028&quot; data-origin-height=&quot;302&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 String 익스텐션에 이렇게 메서드를 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;rangeOfCharacter의 결과가 nil인지 아닌지만 확인하고, Bool값으로 반환&lt;/b&gt;해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1717055851427&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// String이 숫자를 포함하는지 여부를 반환하는 메서드
func isContainsNumber() -&amp;gt; Bool {
    return self.rangeOfCharacter(from: .decimalDigits) != nil
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  &lt;span&gt;&lt;/span&gt;String에서 특정 패턴 추출&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 숫자 포함 여부는 확인할 수 있게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 문자열에서 특정 패턴을 추출해야 합니다. 저의 경우는 &lt;i&gt;&lt;b&gt;&quot;00분&quot;&lt;/b&gt;&lt;/i&gt;과 &lt;i&gt;&lt;b&gt;&quot;00초&quot;&lt;/b&gt;&lt;/i&gt;의 패턴을 찾아서 분&amp;middot;초 값을 숫자로 뽑아내야 했죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 먼저 ChatGPT 선생님께 도움을 요청했고, 받은 코드를 다듬어서 String 익스텐션으로 아래의 메서드를 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;베끼기만 하는건 도움이 되지 않으니 어떻게 동작하는지를 살펴봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1717056085516&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 서울시 버스 도착 정보의 BusArrival에 있는 arrivalMessage로부터 남은 시간을 구해서 Int타입의 초 값으로 반환하는 메서드
func getSeoulBusRemainingSecond() -&amp;gt; Int {
    let minutePattern = &quot;(\\d+)분&quot;
    let secondPattern = &quot;(\\d+)초&quot;

    guard let minuteRegex = try? NSRegularExpression(pattern: minutePattern) else { return 0 }
    guard let secondRegex = try? NSRegularExpression(pattern: secondPattern) else { return 0 }

    var minutes = 0
    var seconds = 0

    // 분 추출
    if let minuteMatch = minuteRegex.firstMatch(in: self, range: NSRange(location: 0, length: count)) {
        if let range = Range(minuteMatch.range(at: 1), in: self) {
            minutes = Int(self[range]) ?? 0
        }
    }

    // 초 추출
    if let secondMatch = secondRegex.firstMatch(in: self, range: NSRange(location: 0, length: count)) {
        if let range = Range(secondMatch.range(at: 1), in: self) {
            seconds = Int(self[range]) ?? 0
        }
    }

    return (minutes * 60) + seconds
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &quot;O분&quot;과 &quot;O초&quot; 에 대한 정규식 패턴을 정의해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1717056145810&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let minutePattern = &quot;(\\d+)분&quot;
let secondPattern = &quot;(\\d+)초&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 패턴이 의미하는 바는 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;\\d&lt;/b&gt; : &quot;\d&quot;는 숫자 하나(digit)를 나타내는 정규표현식입니다. &quot;\&quot;는 정규표현식에서 특수한 의미를 갖기 때문에 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;백슬래시를 2개 사용해서 &lt;/span&gt;이스케이핑 처리를 해줬습니다.   &lt;b&gt;숫자 하나&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;+&lt;/b&gt; : 앞의 패턴이 하나 이상 반복될 수 있음을 의미합니다. 앞의 \\d패턴이 하나 이상 반복될 수 있다는 의미가 되겠죠.   &lt;b&gt;하나 이상의 숫자가 올 수 있음&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;()&lt;/b&gt; : 캡처 그룹을 의미하며, 요구하는 숫자 패턴과 매칭되는 부분을&amp;nbsp;묶음처리한다고 생각해주면 됩니다.&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 이 정규표현식과 매칭되는 패턴은 이렇습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;i&gt;하나 이상의 숫자 바로 뒤에 &quot;분&quot;이나 &quot;초&quot;라는 문자가 있음&lt;/i&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;i&gt;ex) 15분, 3분, 40초, 3초...&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 정규표현식을 사용하기 위해 NSRegularExpression 객체를 생성해줍니다. 앞서 정의한 패턴을 파라미터로 넘겨주고, 잘못된 정규표현식을 넘길 경우 에러가 발생하기 때문에 NSRegularExpression 객체 생성 시에는 try문을 사용해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1717057583336&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;guard let minuteRegex = try? NSRegularExpression(pattern: minutePattern) else { return 0 }
guard let secondRegex = try? NSRegularExpression(pattern: secondPattern) else { return 0 }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 각각의 패턴을 찾아서 &lt;i&gt;&lt;b&gt;분&lt;/b&gt;&lt;/i&gt;과 &lt;i&gt;&lt;b&gt;초&lt;/b&gt;&lt;/i&gt;를 추출합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;NSRegularExpression의 firstMatch(in:options:range:)&lt;/b&gt;&lt;/i&gt; 메서드를 사용해줍니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; letter-spacing: 0px;&quot;&gt;이 메서드는 문자열에서 정규표현식 패턴과 일치하는 첫번째 문자열을 반환해줍니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;i&gt;&lt;b&gt;minuteMatch.range(at: 1)&lt;/b&gt;&lt;/i&gt;는 캡처 그룹 1에 해당하는 범위를 가져오게 되는데,&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;정규표현식&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;(\\d+)분&quot;&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;에서&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;숫자 부분&amp;nbsp;\d+&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;를 나타냅니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;마지막으로 매칭된 부분을 Range 객체로 변환하고, 문자열의 서브스트링에서 Int값을 추출해줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1717057736938&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var minutes = 0
var seconds = 0

// 분 추출
if let minuteMatch = minuteRegex.firstMatch(in: self, range: NSRange(location: 0, length: count)) {
    if let range = Range(minuteMatch.range(at: 1), in: self) {
        minutes = Int(self[range]) ?? 0
    }
}

// 초 추출
if let secondMatch = secondRegex.firstMatch(in: self, range: NSRange(location: 0, length: count)) {
    if let range = Range(secondMatch.range(at: 1), in: self) {
        seconds = Int(self[range]) ?? 0
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 아래처럼 사용할 수 있게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1717059256244&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Foundation

let arrivalMessage1 = &quot;14분30초후[6번째전]&quot;
let arrivalMessage2 = &quot;1분4초후[1번째전]&quot;

extension String {
    func getSeoulBusRemainingSecond() -&amp;gt; Int {
        let minutePattern = &quot;(\\d+)분&quot;
        let secondPattern = &quot;(\\d+)초&quot;
        
        guard let minuteRegex = try? NSRegularExpression(pattern: minutePattern) else { return 0 }
        guard let secondRegex = try? NSRegularExpression(pattern: secondPattern) else { return 0 }
        
        var minutes = 0
        var seconds = 0
        
        // 분 추출
        if let minuteMatch = minuteRegex.firstMatch(in: self, range: NSRange(location: 0, length: count)) {
            if let range = Range(minuteMatch.range(at: 1), in: self) {
                minutes = Int(self[range]) ?? 0
            }
        }
        
        // 초 추출
        if let secondMatch = secondRegex.firstMatch(in: self, range: NSRange(location: 0, length: count)) {
            if let range = Range(secondMatch.range(at: 1), in: self) {
                seconds = Int(self[range]) ?? 0
            }
        }
        
        return (minutes * 60) + seconds
    }
}

arrivalMessage1.getSeoulBusRemainingSecond() // 870
arrivalMessage2.getSeoulBusRemainingSecond() // 64&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실제 활용 코드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제로 프로젝트에서 사용한 코드를 보여드리겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버스 도착 정보 메세지를 받아서 위의 메서드들을 활용하여 결과를 ArrivalStatus 타입으로 반환해주었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1717059308744&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 버스의 도착 상태를 구해서 BusArrivalStatus 타입 값을 반환하는 메서드
func getBusArrivalStatusFromSeoulBusStation(arrivalMessage: String) -&amp;gt; ArrivalStatus {
    if arrivalMessage.contains(&quot;출발대기&quot;) {
        return .waiting
    } else if arrivalMessage.contains(&quot;운행종료&quot;) {
        return .finished
    } else if arrivalMessage.contains(&quot;곧 도착&quot;) {
        return .arriveSoon
    } else if arrivalMessage.isContainsNumber() {
        return .coming(remainingSecond: arrivalMessage.getSeoulBusRemainingSecond())
    } else {
        return .unknown
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;레퍼런스&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tngusmiso.tistory.com/62&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://tngusmiso.tistory.com/62&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://borabong.tistory.com/32&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://borabong.tistory.com/32&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>iOS</category>
      <category>nsregularexpression</category>
      <category>rangeofcharacter</category>
      <category>swift 문자열 숫자 포함</category>
      <category>swift 문자열 패턴 추출</category>
      <category>swift 정규표현식</category>
      <author>kybeen</author>
      <guid isPermaLink="true">https://kybeen.tistory.com/150</guid>
      <comments>https://kybeen.tistory.com/150#entry150comment</comments>
      <pubDate>Thu, 30 May 2024 17:57:09 +0900</pubDate>
    </item>
    <item>
      <title>[ iOS ] Comparable 프로토콜을 사용하여 열거형 값을 비교하기</title>
      <link>https://kybeen.tistory.com/149</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;사이드 프로젝트를 진행하면서&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;대중교통을 통해 갈 수 있는 경로들을 보여주면서, 각각의 경로에 대한 실시간 도착정보가 제공되도록 하려고 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 &lt;u&gt;지하철과 버스의 실시간 도착 정보 API&lt;/u&gt;를 사용하게 되었고, 각각의 API를 통해 &lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;도착까지 남은 시간을 초 단위의 Int값&lt;/b&gt;&lt;/span&gt;으로 받아와서 앱 상에서는 남은 시간을 실시간으로 카운트하며 표시해주려고 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 도착 정보에 대한 데이터 타입은 단순하게&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;남은 시간(초)를 나타내는 Int값&lt;/span&gt;&lt;/b&gt;이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;지하철 도착 정보 API&lt;/b&gt;의 경우 도착 정보로 도착까지 남은 시간을 초 단위의 숫자값으로 제공했기 때문에 Int타입으로 쉽게 변환하여 사용할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;버스 도착 정보 API&lt;/b&gt;는 조금 달랐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 API의 응답 데이터에서 &lt;span style=&quot;color: #ee2323;&quot;&gt;도착까지 얼마나 남았는지를 알 수 있는 정보는 아래의 도착 메세지가 유일&lt;/span&gt;했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 데이터는 문자열 값으로 제공되었고, 이 문자열을 가공해서 남은 시간을 추출해야 했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1717002088708&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;arrmsg1&quot;: &quot;11분54초후[4번째 전]&quot;, // 첫번째 도착예정버스의 도착정보메세지
&quot;arrmsg2&quot;: &quot;출발대기&quot;, // 두번째 도착예정버스의 도착정보메세지&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;처음에는 단순하게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;u&gt;&lt;i&gt;&quot;00분00초후&quot;&lt;/i&gt;형식의 메세지에서남은 시간을 Int 형식의 값&lt;/u&gt;으로 뽑아오면 되겠지 싶었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 API를 여러번 호출해보며 확인한 결과, 이 도착 메세지 형식의 종류는 크게 4가지로 나뉘어진다는 것을 알게 되었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;&quot;곧 도착&quot;&lt;/b&gt;&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;&quot;00분00초후[0번째 전]&quot;&lt;/b&gt;&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;&quot;운행종료&quot;&lt;/b&gt;&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;&quot;출발대기&quot;&lt;/b&gt;&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 도착 정보로 남은 시간의 Int값만 사용할 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;i&gt;&quot;곧 도착&quot;&lt;/i&gt; 형식의 응답은 정확하게 남은 시간을 카운트할 수 없었습니다.&lt;/span&gt; 도착까지 1분 미만의 시간이 남았을 때 &lt;i&gt;&quot;곧 도착&quot;&lt;/i&gt; 메세지가 표시되는 것으로 보여졌기 때문에 임의로 60초가 남았다고 할 수도 없었고, 그냥 곧 도착한다는 메세지를 보여주는 것이 제일 좋아보였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;i&gt;&quot;운행종료&quot;&lt;/i&gt;와 &lt;i&gt;&quot;출발대기&quot;&lt;/i&gt; 형식으로&amp;nbsp;응답이 왔을 때에 대한 처리도 할 수 없었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 도착까지 남은 시간만 활용하기에는 무리가 있었고, 도착 정보를 표현하기 위한 타입을 아래와 같이 새로 정의하게 되었습니다.&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;남은시간을 숫자로 계산할 수 있는 경우에는 숫자값을 활용하지만, 그렇지 않은 경우에는 별도의 처리를 할 수 있도록 열거형 타입을 정의했습니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717004290525&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;enum ArrivalStatus: Comparable {
    case arriveSoon // 곧 도착
    case coming(remainingSecond: Int) // 운행중(남은시간(초))
    case waiting // 출발대기
    case finished // 운행종료
    case unknown // 알 수 없음
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이 타입을 보시면 열거형 타입이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Comparable 프로토콜&lt;/b&gt;&lt;/span&gt;을 채택한 것을 알 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Comparable 프로토콜&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Comparable 프로토콜은 이름대로 대소비교를 할 수 있도록 만들어주는 프로토콜입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 프로토콜을 채택하게 되면 커스텀 타입에서도 &lt;b&gt;&lt;i&gt;&amp;lt;, &amp;gt;, &amp;lt;=, &amp;gt;=&lt;/i&gt; 연산자를 통한 대소비교&lt;/b&gt;가 가능해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swift의 기본 타입인 Int, Double, String 등도 이 프로토콜을 채택하고 있었기 때문에 대소비교가 가능한 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구조체나 클래스 같은 복합적인 타입&lt;/b&gt;에서는 &lt;u&gt;비교의 기준을 무엇으로 할 지 명확하지 않기 때문에&lt;/u&gt;, Comparable 프로토콜 채택 시에 &lt;b&gt;프로토콜 메서드를 구현&lt;/b&gt;해서 기준을 정해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;프로토콜 채택만 하게 되면 에러가 발생합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1290&quot; data-origin-height=&quot;166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUFRcF/btsHF3uTj5s/hKWvEIgqwbTLCwWifHqZWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUFRcF/btsHF3uTj5s/hKWvEIgqwbTLCwWifHqZWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUFRcF/btsHF3uTj5s/hKWvEIgqwbTLCwWifHqZWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUFRcF%2FbtsHF3uTj5s%2FhKWvEIgqwbTLCwWifHqZWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;752&quot; height=&quot;97&quot; data-origin-width=&quot;1290&quot; data-origin-height=&quot;166&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 메세지를 클릭해서 fix를 누르면 &lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;&amp;lt; 메서드&lt;/b&gt;&lt;/span&gt;를 작성하도록 만들어줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;308&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZkWNN/btsHGf9JtqJ/lf866cgrqRBNeZTKYKeHp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZkWNN/btsHGf9JtqJ/lf866cgrqRBNeZTKYKeHp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZkWNN/btsHGf9JtqJ/lf866cgrqRBNeZTKYKeHp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZkWNN%2FbtsHGf9JtqJ%2Flf866cgrqRBNeZTKYKeHp0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;615&quot; height=&quot;204&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;308&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메서드의 내부에 비교 기준을 작성해주면 됩니다. &lt;u&gt;&amp;lt; 메서드 하나만 작성해주면 이를 기준으로 나머지 연산자를 통한 비교도 모두 자동&lt;/u&gt;으로 처리됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짬을 더 많이 먹은 군인이 더 큰 값이 되도록 아래와 같이 구현해주면... 대소비교가 가능해집니다.&lt;/p&gt;
&lt;pre id=&quot;code_1717005794977&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct Soldier: Comparable {
    static func &amp;lt; (lhs: Soldier, rhs: Soldier) -&amp;gt; Bool {
        return lhs.zzam &amp;lt; rhs.zzam
    }
    
    var name = &quot;&quot;
    var zzam = 0
}

let eren = Soldier(name: &quot;에렌예거&quot;, zzam: 10)
let levi = Soldier(name: &quot;리바이&quot;, zzam: 500)
 
eren &amp;lt; levi // true
eren &amp;gt; levi // false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  열거형의 경우&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 열거형의 경우는 조금 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swift 5.3 미만 버전이라면 Comparable을 채택하고 구현도 해줘야 하지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;Swift 5.3 이상의 버전&lt;/u&gt;에서는 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;연관값이 없거나&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; |&lt;/span&gt; &lt;b&gt;연관값이 모두 Comparable 프로토콜을 채택하고 있는 경우&lt;/b&gt;&lt;/span&gt;에는 &lt;b&gt;Comparable을 채택하는 것만으로도 사용이 가능&lt;/b&gt;해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 위에서 제가 정의한 열거형 타입은 따로 구현을 해줄 필요가 없었습니다. 연관값으로 사용되는 건 Int값 하나 뿐이었는데, 이는 이미 프로토콜이 채택되어있기 때문이죠.&lt;/p&gt;
&lt;pre id=&quot;code_1717006210141&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;enum ArrivalStatus: Comparable {
    case arriveSoon // 곧 도착
    case coming(remainingSecond: Int) // 운행중(남은시간(초))
    case waiting // 출발대기
    case finished // 운행종료
    case unknown // 알 수 없음
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  그럼 대소비교의 기준은 어떻게 되냐?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;별다른 구현이 없다면 &lt;span style=&quot;color: #8a3db6;&quot;&gt;열거형 case의 비교 기준은 아래로 갈수록 큰 값&lt;/span&gt;이 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 ArrivalStatus 타입에서는 아래와 같은 대소관계가 성립하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1717006428743&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let a: ArrivalStatus = .arriveSoon // 곧 도착
let b: ArrivalStatus = .coming(remainingSecond: 10) // 10초 남음
let c: ArrivalStatus = .coming(remainingSecond: 16) // 16초 남음
let d: ArrivalStatus = .waiting // 출발대기
let e: ArrivalStatus = .finished // 운행종료
let f: ArrivalStatus = .unknown // 알 수 없음
// a &amp;lt; b &amp;lt; c &amp;lt; d &amp;lt; e &amp;lt; f

print(a&amp;lt;b) // true
print(a&amp;lt;c) // true
print(a&amp;lt;d) // true
print(a&amp;lt;e) // true
print(a&amp;lt;f) // true

print(b&amp;lt;c) // true
print(b&amp;lt;d) // true
print(b&amp;lt;e) // true
print(b&amp;lt;f) // true

print(c&amp;lt;d) // true
print(c&amp;lt;e) // true
print(c&amp;lt;f) // true

print(d&amp;lt;e) // true
print(d&amp;lt;f) // true

print(e&amp;lt;f) // true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✅ 직접 활용한 방식&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 대소관계가 성립하기 때문에 &lt;u&gt;여러 개의 도착 정보가 배열에 들어있을 때&lt;/u&gt;,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;정렬을 통해 가장 도착이 임박한 도착 정보를 맨 앞으로&lt;/span&gt; 가져올 수 있게 됩니다.&lt;/li&gt;
&lt;li&gt;또한 현재 오고 있는 버스나 지하철이 없더라도, &lt;i&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;출발대기&amp;gt;운행종료&amp;gt;알수없음&lt;/span&gt;&lt;/i&gt; 의 우선순위대로 적절한 메세지로 대체할 수 있게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버스의 도착 정보를 받기 위해 사용하는 API는 &lt;a href=&quot;https://www.data.go.kr/data/15000303/openapi.do&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;정류소 정보를 조회하는 API&lt;/b&gt;&lt;/a&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 &lt;b&gt;해당 정류소를 지나는 모든 노선에 대한 정보&lt;/b&gt;가 받아지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버스의 경우 &lt;b&gt;동일한 경로를 통하는 노선이 여러개일 수가 있기 때문에, 여러 노선의 도착 정보 중 도착이 가장 임박한 정보를 보여줄 수 있어야 했습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;i&gt;ex) &lt;span style=&quot;text-align: start;&quot;&gt;&quot;불광역3.6호선&quot; ~ &quot;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;서대문역사거리.농협중앙회&quot;의 동일한 경로를 지나는 버스노선은 총 3개   720, 741, 705&lt;/span&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;span style=&quot;caret-color: #9d9d9d;&quot;&gt;&lt;i&gt;그렇기 때문에 &lt;i&gt;&quot;&lt;i&gt;&lt;span style=&quot;text-align: start;&quot;&gt;불광역3.6호선&lt;/span&gt;&lt;/i&gt;&quot; 정류소를 지나는 모든 노선의 도착 정보에서 720, 741, 705의 것들만 필터링하고 3가지 노선 중 가장 도착이 임박한 정보를 보여줘야 한다.&lt;/i&gt;&lt;/i&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 코드를 상세하게 설명하기에는 복잡하기 때문에 이 부분이 적용되는 코드를 살펴보면&lt;/p&gt;
&lt;pre id=&quot;code_1717007220806&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 서울시 정류소정보조회 API 호출
self.apiService.fetchSeoulRealtimeBusStationInfo(arsID: arsID) { result in
    switch result {
    case .success(let seoulRealtimeBusStationDTO):
        // 노선ID+노선명이 매칭되는 도착 정보들만 필터링
        let filteredArrivals = seoulRealtimeBusStationDTO.arrivals.itemList.filter {
            (routeIDs.contains($0.busRouteID)) &amp;amp;&amp;amp; (routeNames.contains($0.busRouteName))
        }
        // 각 노선들에 대한 도착 정보에서 첫번째, 두번째 도착 메시지를 추출하고, ArrivalStatus의 우선순위가 높은(작은값) 순서대로 정렬
        let firstArrivals = filteredArrivals.map { self.getBusArrivalStatusFromSeoulBusStation(arrivalMessage: $0.arrivalMessage1) }.sorted(by: &amp;lt;)
        let secondArrivals = filteredArrivals.map { self.getBusArrivalStatusFromSeoulBusStation(arrivalMessage: $0.arrivalMessage2) }.sorted(by: &amp;lt;)

        // 가장 가까운 도착 정보를 튜플에 반영
        if let closestFirstArrival = firstArrivals.first {
            realtimeArrival.first = closestFirstArrival
        }
        if let closestSecondArrival = secondArrivals.first {
            realtimeArrival.second = closestSecondArrival
        }
        ...
	...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;정류소정보조회 API를 호출해서 응답을 받아옵니다.&lt;/li&gt;
&lt;li&gt;필요한 노선들의 정보만 필터링하고, 응답 메세지를 ArrivalStatus 타입의 값으로 변환하여 이를 배열로 만들어줍니다.&lt;/li&gt;
&lt;li&gt;sort 메서드로 정렬을 해주게 되면, firstArrivals와 secondArrivals 배열의 맨 앞(first)에 해당하는 원소에는 가장 도착이 임박한 데이터가 오게 되고, 이를 활용해줍니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;레퍼런스&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://babbab2.tistory.com/150&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://babbab2.tistory.com/150&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1717006258163&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Swift) Comparable에 대해 알아보자&quot; data-og-description=&quot;안녕하세요 :) 소들입니다 일요일인데 포스팅 쓰러 카페에 왔습니다!!!!!! 제가 한 3주 정도 현업 때문에 정말 너무너무 바빴어서..;; 사실 풀 재택근무중인데 굳이 회사나가서 새벽까지 개발하면 &quot; data-og-host=&quot;babbab2.tistory.com&quot; data-og-source-url=&quot;https://babbab2.tistory.com/150&quot; data-og-url=&quot;https://babbab2.tistory.com/150&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/crrw45/hyWdlfeFrK/d0KG3s2dtKUTXul0paKyOk/img.png?width=716&amp;amp;height=84&amp;amp;face=0_0_716_84,https://scrap.kakaocdn.net/dn/CZ8SR/hyWdn5bjAw/ZPDcirIPdbPezTgIP5p43k/img.png?width=716&amp;amp;height=84&amp;amp;face=0_0_716_84,https://scrap.kakaocdn.net/dn/h21KR/hyWdlTQ0ot/HDRWgiqnIJjgqMncNIqFk1/img.png?width=1874&amp;amp;height=338&amp;amp;face=0_0_1874_338&quot;&gt;&lt;a href=&quot;https://babbab2.tistory.com/150&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://babbab2.tistory.com/150&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/crrw45/hyWdlfeFrK/d0KG3s2dtKUTXul0paKyOk/img.png?width=716&amp;amp;height=84&amp;amp;face=0_0_716_84,https://scrap.kakaocdn.net/dn/CZ8SR/hyWdn5bjAw/ZPDcirIPdbPezTgIP5p43k/img.png?width=716&amp;amp;height=84&amp;amp;face=0_0_716_84,https://scrap.kakaocdn.net/dn/h21KR/hyWdlTQ0ot/HDRWgiqnIJjgqMncNIqFk1/img.png?width=1874&amp;amp;height=338&amp;amp;face=0_0_1874_338');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Swift) Comparable에 대해 알아보자&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요 :) 소들입니다 일요일인데 포스팅 쓰러 카페에 왔습니다!!!!!! 제가 한 3주 정도 현업 때문에 정말 너무너무 바빴어서..;; 사실 풀 재택근무중인데 굳이 회사나가서 새벽까지 개발하면&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;babbab2.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>iOS</category>
      <category>comparable</category>
      <category>comparable enum</category>
      <category>comparable 열거형</category>
      <category>swift comparable</category>
      <category>swift enum 값 비교</category>
      <category>swift enum 비교</category>
      <category>swift 열거형 비교</category>
      <author>kybeen</author>
      <guid isPermaLink="true">https://kybeen.tistory.com/149</guid>
      <comments>https://kybeen.tistory.com/149#entry149comment</comments>
      <pubDate>Thu, 30 May 2024 03:31:17 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] SwiftUI의 TextField에 Clear Button 추가하기</title>
      <link>https://kybeen.tistory.com/148</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI로 사이드 프로젝트 개발을 하다가 아래 사진처럼 TextField에 입력한 텍스트를 한번에 지울 수 있는 &lt;b&gt;Clear Button&lt;/b&gt;을 추가하려고 했습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;1488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D9Jyh/btsHDWo5lO3/yYAaiqDpiQOcyzIAWZ4ki0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D9Jyh/btsHDWo5lO3/yYAaiqDpiQOcyzIAWZ4ki0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D9Jyh/btsHDWo5lO3/yYAaiqDpiQOcyzIAWZ4ki0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FD9Jyh%2FbtsHDWo5lO3%2FyYAaiqDpiQOcyzIAWZ4ki0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;370&quot; height=&quot;601&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;1488&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;애플의 HIG 문서에서는 Text fields 사용 시,&lt;br&gt;&lt;i&gt;&lt;b&gt;&quot;사람들이&amp;nbsp;입력을&amp;nbsp;지울&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;텍스트&amp;nbsp;필드의&amp;nbsp;마지막&amp;nbsp;끝에&amp;nbsp;지우기&amp;nbsp;버튼을&amp;nbsp;표시하세요.&quot;&lt;/b&gt;&lt;/i&gt;&lt;br&gt;라고 하면서 Clear Button의 추가를 권장하고 있습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1356&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbT6PD/btsHGeH7krB/PPyr8HLpZxuXUmGrkksj11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbT6PD/btsHGeH7krB/PPyr8HLpZxuXUmGrkksj11/img.png&quot; data-alt=&quot;출처 : https://developer.apple.com/design/human-interface-guidelines/text-fields&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbT6PD/btsHGeH7krB/PPyr8HLpZxuXUmGrkksj11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbT6PD%2FbtsHGeH7krB%2FPPyr8HLpZxuXUmGrkksj11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;717&quot; height=&quot;240&quot; data-origin-width=&quot;1356&quot; data-origin-height=&quot;454&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://developer.apple.com/design/human-interface-guidelines/text-fields&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;그렇기 때문에 UIKit의 UITextField에는 clear button 설정과 관련된 옵션이 기본적으로 제공되었고, SwiftUI의 TextField에도 당연히 clear button 관련 옵션이 있을 거라 생각했습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;하지만 찾아보니 &lt;span style=&quot;color: #ee2323;&quot;&gt;SwiftUI에서는 기본적으로 제공되는 clear button이 없었습니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;검색을 해보면 대체로 2가지 방법 중 하나를 사용하는 것으로 보였습니다.&lt;/p&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;&lt;li&gt;&lt;b&gt;UITextField를 가져와서 사용&lt;/b&gt;&lt;/li&gt;&lt;li&gt;&lt;b&gt;ViewModifier를 활용한 커스텀 modifier 만들기&lt;/b&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;각각의 방법을 살펴 보겠습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;일단 기본적인 SwiftUI 뷰를 아래와 같이 구성해주었고,&lt;br&gt;이 뷰를 기반으로 Clear Button을 적용해보겠습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;import SwiftUI

struct ClearButtonSampleView: View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@State private var myText: String = &quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var body: some View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;VStack {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Text(&quot;텍스트를 입력해주세요.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.font(.title)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.bold()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;TextField(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;텍스트 입력&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;text: $myText
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.textFieldStyle(.roundedBorder)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.padding()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;TextField를 그냥 사용한 현재 상태에서는 텍스트를 입력해도 clear button이 적용되지 않습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;1332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ekAzP6/btsHGbSpLGr/iYrW7FfbaILtMVShT5HDL1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ekAzP6/btsHGbSpLGr/iYrW7FfbaILtMVShT5HDL1/img.gif&quot; data-alt=&quot;clear button 적용 ❌&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ekAzP6/btsHGbSpLGr/iYrW7FfbaILtMVShT5HDL1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/ekAzP6/btsHGbSpLGr/iYrW7FfbaILtMVShT5HDL1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;299&quot; height=&quot;617&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;1332&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;clear button 적용 ❌&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1️⃣ UITextField를 가져와서 사용&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;UIKit UITextField의 &lt;i&gt;&lt;b&gt;appearance&lt;/b&gt;&lt;/i&gt; 객체를 통해 &lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;i&gt;&lt;b&gt;clearButtonMode&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; 속성을 지정해주면 SwiftUI의 TextField 사용 시에도 똑같이 clear button이 적용됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;View의 init()이나, .onAppear() 등에 &lt;span style=&quot;color: #333333;&quot;&gt;아래의 코드만&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt; 추가해주면 clear button이 나타나는 것을 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;UITextField.appearance().clearButtonMode = .whileEditing&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;1332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYTu06/btsHECX1YOz/ovNpahI10uSlsCqSKsN1vk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYTu06/btsHECX1YOz/ovNpahI10uSlsCqSKsN1vk/img.gif&quot; data-alt=&quot;clear button 적용 ✅&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYTu06/btsHECX1YOz/ovNpahI10uSlsCqSKsN1vk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bYTu06/btsHECX1YOz/ovNpahI10uSlsCqSKsN1vk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;301&quot; height=&quot;621&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;1332&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;clear button 적용 ✅&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;저 같은 경우는 이니셜라이저에 해당 코드를 넣어주었고, 전체 코드는 아래와 같습니다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;⚙️ 전체 코드 - UITextField를 가져와서 사용&lt;/b&gt;&lt;/h4&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;import SwiftUI

// MARK: - UITextField 사용
struct ClearButtonSampleView: View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@State private var myText: String = &quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;init() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// UITextField의 clearButton 속성을 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;UITextField.appearance().clearButtonMode = .whileEditing
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var body: some View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;VStack {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Text(&quot;텍스트를 입력해주세요.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.font(.title)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.bold()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;TextField(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;텍스트 입력&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;text: $myText
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.textFieldStyle(.roundedBorder)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.padding()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;이 방법을 사용하면 굉장히 간단하게 clear button을 적용할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이는 TextField가 UITextField를 기반으로 구성되어 있기 때문에 가능한 방법이라고 합니다. TextField의 내부 코드까지는 공개되어 있지 않아서 확인할 수는 없었지만 그렇기 때문에 UITextField의 속성을 건드렸는데, TextField에 적용이 되는 것이라고 볼 수 있을 것 같습니다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2️⃣ ViewModifier를 활용한 커스텀 modifier 만들기&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;1️⃣의&amp;nbsp;방법은 아주 간단하고 쉽지만, clear button의 모양과 기능을 직접 커스텀하는 것은 어렵습니다.&lt;br&gt;기본적으로 제공되는 모양과 기능을 그대로 써도 상관 없다면 괜찮지만, 그렇지 않은 경우에는 2️⃣의 방법을 사용하게 됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;두번째 방법은 &lt;a href=&quot;https://developer.apple.com/documentation/swiftui/viewmodifier&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;i&gt;&lt;b&gt;ViewModifier protocol&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;을 활용해 커스텀 modifier를 만들어준 뒤, 이를 TextField에 적용시켜주는 방법입니다.&lt;br&gt;이 방법을 채택할 경우에는 clear button를 통해 더 상세한 동작 제어가 가능해집니다.&lt;br&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;i&gt;ex) clear button을 통해 없앤 텍스트를 다시 복구(undo), 버튼의 외형 커스텀&lt;/i&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;먼저 ViewModifier 프로토콜을 채택하는 ClearButton 구조체를 생성해줍니다. ViewModifier 프로토콜을 채택하고 해당 프로토콜에서 요구하는&amp;nbsp;&lt;i&gt;&lt;b&gt;body(content:)&lt;/b&gt;&lt;/i&gt; 메서드를 구현해주면, 커스텀 modifier가 적용될 content에 접근할 수 있게 됩니다. 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;그 후 some View 타입을 반환하면서 modifier가 적용된 뷰를 사용할 수 있게 됩니다.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;TextField에서 입력한 텍스트를 받기 위한 &lt;i&gt;&lt;b&gt;바인딩 프로퍼티&lt;/b&gt;&lt;/i&gt;도 정의해줍니다.&lt;/li&gt; 
&lt;/ul&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;struct ClearButton: ViewModifier {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Binding var text: String
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;func body(content: Content) -&amp;gt; some View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;이제 &lt;i&gt;&lt;b&gt;body(content:)&lt;/b&gt;&lt;/i&gt; 메서드 내부에 아래와 같이 내용을 작성해줍니다.&lt;/p&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;&lt;li&gt;ZStack으로 &lt;b&gt;content(modifier가 적용될 컴포넌트 - TextField)를 감싸줍니다.&lt;/b&gt;&lt;/li&gt;&lt;li&gt;&lt;b&gt;trailing alignment&lt;/b&gt;를 적용해줍니다. (clear button을 뒤로 배치하기 위해)&lt;/li&gt;&lt;li&gt;입력된 &lt;b&gt;text가 비어 있지 않을 때만 clear button이 표시&lt;/b&gt;되도록 해줍니다.&lt;/li&gt;&lt;li&gt;SF Symbol의 multiply.circle.fill을 사용해서 clear button을 만들어줍니다.&lt;/li&gt;&lt;li&gt;버튼 클릭 시에는 입력한 text를 빈 문자열로 만들어줍니다.&lt;/li&gt;&lt;/ol&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;// 2. trailing alignment 적용
ZStack(alignment: .trailing) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 1. ZStack으로 content를 감싸줌
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 3. 입력된 text가 비어 있지 않을 때만 clear button 표시
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if !text.isEmpty {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 4. 커스텀 clear button 구성
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Button {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 5. clear button 클릭 시 동작
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;text = &quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} label: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Image(systemName: &quot;multiply.circle.fill&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.foregroundStyle(.gray)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.padding(.trailing, 8)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;이제 clear button의 구성은 완료했고, 뷰에서 modifier를 적용할 수 있도록 메서드를 정의해줍니다.&lt;br&gt;&lt;u&gt;View의 익스텐션으로 바인딩 text 프로퍼티를 전달해서 커스텀 modifier를 호출하는 메서드를 정의해줍니다.&lt;/u&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;extension View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;func clearButton(text: Binding&amp;lt;String&amp;gt;) -&amp;gt; some View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;modifier(ClearButton(text: text))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;물론 TextField에서 직접&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;.modifier(ClearButton(text: $myText))&lt;/b&gt;&lt;/i&gt;를 호출해줘도 되지만 이런 식으로 사용하는게 국룰이라고 합니다. 사용할 modifier의 목적도 더 명확하게 명시하면서 호출 시 코드도 간결해지겠죠?&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;이제 아래와 같이 코드를 작성해주면 텍스트필드에 clear button modifier가 적용됩니다.&lt;br&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;i&gt;&lt;b&gt;.clearButton(text: $myText)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;를 통해 입력한 텍스트 프로퍼티를 ClearButton modifier로 전달하여 입력한 텍스트에 따라 clear button을 화면에 보여주거나 숨기도록 해줄 수 있습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;TextField(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;텍스트 입력&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;text: $myText
)
.textFieldStyle(.roundedBorder)
.clearButton(text: $myText)&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;전체 코드는 아래와 같습니다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;⚙️ 전체 코드 - ViewModifier를 활용한 커스텀 modifier 사용&lt;/b&gt;&lt;/h4&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;import SwiftUI

// MARK: - 커스텀 Modifier 사용
struct ClearButtonSampleView: View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@State private var myText: String = &quot;&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var body: some View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;VStack {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Text(&quot;텍스트를 입력해주세요.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.font(.title)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.bold()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;TextField(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;텍스트 입력&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;text: $myText
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.textFieldStyle(.roundedBorder)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.clearButton(text: $myText)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.padding()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}

struct ClearButton: ViewModifier {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Binding var text: String
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;func body(content: Content) -&amp;gt; some View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 2. trailing alignment 적용
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ZStack(alignment: .trailing) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 1. ZStack으로 content를 감싸줌
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 3. 입력된 text가 비어 있지 않을 때만 clear button 표시
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if !text.isEmpty {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 4. 커스텀 clear button 구성
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Button {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 5. clear button 클릭 시 동작
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;text = &quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} label: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Image(systemName: &quot;multiply.circle.fill&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.foregroundStyle(.gray)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.padding(.trailing, 8)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}

extension View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;func clearButton(text: Binding&amp;lt;String&amp;gt;) -&amp;gt; some View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;modifier(ClearButton(text: text))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;1332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CBXg4/btsHFOiUIv0/ahucPC2kkKuWy3cYKq7GI1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CBXg4/btsHFOiUIv0/ahucPC2kkKuWy3cYKq7GI1/img.gif&quot; data-alt=&quot;clear button 적용 ✅&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CBXg4/btsHFOiUIv0/ahucPC2kkKuWy3cYKq7GI1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/CBXg4/btsHFOiUIv0/ahucPC2kkKuWy3cYKq7GI1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;321&quot; height=&quot;662&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;1332&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;clear button 적용 ✅&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  clear button 커스텀&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이제 ClearButton modifier를 수정함으로써 clear button의 외형이나 동작을 커스텀할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;한번 아래과 같이 수정해보았습니다.&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;버튼 모양을 변경&lt;/li&gt;&lt;li&gt;clear button 클릭 시 텍스트를 지우지 않고, 약올리는 문구 출력&lt;/li&gt;&lt;/ul&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;if !text.isEmpty {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Button {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;text = &quot;안지워줄건데 ㅋ&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} label: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Image(systemName: &quot;delete.backward.fill&quot;).foregroundStyle(.orange.gradient.opacity(0.6))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.padding(.trailing, 8)
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;1332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkANA0/btsHET6dEW4/IPFGQgifO4BX4aDADJpPc1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkANA0/btsHET6dEW4/IPFGQgifO4BX4aDADJpPc1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkANA0/btsHET6dEW4/IPFGQgifO4BX4aDADJpPc1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bkANA0/btsHET6dEW4/IPFGQgifO4BX4aDADJpPc1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;323&quot; height=&quot;666&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;1332&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;레퍼런스&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.devtechie.com/community/public/posts/231542-how-to-add-clear-button-in-textfield&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://www.devtechie.com/community/public/posts/231542-how-to-add-clear-button-in-textfield&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;How to add clear button in TextField&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;SwiftUI TextField provides many functionalities out of the box but if you are looking for clear button similar to what UIKit version of TextField provides, you will not find it as its not available. At least without adding extra bit of code. In this articl&quot; data-og-host=&quot;www.devtechie.com&quot; data-og-source-url=&quot;https://www.devtechie.com/community/public/posts/231542-how-to-add-clear-button-in-textfield&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/L4R9X/hyV9SS1e5T/PXd5FlV7ZcP37kMuwAl2GK/img.png?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-og-url=&quot;https://www.devtechie.com/community/public/posts/231542-how-to-add-clear-button-in-textfield&quot;&gt;&lt;a href=&quot;https://www.devtechie.com/community/public/posts/231542-how-to-add-clear-button-in-textfield&quot; target=&quot;_blank&quot; data-source-url=&quot;https://www.devtechie.com/community/public/posts/231542-how-to-add-clear-button-in-textfield&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/L4R9X/hyV9SS1e5T/PXd5FlV7ZcP37kMuwAl2GK/img.png?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;How to add clear button in TextField&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;SwiftUI TextField provides many functionalities out of the box but if you are looking for clear button similar to what UIKit version of TextField provides, you will not find it as its not available. At least without adding extra bit of code. In this articl&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;www.devtechie.com&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/swiftui/viewmodifier&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://developer.apple.com/documentation/swiftui/viewmodifier&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;ViewModifier | Apple Developer Documentation&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;A modifier that you apply to a view or another view modifier, producing a different version of the original value.&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/documentation/swiftui/viewmodifier&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dgRqZJ/hyWdsd4cWh/WLippIrukYHIOeISS1QJoK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bA49sk/hyWdj2tHLk/8kDZH15dABwFfQ150nD2b1/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512&quot; data-og-url=&quot;https://docs.developer.apple.com/documentation/swiftui/viewmodifier&quot;&gt;&lt;a href=&quot;https://docs.developer.apple.com/documentation/swiftui/viewmodifier&quot; target=&quot;_blank&quot; data-source-url=&quot;https://developer.apple.com/documentation/swiftui/viewmodifier&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dgRqZJ/hyWdsd4cWh/WLippIrukYHIOeISS1QJoK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bA49sk/hyWdj2tHLk/8kDZH15dABwFfQ150nD2b1/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;ViewModifier | Apple Developer Documentation&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;A modifier that you apply to a view or another view modifier, producing a different version of the original value.&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;developer.apple.com&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>iOS</category>
      <category>clear button</category>
      <category>SwiftUI</category>
      <category>swiftui clear button</category>
      <category>TextField</category>
      <category>textfield clear button</category>
      <category>UITextfield</category>
      <category>ViewModifier</category>
      <category>커스텀 modifier</category>
      <author>kybeen</author>
      <guid isPermaLink="true">https://kybeen.tistory.com/148</guid>
      <comments>https://kybeen.tistory.com/148#entry148comment</comments>
      <pubDate>Tue, 28 May 2024 18:23:16 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] Build input file cannot be found: '~/Info.plist' 에러 발생 시</title>
      <link>https://kybeen.tistory.com/147</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;에러 설명&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Xcode로 개발을 하면서 Info.plist 파일의 위치를 옮기게 되면, 빌드 시에 아래와 같은 오류가 발생합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-05 오후 4.28.31.png&quot; data-origin-width=&quot;1114&quot; data-origin-height=&quot;164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8X2zi/btsHbfWyFGC/y5YjjJfcIh6PenjKz9xYe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8X2zi/btsHbfWyFGC/y5YjjJfcIh6PenjKz9xYe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8X2zi/btsHbfWyFGC/y5YjjJfcIh6PenjKz9xYe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8X2zi%2FbtsHbfWyFGC%2Fy5YjjJfcIh6PenjKz9xYe0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1114&quot; height=&quot;164&quot; data-filename=&quot;스크린샷 2024-05-05 오후 4.28.31.png&quot; data-origin-width=&quot;1114&quot; data-origin-height=&quot;164&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Build input file cannot be found: '~/Info.plist'. Did you forget to declare this file as an output of a script phase or custom build rule which produces it?&lt;/span&gt;&lt;/i&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  빌드 인풋 파일로 Info.plist 파일이 필요한데 찾을 수 없다는 뜻입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이는 해당 빌드 타겟에 지정된 &lt;i&gt;Info.plist&lt;/i&gt;의 경로는 그대로인데 실제 &lt;i&gt;Info.plist&lt;/i&gt; 파일의 경로는 달라서 이를 찾지 못하기 때문에 발생하는 문제입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저같은 경우는 프로젝트 내부에 Resources 폴더를 만들어주고, 루트 경로에 있던 Info.plist 파일을 그 안으로 옮겨주었기 때문에 기존 경로와 매칭이 되지 않았습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pwd3b/btsHbCw8D5W/MEoXgcd8TADfWq9E1SIlek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pwd3b/btsHbCw8D5W/MEoXgcd8TADfWq9E1SIlek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pwd3b/btsHbCw8D5W/MEoXgcd8TADfWq9E1SIlek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpwd3b%2FbtsHbCw8D5W%2FMEoXgcd8TADfWq9E1SIlek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;667&quot; height=&quot;194&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;332&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결 방법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 방법은 간단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;앱 타겟의 Build Settings로 들어가서 Info.plist 파일의 경로 설정을 현재 경로로 수정해주면 됩니다.&lt;/u&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-05 오후 4.35.50.png&quot; data-origin-width=&quot;2744&quot; data-origin-height=&quot;1074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beiws0/btsHd7oWYaK/pGnHCTf5yEaFA5ovwL1sdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beiws0/btsHd7oWYaK/pGnHCTf5yEaFA5ovwL1sdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beiws0/btsHd7oWYaK/pGnHCTf5yEaFA5ovwL1sdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbeiws0%2FbtsHd7oWYaK%2FpGnHCTf5yEaFA5ovwL1sdK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2744&quot; height=&quot;1074&quot; data-filename=&quot;스크린샷 2024-05-05 오후 4.35.50.png&quot; data-origin-width=&quot;2744&quot; data-origin-height=&quot;1074&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 경로에 맞게 수정해주었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-05 오후 4.38.38.png&quot; data-origin-width=&quot;1550&quot; data-origin-height=&quot;398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNaIUe/btsHb40ZUBk/0EkRRsiNJbHXKTDfRNqXy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNaIUe/btsHb40ZUBk/0EkRRsiNJbHXKTDfRNqXy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNaIUe/btsHb40ZUBk/0EkRRsiNJbHXKTDfRNqXy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNaIUe%2FbtsHb40ZUBk%2F0EkRRsiNJbHXKTDfRNqXy0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1550&quot; height=&quot;398&quot; data-filename=&quot;스크린샷 2024-05-05 오후 4.38.38.png&quot; data-origin-width=&quot;1550&quot; data-origin-height=&quot;398&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 빌드를 하면 정상적으로 빌드가 될 것입니다. &lt;/p&gt;</description>
      <category>iOS</category>
      <category>build input file cannot be found</category>
      <category>info.plist</category>
      <category>info.plist 오류</category>
      <category>xcode 오류</category>
      <author>kybeen</author>
      <guid isPermaLink="true">https://kybeen.tistory.com/147</guid>
      <comments>https://kybeen.tistory.com/147#entry147comment</comments>
      <pubDate>Sun, 5 May 2024 16:40:33 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] Core Location 테스트하기 (외부 의존성의 응답을 테스트하기)</title>
      <link>https://kybeen.tistory.com/146</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;사이드 프로젝트를 진행하면서 Core Location을 사용해 &lt;b&gt;현재 위치를 불러오는 기능&lt;/b&gt;을 구현하고, 이에 대한 &lt;b&gt;테스트 코드&lt;/b&gt;를 작성하려고 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;LocationService 클래스 정의&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위치 서비스 관련 동작을 처리하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;&lt;b&gt;LocationService&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;라는 이름의 클래스를 정의해주었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해당 클래스는 Core Location의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;i&gt;&lt;b&gt;CLLocationManager&lt;/b&gt;&lt;/i&gt;&amp;nbsp;인스턴스를 활용하여 위치 서비스 관련 메서드를 호출&lt;/span&gt;하고, &lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;i&gt;&lt;b&gt;CLLocationManagerDelegate&lt;/b&gt;&lt;/i&gt;를 채택하여 위치 서비스의 결과를 받아서 처리&lt;/span&gt;할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해당 프로젝트에서 필요한 동작은 &lt;u&gt;현재 위치의 위도,경도값&lt;/u&gt;을 받아오는 것 하나 뿐이기 때문에 간단하게 필요한 내용을 아래와 같이 작성해주었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714661094263&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import CoreLocation

// MARK: - LocationService 정의

final class LocationService: NSObject {
    
    typealias LocationCallback = (CLLocation?) -&amp;gt; Void
    
    private let locationManager: CLLocationManager
    private var fetchLocationCallBack: LocationCallback?
    
    init(locationManager: CLLocationManager = CLLocationManager()) {
        self.locationManager = locationManager
        super.init()
        self.locationManager.delegate = self
    }
    
    // 현재 위치를 불러오기
    func fetchCurrentLocation(completion: @escaping LocationCallback) {
        fetchLocationCallBack = completion
        locationManager.requestLocation()
    }
}

// MARK: - CLLocationManagerDelegate 델리게이트 구현

extension LocationService: CLLocationManagerDelegate {
    
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        switch manager.authorizationStatus {
        case .authorizedWhenInUse:
            print(&quot;위치 서비스 사용 가능&quot;)
        case .restricted, .denied:
            print(&quot;위치 서비스 사용 불가&quot;)
            // TODO: - &quot;위치 서비스를 사용하려면 승인이 필요합니다.&quot; 팝업 띄우기
        case .notDetermined:
            print(&quot;권한 설정 필요&quot;)
            locationManager.requestWhenInUseAuthorization() // 권한 요청
        default:
            break
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        if let location = locations.last {
            fetchLocationCallBack?(location)
            fetchLocationCallBack = nil
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        fetchLocationCallBack?(nil)
        fetchLocationCallBack = nil
        // TODO: - 실패 시 UI처리 필요
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;현재 위치를 받아오는 동작 코드를 더 자세하게 살펴보겠습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;LocationService 클래스의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;i&gt;&lt;b&gt;fetchCurrentLocation(completion:)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메서드를 호출하면 &lt;u&gt;파라미터로 넘겨준 탈출 클로저를 통해 위치 값을 받아올 수 있습니다.&lt;/u&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Core Location에서 현재 위치를 받아오려면&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;1&quot;&gt;CLLocationManagerDelegate&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;를 채택한 델리게이트 객체의&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;3&quot;&gt;locationManager(_:didUpdateLocations:)&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;메서드 내부에서 인자로 전달받은&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;5&quot;&gt;locations&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;값을 통해 위치 정보를 추출해줘야 합니다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;이를 위해&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;1&quot;&gt;CLLocation&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;타입의&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;3&quot;&gt;위치 값을 전달할 콜백을 정의&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;해주었고, &lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;i&gt;&lt;b&gt;fetchCurrentLocation(completion:)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;를 통해 전달받은 클로저를 해당 콜백에 할당해주었습니다. 그 후&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;7&quot;&gt;CLLocationManager&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;의&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;9&quot;&gt;requestLocation()&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;메서드가 호출됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1714661780416&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final class LocationService: NSObject {
    
		// 위치 값을 인자로 전달하는 콜백 타입
		typealias LocationCallback = (CLLocation?) -&amp;gt; Void
		
		private var fetchLocationCallBack: LocationCallback?
		
		// 현재 위치를 불러오기
		func fetchCurrentLocation(completion: @escaping LocationCallback) {
		    fetchLocationCallBack = completion
		    locationManager.requestLocation()
		}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;0&quot;&gt;CLLocationManager&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;의&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;2&quot;&gt;requestLocation()&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;메서드가 호출되면,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;4&quot;&gt;CLLocationManagerDelegate&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;의 델리게이트 메서드 내부에서 인자로 받은&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;6&quot;&gt;locations&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;값을 콜백 클로저로 넘겨주게 됩니다. (실패 시에는 nil 전달)&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1714661925193&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;extension LocationService: CLLocationManagerDelegate {
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        if let location = locations.last {
            fetchLocationCallBack?(location)
            fetchLocationCallBack = nil
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        fetchLocationCallBack?(nil)
        fetchLocationCallBack = nil
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;테스트 코드 작성&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드를 통해 테스트하고 싶은 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;i&gt;&lt;b&gt;CLLocationManager&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;b&gt;가 위치 값을 제대로 반환하는지&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 검증하기 위한 테스트코드를 아래와 같이 작성해주었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714662033104&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import XCTest
import CoreLocation
@testable import Data

final class LocationServiceTests: XCTestCase {
    
    private var sut: LocationService!
    
    override func setUpWithError() throws {
        super.setUp()
        sut = LocationService()
    }
    
    override func tearDownWithError() throws {
        super.tearDown()
        sut = nil
    }
    
    func test_fetchCurrentLocation을_호출하면_위치정보를_얻음() {
        let expectedLocation = CLLocation(latitude: 37.6134436427887, longitude: 126.926493082645)
        let completionExpectation = expectation(description: &quot;fetchCurrentLocation completion expectation&quot;)
        
        sut.fetchCurrentLocation { location in
            completionExpectation.fulfill()
            if let location = location {
                XCTAssertEqual(location.coordinate.latitude, expectedLocation.coordinate.latitude)
                XCTAssertEqual(location.coordinate.longitude, expectedLocation.coordinate.longitude)
            }
        }
        wait(for: [completionExpectation], timeout: 1)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;0&quot;&gt;expectedLocation&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;span style=&quot;text-align: start;&quot;&gt;에 예상 결과값을 넣고,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;2&quot;&gt;fetchCurrentLocation()&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;메서드를 호출하여 콜백으로 전달된 인자를 통해 값을 검증합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1714662097451&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func test_fetchCurrentLocation을_호출하면_위치정보를_얻음() {
    let expectedLocation = CLLocation(latitude: 37.6134436427887, longitude: 126.926493082645)
    let completionExpectation = expectation(description: &quot;fetchCurrentLocation completion expectation&quot;)
    
    sut.fetchCurrentLocation { location in
        if let location = location {
            completionExpectation.fulfill()
            XCTAssertEqual(location.coordinate.latitude, expectedLocation.coordinate.latitude)
            XCTAssertEqual(location.coordinate.longitude, expectedLocation.coordinate.longitude)
        }
    }
    wait(for: [completionExpectation], timeout: 1)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;하지만 이 테스트는 실패합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;왜냐하면 해당 테스트 동작 중에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;실제 기기의 위치값&lt;/b&gt;이 필요한데, &lt;span style=&quot;color: #ee2323;&quot;&gt;테스트 중에 그 값을 받아올(주입시킬) 방법이 없기 때문입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;fail.png&quot; data-origin-width=&quot;2008&quot; data-origin-height=&quot;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x7A2f/btsG79H8kth/ZESMnXkBQlKlJS1VOMk2W0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x7A2f/btsG79H8kth/ZESMnXkBQlKlJS1VOMk2W0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x7A2f/btsG79H8kth/ZESMnXkBQlKlJS1VOMk2W0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx7A2f%2FbtsG79H8kth%2FZESMnXkBQlKlJS1VOMk2W0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;704&quot; height=&quot;181&quot; data-filename=&quot;fail.png&quot; data-origin-width=&quot;2008&quot; data-origin-height=&quot;516&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;그렇기 때문에 CoreLocation의 인터페이스를 직접적으로 사용하지 않으면서 테스트할 수 있도록 수정이 필요합니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Core Location 인터페이스 추상화&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. CLLocationManager의 인터페이스를 정의하여 추상화하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;먼저, &lt;span style=&quot;color: #ef6f53;&quot;&gt;실제 동작 시 사용하는&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;1&quot;&gt;CLLocationManager&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;span style=&quot;text-align: start;&quot;&gt;의&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;3&quot;&gt;프로퍼티/메서드를 그대로 인터페이스 프로토콜로 정의&lt;/span&gt;&lt;/u&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;해줍니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;5&quot;&gt;LocationManagerInterface&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;프로토콜을 생성한 뒤,&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;7&quot;&gt;LocationService&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;클래스에서 사용되는&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;9&quot;&gt;CLLocationManager&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 객체와 동일한 프로&lt;/span&gt;퍼티와 메서드를 요구하도록 작성했습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1714662586985&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protocol LocationManagerInterface {
    var locationManagerDelegate: CLLocationManagerDelegate? { get set }
    func requestWhenInUseAuthorization()
    func requestLocation()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;이제&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;1&quot;&gt;LocationService&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;클래스에서&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;3&quot;&gt;CLLocationManager&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;4&quot;&gt;에 대한 참조를&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;5&quot;&gt;LocationManagerInterface&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;6&quot;&gt;의 참조로 변경&lt;/span&gt;&lt;/span&gt;&lt;/u&gt;&lt;span style=&quot;text-align: start;&quot;&gt;해줍니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;또한&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;8&quot;&gt;델리게이트 지정을&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;9&quot;&gt;delegate&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;10&quot;&gt;가 아닌, 인터페이스 프로토콜에 정의한&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;11&quot;&gt;locationManagerDelegate&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/u&gt;&lt;span style=&quot;text-align: start;&quot;&gt;로 해줍니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1714662712652&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final class LocationService: NSObject {

    private var locationManager: LocationManagerInterface
    
    init(locationManager: LocationManagerInterface = CLLocationManager()) {
        self.locationManager = locationManager
        super.init()
        self.locationManager.locationManagerDelegate = self
    }
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;이렇게 함으로써&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;1&quot;&gt;LocationService&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;span style=&quot;text-align: start;&quot;&gt;의&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;i&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;3&quot;&gt;CLLocationManager&lt;/span&gt;&lt;/i&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;4&quot;&gt;에 대한 의존성을 끊어줄 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 아직&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;CLLocationManager&lt;/b&gt;&lt;/i&gt;가 인터페이스 프로토콜을 채택하지 않았기 때문에 프로토콜을 채택하도록 해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ㅌㅌ.png&quot; data-origin-width=&quot;1880&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/djhduD/btsG9dJN4lF/xuaQWxJP06Vs52Nf1kIt30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djhduD/btsG9dJN4lF/xuaQWxJP06Vs52Nf1kIt30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djhduD/btsG9dJN4lF/xuaQWxJP06Vs52Nf1kIt30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdjhduD%2FbtsG9dJN4lF%2FxuaQWxJP06Vs52Nf1kIt30%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;735&quot; height=&quot;82&quot; data-filename=&quot;ㅌㅌ.png&quot; data-origin-width=&quot;1880&quot; data-origin-height=&quot;210&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;프로토콜의 요구사항들은 어차피&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;CLLocationManager&lt;/b&gt;&lt;/i&gt;에 이미 정의된 내용이기 때문에 별도로 구현할 필요가 없습니다.&lt;/u&gt;&lt;/p&gt;
&lt;pre class=&quot;swift&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;extension CLLocationManager: LocationManagerInterface {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;i&gt;&lt;b&gt;locationManagerDelegate&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;를 아직 정의하지 않았기 때문에 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;이를 새로 정의해주어야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;델.png&quot; data-origin-width=&quot;1904&quot; data-origin-height=&quot;46&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3ZO5Q/btsG80DVJhE/BRJaMOwMvXgXSemiKyPwfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3ZO5Q/btsG80DVJhE/BRJaMOwMvXgXSemiKyPwfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3ZO5Q/btsG80DVJhE/BRJaMOwMvXgXSemiKyPwfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3ZO5Q%2FbtsG80DVJhE%2FBRJaMOwMvXgXSemiKyPwfk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1904&quot; height=&quot;46&quot; data-filename=&quot;델.png&quot; data-origin-width=&quot;1904&quot; data-origin-height=&quot;46&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;CLLocationManagerDelegate의 인터페이스를 정의하여 추상화하기&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;LocationManagerInterface&lt;/b&gt;&lt;/i&gt;를 채택하는 과정에서 한 단계 작업을 더 해줘야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;왜냐하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;LocationService&lt;/b&gt;&lt;/i&gt;는&lt;span&gt; &lt;/span&gt;&lt;u&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;&lt;b&gt;CLLocationManagerDelegate&lt;/b&gt;&lt;/i&gt;로 인해 CoreLocation 시스템 델리게이트에 아직 강하게 연결&lt;/span&gt;&lt;/u&gt;되어 있기 때문입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;이 의존성을 끊어주기 위해&lt;span&gt; &lt;/span&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;i&gt;&lt;b&gt;LocationManagerDelegate&lt;/b&gt;&lt;/i&gt;&amp;nbsp;델리게이트를 새로 정의&lt;/span&gt;해줍니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;i&gt;&lt;b&gt;CLLocationManagerDelegate&lt;/b&gt;&lt;/i&gt;에서 사용되는 메서드들과 형식을 맞춰서 정의&lt;/u&gt;해줍니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;i&gt;&lt;b&gt;manager&lt;/b&gt;&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;파라미터의 타입은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;CLLocationManager&lt;/b&gt;&lt;/i&gt;가 아닌&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;LocationManagerInterface&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;가 됩니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;protocol LocationManagerDelegate: AnyObject {
    func locationManagerAbstract(_ manager: LocationManagerInterface, didChangeAuthorization status: CLAuthorizationStatus)
    func locationManagerAbstract(_ manager: LocationManagerInterface, didUpdateLocations locations: [CLLocation])
    func locationManagerAbstract(_ manager: LocationManagerInterface, didFailWithError error: Error)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;이제&amp;nbsp;CLLocationManager에서&amp;nbsp;locationManagerDelegate를 구현&lt;/b&gt;해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;CLLocationManager&lt;/b&gt;&lt;/i&gt;의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;delegate&lt;/b&gt;&lt;/i&gt;로 접근할 수 있도록 타입캐스팅하는 코드를 &lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;접근자와 설정자에 &lt;/span&gt;작성&lt;/b&gt;해주었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 하면&lt;span style=&quot;color: #8a3db6;&quot;&gt;&amp;nbsp;&lt;i&gt;&lt;b&gt;locationManagerDelegate&lt;/b&gt;&lt;/i&gt;를 통해&amp;nbsp;&lt;i&gt;CLLocationManager&lt;/i&gt;의&amp;nbsp;&lt;i&gt;delegate&lt;/i&gt;로 접근&lt;span style=&quot;color: #000000;&quot;&gt;하&lt;/span&gt;&lt;/span&gt;게 되고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;b&gt;&lt;i&gt;LocationService&lt;/i&gt;의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;CLLocationManagerDelegate&lt;/i&gt;에 대한 의존성을 끊어줄 수 있습니다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;CoreLocation의 시스템 델리게이트를 활용해 데이터를 받아올 수 있으면서도 delegate은 직접 접근하지 않게 되는 것이죠. 덕분에 CoreLocation을 직접 사용하지 않는 임의의 mock 클래스를 만들어 동작을 테스트할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; style=&quot;color: #000000; text-align: start;&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;protocol LocationManagerInterface {
    var locationManagerDelegate: LocationManagerDelegate? { get set }
    ...
}

extension CLLocationManager: LocationManagerInterface {
    var locationManagerDelegate: LocationManagerDelegate? {
        get {
            return delegate as? LocationManagerDelegate
        }
        set {
            delegate = newValue as? CLLocationManagerDelegate
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;추상화된 델리게이트를 채택하는 코드로 수정&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;CLLocationManager&lt;/b&gt;&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;프로토콜 정보를 새로 정의한 프로토콜로 변경해주어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 먼저&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;&lt;i&gt;LocationService&lt;/i&gt;&lt;/b&gt;&amp;nbsp;클래스가&amp;nbsp;&lt;i&gt;&lt;b&gt;LocationManagerDelegate&lt;/b&gt;&lt;/i&gt;를 준수하도록 코드를 작성&lt;/span&gt;해줍니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존에&lt;span&gt; &lt;/span&gt;&lt;u&gt;&lt;i&gt;&lt;b&gt;CLLocationManagerDelegate&lt;/b&gt;&lt;/i&gt;를 채택하며 구현한 코드를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;LocationManagerDelegate&lt;/b&gt;&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;형식에 맞게 옮겨줍니다.&lt;/u&gt;&lt;u&gt;&lt;/u&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1714663346345&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;extension LocationService: LocationManagerDelegate {
    
    func locationManagerAbstract(_ manager: LocationManagerInterface, didChangeAuthorization status: CLAuthorizationStatus) {
        switch status {
        case .authorizedWhenInUse:
            print(&quot;위치 서비스 사용 가능&quot;)
        case .restricted, .denied:
            print(&quot;위치 서비스 사용 불가&quot;)
            // TODO: - &quot;위치 서비스를 사용하려면 승인이 필요합니다.&quot; 팝업 띄우기
        case .notDetermined:
            print(&quot;권한 설정 필요&quot;)
            locationManager.requestWhenInUseAuthorization() // 권한 요청
        default:
            break
        }
    }

    func locationManagerAbstract(_ manager: LocationManagerInterface, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.last else { return }
        fetchLocationCallBack?(location)
        fetchLocationCallBack = nil
    }

    func locationManagerAbstract(_ manager: LocationManagerInterface, didFailWithError error: Error) {
        fetchLocationCallBack?(nil)
        fetchLocationCallBack = nil
        // TODO: - 실패 시 UI처리 필요
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 기존의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;CLLocationManagerDelegate&lt;/b&gt;&lt;/i&gt;의 델리게이트 메서드들이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;각각 대응하는&amp;nbsp;LocationManagerDelegate의 메서드를 호출하도록 수정&lt;/b&gt;&lt;/span&gt;해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714663449290&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;extension LocationService: CLLocationManagerDelegate {
    
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        locationManagerAbstract(manager, didChangeAuthorization: manager.authorizationStatus)
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        locationManagerAbstract(manager, didUpdateLocations: locations)
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        locationManagerAbstract(manager, didFailWithError: error)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;추상화된 인터페이스 기반의 테스트코드 작성&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;LocationManagerInterface&lt;/b&gt;&lt;/i&gt;를 활용하여 직접 제어가 가능한 클래스를 사용할 수 있기 때문에 &lt;u&gt;mock을 생성하고, 주입하고자 하는 값들을 넣어줄 수 있습니다.&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;i&gt;&lt;b&gt;CLLocationManager&lt;/b&gt;&lt;/i&gt;&amp;nbsp;대신 동작할 Mock 객체를 생성&lt;/span&gt;해주었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;u&gt;&lt;i&gt;&lt;b&gt;locationToReturn&lt;/b&gt;&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;클로저를 통해 임의의 위치값을 넘겨줍니다.&lt;/u&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1714663583386&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// CLLocationManager 대신 동작할 Mock 객체
struct LocationManagerMock: LocationManagerInterface {
    var locationManagerDelegate: LocationManagerDelegate?
    
    var locationToReturn: (() -&amp;gt; CLLocation)?
    
    func requestLocation() {
        guard let location = locationToReturn?() else { return }
        locationManagerDelegate?.locationManagerAbstract(self, didUpdateLocations: [location])
    }
    
    func requestWhenInUseAuthorization() {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mock 객체를 활용하여 테스트 코드를 아래와 같이 작성해주었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;각 코드의 동작은 아래와 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;mock 인스턴스 생성&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;CLLocationManager&lt;/b&gt;&lt;/i&gt;의 &lt;i&gt;&lt;b&gt;requestLocation()&lt;/b&gt;&lt;/i&gt;메서드가 호출됐을 때 받아올 것이라고 &lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;기대되는 임의의 위치값을 전달&lt;/span&gt;&lt;/b&gt;해줍니다.&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;LocationService&lt;/b&gt;&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;인스턴스 생성 시 &lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;이니셜라이저를 통해 mock 인스턴스를&amp;nbsp;locationManager로 주입&lt;/span&gt;&lt;/b&gt;해줍니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 클래스가 아닌 인터페이스에 따라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;locationManager&lt;/b&gt;&lt;/i&gt;를 만들어줌으로써 &lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;의존성이 역전&lt;/b&gt;&lt;/span&gt;됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1714663724146&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func test_fetchCurrentLocation을_호출하면_위치정보를_얻음() {
    // 1
    var locationManagerMock = LocationManagerMock()
    
    // 2
    locationManagerMock.locationToReturn = {
        return CLLocation(latitude: 37.6134436427887, longitude: 126.926493082645)
    }
    
    // 3
    let sut = LocationService(locationManager: locationManagerMock)
    
    let expectedLocation = CLLocation(latitude: 37.6134436427887, longitude: 126.926493082645)
    let completionExpectation = expectation(description: &quot;fetchCurrentLocation completion expectation&quot;)
    
    sut.fetchCurrentLocation { location in
        if let location = location {
            completionExpectation.fulfill()
            XCTAssertEqual(location.coordinate.latitude, expectedLocation.coordinate.latitude)
            XCTAssertEqual(location.coordinate.longitude, expectedLocation.coordinate.longitude)
        }
    }
    wait(for: [completionExpectation], timeout: 1)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해준 뒤에는 테스트 코드가 통과하는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;i&gt;&lt;b&gt;CLLocationManager&lt;/b&gt;&lt;/i&gt;와 동일하게 동작하지만 실제 기기의 위치값을 받아올 필요가 없는&amp;nbsp;&lt;i&gt;&lt;b&gt;LocationManagerMock&lt;/b&gt;&lt;/i&gt;&amp;nbsp;클래스를 사용해서 위치 정보를 받았을 때 콜백으로 제대로 넘겨지는지를 테스트할 수 있게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ㅇㅇ.png&quot; data-origin-width=&quot;1804&quot; data-origin-height=&quot;878&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTV7HX/btsG8S6Z62t/6VlbgAptQqbaCN9yzv76ck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTV7HX/btsG8S6Z62t/6VlbgAptQqbaCN9yzv76ck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTV7HX/btsG8S6Z62t/6VlbgAptQqbaCN9yzv76ck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTV7HX%2FbtsG8S6Z62t%2F6VlbgAptQqbaCN9yzv76ck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;710&quot; height=&quot;346&quot; data-filename=&quot;ㅇㅇ.png&quot; data-origin-width=&quot;1804&quot; data-origin-height=&quot;878&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;이와 동일한 방식으로 임의의 값을 넘겨주면서 인증, 에러 등의 다른 상황도 테스트할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;리팩토링 후 최종 코드&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;테스트 코드 내용을 &lt;span style=&quot;color: #f89009;&quot;&gt;&lt;i&gt;&lt;b&gt;Given-When-Then 형식&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;에 맞게 리팩토링하여 가독성을 높여주었고, 위치 정보 불러오기를 실패했을 경우를 테스트하는 코드를 추가적으로 작성해보았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; &amp;nbsp;LocationService 코드 수정&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 위해 기존&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;LocationService&lt;/b&gt;&lt;/i&gt;의 콜백에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;i&gt;&lt;b&gt;CLLocation&lt;/b&gt;&lt;/i&gt;값과&amp;nbsp;&lt;i&gt;&lt;b&gt;Error&lt;/b&gt;&lt;/i&gt;&amp;nbsp;타입의 값을 인자로 넘겨주도록 수정&lt;/span&gt;했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;u&gt;커스텀 에러 타입인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;LocationError&lt;/b&gt;&lt;/i&gt;를 정의하고, 콜백에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;Error&lt;/b&gt;&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;타입의 인자를 추가&lt;/u&gt;해주었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1714664017211&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;enum LocationError: Error {
    case fetchFailed
}

final class LocationService: NSObject {
    
    typealias LocationCallback = (CLLocation?, Error?) -&amp;gt; Void // 위치 값을 인자로 전달하는 콜백 타입
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이에 맞게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;LocationManagerDelegate&lt;/b&gt;&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;구현부에서 &lt;u&gt;콜백으로 인자를 넘겨주는 부분을 수정&lt;/u&gt;해줍니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위치 정보 불러오기 성공 시에는 에러에 nil 값을 넘겨주고, 실패 시에는 새로 정의한 에러 타입의 값을 넘겨줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1714664073389&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;extension LocationService: LocationManagerDelegate {
    
    ...

    func locationManagerAbstract(_ manager: LocationManagerInterface, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.last else { return }
        fetchLocationCallBack?(location, nil)
        fetchLocationCallBack = nil
    }

    func locationManagerAbstract(_ manager: LocationManagerInterface, didFailWithError error: Error) {
        fetchLocationCallBack?(nil, LocationError.fetchFailed)
        fetchLocationCallBack = nil
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;테스트 코드 수정&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제 다른 테스트도 추가하기 위해 테스트 코드를 수정해주겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;권한 상태에 따른 테스트를 위해 테스트 시 반환될 권한값도 정의&lt;/span&gt;해주었습니다.  &amp;nbsp;&lt;i&gt;&lt;b&gt;authorizationStatusToReturn&lt;/b&gt;&lt;/i&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;requestWhenInUseAuthorization()&lt;/b&gt;&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;호출 시 해당 값을 넘겨주어 권한 상태에 따른 동작을 테스트할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;i&gt;&lt;b&gt;locationToReturn&lt;/b&gt;&lt;/i&gt;의 반환값 유무에 따라&amp;nbsp;&lt;i&gt;&lt;b&gt;requestLocation()&lt;/b&gt;&lt;/i&gt;의 동작을 나눠주었습니다.&lt;/span&gt; 반환된 위치 정보가 없는 경우에 에러 처리 코드가 호출되는지 테스트할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1714664197277&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// CLLocationManager 대신 동작할 Mock 객체
struct LocationManagerMock: LocationManagerInterface {
    var locationManagerDelegate: LocationManagerDelegate?
    
    var locationToReturn: (() -&amp;gt; CLLocation)? // 테스트 시 전달할 위치값
    var authorizationStatusToReturn: CLAuthorizationStatus = .notDetermined // 테스트 시 전달할 권한 상태
    
    func requestLocation() {
        if let location = locationToReturn?() {
            locationManagerDelegate?.locationManagerAbstract(self, didUpdateLocations: [location])
        } else {
            locationManagerDelegate?.locationManagerAbstract(self, didFailWithError: LocationError.fetchFailed)
        }
    }
    
    func requestWhenInUseAuthorization() {
        locationManagerDelegate?.locationManagerAbstract(self, didChangeAuthorization: authorizationStatusToReturn)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;위치 정보를 받아오지 못한 경우의 테스트 코드를 추가&lt;/span&gt;&lt;/b&gt;해주었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;또한 &lt;u&gt;테스트 코드 내용을 Given-When-Then 구조&lt;/u&gt;로 맞춰주었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1714664242127&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final class LocationServiceTests: XCTestCase {
    
    private var sut: LocationService!
    
    override func setUpWithError() throws {
        super.setUp()
    }
    
    override func tearDownWithError() throws {
        super.tearDown()
        sut = nil
    }
  
    func test_fetchCurrentLocation을_호출시_위치가_반환됐을_때_위치정보가_제대로_전달되는지_확인() {
        // Given
        var locationManagerMock = LocationManagerMock()
        locationManagerMock.locationToReturn = {
            return CLLocation(latitude: 37.6134436427887, longitude: 126.926493082645) // locationManager가 반환할 위치값
        }
        sut = LocationService(locationManager: locationManagerMock)
        
        let expectedLocation = CLLocation(latitude: 37.6134436427887, longitude: 126.926493082645) // 기대값
        var result = CLLocation(latitude: 0.0, longitude: 0.0) // 반환 결과를 받을 변수
        let completionExpectation = expectation(description: &quot;fetchCurrentLocation completion expectation&quot;)
        
        // When
        sut.fetchCurrentLocation { location, _ in
            if let location = location {
                result = location
                completionExpectation.fulfill()
            }
        }
        wait(for: [completionExpectation], timeout: 1)
        
        // Then
        XCTAssertEqual(result.coordinate.latitude, expectedLocation.coordinate.latitude)
        XCTAssertEqual(result.coordinate.longitude, expectedLocation.coordinate.longitude)
    }
    
    func test_fetchCurrentLocation을_호출시_위치가_반환되지_않으면_에러를_던지는지_확인() {
        // Given
        var locationManagerMock = LocationManagerMock()
        locationManagerMock.locationToReturn = {
            return nil
        }
        sut = LocationService(locationManager: locationManagerMock)
        var result: (location:CLLocation?, error:Error?)
        let completionExpectation = expectation(description: &quot;fetchCurrentLocation completion expectation&quot;)
        
        // When
        sut.fetchCurrentLocation { location, error in
            result.location = location
            result.error = error
            completionExpectation.fulfill()
        }
        wait(for: [completionExpectation], timeout: 1)
        
        // Then
        XCTAssertNil(result.location)
        XCTAssertNotNil(result.error)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  추가 리팩토링&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2개 테스트 코드의 Given 부분을 보면 중복되는 코드가 많습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;LocationService&lt;/b&gt;&lt;/i&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;LocationManagerMock&lt;/b&gt;&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;인스턴스를 생성하는 부분은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;i&gt;&lt;b&gt;setUpWithError()&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;에서 처리해주는 것이 좋을 것 같습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Given 단계에서는 테스트 시 넘겨줄 값을 지정하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;LocationManagerMock&lt;/b&gt;&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;인스턴스를 생성하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;locationToReturn&lt;/b&gt;&lt;/i&gt;에 값을 할당해줍니다.&lt;span&gt; &lt;/span&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;i&gt;&lt;b&gt;LocationService&lt;/b&gt;&lt;/i&gt;에서는 델리게이트 할당이 이니셜라이저에서 이루어지기 때문에 mock 인스턴스를 생성한 이후에, 그 mock 인스턴스를 &lt;i&gt;LocationService&lt;/i&gt;의 이니셜라이저로 넘겨주면서&amp;nbsp;&lt;i&gt;LocationService&lt;/i&gt;를 초기화해주어야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그렇기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;i&gt;&lt;b&gt;mock 인스턴스 생성 &amp;rarr; sut 생성&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;의 순서로 코드를 작성할 수밖에 없습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;i&gt;&lt;b&gt;locationToReturn&lt;/b&gt;&lt;/i&gt;은 mock 객체에서 새로 정의한 프로퍼티이기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;LocationService&lt;/b&gt;&lt;/i&gt;에서는 접근할 수가 없습니다.&lt;/u&gt; (&lt;i&gt;&lt;b&gt;LocationManagerInterface&lt;/b&gt;&lt;/i&gt;에서 요구하지 않는 프로퍼티임)&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그렇다고 테스트 시에만 사용하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;locationToReturn&lt;/b&gt;&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;프로퍼티를 인터페이스 프로토콜에 추가로 정의해주는 것은 실제 동작 시에 불필요한 프로퍼티가 추가되는 것이기 때문에 불필요하다고 생각됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;그래서 저는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;mock 객체용 인터페이스를 추가적으로 정의&lt;/b&gt;&lt;/span&gt;해주었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;새로운 인터페이스 프로토콜은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;LocationManagerInterface&lt;/b&gt;&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;프로토콜을 상속받도록 구현&lt;/u&gt;했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714664435232&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protocol LocationManagerMockInterface: LocationManagerInterface {
    var locationToReturn: (() -&amp;gt; CLLocation?)? { get set }
    var authorizationStatusToReturn: CLAuthorizationStatus { get set }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;u&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;1&quot;&gt;mock 객체의 채택 프로토콜을 수정&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/u&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;u&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot; data-token-index=&quot;3&quot;&gt;타입도&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #337ea9; text-align: start;&quot; data-token-index=&quot;4&quot;&gt;struct &amp;rarr; class&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot; data-token-index=&quot;5&quot;&gt;로 변경&lt;/span&gt;&lt;/b&gt;&lt;/u&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;해주었습니다. 이는 &lt;b&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;인스턴스의 참조를 사용하기 위해서&lt;/span&gt;&lt;/b&gt;인데, 자세한 이유는 아래에서 설명하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1714664493070&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// CLLocationManager 대신 동작할 Mock 객체
class LocationManagerMock: LocationManagerMockInterface {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;setUpWithError()&lt;/b&gt;&lt;/i&gt;에서 &lt;u&gt;먼저 sut 인스턴스를 초기화&lt;/u&gt;해줍니다. &lt;u&gt;이니셜라이저를 통해 &lt;i&gt;&lt;b&gt;LocationManagerMock&lt;/b&gt;&lt;/i&gt; 인스턴스를 프로퍼티로 할당&lt;/u&gt;해줍니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;sut 인스턴스의 초기화 이후에 mock 인스턴스를 할당해주면 델리게이트 지정이 되지 않습니다. 왜냐하면 &lt;u&gt;델리게이트 할당이 sut(LocationService) 객체의 초기화 단계에서 이루지기 때문&lt;/u&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;setUpWithError()&lt;/b&gt;&lt;/i&gt;에서 인스턴스 초기화가 이루어지기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;이제 각 테스트 코드의 Given 단계에서는 인스턴스를 생성할 필요 없이 반환값의 세팅만 해주면 됩니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1714665150605&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;override func setUpWithError() throws {
    super.setUp()
    sut = LocationService(locationManager: LocationManagerMock())
}

override func tearDownWithError() throws {
    super.tearDown()
    sut = nil
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;mock 객체의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;locationToReturn&lt;/b&gt;&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;프로퍼티는 &lt;span style=&quot;color: #0593d3;&quot;&gt;새로운 인터페이스 프로토콜의 요구사항이기 때문에 기존&amp;nbsp;&lt;i&gt;&lt;b&gt;locationManager&lt;/b&gt;&lt;/i&gt;로 바로 접근하게 되면 접근할 수가 없습니다.&lt;/span&gt; 그렇기 때문에&lt;span style=&quot;color: #8a3db6;&quot;&gt;&amp;nbsp;&lt;b&gt;다운캐스팅&lt;/b&gt;을&lt;/span&gt; 진행해줍니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;u&gt;다운캐스팅 이후에는&amp;nbsp;&lt;i&gt;&lt;b&gt;locationToReturn&lt;/b&gt;&lt;/i&gt;&amp;nbsp;프로퍼티로 접근이 가능&lt;/u&gt;&lt;/span&gt;해집니다. 하지만 다운캐스팅된&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;&lt;i&gt;l&lt;/i&gt;&lt;/b&gt;&lt;b&gt;&lt;i&gt;ocationManagerMock&lt;/i&gt;&lt;/b&gt;는if var 구문 내부에서만 사용할 수 있기 때문에 앞에서 mock 객체를 참조 타입인 클래스로 수정해준 것&lt;/span&gt;입니다. 이렇게 하면 &lt;u&gt;if var 구문 내부에서의 수정이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;locationManager&lt;/b&gt;&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;인스턴스에 반영&lt;/u&gt;됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714665029962&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func test_fetchCurrentLocation을_호출시_위치가_반환됐을_때_위치정보가_제대로_전달되는지_확인() {
    // Given
    if var locationManagerMock = sut.locationManager as? LocationManagerMockInterface {
        locationManagerMock.locationToReturn = {
            return CLLocation(latitude: 37.6134436427887, longitude: 126.926493082645) // locationManager가 반환할 위치값
        }
    }
    
    let expectedLocation = CLLocation(latitude: 37.6134436427887, longitude: 126.926493082645) // 기대값
    var result = CLLocation(latitude: 0.0, longitude: 0.0) // 반환 결과를 받을 변수
    let completionExpectation = expectation(description: &quot;fetchCurrentLocation completion expectation&quot;)
    
    // When
    sut.fetchCurrentLocation { location, _ in
        if let location = location {
            result = location
            completionExpectation.fulfill()
        }
    }
    wait(for: [completionExpectation], timeout: 1)
    
    // Then
    XCTAssertEqual(result.coordinate.latitude, expectedLocation.coordinate.latitude)
    XCTAssertEqual(result.coordinate.longitude, expectedLocation.coordinate.longitude)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;최종적으로 작성된 테스트 코드는 아래와 같습니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1714665217995&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import XCTest
import CoreLocation
@testable import Data

protocol LocationManagerMockInterface: LocationManagerInterface {
    var locationToReturn: (() -&amp;gt; CLLocation?)? { get set }
    var authorizationStatusToReturn: CLAuthorizationStatus { get set }
}

// CLLocationManager 대신 동작할 Mock 객체
class LocationManagerMock: LocationManagerMockInterface {
    var locationManagerDelegate: LocationManagerDelegate?
    
    var locationToReturn: (() -&amp;gt; CLLocation?)? // 테스트 시 전달할 위치값
    var authorizationStatusToReturn: CLAuthorizationStatus = .notDetermined // 테스트 시 전달할 권한 상태
    
    func requestLocation() {
        if let location = locationToReturn?() {
            locationManagerDelegate?.locationManagerAbstract(self, didUpdateLocations: [location])
        } else {
            locationManagerDelegate?.locationManagerAbstract(self, didFailWithError: LocationError.fetchFailed)
        }
    }
    
    func requestWhenInUseAuthorization() {
        locationManagerDelegate?.locationManagerAbstract(self, didChangeAuthorization: authorizationStatusToReturn)
    }
}

final class LocationServiceTests: XCTestCase {
    
    private var sut: LocationService!
    
    override func setUpWithError() throws {
        super.setUp()
        sut = LocationService(locationManager: LocationManagerMock())
    }
    
    override func tearDownWithError() throws {
        super.tearDown()
        sut = nil
    }
  
    func test_fetchCurrentLocation을_호출시_위치가_반환됐을_때_위치정보가_제대로_전달되는지_확인() {
        // Given
        if var locationManagerMock = sut.locationManager as? LocationManagerMockInterface {
            locationManagerMock.locationToReturn = {
                return CLLocation(latitude: 37.6134436427887, longitude: 126.926493082645) // locationManager가 반환할 위치값
            }
        }
        
        let expectedLocation = CLLocation(latitude: 37.6134436427887, longitude: 126.926493082645) // 기대값
        var result = CLLocation(latitude: 0.0, longitude: 0.0) // 반환 결과를 받을 변수
        let completionExpectation = expectation(description: &quot;fetchCurrentLocation completion expectation&quot;)
        
        // When
        sut.fetchCurrentLocation { location, _ in
            if let location = location {
                result = location
                completionExpectation.fulfill()
            }
        }
        wait(for: [completionExpectation], timeout: 1)
        
        // Then
        XCTAssertEqual(result.coordinate.latitude, expectedLocation.coordinate.latitude)
        XCTAssertEqual(result.coordinate.longitude, expectedLocation.coordinate.longitude)
    }
    
    func test_fetchCurrentLocation을_호출시_위치가_반환되지_않으면_에러를_던지는지_확인() {
        // Given
        if var locationManagerMock = sut.locationManager as? LocationManagerMockInterface {
            locationManagerMock.locationToReturn = {
                return nil
            }
        }
        
        var result: (location:CLLocation?, error:Error?)
        let completionExpectation = expectation(description: &quot;fetchCurrentLocation completion expectation&quot;)
        
        // When
        sut.fetchCurrentLocation { location, error in
            result.location = location
            result.error = error
            completionExpectation.fulfill()
        }
        wait(for: [completionExpectation], timeout: 1)
        
        // Then
        XCTAssertNil(result.location)
        XCTAssertNotNil(result.error)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트도 무사히 통과하게 되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-02 오후 7.38.56.png&quot; data-origin-width=&quot;2066&quot; data-origin-height=&quot;1666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w2V1I/btsG7mVxkxo/Ll7Yb3MncVKA1RkwXB9bak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w2V1I/btsG7mVxkxo/Ll7Yb3MncVKA1RkwXB9bak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w2V1I/btsG7mVxkxo/Ll7Yb3MncVKA1RkwXB9bak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw2V1I%2FbtsG7mVxkxo%2FLl7Yb3MncVKA1RkwXB9bak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;699&quot; height=&quot;564&quot; data-filename=&quot;스크린샷 2024-05-02 오후 7.38.56.png&quot; data-origin-width=&quot;2066&quot; data-origin-height=&quot;1666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;아쉬운 점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;이렇게 수정하면서 결과적으로 각각의 테스트 코드 내부에서 인스턴스는 매번 생성하지 않게 되었습니다. 코드 길이도 같이 줄어들 것이라 기대했지만 타입 캐스팅 때문에 코드의 길이가 줄어들지는 않았습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;또한 위치 권한 상태에 따른 동작도 테스트해보려 했지만 Core Location에서 내부적으로 설정되어 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;locationManagerDidChangeAuthorization(_ manager: CLLocationManager)&lt;/b&gt;&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메서드의 호출 조건이 새로운&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;CLLocationManager&lt;/b&gt;&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;클래스 생성 시와 권한 상태가 변경됐을 때라서, 이를 mock 객체로 제어하는 방법이 떠오르지 않아 진행하지 못했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;테스트코드를 처음 작성해봤는데, 처음부터 너무 어려운 테스트 코드를 시도한게 아닐까 싶은 생각이 들었습니다. 나름 자료를 찾아보면서 참고하며 작성한 코드지만 정말 테스트코드로서 의미가 있는건가에 대한 확신은 덜했던 것 같습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래도 이번 경험을 통해 어느 부분에서 테스트가 필요한 것인지, 어떻게 테스트하는 것인지 등을 어느 정도 짚고 넘어갈 수 있었다고 생각합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번에는 동작을 구현한 다음 테스트 코드로 검증을 했지만, 이후에는 TDD 기반으로 개발을 진행해보고 싶다는 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;참고자료&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dzone.com/articles/improving-testability-of-cllocationmanager&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dzone.com/articles/improving-testability-of-cllocationmanager&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://longlivedrgn-miro.tistory.com/61&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://longlivedrgn-miro.tistory.com/61&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>iOS</category>
      <category>Core Location</category>
      <category>core location 테스트</category>
      <category>ios 테스트</category>
      <category>ios 테스트 코드</category>
      <category>TDD</category>
      <category>xcode 테스트</category>
      <author>kybeen</author>
      <guid isPermaLink="true">https://kybeen.tistory.com/146</guid>
      <comments>https://kybeen.tistory.com/146#entry146comment</comments>
      <pubDate>Fri, 3 May 2024 00:57:42 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] Core Location 사용해보기</title>
      <link>https://kybeen.tistory.com/145</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;코로.png&quot; data-origin-width=&quot;1582&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/efD287/btsGOIXL4Nj/M3yBZwYo0zGNyTa96ekJSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/efD287/btsGOIXL4Nj/M3yBZwYo0zGNyTa96ekJSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/efD287/btsGOIXL4Nj/M3yBZwYo0zGNyTa96ekJSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FefD287%2FbtsGOIXL4Nj%2FM3yBZwYo0zGNyTa96ekJSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1582&quot; height=&quot;420&quot; data-filename=&quot;코로.png&quot; data-origin-width=&quot;1582&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플에서는 위치 관련 서비스를 위한 Core Location 이라는 프레임워크를 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Core Location 프레임워크를 사용하여 사용자의 위치 데이터를 받고 처리하는 방법을 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Core Location이란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Core Location을 사용하면 디바이스의 내장 수신 장치들을 활용하여 위치 데이터를 수집하고 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;CLLocationManager&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Core Location 서비스를 구성-시작-종료 하기 위해서는&amp;nbsp;&lt;b&gt;CLLocationManager&lt;/b&gt; 클래스 인스턴스를 사용해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLLocationManager는 아래와 같은 기능들을 지원하는데, 이번 글에서 주로 필요한 내용은 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;위치 변화 추적(Standard and significant location updates)&lt;/b&gt;&lt;/span&gt;이 되겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;위치 변화 추적(Standard and significant location updates)&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지역 모니터링(Region monitoring)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비콘 감지(Beacon ranging)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;나침반 관련 처리(Compass headlings)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;권한 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Core Location에서 제공하는 위치 서비스 기능을 사용하려면 권한 설정이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  Info.plist에 usage description 작성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;인증 요청 시 메세지에 표시할 내용을 작성해줍니다. Info.plist에 관련 key를 추가하여, string value로 앱이 위치 데이터에 액세스하려는 이유를 설명하는 내용을 작성해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1472&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OtxhA/btsGNEWX3xm/51uixK3zc16Kskbu09RRr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OtxhA/btsGNEWX3xm/51uixK3zc16Kskbu09RRr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OtxhA/btsGNEWX3xm/51uixK3zc16Kskbu09RRr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOtxhA%2FbtsGNEWX3xm%2F51uixK3zc16Kskbu09RRr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;669&quot; height=&quot;269&quot; data-origin-width=&quot;1472&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위치 데이터의 접근 수준에 따라 서로 다른 key를 설정해주어야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Privacy - Location When In Use Usage Description&lt;/b&gt;&lt;/span&gt; : &lt;b&gt;When in Use / Always&lt;/b&gt; 권한 모두 필요&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Privacy - Location Always and When In Use Usage Description&lt;/b&gt;&lt;/span&gt; : &lt;b&gt;Always&lt;/b&gt;일 때 필요&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Privacy - Location Usage Description&lt;/b&gt;&lt;/span&gt; : &lt;b&gt;macOS&lt;/b&gt; 앱일 경우 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Core Location은 2가지 접근 수준을 지원합니다. Always의 경우에는 내비게이션 같은 앱이 아닌 이상 배터리 효율을 위해 사용하지 않는 것이 좋습니다. 저는 When in Use 수준으로 사용할 것이기 때문에 Info.plist의 key로 &lt;b&gt;&lt;i&gt;Privacy - Location When In Use Usage Description&lt;/i&gt;&lt;/b&gt;만 추가해주었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;When in Use&lt;/b&gt;&lt;/span&gt; : 앱을 사용할 때만 위치 업데이트를 사용&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;Always&lt;/b&gt;&lt;/span&gt; : 항상 위치 업데이트 사용 가능 (백그라운드에서도)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  위치 서비스 권한 요청하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 앱에서 코드로 권한을 요청하려면 &lt;span style=&quot;color: #006dd7;&quot;&gt;앱의 현재 인증 상태를 확인&lt;/span&gt;하고, &lt;span style=&quot;color: #006dd7;&quot;&gt;필요한 경우에 인증 요청&lt;/span&gt;을 하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;CLLocationManager&lt;/b&gt;&lt;/i&gt; 객체의 &lt;i&gt;&lt;b&gt;authorizationStatus&lt;/b&gt;&lt;/i&gt; 프로퍼티를 통해 현재 앱의 인증 상태를 받아올 수 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt; &lt;b&gt;CLLocationManagerDelegate&lt;/b&gt;&lt;/i&gt; 프로토콜을 채택한 location manager 객체에서 &lt;i&gt;&lt;b&gt;locationManagerDidChangeAuthorization(_:)&lt;/b&gt;&lt;/i&gt; 메서드를 통해 인증 상태에 따른 처리를 할 수 있습니다. 해당 메서드는 &lt;span style=&quot;color: #8a3db6;&quot;&gt;앱이 location manager 객체를 생성했을 때와 인증 상태가 변경될 때마다 호출&lt;/span&gt;됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;(코드 기반의 UIKit으로 작성된 코드입니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰 컨트롤러에서 CLLocationManager 객체를 생성하고, 뷰 컨트롤러를 델리게이트로 지정해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 뷰 컨트롤에서 CLLocationManagerDelegate 프로토콜을 채택하고, locationManagerDidChangeAuthorization(_:) 메서드를 구현하여 인증 상태에 따른 처리 코드를 작성해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;When in Use&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;레벨의 승인을 요청할 때는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;&lt;b&gt;locationManager.requestWhenInUseAuthorization&lt;/b&gt;&lt;/i&gt;&lt;i&gt;&lt;b&gt;()&lt;/b&gt;&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 사용해 요청할 수 있습니다. &amp;nbsp;(Always 레벨이라면 requestAlwaysAuthorization() 이겠죠?)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1713802564521&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import CoreLocation

public final class MainViewController: UIViewController {
    ...
    let locationManager: CLLocationManager
    
    init() {
        print(&quot;locationManager 인스턴스 생성!!&quot;)
        locationManager = CLLocationManager()
        super.init(nibName: nil, bundle: nil)
    }
    
	...
    
    public override func viewDidLoad() {
        super.viewDidLoad()
        locationManager.delegate = self
    }
    
	...
}

extension MainViewController: CLLocationManagerDelegate {
    
    public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        // CLLocationManager 객체의 athorizationStatus 프로퍼티를 통해 앱의 현재 인증 상태를 확인 가능
        switch manager.authorizationStatus {
        case .authorizedWhenInUse: // 위치 서비스를 사용 가능한 상태
            print(&quot;위치 서비스 사용 가능&quot;)
            break
        case .restricted, .denied: // 위치 서비스를 사용 가능하지 않은 상태
            print(&quot;위치 서비스 사용 불가&quot;)
            break
        case .notDetermined: // 권한 설정이 되어 있지 않은 상태
            print(&quot;권한 설정 필요&quot;)
            locationManager.requestWhenInUseAuthorization() // 권한 요청
            break
        default:
            break
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 설명했다시피 &lt;i&gt;locationManagerDidChangeAuthorization(_:)&lt;/i&gt; 메서드는 &lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;location manager 객체가 생성되었을 때&lt;/span&gt;&lt;/b&gt;와 &lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;인증 상태가 변경될 때&lt;/b&gt;&lt;/span&gt;마다 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 프로젝트를 실행하기 되면 해당 뷰 컨트롤러가 초기화될 때 CLLocationManager 클래스의 인스턴스인 &lt;u&gt;locationManager 객체도 초기화되면서 델리게이트 메서드를 호출&lt;/u&gt;하여 인증 상태를 한 번 확인하게 됩니다. 이 때는 아직 권한 설정이 되어 있지 않기 때문에 &lt;i&gt;&lt;b&gt;&quot;권한 설정 필요&quot;&lt;/b&gt;&lt;/i&gt; 라는 메세지가 출력될 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후 권한 요청 팝업이 뜨고, &lt;u&gt;권한을 설정하게 된다면 다시 한 번 델리게이트 메서드가 호출&lt;/u&gt;되며 설정된 인증 상태를 확인하게 됩니다. 만약 승인을 했다면 &lt;i&gt;&lt;b&gt;&quot;위치 서비스 사용 가능&quot;&lt;/b&gt;&lt;/i&gt; 이라고 메세지가 출력될 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면-기록-2024-04-23-오전-1.21.57.gif&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;1426&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DAp2L/btsGQmulO9q/PKUOlFaK5cgVIzO2g3kPI1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DAp2L/btsGQmulO9q/PKUOlFaK5cgVIzO2g3kPI1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DAp2L/btsGQmulO9q/PKUOlFaK5cgVIzO2g3kPI1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/DAp2L/btsGQmulO9q/PKUOlFaK5cgVIzO2g3kPI1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;421&quot; height=&quot;413&quot; data-filename=&quot;화면-기록-2024-04-23-오전-1.21.57.gif&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;1426&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;현재 유저의 위치 받아오기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 위치 데이터에 대한 접근 권한을 받았으니 위치 데이터를 활용하는 법을 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Core Location은 여러 종류의 위치 서비스를 제공하는데, 앱에서 위치 데이터의 활용 용도와 배터리 효율을 고려하여 CLLocationManager 객체가 적절한 메서드를 호출하도록 구성해주어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLLocationManager 객체에서 제공하는 &lt;b&gt;Standard location service(표준 위치 서비스)&lt;/b&gt;와 관련된 메서드&amp;amp;프로퍼티는 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;i&gt;&lt;b&gt;func startUpdatingLocation()&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; : 유저의 현재 위치를 보고하는 업데이트를 시작 (추적 시작)&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;i&gt;&lt;b&gt;func stopUpdatingLocation()&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; : 위치 업데이트를 중단&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;func requestLocation()&lt;/b&gt;&lt;/span&gt;&lt;/i&gt; : 유저의 현재 위치를 1번 요청&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;var pausesLocationUpdatesAutomatically: Bool&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: location manager 객체가 위치 업데이트를 직접 일시 중지 할 수 있는지에 대한 프로퍼티&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;i&gt;&lt;b&gt;var allowsBackgroundLocationUpdates: Bool&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 앱이 백그라운드에서도 위치 업데이트를 받을 수 있는지에 대한 프로퍼티&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;i&gt;&lt;b&gt;var showsBackgroundLocationIndicator: Bool&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 앱이 백그라운드에서 위치 서비스를 사용하고 있을 때 상단 status 바에 표시할지 말지&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;i&gt;&lt;b&gt;var activityType: CLActivityType&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 앱이 앱의 위치 세션에서 사용자가 일반적으로 수행할 것으로 기대하는 활동 유형.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;i&gt;&lt;b&gt;enum CLActivityType&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; : 위치 업데이트와 관련된 활동 유형&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 간단하게 버튼을 누르면 현재 위치를 1번 받아와서 화면에 표시해주도록 구현해보려고 합니다. 그렇게 하기 위해서는 이 중에서 &lt;a href=&quot;https://developer.apple.com/documentation/corelocation/cllocationmanager/1620548-requestlocation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;i&gt;&lt;b&gt;requestLocation()&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/a&gt; 메서드를 활용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  requeestLocation() 메서드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLLocationManager 객체가 &lt;i&gt;requestLocation()&lt;/i&gt; 메서드를 호출하면, &lt;b&gt;델리게이트 객체의 &lt;i&gt;locationManager(_:didUpdateLocations:)&lt;/i&gt; 메서드를 통해 위치 데이터를 받아올 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;⚠️ requestLocation() 메서드 사용 시에는 아래 2가지 델리게이트 메서드를 &lt;span style=&quot;color: #ee2323;&quot;&gt;반드시&lt;/span&gt; 구현해주어야 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;i&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;locationManager(_:didUpdateLocations:)&lt;/span&gt;&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;locationManager(_:didFailWithError:)&lt;/span&gt;&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면의 버튼을 클릭했을 때 locationManager가 requestLocation()를 호출하고, 델리게이트 메서드에서는 위치 데이터를 받아서 화면에 표시하도록 코드를 작성해 보았습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713804258531&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public final class MainViewController: UIViewController {
	...
    
    public override func viewDidLoad() {
        super.viewDidLoad()
        locationManager.delegate = self
        customView.locationButton.addTarget(self, action: #selector(loadUserLocation), for: .touchUpInside)
    }
    
	...
    
    @objc func loadUserLocation() {
        locationManager.requestLocation()
    }
}

extension MainViewController: CLLocationManagerDelegate {
	...
    
    public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        if let coordinate = locations.last?.coordinate {
            customView.locationLabel.text = &quot;현재 위치 : \(coordinate.latitude), \(coordinate.longitude)&quot;
        }
    }
    
    public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print(&quot;위치 정보를 받아오는 데 실패함&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 빌드하면 아래와 같이 동작하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Simulator Screen Recording - iPhone 15 Pro - 2024-04-23 at 01.46.58.gif&quot; data-origin-width=&quot;295&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bURCXd/btsGQkXB6cX/oPjP1yZ81BYP6fAkBLcwE1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bURCXd/btsGQkXB6cX/oPjP1yZ81BYP6fAkBLcwE1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bURCXd/btsGQkXB6cX/oPjP1yZ81BYP6fAkBLcwE1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bURCXd/btsGQkXB6cX/oPjP1yZ81BYP6fAkBLcwE1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;214&quot; height=&quot;464&quot; data-filename=&quot;Simulator Screen Recording - iPhone 15 Pro - 2024-04-23 at 01.46.58.gif&quot; data-origin-width=&quot;295&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;전체 코드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ViewController&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1713804545264&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import UIKit
import CoreLocation

public final class MainViewController: UIViewController {
    
    let customView = MainView()
    let locationManager: CLLocationManager
    
    init() {
        print(&quot;locationManager 인스턴스 생성!!&quot;)
        locationManager = CLLocationManager()
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError(&quot;init(coder:) has not been implemented&quot;)
    }
    
    public override func viewDidLoad() {
        super.viewDidLoad()
        locationManager.delegate = self
        customView.locationButton.addTarget(self, action: #selector(loadUserLocation), for: .touchUpInside)
    }
    
    public override func loadView() {
        view = customView
    }
    
    @objc func loadUserLocation() {
        locationManager.requestLocation()
    }
}

extension MainViewController: CLLocationManagerDelegate {
    
    public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        // CLLocationManager 객체의 athorizationStatus 프로퍼티를 통해 앱의 현재 인증 상태를 확인 가능
        switch manager.authorizationStatus {
        case .authorizedWhenInUse: // 위치 서비스를 사용 가능한 상태
            print(&quot;위치 서비스 사용 가능&quot;)
            break
        case .restricted, .denied: // 위치 서비스를 사용 가능하지 않은 상태
            print(&quot;위치 서비스 사용 불가&quot;)
            break
        case .notDetermined: // 권한 설정이 되어 있지 않은 상태
            print(&quot;권한 설정 필요&quot;)
            locationManager.requestWhenInUseAuthorization() // 권한 요청
            break
        default:
            break
        }
    }
    
    public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        if let coordinate = locations.last?.coordinate {
            customView.locationLabel.text = &quot;현재 위치 : \(coordinate.latitude), \(coordinate.longitude)&quot;
        }
    }
    
    public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print(&quot;위치 정보를 받아오는 데 실패함&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;View&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1713804569697&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import UIKit
import SnapKit

public final class MainView: UIView {
    
    lazy var locationLabel: UILabel = {
        let label = UILabel()
        label.textColor = .black
        label.text = &quot;현재 위치 : &quot;
        return label
    }()
    
    lazy var locationButton: UIButton = {
        let config = UIButton.Configuration.filled()
        let button = UIButton(configuration: config)
        button.setTitle(&quot;현재 위치 받아오기&quot;, for: .normal)
        return button
    }()
    
    init() {
        super.init(frame: .zero)
        setUI()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    func setUI() {
        self.backgroundColor = .white
        
        addSubview(locationLabel)
        locationLabel.snp.makeConstraints { make in
            make.center.equalToSuperview()
        }
        
        addSubview(locationButton)
        locationButton.snp.makeConstraints { make in
            make.centerX.equalToSuperview()
            make.top.equalTo(locationLabel.snp.bottom).offset(50)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Core Location의 공식 문서를 읽고 노션에 정리한 링크입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://healthy-degree-cc2.notion.site/Core-Location-a433acae3797473fba627f49f324d326?pvs=4&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://healthy-degree-cc2.notion.site/Core-Location-a433acae3797473fba627f49f324d326?pvs=4&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1713804644901&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Core Location | Notion&quot; data-og-description=&quot;Core Location 이란?&quot; data-og-host=&quot;healthy-degree-cc2.notion.site&quot; data-og-source-url=&quot;https://healthy-degree-cc2.notion.site/Core-Location-a433acae3797473fba627f49f324d326?pvs=4&quot; data-og-url=&quot;https://healthy-degree-cc2.notion.site/a433acae3797473fba627f49f324d326&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c4hqNx/hyVS3GM5yd/C4mIBUue8NIarKou9JpTB1/img.png?width=2000&amp;amp;height=531&amp;amp;face=0_0_2000_531,https://scrap.kakaocdn.net/dn/hiF0j/hyVVxfbOhx/dy78cDbxmeK5LjYDurEGPk/img.png?width=2000&amp;amp;height=531&amp;amp;face=0_0_2000_531&quot;&gt;&lt;a href=&quot;https://healthy-degree-cc2.notion.site/Core-Location-a433acae3797473fba627f49f324d326?pvs=4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://healthy-degree-cc2.notion.site/Core-Location-a433acae3797473fba627f49f324d326?pvs=4&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c4hqNx/hyVS3GM5yd/C4mIBUue8NIarKou9JpTB1/img.png?width=2000&amp;amp;height=531&amp;amp;face=0_0_2000_531,https://scrap.kakaocdn.net/dn/hiF0j/hyVVxfbOhx/dy78cDbxmeK5LjYDurEGPk/img.png?width=2000&amp;amp;height=531&amp;amp;face=0_0_2000_531');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Core Location | Notion&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Core Location 이란?&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;healthy-degree-cc2.notion.site&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/corelocation&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/documentation/corelocation&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1713804677598&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Core Location | Apple Developer Documentation&quot; data-og-description=&quot;Obtain the geographic location and orientation of a device.&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/documentation/corelocation&quot; data-og-url=&quot;https://docs.developer.apple.com/documentation/corelocation&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/qr7Wy/hyVVJ0ZqAq/aiBAKggdZeni6RbMEOBDfk/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/APmv1/hyVVFEiqfT/M64MCf2f4ILH1ZZ1ceZD81/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/corelocation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/documentation/corelocation&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/qr7Wy/hyVVJ0ZqAq/aiBAKggdZeni6RbMEOBDfk/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/APmv1/hyVVFEiqfT/M64MCf2f4ILH1ZZ1ceZD81/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Core Location | Apple Developer Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Obtain the geographic location and orientation of a device.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/corelocation/configuring_your_app_to_use_location_services&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/documentation/corelocation/configuring_your_app_to_use_location_services&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1713804679136&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Configuring your app to use location services | Apple Developer Documentation&quot; data-og-description=&quot;Verify that location services are available and configure your app before you start collecting location data.&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/documentation/corelocation/configuring_your_app_to_use_location_services&quot; data-og-url=&quot;https://docs.developer.apple.com/documentation/corelocation/configuring_your_app_to_use_location_services&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ebLpFD/hyVVyd5Vq5/9qEn7HywtToje5IPb16iok/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/S0Yjk/hyVVJGGDwC/UWUm63MSKttkyKGCJwQ6fK/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/corelocation/configuring_your_app_to_use_location_services&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/documentation/corelocation/configuring_your_app_to_use_location_services&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ebLpFD/hyVVyd5Vq5/9qEn7HywtToje5IPb16iok/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/S0Yjk/hyVVJGGDwC/UWUm63MSKttkyKGCJwQ6fK/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Configuring your app to use location services | Apple Developer Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Verify that location services are available and configure your app before you start collecting location data.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/corelocation/requesting_authorization_to_use_location_services&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/documentation/corelocation/requesting_authorization_to_use_location_services&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1713804689824&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Requesting authorization to use location services | Apple Developer Documentation&quot; data-og-description=&quot;Obtain authorization to use location services and manage changes to your app&amp;rsquo;s authorization status.&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/documentation/corelocation/requesting_authorization_to_use_location_services&quot; data-og-url=&quot;https://docs.developer.apple.com/documentation/corelocation/requesting_authorization_to_use_location_services&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/6sZrz/hyVSYeoOAr/xz5SbMfDoDOXN0Gde2Odf1/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/c2ClEP/hyVVCguNiI/jYF4PFtqayzjqwrWHK6vO1/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/corelocation/requesting_authorization_to_use_location_services&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/documentation/corelocation/requesting_authorization_to_use_location_services&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/6sZrz/hyVSYeoOAr/xz5SbMfDoDOXN0Gde2Odf1/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/c2ClEP/hyVVCguNiI/jYF4PFtqayzjqwrWHK6vO1/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Requesting authorization to use location services | Apple Developer Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Obtain authorization to use location services and manage changes to your app&amp;rsquo;s authorization status.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/corelocation/getting_the_current_location_of_a_device&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/documentation/corelocation/getting_the_current_location_of_a_device&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1713804696324&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Getting the current location of a device | Apple Developer Documentation&quot; data-og-description=&quot;Start location services and provide information the system needs to optimize power usage for those services.&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/documentation/corelocation/getting_the_current_location_of_a_device&quot; data-og-url=&quot;https://docs.developer.apple.com/documentation/corelocation/getting_the_current_location_of_a_device&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bV1vdE/hyVSWnmPDG/FzXCyTq5uzsw1SSoCWNIXK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/xjDwd/hyVSW1YmWM/ZM7rakCfVNaKkVREl8tnAk/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/corelocation/getting_the_current_location_of_a_device&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/documentation/corelocation/getting_the_current_location_of_a_device&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bV1vdE/hyVSWnmPDG/FzXCyTq5uzsw1SSoCWNIXK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/xjDwd/hyVSW1YmWM/ZM7rakCfVNaKkVREl8tnAk/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Getting the current location of a device | Apple Developer Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Start location services and provide information the system needs to optimize power usage for those services.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/corelocation/cllocationmanager&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/documentation/corelocation/cllocationmanager&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1713804703568&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;CLLocationManager | Apple Developer Documentation&quot; data-og-description=&quot;The object that you use to start and stop the delivery of location-related events to your app.&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/documentation/corelocation/cllocationmanager&quot; data-og-url=&quot;https://docs.developer.apple.com/documentation/corelocation/cllocationmanager&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/WyGjW/hyVSUiOqfe/aE4VkjAawJMEQcaSrX8K71/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/gI29m/hyVSUiOqdH/6iy665lnP4pkpum9QrhJ20/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/corelocation/cllocationmanager&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/documentation/corelocation/cllocationmanager&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/WyGjW/hyVSUiOqfe/aE4VkjAawJMEQcaSrX8K71/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/gI29m/hyVSUiOqdH/6iy665lnP4pkpum9QrhJ20/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;CLLocationManager | Apple Developer Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The object that you use to start and stop the delivery of location-related events to your app.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>iOS</category>
      <category>CLLocationManager</category>
      <category>Core Location</category>
      <category>CoreLocation</category>
      <category>iOS 위치</category>
      <category>iOS 위치 권한</category>
      <category>iOS 위치 데이터</category>
      <category>iOS 위치 정보</category>
      <category>iOS 현재 위치</category>
      <author>kybeen</author>
      <guid isPermaLink="true">https://kybeen.tistory.com/145</guid>
      <comments>https://kybeen.tistory.com/145#entry145comment</comments>
      <pubDate>Tue, 23 Apr 2024 01:52:24 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] 외국 밈(meme)에서 많이 쓰는 폰트 적용해보기 (SwiftUI)</title>
      <link>https://kybeen.tistory.com/144</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;인터넷 유머에 익숙한 사람, 특히 외국 밈에 익숙한 사람이라면 아래와 같은 형식의 짤을 많이 봤을 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcfwiK/btsGIVxlczU/JNwm4Y2cVSZnKUVQh1L3Bk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcfwiK/btsGIVxlczU/JNwm4Y2cVSZnKUVQh1L3Bk/img.jpg&quot; data-alt=&quot;출처 :&amp;amp;amp;amp;nbsp;https://www.vox.com/2015/7/26/9036993/meme-font-impact&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcfwiK/btsGIVxlczU/JNwm4Y2cVSZnKUVQh1L3Bk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcfwiK%2FbtsGIVxlczU%2FJNwm4Y2cVSZnKUVQh1L3Bk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;628&quot; height=&quot;330&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 :&amp;amp;amp;nbsp;https://www.vox.com/2015/7/26/9036993/meme-font-impact&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 외국 밈 짤에서는 저렇게 두꺼운 흰색 글자에 검은색 테두리를 갖는 폰트가 사용됩니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;당장 구글에 'meme'이라고만 검색해도 그 폰트를 잔뜩 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2506&quot; data-origin-height=&quot;2550&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3MQXH/btsGI7LbEXO/knKkL8os6jlu9M07lHGGi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3MQXH/btsGI7LbEXO/knKkL8os6jlu9M07lHGGi0/img.png&quot; data-alt=&quot;'meme' 검색 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3MQXH/btsGI7LbEXO/knKkL8os6jlu9M07lHGGi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3MQXH%2FbtsGI7LbEXO%2FknKkL8os6jlu9M07lHGGi0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;608&quot; height=&quot;619&quot; data-origin-width=&quot;2506&quot; data-origin-height=&quot;2550&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;'meme' 검색 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;토이 프로젝트를 하나 만들면서 이 폰트를 사용하고 싶어졌습니다.&lt;br /&gt;해당 프로젝트는 SwiftUI로 진행할 것이기 때문에 SwiftUI로 이 폰트를 사용하는 법을 알아보겠습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;그래서 이 폰트가 뭔가요?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외국 밈에서 많이 사용하는 이 폰트의 이름은 &lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;'Impact(임팩트)'&lt;/b&gt;&lt;/span&gt;라고 합니다.&lt;br /&gt;인터넷 밈에 자주 사용되기 때문에 생겨난지 얼마 되지 않은 폰트라고 생각될 수도 있는데, &lt;u&gt;1965년에 생겨난 매우 근본 있는 폰트&lt;/u&gt;라고 합니다.&lt;br /&gt;사진 위에서도 명확하게 텍스트를 구분시켜주는 서체이기 때문에 밈에 자주 쓰이기 시작했다는 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxDKJa/btsGKvEunnK/M7z7vjNYpHINKWA8qo2VhK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxDKJa/btsGKvEunnK/M7z7vjNYpHINKWA8qo2VhK/img.jpg&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;754&quot; style=&quot;width: 41.423298%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxDKJa/btsGKvEunnK/M7z7vjNYpHINKWA8qo2VhK/img.jpg&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxDKJa%2FbtsGKvEunnK%2FM7z7vjNYpHINKWA8qo2VhK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;754&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EkWYX/btsGIYgzqIz/h0DmMfDwBkJiWbGybLkMd1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EkWYX/btsGIYgzqIz/h0DmMfDwBkJiWbGybLkMd1/img.jpg&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;544&quot; style=&quot;width: 57.41391%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EkWYX/btsGIYgzqIz/h0DmMfDwBkJiWbGybLkMd1/img.jpg&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEkWYX%2FbtsGIYgzqIz%2Fh0DmMfDwBkJiWbGybLkMd1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;544&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;출처 :&amp;amp;nbsp;https://www.vox.com/2015/7/26/9036993/meme-font-impact&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이 포스팅은 SwiftUI에서 사용하는 법을 다루는 것이 메인이기 때문에, Impact 폰트가 밈에서 많이 쓰이게 된 유래에 대한 설명은 아래 글을 참고해주시면 될 것 같습니다.&lt;br /&gt;&lt;a href=&quot;https://www.vox.com/2015/7/26/9036993/meme-font-impact&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://www.vox.com/2015/7/26/9036993/meme-font-impact&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;The reason every meme uses that one font&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;Why do all memes look so similar? There are a few very good reasons.&quot; data-og-host=&quot;www.vox.com&quot; data-og-source-url=&quot;https://www.vox.com/2015/7/26/9036993/meme-font-impact&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/df8C6B/hyVSUBMe3B/smk1uZVhLZHj7Fz3HLtqZ1/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=465_170_733_462,https://scrap.kakaocdn.net/dn/bdT7A1/hyVPKOmf6h/L26FuqmSzw8gkXlEByKySK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=465_170_733_462,https://scrap.kakaocdn.net/dn/tF7bt/hyVSUPkCFj/uwZqLmKcTmRFIQVnK7Mdk0/img.jpg?width=1200&amp;amp;height=754&amp;amp;face=0_0_1200_754&quot; data-og-url=&quot;https://www.vox.com/2015/7/26/9036993/meme-font-impact&quot;&gt;&lt;a href=&quot;https://www.vox.com/2015/7/26/9036993/meme-font-impact&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.vox.com/2015/7/26/9036993/meme-font-impact&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/df8C6B/hyVSUBMe3B/smk1uZVhLZHj7Fz3HLtqZ1/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=465_170_733_462,https://scrap.kakaocdn.net/dn/bdT7A1/hyVPKOmf6h/L26FuqmSzw8gkXlEByKySK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=465_170_733_462,https://scrap.kakaocdn.net/dn/tF7bt/hyVSUPkCFj/uwZqLmKcTmRFIQVnK7Mdk0/img.jpg?width=1200&amp;amp;height=754&amp;amp;face=0_0_1200_754');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;The reason every meme uses that one font&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Why do all memes look so similar? There are a few very good reasons.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.vox.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Impact 폰트 다운로드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Impact 서체는 개인 및 상업적 용도로 무료로 사용하는 것이 가능하다고 합니다.&lt;br /&gt;참고로 폰트는 '폰트 서체 자체'가 아니라 '디지털화한 파일 형식인 ttf'일 때 저작권법으로 보호받는다고 하네요. (&lt;a title=&quot;참고&quot; href=&quot;https://www.tosspayments.com/blog/articles/legal2-2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;참고&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;)&lt;br /&gt;&amp;nbsp;&lt;br /&gt;SwiftUI에서 커스텀 폰트를 적용하려면 해당 폰트의 ttf 파일을 프로젝트에 추가해야 하기 때문에 다운받아주겠습니다.&lt;br /&gt;저는 &lt;a title=&quot;여기&quot; href=&quot;https://cofonts.net/impact-font/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;여기&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;에서 다운받았습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIax3o/btsGIOyn9wC/spmLE4neiKxk5ti28iDOH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIax3o/btsGIOyn9wC/spmLE4neiKxk5ti28iDOH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIax3o/btsGIOyn9wC/spmLE4neiKxk5ti28iDOH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIax3o%2FbtsGIOyn9wC%2FspmLE4neiKxk5ti28iDOH1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;385&quot; height=&quot;245&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;318&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;프로젝트에서 폰트 적용하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Xcode 프로젝트에 폰트 파일을 추가해 주었습니다.&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;폰트 파일을 타겟에 추가해주지 않으면 폰트가 적용되지 않기 때문에 &lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Add to targets&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;에 체크를 해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1468&quot; data-origin-height=&quot;860&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEOwBK/btsGJS7OXe4/92nzlit9v25F0RmPEB0dO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEOwBK/btsGJS7OXe4/92nzlit9v25F0RmPEB0dO1/img.png&quot; data-alt=&quot;Add to targets 체크&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEOwBK/btsGJS7OXe4/92nzlit9v25F0RmPEB0dO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEOwBK%2FbtsGJS7OXe4%2F92nzlit9v25F0RmPEB0dO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;635&quot; height=&quot;372&quot; data-origin-width=&quot;1468&quot; data-origin-height=&quot;860&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Add to targets 체크&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2938&quot; data-origin-height=&quot;824&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XL0uF/btsGKw4vdG1/9tVGxP9SCrtkX5RigndG91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XL0uF/btsGKw4vdG1/9tVGxP9SCrtkX5RigndG91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XL0uF/btsGKw4vdG1/9tVGxP9SCrtkX5RigndG91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXL0uF%2FbtsGKw4vdG1%2F9tVGxP9SCrtkX5RigndG91%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;657&quot; height=&quot;184&quot; data-origin-width=&quot;2938&quot; data-origin-height=&quot;824&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그리고 Info.plist에 &lt;b&gt;Fonts provided by application&lt;/b&gt; 항목을 추가하여 적용하려는 폰트들을 아이템으로 추가해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yXHKb/btsGJgahlsb/W7nnnMHSV99GOEoWw9YMY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yXHKb/btsGJgahlsb/W7nnnMHSV99GOEoWw9YMY1/img.png&quot; data-alt=&quot;Info.plist에 추가&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yXHKb/btsGJgahlsb/W7nnnMHSV99GOEoWw9YMY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyXHKb%2FbtsGJgahlsb%2FW7nnnMHSV99GOEoWw9YMY1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;694&quot; height=&quot;92&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;152&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Info.plist에 추가&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;Text&lt;/b&gt;에 폰트를 적용해주는 코드는 아래와 같습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;Text(&quot;WHY EVERY MEMES USE THIS FONT?&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.foregroundStyle(.white)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.font(.custom(&quot;impact&quot;, size: 30))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;임팩트 폰트가 잘 적용된 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2614&quot; data-origin-height=&quot;1666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dkB8ln/btsGLf16MEt/NvjmTOUDRKENpQafFkzAWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dkB8ln/btsGLf16MEt/NvjmTOUDRKENpQafFkzAWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dkB8ln/btsGLf16MEt/NvjmTOUDRKENpQafFkzAWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdkB8ln%2FbtsGLf16MEt%2FNvjmTOUDRKENpQafFkzAWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;680&quot; height=&quot;433&quot; data-origin-width=&quot;2614&quot; data-origin-height=&quot;1666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;하지만 뭔가 아쉽습니다.&lt;br /&gt;왜냐하면 텍스트에 &lt;b&gt;검은색 테두리&lt;/b&gt;가 빠져 있기 때문이죠&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEWjAi/btsGI3IQ6Ce/kKYWlKCSergCs9lawb7RKk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEWjAi/btsGI3IQ6Ce/kKYWlKCSergCs9lawb7RKk/img.jpg&quot; data-alt=&quot;출처 :&amp;amp;amp;amp;nbsp;https://sproutsocial.com/glossary/meme/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEWjAi/btsGI3IQ6Ce/kKYWlKCSergCs9lawb7RKk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEWjAi%2FbtsGI3IQ6Ce%2FkKYWlKCSergCs9lawb7RKk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;411&quot; height=&quot;275&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 :&amp;amp;amp;nbsp;https://sproutsocial.com/glossary/meme/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;검은색 테두리 적용하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜 밈 서체가 되기 위해 검은색 테두리가 적용된 &lt;b&gt;Text&lt;/b&gt;를 만들어보겠습니다.&lt;br /&gt;애플 쪽에서 기본적으로 테두리가 적용된 텍스트를 지원하지는 않기 때문에 직접 테두리를 넣어줘야 합니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. .bold()가 적용된 Text 깔아주기 - ❌&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ZStack에서 같은 Text를 2개 겹치고, &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;더 뒤에 있는 Text에 검은색 서체와 .bold()를 적용&lt;/b&gt;&lt;/span&gt;해줘봤습니다.&lt;br /&gt;하지만 원하던 모양이 나오지 않았습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;Text(&quot;WHY EVERY MEMES USE THIS FONT?&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.foregroundStyle(.black)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.font(.custom(&quot;impact&quot;, size: 30.5))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.bold()
Text(&quot;WHY EVERY MEMES USE THIS FONT?&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.foregroundStyle(.white)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.font(.custom(&quot;impact&quot;, size: 30))&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;1338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4LvEK/btsGLudPU0r/MMzsQbSCs5EEZd6N8HvQu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4LvEK/btsGLudPU0r/MMzsQbSCs5EEZd6N8HvQu0/img.png&quot; data-alt=&quot;bold 텍스트 깔아주기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4LvEK/btsGLudPU0r/MMzsQbSCs5EEZd6N8HvQu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4LvEK%2FbtsGLudPU0r%2FMMzsQbSCs5EEZd6N8HvQu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;211&quot; height=&quot;434&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;1338&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;bold 텍스트 깔아주기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;2. 주위로 offset을 적용한 Text 여러개 겹치기 - ✅&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 방법이랑 비슷하지만 이번에는 bold 텍스트를 깔아주는 것이 아니라, &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Text의 주변 방향으로 offset만큼 이동시킨 Text들을 여러개&lt;/b&gt;&lt;/span&gt; 깔아주었습니다. OutlineText라는 이름의 커스텀 Text로 정의해주었습니다.&lt;br /&gt;(&lt;a href=&quot;https://stackoverflow.com/questions/57334125/how-to-make-text-stroke-in-swiftui&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;background-color: #e6f5ff;&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;여기&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;코드를 참고하여 만들었습니다.&lt;/span&gt;)&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;struct OutlineText: View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;let text: String
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;let width: CGFloat
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;let color: Color
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var body: some View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ZStack{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ZStack{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Text(text).offset(x:&amp;nbsp;&amp;nbsp;width, y:&amp;nbsp;&amp;nbsp;width)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.font(.impactFont30)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Text(text).offset(x: -width, y: -width)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.font(.impactFont30)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Text(text).offset(x: -width, y:&amp;nbsp;&amp;nbsp;width)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.font(.impactFont30)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Text(text).offset(x:&amp;nbsp;&amp;nbsp;width, y: -width)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.font(.impactFont30)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.foregroundColor(color)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Text(text)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.font(.impactFont30)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.foregroundStyle(.white)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;크기 30에 impact 폰트가 적용된 impactFont30 값을 Font 값의 익스텐션에서 정의해주었습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;extension Font {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;static let impactFont30: Font = .custom(&quot;impact&quot;, size: 30)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;OutlineText를 적용한 모습은 아래와 같습니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;struct TestView: View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var body: some View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ZStack {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Color(.gray)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OutlineText(text: &quot;WHY EVERY MEMES USE THIS FONT?&quot;, width: 1, color: .black)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;꽤나 괜찮은 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;1318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dUJHEf/btsGIgaOgk4/Bi8FTntcOGwm5oC9DB50M1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dUJHEf/btsGIgaOgk4/Bi8FTntcOGwm5oC9DB50M1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dUJHEf/btsGIgaOgk4/Bi8FTntcOGwm5oC9DB50M1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdUJHEf%2FbtsGIgaOgk4%2FBi8FTntcOGwm5oC9DB50M1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;311&quot; height=&quot;636&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;1318&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. shadow 효과가 적용된 커스텀 modifier 정의하기 - ✅&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 shadow 효과를 적용해주는 커스텀 modifier를 정의해주는 방법입니다.&lt;br /&gt;(&lt;a title=&quot;여기&quot; href=&quot;https://www.youtube.com/watch?v=MJLoKS1i1oQ&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;여기&lt;/span&gt;&lt;/span&gt;&lt;/a&gt; 코드를 참고하여 만들었습니다.)&lt;br /&gt;&amp;nbsp;&lt;br /&gt; &amp;nbsp;ViewModifier 프로토콜을 채택한 ShadowOutlineModifier를 정의해줍니다.&lt;br /&gt;body(content:) 메서드를 통해 modifier를 호출한 View의 body를 받아와서 원하는 처리가 가능합니다.&lt;br /&gt;아래 modifier에서는 shadow 효과를 인자로 받은 lineWidth 값에 맞게 점점 넓게 적용해줍니다. 이렇게 하면 lineWidth 넓이 만큼 그림자가 지속적으로 진하게 적용됩니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;  View의 익스텐션으로 modifier를 정의해줍니다.&lt;br /&gt;.modifier(ShadowOutlineModifier()) 형식으로 사용할 수도 있지만 편의를 위해 식별하기 쉬운 modifier 명으로 정의해줍니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;struct ShadowOutlineModifier: ViewModifier {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var color: Color
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var lineWidth: Int
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;func body(content: Content) -&amp;gt; some View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var newContent = AnyView(content)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for _ in 0..&amp;lt;lineWidth {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;newContent = AnyView(newContent.shadow(color: color, radius: 1))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return newContent
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
}

extension View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;func shadowOutline(color: Color, lineWidth: Int) -&amp;gt; some View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.modifier(ShadowOutlineModifier(color: color, lineWidth: lineWidth))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;modifier를 적용하는 코드입니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;struct TestView: View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var body: some View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ZStack {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Color(.gray)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Text(&quot;WHY EVERY MEMES USE THIS FONT?&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.font(.impactFont30)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.foregroundStyle(.white)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.shadowOutline(color: .black, lineWidth: 3)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;1334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOqyuX/btsGJtggcsr/gtPPzqKC69iM4xqDo5eRz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOqyuX/btsGJtggcsr/gtPPzqKC69iM4xqDo5eRz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOqyuX/btsGJtggcsr/gtPPzqKC69iM4xqDo5eRz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOqyuX%2FbtsGJtggcsr%2FgtPPzqKC69iM4xqDo5eRz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;299&quot; height=&quot;610&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;1334&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실제 짤 만들어보기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 impact 폰트에 진짜 사진도 같이 활용해서 실제 밈 짤처럼 만들어보겠습니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;2. 커스텀 Text 사용 (offset 적용)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;swift&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;struct TestView: View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var body: some View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ZStack {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Image(&quot;squirrel&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.resizable()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.aspectRatio(contentMode: .fit)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OutlineText(text: &quot;\n\n\n\n\nI DIDN'T STEAL IT!!!!&quot;, width: 2, color: .black)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;1336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ckgo2/btsGJR8ZAEk/M6NMw3qnSpjmKdvBE6XSC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ckgo2/btsGJR8ZAEk/M6NMw3qnSpjmKdvBE6XSC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ckgo2/btsGJR8ZAEk/M6NMw3qnSpjmKdvBE6XSC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCkgo2%2FbtsGJR8ZAEk%2FM6NMw3qnSpjmKdvBE6XSC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;322&quot; height=&quot;668&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;1336&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;3. 커스텀 modifier 사용 (그림자 효과)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;swift&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;struct TestView: View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var body: some View {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ZStack {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Image(&quot;squirrel&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.resizable()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.aspectRatio(contentMode: .fit)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Text(&quot;\n\n\n\n\nI DIDN'T STEAL IT!!!!&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.font(.impactFont30)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.foregroundStyle(.white)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.shadowOutline(color: .black, lineWidth: 5)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;1332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sYcUJ/btsGLuyd6Tr/vVEHbO03dH2ZYgFxGf1xzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sYcUJ/btsGLuyd6Tr/vVEHbO03dH2ZYgFxGf1xzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sYcUJ/btsGLuyd6Tr/vVEHbO03dH2ZYgFxGf1xzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsYcUJ%2FbtsGLuyd6Tr%2FvVEHbO03dH2ZYgFxGf1xzK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;318&quot; height=&quot;664&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;1332&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그럴싸하네요&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.vox.com/2015/7/26/9036993/meme-font-impact&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;https://www.vox.com/2015/7/26/9036993/meme-font-impact&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://www.tosspayments.com/blog/articles/legal2-2&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;https://www.tosspayments.com/blog/articles/legal2-2&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://cofonts.net/impact-font/&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;https://cofonts.net/impact-font/&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://stackoverflow.com/questions/57334125/how-to-make-text-stroke-in-swiftui&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;https://stackoverflow.com/questions/57334125/how-to-make-text-stroke-in-swiftui&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=MJLoKS1i1oQ&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;https://www.youtube.com/watch?v=MJLoKS1i1oQ&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>iOS</category>
      <category>SwiftUI</category>
      <category>SwiftUI 커스텀 modifier</category>
      <category>SwiftUI 커스텀 텍스트</category>
      <category>SwiftUI 커스텀 폰트</category>
      <category>SwiftUI 텍스트 테두리</category>
      <category>SwiftUI 폰트</category>
      <category>밈 폰트</category>
      <category>외국 밈</category>
      <category>외국 밈 폰트</category>
      <author>kybeen</author>
      <guid isPermaLink="true">https://kybeen.tistory.com/144</guid>
      <comments>https://kybeen.tistory.com/144#entry144comment</comments>
      <pubDate>Thu, 18 Apr 2024 17:45:48 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] M1 맥북 + Xcode 15 환경에서 SwiftLint 적용 시 오류 처리</title>
      <link>https://kybeen.tistory.com/143</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;M1 맥북 + Xcode 15 환경에서 SwiftLint를 적용해보면서 겪은 문제에 대해 정리한 글입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 설명된 오류처리와 함께 &lt;b&gt;추가적인 설정과 SwiftLint 설치 및 적용 과정&lt;/b&gt;도 정리한 내용을 보고 싶다면&lt;b&gt;&amp;nbsp;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://healthy-degree-cc2.notion.site/SwiftLint-c83639dcaecb4a27b400ca0430ed8e69?pvs=4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;&lt;/span&gt;&lt;/b&gt;를 참고해주세요&lt;/p&gt;
&lt;figure id=&quot;og_1709056549069&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;SwiftLint 적용하기 | Notion&quot; data-og-description=&quot;1. SwiftLint 설치&quot; data-og-host=&quot;healthy-degree-cc2.notion.site&quot; data-og-source-url=&quot;https://healthy-degree-cc2.notion.site/SwiftLint-c83639dcaecb4a27b400ca0430ed8e69?pvs=4&quot; data-og-url=&quot;https://healthy-degree-cc2.notion.site/c83639dcaecb4a27b400ca0430ed8e69&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/baO8np/hyVqtMvi0h/EMLbbfgIMxbIsdkgA3wje0/img.png?width=2000&amp;amp;height=1400&amp;amp;face=0_0_2000_1400,https://scrap.kakaocdn.net/dn/hiHgB/hyVqhSRcDZ/35cySUeVhhF87OKwntylyk/img.png?width=2000&amp;amp;height=1400&amp;amp;face=0_0_2000_1400&quot;&gt;&lt;a href=&quot;https://healthy-degree-cc2.notion.site/SwiftLint-c83639dcaecb4a27b400ca0430ed8e69?pvs=4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://healthy-degree-cc2.notion.site/SwiftLint-c83639dcaecb4a27b400ca0430ed8e69?pvs=4&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/baO8np/hyVqtMvi0h/EMLbbfgIMxbIsdkgA3wje0/img.png?width=2000&amp;amp;height=1400&amp;amp;face=0_0_2000_1400,https://scrap.kakaocdn.net/dn/hiHgB/hyVqhSRcDZ/35cySUeVhhF87OKwntylyk/img.png?width=2000&amp;amp;height=1400&amp;amp;face=0_0_2000_1400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;SwiftLint 적용하기 | Notion&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. SwiftLint 설치&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;healthy-degree-cc2.notion.site&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;‼️ 만약 &lt;b&gt;M1 이상 맥북 + Xcode 15 이상 버전&lt;/b&gt;의 개발 환경이라면 아래에서 소개드릴 2가지 문제를 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;반드시 해결&lt;/span&gt;&lt;/b&gt;해주어야 SwiftLint를 정상적으로 사용할 수 있는 것 같습니다.&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt; Homebrew로 설치 시 Apple Silicon에서 SwiftLint를 찾지 못하는 문제&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Apple Silicon(M1, M2 등)에서 Homebrew를 통해 SwiftLint를 설치하는 경우, 빌드 시에 아래와 같은 경고가 발생합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot; data-token-index=&quot;0&quot;&gt;warning: SwiftLint not installed, download from&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/realm/SwiftLint&quot;&gt;https://github.com/realm/SwiftLint&lt;/a&gt;&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ㅊㅊ.png&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0V9KW/btsFjJE3Jo0/uFk4J8hKjv7p1SzKOMjzek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0V9KW/btsFjJE3Jo0/uFk4J8hKjv7p1SzKOMjzek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0V9KW/btsFjJE3Jo0/uFk4J8hKjv7p1SzKOMjzek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0V9KW%2FbtsFjJE3Jo0%2FuFk4J8hKjv7p1SzKOMjzek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;447&quot; height=&quot;71&quot; data-filename=&quot;ㅊㅊ.png&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;110&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;이는 Apple Silicon에서 Homebrew가 기본적으로 &lt;b&gt;&lt;i&gt;/opt/homebrew/bin&lt;/i&gt;&lt;/b&gt; 폴더에 바이너리를 설치하는데, Xcode가 해당 경로를 몰라서 SwiftLint를 찾지 못하기 때문에 나타나는 경고입니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;✅ Xcode 프로젝트의 &lt;b&gt;Build Phases&lt;/b&gt; 옵션에서 추가해준 SwiftLint 실행 스크립트에서 &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;i&gt;&lt;b&gt;PATH&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; 환경 변수에 &lt;b&gt;&lt;i&gt;/opt/homebrew/bin&lt;/i&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span&gt; 경로를 지정해주면 됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1709055424862&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 이 코드를 추가
if [[ &quot;$(uname -m)&quot; == arm64 ]]; then
    export PATH=&quot;/opt/homebrew/bin:$PATH&quot;
fi

if which swiftlint &amp;gt; /dev/null; then
  swiftlint
else
  echo &quot;warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint&quot;
fi&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;dd.png&quot; data-origin-width=&quot;1314&quot; data-origin-height=&quot;556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dMfOOA/btsFiPyLrdn/UhVuQShka7H5UxNXzxhbh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dMfOOA/btsFiPyLrdn/UhVuQShka7H5UxNXzxhbh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dMfOOA/btsFiPyLrdn/UhVuQShka7H5UxNXzxhbh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdMfOOA%2FbtsFiPyLrdn%2FUhVuQShka7H5UxNXzxhbh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;612&quot; height=&quot;259&quot; data-filename=&quot;dd.png&quot; data-origin-width=&quot;1314&quot; data-origin-height=&quot;556&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;Xcode 15에서 - Failed with a nonzero exit code 또는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot; data-token-index=&quot;1&quot;&gt;error: Sandbox: swiftlint(19427) deny(1) file-read-data.&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;오류&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftLint의 깃허브를 살펴보니 해당 문제는 Xcode 15버전부터 발생하는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;린트 설정 후 빌드를 하면 아래 사진처럼 &lt;span style=&quot;color: #ee2323;&quot;&gt;스크립트 실행이 실패했다는 오류가 발생&lt;/span&gt;합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ㅇㅇ.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnFNqY/btsFlOy4DZs/mVlSdwGR1bqWEqww0ZZrxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnFNqY/btsFlOy4DZs/mVlSdwGR1bqWEqww0ZZrxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnFNqY/btsFlOy4DZs/mVlSdwGR1bqWEqww0ZZrxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnFNqY%2FbtsFlOy4DZs%2FmVlSdwGR1bqWEqww0ZZrxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;413&quot; height=&quot;142&quot; data-filename=&quot;ㅇㅇ.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  이는 Xcode 15에서 &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;ENABLE_USER_SCRIPT_SANDBOXING&lt;/b&gt;&lt;/span&gt; 이라는 빌드 설정의 기본값을 &lt;b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;NO&lt;/span&gt;&lt;/b&gt;에서 &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;YES&lt;/b&gt;&lt;/span&gt;로 변경했기 때문에 발생하는 오류입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;이 설정은 빌드 시스템에게 소스 파일이나 중간 빌드 개체에 액세스하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;i&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot; data-token-index=&quot;1&quot;&gt;scripts phases&lt;/span&gt;&lt;/i&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;를 차단할지 여부에 대한 내용인데, 이 값이&lt;b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #ef5369; text-align: start;&quot; data-token-index=&quot;3&quot;&gt;YES&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;로 되어 있어서 추가한 SwiftLint Script가 소스 파일에 접근하는 것이 차단되는 것입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;추가적으로 SwiftLint의 깃허브에서도 해당 변경사항으로 인한 문제를 언급하고 있는데, 여기서는 &lt;b&gt;&lt;i&gt;Failed with a nonzero exit code&lt;/i&gt;&lt;/b&gt;가 아닌 &lt;span style=&quot;color: #000000; text-align: start;&quot; data-token-index=&quot;0&quot;&gt;&lt;i&gt;&lt;b&gt;error: Sandbox: swiftlint(19427) deny(1) file-read-data.&lt;/b&gt;&lt;/i&gt; 라는 에러 메세지가 나올 것이라고 말해주고 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;어쨌든 두 가지 오류 모두 같은 방법으로 해결이 가능합니다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;✅ Xcode에서 &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;ENABLE_USER_SCRIPT_SANDBOXING&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 설정값을 다시&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;NO&lt;/span&gt;&lt;/b&gt;로 변경해주면 됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래 사진을 따라서 프로젝트 타겟의 &lt;i&gt;&lt;b&gt;Build Settings&lt;/b&gt;&lt;/i&gt;로 들어간 뒤, &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;ENABLE_USER_SCRIPT_SANDBOXING&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;설정값을 &lt;b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;NO&lt;/span&gt;&lt;/b&gt;로 변경해줍니다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ㅇㅇㄴ.png&quot; data-origin-width=&quot;2500&quot; data-origin-height=&quot;652&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/weXQS/btsFiO0SIhB/e0S9rb7RuFn2xBhnDTMvzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/weXQS/btsFiO0SIhB/e0S9rb7RuFn2xBhnDTMvzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/weXQS/btsFiO0SIhB/e0S9rb7RuFn2xBhnDTMvzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FweXQS%2FbtsFiO0SIhB%2Fe0S9rb7RuFn2xBhnDTMvzk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2500&quot; height=&quot;652&quot; data-filename=&quot;ㅇㅇㄴ.png&quot; data-origin-width=&quot;2500&quot; data-origin-height=&quot;652&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해주면 정상적으로 빌드가 될 것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;참고 사이트&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SwiftLint 공식문서(깃허브) : &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;a style=&quot;letter-spacing: 0px;&quot; href=&quot;https://github.com/realm/SwiftLint/blob/main/README_KR.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/realm/SwiftLint/blob/main/README_KR.md&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/realm/SwiftLint/issues/5130&quot;&gt;https://github.com/realm/SwiftLint/issues/5130&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1709056459984&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;SwiftLint Run Script Phase fails on Xcode 15 &amp;middot; Issue #5130 &amp;middot; realm/SwiftLint&quot; data-og-description=&quot;New Issue Checklist Updated SwiftLint to the latest version I searched for existing GitHub issues Describe the bug Using SwiftLint on Xcode 15 Beta causes an error: &amp;quot;Command PhaseScriptExecution fa...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/realm/SwiftLint/issues/5130&quot; data-og-url=&quot;https://github.com/realm/SwiftLint/issues/5130&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Jc30F/hyVqkPAEyP/XKllA5YWrUlIugiLY0bz2K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/realm/SwiftLint/issues/5130&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/realm/SwiftLint/issues/5130&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Jc30F/hyVqkPAEyP/XKllA5YWrUlIugiLY0bz2K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;SwiftLint Run Script Phase fails on Xcode 15 &amp;middot; Issue #5130 &amp;middot; realm/SwiftLint&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;New Issue Checklist Updated SwiftLint to the latest version I searched for existing GitHub issues Describe the bug Using SwiftLint on Xcode 15 Beta causes an error: &quot;Command PhaseScriptExecution fa...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://thisdevbrain.com/swiftlint-permission-issue/&quot;&gt;How to fix the &quot;The file &amp;ldquo;.swiftlint.yml&amp;rdquo; couldn&amp;rsquo;t be opened because you don&amp;rsquo;t have permission to view it&quot; issue&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1709056466324&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;How to fix the &amp;quot;The file &amp;ldquo;.swiftlint.yml&amp;rdquo; couldn&amp;rsquo;t be opened because you don&amp;rsquo;t have permission to view it&amp;quot; issue&quot; data-og-description=&quot;When we use swiftlint with Xcode 15 we can get the &amp;quot;The file &amp;ldquo;.swiftlint.yml&amp;rdquo; couldn&amp;rsquo;t be opened because you don&amp;rsquo;t have permission to view it&amp;quot; error. Here is how to fix it.&quot; data-og-host=&quot;thisdevbrain.com&quot; data-og-source-url=&quot;https://thisdevbrain.com/swiftlint-permission-issue/&quot; data-og-url=&quot;https://thisdevbrain.com/swiftlint-permission-issue/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ycaOo/hyVqsfLyjE/3Q4H0Vspw5G1NWaOzb6bR1/img.jpg?width=1280&amp;amp;height=850&amp;amp;face=0_0_1280_850,https://scrap.kakaocdn.net/dn/WzGEz/hyVqt6OXO9/RTfAfYscMx8nYcKzJcsQ2K/img.jpg?width=1280&amp;amp;height=850&amp;amp;face=0_0_1280_850,https://scrap.kakaocdn.net/dn/bTigTr/hyVqmfxF06/Z8Aua43SiVQJfCvgoNrxck/img.png?width=2000&amp;amp;height=1035&amp;amp;face=0_0_2000_1035&quot;&gt;&lt;a href=&quot;https://thisdevbrain.com/swiftlint-permission-issue/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://thisdevbrain.com/swiftlint-permission-issue/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ycaOo/hyVqsfLyjE/3Q4H0Vspw5G1NWaOzb6bR1/img.jpg?width=1280&amp;amp;height=850&amp;amp;face=0_0_1280_850,https://scrap.kakaocdn.net/dn/WzGEz/hyVqt6OXO9/RTfAfYscMx8nYcKzJcsQ2K/img.jpg?width=1280&amp;amp;height=850&amp;amp;face=0_0_1280_850,https://scrap.kakaocdn.net/dn/bTigTr/hyVqmfxF06/Z8Aua43SiVQJfCvgoNrxck/img.png?width=2000&amp;amp;height=1035&amp;amp;face=0_0_2000_1035');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;How to fix the &quot;The file &amp;ldquo;.swiftlint.yml&amp;rdquo; couldn&amp;rsquo;t be opened because you don&amp;rsquo;t have permission to view it&quot; issue&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;When we use swiftlint with Xcode 15 we can get the &quot;The file &amp;ldquo;.swiftlint.yml&amp;rdquo; couldn&amp;rsquo;t be opened because you don&amp;rsquo;t have permission to view it&quot; error. Here is how to fix it.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;thisdevbrain.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>iOS</category>
      <category>Failed with</category>
      <category>Failed with a nonzero exit code</category>
      <category>ios</category>
      <category>M1 맥북 SwiftLint 오류</category>
      <category>Swift Lint</category>
      <category>SwiftLint</category>
      <category>SwiftLint is not installed</category>
      <category>SwiftLint nonzero exit code</category>
      <category>SwiftLint 오류</category>
      <category>Xcode15 SwiftLint</category>
      <author>kybeen</author>
      <guid isPermaLink="true">https://kybeen.tistory.com/143</guid>
      <comments>https://kybeen.tistory.com/143#entry143comment</comments>
      <pubDate>Wed, 28 Feb 2024 02:58:39 +0900</pubDate>
    </item>
    <item>
      <title>[ iOS ] CustomStringConvertible 프로토콜</title>
      <link>https://kybeen.tistory.com/142</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;공식 문서&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/swift/customstringconvertible&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://developer.apple.com/documentation/swift/customstringconvertible&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;CustomStringConvertible | Apple Developer Documentation&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;A type with a customized textual representation.&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/documentation/swift/customstringconvertible&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/l2Fmi/hyVjlHmFHQ/nqK4S40rF6K4LugnOEy7PK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/nfEcX/hyVjbkrUfD/zXphA5P4wsMRbgDcXActcK/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512&quot; data-og-url=&quot;https://docs.developer.apple.com/documentation/swift/customstringconvertible&quot;&gt;&lt;a href=&quot;https://docs.developer.apple.com/documentation/swift/customstringconvertible&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/documentation/swift/customstringconvertible&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/l2Fmi/hyVjlHmFHQ/nqK4S40rF6K4LugnOEy7PK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/nfEcX/hyVjbkrUfD/zXphA5P4wsMRbgDcXActcK/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;CustomStringConvertible | Apple Developer Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A type with a customized textual representation.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;Swift의 CustomStringConvertible 프로토콜은 &lt;b&gt;커스텀된 텍스트 표현&lt;/b&gt;을 사용할 수 있도록 해주는 프로토콜임&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1432&quot; data-origin-height=&quot;478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rJx8E/btsEF6nFC1u/7G5rh39iTT08hbiHJkZUbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rJx8E/btsEF6nFC1u/7G5rh39iTT08hbiHJkZUbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rJx8E/btsEF6nFC1u/7G5rh39iTT08hbiHJkZUbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrJx8E%2FbtsEF6nFC1u%2F7G5rh39iTT08hbiHJkZUbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;653&quot; height=&quot;218&quot; data-origin-width=&quot;1432&quot; data-origin-height=&quot;478&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CustomStringConvertible 프로토콜을 준수하는 타입은 인스턴스를 문자열로 변환할 때 그 타입만의 자체 표현을 제공할 수 있다.&lt;br /&gt;만약 CustomStringConvertible 프로토콜을 준수하는 인스턴스가 &lt;i&gt;&lt;b&gt;String(describing:)&lt;/b&gt;&lt;/i&gt; 이니셜라이저나 &lt;i&gt;&lt;b&gt;print(_:)&lt;/b&gt;&lt;/i&gt; 함수의 인자로 전달되면 해당 이니셜라이저와 함수는 인스턴스의 &lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;description 프로퍼티&lt;/b&gt;&lt;/span&gt;를 사용한다.&lt;br /&gt;(description 프로퍼티에 직접 접근하거나 CustomStringConvertible을 제네릭 타입 제약에 사용하는 것은 권장되지 않음)&lt;br /&gt;&amp;nbsp;&lt;br /&gt;  즉, &lt;b&gt;타입 자체를 원하는 형식의 문자열로 바로 표현하고 싶을 때 사용할 수 있는 프로토콜&lt;/b&gt;인 것 같다.&lt;br /&gt;description 프로퍼티에 직접 접근하는 것을 권장하지 않는 이유는 이렇게 사용할 경우 굳이 CustomStringConvertible 프로토콜을 채택할 필요 없이 그냥 연산 프로퍼티를 추가하면 되기 때뭉에 용도를 구분해주고자 그런게 아닐까 싶다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;CustomStringConvertible 프로토콜 준수하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;description&lt;/b&gt; 프로퍼티를 정의해주는 것으로 CustomStringConvertible 프로토콜을 준수할 수 있다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;공식 문서의 예제를 보면 CustomStringConvertible 프로토콜을 준수하지 않을 때는 Point 구조체를 print() 시에 구조체가 그대로 나오지만, Point 구조체가 CustomStringConvertible 프로토콜을 준수할 때는 print() 시에 자동으로 description 프로퍼티에 정의된 형식으로 출력되는 것을 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;// MARK: - CustomStringConvertible 프로토콜 준수 x
struct Point {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;let x: Int, y: Int
}


let p = Point(x: 21, y: 30)
print(p)
// Prints &quot;Point(x: 21, y: 30)&quot;


// MARK: - CustomStringConvertible 프로토콜 준수
extension Point: CustomStringConvertible {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var description: String {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return &quot;(\(x), \(y))&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}


print(p)
// Prints &quot;(21, 30)&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;적용해보기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CustomStringConvertible 프로토콜을 직접 적용해보았다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지하철 도착 정보에 대한 구조체인 ArrivalInfo의 restTime 프로퍼티는 초 단위로 값을 받아오기 때문에 00분00초 형식으로 표현을 해주어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;swift&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;struct ArrivalInfo {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var restTime: RestTimeSecond&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 남은 시간 (단위: 초)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var prevStation: String&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 이전역
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var nextStation: String&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 다음역
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var station: String&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 현재역
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CustomStringConvertible 프로토콜을 준수하는 RestTimeSecond라는 타입을 정의해서 초 단위로 표현된 Int 값을 &quot;00분00초&quot; 의 String으로 표현해주도록 description 프로퍼티를 정의해주었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;swift&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;struct RestTimeSecond: CustomStringConvertible {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var second: Int
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var description: String {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return &quot;\(second/60)분\(second%60)초&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이후 실시간 정보를 받아와서 텍스트로 표시하도록 코드를 작성해주었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;swift&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;self?.subwayView.timeLabel.text =&amp;nbsp;&amp;nbsp;&quot;\(arrivalInfoList[1].restTime) 남았습니다.&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공식 문서의 설명에서 String(describing:) 이니셜라이저 사용 시에 description 프로퍼티가 적용이 된다고 했기 때문에 문자열 보간 시에도 적용이 되지 않을까 싶어서 사용해봤는데 적용된다 ㅎㅎ&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;608&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c3cYTZ/btsEPKXEHn0/lwK4eFHBuHUoIdNCpcoZlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c3cYTZ/btsEPKXEHn0/lwK4eFHBuHUoIdNCpcoZlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3cYTZ/btsEPKXEHn0/lwK4eFHBuHUoIdNCpcoZlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc3cYTZ%2FbtsEPKXEHn0%2FlwK4eFHBuHUoIdNCpcoZlk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;479&quot; height=&quot;416&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;608&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>iOS</category>
      <category>CustomStringConvertible</category>
      <category>CustomStringConvertible 프로토콜</category>
      <category>Swift 문자열 변환</category>
      <category>Swift 커스텀 String</category>
      <category>문자열 표현</category>
      <author>kybeen</author>
      <guid isPermaLink="true">https://kybeen.tistory.com/142</guid>
      <comments>https://kybeen.tistory.com/142#entry142comment</comments>
      <pubDate>Tue, 13 Feb 2024 15:49:01 +0900</pubDate>
    </item>
  </channel>
</rss>