<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>yeahg_dev.log</title>
        <link>https://velog.io/</link>
        <description>i🍎S 개발을 합니다</description>
        <lastBuildDate>Tue, 22 Aug 2023 09:51:22 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>yeahg_dev.log</title>
            <url>https://velog.velcdn.com/images/yeahg_dev/profile/9fe74894-29db-4af1-9862-4ea0ac8aa973/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. yeahg_dev.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/yeahg_dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[iOS 배포] .ipa란?  .ipa 디바이스에서 실행하는 방법]]></title>
            <link>https://velog.io/@yeahg_dev/iOS-%EB%B0%B0%ED%8F%AC-.ipa%EB%9E%80-.ipa-%EB%94%94%EB%B0%94%EC%9D%B4%EC%8A%A4%EC%97%90%EC%84%9C-%EC%8B%A4%ED%96%89%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@yeahg_dev/iOS-%EB%B0%B0%ED%8F%AC-.ipa%EB%9E%80-.ipa-%EB%94%94%EB%B0%94%EC%9D%B4%EC%8A%A4%EC%97%90%EC%84%9C-%EC%8B%A4%ED%96%89%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Tue, 22 Aug 2023 09:51:22 GMT</pubDate>
            <description><![CDATA[<h2 id="ipa-확장자란">.ipa 확장자란?</h2>
<ul>
<li><p>iOS앱 실행 파일을 포함한 애플리케이션 파일 패키지 형식.</p>
</li>
<li><p>앱 스토어(App Store)를 통해 배포되거나, 엔터프라이즈 배포 또는 테스팅 목적으로 내부적으로 배포될 때 사용됩니다.</p>
</li>
<li><p>빌드한 아카이브를 <code>Distribute App</code>을 통해 코드 서명까지 거치면 ipa파일을 만들 수 있습니다. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/0e4591c1-290a-4249-a93e-d2952c028aed/image.png" alt=""></p>
</li>
<li><p>여러개의 ipa파일이 생성된 이유는 배포할 때 app thining 옵션에서 <code>All Compatible device vairants</code> 를 선택했기 때문입니다. app-thining.plist 파일에 들어가면 각 파일에 대한 정보를 볼 수 있습니다.
<img src="https://velog.velcdn.com/images/yeahg_dev/post/375ce5c8-5498-40e0-a1ef-21bea97e4632/image.png" alt=""></p>
</li>
</ul>
<ul>
<li>.zip으로 확장자를 바꿔서 내부 패키지를 열어 볼 수도 있습니다.
: <code>.zip</code>으로 확장자 변경 &gt; 압축 풀기 &gt; Payload 폴더 &gt; 응용 프로그램 우클릭 &gt; 패키지 내용 보기
<img src="https://velog.velcdn.com/images/yeahg_dev/post/3ebcd0ad-dd55-4f82-9c2a-49d808804359/image.png" alt=""></li>
<li>실행 가능한 바이너리 코드</li>
<li>리소스 (이미지, </li>
<li>ProvisioningProfile</li>
<li>info.plist... 등으로 이루어진걸 확인할 수 있습니다. </li>
</ul>
<br>

<h2 id="ipa-파일을-기기에서-실행시키는-방법">.ipa 파일을 기기에서 실행시키는 방법</h2>
<ul>
<li>AppStoreConnect나 TestFlight에 올리지 않고 기기에 실행하는 방법은 아래와 같은 것이 있습니다.</li>
<li>단, Provisioning Profile에 따라 설치할 수 있는 기기가 한정적일 수 있습니다.</li>
</ul>
<ol>
<li><p>Xcode - Device and Simulator에서 Add App</p>
</li>
<li><p>Apple Configurator 앱 추가
Apple Configurator(앱 스토어 다운) : 대량의 Apple 기기를 설정, 관리하고 앱을 배포할 수 있는 프로그램. 주로 기업, 학교, 조직 등에서 여러 iOS 기기를 효율적으로 관리하고 설정하는 데 활용됩니다.</p>
</li>
<li><p>OAT(over-the-air) : html로 앱을 다운로드 받는 방법 (Firebase, Dropbox등이 이 방법 사용),  manifest.plist 파일이 필요하므로 아래 옵션을 체크해야합니다. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/89b972b0-3b2d-40d1-8a31-744e60b4b40d/image.jpg" alt=""></p>
</li>
<li><p><a href="https://support.apple.com/ko-kr/guide/security/sec013b5d35d/web">MDM(Mobile Device Manager)</a></p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Xcode custom script 시작하기]]></title>
            <link>https://velog.io/@yeahg_dev/Xcode-custom-script-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yeahg_dev/Xcode-custom-script-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 06 Mar 2023 07:45:47 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요! 릴리입니다~
오늘은 Xcode의 Build phase에서 custom script를 실행하는 방법과 용어들에 대해 초심자의 눈으로 하나하나 알아보도록 하겠습니당🤓</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/0d31f428-851a-4775-98d0-e9aa404ff44a/image.png" alt=""></p>
<br>

<h2 id="intro-script-얼렁뚱땅-써본-적은-이씀">Intro: script 얼렁뚱땅 써본 적은 이씀</h2>
<p>우선 저는 <a href="https://github.com/SwiftGen/SwiftGen">SwiftGen</a>과 <a href="https://trilliwon.medium.com/ios%EC%97%90%EC%84%9C-localization%ED%95%98%EB%8A%94-gorgeous-%ED%95%9C-%EB%B0%A9%EB%B2%95-f82ac29d2cfe">.string파일을 타입으로 자동 생성하기</a>위해 Build phase에서 script를 실행시켜본 적은 있습니다. </p>
<p>두 번 모두 글에서 사용 방법을 하나 하나 알려주셨기 때문에 하라는 대로 하니 스크립트를 정상적으로 실행할 수 있었습니다. 하지만... script는 뭐고..? <code>Input File</code>과 <code>Output File</code>은 뭔지..알지도 못하고 사용하니 찜찜하더군요.. 결국 스크립트를 수정하다보니 무엇을 어떻게 건들여야할지 감조차 잡히지 않는 결과를. . .🤦‍♀️ </p>
<p>그래서 오늘 제대로 알기 위해 포스팅을 작성하게 되었습니다!</p>
<br>

<h1 id="shell-script가-뭐지">Shell Script가 뭐지?</h1>
<blockquote>
<p>셸 스크립트(shell script)는 <strong>셸</strong>이나 <strong>명령 줄 인터프리터</strong>에서 돌아가도록 작성되었거나 한 운영 체제를 위해 쓰인 <strong>스크립트</strong>이다. 단순한 도메인 고유 언어로 여기기도 한다. 셸 스크립트가 수행하는 일반 기능으로는 파일 이용, 프로그램 실행, 문자열 출력 등이 있다.   - 위키백과 -</p>
</blockquote>
<ul>
<li><p>셸(shell):   운영 체제 상에서 다양한 운영 체제 기능과 서비스를 구현하는 인터페이스를 제공하는 프로그램이다. 셸(껍데기의 영어 단어)은 <strong>사용자와 운영 체제의 내부(커널) 사이</strong>의 인터페이스를 감싸는 층이기 때문에 그러한 이름이 붙었다. 유닉스에는 본 셸(sh), C 셸(csh), 콘 셸(ksh), 스키마 셸(scsh), Z셸(zsh)이 있다. </p>
</li>
<li><p>명령어 인터프리터(command-line interpreter) : 입력된 명령어를 읽고 명령어를 하나 이상의 시스템 호출로 변환함으로써 실행시키는 프로그램. </p>
</li>
<li><p>스크립트 언어(script language) : 응용 소프트웨어를 제어하는 컴퓨터 프로그래밍 언어. 스크립트 언어는 응용 프로그램과 독립하여 사용되고 일반적으로 응용 프로그램의 언어와 다른 언어로 사용되어 최종사용자가 응용 프로그램의 동작을 사용자의 요구에 맞게 수행할 수 있도록 해준다. </p>
</li>
</ul>
<p><strong>운영체제의 기능을 이용할 수 있는 프로그래밍 언어</strong>이며 <strong>쉘이나 명령 줄 인터프리터에서 실행</strong>되는 언어라고 이해했다. 특징들은 아래와 같다. </p>
<ul>
<li>.sh라는 파일 확장자를 가진 파일이 특정 종류의 셸 스크립트를 가리키는 것이 보통이지만, 대부분의 셸 스크립트는 파일 확장자를 지니지 않는다.</li>
<li>컴파일 단계가 없기 때문에 디버깅을 하는 동안 빠르게 실행 가능하다</li>
</ul>
<br>

<h1 id="build-phase는-뭐지">Build phase는 뭐지?</h1>
<p>쉘 스크립트 언어는 Build phase에서 추가할 수 있다. 
그렇다면 Build Phase는 뭘 하는 단계일까?</p>
<p><strong>타겟을 빌드할 때 타겟 별로 개별 태스크를 설정</strong>하는 단계이다. </p>
<br>

<h1 id="new-run-script-phase를-하면">New Run script phase를 하면?</h1>
<p>이제 Build phase에서 <code>+</code> 버튼을 눌러서 <code>New Run script phase</code>를 클릭하면 <strong>빌드 단계 동안 특정 셸 스크립트 언어를 실행</strong>할 수 있다. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/91f5a4e8-3257-4810-adfc-f3518fe217a0/image.png" alt=""></p>
<ul>
<li>스크립트는 <code>$(SRCROOT)</code>, 타겟의 소스 파일을 포함하는 디렉토리와 같이 <strong>타겟의 <a href="https://developer.apple.com/documentation/xcode/build-settings-reference?changes=_8">빌드 셋팅</a></strong>을 참조할 수 있다. </li>
<li>input, output file의 리스트를 제공할 수 있다. </li>
<li>input과 output 파일이 없을 땐 스크립트는 빌드될 때마다 항상 실행된다. </li>
<li>반면 input과 output 파일이 있을 땐 전에 실행된 적이 없거나, input 파일 중 하나라도 수정되었거나, output 파일이 하나라도 없어졌을 때 실행된다. </li>
<li>run script는 모든 빌드에서 가능할 수도, 또는 installation build 동안만, 또는 하나의 타겟당 여러번의 run script가 추가될 수 있다. </li>
<li>타겟의 다른 build phase(컴파일 및 링크 빌드 단계등)와 별도로 실행된다.</li>
</ul>
<br>

<h1 id="run-script의-각-메뉴-살펴보기">Run script의 각 메뉴 살펴보기</h1>
<blockquote>
<p>원문 : <a href="https://developer.apple.com/documentation/xcode/running-custom-scripts-during-a-build?changes=_8">📄 Article: Running custom scripts during a build</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/554a22f5-8d71-4db4-b314-8bf78887b28b/image.png" alt=""></p>
<h2 id="shell-script-environment-란">Shell script environment 란?</h2>
<p><code>Shell</code>이라는 제목 옆에는 <code>/bin/sh</code>가 쓰여 있고,
이 곳에 shell script environment를 쓰라고 안내되어 있다. </p>
<blockquote>
<p><strong>실행될 셸의 경로</strong>를 적어주면 된다.</p>
</blockquote>
<p><code>/bin/sh</code>는 기본 셸을 사용하겠다는 의미이다. 
<code>sh</code>는 <a href="https://ko.wikipedia.org/wiki/%EB%B3%B8_%EC%85%B8">본 셸(Bourne shell)</a>의 줄임말로 유닉스의 기본 셸이다. </p>
<br>

<h2 id="script-제공하기">script 제공하기</h2>
<p>실행할 스크립트를 아래 칸에 작성하거나, 스크립트 파일의 경로를 작성한다. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/31522b2e-684e-480d-9541-d0d2b6c851a3/image.png" alt=""></p>
<h2 id="input-ouptut-file-이란">Input, Ouptut File 이란?</h2>
<p>Input File로는 스크립트가 처리할 데이터를 제공할 수 있고, 
Output file은 script에 의해 생성된 데이터를 저장할 수 있다.  </p>
<p>2가지 방법으로 input, output file을 적을 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/16c7e7cf-ab84-4e28-88ba-bdf45ad07aa9/image.png" alt=""></p>
<ol>
<li><p>각 파일의 path를 string으로 제공하는 방법
파일의 경로를 빌드하기 전 미리 알고 있거나, 바뀌지 않을 것이라 확신할 때 사용할 수 있다.
<img src="https://velog.velcdn.com/images/yeahg_dev/post/265dec48-7260-4265-a141-c39fd13fdf57/image.png" alt=""></p>
</li>
<li><p><code>.xcfilelist</code> 파일 확장자의 textfile로 파일 리스트를 제공하는 방법
파일 리스트는 파일들을 편집하기 용이하고, 특히 리스트가 자주 변경되거나 리스트에 커멘츠를 남기고 싶을 때 유용하다.
<img src="https://velog.velcdn.com/images/yeahg_dev/post/e0a7b5b7-0fe5-48c8-b2d3-8b4f27ae0247/image.png" alt=""></p>
</li>
</ol>
<p>Path String은 <code>$(SRCROOT)</code>와 같은 build variable을 사용할 수 있다. </p>
<ul>
<li><code>$(SRCROOT)</code> : build variable로 프로젝트가 속한 루트 디렉토리를 저장하고 있음</li>
</ul>
<blockquote>
<p>💡 Note
input, output file을 적는 것이 필수는 아니지만, 적는 것을 강력 권장한다. Xcode는 input, output file의 세트를 사용해 필요시에만 스크립트를 실행시킴으로싸 빌드 타임 최적화를 한다. 만약 Input, output file을 적지 않으면 타겟을 빌드할 때마다 스크립트가 실행된다. 더 많은 정보는 <a href="https://developer.apple.com/documentation/Xcode/improving-the-speed-of-incremental-builds?changes=_8">Improving the speed of incremental builds</a>를 참고할 것</p>
</blockquote>
<br>


<h2 id="script-specific-environment-variable">Script-specific environment variable</h2>
<p>그 전에 알아본 것이 environment variable와 shell variable이다. 
shell은 두가지 타입의 variable을 갖는다.</p>
<h3 id="environment-variable">environment variable</h3>
<ul>
<li>전역(global) 변수. 한 번 정의되면 모든 shell에서 공유된다. </li>
<li>자식 shell에게 상속된다.</li>
<li><code>env</code> 명령어로 environment variable 리스트를 볼 수 있다.
<img src="https://velog.velcdn.com/images/yeahg_dev/post/a8f958f8-c7dc-4bf7-9d40-ec530cf3d12d/image.png" alt=""></li>
</ul>
<h3 id="shell-variable">shell variable</h3>
<ul>
<li>정의된 shell에서만 접근 가능한 변수.</li>
<li>자식 shell에게 상속되지 않는다. </li>
<li><code>export</code>명령어를 사용하면 전역 변수로 사용할 수 있다<pre><code class="language-shell">FOO=BAR // shell variable 정의
export FOO // environment variable로 정의 됨</code></pre>
</li>
</ul>
<p>변수에 값을 설정할 때는 이렇게</p>
<pre><code class="language-shell">FOO=BAR </code></pre>
<p>변수의 값을 사용할 때는 변수 앞에 <code>$</code>를 붙인다. </p>
<pre><code class="language-shell">echo $FOO </code></pre>
<p>다시 돌아와서 Xcode는 각 script를 위한 environment variable을 생성하고, 변수를 통해 script에 관련된 파일에 접근할 수 있다. (아래 그림은 생성하는 변수 리스트)</p>
<p>environment variable이라고 명칭해서 모든 셸에서 접근 가능한 변수인가 헷갈렸는데, 특정 스크립트에 관련된 정보를 제공하므로 해당 스크립트를 실행하는 셸에서만 사용할 수 있는 변수인 것 같다. </p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/d08b46cd-4e4e-4a09-85a0-fac219baf001/image.png" alt=""></p>
<p>또한 <a href="https://developer.apple.com/documentation/xcode/build-settings-reference?changes=_8">빌드 세팅</a>에도 접근 할 수 있다. </p>
<br>

<h2 id="script에서-error나-warning-log-찍기">Script에서 error나 warning Log 찍기</h2>
<p>error나 warning log를 찍는 스크립트를 작성하면 Xcode build log에 메시지를 찍을 수 있다.</p>
<p><code>echo</code> 명령어를 사용해 아래 포맷과 같이 작성한다. </p>
<pre><code class="language-shell">// message format
[filename]:[linenumber]: error | warning | note : [message]</code></pre>
<ul>
<li>filename : 에러가 특정 파일에서 발생한다면 에러가 발생하는 파일의 절대 경로를 명시</li>
<li>linenumber : 에러가 파일의 특정 라인에서 발생한다면 에러가 발생하는 라인의 번호를 명시</li>
</ul>
<pre><code class="language-shell">// 예시
echo &quot;error: An expected input file was missing.&quot;
echo &quot;warning: Skipping a file of an unknown type.&quot;</code></pre>
<br>

<h2 id="build-failure-유발하기">Build failure 유발하기</h2>
<p>스크립트가 실패하고 회복할 수 없을 때 nonzero exit code를 리턴하자. 
그리고 실패에 대한 적절한 정보도 함께 제공할 수도 있다. </p>
<p>Xcode는 nonzero exit 값을 빌드 실패로 간주한다. </p>
<pre><code class="language-shell">echo &quot;error: A fatal error occurred in the script.&quot;
exit 1</code></pre>
<p>위와 같은 코드는 에러 메시지를 빌드 로그에 남기고 빌드를 실패시킨다. </p>
<br>

<h2 id="정리">정리</h2>
<ul>
<li>Build phase에 <code>add New Run script</code>를 하면 운영체제와 상호작용할 수 있는 shell을 이용해 빌드 타임에 코드를 실행한다. 타겟별로 추가할 수 있기 때문에 타겟 개별 적으로 할 작업을 여기서 실행시킬 수 있다.</li>
<li>스크립트가 처리할 데이터를 input file로, 스크립트가 처리한 데이터를 저장할 output file을 지정할 수 있다. </li>
<li>Xcode가 제공하는 environmnet variable로 스크립트에서 관련 파일들과 빌드 세팅에 접근 할 수 있다. </li>
<li>스크립트에 error log를 프린트하는 스크립트를 작성하면 빌드 로그를 찍을 수도 있고, nonzero exit을 통해 빌드 실패를 유발할 수도 있다.</li>
</ul>
<br>

<h3 id="references">References</h3>
<p><a href="https://ko.wikipedia.org/wiki/%EC%85%B8">위키백과: Shell</a>
<a href="https://groups.google.com/g/han.comp.os.linux.apps/c/6fpaKm_qI0Q?pli=1">#!bin/bash의 의미</a>
<a href="https://www.purdue.edu/hla/sites/varalalab/wp-content/uploads/sites/20/2018/02/Lecture_5.pdf">Shell과 Environment vs. Shell variables(영어 강의 자료)</a>
<a href="https://mslilsunshine.tistory.com/141">Shell Environment에 대해 정리한 블로그</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[공공데이터포털 SERVICE_KEY_IS_NOT_REGISTERED_ERROR 원인 파헤치기]]></title>
            <link>https://velog.io/@yeahg_dev/%EA%B3%B5%EA%B3%B5%EB%8D%B0%EC%9D%B4%ED%84%B0%ED%8F%AC%ED%84%B8-SERVICEKEYISNOTREGISTEREDERROR-%EC%9B%90%EC%9D%B8-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0</link>
            <guid>https://velog.io/@yeahg_dev/%EA%B3%B5%EA%B3%B5%EB%8D%B0%EC%9D%B4%ED%84%B0%ED%8F%AC%ED%84%B8-SERVICEKEYISNOTREGISTEREDERROR-%EC%9B%90%EC%9D%B8-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0</guid>
            <pubDate>Tue, 14 Feb 2023 07:29:56 GMT</pubDate>
            <description><![CDATA[<h1 id="🚨-문제">🚨 문제</h1>
<hr>
<p>포스트맨에서는 정상적으로 오던 응답이,
앱에서 호출하니 <code>SERVICE_KEY_IS_NOT_REGISTERED_ERROR</code>라는 응답을 받았다. </p>
<p>Response URL을 브라우저에 로드한 결과⬇️
(Parsing 실패 에러로 빠져서 이상한데서 삽질함...🤦‍♀️)
<img src="https://velog.velcdn.com/images/yeahg_dev/post/cc77fbe1-174e-4b38-bc33-4043cd632441/image.png" alt=""></p>
<br>

<h1 id="🕵🏻♀️-원인-파헤치기">🕵🏻‍♀️ 원인 파헤치기</h1>
<hr>
<p>구글링을 해보니 APIkey 인코딩이 주된 원인이었다.</p>
<p>공공데이터포털에 올라와 있는 Q&amp;A와 같이, 우선 APIKey는 URL 인코딩을 해야한다. </p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/d85e8331-b3ec-43df-a7af-29f9ed8cafd8/image.png" alt="">
<a href="http://data.gb.go.kr/page.do?step=258&amp;parm_bod_uid=11&amp;pageOrder=0&amp;srchEnable=1&amp;srchSDate=&amp;srchKeyword=&amp;pageNo=1&amp;pageRef=0&amp;mnu_uid=116&amp;srchBgpUid=-1&amp;pagePrvNxt=1&amp;srchEDate=&amp;srchVoteType=-1&amp;srchColumn=&amp;">해당 Q&amp;A 바로가기</a></p>
<h2 id="url-인코딩-그게-뭔데">URL 인코딩 그게 뭔데?</h2>
<p>URL 인코딩...전송전에 URL 문자를 인코딩해아했다고 한 것 같은데...
잘 기억이 나지 않아 다시 찾아보았다. </p>
<h3 id="퍼센트-인코딩percent-encoding"><strong>퍼센트 인코딩(percent encoding)</strong></h3>
<p>URL 인코딩에서는 퍼센트 인코딩(percent encoding)이라는 방식을 사용한다. 
퍼센트 인코딩은 <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>에 정의되어있다. 
퍼센트 인코딩 URL인코딩뿐만 아니라 URI, URN에도 사용할 수 있으므로 정확히는 &#39;퍼센트 인코딩(Percent encoding)&#39;이라는 용어가 더 적합하다고 한다. </p>
<p>퍼센트 인코딩을 하는 이유는 인터넷에서 주고 받을 수 있는 문자는 ASCII 문자뿐이기 때문이다. 
따라서 ASCII가 아닌 문자는 전송 가능한 형태로 인코딩을 해야한다.</p>
<ul>
<li>퍼센트 인코딩은 URL에서 1) <strong>URL로 사용할 수 없는 문자나</strong> 2) <strong>URL로 사용할 순 있지만 의미가 왜곡될 수 있는 문자</strong>를 <code>%XX</code> (XX는 16진수)로 변환하는 방법이다.<ul>
<li>1) 예로 한글은 ASCII가 아니기 때문에 UTF-8과 같은 방식으로 인코딩해야한다.<ul>
<li>예시 : <code>한글</code> -&gt; <code>%ED%95%9C%EA%B8%80</code></li>
</ul>
</li>
<li>2) ASCII라도 <a href="https://www.rfc-editor.org/rfc/rfc3986#section-2.2">예약된 의미를 가진 문자</a>의 경우, 예약된 의미 말고 그 문자 자체를 전달하고 싶을 때는 이스케이프 처리가 필요하다<ul>
<li>예시 :  <code>/</code> (URL의 각 레벨을 구분) , <code>&amp;</code> (쿼리 파라미터를 구분) , <code>=</code> (쿼리 파라미터 값 지정)</li>
<li><code>A&amp;B</code> 라는 글자를 보내고 싶을 땐 → <code>A%26B</code> ’&amp;’을 이스케이프 처리</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><a href="https://it-eldorado.tistory.com/143">[Web] URL 인코딩/디코딩 (URL Encoding/Decoding)</a> 글을 참고했다. </p>
<p>URL Encoding해주는 사이트도 존재한다.
<a href="https://www.url-encode-decode.com/">URL Encode Decode - URL Percent Encoding and Decoding.</a></p>
<br>

<h2 id="urlcomponent는-자동으로-인코딩해준다는데">URLComponent는 자동으로 인코딩해준다는데?</h2>
<p>그런데 iOS에서 URL을 만들때 사용하는 <code>URLComponent</code> 타입은 자동으로 인코딩을 해준다. 
<a href="https://developer.apple.com/documentation/foundation/urlcomponents">https://developer.apple.com/documentation/foundation/urlcomponents</a>
<img src="https://velog.velcdn.com/images/yeahg_dev/post/61c24c7c-f6dd-4783-9fd3-cd0a9d88d12b/image.png" alt=""></p>
<p>공공데이터 포털에서는 encodedKey, decodedKey 2타입을 제공한다. </p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/2274a90c-0c94-4818-a56d-3fe886952a9e/image.png" alt=""></p>
<p>따라서 decodedKey를 쿼리 파라미터에 넣으면 한번만 인코딩되니 정상적으로 될 것이라 생각했다. </p>
<p>근데 안됨...💩</p>
<h2 id="그럼-왜-안되는-거야">그럼 왜 안되는 거야?</h2>
<p><a href="https://stackoverflow.com/questions/43052657/encode-using-urlcomponents-in-swift">스택오버플로우</a>에서 힌트를 얻을 수 있었다. </p>
<p>이유는 <code>URLComponent</code>가 <code>+</code>는 인코딩하지 않기 때문이다. 따라서 <code>+</code>는 수동으로 인코딩해야한다고한다.  </p>
<p><a href="https://developer.apple.com/documentation/foundation/nsurlcomponents/1407752-queryitems">공식문서</a>에도 아래와 같은 내용이 Note로 제공되고 있다.
<img src="https://velog.velcdn.com/images/yeahg_dev/post/917a9d7f-59ee-4f3c-be39-ba0e03f1ac92/image.png" alt=""></p>
<p>요약하자면</p>
<ul>
<li>RFC 3986은 어떤 문자가 퍼센트 인코딩되어야하는지는 정의하지만, 문자의 해석 방법은 정의하고 있지 않음</li>
<li>따라서 다른 컨벤션과 호환 문제를 겪을 수 있음</li>
<li>그 중 중요한 예로 플러스 사인 (+)이 처리되는 방식인데, RFC 3986은 쿼리에서 유효한 문자로 퍼센트 인코딩 되지 않는다</li>
<li>하지만  <a href="https://www.w3.org/Addressing/URL/4_URI_Recommentations.html">W3C recommendations for URI addressing</a>에 따르면, 쿼리 스트링에서 플러스 사인은 공백의 축약형으로 해석된다. </li>
</ul>
<p>즉, 공공데이터포털에서 제공하는 apiKey는 &#39;W3C recommendations for URI addressing&#39;에 따라 <code>+</code>를 <code>%2B</code>로 변환하는데, <code>URLComponents</code>에서는 그냥 <code>+</code> 그대로 뒀기 때문에 키가 틀리는 오류가 발생한 것이다.</p>
<br>

