<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Keem.dev</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Fri, 27 Dec 2024 15:09:03 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Keem.dev</title>
            <url>https://velog.velcdn.com/images/keem-hyun/profile/e2d988ec-ad87-4a62-b013-ee3f501c3511/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Keem.dev. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/keem-hyun" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Supabase 명령어 및 레퍼런스 정리]]></title>
            <link>https://velog.io/@keem-hyun/Supabase-%EB%AA%85%EB%A0%B9%EC%96%B4-%EB%B0%8F-%EB%A0%88%ED%8D%BC%EB%9F%B0%EC%8A%A4-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@keem-hyun/Supabase-%EB%AA%85%EB%A0%B9%EC%96%B4-%EB%B0%8F-%EB%A0%88%ED%8D%BC%EB%9F%B0%EC%8A%A4-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Fri, 27 Dec 2024 15:09:03 GMT</pubDate>
            <description><![CDATA[<h1 id="supabase">Supabase</h1>
<h2 id="레퍼런스">레퍼런스</h2>
<ul>
<li><a href="https://www.kyulabs.app/f0a4d5bc-9058-430b-a42b-2c12ea0d07bb">Supabase + Flutter 튜토리얼</a></li>
<li><a href="https://supabase.com/docs">Supabase 공식 문서</a></li>
</ul>
<h2 id="로컬-개발-환경설정">로컬 개발 환경설정</h2>
<ol>
<li><p><a href="https://supabase.com/docs/guides/local-development/cli/getting-started?queryGroups=platform&amp;platform=windows">Supabase CLI 설치</a></p>
</li>
<li><p><a href="https://docs.docker.com/desktop/">Docker Desktop 설치</a></p>
</li>
<li><p><a href="https://docs.deno.com/runtime/getting_started/installation/">Deno 설치</a></p>
</li>
<li><p>supabase 디렉토리로 이동하여 init 명령어 실행</p>
<ul>
<li>Generate VS Code settings for Deno? y 선택<pre><code class="language-bash">supabase init</code></pre>
</li>
</ul>
</li>
<li><p>로컬 Supabase 프로젝트 실행</p>
<pre><code class="language-bash">supabase start</code></pre>
</li>
</ol>
<ul>
<li>supabase_vector_supabase container is not ready: unhealthy 에러 시
  config.toml 파일에서 analytics.enabled = false 로 설정</li>
</ul>
<h2 id="supabase-명령어">Supabase 명령어</h2>
<h3 id="supabase-실행">Supabase 실행</h3>
<pre><code class="language-bash">supabase start</code></pre>
<h3 id="supabase-중단">Supabase 중단</h3>
<pre><code class="language-bash">supabase stop</code></pre>
<h3 id="supabase-상태-확인">Supabase 상태 확인</h3>
<pre><code class="language-bash">supabase status</code></pre>
<h3 id="supabase-연결-중단">Supabase 연결 중단</h3>
<pre><code class="language-bash">supabase unlink</code></pre>
<h3 id="supabase-연결">Supabase 연결</h3>
<pre><code class="language-bash">supabase link</code></pre>
<h3 id="supabase-로그인">supabase 로그인</h3>
<pre><code class="language-bash">supabase login</code></pre>
<h2 id="supabase-edge-functions-배포">Supabase Edge Functions 배포</h2>
<p>엔드포인트는 폴더명(ex.api, test 등)으로 정의됨.
폴더 내 실행 파일명은 index.ts 파일이어야 함.</p>
<ul>
<li>supabase functions 로컬 배포<ul>
<li>Supabase Edge Functions는 Hot Reload를 지원합니다. 코드에 변경사항이 생겼을때 재배포하지 않아도 서버에 바로 반영됩니다.</li>
<li>no-verify-jwt 옵션은 인증 검증을 비활성화합니다.</li>
</ul>
</li>
</ul>
<pre><code class="language-bash">supabase functions serve --no-verify-jwt</code></pre>
<ul>
<li>Supabase functions public 배포</li>
</ul>
<pre><code class="language-bash">supabase functions deploy</code></pre>
<ul>
<li>Supabase functions public 배포 중단</li>
</ul>
<pre><code class="language-bash">supabase functions delete &lt;function-name&gt;</code></pre>
<h2 id="로컬에서-배포-환경-마이그레이션">로컬에서 배포 환경 마이그레이션</h2>
<h3 id="관련-supabase-docs-링크"><a href="https://supabase.com/docs/guides/local-development/overview">관련 supabase docs 링크</a></h3>
<ol>
<li>마이그레이션 파일 생성</li>
</ol>
<ul>
<li>새 마이그레이션 생성<pre><code class="language-bash">supabase migration new &lt;파일명&gt;</code></pre>
</li>
</ul>
<ol start="2">
<li>스키마 및 데이터 덤프 생성</li>
</ol>
<ul>
<li><p>스키마 덤프 (public 스키마만)</p>
<pre><code class="language-bash">supabase db dump -f schema.sql --db-url &lt;local-db-url&gt; --schema=public</code></pre>
</li>
<li><p>데이터 덤프 (public 스키마만)</p>
<pre><code class="language-bash">supabase db dump -f data.sql --data-only  --db-url &lt;local-db-url&gt; --schema=public</code></pre>
</li>
</ul>
<p>3.덤프 내용을 마이그레이션 파일에 복사</p>
<pre><code class="language-bash">cat schema.sql(스키마 덤프 파일명) data.sql(데이터 덤프 파일명) &gt; supabase/migrations/[timestamp]_[파일명].sql</code></pre>
<ol start="4">
<li><p>마이그레이션 적용</p>
<pre><code class="language-bash">supabase db push</code></pre>
</li>
<li><p>Edge Functions</p>
<ul>
<li>배포 명령어 실행<pre><code class="language-bash">supabase functions deploy</code></pre>
</li>
</ul>
</li>
<li><p>스토리지 설정 </p>
<ul>
<li>수동으로 업로드 하거나, 스크립트를 통해 업로드 필요</li>
</ul>
</li>
</ol>
<h2 id="리모트에서-로컬로-데이터베이스-가져오기">리모트에서 로컬로 데이터베이스 가져오기</h2>
<ol>
<li>리모트에서 스키마와 데이터 가져오기</li>
</ol>
<pre><code class="language-bash"># 스키마 덤프
supabase db dump -f schema.sql --db-url &lt;remote-db-url&gt;
# 혹은 migration 파일 생성
supabase db pull --db-url &lt;remote-db-url&gt;

# 데이터 덤프
supabase db dump -f data.sql --data-only --db-url &lt;remote-db-url&gt;</code></pre>
<p>2.새로운 마이그레이션과 시드 설정</p>
<pre><code class="language-bash"># 스키마를 마이그레이션으로 복사
cp schema.sql supabase/migrations/$(date +%Y%m%d%H%M%S)_init.sql
# 혹은 2번에서 migration 파일을 생성했다면 패스.

# 데이터를 시드로 복사
cp data.sql supabase/seeds/seed.sql

# 로컬 DB 리셋
supabase db reset</code></pre>
<h2 id="database-명령어">Database 명령어</h2>
<ul>
<li><p>로컬 데이터베이스 리셋</p>
<ul>
<li>seed 파일이 있다면 seed.sql로 복원됨</li>
<li>config.toml 파일에서 enabled = false 일 경우 데이터베이스 리셋 안됨</li>
<li>enabled = true 일 경우 데이터베이스 리셋 후 seed.sql 파일로 복원<pre><code class="language-bash">supabase db reset</code></pre>
</li>
</ul>
</li>
<li><p>리모트 데이터베이스 리셋</p>
<pre><code class="language-bash">supabase db reset --db-url &lt;db-url&gt;</code></pre>
</li>
<li><p>프로덕션 db 덤프 생성</p>
<pre><code class="language-bash">supabase db dump -f backup.sql --db-url &lt;db-url&gt;</code></pre>
</li>
<li><p>스키마 덤프 (public 스키마만)</p>
<pre><code class="language-bash">supabase db dump -f schema.sql --db-url &lt;db-url&gt; --schema=public</code></pre>
</li>
<li><p>데이터 덤프 (public 스키마만)</p>
<pre><code class="language-bash">supabase db dump -f data.sql --data-only  --db-url &lt;db-url&gt; --schema=public</code></pre>
</li>
<li><p>새 마이그레이션 생성</p>
<pre><code class="language-bash">supabase migration new &lt;파일명&gt;</code></pre>
</li>
<li><p>덤프 내용을 마이그레이션 파일에 복사</p>
<pre><code class="language-bash">cat schema.sql(스키마 덤프 파일명) data.sql(데이터 덤프 파일명) &gt; supabase/migrations/[timestamp]_[파일명].sql</code></pre>
</li>
<li><p>로컬의 Schema 변경사항을 public supabase로 전달하여 적용</p>
<pre><code class="language-bash">supabase db push</code></pre>
</li>
<li><p>연결된 Public Supabase DB로부터 Schema를 로컬로 가져옴</p>
<pre><code class="language-bash">supabase db pull</code></pre>
</li>
<li><p>데이터베이스의 현재 상태와 마지막 마이그레이션 상태 비교하여 파일로 저장</p>
<pre><code class="language-bash">supabase db diff
# 혹은 파일 이름 명시
supabase db diff -f &lt;파일명&gt;.sql</code></pre>
</li>
</ul>
<h2 id="edge-functions-명령어">Edge Functions 명령어</h2>
<ul>
<li>함수 생성<pre><code class="language-bash">supabase functions new &lt;함수명&gt;</code></pre>
</li>
</ul>
<h2 id="레퍼런스-1">레퍼런스</h2>
<h3 id="auth">Auth</h3>
<ul>
<li><p><a href="https://supabase.com/docs/guides/auth/managing-user-data?queryGroups=language&amp;language=dart">Supabase 공식 문서 - User Management</a></p>
</li>
<li><p><a href="https://velog.io/@dev_leewoooo/supabase%EC%97%90-Customer-SMTP-Provider-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-with-Resend">supabase에 Customer SMTP Provider 적용하기 (with Resend)</a></p>
</li>
</ul>
<h3 id="edge-functions">Edge Functions</h3>
<ul>
<li><a href="https://supabase.com/docs/guides/functions/examples/slack-bot-mention">Supabase 공식 문서 - Slack Bot Mention Edge Function</a></li>
</ul>
<h3 id="supabase-cli">Supabase CLI</h3>
<ul>
<li><a href="https://supabase.com/docs/reference/cli/start">Supabase 공식 문서 - Supabase CLI</a></li>
</ul>
<h3 id="cicd">CI/CD</h3>
<ul>
<li><a href="https://supabase.com/docs/guides/functions/cicd-workflow">Supabase 공식 문서 - Deploying with CI / CD pipelines</a></li>
<li><a href="https://supabase.com/docs/guides/deployment/ci/backups">Supabase 공식 문서 - 백업 자동화</a></li>
</ul>
<h3 id="supabase-client-library-reference">Supabase Client Library Reference</h3>
<ul>
<li><a href="https://supabase.com/docs/reference/dart/start?queryGroups=version&amp;version=2.0x">Supabase + Dart</a></li>
<li><a href="https://supabase.com/docs/reference/swift/start">Supabase + Swift</a></li>
<li><a href="https://supabase.com/docs/reference/kotlin/start">Supabase + Kotlin</a></li>
</ul>
<h3 id="환경-분리">환경 분리</h3>
<ul>
<li><a href="https://supabase.com/docs/guides/deployment">Supabase 공식 문서 - 환경 분리</a></li>
<li>Supabase는 Trunk Based Development workflow를 따르고 있음. <a href="https://tech.mfort.co.kr/blog/2022-08-05-trunk-based-development/">Git Flow에서 트렁크 기반 개발으로 나아가기</a></li>
</ul>
<h3 id="supabase-git-연동">supabase git 연동</h3>
<ul>
<li><a href="https://supabase.com/docs/guides/deployment/branching#preparing-your-git-repository">Supabase 공식 문서 - Git 연동</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Devfest Incheon / Songdo 2024 후기]]></title>
            <link>https://velog.io/@keem-hyun/Devfest-Incheon-Songdo-2024-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@keem-hyun/Devfest-Incheon-Songdo-2024-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Sun, 22 Dec 2024 02:00:14 GMT</pubDate>
            <description><![CDATA[<p>어제 진행되었던 GDG Devfest Incheon을 다녀왔습니다.</p>
<p>송도가 생각보다 거리가 멀어 갈 때는 괜히 신청했나 했지만, 컨퍼런스가 진행된 송도 컨벤시아가 쾌적하기도 하고 세션들의 내용도 좋아서 내년에도 참여하고 싶다 생각이 들었어요.</p>
<h2 id="시니어-개발자-노하우---개발-잘하는-방법-권대건님--jnpmedi">시니어 개발자 노하우 - 개발 잘하는 방법 (권대건님 / JNPMEDI)</h2>
<h4 id="부제---문제정의부터-해결까지-완벽과정">부제 - 문제정의부터 해결까지 완벽과정</h4>
<p>이전 슥삭 CTO를 지내시고, 현재는 JNPMEDI에서 개발 리드를 하고 계시는 권대건님이 &quot;시니어 개발자 노하우 - 개발 잘하는 방법&quot; 이라는 주제로 발표를 해주셨어요.</p>
<p>저의 경우는 아직 주니어 입장에서 큰 그림을 보지 못한 채 개발을 하는 경우가 많은 것 같습니다.</p>
<p>해당 발표에서 대건님은 문제 정의와 해결에 대한 여러 가지 방법론과 예시를 알려주었는데, 그 중에서 문제 정의 canvas와 5 why&#39;s 분석은 내가 어떤 문제를 해결해야 하는 지 파악하는 데 유용할 것 같습니다.</p>
<p>하단은 제가 발표를 들으면서 메모한 내용입니다.</p>
<pre><code>문제 정의란? 문제를 해결하기 위해 올바르게 이해하는 과정

문제 정의 핵심 요소

- 무엇이 문제인지 / 왜 문제가 발생했는지  / 어떤 결과를 기대하는지

제대로된 문제 정의가 이루어지지 않고 프로젝트가 진행되면 리소스가 낭비됨(프로젝트 지연, 비용 초과 등)

이러한 실패를 겪지 않으려면 초기 단계에서 충분한 시간을 투자해야 함 - align을 위해

문제 정의를 도와주는 툴

fishbone diagram

문제 정의 canvas는 6가지로 이루어져 있음
context - 맥락 이해
root problem - 내면의 문제 파악
alternative - 현재 상태에서 다른 대안은 없는가. 개발을 하지 않더라도 해결할 수 있는 대안 등. 임팩트 확인
customers - 대상이 누구인지 / 먼저 대상을 정의하지 않기  
emotional impact - 당사자가 어떤 문제를 겪고 있는지 
quantifiable impact 정량적인 임팩트 - 돈, 상품, 시간 등이 얼마나 감소하는지
alternative shortcoming - 대안에 대한 단점 확인

5 why’s 분석 - 5번의 why를 생각하는 것. 꼬리 물기.

계획 수립

- 기한이 있고 / 구체적이고 / 예측 가능하고 / 현실적 / 달성 가능한(다음 로드맵을 계속 고려해야 함 ex. ver1, ver2, 외부 내부 달성 방해 요인 파악 필요)

우선순위 설정

MoSCoW 기법 (Must, Should, Could, Won’t)

문제 해결 - 바라보는 관점에 따라 해결할 수 있는 방법이 달라짐

needed - 유연한 사고 방식

- 복잡한 문제를 작은 단위로 쪼개기 
ex - 개발할 때 공통의 성격을 가진 문제를 결합하는 것도 중요함
- 추상화 레벨을 조정하기 - 큰그림과 세부 사항 균형 맞추기
ex - 해당 기능 개발할 때, 문제 요구사항을 고려하면서 해야 함. 확장성 고려
- 가설로 접근하기  - 문제를 명확하게 정의
ex - 문제가 생겼을 때 코드부터 보는 것이 아닌, 문제 가능성 케이스들을 고려해보는 것.
- 디버깅과 회고 - 실패에서 배워라
레슨런이 반복되도록
- 협업의 힘 - 혼자서 해결하지 마라
소통과정에서 생기는 마찰을 무서워하지 말고 계속 핑퐁하다 보면 더 나은 문제 해결이 가능함

최적의 코드 작성

- DRY 원칙 적용 (Don’t repeat yourself)
- KISS 원칙 적용 ( Keep it simple, stupid)

코드 리뷰

- 코드 품질 향상 / 학습 및 지식 공유 / 협업과 소통 / 유지보수 용이 / 개발 속도 향상</code></pre><h2 id="빅테크-엔지니어-성장비법-우리팀에-복붙하기">빅테크 엔지니어 성장비법 우리팀에 복붙하기</h2>
<h4 id="엔지니어링-커리어-래더로-팀-메타인지를-함께-올리는-방법">엔지니어링 커리어 래더로 팀 메타인지를 함께 올리는 방법</h4>
<p>해당 주제는 하이퍼커넥트에서 엔지니어로, 뱅크샐러드에서 매니저를 경험하시고 현재는 마나부 라는 회사를 창업하신 박준호님이 발표를 해주셨습니다.
커리어 래더 라는 것은 처음 들어봤고, L3, L4.. 라는 명칭만 들어봤는데 레벨을 통한 체계화된 직무 역량 관리를 간접 경험해볼 수 있어서 좋았습니다.</p>
<pre><code>### 엔지니어링 커리어 래더란

엔지니어 커리어를 체계적으로 성장할 수 있게 돕는 명확한 역할과 기대치를 정의한 프레임워크

빅테크에서 주로 쓰는 레벨 제도 ex. L3, L4, L5…

매니저가 무엇이냐?

우리나라에선 매니저라는 역할이 잘 정의되어 있지 않았음. 매니저 ≠ 팀장.

사람을 잘 관리하는 것과 기술 역량이 높은 것은 분리 되어야 할 필요가 있음.

매니저는 보통 L5부터

L3: 인턴 / 신입 / 주니어

L4: 주니어와 시니어 사이 중니어
 기능 단위를 도맡아 하고, 책임감 / 안정성

L5: 테크 리드 / 시니어 엔지니어

L6: 스태프 엔지니어

페이스북은 L3 → L4 2년, L4 → L5 4년 정도 소요. L5부터는 비즈니스 임팩트 있는 큰 프로젝트에 기여 필요

### 엔지니어 커리어 래더링 적용하기

매니저와 엔지니어가 대화하는 것이 제일 중요. ex. 원온원을 통해

역량 기준을 통해 항목에 색을 입혀 서로의 시선을 봄.

역량 기준이 없다면 어디선가 퍼오기

커리어 래더 - 희망편

ex. 다른 회사에서 좋은 오퍼를 받았을 때 매니저와 고민 / 매니저가 고민을 같이 함

커리어 래더 - 절망편

커리어 래더 - 응용편

커리어 래더 - 소통하기

뱅크샐러드에서 적용했던 엔지니어링 매니징 방법 / 매니저와 같이 얼라인

### 주기적으로 발전을 체크해보는 방법

3~6개월 정도의 주기가 적당함.

역량표를 바탕으로 6개월간의 변화를 소재로 대화

- 개발자는 성장 확인
- 매니저는 기회 확보

다음 라운드의 향상 목표 선정</code></pre><p><img src="https://velog.velcdn.com/images/keem-hyun/post/b92ea518-b2d6-42d1-a066-cda761d84bef/image.JPG" alt="">
<img src="https://velog.velcdn.com/images/keem-hyun/post/e151c6f6-e4e4-4c6f-8bb5-7f4d27b98aab/image.JPG" alt="">
<img src="https://velog.velcdn.com/images/keem-hyun/post/9c577e27-92aa-47e7-8b06-e9215fa66e45/image.JPG" alt="">
<img src="https://velog.velcdn.com/images/keem-hyun/post/bbdecc8b-f2f2-4e93-9df2-9e54b4a8d370/image.JPG" alt="">
<img src="https://velog.velcdn.com/images/keem-hyun/post/ea20d192-33f5-4cec-9c5d-368e21d17283/image.JPG" alt=""></p>
<p>다른 회사의 커리어 래더에 대한 이야기들은 <a href="https://www.swyx.io/career-ladders">여기서</a> 확인해보실 수 있어요.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MacOS] Supabase 로컬 개발 환경 세팅]]></title>
            <link>https://velog.io/@keem-hyun/MacOS-Supabase-%EB%A1%9C%EC%BB%AC-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%B8%ED%8C%85</link>
            <guid>https://velog.io/@keem-hyun/MacOS-Supabase-%EB%A1%9C%EC%BB%AC-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%B8%ED%8C%85</guid>
            <pubDate>Fri, 29 Nov 2024 13:27:30 GMT</pubDate>
            <description><![CDATA[<h3 id="1-supabase-cli-설치">1. <a href="https://supabase.com/docs/guides/local-development/cli/getting-started?queryGroups=platform&amp;platform=macos">Supabase CLI 설치</a></h3>
<p>brew를 이용해서 설치합니다.</p>
<pre><code class="language-bash">brew install supabase/tap/supabase</code></pre>
<h3 id="2-docker-desktop-설치">2. <a href="https://www.docker.com/products/docker-desktop/">Docker Desktop 설치</a></h3>
<h3 id="3-deno-설치">3. <a href="https://docs.deno.com/runtime/getting_started/installation/">Deno 설치</a></h3>
<p>deno 또한 brew로 설치합니다.</p>
<pre><code class="language-bash">brew install deno</code></pre>
<p>supabase Edge functions 사용 시 전 Deno를 사용하려고 설치했는데, 다른 걸로 진행한다면 설치를 안 해도 됩니다.</p>
<h3 id="4-supabase-프로젝트-생성">4. supabase 프로젝트 생성</h3>
<p>작업을 원하는 디렉토리로 이동하여 프로젝트를 생성합니다.</p>
<pre><code class="language-bash">supabase init</code></pre>
<p>Generate VS Code settings for Deno? [y/N] 가 나타나는데 Deno를 사용한다면 y, 아니라면 n을 입력해주세요.</p>
<h3 id="5-supabase-시작">5. supabase 시작</h3>
<pre><code class="language-bash">supabase start</code></pre>
<p>docker desktop이 실행중인 상태에서 상단 명령어를 입력해주세요.</p>
<p>성공을 하게 되면 하단과 같이 나타나는데 Studio Url을 브라우저에 입력하면 대시보드를 통해 supabase를 사용할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/keem-hyun/post/4772fb05-13a1-4f46-a8f2-9c3e4f72b839/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Cursor AI 혹은 VSCode로 Swift 개발 환경 세팅하기]]></title>
            <link>https://velog.io/@keem-hyun/Cursor-AI%EB%A1%9C-Swift-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%B8%ED%8C%85%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@keem-hyun/Cursor-AI%EB%A1%9C-Swift-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%B8%ED%8C%85%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 24 Nov 2024 09:50:42 GMT</pubDate>
            <description><![CDATA[<p>요즘 AI 도구의 발달로 AI를 어떻게 해야 잘 활용할 수 있지에 대한 고민을 하고 있는데요,
AI 도구 중 코드 작성에 많은 도움을 주는 Cursor AI로 Swift 개발 환경을 세팅하는 아티클을 발견해서 공유합니다!</p>
<p>해당 아티클: <a href="https://dimillian.medium.com/how-to-use-cursor-for-ios-development-54b912c23941"><strong>How to use VSCode/Cursor for iOS development</strong></a></p>
<p>세팅을 하고 나서 sweetpad를 통해서 빌드를 해보았습니다.
<img src="https://velog.velcdn.com/images/keem-hyun/post/d33bdcd0-ae3c-4d5c-b122-c98c09439e38/image.png" alt=""><img src="https://velog.velcdn.com/images/keem-hyun/post/de1e62f4-1cdb-435b-b96b-48cb852f2787/image.png" alt=""><img src="https://velog.velcdn.com/images/keem-hyun/post/04d79dc8-c698-4e8f-9f75-fce31b1b632e/image.png" alt=""><img src="https://velog.velcdn.com/images/keem-hyun/post/c3c9a751-235e-4be2-91d7-783d113b99a4/image.png" alt=""></p>
<ol>
<li>Sweetpad 익스텐션 설치</li>
<li>탭에서 Sweetpad 선택</li>
<li>launch(플레이 버튼) 진행</li>
<li>터미널에서 빌드 동작을 확인할 수 있어요 (xcode에서의 콘솔)</li>
</ol>
<p>저도 현재 해당 아티클에 따라 세팅 진행 후 빌드까지만 진행을 해보아서 추후 사용하면서 추가적인 팁들이 있다면 추가해보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[flutter window,iOS 폴더만 프로젝트 생성하기]]></title>
            <link>https://velog.io/@keem-hyun/flutter-windowiOS-%ED%8F%B4%EB%8D%94%EB%A7%8C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@keem-hyun/flutter-windowiOS-%ED%8F%B4%EB%8D%94%EB%A7%8C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 19 Nov 2024 13:59:03 GMT</pubDate>
            <description><![CDATA[<p>이 명령어는 안드로이드와 iOS 플랫폼만 포함된 프로젝트를 생성합니다. 다른 플랫폼(웹, 윈도우, 맥, 리눅스)은 제외됩니다.</p>
<pre><code>flutter create --platforms android,ios my_project_name</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[네트워크] OSI 7 계층 정리]]></title>
            <link>https://velog.io/@keem-hyun/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-OSI-7-%EA%B3%84%EC%B8%B5-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@keem-hyun/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-OSI-7-%EA%B3%84%EC%B8%B5-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sun, 22 Sep 2024 08:19:07 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/keem-hyun/post/d4912704-f015-4ad4-aff6-8bde0de632d8/image.png" alt="">
이미지출처: <a href="https://www.cloudflare.com/ko-kr/learning/ddos/glossary/open-systems-interconnection-model-osi/">CLOUDFLARE</a></p>
<h1 id="1계층---물리-계층-피지컬-계층">1계층 - 물리 계층 (피지컬 계층)</h1>
<hr>
<p>1계층은 물리 계층으로, 주로 전기 신호를 전달하는 데 초점이 맞추어져 있다. 그러므로 출발지와 목적지를 구분할 수 없다.</p>
<p>1계층에서는 들어온 전기 신호를 그대로 잘 전달하는 것이 목적이므로 전기 신호가 1계층 장비에 들어오면 이 전기 신호를 재생성하여 내보낸다.</p>
<p>1계층 장비는 주소의 개념이 없으므로 전기 신호가 들어온 포트를 제외하고 모든 포트에 같은 전기 신호를 전송한다.</p>
<p>주요 장비: 허브, 리피터, 케이블, 커넥터, 트랜시버 등</p>
<p>주요 프로토콜: RS-232, RS-449, V.35, S 등의 케이블</p>
<h1 id="2계층---데이터-링크-계층">2계층 - 데이터 링크 계층</h1>
<hr>
<p>2계층은 데이터 링크 계층으로 전기 신호를 모아 우리가 알아볼 수 있는 데이터 형태로 처리한다.</p>
<p>2계층은 주소 정보를 정의하고 정확한 주소로 통신이 되도록 하는 데 초점이 맞추어져 있다.</p>
<p>출발지와 목적지 주소를 확인하고 내게 보낸 것이 맞는지 혹은 내가 처리해야 하는지에 대해 검사 후 데이터 처리를 수행한다.</p>
<p>전기 신호를 모아 데이터 형태로 처리하므로 데이터에 대한 에러를 탐지하거나 고치는 역할을 수행할 수 있다.</p>
<p>주소 체계: MAC 주소</p>
<p>주요 장비: 네트워크 인터페이스 카드, 스위치</p>
<p>주요 프로토콜: IEEE 802.2, FDDI</p>
<h1 id="3계층---네트워크-계층">3계층 - 네트워크 계층</h1>
<hr>
<p>3계층에서는 IP 주소와 같은 논리적인 주소가 정의된다.</p>
<p>IP 주소는 사용자가 환경에 맞게 변경해 사용할 수 있고 네트워크 주소 부분과 호스트 주소 부분으로 나뉜다.</p>
<p>3계층을 이해할 수 있는 장비나 단말은 네트워크 주소 정보를 이용해 자신이 속한 네트워크와 원격지 네트워크를 구분할 수 있고 원격지 네트워크를 가려면 어디로 가야 하는 지 경로를 지정할 수 있다.</p>
<p>주소 체계: IP 주소</p>
<p>주요 장비: 라우터, L3 스위치</p>
<p>주요 프로토콜: ARP, IPv4, IPv6, NAT, IPSec, VRRP, 라우팅 프로토콜</p>
<h1 id="4계층---트랜스포트-계층">4계층 - 트랜스포트 계층</h1>
<hr>
<p>4계층은 주고 받는 데이터들이 정상적으로 잘 보내지도록 확인하는 역할을 한다.</p>
<p>패킷 네트워크는 데이터를 분할해 패킷에 담아 보내기 때문에, 중간에 패킷이 유실되거나 순서가 바뀌는 경우가 있다. 이러한 문제가 생겼을 때 바로잡아 주는 역할을 4계층에서 담당한다.</p>
<p>시퀀스 번호와 ACK 번호, 포트 번호를 이용해 해당 문제를 해결할 수 있다.</p>
<p>주요 장비: 로드 밸런서, 방화벽</p>
<p>주요 프로토콜: TCP, UDP, STCP, DCCP, AH, AEP</p>
<h1 id="5계층---세션-계층">5계층 - 세션 계층</h1>
<hr>
<p>5계층은 양 끝단의 응용 프로세스가 연결을 성립하도록 도와주고 연결이 안정적으로 유지되도록 관리하고 작업 완료 후에는 연결을 끊는 역할을 한다. </p>
<p>주요 프로토콜: L2TP, PPTP, NFS, RPC, RTCP, SIP, SSH</p>
<h1 id="6계층---프레젠테이션-계층">6계층 - 프레젠테이션 계층</h1>
<hr>
<p>6계층은 표현 방식이 다른 애플리케이션이나 시스템 간의 통신을 돕기 위해 하나의 통일된 구문 형식으로 변환시키는 기능을 수행한다. </p>
<p>MIME 인코딩이나 암호화, 압축, 코드 변환과 같은 동작이 이 계층에서 이루어진다.</p>
<p>주요 프로토콜: TLS, AFP, SSH</p>
<h1 id="7계층---애플리케이션-계층">7계층 - 애플리케이션 계층</h1>
<hr>
<p>7계층은 애플리케이션 프로세스를 정의하고 애플리케이션 서비스를 수행한다.</p>
<p>네트워크 소프트웨어의 UI 부분이나 사용자 입.출력 부분을 정의하는 것이 애플리케이션 계층의 역할이다.</p>
<p>주요 장비: ADC, NGFW, WAF</p>
<p>주요 프로토콜: HTTP, SMP, SMTP, STUN, TFTP, TELNET</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[24년 상반기 회고 (ft. 창업)]]></title>
            <link>https://velog.io/@keem-hyun/24%EB%85%84-%EC%83%81%EB%B0%98%EA%B8%B0-%ED%9A%8C%EA%B3%A0-ft.-%EC%B0%BD%EC%97%85-%EC%B7%A8%EC%97%85</link>
            <guid>https://velog.io/@keem-hyun/24%EB%85%84-%EC%83%81%EB%B0%98%EA%B8%B0-%ED%9A%8C%EA%B3%A0-ft.-%EC%B0%BD%EC%97%85-%EC%B7%A8%EC%97%85</guid>
            <pubDate>Mon, 01 Jul 2024 12:10:38 GMT</pubDate>
            <description><![CDATA[<p>과거의 일들을 기억하고자, 앞으로의 동기를 잃지 않고자 회고를 하는 습관을 들이려고 합니다.</p>
<h2 id="창업2023920246">창업(2023.9~2024.6)</h2>
<p>SI 회사에서 잠깐 iOS 개발자로 일을 하다가 창업팀에 합류했었다.</p>
<p>살면서 막연하게 창업해보고 싶다 하는 마음 반과 그렇지 않은 곳도 있겠지만 SI 사업 특성 상 기능들을 찍어내는 일에서 재미를 느끼지 못하고 뛰쳐 나가고 싶은 마음 반이 합쳐져 창업팀에 합류했다.</p>
<p>내가 대표자가 되어 창업을 하기에는 아직 두려웠다. 당시엔 내가 대표자로서의 역량도 없다고 생각했다. 그래서 나의 역량이 필요한 창업팀을 찾아보고자 했고, 내가 가지고 있지 않을 걸 가지고 있는 창업팀에 합류하고 싶었다.</p>
<p>거의 10군데의 창업팀과 커피챗을 했다. 
그 중에서도 가장 같이 하고싶다 라고 생각했던 곳은 대표가 몇 년간 해당 문제를 해결해보고자 매달리고 있었고, 소셜 벤처 분야에 속했던 해당 아이템은 기술로 개개인에게 정말 큰 영향을 미칠 수 있지 않을까 하는 기대감을 주었다.</p>
<p>창업팀에 합류하면서 이러한 것을 경험할 수 있지 않을까 기대했다.</p>
<ul>
<li>0 to 1의 경험</li>
<li>실리콘밸리에서 근무하고 있는 CTO가 있어 배울 수 있는 부분이 많지 않을까</li>
<li>창업 아이템은 소셜벤처였으므로 기술로 세상을 바꿀 수 있지 않을까 / 한 명에게라도 영향을 미친다면 그 영향은 다른 도메인보다 임팩트가 크지 않을까</li>
</ul>
<p>현실은 녹록치 않았다. 0 to 1은 달성하지 못했고, 개인적인 기술 역량의 깊이도 쌓지 못했다.</p>
<p>그럼에도 정말 많이 배웠고, 인격적으로 성장했다.</p>
<p>다양한 툴로 빠르게 제품을 만드는 법을 배웠다. 인격적으로 훌륭한 팀원들 덕에 각 팀원들에게 배울 점들이 많았고, 외부 고객들과 어떻게 커뮤니케이션 해야 하는지, 초기 창업의 과정에서 지원 받을 수 있는 것들이 어떤 게 있는지, 투자자들은 어떤 걸 기대하는지 배웠다.</p>
<p>결과는 실패이지만, 나 라는 사람이 어떤 성향의 사람인가도 알 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS] 인앱결제 구독, 손쉽게 StoreKit2로 구현하기]]></title>
            <link>https://velog.io/@keem-hyun/iOS-%EC%9D%B8%EC%95%B1%EA%B2%B0%EC%A0%9C-%EA%B5%AC%EB%8F%85-StoreKit2%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@keem-hyun/iOS-%EC%9D%B8%EC%95%B1%EA%B2%B0%EC%A0%9C-%EA%B5%AC%EB%8F%85-StoreKit2%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 23 Jan 2024 12:46:43 GMT</pubDate>
            <description><![CDATA[<h2 id="인앱결제를-구현하기-전-준비물">인앱결제를 구현하기 전 준비물</h2>
<ol>
<li>사업자 등록</li>
<li>통신판매업 신고 (유료 앱 등록 시 필요)</li>
</ol>
<p>이 두가지는 인터넷에 찾아보면 잘 설명된 블로그가 많아 생략하겠습니다!</p>
<h2 id="인앱결제-상품-종류">인앱결제 상품 종류</h2>
<p>1.<strong>소모성 항목 (Consumable)</strong>
소모성 앱 내 구입은 한 번 사용하면 소모되며 다시 구입할 수 있습니다. 
ex- 게임 내 포인트, 재화 등</p>
<p>2.<strong>비소모성 항목 (Non-consumable)</strong>
비소모성은 한 번 구입하면 만료되지 않습니다. 
ex- 사진 앱의 필터 등</p>
<p>3.<strong>자동 갱신 구독 (Auto-renewable subscriptions)</strong>
이번 포스팅에서 구현할 기능이며, 앱의 콘텐츠, 서비스 또는 프리미엄 기능에 대해 구독 기능을 제공하여 원하는 기간마다 자동 결제되는 항목입니다. 
ex- SaaS 등</p>
<p>4.<strong>비갱신형 구독 (Non-renewing subscriptions)</strong>
한정된 기간 동안 서비스 또는 콘텐츠에 대한 이용 권한을 제공합니다. 이 항목은 자동 갱신을 제공하지 않아 매번 구매를 다시 해야 합니다. 
ex- 게임 내 콘텐츠의 정기권 등</p>
<h2 id="앱스토어-설정">앱스토어 설정</h2>
<p><img src="https://velog.velcdn.com/images/keem-hyun/post/8877057a-8375-47cf-9be4-a08023d92d2a/image.png" alt="">
좌측에 구독 항목을 클릭합니다.
<img src="https://velog.velcdn.com/images/keem-hyun/post/f982574e-623b-4b2c-b3d7-10b21ebe5ddf/image.png" alt="">
구독 그룹을 생성합니다.
<img src="https://velog.velcdn.com/images/keem-hyun/post/23038e7e-b041-4c27-b2db-6b30f70a6fb4/image.png" alt="">
원하는 구독 상품을 생성합니다. 저는 연간과 월간 두 가지를 생성했어요.
<img src="https://velog.velcdn.com/images/keem-hyun/post/b7360288-4daf-45aa-b2d6-ffece9454e3d/image.png" alt="">
<img src="https://velog.velcdn.com/images/keem-hyun/post/1a6aea48-fa61-44f3-8c45-2eb572acd309/image.png" alt="">
구독 기간과 구독 가격, 현지화, 스크린샷을 추가해줍니다.</p>
<h2 id="storekit-file-config">StoreKit file Config</h2>
<p>StoreKit 파일을 이용하면 하단과 같은 이점이 있습니다.</p>
<blockquote>
<p>시뮬레이터에서 구매 플로우 테스트 가능
구매 플로우 unit &amp; UI 테스트 가능
로컬에서 네트워크 손실 테스트 가능
실패, 갱신, 청구 문제 등 트랜잭션 테스트 가능</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/keem-hyun/post/48191fa0-10eb-40c0-b698-84da8531918f/image.png" alt=""><img src="https://velog.velcdn.com/images/keem-hyun/post/f1bc0a27-55fc-4f5e-bf68-99d642d19425/image.png" alt="">
StoreKit 검색 후 하단의 체크박스를 선택합니다.
<img src="https://velog.velcdn.com/images/keem-hyun/post/c09b94f0-8dc9-461a-9523-cced0d6eb214/image.png" alt="">
StoreKit Configuration 에서 생성한 파일을 선택해줍니다.</p>
<h2 id="storekit2-구현">StoreKit2 구현</h2>
<p>그럼 이제  StoreKit2를 이용한 인앱결제를 구현해보겠습니다. </p>
<pre><code class="language-swift">import UIKit
import StoreKit

class PurchaseManager {

    static let shared = PurchaseManager()

    private let productIds = [&quot;com.productName.premium.monthly&quot;, &quot;com.productName.premium.yearly&quot;]
    private(set) var products: [Product] = []
    private var productsLoaded = false

    private(set) var purchasedProductIDs = Set&lt;String&gt;()
    var hasUnlockedPro: Bool {
        return !self.purchasedProductIDs.isEmpty
    }

    private var updatesTask: Task&lt;Void, Never&gt;? = nil


    private init() {} // 싱글톤으로 구현

    func loadProducts() async throws {
        guard !self.productsLoaded else { return }
        self.products = try await Product.products(for: productIds)
        self.productsLoaded = true
    }

    func purchase(_ product: Product) async throws {
        let result = try await product.purchase()

        switch result {
        case let .success(.verified(transaction)):
            await transaction.finish()
            await self.updatePurchasedProducts()
        case let .success(.unverified(_, error)):
            // 구매를 성공했으나, verified 실패
            break
        case .pending:
            // Transaction waiting on SCA (Strong Customer Authentication) or
            // approval from Ask to Buy
            break
        case .userCancelled:
            LoadingIndicator.hideLoading()
            break
        @unknown default:
            LoadingIndicator.hideLoading()
            break
        }
    }

    func updatePurchasedProducts() async {
        for await result in Transaction.currentEntitlements {
            guard case .verified(let transaction) = result else {
                continue
            }
            // 거래의 취소날짜가 없는 경우
            if transaction.revocationDate == nil {
                self.purchasedProductIDs.insert(transaction.productID)
                self.currentSubscriptionType = transaction.productID
            } else {
                // 거래가 취소된 경우
                self.purchasedProductIDs.remove(transaction.productID)
            }
        }
    }

    func startObservingTransactionUpdates() {
        updatesTask = Task(priority: .background) { [weak self] in
            for await _ in Transaction.updates {
                await self?.updatePurchasedProducts()
            }
        }
    }

    func stopObservingTransactionUpdates() {
        updatesTask?.cancel()
        updatesTask = nil
    }

}</code></pre>
<pre><code class="language-swift">private func subscribeButtonTapped() {
        var selectedProductId = &quot;&quot;

        switch self.selectedButton {
        case &quot;yearly&quot;:
            selectedProductId = &quot;com.productName.premium.yearly&quot;
        case &quot;monthly&quot;:
            selectedProductId = &quot;com.productName.premium.monthly&quot;
        default:
            // 버튼 선택 alert
        }
        // 선택된 제품 ID에 해당하는 Product 객체 찾기
        if let selectedProduct = products.first(where: { $0.id == selectedProductId }) {
            Task {
                do {
                    try await self.purchaseManager.purchase(selectedProduct)
                    self.updateSubscribeStatus()
                } catch {
                    // 에러 처리
                    print(&quot;Purchase Error: \(error)&quot;)()                
                    }
            }
        } else {
            // 해당하는 제품을 찾을 수 없는 경우
            print(&quot;에러&quot;)
        }
    }</code></pre>
<p>PurchaseManager라는 클래스를 싱글톤으로 구현하여 관리하였습니다.
productIds 배열에 앱스토어에서 정의한 제품 ID와 동일한 string을 넣어줍니다.</p>
<h2 id="loadproducts">loadProducts</h2>
<p>product id를 활용하여 앱스토어에 있는 상품의 정보(이름, 가격)를 가져올 수 있습니다.</p>
<h2 id="purchase">purchase</h2>
<h4 id="success--verified">Success – verified</h4>
<p>-&gt; 구매 성공</p>
<h4 id="success--unverified">Success – unverified</h4>
<p>-&gt; 구매는 성공했지만, StoreKit의 유효성 검증 실패.</p>
<h4 id="pending">Pending</h4>
<p>-&gt; 다른 앱을 보니, 부모에게 구매 요청을 하는 기능이 있던데 그렇게 했을 경우 구매가 승인되거나 거부될 때까지 pending 상태로 유지됩니다. 승인이 완료되면 거래가 업데이트됩니다.</p>
<h4 id="user-canceled">User Canceled</h4>
<p>-&gt; 사용자 취소</p>
<h2 id="updatepurchasedproducts">updatePurchasedProducts</h2>
<p>앱 시작 시, 구매가 이루어진 후, 트랜잭션이 업데이트될 때 이 함수를 호출하여 구매 상태에 대한 동기화가 필요합니다.
저는 AppDelegate - didFinishLaunchingWithOptions에서 startObservingTransactionUpdates 함수를 실행하여 트랜잭션 업데이트 상태를 관찰하게 하였습니다.
그리고, applicationWillTerminate에서 stopObservingTransactionUpdates 함수를 실행하여 앱이 종료될 때, 트랜잭션 관찰을 멈추게 하였습니다.</p>
<p><img src="https://velog.velcdn.com/images/keem-hyun/post/075aad71-4f73-49c0-a3f6-89f42228d5b4/image.png" alt=""></p>
<p><strong>Transaction.currentEntitlements</strong>에 대해 말씀을 드리면, StoreKit2에서는 currentEntitlements가 로컬로 캐시된 데이터를 반환한다고 합니다. 그래서 사용자가 네트워크가 끊기는 등 오프라인 상태여도 데이터를 가지고 있다가 온라인 상태일 때 푸시하여 앱이 최신 트랜잭션을 가지고 있을 수 있게 한다고 하네요.
또한 갱신, 취소 또는 청구 등의 상태를 반영하는 트랜잭션이 currentEntitlements에 업데이트가 되어서 구매 상태에 대해서도 따로 관리하지 않아도 괜찮은 것 같습니다.
그래서 저희는, 이러한 것들을 신경쓰지 않아도 된다는 게 큰 장점인 것으로 보여집니다.</p>
<h2 id="심사-주의사항">심사 주의사항</h2>
<h4 id="심사-시-주의하여-작성하거나-확인해야-하는-것들이-있습니다">심사 시 주의하여 작성하거나 확인해야 하는 것들이 있습니다.</h4>
<h4 id="1-앱-설명">1. 앱 설명</h4>
<blockquote>
</blockquote>
<p>앱 설명 내 구독 관련 description이 필요합니다.</p>
<ul>
<li>구매 확인 시 결제는 애플 ID 계정에 청구됩니다. 구독은 현재 기간이 종료되기 적어도 24시간 전에 취소하지 않으면 자동으로 갱신됩니다. 구독은 현재 기간이 종료되기 24시간 이내에 갱신으로 청구됩니다. 구독 후 앱스토어 계정 설정에서 구독을 관리하고 취소할 수 있습니다.
더 자세한 내용은 다음 링크에서 확인해주세요.
이용약관: 링크 첨부</li>
</ul>
<h4 id="2-ui">2. UI</h4>
<p>이미지는 예시로 가져온 슬립루틴의 구독 페이지입니다.
보는 것처럼 이용약관, 개인정보 취급방침에 대한 링크가 필요하고 구매복원도 필요합니다.
<img src="https://velog.velcdn.com/images/keem-hyun/post/bfb9d67d-c4f8-4ef3-a291-1c7bc5ec37b6/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/keem-hyun/post/693abd89-56b3-4569-a529-fb8fd2f22800/image.png" alt="">
또한, 무료 체험이 있다면 언제 무료 체험이 끝나는지, 끝난 후로는 얼마의 금액으로 구독이 되는지 명시가 필요합니다.</p>
<h2 id="레퍼런스">레퍼런스</h2>
<p><a href="https://www.revenuecat.com/blog/engineering/ios-in-app-subscription-tutorial-with-storekit-2-and-swift/">https://www.revenuecat.com/blog/engineering/ios-in-app-subscription-tutorial-with-storekit-2-and-swift/</a>
<a href="https://developer.apple.com/videos/play/wwdc2020/10659/">WWDC20 – Introducing StoreKit Testing in Xcode
</a><a href="https://developer.apple.com/videos/play/wwdc2022/10039/">WWDC22 – What’s new in StoreKit testing</a>
<a href="https://developer.apple.com/videos/play/wwdc2021/10114/">WWDC21 - Meet StoreKit2</a>
<a href="https://brunch.co.kr/@joonwonlee/34">리젝 없이 iOS 구독앱, 한방에 출시하기</a>
<a href="https://developer.apple.com/videos/play/wwdc2022/110404">WWDC22 - Implement proactive in-app purchase restore</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS] Side Menu 직접 만들어보자 (Programmatically)]]></title>
            <link>https://velog.io/@keem-hyun/iOS-Side-Menu-%EC%A7%81%EC%A0%91-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EC%9E%90-Programmatically</link>
            <guid>https://velog.io/@keem-hyun/iOS-Side-Menu-%EC%A7%81%EC%A0%91-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EC%9E%90-Programmatically</guid>
            <pubDate>Fri, 05 Jan 2024 10:04:07 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/keem-hyun/post/4f3fbdb5-50d7-4988-8930-1539acdbb405/image.gif" alt=""></p>
