<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>삼쓰의 개발 블로그</title>
    <link>https://woongsios.tistory.com/</link>
    <description>woongS, iOS, succesS

삼쓰의 개발 블로그</description>
    <language>ko</language>
    <pubDate>Tue, 14 Apr 2026 13:12:19 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>삼쓰 웅쓰</managingEditor>
    <item>
      <title>Xcode 15 firebase firestore 에서 컴파일 에러 발생할 때</title>
      <link>https://woongsios.tistory.com/329</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;편법 주의 ⚠️&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Xcode 14대에서 15로 올리고 나니, 아래 코드부분에서 &lt;span style=&quot;color: #ee2323;&quot;&gt;An attribute list cannot appear here&lt;/span&gt; 에러가 발생하면서 컴파일이 되지 않을 때.&lt;/p&gt;
&lt;pre id=&quot;code_1701449358697&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Public constant
ABSL_CONST_INIT extern &quot;C&quot; const int64_t kFIRFirestoreCacheSizeUnlimited =
    Settings::CacheSizeUnlimited;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ABSL_CONST_INIT 코드를 지워봅시다.&lt;br /&gt;xcode에서는 잠겨서 수정이 안 된다면 텍스트 편집기나 다른 파일로 열어서 삭제,,,&lt;/p&gt;
&lt;pre id=&quot;code_1701449498192&quot; class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// Public constant
extern &quot;C&quot; const int64_t kFIRFirestoreCacheSizeUnlimited =
    Settings::CacheSizeUnlimited;&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;제 경우 SPM 으로 Firebase 9.6.0 를 사용하고 있었는데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/firebase/firebase-ios-sdk/issues/11840&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/firebase/firebase-ios-sdk/issues/11840&lt;/a&gt; 여기서 해결법을 찾았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버전 올리면 또 다른 데서 문제가 생기고... 다른 방법은 잘 되지 않더라구요,,,&amp;nbsp;&lt;br /&gt;올바른 방법 같진 않지만 저처럼 급하시다면 편법(?)으로 ...&lt;/p&gt;</description>
      <category>iOS</category>
      <category>An attribute list cannot appear here</category>
      <category>Firebase</category>
      <category>firestore</category>
      <category>Xcode15</category>
      <author>삼쓰 웅쓰</author>
      <guid isPermaLink="true">https://woongsios.tistory.com/329</guid>
      <comments>https://woongsios.tistory.com/329#entry329comment</comments>
      <pubDate>Sat, 2 Dec 2023 01:56:00 +0900</pubDate>
    </item>
    <item>
      <title>APNs 를 통한 Push 알림 이해하기</title>
      <link>https://woongsios.tistory.com/326</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 개개인이 갖고 있는 아이폰 디바이스에 푸시 알림이 오는 과정에 대해 알아보려고 합니다.&lt;br /&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;h2 data-ke-size=&quot;size26&quot;&gt;흐름 이해하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개별 디바이스에 직접적으로 푸시 알림을 보낼 수 있는 주체는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;오직 APNs 뿐&lt;/b&gt;입니다.&lt;br /&gt;APNs 란 Apple Push Notification server 의 약자로 즉 애플만이 직접적으로 디바이스에 푸시 알림을 보낼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1658&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nsTsY/btrS4YvQa8u/wxYdK3u06SCFEpEiMSWew1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nsTsY/btrS4YvQa8u/wxYdK3u06SCFEpEiMSWew1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nsTsY/btrS4YvQa8u/wxYdK3u06SCFEpEiMSWew1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnsTsY%2FbtrS4YvQa8u%2FwxYdK3u06SCFEpEiMSWew1%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;1658&quot; height=&quot;484&quot; data-origin-width=&quot;1658&quot; data-origin-height=&quot;484&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;따라서 저희는 APNs 에게 우리 앱에 푸시를 보내줘! 라고 요청해야 합니다.&lt;br /&gt;이 요청이 유효한지 보고 APNs 가 푸시를 보낼지 말지를 판단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 요청 또한 정해진 규격에 맞게 APNs 와 연결한 후에 가능하고, 이를 위해 APNs 에게 요청해줄 저희의 서버가 필요합니다.&lt;br /&gt;문서에서 이를 Provider Server 라고 칭하는데 여기선 간략하게 저희 '서버' 정도로 칭하도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇담 APNs 에서는 뭘 보고 보내도 될지 판단할 수 있을까요?&lt;br /&gt;&lt;span&gt;당연히&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;사전에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;앱에서 저희 서버에게 필요한 정보들을 알려줘야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;1092&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b44etv/btrS7umtbSi/6wWDZeRKL5Ams123X5GroK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b44etv/btrS7umtbSi/6wWDZeRKL5Ams123X5GroK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b44etv/btrS7umtbSi/6wWDZeRKL5Ams123X5GroK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb44etv%2FbtrS7umtbSi%2F6wWDZeRKL5Ams123X5GroK%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;1712&quot; height=&quot;1092&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;1092&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네 이게 매우 많이 후려친(?) 큰 흐름입니다. 먼저 전체적인 흐름을 한번 생각 해봤습니다.&lt;br /&gt;앱에선 4번을 받기 위한 작업도 필요하지만, 이건 크게 어렵지도 않고 다른 자료들도 많이 있으니 여기선 다루지 않겠습니다.&lt;br /&gt;오늘 저희의 관심사는 1, 2, 3 입니다. 이제 필요한 건 뭔지, 어떻게 보내고 검증하는지를 더 살펴보겠습니다.&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;APNs 에 보내려면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;APNs 에 요청하려면 다음 정보들이 필요합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JSON payload -&amp;gt; 문서 &amp;amp; 서버 &lt;/li&gt;