<p>인코딩과 디코딩키를 대조해보면 알 수 있다!
<img src="https://velog.velcdn.com/images/yeahg_dev/post/de8939e5-c7a9-4bb8-a5e3-b5528ce28115/image.png" alt=""></p>
<br>

<h2 id="해결-방법">해결 방법</h2>
<p>따라서 퍼센트 인코딩된 쿼리에 <code>+</code> 를 <code>%2B</code> 로 직접 변환해 줌으로써 해결했다. </p>
<ul>
<li><code>percentEncodedQuery</code> : 퍼센트 인코딩된 쿼리</li>
</ul>
<pre><code class="language-swift">var url: URL? {
        var urlComponents = URLComponents(string: baseURLString + path)
        urlComponents?.queryItems = query.map {
            URLQueryItem(name: $0.key, value: &quot;\($0.value)&quot;) }
         // replacingOccurrences로 문자 변환
        let encodedQuery = urlComponents?.percentEncodedQuery?.replacingOccurrences(of: &quot;+&quot;, with: &quot;%2B&quot;)
        urlComponents?.percentEncodedQuery = encodedQuery
        return urlComponents?.url
    }</code></pre>
<br>

<h1 id="🚀-교훈">🚀 교훈</h1>
<hr>
<ul>
<li><p>네트워크 규약들은 정말 많다는 것을 다시 한번 느끼게 되었다.
규약들의 호환성 때문에도 문제가 발생할 수 있다는 관점을 배우게 되었다!</p>
</li>
<li><p>api key error로 status 200을 받을 수 도 있다니...
그래서 xml을 JSON encoding 방식으로 파싱하려 하다보니 Parsing Fail로 빠져서 디버깅하는데 오랜 시간이 걸렸다.
서버사이드와 약속을 공유하는 건 매우 중요한 일이겠다..고 간접적으로 느낄 수 있던 사례였다.
  ✅ 개선: CountryCodeAPIService에서 data를 파싱하기전에 xml타입으로 데이터가 왔는지 체크하는 로직을 추가로 구현해야겠다. </p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[String Literal 정복 (feat. 백준 10172 개)]]></title>
            <link>https://velog.io/@yeahg_dev/String-Literal-%EC%A0%95%EB%B3%B5-feat.-%EB%B0%B1%EC%A4%80-10172</link>
            <guid>https://velog.io/@yeahg_dev/String-Literal-%EC%A0%95%EB%B3%B5-feat.-%EB%B0%B1%EC%A4%80-10172</guid>
            <pubDate>Thu, 02 Feb 2023 11:41:47 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요!
오늘은 백준의 <a href="https://www.acmicpc.net/problem/10172">10172번 개</a>, <a href="https://www.acmicpc.net/problem/10171">10171 고양이</a>, <a href="https://www.acmicpc.net/problem/25083">25083 새싹</a>을 풀다가 알게된 String Literal의 다양한 기능들에 대해 알아보겠습니다!🐶🐈🌱</p>
<br>

<h1 id="string-literals"><strong>String Literals</strong></h1>
<hr>
<h3 id="multiline-string-literals"><strong>Multiline String Literals</strong></h3>
<ul>
<li>줄바꿈이 있는 여러 줄의 문장을 <code>\n</code> 사용하지 않고 그대로 쓰고 싶을 때</li>
</ul>
<pre><code class="language-swift">let quotation = &quot;&quot;&quot;
The White Rabbit put on his spectacles.  &quot;Where shall I begin,
please your Majesty?&quot; he asked.

&quot;Begin at the beginning,&quot; the King said gravely, &quot;and go on
till you come to the end; then stop.&quot;
&quot;&quot;&quot;</code></pre>
<br>


<ul>
<li><code>\n</code> 을 사용하면 줄 바꿈이 반영되어 출력됨</li>
</ul>
<pre><code class="language-swift">let softWrappedQuotation = &quot;&quot;&quot;
The White Rabbit put on his spectacles.  &quot;Where shall I begin, \n
please your Majesty?&quot; he asked.

&quot;Begin at the beginning,&quot; the King said gravely, &quot;and go on \n
till you come to the end; then stop.&quot;
&quot;&quot;&quot;</code></pre>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/a3625fb1-e093-450e-8cea-38076ebc3a50/image.png" alt=""></p>
<br>


<ul>
<li>소스코드의 가독성을 위해서 줄을 구분하고 싶으면 <code>\</code> 로 구분하기! (출력값에는 줄바꿈이 반영되지 않음!)</li>
</ul>
<pre><code class="language-swift">let softWrappedQuotation = &quot;&quot;&quot;
The White Rabbit put on his spectacles.  &quot;Where shall I begin, \
please your Majesty?&quot; he asked.

&quot;Begin at the beginning,&quot; the King said gravely, &quot;and go on \
till you come to the end; then stop.&quot;
&quot;&quot;&quot;</code></pre>
<p>출력을 보면 <code>\</code> 는 무시되는 것을 확인할 수 있음</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/6b0a1d34-556f-43be-a556-cec3e88a4ebb/image.png" alt=""></p>
<br>

<ul>
<li>closing quatation marks(”””)의 시작 지점이 문장 시작의 기준이됨</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/255d8ab6-85b4-442f-b890-36afa39dcea3/image.png" alt=""></p>
<pre><code class="language-swift">let lineWithIndentation = &quot;&quot;&quot;
    closing quotation marks에 맞춰서 시작하니까 공백 없음
        이건 공백 있음
    여기부터 시작이니까 이건 공백 없음
    &quot;&quot;&quot;</code></pre>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/c88b18dc-6b63-4a89-965b-5b4709782878/image.png" alt=""></p>
<br>

<h2 id="special-characters-in-string-literals"><strong>Special Characters in String Literals</strong></h2>
<ul>
<li>아래와 같은 특수 문자는 특수 문자 앞에 <code>\</code>를 붙여서 사용</li>
</ul>
<p>(아래와 같은 형태를 escaped speical character라고 부름)</p>
<ul>
<li><code>\0</code>(null character),</li>
<li><code>\\</code>(backslash)</li>
<li><code>\t</code> (horizontal tab)</li>
<li><code>\n</code>(line feed)</li>
<li><code>\r</code>(carriage return)</li>
<li><code>\&quot;</code>(double quotation mark)</li>
<li><code>\&#39;</code>(single quotation mark)</li>
</ul>
<pre><code class="language-swift">let wiseWords = &quot;\&quot;Imagination is more important than knowledge\&quot; - Einstein&quot;</code></pre>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/c141326c-1ee1-41fc-a1dd-15e80e5ff699/image.png" alt=""></p>
<br>


<ul>
<li><code>\u{유니코드 스칼라 값}</code> 으로 문자를 표현할 수도 있음 (유니코드 스칼라 값은 1-8 digit hexadecimal number)</li>
</ul>
<pre><code class="language-swift">let dollarSign = &quot;\u{24}&quot;        // $,  Unicode scalar U+0024
let blackHeart = &quot;\u{2665}&quot;      // ♥,  Unicode scalar U+2665
let sparklingHeart = &quot;\u{1F496}&quot; // 💖, Unicode scalar U+1F496</code></pre>
<br>

<ul>
<li>multiline string literal은 3개의 쌍따옴표(“””)를 쓰므로 쌍따옴표 2개(””)까지는 <code>\</code> 없이 그냥 사용 할 수 있음</li>
<li>따라서 쌍따옴표 3개 사용하려면 아래 처럼 표시 (또는 아래의 Extended String Delimiters사용 )</li>
</ul>
<pre><code class="language-swift">let threeDoubleQuotationMarks = &quot;&quot;&quot;
Escaping the first quotation mark \&quot;&quot;&quot;
Escaping all three quotation marks \&quot;\&quot;\&quot;
&quot;&quot;&quot;</code></pre>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/ca9ded6b-b1e1-48e7-847e-44c724c3de4f/image.png" alt=""></p>
<br>

<h2 id="extended-string-delimiters"><strong>Extended String Delimiters</strong></h2>
<ul>
<li>extended delimiters를 사용하면, escaped special character(<code>\특수문자</code>)를 string literal로 쓸 수 있음</li>
<li>즉, 백 슬래시를 무력화 가능</li>
<li>extended delimiter는 <code>&quot;</code>에 <code>#</code> 을 붙이면 됨</li>
</ul>
<pre><code class="language-swift">let extendedStringDelimiter = #&quot;Line 1\nLine 2&quot;#</code></pre>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/5211b150-2510-4649-8575-527616757249/image.png" alt=""></p>
<br>

<ul>
<li>mulitline string literal도 동일하게 <code>&quot;&quot;&quot;</code>에 <code>#</code>을 붙이면 됨</li>
</ul>
<pre><code class="language-swift">let extenedStringDelimiter = #&quot;&quot;&quot;
extenedStringDelimiter에서는 \는 의미 없다, \n \t \\
&quot;&quot;&quot;#</code></pre>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/d54dd654-adfd-4ad0-a7fa-ed33b352544e/image.png" alt=""></p>
<br>

<ul>
<li>뿐만 아니라 쌍따옴표 3개(“””)도 리터럴도 쓸 수 있음</li>
</ul>
<pre><code class="language-swift">let threeMoreDoubleQuotationMarks = #&quot;&quot;&quot;
    Here are three more double quotes: &quot;&quot;&quot;
&quot;&quot;&quot;#</code></pre>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/5a80e92a-f592-40eb-9f9f-afbdc87cdb5f/image.png" alt=""></p>
<br>