<p>프로젝트에서 Side Menu를 구현할 일이 있어 라이브러리를 사용하지 않고 직접 만들어봤습니다.</p>
<p>하단의 코드는 SideMenu가 될 뷰 컨트롤러입니다.
여기에서는 백그라운드와 corner Radius 정도만 설정해줬습니다.</p>
<pre><code class="language-swift">class SideMenuViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        self.view.layer.cornerRadius = 20
        self.view.clipsToBounds = true

        self.view.backgroundColor = .blue.withAlphaComponent(0.3)

    }

}</code></pre>
<p>SideMenu의 상위 뷰(여기서는 ViewController)에서 사용할 SideMenu ViewController와 SideMenu 등장 시 딤 처리할 dimmingView, sideMenu 버튼인 sideMenuButton을 선언해줍니다.</p>
<pre><code class="language-swift">class ViewController: UIViewController {
    private var sideMenuViewController = SideMenuViewController()
    private var dimmingView: UIView?

    private lazy var sideMenuButton = UIImageView().then {
        $0.image = UIImage(systemName: &quot;text.justify&quot;)
        $0.tintColor = .black

        let tap = UITapGestureRecognizer(target: self, action: #selector(presentSideMenu))
        $0.addGestureRecognizer(tap)
        $0.isUserInteractionEnabled = true
    }

}
</code></pre>
<p>viewDidLoad 에서 UI를 잡아줍니다. 
sideMenu가 나타나있을 때, dim 처리 된 배경 클릭 시 sideMenu를 다시 사라지게 하기 위해 handleDimmingViewTap 이라는 제스처를 추가하였습니다. </p>
<pre><code class="language-swift">override func viewDidLoad() {
        super.viewDidLoad()
        addDimmingView()

    }

    private func addDimmingView() {
        dimmingView = UIView(frame: self.view.bounds)
        dimmingView?.backgroundColor = UIColor.black.withAlphaComponent(0.5)
        dimmingView?.isHidden = true
        view.addSubview(dimmingView!)

        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleDimmingViewTap))
        dimmingView?.addGestureRecognizer(tapGesture)
    }</code></pre>