&lt;li&gt;Device Token -&amp;gt; ?&lt;/li&gt;
&lt;li&gt;스펙이 정의된 Request Header&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;-&amp;gt; 문서 &amp;amp; 서버 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;(토큰 기반일 때만, 이하 Auth Token) Authentication Token -&amp;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;JSON payload, request header 는 서버에서 처리해주실테고,, 문서에 잘 나와있으니&lt;br /&gt;저희는 JSON 형태의 뭔가가 있고 header 정의해야 하는구나! 정도로 넘어가면 알고 될 거 같습니다.&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;남는 의문.&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Device Token 이 뭐지? 어떻게 얻지?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 토큰 기반은 뭐지? 그럼 다른 건?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Auth Token 이 뭐지? 어떻게 얻지?&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;Device Token&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;APNs 가 기기에 푸시를 제대로 보내기 위한 고유 주소 역할을 합니다.&lt;br /&gt;&lt;/span&gt;&lt;span&gt;기기와 앱 모두 고유한 주소를 갖습니다.&lt;br /&gt;&lt;/span&gt;당연히 두 앱이 같은 기기에 설치되어 있더라도 한 앱의 토큰은 다른 앱에서 쓸 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;앱이 시작되면 APNs 에 요청해 Device Token을 얻을 수 있고&lt;br /&gt;이걸 우리 서버에 보내 저장하고 있도록 합니다.&lt;br /&gt;&lt;/span&gt;&lt;span&gt;참고로 이 Device Token 은 앱이 백업될 때 새로 발급되니 절대 로컬에 캐싱해두지 말라고 하네요.&lt;br /&gt;(백업은 사용자가 앱을 다른 다비이스에 설치하거나, os를 재설치 하는 경우 등을 얘기하는 것 같습니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Deivce Token은&lt;br /&gt;&lt;/span&gt;&lt;span&gt;UIApplication의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://developer.apple.com/documentation/uikit/uiapplication/1623078-registerforremotenotifications&quot;&gt;registerForRemoteNotifications()&lt;/a&gt;&lt;/span&gt;&lt;span data-v-97436168=&quot;&quot; data-v-91bcffaa=&quot;&quot;&gt;&amp;nbsp;메서드로 요청하고 올바르게 자격 인증이 되어있다면,&lt;br /&gt;&lt;/span&gt;&lt;span data-v-97436168=&quot;&quot; data-v-91bcffaa=&quot;&quot;&gt;App Delegate의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622958-application&quot;&gt;application(_:didRegisterForRemoteNotificationsWithDeviceToken:)&lt;/a&gt;&amp;nbsp;메소드로 받을 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(저 사전 자격 인증도 알아봐야 하는데요, 내용이 자꾸 길어지니 이건 따로 다뤄보도록 하겠습니다. 아무튼 유료 계정이 필요합니다.. ㅠ)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;두 가지 인증방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 필수 정보에서 payload, request header, device token 등은 어떻게 푸시를 보낼지에 대한 정보에 가까웠다면,&lt;br /&gt;이제는 어떻게 검증할지에 대해 알아봐야 할 차례입니다.&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;1070&quot; data-origin-height=&quot;264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ygkcK/btrS6S8WJlq/5RJVRZ4jHOvQknN60kvDHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ygkcK/btrS6S8WJlq/5RJVRZ4jHOvQknN60kvDHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ygkcK/btrS6S8WJlq/5RJVRZ4jHOvQknN60kvDHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FygkcK%2FbtrS6S8WJlq%2F5RJVRZ4jHOvQknN60kvDHk%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;1070&quot; height=&quot;264&quot; data-origin-width=&quot;1070&quot; data-origin-height=&quot;264&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Certificate-Based Connection&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;증명서 기반의 연결은 서버가 APNs 와 연결할 때 증명서를 통해 보안 연결하는 방식입니다.&lt;br /&gt;서버레벨에서 인증되기 때문에 노티 요청에서 별도의 인증 토큰은 필요하지 않습니다.&lt;br /&gt;인증 토큰이 없기 때문에 개별 요청의 크기도 토큰 기반에 비해 약간 작습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다수의 앱에 노티를 보내기 위해서 각각의 앱마다 개별 증명서가 필요하고, 앱 별로 개별 보안 연결 관리가 필요합니다.&lt;br /&gt;결과적으로는 토큰 기반이 더 관리가 쉽습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Token-Based Connection&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰 기반 인증의 경우엔 앞서 얘기 나온 대로, 요청을 보낼 때 Auth Token 이 필요합니다.&amp;nbsp;&lt;br /&gt;&lt;span&gt;보낼 때 토큰을 함께 보내니 stateless 하게 APNs와 통신할 수 있는데요, APNs 가 증명서를 찾을 필요도 없고 서버의 다른 정보도 필요 없으니 보다 빠르게 통신할 수 있습니다.&lt;br /&gt;&lt;/span&gt;개별 요청에 인증 토큰이 포함되니 증명서 인증 방식 보다는 요청의 크기가 약간 큽니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰 방식은 여러 서버에서 같은 토큰을 사용할 수 있고,&lt;br /&gt;하나의 토큰으로 사내 여러 앱에 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플이 제공한 공급자 서명키로 최소 한 시간에 한 번 이상 암호화해 업데이트가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공급자 서명키를 만들면&lt;br /&gt;10 글자의 Key ID 와 텍스트 파일로 된 p8 확장자 키를 받습니다.&amp;nbsp;&lt;br /&gt;Key ID는 JSON Payload 에 포함되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 사전에 서버에 Key ID와 p8 키가 모두 등록되어있어야 하겠죠?&lt;br /&gt;키는 한 번 발급받으면 다시 재발급 받을 수 없으니 잘 관리해야 합니다 :)&lt;br /&gt;잃어버리면 revoke 하고 새로 발급받아야 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;정리하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;애플(APNs) 만이 디바이스에 푸시를 보낼 수 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;APNs 푸시 쏴달라고 요쳥할 서버가 필요함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;요청에는 푸시를 보낼 때 필수적으로 필요한 3가지 정보가 필요함.&lt;br /&gt;ㄴ JSON Payload, Request Header, Device Token&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;인증도 필요한데 2가지 방법이 있음. 토큰 기반, 증명서 기반.&lt;br /&gt;ㄴ 증명서 기반은 서버 연결시 인증하는 방법, 서버 레벨에서 인증이 되었기 때문에 요청에 별도의 인증 토큰은 필요 없음.&lt;br /&gt;요청에 토큰 정보가 없으니 크기가 약간 적지만,&lt;span&gt;&amp;nbsp;&lt;/span&gt;앱 마다 개별 증명서가 필요하고 이걸로 개별적으로 보안 연결해서 관리해야함.&lt;br /&gt;ㄴ 토큰 기반은, 요청 보낼 때 토큰 정보를 보내는 방법. APNs 가 증명서를 확인할 필요도 없고, 서버의 다른 정보도 필요치 않으니 stateless 하게 통신 가능하고 빠름. 대신 토큰이 들어가니 요청 크기가 약간 증가함.&lt;br /&gt;서버는 한 시간에 한 번 이상&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;p8 인증키로&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;암호화해서 업데이트 시켜줘야함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이렇게 APNs 를 통해 원격 푸시 알림을 보내는 과정에 대해 알아봤습니다.&lt;br /&gt;저는 그 동안 항상 머리 속에 그림이 잘 안 그려지고 두리뭉술 하게만 알고 있었는데, 이번에 정리하면서 많은 의문이 해소되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;다른 분들에게도 도움이되었으면 좋겠네요.&lt;br /&gt;읽어주셔서 감사합니다.&lt;/p&gt;
&lt;h1 data-v-91bcffaa=&quot;&quot;&gt;&amp;nbsp;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-v-97436168=&quot;&quot; data-v-91bcffaa=&quot;&quot;&gt;참고&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;http://%20https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns&quot;&gt;&lt;span data-v-97436168=&quot;&quot; data-v-91bcffaa=&quot;&quot;&gt;Sending Notification Requests to APNs&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/establishing_a_certificate-based_connection_to_apns&quot;&gt;&lt;span data-v-97436168=&quot;&quot; data-v-91bcffaa=&quot;&quot;&gt;Establishing a Certificate-Based Connection to APNs&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/establishing_a_token-based_connection_to_apns&quot;&gt;&lt;span data-v-97436168=&quot;&quot; data-v-91bcffaa=&quot;&quot;&gt;Establishing a Token-Based Connection to APNs&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/usernotifications/registering_your_app_with_apns&quot;&gt;&lt;span data-v-97436168=&quot;&quot; data-v-91bcffaa=&quot;&quot;&gt;Registering Your App with APNs&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>iOS</category>
      <category>apns</category>
      <category>certificate-based</category>
      <category>ios</category>
      <category>p8</category>
      <category>push notification</category>
      <category>token-based</category>
      <category>인증서</category>
      <category>토큰</category>
      <category>푸시 알림</category>
      <author>삼쓰 웅쓰</author>
      <guid isPermaLink="true">https://woongsios.tistory.com/326</guid>
      <comments>https://woongsios.tistory.com/326#entry326comment</comments>
      <pubDate>Wed, 7 Dec 2022 21:57:56 +0900</pubDate>
    </item>
    <item>
      <title>Xcode 여러 버전 설치 후 git 문제</title>
      <link>https://woongsios.tistory.com/325</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;br&gt;아래와 같은 에러가 나온다면, git에서 제대로 된 &lt;b&gt;Xcode 경로를 찾지못해 발생하는 에러&lt;/b&gt;입니다.&lt;/p&gt;
&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;xcrun: error: active developer path (&quot;/Users/~~~/Xcode13.2.app/Contents/Developer&quot;) does not exist
Use `sudo xcode-select --switch path/to/Xcode.app` to specify the Xcode that you wish to use for command line developer tools, or use `xcode-select --install` to install the standalone command line developer tools.
See `man xcode-select` for more details.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;br&gt;에러 메시지에서 알려주는 대로 아래 같이 &lt;b&gt;Xcode의 경로를 지정&lt;/b&gt;해주면 됩니다.&lt;br&gt;저는 Applications/ 밑에 Xcode.app 을 따라가려고 해요 :)&lt;/p&gt;
&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;sudo xcode-select --switch /Applications/Xcode.app&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;/p&gt;</description>
      <category>iOS</category>
      <category>error</category>
      <category>Git</category>
      <category>XCode</category>
      <category>xcode 버전</category>
      <category>xcode-select</category>
      <author>삼쓰 웅쓰</author>
      <guid isPermaLink="true">https://woongsios.tistory.com/325</guid>
      <comments>https://woongsios.tistory.com/325#entry325comment</comments>
      <pubDate>Fri, 13 May 2022 14:14:25 +0900</pubDate>
    </item>
    <item>
      <title>4장 주석</title>
      <link>https://woongsios.tistory.com/322</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;있으나 마나 한 주석을 달려는 유혹에서 벗어나 코드를 정리하라. &lt;br /&gt;더 낫고, 행복한 프로그래머가 되는 지름길이다.&lt;/span&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;&lt;b&gt;함수나 변수로 표현할 수 있다면 주석을 달지 마라&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;주석은 나쁜 코드를 보완하지 못한다&lt;/span&gt;&lt;/h3&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;/h3&gt;
&lt;pre id=&quot;code_1639573288936&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 직원에게 복지 혜택을 받을 자격이 있는지 검사한다.
if ((employee.flags &amp;amp; HOURLY_FLAG) &amp;amp;&amp;amp;
	(employee.age &amp;gt; 65))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드보단&lt;/p&gt;