<ul>
<li>아래와 같이 특수문자로 그려진 귀여운 강아지도 그대로 출력 가능</li>
</ul>
<pre><code class="language-swift">let extenedStringDelimiter = #&quot;&quot;&quot;
    |\_/|
    |q p|   /}
    ( 0 )&quot;&quot;&quot;\
    |&quot;^&quot;`    |
    ||_/=\\__|
&quot;&quot;&quot;#
출처: 백준 [https://www.acmicpc.net/problem/10172](https://www.acmicpc.net/problem/10172) (String Literal 공부의 시발 지점)</code></pre>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/a8962df2-ccaa-4da5-b8f6-61bb35680c78/image.png" alt=""></p>
<br>


<ul>
<li>만약, 특수 문자의 기능을 사용하고 싶다면 <code>\</code>뒤에 <code>#</code>을 붙여주면 됨</li>
<li><code>#</code> 개수는 통일해야함</li>
</ul>
<pre><code class="language-swift">let extendedStringDelimiter = #&quot;Line 1\#nLine 2&quot;#</code></pre>
<pre><code class="language-swift">let extendedStringDelimiter = ###&quot;Line1\###nLine2&quot;###</code></pre>
<p>동일한 결과 출력됨</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/133936de-bc7e-400c-b017-63d50784e79a/image.png" alt=""></p>
<br>

<h3 id="reference">Reference</h3>
<p><a href="https://docs.swift.org/swift-book/LanguageGuide/StringsAndCharacters.html">https://docs.swift.org/swift-book/LanguageGuide/StringsAndCharacters.html</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Realm이랑 Concurrency같이 쓰기]]></title>
            <link>https://velog.io/@yeahg_dev/Realm%EC%9D%B4%EB%9E%91-Concurrency%EA%B0%99%EC%9D%B4-%EC%93%B0%EA%B8%B0</link>
            <guid>https://velog.io/@yeahg_dev/Realm%EC%9D%B4%EB%9E%91-Concurrency%EA%B0%99%EC%9D%B4-%EC%93%B0%EA%B8%B0</guid>
            <pubDate>Sat, 07 Jan 2023 11:46:31 GMT</pubDate>
            <description><![CDATA[<p>Realm의 DB transaction은 비동기로 백그라운드에서 처리하고, 그 결과(성공, 실패)에 따라 후속 처리를 해주도록 구현하고 싶었습니다. 백그라운드에서 처리하려는 이유는 Main thread에서 처리하게되면 UI의 응답성을 저하시킬 수 있기 때문입니다. 비동기적으로 결과값을 처리하려면 completion handler를 사용해야하는데, 이는 코드의 가독성을 떨어트리고 흐름을 파악하기 힘들게 만듭니다. 따라서 Swift Concurrency의 <code>async/await</code>을 사용해 리팩토링을 시도해보았습니다. 이 글은 그 과정을 기록한 내용입니다.</p>
<h3 id="들어가기-전-프로젝트-코드-간단-설명">들어가기 전, 프로젝트 코드 간단 설명!</h3>
<ul>
<li>iOS어플들을 검색할 수 있는 앱입니다. </li>
<li>검색을 할 때마다 검색 기록(keyword, 나라 설정, 플랫폼 설정)이     <code>RecentSearchKeywordRepository</code>에 저장되고, <code>RecentSearchKeywordRepository</code>에서 검색 기록을 페치한 다음, 최근 검색어 테이블뷰에 뿌려줍니다. <ul>
<li><code>RecentSearchKeywordRepository</code>에서의 모든 CRUD는 completion handler를 호출합니다. CRUD 작업이 완료된 후 결과를 핸들링해주고, 비동기적으로 CURD를 처리해주기 위해서입니다. </li>
</ul>
</li>
<li>검색 기록 셀을 탭하면 해당 기록을 이용해 API호출을 하고, 검색 결과를 화면에 보여줍니다. 그리고 새로운 검색 기록을 만들어 <code>RecentSearchKeywordRepository</code>에 저장합니다.</li>
<li>검색을 수행하는 API는 <code>AppSearchUsecase</code>에 구현합니다. <code>AppSearchUsecase</code>는 <code>RecentSearchKeywordRepository</code>를 사용합니다. </li>
<li>최근 검색어 테이블뷰를 관리하는 <code>RecentSearchKeywordTableViewModel</code>은 <code>AppSearchUsecase</code>를 사용하여 검색로직을 실행하고, 결과에 따라 뷰를 업데이트합니다. 여기서 결과에 따라 뷰를 업데이트 시키기 위해 completion에 뷰 업데이트 코드를 작성합니다. </li>
</ul>
<br>

<h2 id="🚨-asyncawait-코드를-realm과-쓰면서-발생했던-문제">🚨 async/await 코드를 Realm과 쓰면서 발생했던 문제</h2>
<p>Realm은 스레드에 민감한 객체입니다. 따라서 Realm 인스턴스는 생성된 스레드에서만 유효합니다.</p>
<p>제가 만든 Realm 인스턴스는 아래와 같이 Main thread에서 생성되었습니다. </p>
<pre><code class="language-swift">struct RealmSearchKeywordRepository: SearchKeywordRepository {

    private let realm: Realm! 

    init?() {
        if let searchKeywordRealm = SearckKeywordRealmStore()?.defaultRealm {
            realm = searchKeywordRealm
            print(&quot;📂\(self)&#39;s file URL : \(searchKeywordRealm.configuration.fileURL)&quot;)
        } else {
            return nil
        }
    }

    func create(
        keyword: RecentSearchKeyword,
        completion: @escaping (Result&lt;RecentSearchKeyword, Error&gt;) -&gt; Void)
    {
        let searchKeyword = RecentSearchKeywordRealm(model: keyword)
        do {
            try realm.write {
                realm.add(searchKeyword)
            }
            completion(.success(searchKeyword.toDomain()!))
        } catch {
            print(&quot;failed in \(self): \(error)&quot;)
            completion(.failure(RealmSearchKeywordRepositoryError.realmOperationFailure))
        }
    }
    ...

 }


final class SearckKeywordRealmStore {

    let defaultRealm: Realm!

    init?() {
        do {
            // main thread에서 생성
            defaultRealm = try Realm()
        } catch  {
            print(&quot;Error initiating new realm \(error)&quot;)
            return nil
        }
    }

}
</code></pre>
<p>그런데 Swift Concurrency를 도입하고 Repository를 사용하는 Usecase의 함수를 <code>async</code>로 변경하고, 실행하니 아래와 같은 크래시가 발생을 했습니다. </p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/62b6d7b0-6eda-450f-a4d8-475c2b485360/image.png" alt=""></p>
<p>그 이유는 기존엔 Repository의 <code>create</code>가  메인 스레드에서 실행되어서 문제가 없었지만, Usecase의 메서드가 <code>async</code>로 변경되면서 <code>create</code>는 백그라운드 스레드(Thread7)로 작업이 할당되었기 때문입니다. Realm 인스턴스는 메인 스레드에서 탄생했지만, 백그라운드 스레드에서 접근하려고 하니 잘못된 스레드에서 접근이 되었다는 에러가 발생하는 것입니다.</p>
<p>또한 DB작업을 메인 스레드에서 처리하면 앱의 응답성을 저하시킬 수 있는 문제도 있습니다. </p>
<pre><code class="language-swift"> // RealmSearchKeywordRepository
 func create(
        keyword: RecentSearchKeyword,
        completion: @escaping (Result&lt;RecentSearchKeyword, Error&gt;) -&gt; Void)
    {
        let searchKeyword = RecentSearchKeywordRealm(model: keyword)
        do {
            // thread7에서 접근하니 오류🚨
            try realm.write {
                realm.add(searchKeyword)
            }
            completion(.success(searchKeyword.toDomain()!))
        } catch {
            print(&quot;failed in \(self): \(error)&quot;)
            completion(.failure(RealmSearchKeywordRepositoryError.realmOperationFailure))
        }
    }
</code></pre>
<blockquote>
<p>따라서 Realm을 메인 스레드가 아닌 특정 스레드에서 만들고, 특정 스레드에서만 작업이 처리되도록 컨트롤을 하는 방법을 생각해보았습니다.  </p>
</blockquote>
<h2 id="✅-serialqueue를-사용해-realm을-백그라운드-스레드에서-사용하기">✅ SerialQueue를 사용해 Realm을 백그라운드 스레드에서 사용하기</h2>
<p>Realm을 특정스레드에서만 사용하기 위해 SerialQueue를 생성해주고, 이 SerialQueue에서만 transaction을 수행하도록 변경해보겠습니다. </p>
<pre><code class="language-swift">final class SearckKeywordRealmStore {

    var defaultRealm: Realm!
    let serialQueue: DispatchQueue

    init?() {
        // Realm이 사용할 Serail Dispatch Queue 생성
        serialQueue = DispatchQueue(label: &quot;serial-queue&quot;)
        do {
            try serialQueue.sync {
                defaultRealm = try Realm(configuration: .defaultConfiguration, queue: serialQueue)
            }
        } catch  {
            print(&quot;Error initiating new realm \(error)&quot;)
            return nil
        }
    }

}

struct RealmSearchKeywordRepository: SearchKeywordRepository {

    private let realm: Realm!
    private let realmQueue: DispatchQueue!

    init?() {
        if let searchKeywordRealm = SearckKeywordRealmStore() {
            realm = searchKeywordRealm.defaultRealm
            realmQueue = searchKeywordRealm.serialQueue
            print(&quot;📂\(self)&#39;s file URL : \(realm.configuration.fileURL)&quot;)
        } else {
            return nil
        }
    }

    func create(
        keyword: RecentSearchKeyword,
        completion: @escaping (Result&lt;RecentSearchKeyword, Error&gt;) -&gt; Void)
    {
        // serialQueue에 async하게 작업을 보냄
        realmQueue.async {
            let searchKeyword = RecentSearchKeywordRealm(model: keyword)
            do {
                try realm.write {
                    realm.add(searchKeyword)
                }
                completion(.success(searchKeyword.toDomain()!))
            } catch {
                print(&quot;failed in \(self): \(error)&quot;)
                completion(.failure(RealmSearchKeywordRepositoryError.realmOperationFailure))
            }
        }
    }
    ...
}</code></pre>
<p>작성을 하고 실행을 하니 도 다른 스레드 크래시가 났습니다..</p>
<p>그 이유는 <code>create</code>의 completion에 <code>tableView.reloadData()</code>와 같은 UI조작 코드를 전달했고, 결과적으로 백그라운드 스레드에서 UI 코드가 호출되었기 때문입니다. Repository의 <code>create</code>은 Usecase의 함수에서 호출되고, Usecase의 함수는 ViewModel에서 호출됩니다. </p>
<pre><code class="language-swift">// RecentSearchKeywordTableViewModel

extension RecentSearchKeywordTableViewModel: UITableViewDelegate {

    // 셀이 눌리면 호출
    func tableView(
        _ tableView: UITableView,
        didSelectRowAt indexPath: IndexPath)
    {
        tableView.deselectRow(at: indexPath, animated: true)
        Task {
            // 선택된 셀의 검색 기록 정보를 이용해서 검색 API를 실행 후, 결과를 리턴
            // 내부에서 Repository에 새로운 검색 기록을 생성
            let result = await cellDidSelected(at: indexPath)
            switch result {
            case .success(let appDetail):
                if appDetail.count == 1 {
                    appDetailViewPresenter?.pushAppDetailView(of: appDetail.first!)
                } else {
                    searchAppResultTableViewUpdater?.updateSearchAppResultTableView(
                        with: appDetail)
                }
            case .failure(let alertViewModel):
                searchAppResultTableViewUpdater?.presentAlert(alertViewModel)
            }
            // 새로운 검색 기록을 보여주기 위해, Repository에서 데이터를 페치한 후 테이블뷰를 업데이트
            fetchLatestData {
                tableView.reloadData()
            }
        }
    }

}

// fetchLatestData의 completion을 fetchAllRecentSearchKeyword에 전달
func fetchLatestData(completion: @escaping () -&gt; Void) {
        fetchAllRecentSearchKeyword(completion: completion)
    }


private func fetchAllRecentSearchKeyword(completion: @escaping () -&gt; Void) {
    // Usecase의 모든 검색 기록을 페치해오는 API를 호출한 후, 결과에 따라 컴플리션을 호출
    recentSearchKeywordUsecase.allRecentSearchKeywords
          { [unowned self] result in
                switch result {
                case .success(let fetchedKeywords)
                    // 테이블 뷰 데이터 업데이트
                    self.keywords = fetchedKeywords
                   // 컴플리션 호출
                    completion()
                case .failure(let failure):
                // 검색 기록 불러오기 실패를 알리는 알림을 보여줌
                    print(&quot;Failed to fetch RecentSearchKeyword. error: \(failure)&quot;
             }
         }
    }


</code></pre>
<p>completion이 결국 메서드를 타고 타고 들어가 serialQueue에서 호출이되고 있었던 것입니다😰</p>
<p>여기서 제 코드가 스레드를 너무나 자유롭게 넘나들고 있는데 별도의 스레드 컨트롤이 없기 때문에 굉장히 위험한 코드란 생각이 들었습니다. </p>
<p>또한 completion handler의 전달에 의해 코드의 흐름이 직관적으로 파악되지 않았고, 따라서 스레드의 흐름 또한 파악하기 어렵다고 생각했습니다. </p>
<blockquote>
<p>Swift Concurrency를 사용해 completion handler를 제거하고 코드를 동기적으로 변경하여 문제를 해결하고자 했습니다. </p>
</blockquote>
<h2 id="asyncawait로-장풍코드-없애고-가독성-높이기">async/await로 장풍코드 없애고, 가독성 높이기</h2>
<p>먼저 <code>RealmSearchKeywordRepository</code>의 함수를 <code>withCheckedThrowingContinuation</code>로 감싸주어 <code>async</code>메서드로 변경했습니다. </p>
<pre><code class="language-swift">struct RealmSearchKeywordRepository: SearchKeywordRepository {

    private let realm: Realm!
    private let realmQueue: DispatchQueue!

    init?() {
        if let searchKeywordRealm = SearckKeywordRealmStore() {
            realm = searchKeywordRealm.defaultRealm
            realmQueue = searchKeywordRealm.serialQueue
            print(&quot;📂\(self)&#39;s file URL : \(realm.configuration.fileURL)&quot;)
        } else {
            return nil
        }
    }

    func create(keyword: RecentSearchKeyword) async throws -&gt; RecentSearchKeyword {
        try await withCheckedThrowingContinuation { continuation in
            realmQueue.async {
                let searchKeyword = RecentSearchKeywordRealm(model: keyword)
                do {
                    try realm.write {
                        realm.add(searchKeyword)
                    }
                    continuation.resume(returning: keyword)
                } catch {
                    print(&quot;failed in \(self): \(error)&quot;)
                    continuation.resume(throwing: RealmSearchKeywordRepositoryError.realmOperationFailure)
                }
            }
        }
    }

   ...
 }</code></pre>
<p><code>RealmSearchKeywordRepository</code>를 사용하는 <code>RecentSearchKeywordManagementUsecase</code> async 메서드로 변경하여 결과 값을 리턴하도록 수정했습니다. </p>
<pre><code class="language-swift">struct RecentSearchKeywordManagementUsecase {

    func allRecentSearchKeywords() async throws -&gt; [RecentSearchKeyword] {
        let isActive = isActiveSavingSearchingKeyword()
        if isActive {
            return try await searchKeywordRepository.readAll(sorted: false)
        } else {
            return []
        }
    }

    ...   
}</code></pre>
<p><code>RecentSearchKeywordTableViewModel</code>에서는 async메서드를 <code>Task</code>로 래핑하여 비동기 메서드를 호출합니다.
최신 데이터를 페치한 후, <code>MainActor</code>에서 tableview를 업데이트 해줍니다. </p>
<pre><code class="language-swift">extension RecentSearchKeywordTableViewModel: UITableViewDelegate {

    func tableView(
        _ tableView: UITableView,
        didSelectRowAt indexPath: IndexPath)
    {
        tableView.deselectRow(at: indexPath, animated: true)
        Task {
            let result = await cellDidSelected(at: indexPath)
            switch result {
            case .success(let appDetail):
                if appDetail.count == 1 {
                    appDetailViewPresenter?.pushAppDetailView(of: appDetail.first!)
                } else {
                    searchAppResultTableViewUpdater?.updateSearchAppResultTableView(
                        with: appDetail)
                }
            case .failure(let alertViewModel):
                searchAppResultTableViewUpdater?.presentAlert(alertViewModel)
            }
            await fetchLatestData()
            // main thread에서 작업을 처리시키기 위해 MainActor에서 실행
            await MainActor.run {
                tableView.reloadData()
            }
        }

    }

    func fetchLatestData() async {
        keywords = await fetchAllRecentSearchKeyword()
        cellModels = keywords.compactMap{ RecentSearchKeywordCellModel(keyword: $0) }
    }


    private func fetchAllRecentSearchKeyword() async -&gt; [RecentSearchKeyword] {
        do {
            return try await recentSearchKeywordUsecase.allRecentSearchKeywords()
        } catch {
            print(&quot;Failed to fetch RecentSearchKeyword. error: \(error)&quot;)
            return []
        }
    }
}</code></pre>
<p>Concurrency로 리팩터링을 한 후 아래와 같은 장점을 느낄 수 있었습니다. </p>
<ul>
<li><code>async</code>를 통해 비동기 작업을 하는 메서드인지 명시적으로 드러냄</li>
<li>completion handler 대신 결과 값을 리턴함으로서, 실행 스레드를 분리시킴</li>
<li>실행 흐름을 직관적으로 파악 가능</li>
</ul>
<br>


<blockquote>
<p>코드에 대한 피드백과 생각 나눔은 환영입니다.🫶</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[CoreAnimation으로 원을 따라 움직이는 화살표 만들기]]></title>
            <link>https://velog.io/@yeahg_dev/CoreAnimation%EC%9C%BC%EB%A1%9C-%EC%9B%90%EC%9D%84-%EB%94%B0%EB%9D%BC-%EC%9B%80%EC%A7%81%EC%9D%B4%EB%8A%94-%ED%99%94%EC%82%B4%ED%91%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@yeahg_dev/CoreAnimation%EC%9C%BC%EB%A1%9C-%EC%9B%90%EC%9D%84-%EB%94%B0%EB%9D%BC-%EC%9B%80%EC%A7%81%EC%9D%B4%EB%8A%94-%ED%99%94%EC%82%B4%ED%91%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Wed, 28 Dec 2022 07:34:02 GMT</pubDate>
            <description><![CDATA[<p>원의 둘레를 따라 움직이며 셀렉된 버튼을 가리키는 화살표를 구현한 과정을 정리해보겠습니다. </p>
<br>

<h2 id="💡-구현-아이디어">💡 구현 아이디어</h2>
<ol>
<li>애니메이션의 시작, 종료 지점의 각을 받아서 Arc(호) 모양의 <code>UIBezierPath</code>를 만듭니다.</li>
<li>해당 path를 <code>CAKeyframeAnimation(keyPath: &quot;position&quot;)</code> 의 <code>path</code> 로 지정합니다.</li>
<li>애니메이션을 시작합니다. </li>
</ol>
<br>

<h2 id="1-cashapelayer-subclass-정의">1. CAShapelayer subclass 정의</h2>
<pre><code class="language-swift">final class AnimatableArrowLayer: CAShapeLayer {

    private let defaultStartAngle = 1.5 * .pi // 기본 위치인 iphone의 angle
    private let centerPoint: CGPoint // 경로가 될 원의 중심
    private let pathRadius: CGFloat // 경로가 될 원의 반지름
    private var startAngle: CGFloat! // 애니메이션 시작점을 저장

    override init(layer: Any) {
        centerPoint = (layer as! AnimatableArrowLayer).centerPoint
        pathRadius = (layer as! AnimatableArrowLayer).pathRadius
        super.init(layer: layer)
    }

    init(center: CGPoint, radius: CGFloat) {
        centerPoint = center
        pathRadius = radius
        super.init()
        let arrowImage = UIImage(named: &quot;arrow&quot;)!
        contents = arrowImage.cgImage
        bounds = CGRect(
            x: 0.0,
            y: 0.0,
            width: arrowImage.size.width,
            height: arrowImage.size.height)
    }

    required init?(coder: NSCoder) {
        fatalError(&quot;init(coder:) has not been implemented&quot;)
    }
    ...
 }</code></pre>
<p>처음엔 커스텀 이니셜라이저안에서 <code>super.init()</code> 만 호출하는 형태의 이니셜라이저만 구현했는데, 그렇게 하면 아래와 같은 크래시가 발생했습니다.
<img src="https://velog.velcdn.com/images/yeahg_dev/post/97ba160b-fd65-4e0b-b724-69869df1e02b/image.png" alt=""></p>
<blockquote>
<p>Fatal error: Use of unimplemented initializer &#39;init(layer:)&#39; for class &#39;app_show_room.AnimatableArrowLayer&#39;</p>
</blockquote>
<p>구현하지 않은 <code>init(layer:)</code> 를 사용했기 때문이라고합니다. </p>
<p><a href="https://developer.apple.com/forums/thread/694566">스택 오버플로우</a>에 따르면 <code>init(layer:)</code> 는 Presentation layer에서 사용되기 위한 레이어의 복사본들을 생성하는데 사용됩니다. 서브클래스들은 이 이니셜라이저를 오버라이드하여 인스턴스 변수들을 복사해서 presentation layer에게 전달할 수 있습니다. 그래서 CoreAnimation이 레이어의 복사본들이 필요하다고 판단이 되면 이 이니셜라이저를 호출하게됩니다. (예를 들어 레이어의 strokeColor를 바꾸는 경우)</p>
<p>그래서 <strong>서브클래스에 커스텀 변수가 있다면 해당 변수의 값을 이 이니셜라이저의 프로퍼티에 전달해주어야합니다.</strong> 여기서 매개변수로 전달되는 레이어는 old layer이기 때문에 서브클래스로 타입캐스팅을 한 후,<code>super.init(layer:)</code>을 호출하기전에 전달해야합니다.</p>
<br>

<h2 id="2-애니메이션-만들기">2. 애니메이션 만들기</h2>
<p>화살표의 position을 애니메이션 줄 것이기 때문에 <code>CAKeyframeAnimation(keyPath: &quot;position&quot;)</code> 을 만듭니다.</p>
<pre><code class="language-swift">func animate(to end: CGFloat) {
        let positionAnimation = CAKeyframeAnimation(keyPath: &quot;position&quot;)</code></pre>
<p><code>startAngle</code>과 <code>endAngle</code>로 정의된 Arc(호) Path를 만듭니다. </p>
<pre><code class="language-swift">let arcPath = UIBezierPath(
                      arcCenter: centerPoint,
                    radius: pathRadius,
                    startAngle: startAngle,
                    endAngle: end,
                       clockwise: clockwise)</code></pre>
<p>애니메이션 방향은 <code>startAngle</code>과 <code>endAngle</code>의 크기를 비교하여 정합니다. 
그리고 keyframeAnimation의 <code>path</code>로 지정합니다. 그럼 화살표의 포지션이 path에 따라 변화하며 움직이게됩니다.
<code>duration</code> (지속시간)과 <code>timingFunction</code>을 입맛대로 설정합니다.</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/1b14cf86-dad8-46bd-8881-e5b35ffef905/image.png" alt=""></p>
<pre><code class="language-swift">let clockwise = (startAngle &lt; end) ? true : false
positionAnimation.path = arcPath.cgPath
positionAnimation.timingFunction = .init(name:CAMediaTimingFunctionName.easeInEaseOut)
positionAnimation.duration = 1</code></pre>
<p>여기까지하고 애니메이션을 실행해주면 아래와 같이 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/3120aa30-0bdb-445e-8176-98fe925a8587/image.gif" alt=""></p>
<ul>
<li>화살표의 방향이 고정된 채로 움직이고</li>
<li>애니메이션이 끝난 후 제자리로 돌아오게됩니다.</li>
</ul>
<p>이제 위 2가지 버그를 고쳐볼게요!</p>
<h2 id="3-화살표를-path에-따라-rotation하기">3. 화살표를 path에 따라 rotation하기</h2>
<p>화살표가 path를 따라 움직인 만큼 rotation되도록 하려면 <code>rotationMode</code> 를 설정합니다. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/311cb7c3-a70b-4f0b-9bab-473cdf9df000/image.png" alt="">
path를 따라 움질일 때 path의 탄젠트에 맞춰서 객체를 돌릴지 결정하는 프로퍼티입니다. </p>
<pre><code class="language-swift"> if clockwise {
        positionAnimation.rotationMode = .rotateAuto
  } else {
       positionAnimation.rotationMode = .rotateAutoReverse
  }</code></pre>
<p>시계 반대 방향으로 움직일 땐 <code>.rotateAutoReverse</code>를 하지 않으면 화살표가 원의 안쪽을 향하더라구요.. path의 방향에 따라 탄젠트 계산이 달리되는 건가요?? </p>
<br>

<h2 id="4-애니메이션-후-상태-유지하기">4. 애니메이션 후 상태 유지하기</h2>
<p>애니메이션이 진행될 때는 presentation layer의 값 들로 layer가 셋팅되다가, 종료되면 model layer의 값으로 reset된다고 합니다. 이 때 model layer의 값을 presentation layer의 값으로 변경 시켜주는 프로퍼티가 <code>fillMode</code> 입니다. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/15c398c4-db09-414e-b0db-a8e3ccf3d129/image.png" alt="">
여기서 presentaion layer와 model layer는 아래 개념에서 등장하는 내용인데요.</p>
<p>CALayer는 3가지 종류의 Layer object가 존재합니다.</p>
<ul>
<li>layer의 프로퍼티를 관리하는 layer tree(model layer tree), 보통 Property를 변경하면 layer tree의 value가 변경됩니다.</li>
<li>현재 스크린에 보여지는 상태의 값(in-flight value)을 관리하는 presentation tree</li>
<li>실제 애니메이션을 수행하는 render tree, private하기 때문에 접근 불가.</li>
</ul>
<p><a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/CoreAnimationBasics/CoreAnimationBasics.html#//apple_ref/doc/uid/TP40004514-CH2-SW3">공식문서 원문</a></p>
<p>현재 model layer의 position 프로퍼티는 startAngle로 설정되어있기 때문에 처음 시작점으로 보여지게 되는 것이고, presentation layer에서 마지막으로 보여진 angle로 현재 position을 설정합니다. <code>.forwards</code> 는 애니메이션이 완료된 후 최종 상태로 남게됩니다. </p>
<pre><code class="language-swift">positionAnimation.fillMode = .forwards</code></pre>
<p><code>isRemovedOnCompletion</code>은 디폴트로 <code>true</code>이기 때문에 애니메이션이 종료된 후 레이어의 애니메이션에서 제거됩니다. 따라서<code>false</code>로 설정해줍니다. </p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/f8e87ab2-3b47-408f-87e0-19b4ac0dc9e9/image.png" alt=""></p>
<pre><code class="language-swift">positionAnimation.isRemovedOnCompletion = false</code></pre>
<p>그리고 기존에 설정된 기기를 반영한 화살표 위치를 잡아주기 위한 애니메이션인 <code>setPosition(_ start:)</code>도 위 방법과 같이 구현해줍니다. <code>setPosition(_ start:)</code>은 이 클래스가 생성되자마자 호출해줍니다. </p>
<pre><code class="language-swift">func setPosition(_ start: CGFloat) {
        self.startAngle = start
        let initialPositionAnimation = CAKeyframeAnimation(keyPath: &quot;position&quot;)
        let clockwise = (defaultStartAngle &lt; start) ? true : false
        let postionPath = UIBezierPath(
            arcCenter: centerPoint,
            radius: pathRadius,
            startAngle: defaultStartAngle,
            endAngle: start,
            clockwise: clockwise)
        initialPositionAnimation.path = postionPath.cgPath
        // 화면이 나타나자마자 실행되므로 원래 그 위치인 것 처럼 보이기위해 아주 짧은 시간동안 진행
        initialPositionAnimation.duration = 0.01
        initialPositionAnimation.isRemovedOnCompletion = false
        initialPositionAnimation.fillMode = .forwards
        if clockwise {
            initialPositionAnimation.rotationMode = .rotateAuto
        } else {
            initialPositionAnimation.rotationMode = .rotateAutoReverse
        }

        add(initialPositionAnimation, forKey: &quot;initialPostion&quot;)
    }</code></pre>
<br>

<p>여기까지 하면 아래와 같은 애니메이션이 완성됩니다. 
그런데 애니메이션 종료 후 위치는 남지만, rotation의 상태가 이상하게 동작합니다. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/6222bfe1-1333-41c6-aef6-4665eabd4217/image.gif" alt=""></p>
<p>애니메이션이 시작되기 전 기존 애니메이션(초기 위치를 설정해주는 애니메이션)을 삭제해주었더니 정상적으로 작동합니다. 
애니메이션을 계속해서 추가해서 <code>.fillMode</code>가 오작동한 것으로 추측은 되는데...확실하겐 모르겠네요🥲</p>
<pre><code class="language-swift">func animate(to end: CGFloat) {
        // 직전에 실행되었던 애니메이션을 제거 
        self.removeAnimation(forKey: &quot;initialPostion&quot;)

        let positionAnimation = CAKeyframeAnimation(keyPath: &quot;position&quot;)
        let clockwise = (startAngle &lt; end) ? true : false
        let arcPath = UIBezierPath(
            arcCenter: centerPoint,
            radius: pathRadius,
            startAngle: startAngle,
            endAngle: end,
            clockwise: clockwise)
        positionAnimation.path = arcPath.cgPath
        positionAnimation.timingFunction = .init(name: CAMediaTimingFunctionName.easeInEaseOut)
        positionAnimation.duration = 1
        positionAnimation.fillMode = .forwards
        positionAnimation.isRemovedOnCompletion = false
        if clockwise {
            positionAnimation.rotationMode = .rotateAuto
        } else {
            positionAnimation.rotationMode = .rotateAutoReverse
        }
        add(positionAnimation, forKey: &quot;position&quot;)

        self.startAngle = end
    }</code></pre>
<p>우여곡절 끝에 완성!!!✌️
<img src="https://velog.velcdn.com/images/yeahg_dev/post/51e0e0fb-ff71-451e-9243-c517b606097c/image.gif" alt=""></p>
<h2 id="전체코드">전체코드</h2>
<pre><code class="language-swift">//
//  AnimatableArrowLayer.swift
//  app-show-room
//
//  Created by Moon Yeji on 2022/12/27.
//

import UIKit

final class AnimatableArrowLayer: CAShapeLayer {

    private let defaultStartAngle = 1.5 * .pi
    private let centerPoint: CGPoint
    private let pathRadius: CGFloat
    private var startAngle: CGFloat!

    override init(layer: Any) {
        centerPoint = (layer as! AnimatableArrowLayer).centerPoint
        pathRadius = (layer as! AnimatableArrowLayer).pathRadius
        super.init(layer: layer)
    }

    init(center: CGPoint, radius: CGFloat) {
        centerPoint = center
        pathRadius = radius
        super.init()
        let arrowImage = UIImage(named: &quot;arrow&quot;)!
        contents = arrowImage.cgImage
        bounds = CGRect(
            x: 0.0,
            y: 0.0,
            width: arrowImage.size.width,
            height: arrowImage.size.height)
    }

    required init?(coder: NSCoder) {
        fatalError(&quot;init(coder:) has not been implemented&quot;)
    }

    func setPosition(_ start: CGFloat) {
        self.startAngle = start
        let initialPositionAnimation = CAKeyframeAnimation(keyPath: &quot;position&quot;)
        let clockwise = (defaultStartAngle &lt; start) ? true : false
        let postionPath = UIBezierPath(
            arcCenter: centerPoint,
            radius: pathRadius,
            startAngle: defaultStartAngle,
            endAngle: start,
            clockwise: clockwise)
        initialPositionAnimation.path = postionPath.cgPath
        initialPositionAnimation.duration = 0.01
        initialPositionAnimation.isRemovedOnCompletion = false
        initialPositionAnimation.fillMode = .forwards
        if clockwise {
            initialPositionAnimation.rotationMode = .rotateAuto
        } else {
            initialPositionAnimation.rotationMode = .rotateAutoReverse
        }

        add(initialPositionAnimation, forKey: &quot;initialPostion&quot;)
    }

    func animate(to end: CGFloat) {
        self.removeAnimation(forKey: &quot;initialPostion&quot;)

        let positionAnimation = CAKeyframeAnimation(keyPath: &quot;position&quot;)
        let clockwise = (startAngle &lt; end) ? true : false
        let arcPath = UIBezierPath(
            arcCenter: centerPoint,
            radius: pathRadius,
            startAngle: startAngle,
            endAngle: end,
            clockwise: clockwise)
        positionAnimation.path = arcPath.cgPath
        positionAnimation.timingFunction = .init(name: CAMediaTimingFunctionName.easeInEaseOut)
        positionAnimation.duration = 0.2
        positionAnimation.fillMode = .forwards
        positionAnimation.isRemovedOnCompletion = false
        if clockwise {
            positionAnimation.rotationMode = .rotateAuto
        } else {
            positionAnimation.rotationMode = .rotateAutoReverse
        }
        add(positionAnimation, forKey: &quot;moveAroundArc&quot;)

        self.startAngle = end
    }

}</code></pre>
<p><a href="https://github.com/yeahg-dev/Chajabwa">🗂 프로젝트 깃헙</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Segmented Controls]]></title>
            <link>https://velog.io/@yeahg_dev/Segmented-Controls</link>
            <guid>https://velog.io/@yeahg_dev/Segmented-Controls</guid>
            <pubDate>Sun, 25 Dec 2022 05:35:55 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>원문 
<a href="https://developer.apple.com/design/human-interface-guidelines/components/selection-and-input/segmented-controls">HIG-Segmented Contorls</a></p>
</blockquote>
<h1 id="segmented-controls">Segmented Controls</h1>
<p>Segmented Controls은 두개 이상의 세그먼트가 직선으로 나열된 형태로, 각 세그먼트는 버튼의 역할을 합니다. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/d78878bb-146a-4017-826d-73f128bc1d8b/image.png" alt=""></p>
<ul>
<li>세그먼트는 위 이미지에서 보이는 하나의 칸을 의미</li>
<li>각 세그먼트는 보통 넓이가 균일</li>
<li>버튼과 같이 text 또는 image를 포함 가능</li>
<li>control 위나 아래에 text Label을 표시 가능</li>
</ul>
<br>

<h1 id="best-practices">Best practices</h1>
<ul>
<li>단일 선택 또는 복수 선택 가능</li>
</ul>
<table>
<thead>
<tr>
<th>단일 선택</th>
<th>복수 선택</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/yeahg_dev/post/09e31cca-d9d9-4a69-b25a-d03d4120a9bf/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/yeahg_dev/post/fb98f8fc-8008-4920-a777-e5a1c0e0d50f/image.png" alt=""></td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
</tbody></table>
<h3 id="객체-상태-뷰에-밀접하게-영향을-주는-선택을-제공하기-위해서-사용하기">객체, 상태, 뷰에 밀접하게 영향을 주는 선택을 제공하기 위해서 사용하기.</h3>
<p>예로 툴바에서 뷰를 스위칭하기위해 segmented control을 사용할 수 있습니다. (컬리가 마켓컬리와 뷰티컬리를 전환할 때와 같이) 세그먼트 컨트롤로 컨텐츠의 추가, 삭제, 편집과 같은 행동을 제공하는건 피하세요!</p>
<h3 id="너무-많은-세그먼트를-사용하지-마세요">너무 많은 세그먼트를 사용하지 마세요.</h3>
<p>너무 많은 세그먼트는 파악하기 어렵고, 탐색하기에 시간이 많이 걸리기 때문입니다. 아이패드와 같은 넓은 인터페이스에서는 최대 5-7개, iphone에서는 최대 5개까지의 세그먼트를 제공하도록 하세요</p>
<h3 id="일반적으로-세그먼트-사이즈를-일관적으로-만드세요">일반적으로 세그먼트 사이즈를 일관적으로 만드세요.</h3>
<p>모든 세그먼트 컨트롤들이 같은 넓이를 가졌을 때 균형잡혀 보입니다. 아이콘과 제목 너비도 가능한 한 일정하게 유지하는 것이 좋습니다.</p>
<br>

<h1 id="content">Content</h1>
<p>이번엔 세그먼트에 표시하는 컨텐츠의 유의사항에 대해 알아볼게요</p>
<h3 id="하나의-세그먼트에는-텍스트-또는-이미지-중-하나만-사용하기를-권장합니다-섞어-쓰지-않기">하나의 세그먼트에는 텍스트 또는 이미지 중 하나만 사용하기를 권장합니다. 섞어 쓰지 않기.</h3>
<p>각 세그먼트는 textLabel 또는 image를 사용할 수 있지만, 그 둘을 섞어쓰면 일관성 없고, 혼란스러운 인터페이스로 보여지게 됩니다. </p>
<h3 id="가능한-한-각-세그먼트-내에서의-컨텐츠도-비슷한-사이즈를-유지하세요">가능한 한, 각 세그먼트 내에서의 컨텐츠도 비슷한 사이즈를 유지하세요.</h3>
<p>보통 모든 세그먼트는 동일한 너비를 가지기 때문에, 만약 컨텐츠들이 채워지는 비율이 다르다면 보기 좋지 않을 수 있습니다.</p>
<h3 id="세그먼트-라벨에는-명사나-명사구를-사용하세요">세그먼트 라벨에는 명사나 명사구를 사용하세요.</h3>
<p>각 세그먼트를 설명하는 제목 형식(대소문자 구분하는)의 텍스트를 사용하세요. 텍스트 라벨에는 설명하는 텍스트를 작성하지 않습니다. </p>
<h1 id="platform-별-유의사항">Platform 별 유의사항</h1>
<h2 id="ios">iOS</h2>
<ul>
<li>NavigationBar에서 세그먼트 컨트롤을 사용할 땐 다른 컨트롤이나 제목과 함께 쓰는건 피하기 </li>
<li>Toolbar에서 사용하는 건 피하기. Toolbar Item이 현재 스크린에서 동작하고 있기 때문에, 사람들이 컨텍스트를 변경하게하는 건 허용하지 않기 때문이라고 함</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[UIView 🆚 CALayer 차이점]]></title>
            <link>https://velog.io/@yeahg_dev/UIView-CALayer-%EC%B0%A8%EC%9D%B4%EC%A0%90</link>
            <guid>https://velog.io/@yeahg_dev/UIView-CALayer-%EC%B0%A8%EC%9D%B4%EC%A0%90</guid>
            <pubDate>Tue, 08 Nov 2022 10:01:08 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요~ 릴리이입니다😃</p>
<p>오늘은 비슷한 역할을 하는 것 같지만, 다른 <code>UIView</code>와 <code>CALayer</code>의 특징들과 차이점에 대해 공부해보겠습니다.</p>
<br>

<h2 id="calayer-어디서-봤더라"><code>CALayer</code> 어디서 봤더라?</h2>
<p><code>UIView</code>의 그림자(shadow)나 경계선(border)을 설정할 때, <code>layer</code>프로퍼티를 통해 설정했던 경험 한 번씩은 있으실 것 같습니다. </p>
<pre><code class="language-swift"> var cellContainerView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = UIColor.pink
        // 그림자
        view.layer.shadowColor = UIColor.grey.cgColor
        view.layer.shadowOpacity = Float(1)
        view.layer.shadowOffset = CGSize(width: 0, height: 1)
        view.layer.shadowRadius = CGFloat(0.5)
        // 코너 직경
        view.layer.cornerRadius = CGFloat(6)
        return view
    }()</code></pre>
<p>뷰의 그림자와 테두리를 그리는데 <code>layer</code>를 거쳐야한다는 걸 보니, 
보여지는 부분과 관련된 객체인 것 같고, 포토샵을 할 때 사용하는 layer와 비슷한 역할을 할 것 같습니다🧐</p>
<br>

<h2 id="uiview-calayer의-근본"><code>UIView</code>, <code>CALayer</code>의 근본</h2>
<p><code>UIView</code>의 <code>layer</code> 프로퍼티는 <code>CALayer</code>라는 타입입니다.</p>
<pre><code class="language-swift">var layer: CALayer { get }</code></pre>
<p><code>CALayer</code>는  CoreAnimation 프레임워크에 속한 객체입니다. 
반면, <code>UIView</code>는 UIKit 프레임워크 소속이지요.</p>
<p>이 둘은 사실 같은 뿌리에서 파생된 객체들이라고 볼 수 있습니다. </p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/75090723-6f70-4b23-9de6-b6fa496c9147/image.png" alt=""></p>
<p>OpenGL은 Graphic Hardware를 통해 그래픽 작업을 할 수 있는 API로, 그래픽 하드웨어에 빠르게 접근할 수 있습니다. iOS8이후로는 OpenGL대신 Metal을 사용한다. 하지만 저수준이기 때문에 단순한 작업에도 많은 코드를 필요로한다는 단점이 있지요.</p>
<p>그래서 등장한 것이 CoreGraphics라는 프레임워크이고, 
CoreGraphics 또한 저수준이기 때문에 더 쉽게 사용할 수 있도록 CoreAnimation이라는 프레임워크가 만들어졌다고 합니다.</p>
<p>CoreAnimation에 그래픽을 구현하기 위한 고급 기능들이 포함되어 있었고, 모바일 어플리케이션을 만들기 쉽도록 간단하게 탄생한 프레임워크가 바로, UIKit인 것입니다. </p>
<br>

<p>같은 뿌리에서 컨텐츠를 보여주기위해 탄생했기 때문에 많은 공통점을 지니고 있었던 것이었군요!</p>
<br>


<h2 id="주요-역할과-비슷한-점">주요 역할과 비슷한 점</h2>
<p>애플 공식문서에서 소개하는 API들을 살펴보면, 제공하는 기능들이 매우 비슷한 걸 확인할 수 있었습니다. </p>
<h3 id="uiview"><a href="https://developer.apple.com/documentation/uikit/uiview">UIView</a></h3>
<ul>
<li>사각형 영역(CGRect)안에서 컨텐츠를 관리</li>
<li>1️⃣ <strong>Drawing and animation</strong><ul>
<li>컨텐츠를 UIKit이나 Core Graphics을 사용해서 사각 영역안에 그린다.</li>
<li>뷰의 프로퍼티를 새로운 값을 줌으로써 애니메이션 효과를 줄 수 있다.</li>
</ul>
</li>
<li>2️⃣ <strong>Layout and subview management</strong><ul>
<li>View는 0개 이상의 subview를 담을 수 있다.</li>
<li>View는 자신의 subview의 사이즈와 위치를 조정할 수 있다.</li>
<li>오토레이아웃을 사용해서 뷰 계층구조 변화에 대응하기 위해  뷰의 resizing, repositioning 규칙을 정의할 수 있다.</li>
</ul>
</li>
<li>3️⃣ <strong>Event Handling</strong><ul>
<li><code>UIResponder</code>의 서브클래스이므로 터치나 다른 타입의 이벤트에 대해 응답할 수 있다.</li>
<li>gesture recognizer를 설치 할 수 있다.</li>
</ul>
</li>
<li>view에 변경 사항이 생기면 <code>draw()</code>를 호출하는데, 이 작업은 CPU의 main thread에서 일어나므로 <strong>비용이 비싸다</strong></li>
</ul>
<h3 id="calayer"><a href="https://developer.apple.com/documentation/quartzcore/calayer">CALayer</a></h3>
<ul>
<li>이미지 기반의 컨텐츠를 관리</li>
<li>컨텐츠를 캡처한 비트맵을 backingstore에 캐싱한다</li>
<li>캐시된 비트맵과 관련 상태 정보를 그래픽 하드웨어에 전달한다. 그래픽 하드웨어는 새로운 상태 정보로 비트맵을 렌더링한다. 하드웨어이므로 소프트웨어보다 <strong>훨씬 빠르다</strong></li>
<li>레이어 자체에 시각적 프로퍼티(backgroudColor, border, shadow...) 설정 할 수 있다</li>
<li>좌표적 프로퍼티(position, size, transform)설정 할 수 있다.</li>
<li>프로퍼티의 값을 변경해 애니메이션을 구현 할 수도 있다</li>
</ul>
<p>큰 역할들에 따라 API를 한 번 비교해보았습니다. 
<br></p>
<h3 id="보여지는-컨텐츠의-특성-설정">보여지는 컨텐츠의 특성 설정</h3>
<table>
<thead>
<tr>
<th>UIView</th>
<th>CALayer</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/yeahg_dev/post/fece8f61-7714-4092-9d10-17047dc599fb/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/yeahg_dev/post/6aa9e969-4683-4ca5-afa2-4e5004058a62/image.png" alt=""></td>
</tr>
<tr>
<td><code>UIView</code>와 <code>CALayer</code> 모두 <code>backgroundColor</code>나 <code>opaque</code> 속성과 같이 시각적 특성, 좌표계 특성(<code>frame</code>, <code>bounds</code>...)을 설정할 수 있습니다.</td>
<td></td>
</tr>
</tbody></table>
<p>그런데 <code>CALayer</code>가 <code>border</code>,<code>shadow</code>, <code>anitaliasing</code> 같이 더 많은 시각적 속성들을 조작할 수 있네요.</p>
<h3 id="계층구조">계층구조</h3>
<p><code>UIView</code>와 <code>CALayer</code>는 공통적으로 계층구조를 형성합니다. </p>
<table>
<thead>
<tr>
<th>UIView</th>
<th>CALayer</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/yeahg_dev/post/4b947c77-21ed-40c0-b15c-119309aa7d13/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/yeahg_dev/post/ee5fd895-03a0-4f5b-a5e0-717f6ad4c349/image.png" alt=""></td>
</tr>
</tbody></table>
<p><code>UIView</code>는 <code>superview</code>와 <code>subview</code>,
<code>CALayer</code>는 <code>superlayer</code>와 <code>sublayer</code>로 계층구조를 정의하죠.</p>
<p>그리고 이런 계층구조를 관리하는 API들이 제공되고 있습니다. </p>
<br>

<h3 id="디스플레이">디스플레이</h3>
<table>
<thead>
<tr>
<th>UIView</th>
<th>CALayer</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/yeahg_dev/post/d03adde3-a3f0-44ef-be5d-b8a68beb6b1e/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/yeahg_dev/post/ee050682-eacc-45e4-992d-b347bf2e5172/image.png" alt=""></td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
</tbody></table>
<p>공통적으로 디스플레이를 업데이트하는 메서드가 제공됩니다.</p>
<br>

<h3 id="레이아웃">레이아웃</h3>
<p>위치와 사이즈를 결정하는 레이아웃 관련 메서드들도 제공됩니다. 
두 객체 모두 <code>constraints</code>(제약)을 사용해 상위 객체와의 관계를 기반으로 레이아웃을 결정할 수 있습니다. 
(UIKit -&gt; NSLayoutConstraints / CALayer -&gt; CAConstraints)
하지만, CAConstraints는 iOS를 지원하지 않습니다.</p>
<table>
<thead>
<tr>
<th>UIView</th>
<th>CALayer</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/yeahg_dev/post/1ad3be75-cd8a-4597-bcb4-a2c4ac5887ef/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/yeahg_dev/post/119e1c4f-09b1-4c32-ba71-b080184f9a97/image.png" alt=""></td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
</tbody></table>
<p><code>UIView</code>에서 autoLayout과 관련된 훨씬 많은 API들이 제공되고 있는 것을 확인할 수 있었습니다.
뿐만 아니라 <code>CALayer</code>에는 없는 <code>safeArea</code>, <code>margin</code>과 같은 레이아웃 가이드를 추가적으로 제공하고 있습니다. </p>
<br>

<h3 id="애니메이션">애니메이션</h3>
<p>공통적으로 animatable한 property(<code>frame</code>,<code>bounds</code>, <code>alpha</code>...)의 값을 변경시켜서 애니메이션을 구현할 수 있습니다. </p>
<p><code>UIView</code>의 애니메이션 또한 실제로는 CoreAnimation에 의해 작동합니다.
<code>UIView</code>에서 제공하는 애니메이션 관련 API는 CoreAnimation의 wrapper라고 볼 수 있습니다. </p>
<table>
<thead>
<tr>
<th>UIView</th>
<th>CALayer</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/yeahg_dev/post/6ee67eba-3bb0-48b9-afcb-1f0097a90211/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/yeahg_dev/post/fbf6e7e9-c806-4c61-9b1d-bf7718c3dc2b/image.png" alt=""></td>
</tr>
</tbody></table>
<br>

<h3 id="다양한-종류의-calayer의-서브클래스">다양한 종류의 CALayer의 서브클래스</h3>
<p><code>UIView</code>가 <code>UITextView</code>, <code>UIStackView</code>, <code>UIScrollView</code> 등 다양한 서브클래스를 가지는 것 처럼, <code>CALayer</code>에도 다양한 종류의 서브클래스가 있습니다.</p>
<ul>
<li><a href="https://developer.apple.com/documentation/quartzcore/catextlayer"><code>CATextLayer</code></a>
string 또는 atttributed string의 간단한 텍스트 레이아웃 및 렌더링을 제공하는 레이어</li>
<li><a href="https://developer.apple.com/documentation/quartzcore/cashapelayer"><code>CAShapeLayer</code></a>
좌표 공간에서 cubic Bezier spline을 그리는 레이어</li>
<li><a href="https://developer.apple.com/documentation/quartzcore/cagradientlayer"><code>CAGradientLayer</code></a>
배경색윝에 그라디언트로 모양을 채우는 레이어</li>
<li><a href="https://developer.apple.com/documentation/quartzcore/caemitterlayer"><code>CAEmitterLayer</code></a>
작은 조각을 내뿜고, 움직이고, 렌더링하는 레이어</li>
<li><a href="https://developer.apple.com/documentation/quartzcore/cascrolllayer"><code>CAScrollLayer</code></a>
bounds보다 큰 크기의 스크롤 할 수 있는 컨텐츠를 보여주는 레이어</li>
<li><a href="https://developer.apple.com/documentation/quartzcore/catiledlayer"><code>CATiledLayer</code></a>
컨텐츠의 타일을 비동기식으로 제공하는 방법을 제공, 여러 수준의 세부 정보를 캐싱하는 레이어</li>
<li><a href="https://developer.apple.com/documentation/quartzcore/catransformlayer"><code>CATransformLayer</code></a>
진짜 3D 레이어 계층을 만드는데 사용되는 레이어</li>
<li><a href="https://developer.apple.com/documentation/quartzcore/careplicatorlayer"><code>CAReplicatorLayer</code></a>
기하, 시간, 색상의 변화를 가진 sublayer의 복사본을 다량 생산하는 레이어</li>
<li><a href="https://developer.apple.com/documentation/quartzcore/cametallayer"><code>CAMetalLayer</code></a>
Metal이 렌더링할 수 있는 Core Animation 레이어로, 일반적으로 화면에 표시됨</li>
</ul>
<br>

<h2 id="차이점">차이점</h2>
<p>위에서 잠깐 언급한 것처럼 <code>UIView</code>는 컨텐츠의 드로잉과 애니메이션을 직접 수행하지 않고, <strong>CoreAnimation에게 위임</strong>합니다. 컨텐츠의 조작과 관련된 API들을 제공하지만 CoreAnimation을 이용하는 것이죠.</p>
<p>그 중 <code>CALayer</code>는 컨텐츠를 캡처하여 비트맵으로 backing store에 캐싱합니다. 이때 레이어의 프로퍼티에 변경이 생기면 Core Animation은 해당 레이어의 비트맵과 상태정보를 GPU로 보내고, GPU가 이 정보를 바탕으로 새로운 비트맵을 그립니다(렌더링). View의 draw(_:)가 메인스레드의 CPU를 사용하는 것과는 다르게, CPU에 부담을 주지 않는 결과를 야기합니다.</p>
<h3 id="gpu-🆚-cpu">GPU 🆚 CPU</h3>
<ul>
<li><code>CALayer</code>는 GPU를 사용해 렌더링을 수행하기 때문에 CPU의 메인스레드에 부하를 주지 않습니다.</li>
<li>따라서 앱의 반응성을 저해하지 않으면서, 고주사율의 부드러운 애니메이션을 제공합니다. </li>
<li><code>CALayer</code>는 UIView보다 가볍기 때문에 성능상의 이점을 얻을 수 있습니다.</li>
</ul>
<p>따라서 렌더링 성능을 높이기 위한 프로퍼티도 제공합니다. </p>
<ul>
<li><a href="https://developer.apple.com/documentation/quartzcore/calayer/1410905-shouldrasterize"><code>shouldRasterize</code></a></li>
<li><a href="https://developer.apple.com/documentation/quartzcore/calayer/1410974-drawsasynchronously"><code>drawAsynchronously</code></a></li>
</ul>
<br>

<h3 id="uiview는-calayer의-delegate이다"><code>UIView</code>는 <code>CALayer</code>의 delegate이다</h3>
<p>UIView는 렌더링을 위한 <a href="https://developer.apple.com/documentation/uikit/uiview/1622436-layer">rootlayer</a>를 프로퍼티로 갖습니다. </p>
<pre><code class="language-swift">var layer: CALayer { get }
</code></pre>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/764f1459-8850-4870-a118-2313b00e984a/image.png" alt=""></p>
<p>그리고 <code>UIView</code>가 layer의 델리게이트<a href="https://developer.apple.com/documentation/quartzcore/calayerdelegate"><code>CALayerDelegate</code></a>입니다.</p>
<p><code>CALayerDelegate</code>는 크게 아래와 같은 역할을 합니다.</p>
<ul>
<li><a href="https://developer.apple.com/documentation/quartzcore/calayerdelegate/2097261-display"><code>func display(CALayer)</code></a> 
컨텐츠를 제공하는 역할</li>
<li><a href="https://developer.apple.com/documentation/quartzcore/calayerdelegate/2097257-layoutsublayers"><code>func layoutSublayers(of: CALayer)</code></a> 
layer의 bouds가 변경될 때, sublayer의 레아아웃 핸들링</li>
<li><a href="https://developer.apple.com/documentation/quartzcore/calayerdelegate/2097264-action"><code>func action(for: CALayer, forKey: String) -&gt; CAAction?</code></a> 
layer 특성 변화에 따른 커스텀한 액션을 만들 수 있음</li>
</ul>
<br>

<h3 id="event-handling">Event Handling</h3>
<pre><code class="language-swift">@MainActor class UIView : UIResponder</code></pre>
<p>UIKit의 <code>UIView</code>는 <code>CALayer</code>와 달리 <code>UIResponder</code>의 서브클래스이기 때문에 Responder chain내에서 사용자 터치 또는 gesture 이벤트를 쉽게 핸들링 할 수 있습니다.</p>
<p>또한 gestureRecognizer를 사용하여 common gesture를 핸들링할 수 있습니다.</p>
<p>그렇다면 <code>CALayer</code>는 어떨까요? 
<code>UIResponder</code>를 상속하지 않기 때문에, 바로 이벤트를 받을 순 없지만, <a href="https://developer.apple.com/documentation/quartzcore/calayer/1410972-hittest"><code>hitTest</code></a>, <a href="https://developer.apple.com/documentation/quartzcore/calayer/1410857-contains"><code>contains</code></a>를 구현하고 있기 때문에, Layer단에서 터치 이벤트를 감지할 수는 있습니다. </p>
<br>

<h3 id="기타-기능">기타 기능</h3>
<p>위에서 언급한 차이점 외에도 각 객체는 아래와 같은 추가적 기능을 가집니다. </p>
<ul>
<li><p><code>UIView</code></p>
<ul>
<li>Accessibility 지원</li>
<li>UIMotionEffect</li>
<li>font-sizing perferences 등등</li>
</ul>
</li>
<li><p><code>CALayer</code></p>
<ul>
<li>Layer filter</li>
<li>Layer actions 등등</li>
</ul>
</li>
</ul>
<br>


<h2 id="정리">정리</h2>
<h3 id="uiview-1">UIView</h3>
<ul>
<li><code>CALayer</code>를 사용하는 wrapper, 실제 드로잉과 렌더링은 CoreAnimation에게 위임</li>
<li>CPU의 메인스레드에서 동작</li>
<li>하나의 뷰 컴포넌트로서, 화면 위의 레이아웃, 계층 구조를 설정하기 위한 API 제공 (오토레이아웃, anchor...)</li>
<li><code>UIResponder</code>의 서브클래스, Responder Chain에 속하므로 사용자 이벤트 핸들링을 지원</li>
</ul>
<h3 id="calayer-1">CALayer</h3>
<ul>
<li>실제 GPU에서 렌더링되는 컨텐츠를 관리하는 역할</li>
<li>높은 frame rate를 유지하며, drawing, animation을 효율적으로 할 수 있음 (<code>UIView</code>보다 가볍고 성능이 좋음)</li>
<li>복잡한 애니메이션이나 퍼포먼스가 요구되는 작업에 효율적</li>
<li>저수준 프레임워크이므로, <code>UIView</code>에서 제공하지 않는 세밀한 컨텐츠 컨트롤이 가능</li>
</ul>
<br>

<p><code>UIView</code>는 컨텐츠를 보여주고, 사용자와 인터렉션할 수 있는 사각형틀을 제공하는 기본 UI Component이다. 
<code>CALayer</code>는 <code>UIView</code>의 기반을 제공한다. 하지만 <code>CALayer</code>만 사용해서 UI를 구현할 순 없다. </p>
<p><code>UIView</code>에서 할 수 없는 이미지간 합성, 복잡한 애니메이션, 최적화가 필요하다면, <code>CALayer</code>로 드로잉, 애니메이션을 구현하자</p>
<br>

<h3 id="references">References</h3>
<p><a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/CoreAnimationBasics/CoreAnimationBasics.html#//apple_ref/doc/uid/TP40004514-CH2-SW3">옛 애플 공식문서 - CoreAnimation Programming Guide</a>
<a href="https://www.kodeco.com/10317653-calayer-tutorial-for-ios-getting-started">CALayer Tutorial</a>
<a href="https://gwangyonglee.tistory.com/54">CALayer란</a>
<a href="https://babbab2.tistory.com/53">iOS) CALayer 제대로 이해하기</a>
<a href="https://www.appcoda.com/calayer-introduction/">TUTORIAL A Beginner’s Guide to CALayer</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[HIG] Playing Haptics 📳]]></title>
            <link>https://velog.io/@yeahg_dev/HIG-Playing-Haptic-feat.-%EB%B6%90%EB%B6%90</link>
            <guid>https://velog.io/@yeahg_dev/HIG-Playing-Haptic-feat.-%EB%B6%90%EB%B6%90</guid>
            <pubDate>Sun, 25 Sep 2022 13:33:51 GMT</pubDate>
            <description><![CDATA[<p>##
안녕하세요! </p>
<p>혹시 붐붐폰 기억하시는 분...계시나요?</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/9c16ed62-a96d-42b6-ad5a-0b4fad78da25/image.png" alt=""></p>
<p>(사진 출처: <a href="https://m.cetizen.com/review.php?pid=1348&amp;q=view&amp;vcat=1&amp;pno=1348">https://m.cetizen.com/review.php?pid=1348&amp;q=view&amp;vcat=1&amp;pno=1348</a>)</p>
<p>어릴 때 가장 가지고 싶었던 핸드폰이었는데요. 지금은 진동이 흔하지만, 이때는 혁명이었죠.. 모델이 붐붐폰을 들고 누워서 오토바이 타는 흉내를 냈던... 광고가 제 마음을 사로잡았던 기억이 아직도 생생하네요 </p>
<p>오랜만에 보고 싶어서 찾아봤습니다ㅋㅋㅋ
<a href="https://www.youtube.com/watch?v=otSFWpwR44U">&#39;내 손 안의 오토바이 스카이 붐붐폰&#39; 광고</a></p>
<p>지금 봐도 갖고싶다.. 너란 붐붐❤️ </p>
<p>그래서 애플의 haptic 기능에 더 애착이 가는 것 같아요...</p>
<br>

<p>추억 팔이에 오늘은 서론이 길었네요.
오늘은 제가 개인적으로 좋아하는 기능인, haptic을 사용하는 가이드에 대해 알아보겠습니다!😆</p>
<blockquote>
<p>🙌 원문에서 햅틱을 실제로 플레이해 볼 수 있습니다
원문 : <a href="https://developer.apple.com/design/human-interface-guidelines/patterns/playing-haptics">Playing haptics</a>
참고 WWDC : <a href="https://developer.apple.com/videos/play/wwdc2021/10278/">Practice audio haptic design</a></p>
</blockquote>
<br>

<h1 id="playing-haptics-📳">Playing haptics 📳</h1>
<blockquote>
<p>햅틱을 사용하면 사람들의 촉각을 끌어들일 수 있고, 실제 세계의 친숙함을 앱과 게임에 가져올 수 있습니다.</p>
</blockquote>
<p>Apple pay 트랜잭션의 확인이나 iOS, watchOS에서 알림 도착시 시각적, 청각적 피드백 외에 햅틱을 재생해서 피드백을 줄 수 있습니다. Force Touch trackpad를 갖춘 Mac에서 앱은 컨텐츠를 드래그할 때 햅틱을 재생하거나, 다양한 강도로 클릭하여 화면 요소에서 다양한 수준의 변화를 만들수 있습니다. tvOS, iPadOS에서는 game controller가 햅틱 피드백을 제공할 수 있죠.</p>
<p>플랫폼에 따라, 시스템은 표준 컴포넌트에 디폴트로 햅틱 피드백을 제공합니다. 예를 들어 iPhone은 switches, sliders, picekrs가 햅틱 피드백을 자동으로 재생합니다. Apple watch는 Taptic Engine이 만든 햅틱과 audible tone을 조합해서 재생합니다. (HIG 사이트에 샘플들이 있어요!) 또한 시스템은 앱과 게임에서 사용할 수 있는 빌트 인 햅틱 패턴을 제공하고, 햅틱 패턴을 커스텀할 수도 있습니다.</p>
<h2 id="best-practices">Best practices</h2>
<h3 id="1-시스템이-제공하는-햅틱-패턴은-문서화된-의미에-따라-사용하세요">1. 시스템이 제공하는 햅틱 패턴은 문서화된 의미에 따라 사용하세요</h3>
<p>시스템은 스탠다드 컨트롤과의 상호작용시 스탠다드 햅틱을 일관적으로 재생합니다. (예를들어 피커를 사용할 때 &quot;드르륵&quot; 햅틱이 재생됨) 그러므로 사람들은 스탠다드 햅틱을 이미 알고 있습니다. 만약 햅틱 패턴의 문서화된 사용 예가 당신 앱의 사용 케이스와 어울리지 않는다면 자신만의 햅틱 패턴을 만들어서 사용하세요.</p>
<br>

<h3 id="2-햅틱을-일관적으로-사용하세요">2. 햅틱을 일관적으로 사용하세요</h3>
<p>각 햅틱과 햅틱을 유발하는 액션 사이에 분명한 인과 관계를 만들어야합니다. 분명한 인과 관계란&#39;이러한 상황엔 이러한 햅틱이 재생되어야해.&#39;와 같은 연관을 의미합니다. 만약 햅틱의 인과 관계를 제대로 만들지 않는다면, 사용자는 혼란 스러울 수 있고, 햅틱을 무의미하게 생각할 수 있습니다. </p>
<p>예를 들어, 게임 캐릭터가 미션을 실패했을 때 특정 햅틱을 재생시키면, 사람들은 그 햅틱엔 부정적 결과를 연상시킬 겁니다. 그런데 같은 햅틱을 레벨 업과 같은 긍정적 결과에도 재생한다면, 사용자에게 혼란을 야기할 것입니다.</p>
<br>

<h3 id="3-다른-피드백을-보완하는-방식으로-햅틱을-사용하세요">3. 다른 피드백을 보완하는 방식으로 햅틱을 사용하세요</h3>
<p>시각적, 청각적, 촉각적 피드백이 조화를 이룰 때, 사용자 경험은 더 조리 있고 자연스럽습니다. 시각, 청각, 촉각은 실제 세상에 존재하기 때문입니다. 예로는 햅틱의 강도와 선명도를 함께 사용하는 애니메이션의 느낌과 일치시키는 것이 있습니다. 또한 사운드와 햅틱을 동시에 재생할 수도 있으니, 개발자 문서 <a href="https://developer.apple.com/documentation/corehaptics/delivering_rich_app_experiences_with_haptics">Delivering rich app experiences with haptics.</a>를 참고해보세요</p>
<br>

<h3 id="4-햅틱을-남용하지-마세요">4. 햅틱을 남용하지 마세요</h3>
<p>햅틱을 자주 재생하면 성가시고 짜증나지만, 종종 재생된다면 햅틱을 제대로 느낄 수 있습니다. 그러므로 대부분의 사람들이 인정하는 햅틱 재생의 균형을 찾기 위해 사용자 테스트를 수행하는 것이 중요합니다. 최고의 햅틱 경험은 햅틱의 존재를 인식하지 못하다가, 꺼졌을 때 알아차리는 경우입니다. </p>
<br>

<h3 id="5-햅틱을-옵셔널로-만드세요">5. 햅틱을 옵셔널로 만드세요</h3>
<p>햅틱을 끄거나 음소거할 수 있는 옵션을 제공하세요. </p>
<br>

<h3 id="6햅틱이-다른-사용자-경험에-영향을-줄-수-있음을-알고-계세요">6.햅틱이 다른 사용자 경험에 영향을 줄 수 있음을 알고 계세요</h3>
<p>햅틱은 진동을 느낄 만큼의 물리적 힘을 만들어냅니다. 그러므로 햅틱이 카메라, 자이로스코프, 마이크와 같이 사용될 때 사용자 경험을 저해할 수 있습니다. </p>
<br>

<h2 id="custom-haptics">Custom haptics</h2>
<p>게임은 게임플레이를 향상 시키기위해 종종 햅틱을 커스텀합니다. 게임보다는 덜 흔하지만, 게임이 아닌 앱도 풍부하고 즐거운 경험을 제공하기 위해 햅틱을 커스텀하기도 합니다. </p>
<p>사용자의 input이나 컨텍스트에 기반해서 다이나믹하게 다른 햅틱 패턴을 만들 수 있습니다. 
게임을 예로 보면, 게임 캐릭터가 나무에서 점프할 때 느끼는 임팩트가 캐릭터가 제자리에서 점프하는 임팩트보다 클 수 있습니다. 충돌과 hit같은 경험은 다가오는 발걸음이나 위험의 정도와 같은 미세한 상황에 따라 다르게 느껴질 수도 있습니다. </p>
<p>햅틱 패턴은 여러개의 이벤트들로 구성됩니다. 가장 많이 사용되는 이벤트로는 <strong>transient event</strong>와 <strong>continous event</strong>가 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/942af38c-a3aa-473e-9422-de0e78c8227f/image.PNG" alt=""></p>
<ul>
<li><strong>일시적(Transient) 이벤트</strong> : 짧고, 컴팩트해서 탭이나 impulse같이 느껴집니다. 시스템의 플래시 라이트 버튼을 사용하는게 transient event의 예입니다. </li>
<li><strong>연속적(Continous) 이벤트</strong> : 지속되는 긴 진동 같습니다. 메시지 앱에서 laser effect로 보내기가 예입니다.</li>
</ul>
<p>아래 그림처럼, 이벤트들을 시간에 따라 배치함으로써 패턴이 만들어집니다. </p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/584d8ed3-1558-4ea0-a32c-6417bdd087d5/image.PNG" alt=""></p>
<p>또한 햅틱 이벤트의 종류에 무관하게, 햅틱의 sharpness(선명도)와 intensity(농도)도 조절가능합니다. </p>
<ul>
<li><strong>sharpness</strong>는 햅틱 경험을 물리적 감각에 대응하는 파형으로 추상화하는 방법으로서 생각해볼 수 있습니다. 선명도, 뾰족함을 지정하면 경험의 의도를 전달할 수 있습니다. 예를 들어, 선명도를 사용해서 부드럽고 둥글며 유기적인 경험을 전달하거나 선명하고 정밀하고 기계적인 경험을 전달 할 수 있습니다. </li>
<li><strong>intensity</strong>는 햅틱의 강도를 의미합니다. </li>
</ul>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/292c419e-adb8-429e-a913-7221a3d771f0/image.PNG" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/af52cd94-749e-434b-b636-2dc1dca63eb4/image.PNG" alt=""></p>
<p>transient event, continuous event를 조합, sharpness와 intensity를 조절하고 선택적으로 오디오 컨텐츠를 포함함으로써 넓은 범위의 햅티 경험을 만들 수 있습니다. 
개발자 가이드는 <a href="https://developer.apple.com/documentation/corehaptics">Core Haptics</a>를 참고하세요!</p>
<br>

<h2 id="platform-consideration---ios에서-햅틱-만드는-방법">Platform consideration - iOS에서 햅틱 만드는 방법</h2>
<p>iPhone에서 햅틱을 넣는 방법은 두가지가 있습니다. (Core haptic까지 포함하면 3가지)</p>
<ol>
<li><p><strong>standard UI Components</strong> 사용 - swithces, sliders, pickers (디폴트로 애플이 디자인한 시스템 햅틱이 재생됩니다)</p>
</li>
<li><p><strong><a href="https://developer.apple.com/documentation/uikit/uifeedbackgenerator">UIFeedbackGenerator</a></strong> 사용 - 미리 만들어둔 햅틱 패턴(Notification, impact, selection)을 사용하면됩니다. 
<a href="https://developer.apple.com/design/human-interface-guidelines/patterns/playing-haptics">여기</a>에가면 햅틱 패턴을 재생해볼 수 있습니다.</p>
</li>
<li><p><strong>Core Haptics</strong> 프레임워크 사용 - 햅틱 커스터마이즈 가능</p>
</li>
</ol>
<br>

<h2 id="마무리">마무리</h2>
<p>햅틱을 만들 때 유의할 점들과 햅틱을 커스텀하게 만드는 방법, 햅틱을 구현하는 방법들에 대해 알아보았습니다.</p>
<p>개인 앱을 출시하게 된다면 꼭 사용해보고 싶네요!ㅎㅎ</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Instrument로 Commit hitch 찾고 제거하기]]></title>
            <link>https://velog.io/@yeahg_dev/Instrument%EB%A1%9C-Commit-hitch-%EC%B0%BE%EA%B3%A0-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yeahg_dev/Instrument%EB%A1%9C-Commit-hitch-%EC%B0%BE%EA%B3%A0-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 11 Sep 2022 09:06:28 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요~ 릴리에요 
오늘은 <a href="https://velog.io/@yeahg_dev/UIAnimation-Hitch%EC%99%80-Render-Loop%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90">UIAnimation Hitch와 Render Loop에 대해 알아보자</a> 에 이어서, Commit hitch를 Instrument로 포착, 분석하는 방법과 hitch를 제거하는 방법에 대해 공부해보겠습니다!</p>
<p><a href="https://developer.apple.com/videos/play/tech-talks/10856/">TechTalk: Find and fix hitches in the commit phase
</a>를 보고 정리한 글입니다.</p>
<p>가보자고~🚀</p>
<br>

<p>먼저 렌더 루프를 간략하게 보고 가겠습니다. </p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/53cd1bca-6565-4b23-9881-a6c491d53805/image.png" alt=""></p>
<p>iOS는 뷰를 보여주기 위해 렌더 루프를 사용합니다. 사용자 이벤트가 앱으로 전달되고, 앱은 사용자 이벤트에 따라 뷰를 업데이트합니다. 그리고 업데이트된 뷰는 GPU에서 렌더링되어 최종적으로 화면에 보여지게됩니다. </p>
<p>사용자 이벤트에 따라 UI를 변경하고, 업데이트된 UI layer tree를 GPU에게 제출하는 것을 <code>Commit</code> 이라 하고, Commit이 다음 VSYNC까지 완료되지 못해 발생하는 hitch를 <code>Commit hitch</code>라고 합니다.  </p>
<h1 id="commit-transaction">Commit Transaction</h1>
<p>먼저 Commit transaction에서 발생되는 일에 대해 알아보겠습니다.</p>
<p>여기 이벤트를 기다리는 뷰가 있습니다. 뷰가 터치 이벤트를 받으면 배경색을 바꾸든, 서브 뷰의 frame을 변경하든 UI를 업데이트하게 됩니다. </p>
<p>시스템은 레이아웃이나 디스플레이 변경이 필요한 서브 뷰들을 기록해둡니다. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/da1f7086-a5b8-4f0a-bcc2-7c2de0f9791d/image.png" alt=""></p>
<p>그리고 다음 커밋 트랜잭션에서 시스템에 의해 <code>draw()</code>, <code>layoutSubviews()</code>가 호출되면서 디스플레이와 레이아웃은 업데이트 됩니다.
<img src="https://velog.velcdn.com/images/yeahg_dev/post/34469a1b-7570-4407-870c-417aeda63f04/image.png" alt=""></p>
<p>커밋 트랜잭션은 디테일하게는 4단계로 구분됩니다. </p>
<p><code>layout</code> ➡️ <code>Display</code> ➡️ <code>Prepare</code> ➡️<code>Commit</code></p>
<h3 id="1-layout-phase">1. Layout phase</h3>
<p>레이아웃 단계에서는 레이아웃이 필요한 모든 뷰의 <code>layoutSubviews()</code>가 호출됩니다. 모든 서브뷰들의 레이아웃이 변경되는 것이죠</p>
<p>레이아웃은 다음과 같은 상황에서 발생합니다.</p>
<ul>
<li>뷰 포지션을 변경할 때 (frame, bounds, transform)</li>
<li>뷰를 추가하거나 삭제할 때</li>
<li><code>setNeedsLayout()</code>을 직접 호출할 때</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/196cc05c-db0d-4c88-a21a-7484ffd9a5ac/image.png" alt=""></p>
<h3 id="2-display-phase">2. Display phase</h3>
<p>디스플레이 단계에서는 컨텐츠 업데이트가 필요한 모든 뷰들의 <code>draw()</code> 가 호출됩니다. </p>
<p>디스플레이는 다음과 같은 상황에서 발생합니다.</p>
<ul>
<li>오버라이드한 <code>draw</code>에서 뷰를 추가할 때</li>
<li><code>setNeedsDisplay()</code>를 직접 호출 할 때</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/04694bf1-b9ca-4463-8447-1ac6587828c6/image.png" alt=""></p>
<h3 id="3-prepare-phase">3. Prepare phase</h3>
<p>준비 단계에선 이미지에 대한 작업을 처리합니다.</p>
<ul>
<li><p>아직 디코드되지 않은 이미지가 있다면 디코드 됩니다. 만약 큰 이미지라면 많은 시간이 걸릴 수 있습니다. </p>
</li>
<li><p>GPU에서 지원하지 않는 Color format의 이미지가 있다면, 이 단계에서 변환됩니다. </p>
</li>
</ul>
<p>이미지의 성능을 최적화하려면 <a href="https://developer.apple.com/videos/play/wwdc2018/219/">WWDC18: Image and Graphics Best Practices</a>을 참고해주세용</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/a71bff58-250e-4647-8abd-083c5f4c3a3c/image.png" alt=""></p>
<h3 id="4-commit-phase">4. Commit phase</h3>
<p>뷰 레이어 트리가 상위뷰부터 하위뷰까지 재귀적으로 포장되고, 렌더 서버에게 전송됩니다. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/547d8638-266c-47cc-8397-7965998839c6/image.png" alt=""></p>
<br>


<h1 id="instrument로-hitch-찾고-개선하기">Instrument로 hitch 찾고, 개선하기</h1>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/974e870c-f1bf-494d-ac98-cb9235b348c5/image.png" alt=""></p>
<p>Xcode12에는 Instrument에 Animation Hitches라는 템플릿이 추가되었습니다. 
이 기능을 사용해서 히치를 찾고 시각적으로 분석해볼겁니다!</p>
<h2 id="example-app">Example App</h2>
<p>예제 앱을 통해 히치를 찾아보고 고쳐볼 건데요, 앱을 스크롤하며 Insrtrument에서 기록을 해보겠습니다. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/697deb93-8faf-49bd-8425-f72226302901/image.png" alt=""></p>
<p>그리고 Instrument를 보면 Hitches라는 트랙에서 감지된 히치들을 보여줍니다!
<img src="https://velog.velcdn.com/images/yeahg_dev/post/25486e24-a9a9-44ab-9848-8434c5c354e9/image.png" alt=""></p>
<p>히치 트랙을 펼치면 더 다양한 정보를 볼 수 있습니다</p>
<p>히치의 지속 시간, 커밋 단계, 렌더 단계, 프레임 수명, VSync를 알 수 있네요!</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/8ba75133-38a1-43f5-891e-27d713ea96cb/image.png" alt=""></p>
<p>hitch가 발생하기 전까지의 간격을 <strong>허용 가능한 지연(Acceptable Latency)</strong>라고 합니다. </p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/4558f062-7620-464e-b3c3-bcaec0223b8f/image.png" alt=""></p>
<p>그 후의 hitch 지속 시간은 <strong>hitch duration</strong>이라고 부릅니다 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/b78c66fc-e401-47ac-97e1-0ffae25c6bc2/image.png" alt=""></p>
<p>detailView에서는 각 히치의 duration, acceptable latency, buffer count 그리고 <strong>hitch type</strong>을 알 수 있습니다. 
hitch type은 hitch가 어떤 단계에서 기인했는지, hitch의 원인을 파악하는데 매우 중요한 정보가 됩니다. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/c50318d0-e6ea-4349-bfa5-d15de58b6ef8/image.png" alt=""></p>
<p>위에서 보여주듯, 선택된 히치는 commit, GPU phase에서 발생했습니다.
Instrument의 UIAnimation hitch 템플릿은 Time profiler template을 포함하고 있기 때문에, 이 시점에 어떤 코드가 실행되었는지 알 수 있습니다.</p>
<p>해당 영역을 선택하고, 커밋이 실행된 프로세스를 필터링합니다. (이 예제에서는 meal planner 앱)
<img src="https://velog.velcdn.com/images/yeahg_dev/post/a6cf3fa1-2b85-49e2-abd7-1abe81cb93f5/image.png" alt=""></p>
<p>그리고 main thread를 선택하고, call tree를 살펴봅니다.
여기서 가장 비싼 호출을 분석할 수 있습니다. <code>QSTEM CollectionViewCell</code>의 <code>updateTags()</code> 가 가장 많은 시간인 10ms를 사용했네요. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/f245af20-1d2b-4a69-bac3-0be5299670ca/image.png" alt=""></p>
<p>이 함수를 분석하면 commit hitch를 유발한 원인이 무엇인지 알 수 있겠죠?</p>
<p>먼저 <code>QSTEM CollectionViewCell</code>의 component들을 살펴보겠습니다. 
<code>UIImageView</code>와 <code>UILabel</code>, <code>MKTMealPlannerTagLabel</code>로 구성되어있습니다. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/8017979b-59cc-444e-97d0-17cbc3c6f0d6/image.png" alt=""></p>
<p>이제 <code>QSTEM CollectionViewCell</code>의 코드를 살펴볼게요.
<code>menuItem</code>은 property observer를 사용하고, 두가지 상황에서 가 호출되고 있습니다. <code>menuItem</code>이 존재하는 경우와 <code>menuItem</code>이 <code>nil</code>인 경우, 2가지 상황에서 <code>updateTags()</code> 가 호출됩니다.
<img src="https://velog.velcdn.com/images/yeahg_dev/post/1dd3660b-125c-40fe-ac06-8890ea389f53/image.png" alt=""></p>
<p><code>updateTags</code>메서드 구현을 살펴보겠습니다. </p>
<p>태그가 없고, 태그를 담는<code>mealTagStackView</code>를 슈퍼뷰에서 제거합니다. 
태그가 있고, <code>mealTagStackView</code>가 없다면 <code>mealTagStackView</code>를 만들어서 <code>rootStackView</code>의 하위뷰에 추가합니다. </p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/a5b46540-04fc-4d7b-ab62-ca90301f91e5/image.png" alt=""></p>
<p>그리고 재사용가능한 tagLabel이 있다면 사용하고, 아니라면<code>MKTMealPlannerTagLabel</code>를 생성해서 태그를 보여줍니다.</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/fb8350f8-f061-4768-ba1d-6801ba2b23ac/image.png" alt=""></p>
<p>Cell의 <code>prepareForReuse</code>를 살펴보겠습니다. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/34efe946-2f41-4292-994e-135ebc739628/image.png" alt=""></p>
<p><code>menuItem</code>에 nil을 할당해주고 있습니다. nil을 할당하는 것은 <code>updateTags</code>를 호출하는 두번째 시나리오에 해당됩니다. 따라서 cell이 디큐될 때마다, 기존의 <code>mealTagStackView</code>를 제거하고, 위에서 구현한 <code>TagLabel</code>의 재사용 로직을 사용하지 못합니다. 매번 새로운 <code>TagLabel</code>을 생성하고 이는 최적의 퍼포먼스를 보장하지 못하겠죠.</p>
<p>또한 하위뷰를 추가하거나 삭제하면 커밋 트랜잭션에서 살펴본 것과 같이, commit phase에서 레이아웃이 업데이트 됩니다. 따라서 Commit phase에 할 일이 증가하게됩니다.</p>
<p>해결 방법은 간단합니다.
<code>prepareForReuse</code>의 아무런 일도 해주지 않으면 됩니다. </p>
<p>그럼 재사용 로직을 활용할 수 있고, 뷰의 계층구조를 변경하고, Label을 생성하는 비용을 줄일 수 있습니다!
<img src="https://velog.velcdn.com/images/yeahg_dev/post/dc678927-45c8-452a-982c-5e7c7af69c83/image.png" alt=""></p>
<p>해결 한 후의 hitch를 비교해보면, 전보다 아주 많이 hitch수가 줄어든 모습입니다!</p>
<table>
<thead>
<tr>
<th>전</th>
<th>후</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/yeahg_dev/post/f1414669-82e1-4508-9c91-0fa13de8139d/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/yeahg_dev/post/2bcd0fc6-ada8-4c00-a9cf-47cc6566e996/image.png" alt=""></td>
</tr>
</tbody></table>
<br>

<blockquote>
<p>이 처럼 Time Profiler를 함께 사용해서, hitch가 발생할 때 어떤 코드가 실행되었는지 파악할 수 있습니다. hitch의 원인을 파악하고 고치는데 매우 중요한 역할을 할 수 있습니다👍</p>
</blockquote>
<br>

<p>Instrument를 통해 앱에서 hitch를 파악하는 방법에 대해 알아봤다면, Commit phase에서 hitch를 줄 일 수 있는 추천 방법, 꿀팁들을 살펴볼게요!</p>
<h1 id="commit-hitch를-줄이는-방법">Commit hitch를 줄이는 방법</h1>
<h2 id="1-keep-views-lightweight">1. Keep views lightweight</h2>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/5aa837d3-e50e-4491-9db6-d524b43ca10a/image.png" alt=""></p>
<h3 id="1-calayer의-property를-최대한-사용하기">1) <code>CALayer</code>의 property를 최대한 사용하기</h3>
<p><code>draw</code>는 CPU에서 연산을 하지만, <code>CALayer</code>는 GPU를 가속해서 사용합니다. CPU에서 연산을 하게되면 mainthread의 부하를 증가시키겠죠? </p>
<p>그러니 <code>CALayer</code>의 프로퍼티로 구현이 가능하면 <code>draw</code> 대신 쓰세요! </p>
<p>만에 하나 <code>draw</code>를 사용하게 되면 성능을 측정하세요</p>
<h3 id="2-필요-없다면-draw-override-하지-않기">2) 필요 없다면 <code>draw</code> override 하지 않기</h3>
<p><code>draw</code>를 override해서 빈 구현 하지 마세요. 
단지 <code>draw</code>를 override하는 것 만으로 커밋 트랜잭션동안 더 많은 메모리와 시간을 사용하게 합니다. </p>
<h3 id="3-view-재사용하기">3) view 재사용하기</h3>
<p>뷰를 재사용 하세요!</p>
<p>뷰 계층구조를 변경하는 추가하고 삭제하는 작업은 비싼 비용입니다. </p>
<h3 id="4-ishidden-사용하기">4) <code>isHidden</code> 사용하기</h3>
<p>애니메이션동안 특정 뷰를 안보이게 하고 싶다면 <code>isHidden</code>프로퍼티를 사용하세요!</p>
<p><code>isHidden</code>은 <code>remove</code>보다 훨씬 저렴한 작업이니까요.</p>
<h2 id="2-reduce-expensive-or-redundant-layout">2. Reduce expensive or redundant layout</h2>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/1c43b5b1-5965-47fc-95cc-d0356800d12a/image.png" alt=""></p>
<h3 id="1-layoutifneeded-보다-setneedslayout-사용하기">1) <code>layoutIfNeeded</code> 보다 <code>setNeedsLayout()</code> 사용하기</h3>
<p>레이아웃을 업데이트하려면 <code>setNeedsLayout()</code> 사용하세요!</p>
<p><code>layoutIfNeeded</code>는 커밋 트랜잭션 시간을 늘리고, 히치를 유발합니다. 대부분 경우는 레이아웃을 업데이트하기위해 다음 런루프를 기다릴 수 있을겁니다. </p>
<h3 id="2-필요한-최소한의-constraint만-사용하세요">2) 필요한 최소한의 constraint만 사용하세요</h3>
<p>복잡한 제약 계산을 피하기 위해, 필요한 최소한의 제약만 사용하세요!</p>
<h3 id="3-자신-뷰의-레이아웃만-업데이트-하세요">3) 자신 뷰의 레이아웃만 업데이트 하세요</h3>
<p>자신 또는 자식 뷰의 레이아웃만 <code>invalidate</code>하세요! 
부모 뷰의 레이아웃을 <code>invalidate</code>하면 재귀적인 레이아웃을 유발하고 비용이 비쌉니다. </p>
<br>


<h1 id="정리">정리</h1>
<p>커밋 히치를 찾고, 원인을 파악하는 방법과 commit hitch를 사전에 예방할 수 있는 꿀팁들에 대해 알아보았습니다. </p>
<p>뷰의 frame rate가 떨어질 때마다, 적용시켜볼 수 있는 실질적 방법들을 알게된것 같아 든든하네요!🙂
Instrument 애용해야겠습니다ㅎㅎㅎ</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[UIAnimation Hitch와 Render Loop에 대해 알아보자]]></title>
            <link>https://velog.io/@yeahg_dev/UIAnimation-Hitch%EC%99%80-Render-Loop%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@yeahg_dev/UIAnimation-Hitch%EC%99%80-Render-Loop%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Tue, 06 Sep 2022 11:28:59 GMT</pubDate>
            <description><![CDATA[<p>UI의 performance는 어떻게 측정할 수 있을까요?</p>
<p>이에 대한 답을 찾기 위해 <a href="https://developer.apple.com/videos/play/tech-talks/10855/">TechTalk: Explore UI animation hitches and the render loop</a>을 시청했습니다. 
위 영상에서는 직접적 방법에 대해 알려주진 않지만, UI 렌더 성능이 저하될 때 나타나는 현상인 <strong>hitch</strong>와 <strong>render loop의 과정</strong>을 설명해주고 있어요!</p>
<p>측정 방법은 위 영상에서 소개한 다른 세션을 보고 포스팅을 해보도록하고요, 오늘은 hitch와 render loop에 대해 정리해보도록 할게요</p>
<br>


<h1 id="hitch는-무엇일까">hitch는 무엇일까?</h1>
<blockquote>
<p>any time a frame appears on screen later than expected</p>
</blockquote>
<p>&quot;프레임이 스크린에 제 때 나타나지 않는, 늦게 나타나는 시간&quot;이라고 정의합니다. </p>
<p>즉 다음 프레임의 생성이 늦어져 애니메이션이 끊기는 시간입니다. </p>
<p>여기서 궁금증이 하나 생겼습니다.</p>
<p>그럼 프레임이 뭘까요?</p>
<h3 id="frame--정지-사진">frame : 정지 사진</h3>
<blockquote>
<p>동영상을 물리적으로 환원하면 시간상 연속된 정지 사진들의 모음으로 볼 수 있는데, 이 각각의 <strong>정지 사진 하나</strong>를 &#39;<a href="https://namu.wiki/w/%ED%94%84%EB%A0%88%EC%9E%84">프레임</a>&#39;이라 부른다.</p>
</blockquote>
<p>우리가 움직이는 영상으로 인식하는 컨텐츠는 사실, 연속된 정지 사진들의 모임입니다. 
사진들을 일정한 시간 단위로 연속적으로 보여주면, 우리는 사진이 움직인다고 느끼는 것이죠!</p>
<p>여기서 하나의 정지 사진을 frame이라고 합니다.</p>
<p>이를 애플리케이션 화면에 대입해보면, 
스크롤이 되어 움직이는 것 같은 화면도 사실은 사진(frame)들의 모음인 것이죠
<img src="https://velog.velcdn.com/images/yeahg_dev/post/dc2d2fb2-f642-483b-9d96-7536de27cb22/image.png" alt=""></p>
<h3 id="fps-frame-per-second">FPS (Frame Per Second)</h3>
<blockquote>
<p>1초당 몇 개의 frame이 보이느냐. 프레임이 보이는 속도</p>
</blockquote>
<p>그래서 &#39;frame이 1초에 몇 개씩 보이느냐&#39;는 영상의 품질을 결정하는 요소가 됩니다. 
인간은 1초에 15장의 사진을 연속적으로 보여주면(15fps), 자연스러운 동영상으로 인식한다고 하네요! </p>
<h3 id="refresh-rate">Refresh rate</h3>
<p>FPS와 함께 많이 사용되는 유사한 개념인 refresh rate도 있습니다. </p>
<p>FPS가 영상의 소스(파일)을 대상으로 한 개념이라면, refresh rate는 디스플레이 디바이스를 대상으로 하는 개념입니다.</p>
<blockquote>
<p>디스플레이 기기가 1초동안 몇 개의 프레임을 표시할 수 있는지. Hz 단위로 표시</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/0848aae0-26ab-4b62-8b88-af45d4026ae5/image.png" alt=""></p>
<p>화면 주사율(Scan rate), 화면 재생 빈도라고도 합니다. </p>
<br>

<h3 id="hitch의-원인">hitch의 원인</h3>
<p>UIAnimation은 이렇게 연속적인 frame을 내보내는데, 
다음 frame이 보여져야할 시간에 보여지지 않고, 이전 frame을 보여준다면 애니메이션이 버벅이겠죠.</p>
<p>그럼 frame이 왜 제 시간에 나타나지 못할까요?</p>
<p>그 원인은 <strong>render loop가 제 시간에 frame을 완성하지 못했기</strong>때문입니다.
<img src="https://velog.velcdn.com/images/yeahg_dev/post/601f6704-b6a6-4c46-958c-1d74e392446d/image.png" alt=""></p>
<br>

<h1 id="the-render-loop">The Render Loop</h1>
<p>렌더 루프란 사용자 터치 이벤트가 앱으로 전달되고, 그에 따른 UI의 변화가 운영체제로 전달되는 과정을 말합니다. 렌더 루프가 한 번 실행 될 때 하나의 프레임이 완성됩니다. </p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/7ecc85d7-257e-435d-ba53-9d3217f45098/image.png" alt=""></p>
<p>렌더루프는 기기의 refresh rate 시점마다 실행됩니다.</p>
<p>예를 들어 아이폰의 refresh rate가 60Hz라면, 1초에 60장의 프레임이 표시되니
1second / 60 = 16.67ms 마다 프레임이 교체됩니다.</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/48d4149e-a8a8-4b02-ae38-5757ed83c889/image.png" alt=""></p>
<p>스크린에서 프레임이 교체되는 시점에 하드웨어는 <strong>VSYNC</strong>라는 이벤트를 방출합니다.
이때까지 다음 프레임은 준비되어있어야 합니다. </p>
<p>(뒤에서 설명하겠지만 &quot;VSYNC == frame이 교체되는 시점&quot;은 렌더 루프의 각 단계가 일을 마쳐야하는 마감기한이 됩니다. 그리고 이 마감기한을 맞추지 못하면 hitch가 발생합니다.)</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/cac64b22-cbea-4d00-8470-fef87a4631fb/image.png" alt=""></p>
<p>정리하자면, 
Refresh rate에 맞춰 일정 시간(16.67ms 또는 8.33ms) 마다 frame이 교체 되고, 
교체되는 시점에 시스템은 VSYNC라는 이벤트를 내보냅니다. 
VSYNC에 맞춰 렌더 루프도 시작합니다. </p>
<h2 id="3-phase-of-render-loop">3 phase of Render Loop</h2>
<p>렌더 루프는 크게는 3가지 단계로 이루어져있습니다. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/dd898975-d48c-4dd2-a08d-2967be8eec25/image.png" alt=""></p>
<ol>
<li><code>App</code> 
이벤트가 처리되고, UI에 변화가 생기는 단계입니다. 이 일은 다음 VSYNC 전에 완료되어야 합니다. 그래야 지연 없이 다음 단계를 시작 할 수 있습니다.</li>
<li><code>Render server</code> 
실제로 UI가 렌더되는 단계입니다. render server라는 별개의 프로세스에서 수행됩니다. 이 단계 또한 다음 VSYNC전에 완료되어야합니다. 그래야 다음 단계에서 새로운 프레임을 보여줄 수 있음</li>
<li><code>On the display</code> 
완성된 새로운 프레임이 화면에 표시됩니다.</li>
</ol>
<p>위 그림에서 보여지듯이
프레임이 보여지기 두 프레임 전에 렌더루프가 시작되어 다음 보여질 프레임을 준비합니다. </p>
<h3 id="double-buffering">Double Buffering</h3>
<p>2 프레임 전에 렌더루프가 시작되는 것을 double buffering이라고 합니다.</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/5518b8c5-f45d-435e-866a-9ca5a5e07319/image.png" alt=""></p>
<h3 id="triple-buffering">Triple Buffering</h3>
<p>Triple Buffering은 hitch를 피하기 위해 3 프레임 전에 시작합니다. render sever에게 2 프레임을 할당함으로써 모든 렌더링 작업을 완료할 수 있도록 하는 것인데요, 이건 fall back 모드라고 합니다. </p>
<p>그래서 영상에서는 Double buffering을 기준으로 설명을 이어나가네요
<img src="https://velog.velcdn.com/images/yeahg_dev/post/90e953a1-d784-4282-b6db-be9d91eff39c/image.png" alt=""></p>
<br>

<h2 id="5-phase-of-render-loop">5 phase of Render Loop</h2>
<p>전체 렌더 루프는 총 5단계로 구성되어있습니다. </p>
<ol>
<li>Event : 앱이 터치 이벤트를 처리하고, UI를 변화 시킬지를 결정 <code>App 단계</code></li>
<li>Commit : 앱이 UI를 업데이트, 업데이트한 UI를 렌더링해달라고 렌더 서버에게 제출 <code>App 단계</code></li>
<li>Render prepare : 렌더 서버는 다음 VSYNC에서 제출본을 받고, 새로운 UI를 GPU에서 그릴 준비를 함 <code>Render 단계</code></li>
<li>Render execute : GPU는 UI를 최종 이미지로 그림 <code>Render 단계</code></li>
<li>Display : 다음 VSYNC에서 사용자에게 최종 이미지인 프레임이 보여짐 <code>Display 단계</code></li>
</ol>
<br>

<p>각 단계에서 어떤 일을 하는지 자세히 살펴볼게요</p>
<h3 id="1-event-phase">1. Event phase</h3>
<p>앱이 이벤트를 처리하고 UI를 변경시키는 단계입니다. 이벤트라 하면 사용자 터치, 네트워크 콜백, 키보드와 같은 이벤트를 포함합니다.</p>
<p>이벤트가 UI를 변경시킨다면, 앱은 레이어의 계층 구조, 배경 색, 사이즈나 위치를 변경합니다.
또한 앱이 레이어의 bounds를 바꾸면, CoreAnimation은 <code>setNeedsLayout</code>을 자동으로 호출합니다. 또는 개발자가 직접 <code>setNeedsLayout</code>을 호출해 UI를 다시 그려달라고 예약할 수 도 있습니다. </p>
<p><code>setNeedsLayout</code>이 호출되어 레이아웃 재계산이 필요한 레이어들을 식별되고, commit phase로 넘깁니다. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/c78ddd0a-cd98-4288-a7f7-a4ad11f7e233/image.png" alt=""></p>
<h3 id="2-commit-phase">2. Commit phase</h3>
<p>레이어의 레이아웃을 배치하고, draw를 하는 단계입니다. </p>
<p>먼저, 시스템은 레이아웃 업데이트 요청들을 병합해서 중복 작업을 제거하고 순서대로 처리합니다. 그리고 레이아웃의 변경이 필요한 레이어들을 부모부터 자식까지 하나씩 배치해 Layer tree를 만듭니다.  </p>
<p>레이아웃은 흔히 일어나는 성능 bottleneck입니다. 따라서 이 과정이 몇 ms안에 이뤄진다는 것을 고려해야합니다. </p>
<p>두번째로, <code>drawRect</code>을 오버라이딩한 커스텀 뷰나 <code>setNeedsDisplay</code>가 호출된 뷰들이 그려집니다. 레이아웃과 마찬가지로 시스템은 이러한 요청들을 병합한 후 레이아웃이 완료되면 한 번에 처리합니다.</p>
<p>코어 애니메이션이 사용되는 한, 이 단계에서는 레이어는 단지 이미지입니다. (CALayer가 관리하는 비트맵 이미지라는 것으로 이해 했는데, 맞나요..? 좀 더 공부해봐야겠습니다)</p>
<p>이렇게 배치되고 그려진 레이어 트리는 render server로 보내집니다.</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/649af761-eadc-4373-87b6-ecabce662e73/image.png" alt=""></p>
<h3 id="3-render-prepare">3. Render Prepare</h3>
<p>레이어 트리를 순회하며 각 레이어의 drawing command를 GPU가 실행할 수 있는 선형 파이프라인으로 준비하는 단계입니다.</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/2826ad80-5076-4b88-aa49-480d113ae43f/image.png" alt=""></p>
<p>top layer에서 부터 시작해 부모에서 자식 순으로, 레이어를 정렬합니다. 개발자 입장에서 보았을 때, 뒤에서 부터 앞으로 정렬 되네요. 따라서 그림은 뒤에서부터 앞으로 그려집니다. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/86b156b6-1ec3-4a25-83db-666398fbfdf5/image.png" alt=""></p>
<h3 id="4-redner-execute">4. Redner Execute</h3>
<p>선형 파이프라인이 GPU로 전달되고, 렌더 서버에서 레이어가 최종본으로 합성됩니다.</p>
<p>어떠한 레이어는 렌더링 소요시간이 길기 때문에, 이는 bottleneck의 원인으로 작용하기도 합니다. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/d6f92067-67eb-4e73-aab1-941bd9e3a624/image.png" alt=""></p>
<h3 id="5-display">5. Display</h3>
<p>이미지가 완성되면 다음 VSYNC에서 화면에 보여집니다.</p>
<h2 id="병렬적으로-실행되는-루프">병렬적으로 실행되는 루프</h2>
<p>파이프라인은 병렬적으로 실행됩니다. 동시다발적으로 각 단계를 수행하고 있습니다. 
따라서 하나의 단계라도 데드라인을 맞추지 못하면 치명적이죠...
<img src="https://velog.velcdn.com/images/yeahg_dev/post/1e0ee016-f9a8-4060-a15f-df717ee670be/image.png" alt=""></p>
<br>

<h1 id="2가지-hitch-타입">2가지 hitch 타입</h1>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/2f2179b7-ba9a-4cb7-8d20-bc8815c00d5c/image.png" alt="">
hitch는 두가지 타입이 있습니다. </p>
<ul>
<li>Commit hitch: 앱 프로세스에서 commit하는데 오래 걸려서 발생</li>
<li>Render hitch: 렌더 서버에서 제 시간내 렌더링하지 못해서 발생</li>
</ul>
<h2 id="1-commit-hitch">1. Commit hitch</h2>
<p>커밋이 기한 내 완료되지 않아 다음 VSYNC에 렌더링을 하지 못하고, 이전 프레임을 2프레임에 걸쳐 보여줍니다. 결국 1개의 프레임만큼 지연이 발생합니다. </p>
<p>이와 같이 지연되는 시간을 hitch time이라 하고, ms로 측정합니다. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/e84d2948-c365-4020-b30e-77625bc6829b/image.png" alt=""></p>
<p>커밋 히치를 찾고, 고치려면? 을 참고해주세요!
<a href="https://velog.io/@yeahg_dev/Instrument%EB%A1%9C-Commit-hitch-%EC%B0%BE%EA%B3%A0-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B0">Commit Hitch를 찾고 제거하는 방법 -&gt; TechTalk: Find and fix hitches in the commit phase</a></p>
<h2 id="2-render-hitch">2. Render hitch</h2>
<p>렌더링이 다음 VSYNC 전까지 완성되지 못해 프레임이 지연되는 현상입니다. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/0e6d98d6-4f01-4322-bf3c-16708ebf98cc/image.png" alt="Find and fix hitches in the commit phase"></p>
<p>렌더 히치에 대해 더 알아보고, 레이어 트리를 최적화하는 방법은 아래 영상을 참고해주세요!
<a href="https://developer.apple.com/videos/play/tech-talks/10857/">Demystify and eliminate hitches in the render phase</a></p>
<br>

<h1 id="hitch-정량적으로-측정하기">hitch 정량적으로 측정하기</h1>
<p>hitch time으로 측정하는 것은 부정확 할 수 있습니다. 
하나의 hitch를 분석할 땐 유용할 수 있지만, 스크롤이나 애니메이션과 같은 상황에서는 부적절합니다. 왜냐하면 스크롤이나 애니메이션은 UI를 보여주는 시간과 프레임의 개수가 똑같아야 비교를 할 수 있기 때문입니다. 심지어 iOS 기기는 항상 스크린을 업데이트 하지 않습니다. 커밋이 있어야 새로운 프레임이 생기죠. 따라서 기기 간에 히치 타임을 비교하는 것이 더 어렵습니다.
<br></p>
<h2 id="hitch-time-ratio">Hitch time ratio</h2>
<p>대신 Hitch time ratio를 미터법으로 사용합니다. </p>
<blockquote>
<p>총 히치 시간을 지속 시간으로 나눈 값
<img src="https://velog.velcdn.com/images/yeahg_dev/post/439fc944-dfb3-4f68-b46a-77afa50c6437/image.png" alt=""></p>
</blockquote>
<p>총 시간으로 나눈 정규화된 값이므로 비교가 가능한 수치라고 할 수 있습니다. </p>
<p>hitch time ratio가 UI의 자연스러움에 얼마나 영향을 주느냐에 따라 기준을 설정해뒀네요.<br>측정하고 개선할 때 참고해봐야겠습니다~</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/e0abd3b9-722e-462f-8f05-cbee0aff59fb/image.png" alt=""></p>
<p>그럼 어떻게 측정하느냐!
는 아래 영상을 참고하라고 하네요</p>
<p><a href="https://developer.apple.com/videos/play/wwdc2020/10077/">Eliminate animation hitches with XCTest</a>
<a href="https://developer.apple.com/videos/play/wwdc2020/10081/">What&#39;s new in MetricKit</a></p>
<br>

<h2 id="👩🏻💻-정리-그래서-개발자가-할-수-있는-일은">👩🏻‍💻 정리: 그래서 개발자가 할 수 있는 일은?</h2>
<p>frame이 렌더 루프에서 만들어지는 과정과, UI의 성능을 가늠할 수 있는 hitch, hitch time ratio에 대해 알아봤습니다.</p>
<p>이번 영상에서는 구체적인 hitch를 제거하는 방법론에 대해선 이야기 하지 않았는데요, 그래서 다음 이야기가 매우 궁금한 채로 끝이 났습니다....</p>
<p>소개해 준 다른 영상들 보러 갈게요~!</p>
<p><a href="https://velog.io/@yeahg_dev/Instrument%EB%A1%9C-Commit-hitch-%EC%B0%BE%EA%B3%A0-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B0">Commit Hitch를 찾고 제거하는 방법 -&gt; TechTalk: Find and fix hitches in the commit phase</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[HIG] Designing for iOS]]></title>
            <link>https://velog.io/@yeahg_dev/HIG-Designing-for-iOS</link>
            <guid>https://velog.io/@yeahg_dev/HIG-Designing-for-iOS</guid>
            <pubDate>Sun, 07 Aug 2022 10:17:20 GMT</pubDate>
            <description><![CDATA[<p>오늘은 HIG에서 iOS라는 플랫폼을 위한 디자인 가이드를 간략하게 알아보도록 하겠습니다.</p>
<p>원문에서 내용을 추려 저의 해석대로 정리한 글입니다.
따라서 저의 글은 참고 정도 해주시고, 아래 원문의 풀 내용을 읽어보시길 추천드립니다. </p>
<blockquote>
<p><a href="https://developer.apple.com/design/human-interface-guidelines/platforms/designing-for-ios">Designing for iOS</a></p>
</blockquote>
<br>

<h1 id="designing-for-ios📱">Designing for iOS📱</h1>
<blockquote>
<p>사람들은 아이폰에 의존하여 언제 어디서나 연결되고, 게임을 플레이하고, 미디어를 시청하고, 일을 수행하고, 개인 데이터를 추적합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/07d85e65-96ec-4c6a-88da-c11a8011f880/image.png" alt=""></p>
<p>앱을 디자인하기 전 아래의 내용을 먼저 이해하세요</p>
<ul>
<li>기기의 근본적인(fundamental) 특성</li>
<li>iOS 경험을 구분짓는 패턴</li>
</ul>
<p>이러한 특징과 패턴을 사용해서 디자인을 결정하면 아이폰 사용자가 환영할 것입니다.</p>
<h3 id="display">Display</h3>
<p>아이폰은 <strong>중간 사이즈</strong>의 <strong>고해상도</strong> 디스플레이를 갖고 있습니다.</p>
<h3 id="ergonomics-인체-공학">Ergonomics (인체 공학)</h3>
<p>사람들은 아이폰을 한 손 또는 두손으로 들고 사용합니다. 가로와 세로 모드를 전환하면서요. 
기기를 사용할 때, 사용자에게서 30cm에서 60cm(1~2 feet) 떨어진 곳에서 봅니다. </p>
<blockquote>
<p>한 손 또는 두 손으로 조작이 용이하도록!
필요에 따라 가로, 세로모드를 지원하자
표시되는 컨텐츠의 사이즈가 너무 작으면 안되겠다</p>
</blockquote>
<h3 id="inputs">Inputs</h3>
<p>멀티 터치 제스처, 화면 키보드 및 voice control을 사용해 이동 중에도 작업을 수행하고 의미있는 작업을 수행할 수 있습니다. 또한, 기기의 가속도계(accelerometers)와 자이로스코프(gyroscopes)로 부터 얻은 데이터과 위치를 사용하길 원할 수 있고, 공간 상호 작용에 참여하길 원할 수도 있습니다.</p>
<p><strong>가속도계</strong>
물체의 가속도 물리량을 측정하는 장치</p>
<p><strong>자이로스코프</strong> 
방향의 측정에 사용되는 기구</p>
<blockquote>
<p>다양한 장치를 활용해서 다양한 종류의 인풋을 받을 수 있군</p>
</blockquote>
<h3 id="app-interactions">APP interactions</h3>
<p>사람들은 메시지를 보내거나, 소셜미디어 업데이트를 확인하는 것과 같이 앱을 일 이분 동안 잠깐 사용할 수도 있고, 미디어를 보거나 게임을 하는 것처럼 몇 시간을 사용할 수도 있습니다. 그리고 사람들은 여러 앱을 동시에 켜두고 스위칭해가며 사용합니다. </p>
<blockquote>
<p>앱 사용 시간에 따른 적절한 인터페이스 제공하기!
 멀티 태스킹시 불편함이 없도록 사용성 개선하기 </p>
</blockquote>
<h3 id="system-features">System features</h3>
<p>iOS는 사람들이 친숙하고 일관된 방식으로 시스템 및 앱과 상호 작용할 수 있도록 도와주는 기능을 제공합니다. </p>
<p><strong>여기서 말하는 시스템 기능이란?</strong></p>
<ul>
<li>Widgets</li>
<li>Home Screen quick actions</li>
<li>Spotlight</li>
<li>Shortcuts</li>
<li>Activity views</li>
</ul>
<blockquote>
<p>시스템 기능을 적재 적소에 활용해볼 수도 있겠다</p>
</blockquote>
<br>

<h1 id="best-practice">Best Practice</h1>
<p>훌륭한 아이폰 사용 경험은 플랫폼과 기기의 성능(사람들이 가장 중요시 여기는)을 통합합니다. 즉, 플랫폼과 기기의 능력이 시너지를 낼 때 쩌는 사용 경험을 느낄 수 있습니다. </p>
<p>iOS에서 편안함을 느낄 수 있도록 아래 방법을 우선시해서 이런 특징과 기능을 통합하세요</p>
<ul>
<li><p>화면 컨트롤의 수를 제한하여 사용자가 지금 하고 있는 주요 작업 및 컨텐츠에 집중할 수 있도록 지원하세요. 그리고 최소한의 상호작용으로 2차적인 세부 정보 및 작업을 얻을 수 있도록 디자인하세요</p>
</li>
<li><p>모양 변화에 자연스럽게 적응하도록 구현하기. 화면 방향 전환, 다크 모드, 다이나믹 타입과 같이 사람들이 자신에게 가장 맞는 설정을 선택할 수 있도록 하세요.</p>
</li>
<li><p>사람들이 폰을 잡는 방식에서 편안한 인터렉션을 제공하세요. 예를 들어, 컨트롤이 화면의 중간 또는 아래쪽 영역에 있을 때 조작하기 쉽고 편리합니다. 따라서 사람들이 네비게이션 뒤로 이동 하고, 리스트에서 액션을 시작할 수 있도록 지원하는 것이 중요합니다.</p>
</li>
<li><p>사용자의 허락을 받아, 사용자에게 데이터 입력을 요구하지 않고 플랫폼의 기능을 통해 데이터를 통합해서 사용자 경험을 향상시키세요. 예를 들어, 결제를 수락하기 위해 비밀 번호 입력을 받는 대신에 생체 인증을 사용하거나, 장치의 위치를 사용하는 방법이 있습니다. </p>
</li>
</ul>
<br>


<h2 id="정리">정리</h2>
<p>iOS가 제공하는 시스템 기능과 장치의 특징과 기능을 잘 이해하고 활용해서 사용자가 편리하도록 만들자!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Special Case Pattern]]></title>
            <link>https://velog.io/@yeahg_dev/Special-Case-Pattern</link>
            <guid>https://velog.io/@yeahg_dev/Special-Case-Pattern</guid>
            <pubDate>Sun, 07 Aug 2022 09:00:45 GMT</pubDate>
            <description><![CDATA[<h1 id="special-case-pattern-특수-사례-패턴">Special Case Pattern (특수 사례 패턴)</h1>
<hr>
<blockquote>
<p>클래스를 만들거나 객체를 조작해 특수 사례를 처리하는 방식</p>
</blockquote>
<p>클린 코드에서 “정상 흐름을 정의하라&quot;(138p)라는 파트에서 소개된 패턴입니다.</p>
<p>이 패턴이 어떻게 구현되는지, 스위프트에선 어떻게 작성해볼 수 있을지 궁금해졌습니다. 
찾아보니 리팩터링(마틴 파울러 저)이라는 책에서도 소개된 패턴이더라구요.
리팩터링을 정리한 <a href="https://hororolol.tistory.com/623">다른 블로그 글</a>을 참고해보니 다양한 방법으로 구현이 될 수 있더군요!</p>
<p>방법 중 서브클래싱을 이용한 방법이 클린 코드 책에서 예시로 든 비용 청구 애플리케이션의 비용 계산 코드와 동일했습니다. 그래서 해당 코드를 스위프트로 바꾸어 작성해보고 패턴의 구현 방법과 특징에 대해 알아보도록 할게요~!</p>
<hr>
<br>

<h2 id="문제-상황">문제 상황</h2>
<ul>
<li>nil을 반환할 수 있는 코드에서는 런타임 에러가 발생할 수 있습니다. </li>
<li>따라서 nil을 체크하는 조건문을 작성해서 런타임 에러를 방지해야합니다 </li>
<li>그런데 이러한 조건문은 많은 곳에서 <strong>중복적인 코드를 만들어냅니다.</strong> </li>
</ul>
<p>또는 </p>
<ul>
<li>에러를 던지는 특이 케이스의 경우에도 호출자에서 try catch문을 중복해서 사용하게 됩니다.</li>
<li>그리고 이러한 코드는 <strong>논리를 따라가기 어렵게 만듭니다.</strong></li>
</ul>
<p>위 상황이 중복적으로 발생한다면 특이 케이스 패턴을 도입을 고민해볼 수 있습니다!</p>
<br>

<p>예시 코드를 볼게요</p>
<p>비용을 기록하는 <code>ExpenseReportDAO</code> 라는 타입이 있습니다.
직원이 식대 비용을 청구하면 <code>Expense</code>라는 타입을 반환하고,
청구하지 않아 ID가 <code>mealExpenseForEmployee</code>에 없으면 <code>mealExpenseNotFound</code>에러를 던집니다.</p>
<pre><code class="language-swift">class ExpenseReportDAO {

    var mealExpenseForEmployee: [Int: Expense] = [:] 

    func getMeals(of employeeID: Int) throws -&gt; Expense {
        // 비용을 청구한 경우
        if let expense = mealExpenseForEmployee[employeeID] {
            return  expense
        }
        // 비용을 청구하지 않은 경우
        throw ExpenseReportError.mealExpenseNotFound
    }

}

enum ExpenseReportError: Error {

    case mealExpenseNotFound
}


class Expense {

    func getTotal() -&gt; Int {
        // 비용의 총합을 리턴
        return 10000
    }

}
</code></pre>
<p>위 함수를 호출하는 코드입니다.
<code>mealExpenseNotFound</code>에러가 발생하면 일일 기본 식비를 총계에 더합니다.</p>
<pre><code class="language-swift">
let exportReportDAO = ExpenseReportDAO()
var total = 0

do {
    let expense = try exportReportDAO.getMeals(of: 5) 
    total += expense.getTotal()
} catch ExpenseReportError.mealExpenseNotFound {
    total += getDailyMealExpense() // 일일 기본 식비 리턴
}
</code></pre>
<p>try catch문을 사용해서 가독성이 떨어지고 작가의 말을 빌리면 논리를 따라가기 힘들게 만드네요..!
Special Case Pattern을 사용해서 개선해보도록 할게요~</p>
<br>


<h2 id="💡-special-case-pattern을-사용하면">💡 Special Case Pattern을 사용하면?</h2>
<h3 id="방법">방법</h3>
<blockquote>
<p><code>nil</code> 을 반환하거나 이상한 값(에러)을 반환하는 대신 정상 객체의 서브 클래스 special case를 반환한다.</p>
</blockquote>
<ul>
<li>중복해서 나타나는 특이 케이스에 대해 서브클래스 생성</li>
</ul>
<pre><code class="language-swift">class BasicDailyMealExpense: Expense {

    func getTotal() -&gt; Int {
        // 일일 기본 식비를 반환
        return 9000
    }

}</code></pre>
<ul>
<li><p>특이 케이스가 발생하는 상황에서 특이 케이스 서브클래스를 리턴</p>
<pre><code class="language-swift">class ExpenseReportDAO {

  func getMeals(of employeeID: Int) -&gt; Expense {
      if let expense = mealsForEmployee[employeeID] {
          return  expense
      }
      return  BasicDailyMealExpense()
  }
</code></pre>
</li>
</ul>
<p>}</p>
<pre><code>- 호출자에서 예외 처리를 하지 않고, 동일한 인터페이스 사용

```swift
let expense = exportReportDAO.getMeals(of: 5) 
total += expense.getTotoal()</code></pre><br>