<p>handleDimmingViewTap에서 애니메이션을 통해 side menu를 사라지게 하였습니다.</p>
<pre><code class="language-swift">@objc private func handleDimmingViewTap() {
        let sideMenuVC = self.sideMenuViewController

        UIView.animate(withDuration: 0.3, animations: {
            // 사이드 메뉴를 원래 위치로 되돌림.
            sideMenuVC.view.frame = CGRect(x: -self.view.frame.width, y: 0, width: self.view.frame.width, height: self.view.frame.height)
            // 어두운 배경 뷰를 숨김.
            self.dimmingView?.alpha = 0
        }) { (finished) in
            // 애니메이션이 완료된 후 사이드 메뉴를 뷰 계층 구조에서 제거.
            sideMenuVC.view.removeFromSuperview()
            sideMenuVC.removeFromParent()
            self.dimmingView?.isHidden = true
        }
    }</code></pre>
<p>sideMenuButton 클릭 시 동작하는 presentSideMenu 입니다.
높이와 너비를 설정해서 sideMenu의 모양을 커스텀할 수 있습니다.
또한 사이드 메뉴의 크기, yPos, 사이드 메뉴가 나타나기 전 크기와 사이드 메뉴가 화면에 표시될 크기를 조정하시면 다양한 애니메이션을 구현할 수 있습니다.</p>
<pre><code class="language-swift">@objc private func presentSideMenu() {
        let sideMenuVC = self.sideMenuViewController

        // 사이드 메뉴 뷰 컨트롤러를 자식으로 추가하고 뷰 계층 구조에 추가.
        self.addChild(sideMenuVC)
        self.view.addSubview(sideMenuVC.view)

        // 사이드 메뉴의 너비를 화면 너비의 80%로 설정.
        let menuWidth = self.view.frame.width * 0.8
        let menuHeight = self.view.frame.height
        let yPos = (self.view.frame.height / 2) - (menuHeight / 2) // 중앙에 위치하도록 yPos 계산


        // 사이드 메뉴의 시작 위치를 화면 왼쪽 바깥으로 설정.
        sideMenuVC.view.frame = CGRect(x: -menuWidth, y: yPos, width: menuWidth, height: menuHeight)

        // 어두운 배경 뷰를 보이게 합니다.
        self.dimmingView?.isHidden = false
        self.dimmingView?.alpha = 0

        UIView.animate(withDuration: 0.3, animations: {
            // 사이드 메뉴를 화면에 표시.
            sideMenuVC.view.frame = CGRect(x: 0, y: yPos, width: menuWidth, height: menuHeight)
            // 어두운 배경 뷰의 투명도를 조절.
            self.dimmingView?.alpha = 0.5
        })
    }</code></pre>