&lt;pre id=&quot;code_1639573313552&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (employee.isEligibleForFullBenefits())&lt;/code&gt;&lt;/pre&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;법적인 주석&lt;/i&gt;&lt;br /&gt;&lt;i&gt;정보를 제공하는 주석&lt;/i&gt;&lt;br /&gt;&lt;i&gt;의도를 설명하는 주석&lt;/i&gt;&lt;br /&gt;&lt;i&gt;의미를 명료하게 밝히는 주석&lt;/i&gt;&lt;br /&gt;&lt;i&gt;결과를 경고하는 주석&lt;br /&gt;TODO 주석&lt;br /&gt;중요성을 강조하는 주석&lt;/i&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;h3 data-ke-size=&quot;size23&quot;&gt;위치를 표시하는 주석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;반드시 필요할 때만, 아주 드물게 사용하는 편이 좋다. 배너를 남용하면 독자가 흔한 잡음으로 여겨 무시한다.&lt;/i&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로는 이렇게 구분되어 있으면 읽기가 편하던데... (원하는 부분을 찾아가기 편함) &lt;br /&gt;그렇지만 잘 짜여 있다면 이런 것도 필요 없으려나..&lt;/p&gt;
&lt;/blockquote&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;/h3&gt;
&lt;pre id=&quot;code_1642166103714&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// PrimeGenerator.java