<h3 id="효과">효과</h3>
<ul>
<li>호출자에서 예외를 확인하지 않고 단순하게 함수를 호출할 수 있다.</li>
<li>특히 많은 클라이언트 단에서 예외를 발생시키는 함수를 사용한다고 했을 때, 중복 코드를 방지할 수 있다</li>
<li>가독성이 올라간다</li>
<li><code>nil</code>로 인한 런타임 에러 방지~ (Swift에서는 옵셔널 체이닝 제공되기 때문에, 더 안전한 코드를 작성할 수  있다)</li>
</ul>
<br>

<h2 id="정리">정리</h2>
<p>모든 에러 처리나 특이 케이스에 적용할 수 있는 패턴은 아니라고 생각했습니다.
<code>nil</code> 이나 에러대신 반환 할 수 있는 대체값이 있는 경우에만 사용할 수 있기 때문입니다. </p>
<p>하지만, 적용이 가능한 상황이라면 적극! 도입해보고 싶네요!!!</p>
<br>


<h3 id="references">References</h3>
<p>마틴 파울러 블로그에 소개된 <a href="https://martinfowler.com/eaaCatalog/specialCase.html">Special Case</a>
리팩터링 책에 소개된 SpecialCasePattern을 정리한 <a href="https://hororolol.tistory.com/623">블로그</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RxSwift] Traits 1편: Control Property/Control Event]]></title>
            <link>https://velog.io/@yeahg_dev/rxSwfit-Traits-1%ED%8E%B8-Control-PropertyControl-Event</link>
            <guid>https://velog.io/@yeahg_dev/rxSwfit-Traits-1%ED%8E%B8-Control-PropertyControl-Event</guid>
            <pubDate>Sun, 10 Jul 2022 11:01:59 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/198ff4a2-7c15-4902-b137-add1c8115830/image.png" alt=""></p>