<p><img src="https://velog.velcdn.com/images/keem-hyun/post/8b0aea34-aea2-4151-808d-6598525b4773/image.gif" alt="">
<img src="https://velog.velcdn.com/images/keem-hyun/post/7e9585c6-5231-4d6f-ac52-49443f89b34d/image.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Swift] 협업을 위한 디자인 시스템 만들기 ft.열거형]]></title>
            <link>https://velog.io/@keem-hyun/Swift-%ED%98%91%EC%97%85%EC%9D%84-%EC%9C%84%ED%95%9C-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0-ft.%EC%97%B4%EA%B1%B0%ED%98%95</link>
            <guid>https://velog.io/@keem-hyun/Swift-%ED%98%91%EC%97%85%EC%9D%84-%EC%9C%84%ED%95%9C-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0-ft.%EC%97%B4%EA%B1%B0%ED%98%95</guid>
            <pubDate>Sat, 26 Aug 2023 11:54:27 GMT</pubDate>
            <description><![CDATA[<p>협업을 하다 보니, 같은 이미지를 Assets에 또 추가하거나 피그마에는 같은 폰트의 크기와 행간인데 미세하게 다르다거나.. 하는 경우들이 있더라고요.</p>
<p>한 <a href="https://medium.com/@mohamedgamalmohamed48/design-systems-with-enums-in-swift-a-powerful-combination-for-consistency-and-flexibility-d97dbdfb02f3">아티클</a>을 봤는데 현재 진행중인 사이드 프로젝트에 적용할 수 있을 것 같아서 적용해 보았습니다.</p>
<h3 id="컬러">컬러</h3>
<p>앱에서 사용되는 컬러를 아래와 같이 열거형으로 정리했습니다.</p>
<pre><code class="language-swift">enum DesignSystemColor {
    case lightGreen
    case green
    case signature
    case red
    case gray
    case white
}

extension DesignSystemColor {
    var value: UIColor {
        switch self {
        case .lightGreen:
            return UIColor(hex: &quot;11D796&quot;)
        case .green:
            return UIColor(hex: &quot;009967&quot;)
        case .signature:
            return UIColor(hex: &quot;475FFD&quot;)
        case .red:
            return UIColor(hex: &quot;FF2323&quot;)
        case .gray:
            return UIColor(hex: &quot;D9D9D9&quot;)
        case .white:
            return UIColor(hex: &quot;FFFFFF&quot;)
        }
    }
}</code></pre>
<p>또한, 컬러는 하단과 같이 hex 코드로 적용할 수 있게 하였습니다.</p>
<pre><code class="language-swift">extension UIColor {

    convenience init(hex: String) {

        let scanner = Scanner(string: hex)
        _ = scanner.scanString(&quot;#&quot;)

        var rgb: UInt64 = 0
        scanner.scanHexInt64(&amp;rgb)

        let r = Double((rgb &gt;&gt; 16) &amp; 0xFF) / 255.0
        let g = Double((rgb &gt;&gt;  8) &amp; 0xFF) / 255.0
        let b = Double((rgb &gt;&gt;  0) &amp; 0xFF) / 255.0

        self.init(red: r, green: g, blue: b, alpha: 1.0)
    }
}</code></pre>
<h3 id="폰트">폰트</h3>
<p>폰트는 텍스트 굵기와 크기, 행간 순으로 네이밍을 하였으며, 폰트 또한 따로 익스텐션을 적용하여 조금 더 사용이 편하게 하였습니다.</p>
<pre><code class="language-swift">enum DesignSystemFont {
    case bold22L100
    case semibold20L140
    case semibold18L100
    case medium16L100
    case medium16L150
    case semibold14L150
    case regular14L150
    case medium12L150

}

extension DesignSystemFont {
    var value: UIFont {
        switch self {
        case .bold22L100:
            return UIFont.pretendard(.bold, size: 22)
        case .semibold20L140:
            return UIFont.pretendard(.semiBold, size: 20)
        case .semibold18L100:
            return UIFont.pretendard(.semiBold, size: 18)
        case .medium16L100:
            return UIFont.pretendard(.medium, size: 16)
        case .medium16L150:
            return UIFont.pretendard(.medium, size: 16)
        case .semibold14L150:
            return UIFont.pretendard(.semiBold, size: 14)
        case .regular14L150:
            return UIFont.pretendard(.regular, size: 14)
        case .medium12L150:
            return UIFont.pretendard(.regular, size: 14)
        }
    }

    var lineHeightMultiple: CGFloat {
        switch self {
        case .bold22L100:
            return 0.84
        case .semibold20L140:
            return 1.17
        case .semibold18L100:
            return 0.84
        case .medium16L100:
            return 0.84
        case .medium16L150:
            return 1.26
        case .semibold14L150:
            return 1.26
        case .regular14L150:
            return 1.26
        case .medium12L150:
            return 1.26
        }
    }

}
</code></pre>
<pre><code class="language-swift">extension UIFont {

    enum Pretendard {

        case bold
        case extraBold
        case medium
        case semiBold
        case regular

        var value: String {
            switch self {
            case .bold:
                return &quot;Pretendard-Bold&quot;
            case .medium:
                return &quot;Pretendard-Medium&quot;
            case .semiBold:
                return &quot;Pretendard-SemiBold&quot;
            case .extraBold:
                return &quot;Pretendard-ExtraBold&quot;
            case .regular:
                return &quot;Pretendard-Regular&quot;
            }
        }
    }

    static func pretendard(_ type: Pretendard, size: CGFloat) -&gt; UIFont {
        return UIFont(name: type.value, size: size) ?? UIFont.systemFont(ofSize: size)
    }
}
</code></pre>
<h3 id="아이콘">아이콘</h3>
<pre><code class="language-swift">enum DesignSystemIcon {
    case setting
    case emptyAlarm
    case alarm
    case filter
    case verticalEllipsis
    case emptyCircleCheckmark
    case circleCheckmark
    case emptySquareCheckmark
    case squareCheckmark

}

extension DesignSystemIcon {
    var imageName: String {
        switch self {
        case .setting:
            return &quot;setting&quot;
        case .emptyAlarm:
            return &quot;emptyAlarm&quot;
        case .alarm:
            return &quot;alarm&quot;
        case .filter:
            return &quot;filter&quot;
        case .verticalEllipsis:
            return &quot;verticalEllipsis&quot;
        case .emptyCircleCheckmark:
            return &quot;emptyCircleCheckmark&quot;
        case .circleCheckmark:
            return &quot;circleCheckmark&quot;
        case .emptySquareCheckmark:
            return &quot;emptySquareCheckmark&quot;
        case .squareCheckmark:
            return &quot;squareCheckmark&quot;
        }
    }
}
</code></pre>
<p>폰트와 컬러, 아이콘의 사용법은 하단과 같습니다.</p>
<pre><code class="language-swift">private let testLabel = UILabel().then{
        $0.font = DesignSystemFont.bold22L100.value
        $0.textColor = DesignSystemColor.signature.value
    }

    private let testView = UIImageView().then {
        $0.image = UIImage(named: DesignSystemIcon.circleCheckmark.imageName)
    }</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS] TableView, SearchBar 이용하여 검색 기능 만들기]]></title>
            <link>https://velog.io/@keem-hyun/iOS-TableView-SearchBar-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%EA%B2%80%EC%83%89-%EA%B8%B0%EB%8A%A5-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@keem-hyun/iOS-TableView-SearchBar-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%EA%B2%80%EC%83%89-%EA%B8%B0%EB%8A%A5-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Fri, 14 Jul 2023 08:29:58 GMT</pubDate>
            <description><![CDATA[<p>프로젝트를 진행하면서 직군을 선택하기 위한 기능을 만들었는데, 이를 공유하고자 합니다.</p>
<h2 id="preview">Preview</h2>
<p><img src="https://velog.velcdn.com/images/keem-hyun/post/5b63b5e4-b52b-4392-8d29-efe73c1620a9/image.gif" alt=""></p>
<pre><code class="language-swift">class ViewController: UIViewController {
    // 원하는 아이템을 넣어주면 됩니다.
    private var items = []
    // 검색 결과를 담는 배열
    private var filteredItems: [String] = []
    // checkButton 선택 셀 index
    private var previousIndexPath: IndexPath?
    private var selectedIndexPath: IndexPath?

    // searchBar 설정
    private lazy var searchBar : UISearchBar = {
        let search = UISearchBar()
        search.delegate = self
        search.searchBarStyle = .minimal
        search.showsCancelButton = true
        search.searchTextField.backgroundColor = .clear
        search.searchTextField.borderStyle = .none
        return search
    }()

    // 직무를 보여줄 tableView
    private let tableView : UITableView = {
        let tableView = UITableView()
        tableView.separatorStyle = .none
        return tableView
    }()


}</code></pre>
<p>그 다음 viewdidload 에서 TableView와 searchBar를 설정합니다. (레이아웃 코드는 제거했습니다)</p>
<pre><code class="language-swift">    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white

        self.configureTableView()

        self.reload()
    }
    private func configureTableView() {
        tableView.dataSource = self
        tableView.delegate = self

        // &quot;NameCell&quot; 이라는 이름의 커스텀 셀을 사용했습니다.
        tableView.register(NameCell.self, forCellReuseIdentifier: &quot;cell&quot;)

        self.view.addSubview(tableView)
    }

    private func reload() {
        self.tableView.reloadData()
    }
</code></pre>
<p>UISearchBarDelegate를 채택하여 필요한 메서드들을 구현해줍니다.</p>
<pre><code class="language-swift">    extension ViewController: UISearchBarDelegate {
    // 유저가 텍스트 입력했을 때
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        filterItems(with: searchText)
        self.reload()
    }

    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        // 취소 버튼을 누를 때 검색어를 초기화하고 테이블 뷰를 갱신합니다.
        searchBar.text = nil
        searchBar.resignFirstResponder() // 키보드 내림
        filterItems(with: &quot;&quot;)
        self.reload()
    }

    private func filterItems(with searchText: String) {
        if searchText.isEmpty {
        // 검색어가 비어있으면 모든 항목을 포함
            filteredItems = items 
        } else {
            // 검색어를 기준으로 items 배열을 필터링하여 검색 결과를 filteredItems에 저장
            filteredItems = items.filter { $0.range(of: searchText, options: .caseInsensitive) != nil }
        }
    }

}
</code></pre>
<p>TableView를 관리하는데 필요한 protocol들을 채택하여 필요한 메서드들을 구현해줍니다.</p>
<pre><code class="language-swift">extension ViewController: UITableViewDelegate, UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -&gt; Int {
        let sectionTitles = getSectionTitles()
        return sectionTitles.count
    }


    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -&gt; Int {
        let filteredItemsInSection = getFilteredItemsInSection(section)
        return filteredItemsInSection.count
    }


    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&gt; UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: &quot;cell&quot;, for: indexPath) as! NameCell

        let filteredItemsInSection = getFilteredItemsInSection(indexPath.section)

        let item = filteredItemsInSection[indexPath.row]
        cell.textLabel?.text = item

        if let selectedIndexPath = selectedIndexPath, selectedIndexPath == indexPath {
            cell.checkButton.isHidden = false
        } else {
            cell.checkButton.isHidden = true
        }

        return cell
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let previousIndexPath = selectedIndexPath  // 이전에 선택된 셀의 인덱스 저장
        selectedIndexPath = indexPath  // 선택된 셀의 인덱스 업데이트

        // 이전에 선택된 셀의 인덱스와 현재 선택한 셀의 인덱스가 같으면 체크 버튼을 숨깁니다.
        if previousIndexPath == indexPath {
            if let cell = tableView.cellForRow(at: indexPath) as? NameCell {
                cell.checkButton.isHidden = true
            }
            selectedIndexPath = nil  // 선택된 셀의 인덱스를 nil로 설정하여 선택 해제
        } else {
            // 이전에 선택된 셀의 인덱스와 현재 선택한 셀의 인덱스가 다르면 이전에 선택된 셀을 업데이트합니다.
            if let previousIndexPath = previousIndexPath, let cell = tableView.cellForRow(at: previousIndexPath) as? NameCell {
                cell.checkButton.isHidden = true
            }
            if let cell = tableView.cellForRow(at: indexPath) as? NameCell {
                cell.checkButton.isHidden = false
            }
        }
    }


    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -&gt; String? {
        // 섹션 헤더에 표시할 문자열을 반환합니다.
        let sectionTitles = getSectionTitles()
        if section &lt; sectionTitles.count {
            return sectionTitles[section]
        }
        return nil
    }

    private func searchBarIsEmpty() -&gt; Bool {
        return searchBar.text?.isEmpty ?? true
    }

    private func getSectionTitles() -&gt; [String] {
        // 이름의 첫 글자로 이루어진 섹션 타이틀 배열을 반환합니다.
        let sectionTitles = items.map { name -&gt; String in
            if let firstCharacter = name.first, let unicodeScalar = firstCharacter.unicodeScalars.first {
                let scalarValue = unicodeScalar.value
                if (0xAC00 &lt;= scalarValue &amp;&amp; scalarValue &lt;= 0xD7A3) { // 첫 글자가 한글인 경우
                    let unicodeValue = scalarValue - 0xAC00
                    let choseongIndex = Int(unicodeValue / (21 * 28))
                    let choseong = [&quot;ㄱ&quot;, &quot;ㄲ&quot;, &quot;ㄴ&quot;, &quot;ㄷ&quot;, &quot;ㄸ&quot;, &quot;ㄹ&quot;, &quot;ㅁ&quot;, &quot;ㅂ&quot;, &quot;ㅃ&quot;, &quot;ㅅ&quot;, &quot;ㅆ&quot;, &quot;ㅇ&quot;, &quot;ㅈ&quot;, &quot;ㅉ&quot;, &quot;ㅊ&quot;, &quot;ㅋ&quot;, &quot;ㅌ&quot;, &quot;ㅍ&quot;, &quot;ㅎ&quot;]
                    let choseongCharacter = choseong[choseongIndex]
                    return choseongCharacter
                } else { // 첫 글자가 한글이 아닌 경우
                    return name.prefix(1).uppercased()
                }
            } else { // 이름이 비어있는 경우
                return &quot;&quot;
            }
        }

        let uniqueTitles = Array(Set(sectionTitles)).sorted()
        return uniqueTitles
    }

    private func getFilteredItemsInSection(_ section: Int) -&gt; [String] {
        let sectionTitles = getSectionTitles()
        let sectionTitle = sectionTitles[section]

        let filteredItemsInSection: [String]
        if searchBarIsEmpty() {
            filteredItemsInSection = items.filter { item -&gt; Bool in
                if let firstCharacter = item.first, let unicodeScalar = firstCharacter.unicodeScalars.first {
                    let scalarValue = unicodeScalar.value
                    if (0xAC00 &lt;= scalarValue &amp;&amp; scalarValue &lt;= 0xD7A3) { // 첫 글자가 한글인 경우
                        let unicodeValue = scalarValue - 0xAC00
                        let choseongIndex = Int(unicodeValue / (21 * 28))
                        let choseong = [&quot;ㄱ&quot;, &quot;ㄲ&quot;, &quot;ㄴ&quot;, &quot;ㄷ&quot;, &quot;ㄸ&quot;, &quot;ㄹ&quot;, &quot;ㅁ&quot;, &quot;ㅂ&quot;, &quot;ㅃ&quot;, &quot;ㅅ&quot;, &quot;ㅆ&quot;, &quot;ㅇ&quot;, &quot;ㅈ&quot;, &quot;ㅉ&quot;, &quot;ㅊ&quot;, &quot;ㅋ&quot;, &quot;ㅌ&quot;, &quot;ㅍ&quot;, &quot;ㅎ&quot;]
                        let choseongCharacter = choseong[choseongIndex]
                        return &quot;\(choseongCharacter)&quot; == sectionTitle
                    } else { // 첫 글자가 한글이 아닌 경우
                        return item.prefix(1).uppercased() == sectionTitle
                    }
                } else { // 이름이 비어있는 경우
                    return sectionTitle.isEmpty
                }
            }
        } else {
            filteredItemsInSection = filteredItems.filter { item -&gt; Bool in
                if let firstCharacter = item.first, let unicodeScalar = firstCharacter.unicodeScalars.first {
                    let scalarValue = unicodeScalar.value
                    if (0xAC00 &lt;= scalarValue &amp;&amp; scalarValue &lt;= 0xD7A3) { // 첫 글자가 한글인 경우
                        let unicodeValue = scalarValue - 0xAC00
                        let choseongIndex = Int(unicodeValue / (21 * 28))
                        let choseong = [&quot;ㄱ&quot;, &quot;ㄲ&quot;, &quot;ㄴ&quot;, &quot;ㄷ&quot;, &quot;ㄸ&quot;, &quot;ㄹ&quot;, &quot;ㅁ&quot;, &quot;ㅂ&quot;, &quot;ㅃ&quot;, &quot;ㅅ&quot;, &quot;ㅆ&quot;, &quot;ㅇ&quot;, &quot;ㅈ&quot;, &quot;ㅉ&quot;, &quot;ㅊ&quot;, &quot;ㅋ&quot;, &quot;ㅌ&quot;, &quot;ㅍ&quot;, &quot;ㅎ&quot;]
                        let choseongCharacter = choseong[choseongIndex]
                        return &quot;\(choseongCharacter)&quot; == sectionTitle
                    } else { // 첫 글자가 한글이 아닌 경우
                        return item.prefix(1).uppercased() == sectionTitle
                    }
                } else { // 이름이 비어있는 경우
                    return sectionTitle.isEmpty
                }
            }
        }

        return filteredItemsInSection
    }


}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS] FSCalendar - 주간 캘린더 & 커스텀]]></title>
            <link>https://velog.io/@keem-hyun/iOS-FSCalendar-%EC%A3%BC%EA%B0%84-%EC%BA%98%EB%A6%B0%EB%8D%94-%EC%BB%A4%EC%8A%A4%ED%85%80</link>
            <guid>https://velog.io/@keem-hyun/iOS-FSCalendar-%EC%A3%BC%EA%B0%84-%EC%BA%98%EB%A6%B0%EB%8D%94-%EC%BB%A4%EC%8A%A4%ED%85%80</guid>
            <pubDate>Sun, 11 Jun 2023 06:33:54 GMT</pubDate>
            <description><![CDATA[<p>FSCalendar는 iOS에서 캘린더를 직접 구현하지 않고, 가져다 쓸 수 있는 캘린더 라이브러리입니다.</p>
<p>이번에 해커톤을 진행하면서, 직접 구현하기에는 시간적 제약이 있어 관련 라이브러리를 찾다가 발견해서 사용해봤습니다. 사용하면서 설정했던 부분들을 공유하겠습니다.</p>
<blockquote>
<p>하단 링크를 통해 예시나 설치하는 방법을 알 수 있어요.
<a href="https://github.com/WenchaoD/FSCalendar">https://github.com/WenchaoD/FSCalendar</a></p>
</blockquote>
<h3 id="주간-달력-설정">주간 달력 설정</h3>
<p>FSCalendar에서는 달력 모드를 주간 혹은 월간으로 설정할 수 있습니다.</p>
<pre><code class="language-swift">private let calendar : FSCalendar = {
        let calendar = FSCalendar(frame: .zero)
        calendar.scope = .week // 주간 달력 설정
        calendar.scope = .month // 월간 달력 설정
        return calendar
    }()</code></pre>
<h3 id="delegate-설정">Delegate 설정</h3>
<pre><code class="language-swift">calendar.delegate = self
calendar.dataSource = self</code></pre>
<h3 id="요일-관련-설정">요일 관련 설정</h3>
<p>FSCalendar에서는 요일(월~일)부분을 설정하실 때는 weekday 관련 키워드로 보시면 됩니다.</p>
<pre><code class="language-swift">private let calendar : FSCalendar = {
        let calendar = FSCalendar(frame: .zero)
        calendar.weekdayHeight // 높이 설정
        calendar.appearance.weekdayFont // 폰트 설정
        calendar.appearance.weekdayTextColor // 텍스트 컬러 설정
        calendar.locale = Locale(identifier: &quot;us_US&quot;) // 요일 설정 us면 Mon, ko_KR 이면 월,화...
        calendar.firstWeekday = 2 // 기본 설정은 일요일(일~토)이 시작이나, 2로 설정하면 월요일부터 시작합니다(월~일)
        return calendar
    }()</code></pre>
<h3 id="일수-관련-설정">일수 관련 설정</h3>
<pre><code class="language-swift">private let calendar : FSCalendar = {
        let calendar = FSCalendar(frame: .zero)
        calendar.appearance.titleFont // 일(1,2,3...) 폰트 설정
        calendar.appearance.titleDefaultColor // 선택되지 않은 날의 기본 컬러 설정
        calendar.appearance.titleSelectionColor // 선택된 날의 컬러 설정
        calendar.appearance.titlTodayColor // 금일 컬러 설정
        return calendar
    }()</code></pre>
<h3 id="select-설정">select 설정</h3>
<p>FSCalendar에서 날짜를 선택했을 때, default 값으로 일수를 둘러싼 동그라미가 나옵니다.
<img src="https://velog.velcdn.com/images/keem-hyun/post/cf24a24b-421b-446d-962b-d3810d20a322/image.png" alt="">
전 이 동그라미가 싫어서, 하단과 같이 설정해주었습니다.</p>
<pre><code class="language-swift">private let calendar : FSCalendar = {
        let calendar = FSCalendar(frame: .zero)
        calendar.appearance.selectionColor = .clear
        return calendar
    }()</code></pre>
<p><img src="https://velog.velcdn.com/images/keem-hyun/post/580ffecc-2477-4f20-99db-b4193757e35d/image.png" alt="">
오늘 날짜도 동그라미가 보기 싫어서 클리어 처리하겠습니다.</p>
<pre><code class="language-swift">private let calendar : FSCalendar = {
        let calendar = FSCalendar(frame: .zero)
        calendar.appearance.todayColor = .clear
        return calendar
    }()</code></pre>
<p><img src="https://velog.velcdn.com/images/keem-hyun/post/18cf8964-28ed-4f5b-9d3e-02a61bbeae1e/image.png" alt=""></p>
<h3 id="주간-달력-커스텀">주간 달력 커스텀</h3>
<p>FSCalendar는 밑에서 보는 것처럼 기존 설정이 스와이프를 통한 넘김입니다. 전 버튼을 통해 달력 이동을 하고 싶었고, yyyy년 mm월을 원하는 곳에 두고 싶었습니다.
<img src="https://velog.velcdn.com/images/keem-hyun/post/644c1e87-9811-4b82-b6bf-bb823307780c/image.png" alt=""></p>
<pre><code class="language-swift">// yyyy년 mm월 
private let titleLabel : UILabel = {
        let label = UILabel()
        label.text = &quot;&quot;
        return label
    }()

    // 이전 주 이동 버튼
    private lazy var previousButton : UIButton = {
        let button = UIButton()
        button.setImage(UIImage(systemName: &quot;chevron.left&quot;), for: .normal)
        button.addTarget(self, action: #selector(prevCurrentPage), for: .touchUpInside)
        return button
    }()
    // 다음 주 이동 버튼
    private lazy var nextButton : UIButton = {
        let button = UIButton()
        button.setImage(UIImage(systemName: &quot;chevron.right&quot;), for: .normal)
        button.addTarget(self, action: #selector(nextCurrentPage), for: .touchUpInside)
        return button
    }()

    private var currentPage: Date?

    private let today: Date = {
        return Date()
    }()
    // 다음 주로 이동 함수
    @objc private func nextCurrentPage(isPrev: Bool) {
        let cal = Calendar.current
        var dateComponents = DateComponents()
        dateComponents.weekOfMonth = 1

        self.currentPage = cal.date(byAdding: dateComponents, to: self.currentPage ?? self.today)
        self.calendar.setCurrentPage(self.currentPage!, animated: true)
    }
    // 이전 주로 이동 함수
    @objc private func prevCurrentPage(isPrev: Bool) {
        let cal = Calendar.current
        var dateComponents = DateComponents()
        dateComponents.weekOfMonth = -1

        self.currentPage = cal.date(byAdding: dateComponents, to: self.currentPage ?? self.today)
        self.calendar.setCurrentPage(self.currentPage!, animated: true)
    }
    // mm월이 바뀌면 자동으로 변경
    extension ViewController: FSCalendarDelegate, FSCalendarDataSource, FSCalendarDelegateAppearance {
    func calendarCurrentPageDidChange(_ calendar: FSCalendar) {
        self.titleLabel.text = self.dateFormatter.string(from: calendar.currentPage)
    }
}</code></pre>
<p>위와 같이 만들고, 기존 캘린더의 헤더를 아래의 코드를 통해 없애주면 됩니다.</p>
<pre><code class="language-swift">private let calendar : FSCalendar = {
        let calendar = FSCalendar(frame: .zero)
        calendar.headerHeight = 0
        return calendar
    }()</code></pre>
<p><img src="https://velog.velcdn.com/images/keem-hyun/post/a398cd6d-452c-4a42-8668-5e34d258989f/image.png" alt=""></p>
<h3 id="커스텀하면서-생긴-문제">커스텀하면서 생긴 문제</h3>
<p>처음에 헤더를 건드렸더니, 이렇게 일수가 잘리는 문제가 생겼습니다.
<img src="https://velog.velcdn.com/images/keem-hyun/post/c406b8d8-dc1c-4211-aeab-975865e9bfc7/image.png" alt="">
구글링을 했을 때, 생각보다 저와 비슷한 문제를 겪는 사람들이 꽤 있었고 그 중에서 이 분의 답변을 보게 되었습니다.</p>
<blockquote>
<p><a href="https://github.com/WenchaoD/FSCalendar/issues/655">https://github.com/WenchaoD/FSCalendar/issues/655</a></p>
</blockquote>
<p>첫번째 방법처럼 라이브러리 내 FScalendar.m 파일에서 이렇게 되어 있던 코드를<img src="https://velog.velcdn.com/images/keem-hyun/post/c402cae9-ffd2-4d19-907d-9c3c755cd721/image.png" alt="">
하단과 같이 바꿔주었습니다.<img src="https://velog.velcdn.com/images/keem-hyun/post/5650a270-b3b2-4be8-af42-026503892553/image.png" alt=""></p>
<p>이 부분은 좀 더 찾아봐야 하는데, 내부에서 어떤 계산에 의해 높이가 리턴되다보니 제가 설정했던 높이 값이 안 들어가지 않았을까 싶네요. 위와 같이 변경을 하니, 높이가 잘리지 않고 잘 나타나는 것을 볼 수 있습니다.
<img src="https://velog.velcdn.com/images/keem-hyun/post/92a10a60-c63f-44f5-9530-3cb930204900/image.png" alt=""></p>
<h3 id="마치면서">마치면서</h3>
<p>라이브러리 내부 코드에 주석들로 해당 코드들이 어떤 코드들인지 잘 정리되어 있어, 제 입맛대로 바꾸기 괜찮다고 생각이 들었습니다. 위와 같은 문제를 해결하는데 시간이 좀 걸렸지만 구현하면서 문제가 있다면 라이브러리 내 코드들을 잘 살펴보면서 사용하시면 괜찮을 것 같습니다.
<img src="https://velog.velcdn.com/images/keem-hyun/post/57e83a89-5c13-4ab2-82a1-d99c7c5c40e0/image.png" alt=""></p>
<h4 id="fscalendarappearanceh-파일-내부-중">FSCalendarAppearance.h 파일 내부 중</h4>
<p><img src="https://velog.velcdn.com/images/keem-hyun/post/6ddfb553-99c1-47dc-94da-2cb7efa884d6/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스 LV.1] Swift - 두 정수 사이의 합]]></title>
            <link>https://velog.io/@keem-hyun/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-LV.1-Swift-%EB%91%90-%EC%A0%95%EC%88%98-%EC%82%AC%EC%9D%B4%EC%9D%98-%ED%95%A9</link>
            <guid>https://velog.io/@keem-hyun/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-LV.1-Swift-%EB%91%90-%EC%A0%95%EC%88%98-%EC%82%AC%EC%9D%B4%EC%9D%98-%ED%95%A9</guid>
            <pubDate>Sun, 14 May 2023 03:05:37 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/keem-hyun/post/8c6a068f-1a3b-41b3-81a5-b092191fb436/image.png" alt=""></p>
<p>첫 풀이를 하단과 같이 풀었는데, 시간 초과가 걸림. </p>
<pre><code>func solution(_ a:Int, _ b:Int) -&gt; Int64 {
    if a &gt; b {
        return (b...a).reduce(0){Int64($0) + Int64($1)}
    } else {
        return (a...b).reduce(0){Int64($0) + Int64($1)}    
    }

}</code></pre><p><img src="https://velog.velcdn.com/images/keem-hyun/post/b633e374-5ad1-44c6-bd83-027db3146a18/image.png" alt=""></p>
<p>다른 풀이를 참고해 하단과 같이 풀었다.</p>
<pre><code>func solution(_ a:Int, _ b:Int) -&gt; Int64 {
    return Int64((a&gt;b ? b...a : a...b).reduce(0){$0 + $1})
}</code></pre><p><img src="https://velog.velcdn.com/images/keem-hyun/post/e1fbc37f-0b2a-47b0-b0fe-d2676377d6e4/image.png" alt=""></p>
<p>풀이를 찾다 보니 고차함수로 풀면 훨씬 느리다는 말이 있어 단순 구현으로 제출해봄.</p>
<pre><code>func solution(_ a:Int, _ b:Int) -&gt; Int64 {
    var sum = 0
    for i in (a&gt;b ? b...a : a...b) {
        sum += i
    }
    return Int64(sum)
}</code></pre><p><img src="https://velog.velcdn.com/images/keem-hyun/post/a737f45b-580e-4190-a9ce-3f3247a72e42/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스 LV.1] Swift - 나머지가 1이 되는 수 찾기]]></title>
            <link>https://velog.io/@keem-hyun/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-LV.1-Swift-%EB%82%98%EB%A8%B8%EC%A7%80%EA%B0%80-1%EC%9D%B4-%EB%90%98%EB%8A%94-%EC%88%98-%EC%B0%BE%EA%B8%B0</link>
            <guid>https://velog.io/@keem-hyun/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-LV.1-Swift-%EB%82%98%EB%A8%B8%EC%A7%80%EA%B0%80-1%EC%9D%B4-%EB%90%98%EB%8A%94-%EC%88%98-%EC%B0%BE%EA%B8%B0</guid>
            <pubDate>Fri, 12 May 2023 03:33:05 GMT</pubDate>
            <description><![CDATA[<p>문제</p>
<p><img src="https://velog.velcdn.com/images/keem-hyun/post/8a647390-07d5-4462-b58b-9e8de744972b/image.png" alt=""></p>
<p>풀이</p>
<pre><code>import Foundation

func solution(_ n:Int) -&gt; Int {
    var result = (1...n).filter{n % $0 == 1}
    return result[0]
}</code></pre><p>최근에 다른 문제를 풀면서 filter를 써봐서 보자마자 filter를 쓰고 싶다는 생각이 들음.</p>
<ol>
<li>result 변수에 n을 1부터 n까지 나눠서 나머지가 1인 수를 넣는다.</li>
<li>result에 배열로 값이 들어있으니, index 0인 값을 반환.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS] 간단한 알람 앱 만들기]]></title>
            <link>https://velog.io/@keem-hyun/iOS-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%95%8C%EB%9E%8C-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@keem-hyun/iOS-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%95%8C%EB%9E%8C-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Tue, 25 Apr 2023 08:08:17 GMT</pubDate>
            <description><![CDATA[<p>간단한 알람 앱이지만, 만들면서 어려웠던 부분이 있어 추후에 다시 보고자 기록합니다!</p>
<p><img src="https://velog.velcdn.com/images/keem-hyun/post/e8e0f9f6-679d-4f05-94bc-28da6a6150a2/image.gif" alt=""></p>
<h4 id="기능-설명">기능 설명:</h4>
<ol>
<li>홈 화면에서 + 버튼 누르면 알람 추가 뷰로 이동. </li>
<li>알람 추가 뷰에서 원하는 시간을 고르고 저장을 누르면 홈 화면으로 데이터 전달.</li>
<li>전달받은 데이터를 홈 화면에서 테이블뷰로 표시.</li>
<li>현재 시각과 저장한 알람 시간이 동일하면 Alert 창 띄우기.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/keem-hyun/post/02b53853-d1dc-4fcf-8ff3-b9cd50b3583e/image.png" alt=""></p>
<pre><code>import UIKit

// ViewController(메인뷰) 코드

class ViewController: UIViewController {

    var timePickerData: [String] = [] // 전달받은 데이터 배열로 저장

    var isAlertOn = false // alert 창 

    @IBOutlet weak var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.delegate = self
        tableView.dataSource = self

        // 매 초 updateTime 함수 실행하여 현재시간과 전달 받은 데이터 비교, 시간 일치하면 alert창 띄우기.
        Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTime), userInfo: nil, repeats: true)
    }

    @objc func updateTime() {
        let formatter = DateFormatter()
        formatter.dateFormat = &quot;a hh:mm&quot;
        formatter.locale = Locale(identifier: &quot;ko_KR&quot;) // 오전, 오후로 포맷 변경
        let currentTime = formatter.string(from: Date()) // 현재 시간

        if isAlertOn {
            return
        }

        for data in timePickerData {
            if data == currentTime {
                self.isAlertOn = true

                Timer.scheduledTimer(timeInterval: 60.0, target: self, selector: #selector(self.timerOn), userInfo: nil, repeats: false) // 60초 후 alert 창 종료 

                let alert = UIAlertController(title: &quot;알림&quot;, message: &quot;설정된 시간입니다.&quot;, preferredStyle: .alert)
                alert.addAction(UIAlertAction(title: &quot;확인&quot;, style: UIAlertAction.Style.default) {UIAlertAction in})
                self.present(alert, animated:true, completion: nil)
            }
        }

    }

    @objc func timerOn() {
        isAlertOn = false
    }

    @IBAction func moveVCButton(_ sender: UIBarButtonItem) {
        guard let addAlarmVC = storyboard?.instantiateViewController(withIdentifier: &quot;AlarmEditViewController&quot;) as? AlarmEditViewController else {return}
        addAlarmVC.delegate = self
        self.navigationController?.pushViewController(addAlarmVC, animated: true)
    }
}

extension ViewController: AlarmDelegate {
    func alarmDelegate(data: String) {
        timePickerData.append(data) // 전달받은 데이터, 배열로 값 추가
        self.tableView.reloadData() // 리로드하여 화면에 표시
    }
}

extension ViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -&gt; Int {
        return timePickerData.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&gt; UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: &quot;AlarmTableViewCell&quot;, for: indexPath) as! AlarmTableViewCell
        cell.AlarmTableViewCell.text = timePickerData[indexPath.row]
        return cell
    }

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -&gt; CGFloat {
        return 100
    }
}
</code></pre><pre><code>import UIKit
// AlarmEditViewController(알람 추가 뷰) 코드

protocol AlarmDelegate: AnyObject {
    func alarmDelegate(data: String)
}

class AlarmEditViewController: UIViewController {
    @IBOutlet weak var dismissLabel: UILabel!
    weak var delegate: AlarmDelegate? 
    var alarmData: String?

    override func viewDidLoad() {
        super.viewDidLoad()

        let dismissTapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissClicked))
        dismissLabel.addGestureRecognizer(dismissTapGesture)
        dismissLabel.isUserInteractionEnabled = true
    }

    @objc func dismissClicked(sender: UITapGestureRecognizer) {
        dismiss(animated: true)
    }

    @IBAction func saveButton(_ sender: UIButton) {
        guard let alarmData = alarmData else { return }
        self.delegate?.alarmDelegate(data: alarmData)
        print(&quot;저장버튼: \(alarmData)&quot;)
        print(&quot;delegate: \(delegate!)&quot;)
        self.navigationController?.popViewController(animated: true)
    }

    @IBAction func timePicker(_ sender: UIDatePicker) {
        let formatter = DateFormatter()
        formatter.dateFormat = &quot;a hh:mm&quot;
        formatter.locale = Locale(identifier: &quot;ko_KR&quot;)
        alarmData = formatter.string(from: sender.date)
        print(&quot;alarmData: \(alarmData!)&quot;)
    }
}</code></pre><h4 id="어려웠던-부분-delegate을-이용한-화면-간-데이터-전달-그리고-tableviewreload">어려웠던 부분: Delegate을 이용한 화면 간 데이터 전달, 그리고 tableView.reload..</h4>
<p>저는 이 기능을 구현하면서 Delegate를 이용해봐야겠다 라고 생각했어요, 아직 Delegate가 익숙치 않기 때문에요.
하단에 서술한 대로 Delegate로 데이터 전달을 구현했지만, 메인뷰에는 데이터가 보이지 않아 많이 헤맸는데요. 
알고 보니, 배열에 데이터를 추가한 후 테이블뷰 리로드를 해주지 않아 보이지 않았던 문제라 테이블뷰 리로드 메서드를 실행해주었더니 뷰가 잘 보였습니다.</p>
<h4 id="delegate-활용-방법">Delegate 활용 방법:</h4>
<p>본 앱 같은 경우는 알람 추가 뷰(AlarmEditViewController)에서 메인 뷰에 데이터를 전달하고 있어요.</p>
<ol>
<li>알람 추가 뷰에 protocol 선언. 프로토콜 내 메서드를 선언만 하고 구현하지는 않습니다. 구현은 데이터를 전달 받은 뷰(메인뷰)에서 할거에요. </li>
</ol>
<pre><code>protocol AlarmDelegate: AnyObject {
    func alarmDelegate(data: String)
}</code></pre><ol start="2">
<li><p>알람 추가 뷰 내에 delegate 변수 선언, 프로토콜 채택.</p>
<pre><code>weak var delegate: AlarmDelegate?</code></pre></li>
<li><p>데이터를 전달할 부분에서 delegate 변수를 활용하여 데이터 전달. 여기서는 저장 버튼을 클릭 시, 메인뷰에 전달했어요.</p>
<pre><code>@IBAction func saveButton(_ sender: UIButton) {
     guard let alarmData = alarmData else { return }
     self.delegate?.alarmDelegate(data: alarmData) // 메인뷰에 데이터 전달.
     print(&quot;저장버튼: \(alarmData)&quot;)
     print(&quot;delegate: \(delegate!)&quot;)
     self.navigationController?.popViewController(animated: true)
 }</code></pre></li>