/**
* 이 클래스는 사용자가 지정한 최대 값까지 소수를 구한다.
* 알고리즘은 에라스토테네스의 체다.
* 2에서 시작하는 정수 배열을 대상으로 작업한다.
* 처음으로 남아있는 정수를 찾아 배수를 모두 제거한다.
* 배열에 더 이상 배수가 없을 때까지 반복한다.
*/
public class PrimeGenerator {
...
  private static int determineIterationLimit()
  {
    // 배열에 있는 모든 배수는 배열 크기의 제곱근보다 작은 소수의 인수다.
    // 따라서 이 제곱근보다 더 큰 숫자의 배수는 제거할 필요가 없다.
    double iterationLimit = Math.sqrt(crossedOut.length);
    return (int) iterationLimit();
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;첫 번째 주석이 중복이라고 주장하기 쉽다. 설명이 generatePrimes 함수 자체와 아주 흡사하기 때문이다. 그래도 나는 주석이 있어 알고리즘을 이해하기 쉬워진다고 생각한다. 그래서 남겨 두는 편을 택했다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;두 번째 주석은 거의 확실히 필요하다. 루프 한계값으로 제곱근을 사용한 이유를 설명한다. 나로서는 변수 이름을 바꾸거나 코드 구조를 조정해 이유를 명확하게 설명할 방법을 찾지 못했다. 다른 한편으로 제곱근의 사용은 나만의 생각일지도 모르겠다. 제곱근까지만 루프를 돌면 정말로 시간을 절약할까? 제곱근 계산에 오히려 시간이 더 들지 않을까?&lt;/i&gt;&lt;br /&gt;&lt;i&gt;&amp;nbsp; 숙고할 가치가 있는 문제다. 늙은 C/어셈블리 프로그래머인 내게는 제곱근이 만족스러운 해법이지만 남들이 이를 이해하려 시간과 노력을 투자할 가치가 있는지는 잘 모르겠다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 시각(?)으로 마무리를 해주셔서 좋았다. 이 장에서 느낀 건 역시 주석은 '코드를' 설명하는 게 아니라 코드의 '의도와 맥락'을 설명해야 한다. 물론 그마저도 필요 없다면 베스트....&lt;/p&gt;</description>
      <category>Clean Code</category>
      <category>clean code</category>
      <category>주석</category>
      <category>클린 코드</category>
      <author>삼쓰 웅쓰</author>
      <guid isPermaLink="true">https://woongsios.tistory.com/322</guid>
      <comments>https://woongsios.tistory.com/322#entry322comment</comments>
      <pubDate>Fri, 14 Jan 2022 22:22:20 +0900</pubDate>
    </item>
    <item>
      <title>2021년 회고</title>
      <link>https://woongsios.tistory.com/321</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;파트 회고를 하기로 해서 급하게 돌아보는 2021 (지금 안 하면 안할 것 같다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취업과 함께 뒤늦은 2020 회고를 하고 정신차려보니 반 년의 시간이 흘렀다.&lt;br /&gt;회고를 시작하는 나의 마음: ??? 벌써?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;취업&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드디어 꿈에 그리던 회사에 취업을 했다. 들어오기 전과 들어와서 본 후의 모습은 약간 차이가 있지만 일하고 싶은 회사라는 건 변함없다. 너무 좋은 동료들과 일하고 있는 것 같아 행복하다. 아직 딱히 단점은 없고 좋은 점이 많은데 뭔가 나보다 주변에서 더 좋아하는 것 같아서 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9월부터 정직원으로 일하고 있고 인턴 두 달을 포함하면 6개월차에 접어들고 있다. 엊그제 입사한 것 같은데 언제 이렇게 시간이 흘렀는지 모르겠다. 역시 시간이란 괴물은 정신차리고 잡지 않으면 너무 빨리 도망가는 녀석이다. 요즘 생각하는 시간을 잡는 가장 좋은 방법은 기록이다. 기억은 자주 들여다볼수록 희미해지는 반면, 기록은 또렷해진다. 기억하지 말고 기록하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;일&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지긋지긋한 코로나의 여파로 출근 첫 날부터 지금까지 재택 중이다. 아직까지도 회사에서 일하는 게 집중도 잘되고 좋지만 출퇴근 시간이나 기타 비용을 생각하면... 개인 관리만 잘 할 수 있다면 역시 재택이 좋긴 한 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입사하고 참 많은 걸 배웠다. 기본이 되는 개발부터 업무관리, 일정관리, 커뮤니케이션 등등 일하는 방식 전반에 대해 많이 배웠고 배우고 있는 중이다. 각종 회의, 행사에 다 참여하고 업무까지 완벽하게 처리하시면서 블로그에 정리까지 하시는 분들을 보면 그저 리스펙..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 아직은 업무를 따라가기도 빠듯빠듯 한 것 같다. 분명 놀지도 않고 종일 앉아있는데도 별로 진행된 결과물이 없는 걸 보며 괴로워하던 적도 있었는데, 요즘은 지라 작업로그에 최대한 작은 단위로 진행사항을 기록하며 점점 나아지고 있는 것 같다. 어느 부분에서 시간이 많이 걸리는지 무엇이 문제였는지 돌아볼 수 있어서 좋다. 나는 작은 워딩 하나, 사소한 부분 하나하나 너무 많은 생각을 하다보니 피로도도 높고 효율이 떨어지는 것 같다. 말하기 전에 생각하는 건 좋지만 적당한 선에서 판단하고 빠르게 피드백을 받는 게 더 좋은 해결책이 될 수 있음을 기억하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아, 파트 &lt;a href=&quot;https://brunch.co.kr/magazine/brunch-dev&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;브런치 매거진&lt;/a&gt;에 매 달 글도 쓰기 시작했다. 인턴 때 있었던 일들은 이걸로 대체하면 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://brunch.co.kr/@woongss/1&quot;&gt;https://brunch.co.kr/@woongss/1&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 블로그 느낌에 홍보효과를 노려 쓰기로 했는데 재밌도 있고 쓰면서도 또 배운다. &lt;br /&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;/h2&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파트에서 패스트캠퍼스에 올라온 노수진님의 강의로 스터디를 진행했다. 개인적으로는 몰랐던 부분들에 대해 많이 배워서 너무 좋았다.&amp;nbsp;&lt;br /&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같이 취준했던 ssmmkk와 클린코드를 읽기 시작했다. 둘다 돌고돌아 가긴 했는데 생각하는거나 가치관, 현재 업무 환경 &lt;s&gt;(연봉까지도)&lt;/s&gt;도 너무 비슷해 같이 읽으면서 공감되는 부분도 많아서 재밌게 읽고 있다. 읽으면서 책에서 하지 말라고 하는 대로 짰던 나의 코드가 많이 생각나서 뼈가 많이 아팠고   알아도 그렇게 잘 짜기 힘든데 모르고 살았으면 점점 차이가 얼마나 더 벌어질까 라는 생각이 든다. 최소 1년에 한 번 이상은 반복해서 읽어야겠다.&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;90s - Orenzy&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해 초부터 전에 하던 동아리 사람들과 앱을 하나 만들고 있는데 다들 주 업이 있다보니 미루고 미뤄져 아직까지 하고있다;;; 거의 막바지긴한데 빨리 끝내고 싶다. 올해 안에는 끝내기로 했으나 안될 것 같다,, 덕분에 요즘 주말이 없는데 1월엔 마무리 짓자  &amp;zwj;♂️&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+&lt;br /&gt;다른 친구들과 비밀 프로젝트도 하나 하고있는데 다들 개발만 할 줄 알았지 홍보를 하려니 진도가 잘 안 나간다. 다들 여유도 없어서 빠듯하게 하다가 요즘 좀 침체긴데 연말 충전 좀 하고 다시 달려보기로.  &lt;/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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년쯤부터 어릴 적 친구들과 시작한 독서 스터디가 있는데, 자율에 맡겼더니 침체되어 가는 분위기다. 다들 먹고살기 바쁜 때인 것 같다. 나도 요즘 책 읽을 여유가 없어서 통 못 읽었는데 다시 열심히 읽어봐야겠다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;책 &amp;amp; 운동&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나의 새해 목표에 항상 빠지지 않는 친구들.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;책&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해 18권의 책을 읽었다. 올해 초에 읽었던 책들은 이게 올해였구나 싶을 정도로 멀게 느껴지는데 그만큼 상반기는 너무 길고 힘들었던 것 같다. 반면 하반기는 너무 빠르게 지나가고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽다가 아니다 싶은 책들은 포기했기 때문에 다 읽은 책들은 웬만하면 내용이 좋았는데 그 중에서도 아주 좋았던 책은 빨갛게 칠해봤다. 세이노의 가르침과 돈의 속성은 올해 새로 만난 인생책  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책 읽을 때 아니면 나가 놀거나 거의 개발을 하니까.. 라는 핑계로 개발 서적을 잘 안 읽은 것 같은데... 내년엔 개발서적을 많이 읽어야겠다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;세이노의 가르침&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;클린 아키텍처&lt;br /&gt;&lt;/span&gt;사수가 없어도 괜찮습니다.&lt;br /&gt;고독의 힘&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;돈의 속성&lt;br /&gt;&lt;/span&gt;알면서도 알지 못하는 것들&lt;br /&gt;게임의 명수&lt;br /&gt;우리는 언젠가 만난다&lt;br /&gt;아침 30분&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;부자아빠 가난한 아빠&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;팩트풀니스&lt;br /&gt;&lt;/span&gt;무조건 달라진다&lt;br /&gt;짐 로저스 앞으로 5년 한반도 투자 시나리오&lt;br /&gt;볼드&lt;br /&gt;이야기 인문학&lt;br /&gt;젊은날의 초상&lt;br /&gt;거꾸로 읽는 세계사&lt;br /&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간헐적 단식이 몸에 잘 맞는 것 같아 점심~저녁만 먹으면서 살이 좀 빠졌었는데 요즘 다시 폭식을 하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먹는 걸 포기하긴 너무 힘드니 운동을 열심히 하자. 저번엔 아령, 이번에 스탠드 턱걸이 기구를 새로 샀는데, 비어있는 누나방이 점점 헬스장이 되어간다. 내년엔 몸짱이 되어보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 또 한번 다짐을...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&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;6월에 인턴을 시작하면서 회고 하고 큰 목표만 세우고 올 해 목표를 정하지 않았더니 회고하기가 힘들었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내년 목표&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책 12권 (개발 서적 4권 이상)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그 포스팅 20&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영어공부&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일기쓰기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;턱걸이 10개&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몸무게 75kg&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>일상/끄적끄적</category>
      <category>2021 회고</category>
      <author>삼쓰 웅쓰</author>
      <guid isPermaLink="true">https://woongsios.tistory.com/321</guid>
      <comments>https://woongsios.tistory.com/321#entry321comment</comments>
      <pubDate>Thu, 23 Dec 2021 13:03:15 +0900</pubDate>
    </item>
    <item>
      <title>클린 코드 3장 함수</title>
      <link>https://woongsios.tistory.com/319</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;// 3-1. HtmlUtil.java (FitNesse 20070619)
public static String testableHtml {
	PageData pageData,
	boolean includeSuiteSetup
} throws Exception {
	Wikipage wikiPage = pageData.getWikiPage();
	StringBuffer buffer = new StringBuffer();
	if (pageData.hasAttribute(&quot;Test&quot;)) {
		if (includeSuiteSetup) {
			WikiPage suiteSetup = 
				PageCrawlerImpl.getInheritedPage(
						SuiteResponder.SUITE_SETUP_NAME, wikiPage
				);
			if (suiteSetup != null) {
				WikiPagePath pagePath =
					suiteSetup.getPageCrawler().getFullPath(suiteSetup);
				String pagePathName = PathParser.render(pagePath);
				buffer.append(&quot;!include -setup .&quot;)
							.append(pagePathName)
							.append(&quot;\\n&quot;);
			}
		}
		WikiPage setup =
			PageCrawlerImpl.getInheritedPage(&quot;SetUp&quot;, wikiPage);
		if (setup != null) {
			WikiPagePath setupPath =
				wikiPage.getPageCrawler().getFullPath(setup);
			String setupPathName = PathParser.render(setupPath);
			buffer.append(&quot;!include -setup .&quot;)
						.append(setupPathName)
						.append(&quot;\\n&quot;);
		}
	}
	buffer.append(pageData.getContent());
	if (pageData.hasAttribute(&quot;Test&quot;)) {
			WikiPage teardown = 
				PageCrawlerImpl.getInheritedPage(&quot;TearDown&quot;, wikiPage);
			if (teardown != null) {
				WikiPagePath teardownPath =
					suiteSetup.getPageCrawler().getFullPath(teardown);
				String teardownPathName = PathParser.render(teardownPath);
				buffer.append(&quot;!include -teardown .&quot;)
							.append(pagePathName)
							.append(&quot;\\n&quot;);
			}
			if (includeSuiteSetup) {
				WikiPage suiteTeardown = 
				PageCrawlerImpl.getInheritedPage(
						SuiteResponder.SUITE_TEARDOWN_NAME,
						wikiPage
				);
			if (suiteTeardown != null) {
				WikiPagePath pagePath =
					suiteSetup.getPageCrawler().getFullPath(suiteTeardown);
				String pagePathName = PathParser.render(pagePath);
				buffer.append(&quot;!include -teardown .&quot;)
							.append(pagePathName)
							.append(&quot;\\n&quot;);
			}
		}
	}
	pageData.setContent(buffer.toString());
	return pageData.getHtml();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;// 3-2. 리펙터링 버전
public static String renderPageWithSetupsAndTeardowns(
	PageData pageData, boolean isSuite
) throws Exception {
	boolean isTestPage = pageData.hasAttribute(&quot;Test&quot;);
	if (isTestPage) {
		WikiPage testPage = pageData.getWikiPage();
		StringBuffer newPageContent = new StringBuffer();
		includeSetupPages(testPage, newPageContent, isSuite);
		newPageContent.append(pageData.getContent());
		includeTeardownPages(testPage, newPageContent, isSuite);
		pageData.setContent(newPageContent.toString());
	}
	return pageData.getHtml();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3-2 함수가 읽기 쉽고 이해하기 쉬운 이유는 무엇일까? 의도를 분명히 표현하는 함수를 어떻게 구현할 수 있을까? 함수에 어떤 속성을 부여해야 처음 읽는 사람이 프로그램 내부를 직관적으로 파악할 수 있을까?&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수를 만드는 첫째 규칙은 '작게!'다. 둘째는 '더 작게!'&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3-2 함수도 길다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;// 3-3
public static String renderPageWithSetupsAndTeardowns(
	PageData pageData, boolean isSuite) throws Exception {
	if (isTestPage(pageData))
		includeSetupAndTeardownPages(pageData, isSuite);
	return pageData.getHtml();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블록과 들여 쓰기, if 문/else 문/while 문 등에 들어가는 블록은 한 줄이어야 한다.&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;/h3&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;3-3을 더 이상 줄이기란 불가능 하다. &lt;br /&gt;if 문을 includeSetupsAndTeardownsIfTestPate라는 함수로 만든다면 똑같은 내용을 다르게 표현할 뿐 추상화 수준은 바뀌지 않는다.&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;h3 data-ke-size=&quot;size23&quot;&gt;함수 당 추상화 수준은 하나로!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 내 모든 문장의 추상화 수준이 동일해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3-1에서 &lt;br /&gt;getHtml() - 추상화 수준 아주 높음 &lt;br /&gt;String PagePathName = PathParser.render(pagepath); - 추상화 수준 중간 &lt;br /&gt;.append(&quot;\n&quot;) - 추상화 수준이 아주 낮음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다. 특정 표현이 근본 개념인지 아니면 세부사항인지 구분하기 어려운 탓이다. 하지만 문제는 이 정도로 그치지 않는다. 근본 개념과 세부사항을 뒤섞기 시작하면, 깨어진 창문처럼 사람들이 함수에 세부사항을 점점 더 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 아래로 코드 읽기: &lt;b&gt;내려가기&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;h3 data-ke-size=&quot;size23&quot;&gt;Switch 문&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;switch 문은 작게 만들기 어렵다. 본질적으로 N가지를 처리한다. 불행하게도 switch 문을 완전히 피할 방법은 없다. 하지만 각 switch 문을 저 차원 클래스에 숨기고 절대로 반복하지 않는 방법은 있다. 물론 다형성을 이용한다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;// 3-4
public Money calculatePay(Employee e)
throws InvalidEmployeeType {
	switch (e.type) {
		case COMMISSIONED:

		case HOURLY:
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 함수에는 몇 가지 문제가 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;길다.&lt;/li&gt;
&lt;li&gt;한 가지만 하지 않는다.&lt;/li&gt;
&lt;li&gt;SRP를 위반한다. 코드를 변경할 이유가 여럿&lt;/li&gt;
&lt;li&gt;OCP를 위반한다. 새 직원 유형을 추가할 때마다 코드를 변경하기 때문&lt;/li&gt;
&lt;li&gt;가장 심각한 문제는 위 함수와 구조가 동일한 함수가 무한정 존재&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 switch 문을 추상 팩토리에 꽁꽁 숨긴다. 팩토리는 switch 문을 사용해 적절한 Employee 파생 클래스의 인스턴스를 생성한다. 일반적으로 나는 switch 문을 단 한 번만 참아준다. 다형적 객체를 생성하는 코드 안에서다. 이렇게 상속 관계로 숨긴 후에는 절대로 다른 코드에 노출하지 않는다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;구체적인 구현 부분을 숨겨야 한다는 부분에 공감했다. 특히 switch 같은 경우 케이스 하나가 추가될 때마다 모든 switch를 다 찾아다니며 바꿔야 하니... 유지보수 비용이 너무 커진다! 그러니 switch를 직접 구현하는 부분은 한 곳에만 사용하고 다른 곳에서는 추상화된 코드를 호출만 하면 좋겠다&lt;/blockquote&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;public abstract class Employee {
	public abstract boolean isPayday();
    public abstract Money caculatePay();
    public abstract void deliverPay(Money pay);
}
-----------
public interface EmployeeFactory {
	public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
-----------
public class EmployeeImpl: implements EmployeeFactory {
	public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
    	switch (r.type) {
        	case COMMISSIONED:
            	return new CommissionedEmployee(r);
            case HOURLY:
            	return new HourlyEmployee(r);
            case SALARIED:
            	return new SalariedEmployee(r);
            default:
            	throw new InvalidEmployeeType(r.type);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서술적인 이름을 사용하라!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 이름이 주는 가치는 아무리 강조해도 지나치지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름이 길어도 괜찮다. 겁먹을 필요없다. 길고 서술적인 이름이 짧고 어려운 이름보다 좋다. 길고 서술적인 이름이 서술적인 주석보다 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 이름을 정할 때는 여러 단어가 쉽게 읽히는 명명법을 사용한다. 그런 다음, 여러 단어를 사용해 함수 기능을 잘 표현하는 이름을 선택한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다. 좋은 이름을 고른 후 코드를 더 좋게 재구성하는 사례도 없지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름을 붙일 때는 일관성이 있어야 한다. 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;includeSetupAndTeardownPages, includeSuiteSetupPage, includeSetupPage,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등이 좋은 예다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;includeTeardownPages, includeSuiteTeardownPage, includeTeardownPage 도 당연히 있다. '짐작하는 대로' 다.&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상적인 인수 개수는 0개다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1개는 괜찮다. 2개면 조금 복잡해진다. 3개는 가능한 피하는 편이 좋다. 4개 이상은 특별한 이유가 필요하다. 특별한 이유가 있어도 사용하면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;# 많이 쓰는 단항 형식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인수 1개를 넘기는 가장 흔한 경우는 두 가지.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인수에 질문을 던지는 경우. boolean fileExist(&quot;MyFile&quot;)&lt;/li&gt;
&lt;li&gt;인수를 뭔가로 변환해 결과를 반환하는 경우. InputStream fileOpen(&quot;MyFile&quot;)&lt;br /&gt;&lt;br /&gt;(다소 드물게 사용) 이벤트 형식. 입력 인수만 있고 출력 인수는 없다. 프로그램은 함수 호출을 이벤트로 해석해 입력 인수로 시스템 상태를 바꾼다. passwordAttemptFailedNtimes(int attemps)
&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;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우들이 아니라면 단항 함수는 가급적 피한다. 예를 들어,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;void includeSetupPageInto(StringBuffer pageText)는 가급적 피한다. 변환 함수에서 출력 인수를 사용하면 혼란을 일으킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 인수를 변환하는 함수라면 변환 결과는 반환값으로 돌려준다. &lt;br /&gt;StringBuffer transform(StringBuffer in) 이 &lt;br /&gt;void transform(StringBuffer out) 보다 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;# 플래그 인수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플래그 인수는 추하다. &lt;b&gt;함수로 부울 값을 넘기는 관례는 정말로 끔찍하다.&lt;br /&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;일반적으로 단항 함수보다 이해하기 어렵다.&lt;br /&gt;적절한 경우도 있다. 예를 들어, Point p = new Point(0, 0)가 좋은 예다. 인수 2개는 한 값을 표현하는 요소 두 요소에는 자연적인 순서도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아주 당연하게 여겨지는 이항 함수에도 문제가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;# 삼항 함수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최소화하자. 클래스를 따로 만드는 걸 고려해보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;부수 효과를 일으키지 마라!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이드 이펙트 노노!!&lt;/p&gt;
&lt;pre id=&quot;code_1639305919355&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class UserValidator {
	private Cryptographer cryptographer;
    
    public boolean checkPassword(String userName, String password) {
    	User user = UserGateway.findByName(userName);
        if (user != User.NULL) {
        	String codedPhrase = user.getPhraseEncodedByPassword();
            String phrase = cryptographer.decrypt(codedPhrase, password);
            if (&quot;Valid Password&quot;.equals(phrase)) {
            	Session.initialize(); // ⚠️
                return true;
            }
        }
        return false;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안에서 initailize를 해버리니 밖에서는 파악이 안 된다.&lt;br /&gt;checkPassword는 세션을 초기화해도 괜찮은 경우에만 호출이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요하다면 함수 이름에 분명히 명시한다. checkPasswordAndInitializeSession 이라는 이름이 훨씬 좋다.&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;h2 data-ke-size=&quot;size26&quot;&gt;명령과 조회를 분리하라!&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야한다.&lt;br /&gt;객체 상태를 변경하거나 객체 정보를 반환하거나 둘 중 하나다.&lt;/p&gt;
&lt;pre id=&quot;code_1639306129573&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public boolean set(String attribute, String value);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수는 이름이 attribute인 속성을 찾아 값을 value로 설정한 후 성공하면 true를 반환하고 실패하면 false를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령과 조회를 분리해 혼란을 애초에 뿌리 뽑자.&lt;/p&gt;
&lt;pre id=&quot;code_1639306191276&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (attributeExists(&quot;username&quot;) {
	setAttribute(&quot;username&quot;, &quot;unclebob&quot;);
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;반복하지 마라!&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연 또 당연. 입구와 출구가 하나만 존재해야 한다는, 함수에 return은 하나만 있어야 한다는 구조적 프로그래밍의 목표와 규율은 좋지만 애초에 함수가 작다면 별 이익을 제공하지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수를 작게 만든다면 간혹 return, break, continue를 여러 차례 사용해도 괜찮다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;함수를 어떻게 짜죠?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어를 짜는 행위는 여느 글짓기와 비슷하다. 논문이나 기사를 작성할 때는 먼저 생각을 기록한 후 읽기 좋게 다듬는다. 초안은 대개 서투르고 어수선하므로 원하는 대로 읽힐 때까지 말을 다듬고 문장을 고치고 문단을 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수를 짤 때도 마찬가지다. 처음에는 길고 복잡하다. 들여 쓰기 단계도 많고 중복된 루프도 많다. 인수 목록도 아주 길다. 이름은 즉흥적이고 코드는 중복된다. 하지만 나는 그 서투른 코드를 빠짐없이 테스트하는 단위 케이스로 만든다.&lt;br /&gt;그런 다음 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다. 메서드를 줄이고 순서를 바꾼다. 때로는 전체 클래스를 쪼개기도 한다. 이 와중에도 코드는 항상 단위 테스트를 통과한다.&lt;br /&gt;처음부터 탁 짜내지 않는다. 그게 가능한 사람은 없으리라.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;일단 막 짜고. 테스트를 만들고. 리팩터링(고민 &amp;amp; 다듬기)한다.&lt;br /&gt;의식적으로 연습 또 연습   체화시키자.&lt;/blockquote&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;p data-ke-size=&quot;size16&quot;&gt;피가 되고 살이 되는 내용들이었다. 하나하나 곱씹으며 체화시키자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Clean Code</category>
      <category>clean code</category>
      <category>클린코드</category>
      <category>함수</category>
      <author>삼쓰 웅쓰</author>
      <guid isPermaLink="true">https://woongsios.tistory.com/319</guid>
      <comments>https://woongsios.tistory.com/319#entry319comment</comments>
      <pubDate>Sun, 12 Dec 2021 19:57:48 +0900</pubDate>
    </item>
    <item>
      <title>클린 코드 2장 의미 있는 이름</title>
      <link>https://woongsios.tistory.com/318</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;의도를 분명히 밝혀라&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수나 함수 그리고 클래스 이름은 다음과 같은 굵직한 질문에 모두 답해야 한다. 변수(혹은 함수나 클래스)의 존재 이유는? 수행 기능은? 사용 방법은? 따로 주석이 필요하다면 의도를 분명히 드러내지 못했다는 말이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;int d; // 경과 시간(단위: 날짜)

// ==== d보다는 아래 변수들이 훨씬 명확
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;zephir&quot;&gt;&lt;code&gt;public List&amp;lt;int []&amp;gt; getThem() {
	List&amp;lt;int[]&amp;gt; list1 = new ArrayList&amp;lt;int[]&amp;gt;();
	for (int[] x : theList)
		if (x[0] == 4)
			list1.add(x);
		return list1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 복잡한 문장도 없고 길이도 적당하지만 코드가 하는 일을 짐작하기 어렵다. 문제는 코드의 단순성이 아니라 코드의 함축성이다. 다시 말해, 코드 맥락이 코드 자체에 명시적으로 드러나지 않는다. 위 코드는 암암리에 독자가 다음과 같은 정보를 안다고 가정한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;theList에 무엇이 들었는가?&lt;/li&gt;
&lt;li&gt;theList에서 0번째 값이 어째서 중요한가?&lt;/li&gt;
&lt;li&gt;값 4는 무슨 의미인가?&lt;/li&gt;
&lt;li&gt;함수가 반환하는 리스트 list1을 어떻게 사용하는가?&lt;/li&gt;
&lt;/ol&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;br /&gt;theList &amp;rarr; gameBoard&lt;br /&gt;0번째 칸 &amp;rarr; 칸 상태&lt;br /&gt;값 4 &amp;rarr; 깃발이 꽂힌 상태&lt;br /&gt;등등 각 개념에 이름만 붙여도 코드가 상당히 나아진다.&lt;/p&gt;
&lt;pre class=&quot;zephir&quot;&gt;&lt;code&gt;public List&amp;lt;int []&amp;gt; getFlaggedCells() {
	List&amp;lt;int[]&amp;gt; flaggedCells = new ArrayList&amp;lt;int[]&amp;gt;();
	for (int[] cell : gameBoard)
		if (cell[STATUS_VALUE] == FLAGGED)
			flaggedCells.add(x);
		return flaggedCells;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순성은 변하지 않았고 함수의 이름만 바꿔줬어도 훨씬 명확해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;int 배열 대신 칸을 간단한 클래스로 만들고 isFlagged라는 명시적인 함수를 사용해 FLAGGED라는 상수를 감춰주면 더욱 개선된다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;public List&amp;lt;Cell&amp;gt; getFlaggedCells() {
	List&amp;lt;Cell&amp;gt; flaggedCells = new ArrayList&amp;lt;Cell&amp;gt;();
	for (Cell cell : gameBoard)
		if (cell.isFlagged())
			flaggedCells.add(cell);
		return flaggedCells;
}
&lt;/code&gt;&lt;/pre&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그릇된 정보를 피하라&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래머에게 List는 특수한 의미, 계정을 담는 컨테이너가 실제 List가 아니라면 accountList라 쓰지 않는다. (실제 List라도 List를 쓰지 않는 게 바람직하다.) accountGroup, brunchOfAccounts, Accounts 라 명명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 흡사한 이름을 사용하지 않도록 주의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 모듈에서 XYZControllerForEfficientHandlingOfStrings 가 있고 조금 떨어진 모듈에서 XYZControllerForEfficientStorageOfStrings라는 이름을 쓴다면 겁나게 헷갈린다.&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;h3 data-ke-size=&quot;size23&quot;&gt;의미 있게 구분하라&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연속적인 숫자 (a1, a2, ..., aN) - 아무런 정보도 제공하지 못한다.&lt;/li&gt;
&lt;li&gt;불용어를 추가한 이름 - 역시 아무런 정보도 제공하지 못한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Product라는 클래스가 있다면 ProductInfo, ProductData는 개념을 구분하지 않은 채 이름만 달리 한 경우다. Info나 Data는 a, an, the와 마찬가지로 의미가 불분명한 불용어다.&lt;/li&gt;
&lt;li&gt;getActiveAccount(); getActiveAccounts(); getActiveAccountInfo();&lt;/li&gt;
&lt;li&gt;monyAmount - money customerInfo - customer accountData - account theMessage - message&lt;/li&gt;
&lt;li&gt;위 경우들은 서로 구분이 안 된다. 읽는 사람이 차이를 알도록 이름을 지어라&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;class DtaRcrd102 {
	private Date genymdhms;
	private Date modymdhms;
	private final String pszqint = &quot;102&quot;;
	/* ... */
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vs&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;class Customer {
	private Date generationTimestamp;
	private Date modificationTimestamp;
	private final String recordId = &quot;102&quot;;
	/* ... */
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;검색하기 쉬운 이름을 사용하라&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자 하나를 사용하는 이름과 상수는 쉽게 눈에 띄지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MAX_CLASSES_PER_STUDENT는 찾기 쉽지만 7은 은근히 까다롭다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;e라는 문자도 변수 이름으로 적합하지 못하다. e는 영어에서 가장 많이 쓰이는 문자. 검색이 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름 길이는 범위 크기에 비례해야 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;for (int j=0; j&amp;lt;34; j++) {
	s += (t[j]*4)/5;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vs&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;int realDaysPerIdealDay = 4;
const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for (int j=0; j &amp;lt; NUMBER_OF_TASKS; j++) {
	int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
	int realTaskWeeks = (realTaskDays / WORK_DAYS_PER_WEEK);
	sum += realTaskWeeks;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 sum이 별로 유용하진 않으나 최소한 검색이 가능하다. 이름을 의미있게 지으면 함수가 길어진다. 하지만 WORK_DAYS_PER_WEEK를 찾기가 얼마나 쉬운지 생각해보라. 그냥 5를 사용한다면 5가 들어가는 이름을 모두 찾은 후 의미를 분석해 원하는 상수를 가려내야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자신의 기억력을 자랑하지 마라&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;독자가 코드를 읽으면서 변수 이름을 자신이 아는 이름으로 변환해야 한다면 그 변수 이름은 바람직하지 못하다. 이는 일반적으로 문제 영역이나 해법 영역에서 사용하지 않는 이름을 선택했기 때문에 생기는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;똑똑한 프로그래머와 전문가 프로그래머 사이에서 나타나는 차이점 하나만 들자면, 전문가 프로그래머는 &lt;b&gt;명료함이 최고&lt;/b&gt;라는 사실을 이해한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클래스 이름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 이름과 객체 이름은 명사나 명사구가 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 예&lt;br /&gt;Customer, WikiPage, Account, AddressParser&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나쁜 예&lt;br /&gt;Manager, Processor, Data, Info&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동사는 사용하지 않는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메서드 이름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 이름은 동사나 동사구가 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 예&lt;br /&gt;postPayment, deletePage, save&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접근자(Accessor), 변경자(Mutator), 조건자(Predicate)는 (javabean 표준에 따르면) get, set, is를 붙인다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;string name = employee.getName();
customer.setName(&quot;mike&quot;);
if (paycheck.isPosted())...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자(Constructor)를 중복정의(overload)할 때는 정적 팩토리 메서드를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드는 인수를 설명하는 이름을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Complex fulcrumPoint = Complex.FromRealNumber(23.0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드가 아래 코드보다 좋다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;Complex fulcrumPoint = new Complex(23.0);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한 개념에 한 단어를 사용하라&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추상적인 개념 하나에 단어 하나를 선택해 이를 고수한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 똑같은 메서드를 클래스마다 fetch, retrieve, get으로 제각각 부르면 혼란스럽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로, 동일 코드 기반에 controller, manager, driver를 섞어 쓰면 혼란스럽다. DeviceManager와 ProtocolController는 근본적으로 어떻게 다른가? 어째서 둘 다 Manager가 아닌가? 정말 둘 다 Driver가 아닌가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름이 다르다면 독자는 당연히 클래스도 다르고 타입도 다르리라 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일관성 있는 어휘는 코드를 사용할 프로그래머가 반갑게 여길 선물이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해법 영역에서 가져온 이름을 사용하라&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 이름을 문제 영역(도메인)에서 가져오는 정책은 현명하지 못하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 읽을 사람도 프로그래머라는 사실을 명심한다. 프로그래머에게 익숙한 기술 개념은 아주 많다. 기술 개념에는 기술 이름이 가장 적합한 선택이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 영역에서 가져온 이름을 사용하라&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적절한 '프로그래머 용어'가 없다면 문제 영역(도메인)에서 이름을 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우수한 프로그래머와 설계자라면 해법 영역과 문제 영역을 구분할 줄 알아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의미 있는 맥락을 추가하라&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스, 함수, 이름 공간에 넣어 맥락을 부여한다. 모든 방법이 실패하면 마지막 수단으로 접두어를 붙인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;firstName, lastName, street, houseNumber, city, state, zipcode라는 변수가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수를 훑어보면 주소라는 사실을 금방 알아챈다. 하지만 어느 메서드가 state라는 변수 하나만 사용한다면 변수 state가 주소의 일부라는 사실을 알아채기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;addr라는 접두어를 추가해 addrFirstName, addrLastName, addrState라 쓰면 맥락이 좀 더 분명해진다. 변수가 좀 더 큰 구조에 속한다는 사실이 적어도 독자에게는 분명해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 Address라는 클래스를 생성하면 더 좋다. 그러면 변수가 좀 더 큰 개념에 속한다는 사실이 컴파일러에게도 분명해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맥락이 불분명한 변수. 함수 이름은 맥락 일부만 제공하며, 알고리즘이 나머지 맥락을 제공한다. 함수를 끝까지 읽어보고 나서야 number, verb, pluralModifire라는 변수를 이해할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불행히도 독자가 맥락을 유추해야만 한다. 그냥 메서드만 훑어서는 세 변수의 의미가 불분명하다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;private void printGuessStatistics(char candidate, int count) {
	String number;
	String verb;
	String pluralModifier;
	if (count == 0) {
		number = &quot;no&quot;;
		verb = &quot;are&quot;;
		pluralModifier = &quot;s&quot;;
	} else if (count == 1) {
		number = &quot;1&quot;;
		verb = &quot;is&quot;;
		pluralModifire = &quot;&quot;;
	} else {
		number = Integer.toString(count);
		verb = &quot;are&quot;;
		pluralModifier = &quot;s&quot;;
	}
	String guessMessage = String.format(
		&quot;There %s %s%s&quot;, verb, number, candidate, pluralModifier
	};
	print(guessMessage);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 함수가 좀 길다. 세 변수를 함수 전반에 사용한다. 함수를 작은 조각으로 쪼개고자 GuessStatisticsMessage라는 클래스를 만든 후 세 변수를 클래스에 넣었다. 그러면 세 변수는 맥락이 분명해진다. 즉 세 변수는 확실하게 GuessStatisticsMessage에 속한다. 이렇게 맥락을 개선하면 함수를 쪼개기가 쉬워지므로 알고리즘도 좀 더 명확해진다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class GuessStatisticsMessage {
	private String number;
	private String verb;
	private String pluralModifier;

	public String make(char candidate, int count) {
		createPluralDependentMessageParts(count);
		return String.format)
			&quot;There %s %s %s%s&quot;,
			verb, number, candidate, pluralModifre );
	}

	private void createPluralDependentMessageParts(int count) {
		if (count == 0) {
			thereAreNoLetters();
		} else if (count == 1) {
			thereIsOneLetter();
		} else {
			thereAreManyLetters(count);
		}
	}

	private void thereAreManyLetters(int count) {
		number = Integer.toString(count)
		verb = &quot;are&quot;;
		pluralModifier = &quot;s&quot;;
	}

	private void thereIsOneLetter() {
		number = &quot;1&quot;;
		verb = &quot;is&quot;;
		pluralModifier = &quot;&quot;;
	}

	private void thereAreNoLetters() {
		number = &quot;no&quot;;
		verb = &quot;are&quot;;
		pluralModifier = &quot;s&quot;;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;불필요한 맥락을 없애라&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로는 짧은 이름이 긴 이름보다 좋다. 단, 의미가 분명한 경우에 한해서다. 이름에 불필요한 맥락을 추가하지 않도록 주의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'고급 휘발유 충전소 Gas Station Deluxe'라는 애플리케이션을 짠다고 가정하다. 모든 클래스 이름을 GSD로 시작하겠다는 생각은 전혀 바람직하지 못하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;accountAddress와 customerAddress는 Address 클래스 인스턴스로는 좋은 이름이나 클래스 이름으로는 적합하지 못하다.&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;style5&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;나쁜 예를 읽으면서 내가 어제 오늘 짰던 코드들이 떠올라 마음이 아팠다.&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;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;face&quot; data-emoticon-name=&quot;029&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/029.png&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/029.png&quot; width=&quot;80&quot; /&gt;&lt;/figure&gt;</description>
      <category>Clean Code</category>
      <category>clean code</category>
      <category>네이밍</category>
      <category>어렵</category>
      <category>의미 있는 이름</category>
      <author>삼쓰 웅쓰</author>
      <guid isPermaLink="true">https://woongsios.tistory.com/318</guid>
      <comments>https://woongsios.tistory.com/318#entry318comment</comments>
      <pubDate>Mon, 6 Dec 2021 23:21:55 +0900</pubDate>
    </item>
    <item>
      <title>클린 코드 1장 깨끗한 코드</title>
      <link>https://woongsios.tistory.com/317</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&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;&quot;나중은 결코 오지 않는다.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기한을 맞추는 유일한 방법은, 그러니까 빨리 가는 유일한 방법은, 언제나 코드를 최대한 깨끗하게 유지하는 습관이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래머들이 대충 넘어가는 부분 중 하나가 오류 처리다. 메모리 누수, 경쟁 상태(race condition), 일관성 없는 명명법이 또 다른 예다. 한 마디로 요악하면, 깨끗한 코드는 세세한 사항까지 처리하는 코드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깨끗한 코드란 한 가지를 잘 한다. &lt;br /&gt;나쁜 코드는 너무 많은 일을 하려 애쓰다가 의도가 뒤섞이고 목적이 흐려진다. &lt;br /&gt;깨끗한 코드는 한 가지에 '집중'한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깨끗한 코드는 단순하고 직접적이다. &lt;br /&gt;깨끗한 코드는 잘 쓴 문장처럼 읽힌다. &lt;br /&gt;결코 설계자의 의도를 숨기지 않는다. &lt;br /&gt;오히려 명쾌한 추상화와 단순한 제어문으로 가득하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깨끗한 코드는 작성자가 아닌 사람도 읽기 쉽고 고치기 쉽다. &lt;br /&gt;단위 테스트 케이스와 인수 테스트 케이스가 존재한다. &lt;br /&gt;깨끗한 코드에는 의미 있는 이름이 붙는다. &lt;br /&gt;특정 목적을 달성하는 방법은 하나만 제공한다. &lt;br /&gt;의존성은 최소이며 각 의존성을 명확히 정의한다. &lt;br /&gt;API는 명확하며 최소로 줄였다. &lt;br /&gt;언어에 따라 필요한 모든 정보를 코드만으로 명확히 표현할 수 없기에 코드는 문학적으로 표현해야 마땅하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;깨끗한 코드란 &lt;b&gt;다른&lt;/b&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;/li&gt;
&lt;li&gt;중복이 없다.&lt;/li&gt;
&lt;li&gt;시스템 내 모든 설계 아이디어를 표현한다.&lt;/li&gt;
&lt;li&gt;클래스, 메서드, 함수 등을 최대한 줄인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복을 피하라. 한 기능만 수행하라. 제대로 표현하라. 작게 추상화하라.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 읽는 시간 대 코드를 짜는 시간 비율이 10대 1을 훌쩍 넘는다. 새 코드를 짜면서 우리는 끊임없이 기존 코드를 읽는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비율이 이렇게 높으므로 읽기 쉬운 코드가 매우 중요하다. 주변 코드를 읽지 않으면 새 코드를 짜지 못한다. 주변 코드가 읽기 쉬우면 새 코드를 짜기도 쉽다. 주변 코드를 읽기가 어려우면 새 코드를 짜기도 어렵다. 그러므로 급하다면, 서둘러 끝내려면, 쉽게 짜려면, 읽기 쉽게 만들면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체크아웃 할 때보다 좀 더 깨끗한 코드를 체크인한다면 코드는 절대 나빠지지 않는다. 한꺼번에 많은 시간과 노력을 투자해 코드를 정리할 필요가 없다. 변수 이름 하나를 개선하고, 조금 긴 함수 하나를 분할하고, 약간의 중복을 제거하고, 복잡한 if 문 하나를 정리하면 충분하다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;누가 읽어도 크게 이견이 없을만한 좋은 내용들의 향연이었던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 못짜고 있을 뿐 ,,,  &lt;/p&gt;</description>
      <category>Clean Code</category>
      <category>clean code</category>
      <category>깨끗한 코드</category>
      <category>클린 코드</category>
      <author>삼쓰 웅쓰</author>
      <guid isPermaLink="true">https://woongsios.tistory.com/317</guid>
      <comments>https://woongsios.tistory.com/317#entry317comment</comments>
      <pubDate>Mon, 6 Dec 2021 23:12:37 +0900</pubDate>
    </item>
    <item>
      <title>Skip - 구독 후 처음 n번 스킵하기</title>
      <link>https://woongsios.tistory.com/316</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Skip&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구독 후 처음 n번을 건너 뛰고 싶을 때 사용하는 Operator 입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1594&quot; data-origin-height=&quot;568&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AarSf/btrmCCqvmwR/ZOuhOSmtvE2KLzHukDKQ01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AarSf/btrmCCqvmwR/ZOuhOSmtvE2KLzHukDKQ01/img.png&quot; data-alt=&quot;http://reactivex.io/documentation/ko/operators/skip.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AarSf/btrmCCqvmwR/ZOuhOSmtvE2KLzHukDKQ01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAarSf%2FbtrmCCqvmwR%2FZOuhOSmtvE2KLzHukDKQ01%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;1594&quot; height=&quot;568&quot; data-origin-width=&quot;1594&quot; data-origin-height=&quot;568&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;http://reactivex.io/documentation/ko/operators/skip.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림처럼, 처음 원하는 n번의 이벤트를 스킵해줄 수 있습니다!&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;저는 TextField에 처음 값을 세팅하고 이후에 들어오는 입력부터 유효성 검사를 하고 싶었는데요,&amp;nbsp;&lt;br /&gt;검사가 완료, 실패하고 나서는 텍스트 필드에 변화를 줘야됐어서, 처음에는 유효성 검사를 하면 안 되는 경우를 만났었어요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 사용해서 처음 구독할 때 검사를 스킵해줄 수 있었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1638263872988&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;textField.rx.text.orEmpty
	.skip(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;br /&gt;막상 필요할 때 어떤 오퍼레이터가 필요한지, 무엇이 있는지, 모를 수&amp;nbsp; 있으니 차곡차곡 알아가보도록 해야겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다.&lt;/p&gt;</description>
      <category>RxSwift</category>
      <category>ios</category>
      <category>operators</category>
      <category>rxswift</category>
      <category>SKIP</category>
      <category>구독 후 스킵</category>
      <author>삼쓰 웅쓰</author>
      <guid isPermaLink="true">https://woongsios.tistory.com/316</guid>
      <comments>https://woongsios.tistory.com/316#entry316comment</comments>
      <pubDate>Tue, 30 Nov 2021 18:20:44 +0900</pubDate>
    </item>
    <item>
      <title>UITextField clear button custom</title>
      <link>https://woongsios.tistory.com/315</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;UITextField 에서는 기본적으로 clear 버튼을 제공하는데요,&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;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;기본 clear button&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 기본적인 clear 버튼은 clearButtonMode를 통해 만들어줄 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1635166855216&quot; class=&quot;swift&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 20px; color: #383a42; background: #f8f8f8; font-size: 14px; font-family: 'SF Mono', Menlo, Consolas, Monaco, monospace; border: 1px solid #ebebeb; line-height: 1.71; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;textfield.clearButtonMode = .whileEditing&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;748&quot; data-origin-height=&quot;204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKyRKs/btriQ5iX9GM/NTgKkZCOEfqhmivUEBtxP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKyRKs/btriQ5iX9GM/NTgKkZCOEfqhmivUEBtxP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKyRKs/btriQ5iX9GM/NTgKkZCOEfqhmivUEBtxP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKyRKs%2FbtriQ5iX9GM%2FNTgKkZCOEfqhmivUEBtxP1%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;748&quot; height=&quot;204&quot; data-origin-width=&quot;748&quot; data-origin-height=&quot;204&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;이렇게 4가지 모드가 있는데 각각은 보시면 아시겠죠??&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 이걸 설정해주면 이렇게 clear 버튼이 나타나고, 버튼을 눌러 clear 동작까지 알아서 해줄 수가 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;312&quot; data-origin-height=&quot;66&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kyTDa/btriQ7gJwKf/yELuAl9ZF86SPU6EC50Cw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kyTDa/btriQ7gJwKf/yELuAl9ZF86SPU6EC50Cw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kyTDa/btriQ7gJwKf/yELuAl9ZF86SPU6EC50Cw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkyTDa%2FbtriQ7gJwKf%2FyELuAl9ZF86SPU6EC50Cw0%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;312&quot; height=&quot;66&quot; data-origin-width=&quot;312&quot; data-origin-height=&quot;66&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;h2 data-ke-size=&quot;size26&quot;&gt;clear button custom&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 저 clear 버튼을 커스텀 할 수가 없다는 겁니다..&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;s&gt;(아니   쓰는김에 좀 더 쓰지...)&lt;br /&gt;&lt;/s&gt;제 문제는 저 어두운 배경에서 clear 버튼이 너무 안 보인다는 거 였어요...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결론부터 말씀드리면 저 버튼을 직접 건드릴 생각은 하면 안 되고 textField의 rightView를 직접 붙여줘서 처리해줘야 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히 rightView도 rightViewMode를 제공해서 clear 버튼처럼 어느 상황에 나타날지 처리해줄 수가 있어요!&lt;br /&gt;대신, clear 기능은 직접 구현을 해야합니다... ㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rightView는 overlayView를 지정해주는건데요, clearButton은 따로 지정해주기 전에 미리 내장되어있는 overlayView 에요,&lt;br /&gt;그래서 rightView를 지정하고, rightViewMode를 통해서 rightView로 overlayView를 바꿔주면&lt;br /&gt;기존에 내장되어 있던 clearButton(overlayView)은 나타나지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 원리는 이렇구요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 다른 cancel 이미지를 가지는 버튼을 만들어서 rightView에 붙이는 방식으로 처리를 했습니다.&lt;br /&gt;이 부분은 말 그대로 버튼이나 원하는 뷰를 붙여서 만드는 거라서 원하는 형태로 커스텀 하시면 될 것 같아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 extension으로 이렇게 처리해봤어요&lt;/p&gt;
&lt;pre id=&quot;code_1635166855217&quot; class=&quot;swift&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 20px; color: #383a42; background: #f8f8f8; font-size: 14px; font-family: 'SF Mono', Menlo, Consolas, Monaco, monospace; border: 1px solid #ebebeb; line-height: 1.71; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import UIKit

extension UITextField {
    
    func setClearButton(with image: UIImage, mode: UITextField.ViewMode) {
        let clearButton = UIButton(type: .custom)
        clearButton.setImage(image, for: .normal)
        clearButton.frame = CGRect(x: 0, y: 0, width: 40, height: 40)
        clearButton.contentMode = .scaleAspectFit
        clearButton.addTarget(self, action: #selector(UITextField.clear(sender:)), for: .touchUpInside)
        self.rightView = clearButton
        self.rightViewMode = mode
    }
    
    @objc
    private func clear(sender: AnyObject) {
        self.text = &quot;&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 한 가지 사소한 문제가 발생하는데요, 기존에 clear 버튼의 동작과 조금 다르게 동작 하는 부분이 있습니다... ㅠㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기존에 [clear버튼]은 whileEditing 모드일 때, textField의 text가 비어있을 때 clear 버튼이 보이지 않는데요.&lt;/b&gt;&lt;br /&gt;&lt;b&gt;[rightView]는 whileEditing 모드여도, 그냥 firstResponder만 되어도 rightView가 나타납니다.&lt;/b&gt;&lt;br /&gt;&lt;b&gt;물론 clear를 해도 그대로 남아있죠...&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 큰 문제는 아니라 그대로 써도 되지만.. 기존 clear 버튼처럼 동작하게 하기 위해 다음과 같이 추가해봤습니다.&lt;br /&gt;결국 text가 비어있는지 계속 체크해서 확인해주는 방식이에요. (이게 최선인지는 모르겠습니다,,,)&lt;/p&gt;
&lt;pre id=&quot;code_1635166855217&quot; class=&quot;swift&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 20px; color: #383a42; background: #f8f8f8; font-size: 14px; font-family: 'SF Mono', Menlo, Consolas, Monaco, monospace; border: 1px solid #ebebeb; line-height: 1.71; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import UIKit

extension UITextField {
    
    func setClearButton(with image: UIImage, mode: UITextField.ViewMode) {
        let clearButton = UIButton(type: .custom)
        clearButton.setImage(image, for: .normal)
        clearButton.frame = CGRect(x: 0, y: 0, width: 40, height: 40)
        clearButton.contentMode = .scaleAspectFit
        clearButton.addTarget(self, action: #selector(UITextField.clear(sender:)), for: .touchUpInside)
        self.addTarget(self, action: #selector(UITextField.displayClearButtonIfNeeded), for: .editingDidBegin)
        self.addTarget(self, action: #selector(UITextField.displayClearButtonIfNeeded), for: .editingChanged)
        self.rightView = clearButton
        self.rightViewMode = mode
    }
    
    @objc
    private func displayClearButtonIfNeeded() {
        self.rightView?.isHidden = (self.text?.isEmpty) ?? true
    }
    
    @objc
    private func clear(sender: AnyObject) {
        self.text = &quot;&quot;
        sendActions(for: .editingChanged)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금이라도 도움이 되셨다면 좋겠네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다 :)&lt;/p&gt;</description>
      <category>iOS</category>
      <category>clear button</category>
      <category>ios</category>
      <category>Showing rightView for UITextField only if empty and not selected</category>
      <category>UITextField</category>
      <category>uitextfield clear button custom</category>
      <category>x 버튼</category>
      <author>삼쓰 웅쓰</author>
      <guid isPermaLink="true">https://woongsios.tistory.com/315</guid>
      <comments>https://woongsios.tistory.com/315#entry315comment</comments>
      <pubDate>Mon, 25 Oct 2021 22:02:59 +0900</pubDate>
    </item>
  </channel>
</rss>