<p><a href="https://github.com/ReactiveX/RxSwift/blob/main/Documentation/Traits.md">https://github.com/ReactiveX/RxSwift/blob/main/Documentation/Traits.md</a></p>
<h1 id="traits">Traits</h1>
<hr>
<p>RxSwift의 Observable에 contextual meaning을 제공 + 특정 usecase에서 활용되도록 구현한 문법적 슈가. 
문법적 슈가이므로 Trait대신 Observable을 사용해도 아무 문제 없다.</p>
<p>Observable이 광범위하게 사용된다면, Trait은 <strong>특정 상황</strong>에서만 사용되는 Observable이다.
오늘 살펴볼 <code>ContorlProperty</code>와 <code>ContorlEvent</code>는 UI라는 특정 상황에서만 사용된돠</p>
<h2 id="어떻게-작동할까">어떻게 작동할까?</h2>
<p>Trait은 Observable 시퀀스를 프로퍼티로 가지는 Wrapper 구조체이다.</p>
<pre><code class="language-swift">struct Single&lt;Element&gt; {
    let source: Observable&lt;Element&gt;
}

struct Driver&lt;Element&gt; {
    let source: Observable&lt;Element&gt;
}
...</code></pre>
<p>Trait에서 <code>.asObservable()</code>을 호출하면 다시 원본 Observable로 변환해준다.</p>
<br>