<li><p>데이터를 메인 뷰에 전달해줬으니, 메인 뷰에서 데이터를 받아 원하는 기능 구현. 
메인뷰에서 프로토콜(AlarmDelegate)을 채택해줘야 해요. 그러면 메소드를 구현하라고 알림이 뜨니 원하는 기능 구현. 여기서는 전달 받은 데이터를 배열에 담아줬어요.</p>
<pre><code>extension ViewController: AlarmDelegate {
 func alarmDelegate(data: String) {
     timePickerData.append(data)
     self.tableView.reloadData()
 }
}</code></pre><p>전체 코드는 제 <a href="https://github.com/keem-hyun/study-iOS-all/tree/main/AlarmTable">깃허브</a>에서 보실 수 있어요.</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스 LV.0] Swift - 피자 나눠 먹기 (2)]]></title>
            <link>https://velog.io/@keem-hyun/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-LV.0-Swift-%ED%94%BC%EC%9E%90-%EB%82%98%EB%88%A0-%EB%A8%B9%EA%B8%B0-2</link>
            <guid>https://velog.io/@keem-hyun/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-LV.0-Swift-%ED%94%BC%EC%9E%90-%EB%82%98%EB%88%A0-%EB%A8%B9%EA%B8%B0-2</guid>
            <pubDate>Wed, 01 Feb 2023 16:57:18 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/keem-hyun/post/6deac0d5-97cf-45ea-8665-08afd68ea5b4/image.png" alt=""></p>
<p>안 풀려서 고민하다 보니, 점점 어렵게 생각하게 됐다. 전에 풀었던, 최대공약수를 이용하면 될 것 같은 느낌? 그런데, 바로 풀지는 못해서 이번에도..다른 분의 풀이를 참고하여.. 그래서 최대공약수와 최소공배수 함수를 만들어 풀어보았다.</p>
<pre><code class="language-swift">import Foundation
//최대공약수 gcd 함수
func gcd(_ a:Int, _ b:Int) -&gt; Int {
    if b == 0 {
        return a
    } else {
        return gcd(b, a % b)
    }
}
//최소공배수 lcm 함수
func lcm(_ a:Int, _ b:Int) -&gt; Int {
    return a*b / gcd(a,b)
}

func solution(_ n:Int) -&gt; Int {
    return lcm(n,6) / 6
}</code></pre>
<p>풀고 나서 다른 분의 풀이를 보았는데, 이렇게 풀 수도 있네? 라는 생각이 들었다. while 문을 이용하여 6조각이 한 세트인 a판 피자를 n으로 나누어질 때까지 a를 1부터 카운트 업하는 것이다.</p>
<pre><code class="language-swift">import Foundation

func solution(_ n:Int) -&gt; Int {
// result 변수가 피자의 판 수 
    var result = 1
    while (result * 6) % n != 0 {
        result += 1
    }
    return result
}</code></pre>
<p>다음부터는 문제에 대해 좀 더 고민할 수 있기를!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스 LV.0] Swift - 분수의 덧셈]]></title>
            <link>https://velog.io/@keem-hyun/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-LV.0-Swift-%EB%B6%84%EC%88%98%EC%9D%98-%EB%8D%A7%EC%85%88</link>
            <guid>https://velog.io/@keem-hyun/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-LV.0-Swift-%EB%B6%84%EC%88%98%EC%9D%98-%EB%8D%A7%EC%85%88</guid>
            <pubDate>Fri, 27 Jan 2023 10:36:18 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/keem-hyun/post/bb98e71b-cd6e-41e9-85e5-87fef1db835b/image.png" alt=""></p>