<p><code>Control Property</code>, <code>Control Event</code>는 모두 rxCocoa에 구현되어 있는 Trait이다.</p>
<h1 id="control-property">Control Property</h1>
<blockquote>
<p>UI element의 property를 나타내는 Observable </p>
</blockquote>
<p>Control Property는 <strong>최초의 UI value</strong>와 <strong>사용자가 트리거한 UI value change</strong>를 next로 받는다. 코드로 조작한 value change는 감지하지 못한다. </p>
<ul>
<li><code>share(replay: 1)</code> 로 작동</li>
<li><code>controlProperty</code> 가 메모리 해제될 때 complete된다</li>
<li><code>error</code>를 절대 내보내지 않는다</li>
<li>이벤트를 <code>MainScheduler.instance</code>로 보낸다. </li>
<li>시퀀스의 이벤트가 메인 스케줄러에서 구독되는 것을 보장한다. (<code>subscribeOn(ConcurrentMainScheduler.instance)</code> behavior)</li>
</ul>
<br>

<h1 id="control-event">Control Event</h1>
<blockquote>
<p>UI element의 event를 나타내는 Observable </p>
</blockquote>
<ul>
<li>절대 실패하지 않는다</li>
<li>구독시 어떠한 초기 값도 이벤트로 전달하지 않음</li>
<li><code>controlEvent</code> 가 메모리 해제될 때 complete된다</li>
<li><code>error</code>를 절대 내보내지 않는다</li>
<li>이벤트를 <code>MainScheduler.instance</code>로 보낸다. </li>
<li>시퀀스의 이벤트가 메인 스케줄러에서 구독되는 것을 보장한다. (<code>subscribeOn(ConcurrentMainScheduler.instance)</code> behavior)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RxSwift] 에러 핸들링 :  catch, retry]]></title>
            <link>https://velog.io/@yeahg_dev/rxSwift-%EC%97%90%EB%9F%AC-%ED%95%B8%EB%93%A4%EB%A7%81%EC%9D%84-%ED%95%98%EB%8A%94-%EB%8B%A4%EC%96%91%ED%95%9C-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@yeahg_dev/rxSwift-%EC%97%90%EB%9F%AC-%ED%95%B8%EB%93%A4%EB%A7%81%EC%9D%84-%ED%95%98%EB%8A%94-%EB%8B%A4%EC%96%91%ED%95%9C-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Sun, 26 Jun 2022 14:18:24 GMT</pubDate>
            <description><![CDATA[<p>기본적으로 옵저버블에서 <code>error</code>가 방출되면 해당 옵저버블을 구독하고 있는 스트림은 모두 종료됩니다. 따라서 더 이상 <code>next</code> 이벤트가 방출되지 않습니다.</p>
<p>그럼 <code>error</code>가 방출되었을 때 에러 핸들링을 어떻게 해줄 것인지,
<code>error</code>가 방출되어도 스트림을 종료시키지 않고 이벤트를 받을 수 있는 방법은 무엇이 있을지
알아보도록 하겠습니다!</p>
<p>go go</p>
<br>

<h2 id="catch-catchandreturn"><code>catch</code>, <code>catchAndReturn</code></h2>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/5998845d-ca72-4bd3-b6a0-3bfa932cfdef/image.png" alt=""></p>
<p><code>error</code>가 방출되면 다른 아이템으로 옵저버블 시퀀스를 생성해 <code>onError</code>대신 <code>onNext</code>를 방출하고 <code>completed</code> 됩니다. <code>error</code>대신 다른 <code>item</code>을 내보내고 종료시키는 방법입니다. </p>
<h3 id="catch_-handler-"><code>catch(_ handler: )</code></h3>
<p><code>handler</code>에서 다른 <code>item</code>을 <code>onNext</code>로 내보내는 새로운 옵저버블을 생성해서 리턴합니다.</p>
<pre><code class="language-swift">output.products
            .catch({ _ in
                return Observable.just([]) // 에러 발생시 [] 전달
            })
            .observe(on: MainScheduler.instance)
            .bind(to: tableView.rx.items(cellIdentifier: &quot;ProductTableViewCell&quot;, cellType: ProductTableViewCell.self)) { (row, element, cell) in
                cell.fill(with: element)}
            .disposed(by: disposeBag)</code></pre>
<h3 id="catchandretunrn_-element-"><code>catchAndRetunrn(_ element: )</code></h3>
<p><code>error</code> 대신 내보낼 <code>item</code>을 <code>element</code> 넣어줍니다.</p>
<pre><code class="language-swift">output.products
            .catchAndReturn([]) // 에러 발생시 빈배열 전달
            .observe(on: MainScheduler.instance)
            .bind(to: tableView.rx.items(cellIdentifier: &quot;ProductTableViewCell&quot;, cellType: ProductTableViewCell.self)) { (row, element, cell) in
                cell.fill(with: element)}
            .disposed(by: disposeBag)</code></pre>
<br>

<h2 id="retry-retry_-maxattemptcount-retrywhen"><code>retry</code>, <code>retry(_ maxAttemptCount:)</code>, <code>retry(when:)</code></h2>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/658e17e7-8efb-4af6-854f-31d8c19370f4/image.png" alt="">
옵저버블에서 <code>error</code>가 방출되었을 때, 옵저버블을 구독하고 있는 시퀀스를 <code>dispose</code>시키고 다시 <code>subscribe</code>을 합니다. 이때 <code>error</code>는 옵저버에게 전달되지 않습니다. (따라서 <code>onNext</code>만 옵저버에게 전달되는 것이 보장됩니다)</p>
<p><code>retry()</code>를 사용했을 때 옵저버블의 상태변화를 <code>debug()</code> 로 찍어서 확인해봤습니다.</p>
<pre><code>2022-06-20 17:07:40.715: JuiceMakerViewModel.swift:50 (transfrom(input:)) -&gt; Event error(juiceProductionFailure)
2022-06-20 17:07:40.724: JuiceMakerViewModel.swift:50 (transfrom(input:)) -&gt; isDisposed
2022-06-20 17:07:40.724: JuiceMakerViewModel.swift:50 (transfrom(input:)) -&gt; subscribed</code></pre><br>

<p><code>retry</code>는 3가지 종류가 있습니다. 
<code>retry</code>는 <code>onNext</code>를 받을 때까지 무제한 구독을, 
<code>retry(_ maxAttemptCount:)</code>는 제한된 횟수만큼 구독을, 
<code>retry(when:)</code>은 특정 이벤트가 발생할 때만 구독을 합니다. </p>
<h3 id="retry"><code>retry</code></h3>
<p><code>error</code>가 방출되면, 스트림을 <code>dispose</code>하고 다시 <code>subscribe</code>합니다. 
에러가 발생할 때마다, 성공적으로 종료(<code>onComplete</code>)될 때까지 무한정 재구독을 해주는 연산자입니다.</p>
<p>그래서 유의해서 사용해야합니다!! 특히 통신을 포함하는 스트림의 경우,<code>error</code>대신 올바른 response를 받을 때까지 계속 통신을 시도하므로 유의해서 사용해야합니다.</p>
<h3 id="retry_-maxattemptcount"><code>retry(_ maxAttemptCount:)</code></h3>
<p><code>retry</code>는 재구독을 하는 횟수에 제한이 없었다면, <code>retry(maxAttemptCount:)</code>는 정해진 횟수만큼 <code>retry</code>를 해줍니다.
<code>maxAttemptCount</code>에 제한 횟수를 넣어줍니다.
마지막 찬스에도 에러가 방출되면 옵저버에게 <code>onError</code>이벤트가 전달되고 스트림은 종료됩니다.</p>
<h3 id="retrywhen-notificationhandler"><code>retry(when notificationHandler:)</code></h3>
<p>어떠한 옵저버블에서 <code>next</code>가 방출될 때 다시 구독을 합니다.</p>
<p>예를 들어 특정한 버튼이 눌렸을 때만 구독을 시켜주고 싶다고 한다면 <code>notificationHandler</code>에 버튼이 탭되었을 때 <code>onNext</code>를 보내는 옵저버블을 넣어주면 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Hashable]]></title>
            <link>https://velog.io/@yeahg_dev/Hashable-egc0vnmr</link>
            <guid>https://velog.io/@yeahg_dev/Hashable-egc0vnmr</guid>
            <pubDate>Wed, 22 Jun 2022 04:07:26 GMT</pubDate>
            <description><![CDATA[<h1 id="hashable">Hashable</h1>
<blockquote>
<p>Hasher에게 값을 제공해서 정수 hash value를 생성할 수 있는 타입</p>
</blockquote>
<p>여기서 hasher, hash value는 무슨 뜻 일까요?</p>
<ul>
<li><p><a href="https://developer.apple.com/documentation/swift/hashable/hashvalue">hash value</a> : 해시 함수에 의해 얻어지는 값</p>
</li>
<li><p><code>hash function</code> : 임의의 길이의 데이터를 고정된 길이의 데이터로 매핑하는 함수</p>
<ul>
<li>동일한 입력값에 대해 동일한 출력값을 내보낸다</li>
<li>입력값이 아주 조금만 바뀌어도 출력값은 어마무시하게 많이 달라진다</li>
<li>항상 단방향으로만 움직인다</li>
</ul>
</li>
<li><p><a href="https://developer.apple.com/documentation/swift/hasher">Hasher</a> : Set과 Dictionary에서 사용되는 해시 함수를 가지는 구조체</p>
</li>
</ul>
<p>해시 함수, 해시값은 데이터의 유일한 식별, 암호화에서 많이 활용됩니다.</p>
<br>


<h2 id="구현">구현</h2>
<p><code>Hashable</code>을 채택하면 <code>Hasher</code>에게 input을 제공해 해시 함수로 <code>hash value</code>를 만들 수 있다고 했습니다. 
<code>Hasher</code>에게 input을 제공하는 함수가 <code>hash(into:)</code>입니다.</p>
<p><code>Hashable</code>을 채댁하면 <code>hash(into:)</code>를 필수로 구현해야합니다. 
<code>hash(into:)</code>는 타입의 essential component를 <code>Hasher</code>에게 전달합니다.
이것 가지고 hash value만들어줘~ 하는 거죠.</p>
<blockquote>
<pre><code class="language-swift">func hash(into hasher: inout Hasher)</code></pre>
</blockquote>
<br>

<h3 id="예시">예시</h3>
<pre><code class="language-swift">struct GridPoint {
    var x: Int
    var y: Int
}

extension GridPoint: Hashable {
    static func == (lhs: GridPoint, rhs: GridPoint) -&gt; Bool {
        return lhs.x == rhs.x &amp;&amp; lhs.y == rhs.y
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(x)
        hasher.combine(y)
    }
}</code></pre>
<p>x, y로 좌표를 정의하는 GridPoint구조체가 있습니다. </p>
<p><code>hash(into:)</code>에서 <code>hasher</code>에게 <code>x</code>, <code>y</code>를 넘겨줍니다. 
<code>combine(_:)</code> 전달 받은 값을 <code>hash value</code>를 만들때 사용할 인풋으로 추가합니다. <code>x</code>,<code>y</code>처럼 여러개의 요소를 섞어서 hash value를 만들 수 있나봅니다. </p>
<br>

<h2 id="equatable과-hashable의-관계">Equatable과 Hashable의 관계</h2>
<p>그런데 공식문서에서는 해시에 사용되는 요소가 <code>==</code>연산자 구현에서 비교할 때 사용되는 요소와 동일해아한다고 말합니다. </p>
<blockquote>
<p>The components used for hashing must be the same as the components compared in your type’s == operator implementation.</p>
</blockquote>
<p>그리고 <code>Hashable</code>은 <code>Equatable</code>을 상속합니다.</p>
<blockquote>
<pre><code class="language-swift">protocol Hashable : Equatable</code></pre>
</blockquote>
<p>왜 그럴까요?</p>
<br>
이에 대한 이유는 공식문서에 나와있진 않습니다만,
다양한 추측들이 존재하네요!

<p><a href="https://forums.swift.org/t/why-does-hashable-require-equatable/16817/3">https://forums.swift.org/t/why-does-hashable-require-equatable/16817/3</a></p>
<p>개인적으로는 해시값이 객체를 식별할 수 있는 유일한 값을 제공해주는데, 비교 자체가 안된다면 의미가 없기때문에 Equatable을 상속해서 비교가 가능하게하고, 동일한 객체는 동일한 해시값을 갖도록 한것 아닌가 싶네요</p>
<br>

<h2 id="hashable이-자동으로-구현되는-경우">Hashable이 자동으로 구현되는 경우</h2>
<ol>
<li><p>Swift의 <code>String</code>, <code>Double</code>, <code>Bool</code>, <code>Int</code>등 데이터 타입이 <code>Hashable</code>구현되어 있습니다.</p>
</li>
<li><p>아래의 특징을 가진 타입은 <code>Hashable</code>만 상속하면 컴파일러가 자동으로 <code>Hashable</code>을 구현합니다. </p>
</li>
</ol>
<ul>
<li>모든 저장 프로퍼티가 <code>Hashable</code> 한 구조체</li>
<li>associatee value가 없는 열거형</li>
<li>모든 associatee value가 <code>Hashable</code> 한 열거형</li>
</ul>
<br>

<h2 id="🍟-hash와-hashbrown의-관계">🍟 Hash와 hashbrown의 관계</h2>
<p>해시하면 해시브라운이 자동으로 떠오르는데요.</p>
<ul>
<li>동일한 감자에 대해 동일한 해시브라운을 내보낸다</li>
<li>감자가 아주 조금만 바뀌어도 해시브라운은 어마무시하게 많이 달라진다</li>
<li>감자에서 튀김만 될 수 있다. 해시브라운에서 감자는 될 수 없다</li>
</ul>
<p>네.</p>
<p>해시브라운은 해시라는 이름을 얻을 자격이 있네요</p>
<br>

<h3 id="references">References</h3>
<p>해시가 뭔지 쉽게 설명해줌
<a href="https://www.youtube.com/watch?v=67UwxR3ts2E">https://www.youtube.com/watch?v=67UwxR3ts2E</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RxCocoa] TableView에 Observable 바인드하는 방법]]></title>
            <link>https://velog.io/@yeahg_dev/rxCocoa-TableView%EC%97%90-Observable-%EB%B0%94%EC%9D%B8%EB%93%9C%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@yeahg_dev/rxCocoa-TableView%EC%97%90-Observable-%EB%B0%94%EC%9D%B8%EB%93%9C%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Sun, 19 Jun 2022 10:07:55 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요~!</p>
<p>오늘은 rxCocoa를 사용해 <code>UITableView</code> 에 데이터를 어떻게 뿌려주는지! 그 방법들에 대해 알아보겠습니다🧐</p>
<br>

<p>단일 데이터를 <code>UILabel</code>이나 <code>UITextView</code> 에 바인드 시켜주는건 그렇게 어렵지 않았는데요.
그런데 UITableView, UITableViewDataSource의 메서드들은 Observable과 어떻게 엮어서 사용해야할지...?!?!? 는 전혀 감이 안잡히더라구요.</p>
<p>찾아보니 <code>rxCocoa</code>와 <code>rxDataSource</code>를 사용하면 간편하게 바인드할 수 있었는데요, 그중에서도 <code>rxCocoa</code>의 메서드에 대해 공부해보았습니다.</p>
<p><code>rxCocoa</code>에서는 3가지 메서드를 제공해주는데, 그 중 2개 메서드의 사용방법과 차이점에 대해 알아보도록 하겠습니다.</p>
<blockquote>
<p>🌟 rxCocoa 소스 코드
<a href="https://github.com/ReactiveX/RxSwift/blob/main/RxCocoa/iOS/UITableView%2BRx.swift">https://github.com/ReactiveX/RxSwift/blob/main/RxCocoa/iOS/UITableView%2BRx.swift</a></p>
</blockquote>
<br>





<h2 id="첫번째-방법">첫번째 방법</h2>
<pre><code class="language-swift">public func items&lt;Sequence: Swift.Sequence, Source: ObservableType&gt;
        (_ source: Source)
        -&gt; (_ cellFactory: @escaping (UITableView, Int, Sequence.Element) -&gt; UITableViewCell)
        -&gt; Disposable
        where Source.Element == Sequence {
            return { cellFactory in
                let dataSource = RxTableViewReactiveArrayDataSourceSequenceWrapper&lt;Sequence&gt;(cellFactory: cellFactory)
                return self.items(dataSource: dataSource)(source)
            }
    }</code></pre>
<p>중첩된 구조의 메서드 형태입니다.  </p>
<p>각 파라미터의 역할입니다. 
<code>source</code> : Observable sequence of items
<code>cellFactory</code> : <code>element</code>를 이용해 <code>cell</code>을 configure</p>
<p><code>Disposable</code>을 리턴합니다.</p>
<p>구현은 복잡한데 사용은 어렵지 않습니다🙂</p>
<h3 id="example">Example</h3>
<p>먼저 테이블 뷰 셀에 제공할 데이터 배열을 Observable로 만들어줍니다. </p>
<pre><code class="language-swift">let items = Observable.just([
             &quot;First Item&quot;,
             &quot;Second Item&quot;,
             &quot;Third Item&quot;
         ])</code></pre>
<p><code>items.bind(to: _)</code> 에 <code>바인딩할테이블뷰.rx.items</code> 를 넣고, 
메서드의 후행클로저에 <code>tableView</code>, <code>row</code>, <code>element</code>를 이용해서 cell을 configure해줍니다. </p>
<pre><code class="language-swift">    items.bind(to: tableView.rx.items) { (tableView, row, element) in
             let cell = tableView.dequeueReusableCell(withIdentifier: &quot;Cell&quot;)!
             cell.textLabel?.text = &quot;\(element) @ row \(row)&quot;
             return cell
         }
         .disposed(by: disposeBag)</code></pre>
<p>후행클로저에서 제공되는 파라미터 중<code>tableView</code>에는 제가 위에서 넣어준 바인딩할 테이블뷰, <code>row</code>는 deque될 cell의 row의 <code>Int</code>값, <code>element</code>는 데이터 타입(<code>String</code>)이 들어갑니다.  </p>
<p><code>row</code>마다 다른 cell을 리턴해주어야할 때 사용할 수 있겠네요</p>
<h3 id="프로젝트-적용">프로젝트 적용</h3>
<pre><code class="language-swift">// output: Observable&lt;[ProductViewModel]&gt;
output.products
            .bind(to: tableView.rx.items) { (tableView, row, element) in
                guard let cell = tableView.dequeueReusableCell(withIdentifier: &quot;ProductTableViewCell&quot;) as? ProductTableViewCell else{
                    return ProductTableViewCell()
                }
                cell.fill(with: element)
                return cell
            }
            .disposed(by: disposeBag)</code></pre>
<br>


<h2 id="두번째-방법">두번째 방법</h2>
<p>두번재 방법은 첫번째 방법보다 더 진화된 방법입니다. 리턴할 <code>Cell</code>의 타입을 파라미터로 받아서 타입캐스팅 까지 해준 <code>Cell</code>을 반환합니다.</p>
<pre><code class="language-swift">  public func items&lt;Sequence: Swift.Sequence, Cell: UITableViewCell, Source: ObservableType&gt;
        (cellIdentifier: String, cellType: Cell.Type = Cell.self)
        -&gt; (_ source: Source)
        -&gt; (_ configureCell: @escaping (Int, Sequence.Element, Cell) -&gt; Void)
        -&gt; Disposable
        where Source.Element == Sequence {
        return { source in
            return { configureCell in
                let dataSource = RxTableViewReactiveArrayDataSourceSequenceWrapper&lt;Sequence&gt; { tv, i, item in
                    let indexPath = IndexPath(item: i, section: 0)
                    // cellType으로 강제 캐스팅
                    let cell = tv.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! Cell
                    configureCell(i, item, cell)
                    return cell
                }
                return self.items(dataSource: dataSource)(source)
            }
        }
    }</code></pre>
<h3 id="example-1">Example</h3>
<p>1번 방법과 동일하게 셀에 바인드할 데이터 배열의 Observable을 준비합니다.</p>
<pre><code class="language-swift">  let items = Observable.just([
             &quot;First Item&quot;,
             &quot;Second Item&quot;,
             &quot;Third Item&quot;
         ])</code></pre>
<p>1번 방법과 동일하게 첫번째 파라미터에 <code>바인딩할 테이블뷰.rx.item</code> 를 넣고, <code>cellIdentifer</code> 에 Cell Identifier, <code>cellType</code>에 커스텀 셀 타입을 명시합니다.
그리고 후행 클로저에서 제공된 <code>row</code> , <code>element</code>를 사용해 <code>cell</code>을 configure합니다. </p>
<pre><code class="language-swift">        items.bind(to: tableView.rx.items(cellIdentifier: &quot;Cell&quot;, cellType: UITableViewCell.self)) { (row, element, cell) in
                cell.textLabel?.text = &quot;\(element) @ row \(row)&quot;
             }
             .disposed(by: disposeBag)</code></pre>
<h3 id="프로젝트-적용-1">프로젝트 적용</h3>
<pre><code class="language-swift">// output: Observable&lt;[ProductViewModel]&gt;
  output.products
            .bind(to: tableView.rx.items(cellIdentifier: &quot;ProductTableViewCell&quot;, cellType: ProductTableViewCell.self)) { (row, element, cell) in
                // cell: ProductTableViewCell
                cell.fill(with: element)}
            .disposed(by: disposeBag)</code></pre>
<br>

<h2 id="정리">정리</h2>
<ul>
<li>첫번째 방법: 직접 테이블뷰에서 셀을 디큐한 다음, <code>element</code>로 셀을 configure, 그리고 cell을 리턴한다. </li>
<li>두번째 방법: 명시한 cellType의 cell을 받아서, <code>element</code>로 configue, 그리고 cell을 리턴한다. (단 강제 캐스팅을 하므로 <code>cellType</code>이 틀리면 크래시남 주의)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Domain과 Entity의 개념]]></title>
            <link>https://velog.io/@yeahg_dev/Domain%EA%B3%BC-Entity%EC%9D%98-%EA%B0%9C%EB%85%90</link>
            <guid>https://velog.io/@yeahg_dev/Domain%EA%B3%BC-Entity%EC%9D%98-%EA%B0%9C%EB%85%90</guid>
            <pubDate>Tue, 14 Jun 2022 10:34:05 GMT</pubDate>
            <description><![CDATA[<p>MVVM + 클린 아키텍처를 공부하던 중, <strong>DTO(Data Transfer Object)</strong>라는 개념에 대해 알게되었습니다. 
설명 중 &quot;DTO는 Domain(Entity)를 다른 레이어간에 주고 받을 때 Domain의 비즈니스 로직을 캡슐화하기 위해 사용된다&quot;라는 글을 읽던 와중 Domain? Entity? Model?의 개념은 정확히 어떤 것인지에 대한 궁금증이 생겼습니다. 그리고 클린 아키텍처에서도 Domain, Entity라는 개념이 사용되는데, 정확한 뜻을 모르고 대략 넘겨짚었어서....🥲</p>
<p>그래서 오늘은 Domain, Entity가 각각 어떤 개념인지 알아보려고 합니다!</p>
<blockquote>
<p>학습하며 작성한 글이라 부족한 점이 많습니다. 
🌷피드백, 의견 나눔은 늘 환영입니다🌷</p>
</blockquote>
<br>

<h2 id="layered-architecture-계층화-아키텍처">Layered Architecture (계층화 아키텍처)</h2>
<p>먼저 DTO를 공부하다가 알게된 Layered Architecture, 계층화 아키텍쳐에 대해 잠깐 알아보도록 하겠습니다. </p>
<p>DTO는 계층간 데이터 교환을 위해 사용되는 객체로, 즉 계층으로 이루어진 아키텍처인 Layered Architecture에서 사용됩니다. </p>
<p>layered architecture도 아키텍처 스타일 중 하나로, <strong>비슷한 기능의 모듈과 컴포넌트를 수평한 레이어로 구성</strong>하여, <strong>각 레이어는 애플리케이션에서 특정할 역할을 수행</strong>합니다. <strong>각 레이어별로 관심사를 분리</strong>하는데 목적이 있습니다. 관심사 분리를 통해 각 레이어간 독립적 개발을 용이하게 하고, 설계의 유연성을 높일 수 있습니다. </p>
<h3 id="클린-아키텍처도-layered-architecture일까">클린 아키텍처도 layered architecture일까?</h3>
<p>layered architecture를 검색해보면 3-tier 아키텍처가 가장 많이 눈에 띕니다. 3-tier 아키텍처란 <code>presentation layer</code>, <code>businessLogic layer</code> , <code>data access layer</code> 3개의 레이어로 구성된 아키텍처입니다. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/327eb479-067e-4f3d-a02b-c8c751c26aa8/image.png" alt=""></p>
<p>그럼 클린 아키텍처도 layered architecture일까요?</p>
<p>네 맞습니다. 클린 아키텍처도 layered architecture를 구현한 아키텍처 중 하나입니다. </p>
<p>그러나 3-tier와는 다르게 DomainLayer(Entity)를 계층의 중심에 두고 있는 형태입니다. 
<img src="https://velog.velcdn.com/images/yeahg_dev/post/90b2443c-ea2b-422e-b9d4-f10d722420a8/image.png" alt=""></p>
<br>

<h2 id="domain">Domain</h2>
<p>먼저, 위키백과의 정의를 살펴보겠습니다. </p>
<blockquote>
<p>도메인은 일반적인 요구사항, 전문 용어, 그리고 컴퓨터 프로그래밍 분야에서 문제를 풀기 위해 설계된 어떤 소프트웨어 프로그램에 대한 기능성을 정의하는 연구의 한 영역이다. 도메인 엔지니어링이라고도 알려져 있다.</p>
</blockquote>
<p>도메인은 &quot;<strong>기획의 요구사항을 구현하고, 문제를 풀기 위해 설계된 소프트웨어 프로그램의 기능성을 정의하는 영역</strong>&quot;이라고 설명합니다.</p>
<p>잘 와닿지 않았는데, <a href="https://javacan.tistory.com/entry/what-is-a-domain-model">다른 블로그</a>에서 이해하기 쉽게 풀어내고 있어 설명을 가져와봤습니다. </p>
<p>블로그에 따르면 <strong>앱의 기능, (기획의) 요구사항을 개발하는 영역</strong>을 도메인이라고 정의하고 있습니다. 예를 들어 택시 앱이라면, 도메인은 기사님께 콜을 요청하고, 탑승하고, 요금을 지불하는 과정을 포함하고 있다고 합니다. 또한 도메인 레이어의 결과를 <strong>도메인 모델</strong>이라 하며, 도메인 모델을 가지고 기획자와 개발팀이 요구사항을 올바르게 이해했는지 확인할 수 있고, 의사소통의 도구로도 사용될 수 있습니다.</p>
<br>

<p>즉, 도메인이란 &quot;사용자가 이용하는 앱의 기능, 회사의 비즈니스 로직을 정의하고 있는 영역&quot;이라고 이해해 볼 수 있겠습니다.</p>
<br>


<h2 id="entity">Entity</h2>
<p>Entity는 일반적으로 존재하는 것, 즉 실체를 의미합니다. </p>
<p>다양한 컨텍스트에서 Entity의 의미는 다양하게 구체화 되어 사용됩니다. 즉 사용되는 상황에 따라 다른 의미를 갖고 있습니다. <a href="https://linuxism.ustd.ip.or.kr/m/45">Entitiy의 다양한 해석</a></p>
<h3 id="entity-in-dto">Entity in DTO</h3>
<blockquote>
<ol>
<li>실제 DB의 테이블과 매핑되는 객체. identifier로 구분된다.(DB관련 영속성 엔티티)</li>
<li>비지니스 로직을 포함하는 도메인 엔티티</li>
</ol>
</blockquote>
<p>DTO는 Entity의 변경을 최소화하기 위해 탄생했습니다. 따라서 DTO에서 Entity를 사용할 때, 도메인 엔티티가 될 수도, DB의 영속성 엔티티가 될수도 있습니다.</p>
<h3 id="entity-in-클린-아키텍처">Entity in 클린 아키텍처</h3>
<blockquote>
<p>전사적 비즈니스 규칙을 캡슐화합니다. 메서드를 가지는 객체가 될 수 도 있고, 데이터 구조체, 함수가 될 수도 있습니다. 가장 높은 수준의 규칙을 캡슐화하고 외부 변화에 의해 가장 변하지 않는 부분입니다. </p>
</blockquote>
<p>비즈니스 로직을 캡슐화한 객체라는 의미로 사용됩니다. </p>
<br>

<h2 id="마무리">마무리</h2>
<p>프로그램 아키텍처, 사용언어에 따라 조금씩 의미가 달라질 수 있어서 사용되는 컨텍스트를 고려해 해석해야한다는 걸 배웠습니다! 대략적인 개념은 잡았지만, 명쾌하게 풀린 것 같지 않네요ㅠㅠ</p>
<p>ios외의 개발 영역(java, Spring등)의 기술 블로그도 찾아보며, 플랫폼에 상관없이 프로그래밍은 동일한 관심사들을 공유하고 있다는 걸 다시 느겼던 흥미로운 시간이었네요 😊</p>
<br>


<h3 id="📚-reference">📚 Reference</h3>
<p>layered architecture란?
<a href="https://cs.uwaterloo.ca/~m2nagapp/courses/CS446/1195/Arch_Design_Activity/Layered.pdf">https://cs.uwaterloo.ca/~m2nagapp/courses/CS446/1195/Arch_Design_Activity/Layered.pdf</a>
3tier와 클린아키텍처를 구현하고 비교한 글
<a href="https://betterprogramming.pub/comparing-three-layered-and-clean-architecture-for-web-development-533bda5a1df0">https://betterprogramming.pub/comparing-three-layered-and-clean-architecture-for-web-development-533bda5a1df0</a>
Domain이란?
<a href="https://javacan.tistory.com/entry/what-is-a-domain-model">https://javacan.tistory.com/entry/what-is-a-domain-model</a>
Object와 Entity, Entity의 다양한 용례
<a href="https://linuxism.ustd.ip.or.kr/45">https://linuxism.ustd.ip.or.kr/45</a>
Clean Architecture - Clean Coder Blog
<a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html">https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html</a>
엔티티 또는 도메인객체를 DTO와 분리해야하는 이유
<a href="https://mangkyu.tistory.com/192">https://mangkyu.tistory.com/192</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[WWDC22] Get more mileage out of your app with CarPlay 🚘]]></title>
            <link>https://velog.io/@yeahg_dev/WWDC22-Get-more-mileage-out-of-your-app-with-CarPlay</link>
            <guid>https://velog.io/@yeahg_dev/WWDC22-Get-more-mileage-out-of-your-app-with-CarPlay</guid>
            <pubDate>Fri, 10 Jun 2022 10:17:49 GMT</pubDate>
            <description><![CDATA[<p>Carplay. 아직 면허도 없어서 사용해보진 못했지만🥲, 
앱과 차량이 어떻게 상호작용하고, 뷰는 어떻게 그려주는지 궁금하더라구요.
앞으로 기능이 확대되고 저의 미래에 유용하게 쓰일 기술같아 보여 이번 WWDC22의 Carplay관련 세션을 살펴보고자 합니다🤗</p>
<br>

<blockquote>
<p>WWDC22 영상
 <a href="https://developer.apple.com/videos/play/wwdc2022/10016/">Get more mileage out of your app with CarPlay</a></p>
</blockquote>
<br>

<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/b2468c51-fc95-47e2-80dc-2ae0d5a01b7b/image.png" alt=""></p>
<p>카플레이는 차에서 아이폰을 안전하고 스마트하게 사용하는 방법입니다.</p>
<p>오늘의 세션은 우리 앱에서 카플레이를 어떻게 가능하게 할 것인지에 대해 이야기 할 것입니다.</p>
<p>아래 순서로 살펴보겠습니다.</p>
<ul>
<li>카플레이를 지원하는 앱에 대한 간략한 소개</li>
<li>올해 새롭게 카플레이가 지원되는 앱 타입에 대한 소개</li>
<li>카플레이 앱을 개발하는 툴에 대해 소개</li>
<li>네비게이션 앱에 특정되는 새로운 중요한 기능에 대해 소개</li>
</ul>
<h2 id="carplay-app은">CarPlay app은..</h2>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/e62e131b-8bf4-4b55-9da5-cc74361ea881/image.png" alt=""></p>
<p>카플레이 앱을 개발할 때 1순위로 고려해야할 사용자는, <strong>운전 중인</strong> 운전자입니다. 
따라서 운전에 관련된 태스크만 가능하게 하고, 운전중에 하면 안되는 태스크는 제거해야합니다. 예를 들어 일회성 설정, 앱 회원가입, 약관 읽기 같은 행동은 운전 전이나 운전 후에 하는 것이 좋습니다. 이러한 행동은 카플레이 UI에서는 보여지지 않아야합니다. </p>
<p>카플레이에서 앱을 나타내기 위해서는 자격이 필요합니다. 자격은 앱의 타입에 따라 <a href="https://developer.apple.com/documentation/carplay/requesting_carplay_entitlements">Apple CarPlay developer 웹사이트</a>에서 신청 가능합니다.</p>
<h1 id="supported-app-types">Supported app types</h1>
<hr>
<p>기존 카플레이를 지원하는 앱 종류에는 네비게이션, 오디오, 커뮤니케이션, EV 충전, 주차, 빠른 푸드 주문이 있었습니다. 운전자가 운전중에 하고 싶어하는 것들을 도와주는 기능들입니다. </p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/3fd7ed79-3168-4949-8978-abe1cb7046a2/image.png" alt=""></p>
<p>올해 두 가지 종류가 더 추가 되었습니다. Fueling과 Driving task입니다.</p>
<p>새롭게 추가된 두 종류에 대해 자세히 알아보기전에, 먼저 Template에 대해 빠르고 짧게 이야기 해보겠습니다.</p>
<h2 id="templates">Templates</h2>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/53fb9e53-49a5-412e-a0d0-515ac3fa9949/image.png" alt=""></p>
<p>템플릿은 iOS에서 제공하는 UI 템플릿을 말합니다. 앱이 데이터를 제공하면 iOS가 UI를 알아서 그려줍니다.  템플릿은 폰트 사이즈, UI 레이아웃들을 제공하고, 복잡하지 않은 UI를 구성하도록 도와줍니다. 따라서 모든 카플레이 앱의 UI는 일관적이고, 사용자는 빠르게 UI에 적응하여 사용할 수 있습니다. </p>
<p>또한 템플릿은 카플레이를 지원하는 모든 차 종류에서, 스크린의 사이즈나 인풋 디바이스에 무관하게 앱 UI가 잘 작동하게 합니다.</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/eb6a768f-b52e-4089-a0a6-28649dff664d/image.png" alt=""></p>
<p>템플릿은 개발자의 수고를 덜어줍니다. 버튼의 배열을 보여주는 <code>그리드 템플릿</code>에서부터 테이블을 보여주는 <code>리스트 템플릿</code>까지 다양한 템플릿 중에 선택할 수 있습니다. 가장 중요한건 모든 카플레이에서 이러한 템플릿이 보여지기 때문에 운전자가 UI에 익숙하다는 장점이 있습니다. </p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/0f243785-37f8-4197-9d33-457e5cf0c88a/image.png" alt=""></p>
<p>앱의 종류에 따라 사용가능한 템플릿은 위 차트와 같습니다. 위와 똑같은 차트는 developer document online에서 확인할 수 있습니다.</p>
<br>

<h1 id="새로-추가된-carplay-app-type">새로 추가된 Carplay app type</h1>
<hr>
<h2 id="1-fueling-apps">1. Fueling apps</h2>
<p>아마 기억하시듯이, iOS14에서는 EV Charging 앱을 지원하는 앱 타입을 런치했었습니다. 이러한 앱은 EV Charger 위치 검색 뿐만 아니라 사용자가 올바른 Charging station에 연결하고 시작하도록 돕습니다. 이러한 기능이 전기차가 아닌 차에서도 유용할 것이란 얘기를 많이 들었고, 새로운 fueling app을 통해 전기차뿐만아니라 전통적 가솔린 차나 대체 연료 차에서도 위 기능들을 사용할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/eecc8919-4cc9-4202-a660-14d448573814/image.png" alt=""></p>
<p>네비게이션 앱에서도 장소(여기선 주유소를 말하는 것 같음)를 찾는 것이 가능하기 때문에, fueling app에서는 단순 장소 검색 외 기능들을 제공하세요. “가스 펌프를 작동시키는 것”과 같은 것이  fueling app만이 제공할 수 있는 기능의 좋은 예가 되겠습니다.</p>
<h2 id="2-driving-task-apps">2. Driving task apps</h2>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/c102af4f-8292-42ba-be8c-3f3b058285b9/image.png" alt=""></p>
<p>Driving task app은 넓은 범위의 간단할 태스크를 제공하는 앱 타입입니다. 잊지 말아야할 것은 이러한 앱의 주요 목적은 운전중에 필요한 일을 지원하도록 디자인되어야 한다는 것입니다. 운전중에 해내어야할 태스크가 아닌 운전을 돕는 태스크여야합니다.</p>
<h3 id="examples">Examples</h3>
<ul>
<li>자동차 부품 조작</li>
<li>드라이빙 또는 도로 관련 상태와 정보 제공</li>
<li>운전의 시작과 끝에 해아할 일</li>
</ul>
<p>이 같은 예시의 구체적 사례를 보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/f1692a6e-b2f3-423d-b2c1-adfaa5e4a738/image.png" alt=""></p>
<p>첫번째, 중요한 도로 정보를 보여주는 road status app입니다. CPPointOfInterestTemplate을 사용했습니다. 사용자가 이 앱을 운전중에 사용하기 때문에, 아주 중요하고 짧은 정보를 운전석 가까이 제공하는 것이 중요합니다. 그리고 옆의 지도를 가리면 안되기 때문에 앱의 공간은 제약이 있고, 따라서 간결한 언어를 선택해야합니다.</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/81a36ad8-165e-4d08-b5a9-337f8547fab5/image.png" alt=""></p>
<p>두번째, 차의 accessory를 컨트롤하는 앱입니다. 위 경우엔 trail controller(추적 제어기)입니다. </p>
<p>CPInformationTemplate을 사용해 연결된 부품의 기본 정보를 제공하고, 사용자에게 액션을 받을 수 있는 버튼을 제공합니다. 이 예시에서 중요한 것은, 위 스크린이 앱의 전부라는 것입니다. 연결된 부품을 관리하기 위한 더 많은 기능이 들어갈 수 있지만, 운전중에 필요한 기능이 아니라면 카플레이 UI에서 제외시키는 것이 좋습니다. 비주행 작업은 차량 밖으로 나갈 때 아이폰을 이용하도록 합니다.</p>
<p> <img src="https://velog.velcdn.com/images/yeahg_dev/post/26f347d5-7197-44f2-b2e8-f22c4430473b/image.png" alt=""></p>
<p>마지막으로, CPGridTemplated을 이용한 두개의 버튼만 있는 매우 간단한 앱입니다. </p>
<p>두가지 버튼을 이용해 개인 혹은 비지니스 마일을 추적할 수 있습니다. 새로운 Driving task 앱 타입에 딱 맞는 앱이라 할 수 있겟네요. 운전중에 필요한 일이고,  매우 간단한 작업이기 때문입니다. </p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/438affea-9265-407a-a14b-ca0c558d931e/image.png" alt=""></p>
<p>위와 거의 동일한 UI의 다른 앱입니다. express lane toll transponder app인데, 차에 몇명의 동승자가 있는지 선택할 수 있습니다.</p>
<h3 id="정리">정리</h3>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/7f98456d-4cad-4d6e-a590-a3969ba68c63/image.png" alt=""></p>
<p>✅</p>
<ul>
<li>싱글 스크린의 최소한의 기능을 고려할 것</li>
<li>몇초안에 끝낼 수 있는 태스크를 타겟으로 할 것</li>
</ul>
<p>❌</p>
<ul>
<li>복잡하고 드문 사용 사례는 넣지 말 것</li>
<li>차와 관련된 일이라도 운전중에 필요한 것이 아니라면 넣지 말 것</li>
</ul>
<br>

<h1 id="carplay-simulator">CarPlay Simulator</h1>
<hr>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/c9d91339-59c3-444b-bfc9-2b9e84507eb9/image.png" alt=""></p>
<p>카플레이 앱을 테스트할 수 있는 새로운 도구가 나왔습니다. 바로 Carplay Simulator!</p>
<p>먼저 기존 테스트 툴의 차이점을 설명할게요.</p>
<ul>
<li>Xcode Simulator<ul>
<li>카플레이 윈도우를 내장하고 있음</li>
<li>Xcode Simulator를 사용하던 분들이라면 빠르게 테스트 가능</li>
</ul>
</li>
<li>실기기를 카플레이 가능 차량 또는 aftermarket head unit에 연결해서 테스트 
  (Aftermarket head unit : 부품시장에서 구할 수 있는 헤드 유닛)<ul>
<li>Carplay simulator 출시 전 실제 카플레이 UI를 테스트 할 수 있던 유일한 방법</li>
</ul>
</li>
</ul>
<h3 id="carplay-simulator-1">CarPlay Simulator</h3>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/c7712e1a-ecff-46ae-8675-62b3bfd1f394/image.png" alt=""></p>
<p>카플레이 환경을 똑같이 만들어둔 Mac app입니다. 애플 개발자 사이트의 “<a href="https://developer.apple.com/kr/xcode/resources/">Additional Tools for Xcode</a>”에서 다운로드 합니다. 앱을 실행하고 케이블을 이용해 맥에 연결해서 테스트합니다. 그럼 실제 차에 연결된 것과 똑같이 실행됩니다.</p>
<p>실제 테스트 장면👇 (영상 후반부에 나옴)</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/a001f597-5cbe-41e9-a41f-1f289e0cd3e3/image.png" alt="">
<img src="https://velog.velcdn.com/images/yeahg_dev/post/a56b4a02-61d3-4ef5-b74e-34c990102d1c/image.png" alt=""></p>
<h3 id="장점">장점</h3>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/f9792902-3c59-44c8-b2d1-eec7edfcff9e/image.png" alt=""></p>
<ul>
<li>실제 차량과 똑같이 작동 (주차장을 오갈 필요 없음)</li>
<li>다른 개발 툴과 함께 사용 가능</li>
<li>아이폰의 모든 기능에 접근 가능</li>
</ul>
<p>(일부 시나리오는 실제 카플레이 시스템이나 현재 카플레이 시뮬레이터 없이는 테스트할 수 없습니다)</p>
<ul>
<li>차량의 다양한 설정을 테스트 가능 (예. 다양한 디스플레이 사이즈)</li>
</ul>
<p>시뮬레이터가 어떻게 생겼는지 살펴보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/395cbd6b-1cf6-4d95-aceb-c0fcf93fd75a/image.png" alt=""></p>
<p>스크린 하단에는 차에 있는 hard key와 Knob control이 재현되어있습니다. </p>
<p>터치 스크린 차량은 클릭으로 터치를 재현할 수도 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/bd3ec33c-7b68-410b-b95a-29b8a9af6786/image.png" alt=""></p>
<p>스크린 상단에는 몇가지 빠른 컨트롤이 있습니다. </p>
<p><code>limit UI button</code> 을 사용하면 움직이는 차량이 카플레이 화면의 특정 콘텐츠를 제한하도록 요청 되는 상황을 시뮬레이션 할 수 있습니다. 예를 들어, 오디오 앱의 목록 내용을 줄입니다.</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/06e9e802-f89e-41e5-81ef-340b9bfa01fc/image.png" alt=""></p>
<p><code>Dark UI</code> , <code>Dark Map</code> 은 UI의 라이트 혹은 다크 모드를 보여줍니다. </p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/721daf7c-d5d1-4ec6-8036-afc6d5ac9275/image.png" alt=""></p>
<p><code>Connected</code> 는 카플레이에 폰을 연결해제, 재연결 상황을 재현합니다. </p>
<p>이 버튼을 사용할 땐 폰이 맥에 연결되어 있으므로, Xcode를 이용해 앱에서 카플레이 재연결시나리오를 디버깅할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/36d1bd0f-4670-4d5b-bafa-f6d97f509998/image.png" alt=""></p>
<p><code>Configure</code> 버튼은 더 고급 기능과 함께 secondary 스크린을 띄웁니다.</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/5e9994d1-8c28-4628-bf49-cb097e9b3967/image.png" alt=""></p>
<p>메인 카플레이 디스플레이의 사이즈를 선택 가능합니다. 여러 차종에서 디스플레이가 정상적으로 보여지는지 테스트 할 수 있습니다.
아래는 네비게이션 앱에서 추천하는 테스트 디스플레이 사이즈입니다.</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/9e4b6295-29e5-4cd2-baad-6759eab67e13/image.png" alt=""></p>
<p>카플레이 시뮬레이터에서는 Instrument Cluster(계기판) 디스플레이도 테스트 할 수 있습니다. 계기판은 특히 네비게이션 앱과 찰떡으로 활용할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/f8c74b88-7f97-47e2-80d5-7a093d039dd2/image.png" alt="">
<img src="https://velog.velcdn.com/images/yeahg_dev/post/90b69814-1d5c-45ba-ae5d-f6029268ea3f/image.png" alt=""></p>
<br>


<h1 id="계기판에서-map-디스플레이-가능">계기판에서 Map 디스플레이 가능</h1>
<hr>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/bc510bb1-a32e-4470-be2c-3aab08cbf29b/image.png" alt=""></p>
<p>차량 계기판에서 맵 디스플레이가 가능해졌습니다!</p>
<p>앱에서 계기판 지원을 추가하는 방법입니다. </p>
<p>iOS13에서 네비게이션앱이 카플레이 대시보드에서 보여지도록하는 API를 추가했었잖아요, 그 사용 방법과 유사합니다. </p>
<p>앱의 info.plist에 <code>instrumentClusterNavigationScene</code>을 선언하고, 요구되는 <code>SceneSessionRole</code>을 추가합니다. </p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/c87adcb4-b51a-4d0f-918d-674e62d5e48e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/885630cd-c65a-45c3-9ada-1a8133332611/image.png" alt=""></p>
<p>그리고 <code>CPTemplateApplicationInstrumentCluster Scene delegate</code> 와<code>CPInstrumentClusterControllerDelegate</code> 를 구현합니다.</p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/99dcaf52-92ed-4bf2-9f69-a4fe6ab53a80/image.png" alt=""></p>
<p>컨텐츠를 그릴 수 있는 Window를 제공하고, 계기판이 시작하고 끝나는 타이밍을 알려주며 계기판 뷰가 보여질 수 있도록 합니다. 이게 끝입니다</p>
<p>계기판 뷰에선 좀 더 고려할 사항이 몇가지가 있습니다.</p>
<ul>
<li>계기판은 사용자 맵을 줌인, 줌아웃하는 것을 허용하는데, 이에 대응해서 <code>CPInstrumentClusterControllerDelegate</code> 를 구현해야합니다.</li>
<li>앱이 나침반이나 속도제한을 포함할  때, 언제 이것들을 그릴지 말지를 앱에게 알려주어야합니다.</li>
<li>차량의 다른 계기판 요소에 의해 계기판 뷰가 가려질 수 있습니다. 이때는 <code>viewSafeAreaInsetsDidChange</code> 를 오버라이딩해서 safe area가 변경되었을 때 중요한 컨텐츠가 계기판에서 보여질 수 있도록 보장할 수 있습니다.</li>
</ul>
<br>

<h3 id="🛞-카플레이-실행-모습">🛞 카플레이 실행 모습</h3>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/eef06b22-2f20-4fea-96b1-47567ac7f759/image.png" alt="">
<img src="https://velog.velcdn.com/images/yeahg_dev/post/51931916-25fd-4c17-bb2f-d64071a7b550/image.png" alt=""></p>
<br>

<p>운전자 편의와 안전, 개발의 편의를 위해 탬플릿을 통해 일관된 UI를 구현한다는 점이 인상깊었네요✨</p>
<p>카플레이... 저도 빨리 사용해보고 싶네요. 화이팅😊</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[UICollectionViewCell의 Subview가 안보일 때]]></title>
            <link>https://velog.io/@yeahg_dev/UICollectionViewCell%EC%9D%98-Subview%EA%B0%80-%EC%95%88%EB%B3%B4%EC%9D%BC-%EB%95%8C</link>
            <guid>https://velog.io/@yeahg_dev/UICollectionViewCell%EC%9D%98-Subview%EA%B0%80-%EC%95%88%EB%B3%B4%EC%9D%BC-%EB%95%8C</guid>
            <pubDate>Thu, 09 Jun 2022 07:02:20 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-상황">문제 상황</h2>
<p>스토리보드로 <code>UICollectionViewCell</code>을 만들어서 사용하려던 와중에 <code>CollectionViewCell</code>의 subview(ContentView)가 나타나지 않는 문제를 맞딱뜨렸다.
<img src="https://velog.velcdn.com/images/yeahg_dev/post/119e0ae7-01f6-430c-a6c3-4a895839c561/image.png" alt=""></p>
<p>스토리보드에서 <code>CollectionViewCell</code>을 만들어서 CollectionView 안에 배치했고, 셀안의 ImageView도 ContentView와 제약을 정상적으로 맺어주었다. </p>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/574da0cf-9608-412e-803a-4011a84c6d4b/image.png" alt=""></p>
<p>그리고 <code>UICollectionViewCell</code>을 상속한 <code>ProductDetailCollectionViewCell</code>을 만들고, 스토리보드에서 연결, reuseIdentifier도 생성했다</p>
<pre><code class="language-swift">class PrdouctDetailCollectionViewCell: UICollectionViewCell {

    @IBOutlet weak var prdouctImage: UIImageView!

    override class func awakeFromNib() {
        super.awakeFromNib()
    }

}</code></pre>
<p><img src="https://velog.velcdn.com/images/yeahg_dev/post/7421bb24-327f-42cc-897b-f5bd71c3e69c/image.png" alt=""></p>
<p>스토리보드로부터 Cell UI를 로드하기 때문에 <code>awakeFromNib()</code>이 호출되어야하지만, 호출되지 않았다.</p>
<br>

<h2 id="원인">원인</h2>
<p>원인은 스토리보드에서 셀을 초기화해야하는데 컬렉션뷰셀 클래스로부터 초기화 되었기 때문이었다. 컬렉션뷰에 아래와 같은<code>register</code> 코드를 작성했는데, 때문에 컬렉션뷰는 셀을 디큐할 때 스토리보드로부터 셀을 초기화하는 것이 아니라 커스텀 셀 클래스로 부터 초기화를 한다. </p>
<pre><code class="language-swift"> self.prductImageCollectionView?.register(PrdouctDetailCollectionViewCell.self, forCellWithReuseIdentifier: &quot;PrdouctDetailCollectionViewCell&quot;)</code></pre>
<p>그래서 셀을 디큐할 때  reuseIdentifier &quot;PrdouctDetailCollectionViewCell&quot;에 해당하는 
<code>PrdouctDetailCollectionViewCell</code> 클래스로 부터 셀을 초기화한다. 그러므로 해당 클래스 파일은 UI가 구현되어 있지 않으니... 아무런 하위뷰도 없을 수 밖에...🤦‍♀️</p>
<pre><code class="language-swift"> func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -&gt; UICollectionViewCell {

        guard let cell = self.prductImageCollectionView?.dequeueReusableCell(withReuseIdentifier: &quot;PrdouctDetailCollectionViewCell&quot;, for: indexPath) as? PrdouctDetailCollectionViewCell else {
            return PrdouctDetailCollectionViewCell()
        }

        return cell
    }
</code></pre>
 <br>

<h2 id="해결--정리">해결 &amp; 정리</h2>
<p> 아래 셀 클래스를 등록하는 코드를 없애주었더니 해결!</p>
<pre><code class="language-swift"> self.prductImageCollectionView?.register(PrdouctDetailCollectionViewCell.self, forCellWithReuseIdentifier: &quot;PrdouctDetailCollectionViewCell&quot;)</code></pre>
<p>정상적으로 스토리보드에서 해당 reuseIdentifier에 해당하는 셀을 로드해서 초기화한다. </p>
<br>

<blockquote>
<p><strong>스토리보드로 생성한 셀을 사용할 땐 <code>register</code> 하면 안된다!</strong></p>
<p>⭐️ 스토리보드, nib파일, 코드로 셀을 만드는 방법을 잘 구분해서 사용하자!!</p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>