<p>처음에 이 문제는 풀지 못해 고민하다가 다른 분의 풀이를 참고하여 하단과 같이 풀었다.</p>
<pre><code class="language-swift">import Foundation
// 최대공약수 구하는 함수 gcd
func gcd(_ a:Int, _ b:Int) -&gt; Int {
    if a % b == 0 {
        return b
    }
    return gcd(b, a%b)
}

func solution(_ numer1:Int, _ denom1:Int, _ numer2:Int, _ denom2:Int) -&gt; [Int] {
    // 분자 maxNumer
    var maxNumer = (numer1*denom2) + (numer2*denom1)
    // 분모 maxDenom
    var maxDenom = denom1 * denom2
    // gcdValue 변수에 최대공약수 함수를 이용하여 최대공약수 입력
    var gcdValue = gcd(maxNumer, maxDenom)
    // 배열 변수에 분자와 분모를 최대공약수로 나누어 기약 분수 입력
    var result = [maxNumer/gcdValue, maxDenom/gcdValue]
    // 결과값 반환
    return result
}</code></pre>
<p>다른 분의 아이디어를 참고하여 풀었지만, 처음에 내가 이 문제를 고민하면서 풀려고 했던 방법은 이렇다. 분자(maxNumer) 와 분모(maxDenom) 중 최소값을 구한 다음 그 최소값부터 1까지 순차적으로 각 분자와 분모를 나누어 보는 것이다. 그래서 공통으로 나누어 나머지 값이 0으로  나온다면 그 값이 최대공약수이기 때문에 그 값을 이용하여 결과를 도출하려고 했다. 
이와 같은 방법으로 풀면 하단과 같다.</p>
<pre><code class="language-swift">import Foundation

func solution(_ numer1:Int, _ denom1:Int, _ numer2:Int, _ denom2:Int) -&gt; [Int] {
    // 분자 maxNumer
    var maxNumer = (numer1*denom2) + (numer2*denom1)
    // 분모 maxDenom
    var maxDenom = denom1 * denom2
    // minNum 변수에 min 함수를 이용하여 둘 중 더 작은 값 입력
    let minNum = min(maxNumer, maxDenom)
    // stride 함수를 이용하여 minNum 부터 1까지 내림차순으로 순회?한다. 공통으로 0으로 나누어지는 값이 최대공약수이므로 최대공약수를 활용하여 값 도출.
    for num in stride(from: minNum, to: 1, by: -1) {
        if maxNumer % num == 0 &amp;&amp; maxDenom % num == 0 {
            maxNumer /= num
            maxDenom /= num
        }
    }
    // 결과값 반환
    return [maxNumer, maxDenom]
}</code></pre>
<p>내가 풀고 싶었던 방법은 stride 를 이용하는 거였는데, 답 제출 후 다른 분 풀이를 보니 이런 함수도 있구나 싶었다. 다음에 이런 풀이 방법이 생각난다면 for - stride를 사용해볼 수 있지 않을까 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[swift] Optional ]]></title>
            <link>https://velog.io/@keem-hyun/swift-Optional</link>
            <guid>https://velog.io/@keem-hyun/swift-Optional</guid>
            <pubDate>Sat, 21 Jan 2023 14:06:00 GMT</pubDate>
            <description><![CDATA[<h2 id="optional-이란">Optional 이란?</h2>
<p>Optional은 스위프트에 있는 타입 중 하나입니다.
옵셔널 타입을 선언하면 변수나 상수에 값이 있을 수도, 없을 수도 있다는 뜻입니다.
옵셔널 타입이 아닌 경우 변수에 값이 없다면 에러를 일으키지만 옵셔널 타입을 사용한다면 변수에 값이 없어도 에러가 나지 않게 할 수 있습니다.
즉, 에러가 나지 않도록 임시적인 타입을 담아두는 개념이라고 보면 되겠습니다.</p>
<pre><code>var number1: Int = 5
var number2: Int? </code></pre><p>위와 같이 number1 변수는 정수형, number2 변수는 옵셔널 정수형입니다. 타입에 &quot;?&quot; 를 붙여 옵셔널 타입으로 사용할 수 있습니다.</p>
<p>옵셔널 타입으로 선언할 경우, 값이 없을 때 에러가 아닌 &quot;nil&quot; 이라는 값을 반환합니다.
위에 number2를 print 로 찍어보면 nil 값이 나오는 걸 볼 수 있습니다.</p>
<p>변수가 옵셔널 타입으로 선언되어 있고, 변수에 값이 있다면 바로 사용할 수는 없고 옵셔널 타입을 Unwrapping 하여 사용해야 합니다.</p>
<h3 id="unwrapping-방법---강제-추출">Unwrapping 방법 - 강제 추출</h3>
<pre><code>number2!</code></pre><p>변수에 nil 값이 없다는 걸 확신한다면 !을 사용하여 값을 강제로 추출할 수 있습니다. 그러나, 일반적으로 잘 사용하지는 않습니다.</p>
<h3 id="unwrapping-방법---if문-사용">Unwrapping 방법 - if문 사용</h3>
<pre><code>if number2 != nil {
    print(number2!)
}</code></pre><p>if 문을 통해 변수에 nil 값이 없음을 확인 후, 강제 추출하여 사용할 수 있습니다.</p>
<h3 id="unwrapping-방법---옵셔널-바인딩--if-let--guard-let">Unwrapping 방법 - 옵셔널 바인딩 ( if let / guard let)</h3>
<pre><code>if let num = number2 {
    print(num)
}
guard let num = number2 else { return }
print(num)</code></pre><p>number2에 nil 값이 존재한다면, num에 값이 담기지 않을 것이고 반대로, nil 값이 존재하지 않다면 값이 담겨 num 변수를 사용할 수 있습니다.</p>
<h3 id="unwrapping---닐-코얼레싱-nil-coalescing">Unwrapping - 닐 코얼레싱 (Nil-Coalescing)</h3>
<pre><code>number2 ?? &quot;값이 없음&quot;</code></pre><p>삼항 연산자와 유사한 방식으로 기본값을 제시할 수 있습니다. nil 값이 아니라면 number2 변수의 값을 제시하고, nil 값이라면 ?? 뒤에 설정한 &quot;값이 없음&quot; 이라는 문자열을 제시합니다.</p>
]]></description>
        </item>
    </channel>
</rss>