<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>little_tale.log</title>
        <link>https://velog.io/</link>
        <description>IOS 개발자 새싹이, 작은 이야기로부터</description>
        <lastBuildDate>Tue, 21 Apr 2026 00:55:44 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>little_tale.log</title>
            <url>https://velog.velcdn.com/images/little_tail/profile/c40bc000-265f-422a-a395-5fb9bfc9e43d/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. little_tale.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/little_tail" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[DevOps 의 과정 2편 - 네트워크]]></title>
            <link>https://velog.io/@little_tail/DevOps-%EC%9D%98-%EA%B3%BC%EC%A0%95-2%ED%8E%B8-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC</link>
            <guid>https://velog.io/@little_tail/DevOps-%EC%9D%98-%EA%B3%BC%EC%A0%95-2%ED%8E%B8-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC</guid>
            <pubDate>Tue, 21 Apr 2026 00:55:44 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가기에-앞서">들어가기에 앞서</h2>
<blockquote>
<p>이제 Terraform을 제대로 이해하기 위해
가장 먼저 필요한 <strong>네트워크 개념</strong>을 정리해보자.</p>
</blockquote>
<blockquote>
<p>Terraform은 인프라를 코드로 정의하는 도구지만,
실제로 만들게 되는 것은 결국 <strong>네트워크 + 서버 구조</strong>다.</p>
</blockquote>
<blockquote>
</blockquote>
<ul>
<li>VPC를 만들고</li>
<li>Subnet을 나누고</li>
<li>서버를 배치하고</li>
<li>외부와 통신을 설계하는 것</li>
</ul>
<p>이 모든 것이 결국 <strong>네트워크 위에서 이루어진다.</strong></p>
<blockquote>
</blockquote>
<p>그래서 Terraform을 배우기 전에
<strong>&quot;이 리소스들이 왜 필요한지&quot;</strong>를 이해하는 것이 훨씬 중요하다.</p>
<h1 id="what-vpc">What <strong>VPC</strong></h1>
<p>AWS 안에서 내가 쓰는 네트워크 영역</p>
<blockquote>
<p><code>AWS</code> 내부에서 직접 만드는 <strong>가상</strong>의 <strong>네트워크 공간</strong>
다시말해 AWS 라는 큰 건물이 있다면 내가 따로 쓸 <strong>전용 공간</strong></p>
</blockquote>
<ul>
<li>서버들이 배치될 공간 생성</li>
<li>IP 대역 정하기</li>
<li>subnet 분할</li>
<li>외부 인터넷 연결할지 &amp; 내부끼리만 통신할지</li>
<li><em>핵심은 가장 바깥쪽 틀*</em></li>
</ul>
<h1 id="what-subnet">What <strong>Subnet</strong></h1>
<p>VPC 내부의 또 나뉜 구역
서버를 한 공간에 다 넣었을때 보안과 관리가 어려워진다.</p>
<blockquote>
<ul>
<li>외부에서 접근이 가능한 서버 (Web Server)</li>
<li>내부에서 접근 가능한 서버 (DB)</li>
</ul>
</blockquote>
<p>등 이와 같은 것들을 한 곳이 아닌 여러 구역을 나눠서 관리하려는 목적 즉
Subnet 이다.</p>
<h2 id="🌍-public-subnet--private-subnet">🌍 Public Subnet / Private Subnet</h2>
<h3 id="public-subnet">Public Subnet</h3>
<ul>
<li>인터넷과 직접 통신이 가능한 영역</li>
<li>ALB, Bastion Host 같은 것 들이 위치하다.</li>
</ul>
<p>( ALB: 외부 요청을 받아 내부 서버로 분산 하는 로드밸런서 )
( Bastion Host: 외부에서 내부 서버에 안전 접속위한 중간 서버 )</p>
<h3 id="private-subnet">Private Subnet</h3>
<ul>
<li>인터넷 직접 접근 X</li>
<li>내부 서비스 (App Server, DB ...)</li>
</ul>
<h3 id="핵심-포인트">핵심 포인트</h3>
<ul>
<li>단순 분리가 X = <strong>접근 가능 범위</strong>를 분리!</li>
</ul>
<h2 id="🔗-terraform에서의-의미">🔗 Terraform에서의 의미</h2>
<blockquote>
<p>Terraform에서는 VPC 안에 Subnet을 생성해서
서버를 어디에 둘지 결정한다.</p>
</blockquote>
<blockquote>
<p>다시말해, </p>
</blockquote>
<ul>
<li>VPC = 나라 전체</li>
<li>Subnet = 행정구 </li>
</ul>
<p>자 우리는 지금 나라와 행정구를 나누었다.
이제 <strong>나라간 소통</strong>을 하려면 인터넷이 필요하고
<strong>인터넷은 과연 어떻게 연결</strong>되는 것일까?</p>
<h1 id="what-internet-gateway">What Internet Gateway</h1>
<blockquote>
<p>VPC 와 인터넷은 바로 연결하는 것은 불가능하다.
<strong>외부 인터넷과 완전 분리된</strong> <strong>사설 네트워크 공간</strong>이기에 즉,
보안과 격라를 위해 인터넷과 바로 연결하기는 힘들다.</p>
</blockquote>
<blockquote>
<p>VPC가 외부 인터넷과 통신할 수 있는 <strong>문지기</strong>가 있으면 되겠다!
그것이 <strong>IGW Internet Gateway</strong></p>
</blockquote>
<h2 id="🤔-왜-필요한지-좀-더-고민해보자">🤔 왜 필요한지 좀 더 고민해보자.</h2>
<blockquote>
<p>서버를 생성하였다.
외부에서 접근이 불가하고, 반대로 외부로 나가는 것도 불가하다.</p>
</blockquote>
<blockquote>
<p>이는 매우 안전한 상태가 되었다.
다만 실서비스를 지원하려면 결국 Internet으로 사용자에게 제공해야 한다.
그렇다고 하여도 모든것을 Open 해줄 수 있는것도 아니다
일정 부분 공개 하지 말아야 할 것들이 있을것이다.</p>
</blockquote>
<blockquote>
<p><strong>필요한 경우에만 외부와 소통할 수 있는 장치</strong>가 필요하다.</p>
</blockquote>
<h2 id="🌍-어디에-연결되는걸까">🌍 어디에 연결되는걸까?</h2>
<p>Internet Gateway는 <strong><code>VPC</code></strong>에 연결된다.
즉, 이말은 하나 하나 Subnet에 연결하는 것이 아닌 그들을 관리하는
전체 VPC 가 연결 된다는 것이다.</p>
<blockquote>
<p>다만, 전체에 연결 되었다고 하여서
모든 <strong>Subnet</strong>이 바로 인터넷과 통신하는 것이 아니다.</p>
</blockquote>
<blockquote>
<p>VPC에 IGW를 연결하는 것은 단순히 <code>인터넷으로 나가는 출입문 만들기</code>를 한 것
그래서 Subnet 쪽에서 어떤 출입문을 사용할지는 지정이 필요하게 된다.
이때 필요한 개념이 <strong>Route Table</strong> 이다.</p>
</blockquote>
<ul>
<li>VPC 생성</li>
<li>Subnet 생성</li>
<li>Internet Gateway 연결</li>
<li>Route Table에 경로 설정</li>
</ul>
<p>해당 과정을 거치면 Public Subnet이 동작!</p>
<h2 id="🔗-terraform에서의-의미-1">🔗 Terraform에서의 의미</h2>
<p>Terraform에서는</p>
<ul>
<li>VPC를 만듬</li>
<li>aws_internet_gateway 리소스로 IGW를 연결</li>
</ul>
<h1 id="what-route-table">What Route Table</h1>
<blockquote>
<p>&quot;어디로 가야 하는지&quot; 를 알려주는 경로 정보가 필요하다.
<strong>네트워크의 길안내판</strong> 같은 개념이라고 생각해 보자</p>
</blockquote>
<p>Subnet 안의 서버가 외부와 통신하려고 할 때,
그 요청이 어디로 나가야 하는지 정해져 있지 않다면
패킷은 목적지를 찾지 못한다.</p>
<ul>
<li>인터넷으로 나갈지?</li>
<li>다른 내부 네트워크로 갈지?</li>
<li>NAT Gateway 쪽으로 보낼지?</li>
</ul>
<p>이런 경로를 미리 정해줘야 한다.
즉 <strong>&quot;이 Subnet의 트래픽이 어디로 향할지 결정하는 규칙표&quot;</strong></p>
<h2 id="🌍-어떻게-동작하는가">🌍 어떻게 동작하는가?</h2>
<blockquote>
<p>Route Table은 보통
Destination(목적지) 과 Target(대상) 으로 구성된다.</p>
</blockquote>
<ul>
<li>10.0.0.0/16 → local</li>
<li>0.0.0.0/0 → Internet Gateway</li>
</ul>
<blockquote>
<p>같은 VPC 내부 주소로 가는 트래픽은 내부에서 처리하고
그 외 모든 외부 요청은 Internet Gateway로 보내라는 뜻이다.</p>
</blockquote>
<h4 id="00000--모든-외부-주소">0.0.0.0/0 = 모든 외부 주소</h4>
<h2 id="🌍-public-subnet--private-subnet-차이">🌍 Public Subnet / Private Subnet 차이</h2>
<h3 id="public-subnet-1">Public Subnet</h3>
<ul>
<li>Route Table에 0.0.0.0/0 → Internet Gateway 경로가 있음</li>
<li>외부 인터넷과 통신 가능</li>
</ul>
<h3 id="private-subnet-1">Private Subnet</h3>
<ul>
<li>Internet Gateway로 직접 가는 경로가 없음</li>
<li>외부에서 직접 접근 불가능</li>
</ul>
<h2 id="정리하기">정리하기</h2>
<ul>
<li>IGW만 있으면 문만 있는 상태</li>
<li>Route Table까지 연결해야 실제로 그 문을 사용할 수 있음</li>
</ul>
<h2 id="🔗-terraform에서의-의미-2">🔗 Terraform에서의 의미</h2>
<blockquote>
<p>Terraform에서는 aws_route_table 로 경로 규칙을 만들고,
aws_route_table_association 으로 특정 Subnet에 연결한다.</p>
</blockquote>
<p>VPC에 인터넷 출입문을 만들고
어떤 Subnet이 그 출입문을 사용할지 정하는 것</p>
<h2 id="what-nat-gateway">What NAT GateWay</h2>
<blockquote>
<p>private subnet은 보안때문에
외부 인터넷 직접 접근이 불가하게 구성한다.</p>
</blockquote>
<p>하지만, private subnet도 외부 인터넷과 통신해야 할 때가 있다.
예를들어</p>
<ul>
<li>서버 패키지 설치</li>
<li>보안 업데이트 다운로드</li>
<li>외부 API 호출</li>
<li>Docker 이미지 다운로드</li>
</ul>
<p>이럴 때 사용하는 것이 <strong>NAT Gateway</strong></p>
<p>다만, <strong>외부에서 직접 들어올 수 없게 하는 것</strong> 이게 핵심</p>
<ul>
<li>외부에서는 직접 접근 불가</li>
<li>내부 서버는 필요할 때 외부로 나갈 수 있음</li>
</ul>
<h2 id="🌍-어떻게-동작하는가-1">🌍 어떻게 동작하는가?</h2>
<p>NAT Gateway는 보통
Public Subnet에 생성된다.</p>
<blockquote>
<p>Private Subnet의 Route Table에서
외부로 가는 경로(0.0.0.0/0)를 NAT Gateway로 설정</p>
</blockquote>
<ul>
<li>Private Subnet의 서버가 외부 요청을 보냄</li>
<li>그 요청이 NAT Gateway를 거쳐 인터넷으로 나감</li>
<li>응답은 다시 NAT Gateway를 통해 돌아옴</li>
</ul>
<h2 id="흠-왜-public-한-subnet이지">흠 왜 Public 한 Subnet이지?</h2>
<blockquote>
<p>외부 인터넷과 연결된 상태여야 역할을 할 수 있는데, 
Internet Gateway와 연결 가능한 Public Subnet에 위치에 있어야
인터넷 통신이 가능하기 때문</p>
</blockquote>
<ul>
<li>NAT Gateway는 인터넷과 연결된 곳에 있고</li>
<li>Private Subnet은 NAT를 통해서만 외부와 통신하는 구조</li>
</ul>
<p>다시말해</p>
<ul>
<li>Public Subnet = 외부와 직접 통신 가능</li>
<li>Private Subnet = 외부에서 직접 접근 불가</li>
<li>NAT Gateway = Private Subnet이 필요할 때만 외부로 나갈 수 있게 해줌</li>
</ul>
<h2 id="🔗-terraform에서의-의미-3">🔗 Terraform에서의 의미</h2>
<blockquote>
<p>Terraform에서는 Public Subnet에 NAT Gateway를 생성하고,
Private Subnet의 Route Table에서 외부 경로를 NAT Gateway로 연결한다.</p>
</blockquote>
<p>즉, NAT Gateway는
&quot;Private Subnet 서버의 아웃바운드 인터넷 통신을 위한 구성요소&quot;
라고 이해하면 된다.</p>
<h1 id="security-group">Security Group</h1>
<blockquote>
<p>인스턴스에 붙는 상태 기반 방화벽
EC2 앞에서 트래픽을 허용/차단 하는 필터</p>
</blockquote>
<ul>
<li>인스턴스 단위로 동작</li>
<li>네트워크 레벨이라기 보단 서버 바로 앞</li>
</ul>
<pre><code class="language-txt">[인터넷]
   ↓
[Internet Gateway]
   ↓
[VPC]
   ↓
[Subnet]
   ↓
[Security Group]
   ↓
[EC2]</code></pre>
<h2 id="그래서-역활이--👀">그래서 역활이 ... 👀</h2>
<blockquote>
<p>이 요청을 받아도 되는지 판단!</p>
</blockquote>
<h2 id="규칙-구조-rule">규칙 구조 Rule</h2>
<ul>
<li>port ( 어디로? )</li>
<li>protocol (TCP/UDP...)</li>
<li>source / destination ( who? )</li>
</ul>
<h3 id="💡-예시">💡 예시</h3>
<blockquote>
<p>TCP / 22 / 내 IP
→ “내 컴퓨터만 SSH 접속 허용”
TCP / 80 / 0.0.0.0/0
→ “모든 사람이 웹 접속 가능”</p>
</blockquote>
<h2 id="allow-only">Allow only</h2>
<blockquote>
<ul>
<li>허용 규칙만 있음</li>
<li>나머지는 전부 자동 차단</li>
</ul>
</blockquote>
<h2 id="stateful-하다">Stateful 하다.</h2>
<h3 id="❌-stateless-예-nacl">❌ Stateless (예: NACL)</h3>
<ul>
<li>요청 따로</li>
<li>응답 따로 허용해야 함</li>
</ul>
<h3 id="✅-stateful-sg">✅ Stateful (SG)</h3>
<ul>
<li>요청 허용하면</li>
<li>응답은 자동 허용</li>
</ul>
<h2 id="inbound--outbound">Inbound / Outbound</h2>
<h3 id="inbound-들어오는-것">Inbound (들어오는 것)</h3>
<blockquote>
<p>👉 외부 → EC2</p>
</blockquote>
<ul>
<li>SSH 접속</li>
<li>HTTP 요청</li>
<li>DB 접속<h3 id="🔼-outbound-나가는-것">🔼 Outbound (나가는 것)</h3>
<blockquote>
<p>👉 EC2 → 외부</p>
</blockquote>
</li>
</ul>
<p>API 호출
패키지 설치 (apt, yum)
DB 외부 연결</p>
<blockquote>
<p>기본적으론 <strong>기본값: 전부 허용</strong> 이다.</p>
</blockquote>
<h3 id="문제-생기면">문제 생기면:</h3>
<ul>
<li>SG inbound 확인</li>
<li>포트 확인</li>
<li>Source 확인</li>
</ul>
<h2 id="마무리-하면서">마무리 하면서</h2>
<blockquote>
<p>네트워크를 이번에 조금더 다루면서 다음편 부터는
Terraform 을 직접 사용해보려고 합니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[DevOps 의 과정 1편 - 훑어 보기 + 리눅스]]></title>
            <link>https://velog.io/@little_tail/DevOps-%EC%9D%98-%EA%B3%BC%EC%A0%95-1</link>
            <guid>https://velog.io/@little_tail/DevOps-%EC%9D%98-%EA%B3%BC%EC%A0%95-1</guid>
            <pubDate>Mon, 13 Apr 2026 06:20:41 GMT</pubDate>
            <description><![CDATA[<h1 id="terraform-들어가기-전에-먼저-깔아야-할-기초">Terraform 들어가기 전에 먼저 깔아야 할 기초</h1>
<ol>
<li><p>리눅스 명령어 (기본 정도)</p>
</li>
<li><p>네트워크 기초 </p>
</li>
</ol>
<ul>
<li>VPC, Subnet, Route, NAT, Security Group/Firewall</li>
</ul>
<ol start="3">
<li>클라우드 기초</li>
</ol>
<ul>
<li>IAM, Compute, Storage, Load Balancer</li>
</ul>
<ol start="4">
<li>Git 기초</li>
</ol>
<ul>
<li>branch, PR, revert</li>
</ul>
<ol start="5">
<li>JSON/YAML 읽기 능력</li>
</ol>
<p>** 이중에서 4번 같은 경우는 생략 **</p>
<blockquote>
<p>Terraform은 인프라를 코드로 만들지만,
실제로 다루는 대상은 결국 <strong>리눅스 서버</strong>다.</p>
<p>그래서 최소한의 리눅스 명령어를 모르면
인프라를 만들고도 <strong>디버깅, 로그 확인, 서버 접근 자체가 어려워진다.</strong></p>
</blockquote>
<h2 id="리눅스-기본-명령어">리눅스 기본 명령어</h2>
<blockquote>
<p>Terraform은 인프라를 코드로 만들지만,
운영하게될 대상은 <strong>리눅스 서버/컨테이너/VM</strong></p>
</blockquote>
<h3 id="📂-파일디렉토리-관련">📂 파일/디렉토리 관련</h3>
<ul>
<li><code>pwd</code>: 현재 위치 확인</li>
<li><code>ls</code>: 현재 디렉토리 파일 목록</li>
<li><code>cd</code>: 디렉토리 이동</li>
<li><code>mkdir</code>: 디렉토리 생성</li>
<li><code>cp</code>: 파일/디렉토리 복사</li>
<li><code>mv</code>: 파일 이동/이름 변경</li>
<li><code>rm</code>: 파일 삭제</li>
</ul>
<hr>
<h3 id="📄-파일-내용-확인">📄 파일 내용 확인</h3>
<ul>
<li><code>cat</code>: 파일 전체 출력</li>
<li><code>less</code>: 스크롤하면서 보기</li>
<li><code>head</code>: 앞부분 확인</li>
<li><code>tail</code>: 뒷부분 확인 (로그 볼 때 많이 씀)</li>
<li><code>grep</code>: 특정 문자열 검색</li>
</ul>
<hr>
<h3 id="🔐-권한-관련">🔐 권한 관련</h3>
<ul>
<li><code>chmod</code>: 권한 변경</li>
<li><code>chown</code>: 소유자 변경</li>
</ul>
<hr>
<h3 id="⚙️-프로세스시스템-상태">⚙️ 프로세스/시스템 상태</h3>
<ul>
<li><code>ps</code>: 실행 중인 프로세스 확인</li>
<li><code>top</code> / <code>htop</code>: CPU, 메모리 상태 확인</li>
</ul>
<hr>
<h3 id="🌐-네트워크요청">🌐 네트워크/요청</h3>
<ul>
<li><code>curl</code>: HTTP 요청 보내기 (API 테스트할 때 자주 사용)</li>
</ul>
<hr>
<h3 id="🧪-환경-변수">🧪 환경 변수</h3>
<ul>
<li><code>echo $PATH</code></li>
<li><code>env</code><h3 id="💡-예시">💡 예시</h3>
</li>
</ul>
<pre><code class="language-bash"># 로그 실시간 보기
tail -f app.log

# 로그에서 error만 찾기
grep &quot;error&quot; app.log

# 로그 실시간 + 필터
tail -f app.log | grep &quot;ERROR&quot;</code></pre>
<p>👉 실제 서버 장애 확인할 때 거의 이 조합을 사용한다.</p>
<blockquote>
<p>Mac을 쓰는 경우에도 위 같은 경우를 사용하다 보니
가볍게 다루어 보고 가려고 한다.</p>
</blockquote>
<pre><code class="language-text">pwd   
/Users/jaehyungkim/Desktop/Swift/TuistGoolbitg/Real2/goolbitg-iOS

ls
현재 폴더 내부 파일들

cd
경로 이동

cat
concatenate 줄임말 -&gt; 합치다  -&gt;  여라 파일 내용 하나로 합치기

cat filename.txt                         : 파일 내용 출력
cat file1.txt file2.txt                 : 파일 내용 이어서 출력
cat -n filename.txt                     : 행 번호 붙여서 출력 
cat file1.txt file2.txt &gt; new_file.txt : 파일 여러개 합쳐서 새로운 파일
...

less = 보여지는 만큼만 출력
head = 앞부분 부터 확인하는 명령어
tail = 뒷부분 부터 확인하는 명령어
grep = 특정 문자열 순환탐색
cp = 디렉토리 또는 파일 복사 명령어
cp_r = [복사 디렉토리] [대상 디렉토리]
mv = 파일 이동
rm = 파일 삭제
mkdir = 디렉토리 생성
chmod = 권한 변경 (파일, 디렉토리 권한)
chown = 권한 변경 (파일, 디렉토리 소유권 변경 ex: Group)
ps = 활성프로세스 출력
top or htop = OS 상태 하단 사진 참고

curl = Network Protocol 를통한 데이터 송수신 명령어
curl http://example.com
curl -O http://example.com/file.zip
curl -X POST -H &quot;Content-Type: application/json&quot; -d &#39;{&quot;name&quot;: &quot;user&quot;, &quot;age&quot;: 25}&#39; 
curl -F &quot;file=@/path/to/file.txt&quot; 

환경 변수 확인
echo $PATH, env</code></pre>
<p>@참고 자료 : <a href="https://monkeybusiness.tistory.com/645">https://monkeybusiness.tistory.com/645</a></p>
<p><img src="https://velog.velcdn.com/images/little_tail/post/5a842f2b-19e4-4931-8c9d-64f3e7879a8e/image.png" alt=""></p>
<h2 id="네트워크-기초">네트워크 기초</h2>
<blockquote>
<p>좀 중요하다 결국 인프라이기에 보안, 통신 에 대한 이야기 이게 때문에
이를 좀 어느정도 잡아놔야 한다.
VPC, subnet, route table, internet gateway, NAT gateway, security group
를 다루어 보고자 한다.</p>
</blockquote>
<h3 id="간단한-큰-그림">간단한 큰 그림</h3>
<ul>
<li>VPC: 네트워크 전체 땅</li>
<li>Subnet: 그 땅을 나눈 구역</li>
<li>Route Table: 이 구역의 트래픽이 어디로 갈지 정한 안내판</li>
<li>Internet Gateway: 인터넷과 직접 연결되는 출입문</li>
<li>NAT Gateway: 내부 서버가 바깥으로 나갈 때만 쓰는 대리 출구</li>
<li>Security Group / Firewall: 누가 들어오고 나갈지 통제하는 문지기</li>
</ul>
<p>@참고자료 : <a href="https://docs.aws.amazon.com/vpc/latest/userguide/configure-subnets.html?utm_source=chatgpt.com">https://docs.aws.amazon.com/vpc/latest/userguide/configure-subnets.html?utm_source=chatgpt.com</a></p>
<blockquote>
<p>subnet 은 VPC 안의 IP주소 범위
route table 은 해당 subnet이나 gateway에서 나가는 트래픽 경로 결정 규칙들
InternetGateway 는 VPC와 인터넷 사이 통신 가능케
NAT Gateway는 private subnet 인스턴스가 외부로 나갈 수 있게 하되 외부에서 먼저 들어오는 연결 차단
Security Group은 연결된 리소스의 인바운드/아웃바운드 트래픽을 제어하는 가상 방화벽</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/little_tail/post/ffe2b081-ca27-4f3c-a284-d555a326848c/image.png" alt=""></p>
<h3 id="vpc-란-virtual-private-cloud">VPC 란? (Virtual Private Cloud)</h3>
<blockquote>
<p>클라우드 안에 가상 네트워크 공간</p>
</blockquote>
<ol>
<li>IP 대역을 정한다</li>
<li>subnet을 할당한다.</li>
<li>route 연결</li>
<li>gateway 연결</li>
</ol>
<hr>
<h3 id="internetgateway">InternetGateWay</h3>
<blockquote>
<p>VPC의 구성요소로써 VPC와 인터넷간에 통신을 할 수 있게 만들어주는 역할
출입문</p>
</blockquote>
<ul>
<li>외부 사용자가 접근해야 하는 리소스가 있을 때 필요함!</li>
<li>subnet의 route table에도 인터넷 방향 경로가 있어야 함</li>
</ul>
<hr>
<h3 id="subnet-이란">subnet 이란?</h3>
<ol start="0">
<li>네트워크 안에 네트워크</li>
<li>VPC 안에서 나눈 작은 IP 주소 범위
why? -&gt; 바로 옆으로 전달해도 될때 구지 구지 큰 범위 벗어난후 전달할 필요가 없음
ex) 배송: 도봉구 -&gt; 노원구</li>
</ol>
<ul>
<li>서울시 도봉구 배송 출발 -&gt; 서울 중앙 센터 -&gt; 노원구 센터 </li>
<li>서울시 도봉구 배송 출발 -&gt; 노원구 센터</li>
</ul>
<p><img src="https://cf-assets.www.cloudflare.com/slt3lc6tev37/2pBqIHUTSlxI7EW9XZPKf3/551ab3390ab9ab86fee15c73fd245f6c/subnet-diagram.svg" alt=""></p>
<h4 id="public-subnet">Public Subnet?</h4>
<ul>
<li>인터넷에서 직접 접근 가능한 리소스</li>
<li>외부에서 접근이 가능한 네트워크영역</li>
<li>subnet -&gt; RoutingTable(인터넷 게이트웨이로)</li>
</ul>
<h4 id="private-subnet">Private Subnet?</h4>
<ul>
<li>인터넷에서 직접 접근되면 안 되는 리소스</li>
<li>외부에서 다이렉트로 접근이 불가능한 네트워크 영역</li>
<li>해당 서브넷에 위치한 리소스들은 외부와의 연결이 불가능</li>
</ul>
<h4 id="why">why?</h4>
<ul>
<li>public subnet에는 internet gateway 경로를, private subnet에는 NAT gateway 경로</li>
<li>즉 보안</li>
</ul>
<hr>
<h3 id="route-table-이란">Route Table 이란</h3>
<blockquote>
<p>말 그대로 “이 subnet에서 나가는 패킷을 어디로 보낼지” 정한 규칙표
AWS는 route table을 VPC의 traffic controller라고 표현한다.
서브넷 혹은 게이트웨이를 통해서 네트워크 트래픽이 어디로 향하는지에 대해 결정</p>
</blockquote>
<ul>
<li><p>라우팅 규칙은 목적지 주소에 따른 Next Hop을 지정</p>
</li>
<li><p>10.0.0.0/16 -&gt; local
같은 VPC 내부 통신</p>
</li>
<li><p>0.0.0.0/0 -&gt; igw
인터넷으로 보냄</p>
</li>
<li><p>0.0.0.0/0 -&gt; nat
NAT를 통해 외부로 보냄</p>
</li>
</ul>
<p><img src="https://docs.aws.amazon.com/ko_kr/vpc/latest/userguide/images/route-tables.png" alt=""></p>
<blockquote>
<p>서브넷 5개, 기본 라우팅 테이블 1개, 사용자 지정 라우팅 테이블 3개가 포함된 VPC</p>
</blockquote>
<h4 id="네트워크-문제의-절반은-route에서-난다">네트워크 문제의 절반은 route에서 난다.</h4>
<blockquote>
<p>통신이 안 될 때는 거의 항상 아래 순서로 보자</p>
</blockquote>
<ul>
<li>같은 VPC/대역인가? </li>
<li>route가 맞나?</li>
<li>보안 정책이 막고 있나?</li>
</ul>
<h3 id="nat-gateway--주소-변환-nat-">NAT Gateway ( 주소 변환 NAT )</h3>
<ul>
<li>NAT: 사설 네트워크에 속한 여러 개의 호스트가 하나의 공인 IP 주소를 사용하여 인터넷에 접속하기 위함<ul>
<li>기본적으로 내부에서 시작한 연결의 응답만 돌아오게 하는 구조</li>
</ul>
</li>
</ul>
<blockquote>
<p>private subnet 안의 서버가 외부 인터넷이나 외부 서비스로 나갈 수만 있게 해 주는 장치
즉 외부에서 먼저 연결 시도 불가</p>
</blockquote>
<p><img src="https://docs.aws.amazon.com/ko_kr/vpc/latest/userguide/images/public-nat-gateway-diagram.png" alt=""></p>
<h2 id="firewall이-뭔가-방화벽">Firewall이 뭔가 (방화벽)</h2>
<blockquote>
<p>정의된 규칙에 따라 네트워크 트래픽을 허용하거나 차단하는 보안 시스템 
Cloudflare는 이를 신뢰 영역과 비신뢰 영역 사이에 놓여 트래픽을 통제하는 장치</p>
</blockquote>
<h2 id="security-group은-뭐가-다른가">Security Group은 뭐가 다른가</h2>
<blockquote>
<p>AWS Security Group은 VPC 리소스에 붙는 가상 방화벽
인바운드와 아웃바운드 규칙을 설정하고, 소스/대상, 포트 범위, 프로토콜을 지정
허용된 연결의 응답 트래픽은 별도 규칙 없이도 돌아온다.
어떤 계층의 어떤 서버끼리 말하게 할지 정하는 정책</p>
</blockquote>
<h2 id="과정">과정</h2>
<ul>
<li>VPC 하나 생성</li>
<li>public subnet 2개 생성</li>
<li>private subnet 2개 생성</li>
<li>Internet Gateway 연결</li>
<li>public subnet route table은 IGW로 연결</li>
<li>NAT Gateway는 public subnet에 둠</li>
<li>private subnet route table은 NAT로 연결</li>
<li>ALB는 public subnet</li>
<li>App 서버는 private subnet</li>
<li>DB는 private subnet</li>
<li>Security Group으로 ALB → App, App → DB만 허용</li>
</ul>
<h2 id="로드-밸런서가-뭘까">로드 밸런서가 뭘까?</h2>
<ul>
<li>들어오는 요청을 여러 서버로 나누자</li>
</ul>
<blockquote>
<p>예시
Example.com 접속! -&gt; 서버가 1대가 아닌 3대로</p>
</blockquote>
<h4 id="why-1">why?</h4>
<ul>
<li>서버 한대가 죽어도 동작</li>
<li>사용자 수가 많아져도 나눠 처리</li>
<li>서비스 앞단을 하나의 점으로 통일</li>
</ul>
<h2 id="온프레미스는-뭘까">온프레미스는 뭘까?</h2>
<blockquote>
<p>회사가 소유한 물리적인 서버실, 네트워크 장비, 스토리지 같은 자체 인프라 환경
그럼 AWS는..? -&gt; 클라우드</p>
</blockquote>
<h2 id="서버를-public-subnet에-두면-인터넷인가">서버를 public subnet에 두면 인터넷인가?</h2>
<blockquote>
<p>떙</p>
<ol>
<li>route table은 VPC의 traffic controller</li>
<li>route 규칙이 subnet이나 gateway의 트래픽이 어디로 갈지 결정</li>
<li>인터넷 게이트웨이는 VPC route table에서 인터넷으로 향하는 트래픽의 대상(target) 이 됨</li>
<li>라우팅 규칙으로 결정되는거지 public subnet의 유무가 아님!</li>
</ol>
</blockquote>
<blockquote>
<p>서버가 어떤 subnet에 있든,
그 subnet이 연결된 route table에 인터넷 방향 경로가 있어야 인터넷으로 나갈 수 있다.</p>
</blockquote>
<ol>
<li>VPC에 Internet Gateway가 연결되어 있어야 함</li>
<li>그 subnet의 route table에 0.0.0.0/0 -&gt; Internet Gateway 경로가 있어야 함</li>
</ol>
<h3 id="subnet의-route-table에도-인터넷-방향-경로가-있어야-함">subnet의 route table에도 인터넷 방향 경로가 있어야 함</h3>
<pre><code>0.0.0.0/0 -&gt; igw-xxxx</code></pre><ul>
<li>목적지가 어디든(0.0.0.0/0)</li>
<li>모르면 인터넷 게이트웨이로 보내라</li>
</ul>
<h2 id="alb-가-뭐지">ALB 가 뭐지?</h2>
<blockquote>
<p>Application Load Balancer
클라이언트의 단일 진입점 역할
애플리케이션 계층 트래픽을 여러 대상, AZ(가용영역)으로 분산
웹 서비스 앞에서 HTTP/HTTPS 요청을 받아서 뒤쪽 서버들로 나눠주는 로드밸런서</p>
</blockquote>
<ul>
<li>사용자가 브라우저로 <a href="https://myservice.com">https://myservice.com</a> 접속</li>
<li>ALB가 요청을 받음</li>
<li>뒤에 있는 앱 서버 여러 대 중 하나로 전달</li>
</ul>
<p>ALB는 public subnet
App 서버는 private subnet
DB는 private subnet</p>
<hr>
<h1 id="cidr-이란">CIDR 이란</h1>
<blockquote>
<p>IP 주소 범위를 표현하는 방법
10.0.0.0/16 처럼 적음
10.0.0.0 이 주소
/16 은 앞 16비트가 네트워크 구간</p>
</blockquote>
<h3 id="ipv4-주소는-총-32bit">IPv4 주소는 총 32bit</h3>
<ul>
<li>/16이면 앞 16비트가 네트워크</li>
<li>/24면 앞 24비트가 네트워크</li>
<li>남은 비트는 호스트에</li>
</ul>
<ul>
<li>VPC = 큰 땅</li>
<li>CIDR = 땅의 주소 범위</li>
<li>subnet CIDR = 땅을 잘라 나눈 구획 주소 범위</li>
</ul>
<h2 id="공인-ip--사설-ip">공인 IP / 사설 IP</h2>
<h3 id="공인-ip">공인 IP</h3>
<blockquote>
<p>전 세계적으로 유일하게 식별되는 주소
인터넷에서 직접 라우팅될 수 있는 주소</p>
</blockquote>
<h3 id="사설-ip">사설 IP</h3>
<blockquote>
<p>내부망용 주소
인터넷 공용 라우팅에 직접 쓰이지 않는다.
사설 IP는 내부 네트워크에서만 쓰는 주소다. RFC 1918에서 대표적인 사설 IPv4 대역은 아래 3개다.</p>
</blockquote>
<ul>
<li>10.0.0.0/8</li>
<li>172.16.0.0/12</li>
<li>192.168.0.0/16</li>
</ul>
<h3 id="왜-사설-ip를-쓰냐">왜 사설 IP를 쓰냐</h3>
<blockquote>
<p>인터넷에 연결되는 모든 장비에 공인 IP를 다 줄 수는 없다. 
IPv4 주소는 한정되어 있고, 내부 시스템은 인터넷에서 직접 접근될 필요가 없는 경우도 많다.
내부 시스템은 사설 IP를 쓰고, 외부와 통신이 필요할 때는 NAT 같은 방식으로 나가게 만든다.</p>
</blockquote>
<h2 id="vpc-에선">VPC 에선?</h2>
<blockquote>
<p>VPC 내부 EC2는 기본적으로 사설 IP를 가진다.
public subnet에 있는 인스턴스는 경우에 따라 공인 IP를 추가로 받을 수 있지만
내부 통신 자체는 여전히 사설 IP 중심으로 이뤄진다</p>
</blockquote>
<ul>
<li>App ↔ DB 통신: 사설 IP</li>
<li>외부 사용자 ↔ ALB: 공인 IP 또는 퍼블릭 엔드포인트</li>
</ul>
<h2 id="port란-무엇인지">Port란 무엇인지</h2>
<blockquote>
<p>한 컴퓨터 안에서 어떤 서비스/프로세스에게 트래픽을 보낼지 구분하는 번호
네트워크 연결이 시작되고 끝나는 가상 지점</p>
</blockquote>
<ul>
<li>IP 주소 = 어느 집인지</li>
<li>포트 번호 = 그 집의 몇 번 방인지</li>
</ul>
<p>예를 들어 서버 IP가 10.0.1.15여도,</p>
<ul>
<li>22: SSH</li>
<li>80: HTTP</li>
<li>443: HTTPS</li>
<li>3306: MySQL</li>
<li>5432: PostgreSQL</li>
<li>6379: Redis</li>
<li>8080: 웹 애플리케이션</li>
</ul>
<h1 id="한눈으로-봐보자">한눈으로 봐보자</h1>
<pre><code>[사용자 브라우저]
        |
        |  HTTPS 443
        v
   [ALB / Load Balancer]
        |
        |  HTTP 80 or HTTPS 443
        v
   [App Server 1]
   [App Server 2]
        |
        |  DB Port 3306 or 5432
        v
      [DB]</code></pre><pre><code>VPC (10.0.0.0/16)
├─ Public Subnet A (10.0.1.0/24)
│   ├─ ALB
│   └─ NAT Gateway
│
├─ Public Subnet B (10.0.2.0/24)
│   └─ ALB
│
├─ Private Subnet A (10.0.10.0/24)
│   └─ App Server 1
│
├─ Private Subnet B (10.0.20.0/24)
│   └─ App Server 2
│
└─ Private DB Subnet (10.0.30.0/24)
    └─ DB</code></pre><pre><code>App Server (Private Subnet)
   -&gt; Route Table
   -&gt; NAT Gateway (Public Subnet)
   -&gt; Internet Gateway
   -&gt; Internet</code></pre><h2 id="마무리-하면서">마무리 하면서</h2>
<blockquote>
<p>자자 네트워크 아직 끝이 아니에욧</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[WebView로 이미지를 보내야 할 때, 어떤 방식이 제일 괜찮을까?]]></title>
            <link>https://velog.io/@little_tail/WebView%EB%A1%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A5%BC-%EB%B3%B4%EB%82%B4%EC%95%BC-%ED%95%A0-%EB%95%8C-%EC%96%B4%EB%96%A4-%EB%B0%A9%EC%8B%9D%EC%9D%B4-%EC%A0%9C%EC%9D%BC-%EA%B4%9C%EC%B0%AE%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@little_tail/WebView%EB%A1%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A5%BC-%EB%B3%B4%EB%82%B4%EC%95%BC-%ED%95%A0-%EB%95%8C-%EC%96%B4%EB%96%A4-%EB%B0%A9%EC%8B%9D%EC%9D%B4-%EC%A0%9C%EC%9D%BC-%EA%B4%9C%EC%B0%AE%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Tue, 31 Mar 2026 06:19:39 GMT</pubDate>
            <description><![CDATA[<h2 id="webview로-이미지를-보내야-할-때-어떤-방식이-제일-괜찮을까">WebView로 이미지를 보내야 할 때, 어떤 방식이 제일 괜찮을까?</h2>
<blockquote>
<p>Base64에서 시작해서 Blob, 청크 분할, 그리고 256KB까지
이번에 WKWebView 안으로 이미지를 전달해야 하는 일이 있었다.</p>
</blockquote>
<p>처음엔 단순하게 생각했다.</p>
<p>&quot;Swift에서 이미지 고르고, WebView로 넘기면 끝 아닌가?&quot;
그런데 막상 구현을 시작해보니 생각보다 고려할 게 많았다.</p>
<ol>
<li>어떤 방식이 제일 단순한가</li>
<li>보안상 부담이 없는가</li>
<li>이미지가 많아지면 버틸 수 있는가</li>
<li>웹이 앱 구현에 너무 강하게 묶이지는 않는가</li>
</ol>
<h3 id="이번-글은-그-과정을-정리한-기록이다">이번 글은 그 과정을 정리한 기록이다.</h3>
<blockquote>
<p>결론부터 말하면, 이번 조건에서는 Base64 data URL로 시작해서,
최종적으로는 base64 청크 분할 전송 + 웹에서 Blob 재조립 방식으로 정리했고,
청크 크기는 256KB가 가장 적절했다.</p>
</blockquote>
<h3 id="문제-상황">문제 상황</h3>
<blockquote>
<p>출발점은 아주 단순했다.
지인에게서 이런 질문을 받았다.</p>
</blockquote>
<p><strong>*&quot;WebView로 이미지를 보내야 하는데, 어떤 방식이 좋을까?&quot;*</strong></p>
<blockquote>
<p>즉, 네이티브(iOS/Swift)에서 이미지 선택
그 이미지를 WebView 안의 웹으로 전달
웹에서 렌더링하거나, 이후 서버 업로드에 활용
즉 핵심은 네이티브 -&gt; 웹 이미지 전달 방식이다.</p>
</blockquote>
<h2 id="가장-먼저-떠오른-방법-base64">가장 먼저 떠오른 방법: Base64</h2>
<ul>
<li>이건 구조가 단순하다.</li>
</ul>
<blockquote>
<p>Swift에서 이미지를 Data로 읽고
base64EncodedString()으로 문자열로 바꾸고
웹에 전달해서 img src=&quot;data:image/...;base64,...&quot;로 붙이면 된다
Swift 쪽 흐름은 대략 이랬다.</p>
</blockquote>
<pre><code class="language-swift">guard let data = try? await item.loadTransferable(type: Data.self) else {
    continue
}

let contentType = item.supportedContentTypes.first?.preferredMIMEType ?? &quot;image/jpeg&quot;
let base64 = data.base64EncodedString()
let dataURL = &quot;data:\(contentType);base64,\(base64)&quot;

window.receivePickedImages = function(images) {
  images.forEach((image, index) =&gt; {
    const img = document.createElement(&quot;img&quot;);
    img.src = image.dataUrl;
    img.alt = `picked-${index}`;
    grid.appendChild(img);
  });
};</code></pre>
<ul>
<li>이 방식의 장점은 명확하다.</li>
</ul>
<ol>
<li>구현이 제일 쉽다</li>
<li>디버깅이 쉽다</li>
<li>적은 수의 이미지 테스트에는 충분하다</li>
</ol>
<p>단점...</p>
<ol>
<li>base64는 원본보다 크기가 커진다</li>
<li>data: URL 문자열이 너무 길어진다</li>
<li>긴 문자열이 JS 상태나 DOM 쪽에 남는다</li>
<li>이미지 수가 많아질수록 부담이 커진다</li>
</ol>
<blockquote>
<p>즉, 작게 테스트할 땐 좋지만, 대량 이미지 전달 구조로는 불안하다는 느낌이었다.</p>
</blockquote>
<p>이쯤에서 지인이 이런 이야기를 했다.</p>
<ul>
<li>&quot;보안 때문에 Base64는 어렵지 않나?&quot;</li>
</ul>
<blockquote>
<p>정확히 말하면, Base64는 암호화가 아니라 단순 인코딩이다.
즉 그 자체가 보안을 제공하는 방식은 아니다.</p>
</blockquote>
<p>하지만 실제 구현 관점에서는 충분히 부담이 생긴다.</p>
<ul>
<li>원본 이미지가 긴 문자열 형태로 남고</li>
<li>data: URL로 그대로 DOM에 들어가고</li>
<li>디버깅이나 로깅 과정에서 노출되기 쉬워지기 때문이다</li>
<li>그래서 여기서부터 생각이 바뀌었다.</li>
</ul>
<blockquote>
<p>&quot;그렇다면 최종적으로 웹에서는 문자열이 아니라 이미지 객체를 다루게 하면 어떨까?&quot;</p>
</blockquote>
<h2 id="blob">Blob</h2>
<p>핵심 아이디어는 단순하다.</p>
<blockquote>
<p>웹이 최종적으로 필요한 건 이미지 데이터, 웹 안에서 Blob을 만들고 
URL로 렌더링하면 되지 않을까?</p>
</blockquote>
<p>웹 쪽 개념은 이런 형태다.</p>
<pre><code class="language-ts">const blob = new Blob(chunks, { type: mimeType });
const blobUrl = URL.createObjectURL(blob);
img.src = blobUrl;</code></pre>
<ol>
<li>최종 렌더링은 data: URL이 아니라 blob: URL</li>
<li>긴 문자열을 그대로 DOM에 박지 않아도 됨</li>
<li>웹이 최종 표시 책임을 갖게 됨</li>
</ol>
<p>하지만 여기서 바로 다음 질문이 생긴다.</p>
<p>&quot;Blob을 만들기 위한 원본 바이트를 Swift에서 웹으로 어떻게 넘길 것인가?&quot;</p>
<blockquote>
<p>즉 Blob은 좋은데,
결국 Swift -&gt; WebView 전달 경로를 설계해야 했다.</p>
</blockquote>
<blockquote>
<p>사실 커스텀 스킴도 생각했다
여기서 한 번쯤은 WKURLSchemeHandler 같은 커스텀 스킴 방식도 떠오른다.</p>
</blockquote>
<p>예를 들면 웹에는 이런 URL만 넘기고</p>
<blockquote>
<p>app-image://image/123/original</p>
</blockquote>
<p>실제 이미지 바이트는 네이티브가 직접 공급하는 구조다.</p>
<p>이 방식은 성능만 놓고 보면 상당히 매력적이다.</p>
<blockquote>
<p>웹에서는 그냥 URL처럼 다룰 수 있고
JS 브리지로 큰 바이너리를 직접 넘길 필요가 적고
구조적으로 효율적이다
하지만 이번에는 이 방향을 최종 선택지로 두고 싶지 않았다.</p>
</blockquote>
<p>이유는 하나였다.</p>
<p>웹이 이미지 URL 해석 자체를 앱 구현에 의존하면 안된다.</p>
<p>커스텀 스킴은 결국 앱 쪽 구현에 강하게 묶인다.
즉 웹만 따로 떼어놓고 생각했을 때 독립성이 떨어진다.</p>
<p>그래서 이번엔 커스텀 스킴 대신,
웹이 최종적으로 Blob을 만들고, 앱은 그걸 위한 운반 역할만 하는 구조로 정리하기로 했다.</p>
<blockquote>
<p>[UInt8] 배열 JSON 방식도 해봤다
Blob으로 가는 중간 단계에서
처음엔 Data를 [UInt8] 배열로 바꿔서 JSON으로 넘기는 방식도 생각했다.</p>
</blockquote>
<p>예를 들면 이런 느낌이다.</p>
<pre><code class="language-swift">{
  &quot;bytes&quot;: [137, 80, 78, 71, ...]
}</code></pre>
<p>바이트 배열을 넘기고
웹에서 Uint8Array로 복원하면 끝
그런데 이 방식은 생각보다 비효율적이었다.</p>
<p>이유는 간단하다.</p>
<blockquote>
<p>원래 바이트 하나는 그냥 1바이트인데,
JSON에서는 숫자 문자열로 표현된다.</p>
</blockquote>
<p>예를 들어:</p>
<p>[137, 80, 78, 71]
이 경우 실제 바이너리는 4바이트지만,
JSON 표현은 숫자 문자열 + 쉼표 때문에 훨씬 커진다.</p>
<p>즉:</p>
<blockquote>
<p>바이트 하나가 숫자 문자열이 되고 구분자까지 붙고
전체 payload 크기가 크게 불어난다 그래서 이 방식은 버리고,
그 대신 base64 청크를 웹으로 넘긴 뒤, 웹에서 Blob을 재조립하는 방식으로 바꾸었다.</p>
</blockquote>
<h1 id="base64-청크-분할-전송--웹-blob-재조립">base64 청크 분할 전송 + 웹 Blob 재조립</h1>
<p>현재 샘플에서 최종적으로 정리한 방식은 이렇다.</p>
<ol>
<li>Swift에서 이미지 원본을 Data로 읽는다</li>
<li>한 번에 통째로 보내지 않는다</li>
<li>일정 크기로 청크를 나눈다</li>
<li>각 청크를 base64 문자열로 인코딩한다</li>
<li>JSON payload로 WebView에 전달한다</li>
<li>웹에서 청크를 다시 Uint8Array로 복원한다</li>
<li>모든 청크를 모아 Blob을 만든다</li>
<li>최종적으로 blob: URL로 렌더링한다</li>
</ol>
<pre><code class="language-swift">struct BlobTransferChunker {
    struct Payload: Sendable {
        let jsonString: String
    }

    static func makePayloads(
        id: String,
        mimeType: String,
        data: Data,
        width: Int,
        height: Int,
        chunkSize: Int
    ) -&gt; [Payload] {
        let totalChunks = max(1, Int(ceil(Double(data.count) / Double(chunkSize))))
        var payloads: [Payload] = []
        payloads.reserveCapacity(totalChunks)

        for chunkIndex in 0..&lt;totalChunks {
            let start = chunkIndex * chunkSize
            let end = min(start + chunkSize, data.count)
            let chunk = data.subdata(in: start..&lt;end).base64EncodedString()

            let payload: [String: Any] = [
                &quot;id&quot;: id,
                &quot;mimeType&quot;: mimeType,
                &quot;width&quot;: width,
                &quot;height&quot;: height,
                &quot;index&quot;: chunkIndex,
                &quot;total&quot;: totalChunks,
                &quot;base64&quot;: chunk,
            ]

            guard
                let jsonData = try? JSONSerialization.data(withJSONObject: payload),
                let jsonString = String(data: jsonData, encoding: .utf8)
            else {
                continue
            }

            payloads.append(Payload(jsonString: jsonString))
        }

        return payloads
    }
}</code></pre>
<blockquote>
<p>이 payload를 반복해서 웹으로 넘긴다.</p>
</blockquote>
<pre><code class="language-swift">let payloads = BlobTransferChunker.makePayloads(
    id: id,
    mimeType: mimeType,
    data: data,
    width: width,
    height: height,
    chunkSize: chunkSize
)

for payload in payloads {
    let script = &quot;window.receiveImageChunk(\(payload.jsonString));&quot;

    _ = try await webView.callAsyncJavaScript(
        script,
        arguments: [:],
        in: nil,
        contentWorld: .page
    )
}</code></pre>
<blockquote>
<p>웹에서는 청크를 받아서 다시 조립한다.</p>
</blockquote>
<pre><code class="language-ts">function decodeBase64ToUint8Array(base64) {
  const binary = atob(base64);
  const bytes = new Uint8Array(binary.length);

  for (let i = 0; i &lt; binary.length; i += 1) {
    bytes[i] = binary.charCodeAt(i);
  }

  return bytes;
}</code></pre>
<blockquote>
<p>그리고 모든 청크가 모이면 Blob을 만든다.</p>
</blockquote>
<pre><code class="language-ts">const blob = new Blob(entry.chunks, { type: mimeType });
const blobUrl = URL.createObjectURL(blob);
img.src = blobUrl;</code></pre>
<h1 id="이-방식의-장점과-단점">이 방식의 장점과 단점</h1>
<h2 id="장점">장점</h2>
<blockquote>
<p>[UInt8] 숫자 배열 JSON보다 payload 효율이 좋다
최종 렌더링은 blob: URL이라 data: URL보다 낫다
커스텀 스킴 없이도 동작한다
mixed content를 민감하게 보는 상황에서도 시도할 수 있다</p>
</blockquote>
<h2 id="단점">단점</h2>
<blockquote>
<p>여전히 JS 브리지로 큰 payload를 여러 번 넘긴다
base64 인코딩/디코딩 비용이 있다
구조가 Base64 직행보다 복잡하다
커스텀 스킴보다 효율적인 구조는 아니다
즉, 이 방식은 “가장 빠른 방식”이라기보다
이번 제약 안에서 가장 현실적인 타협점에 가깝다.</p>
</blockquote>
<ul>
<li>그럼 청크는 몇 KB로 자르는 게 좋을까?</li>
</ul>
<p>너무 작으면:</p>
<blockquote>
<p>payload 수가 너무 많아진다
JS 호출 횟수가 늘어난다</p>
</blockquote>
<p>너무 크면:</p>
<blockquote>
<p>한 번에 넘기는 문자열이 무거워진다
브리지 부담이 커진다
그래서 여기서는 말로만 판단하지 않고
아예 테스트 코드를 추가해서 비교해보기로 했다.</p>
</blockquote>
<p>비교 대상은 아래 세 개였다.</p>
<ul>
<li>128KB</li>
<li>256KB</li>
<li>512KB</li>
</ul>
<p>그리고 기준값(base)은 256KB로 잡았다.</p>
<h3 id="테스트-코드-작성">테스트 코드 작성</h3>
<blockquote>
<p>핵심은 100장의 synthetic image data를 기준으로
payload 생성 비용을 비교하는 것이다.</p>
</blockquote>
<pre><code class="language-swift">@Test func benchmarkBase64BlobChunkSizesFor100Images() async throws {
    let imageCount = 100
    let syntheticImageSize = 1 * 1024 * 1024
    let imageData = Self.makeSyntheticImageData(byteCount: syntheticImageSize)
    let images = Array(repeating: imageData, count: imageCount)
    let chunkSizes = [128, 256, 512].map { $0 * 1024 }
    let baselineChunkSize = 256 * 1024

    let results = chunkSizes.map { chunkSize in
        Self.runBenchmark(images: images, chunkSize: chunkSize)
    }

    guard let baseline = results.first(where: { $0.chunkSize == baselineChunkSize }) else {
        Issue.record(&quot;Missing 256KB baseline result&quot;)
        return
    }

    print(&quot;=== Blob Chunk Benchmark (100 images) ===&quot;)
    print(&quot;Synthetic image size per item: \(syntheticImageSize) bytes&quot;)
    print(&quot;Base chunk size: \(baseline.chunkSize / 1024)KB&quot;)
}</code></pre>
<p>테스트 조건은 아래와 같다.</p>
<p>이미지 수: 100
이미지 크기: 장당 1MB
비교 대상: base64 청크 -&gt; JSON payload 생성 비용</p>
<h3 id="측정-결과">측정 결과</h3>
<pre><code class="language-txt">=== Blob Chunk Benchmark (100 images) ===
Synthetic image size per item: 1048576 bytes
Base chunk size: 256KB
chunk=128KB elapsed=320.24ms payloads=800 jsonChars=139888800 vsBase=1.02x
chunk=256KB elapsed=313.60ms payloads=400 jsonChars=139850000 vsBase=1.00x
chunk=512KB elapsed=395.63ms payloads=200 jsonChars=139829800 vsBase=1.26x</code></pre>
<p>결과 해석
이 결과를 보면 꽤 재미있다.</p>
<ul>
<li>128KB
payload 수가 많다 (800)
시간은 나쁘지 않다
하지만 기준값인 256KB보다 아주 약간 느리다</li>
<li>256KB
payload 수와 브리지 부담 사이 균형이 가장 좋았다
이번 테스트에서는 가장 빠른 값이었다</li>
<li>512KB
payload 수는 가장 적다 (200)
그런데 한 번에 넘기는 문자열이 너무 커진다
결과적으로 오히려 더 느렸다</li>
</ul>
<h4 id="즉-이번-조건에서는">즉, 이번 조건에서는</h4>
<blockquote>
<p>&quot;청크를 크게 잡으면 더 빠를 것이다&quot;</p>
</blockquote>
<p>라는 생각은 X</p>
<p>브리지 호출 수만 줄인다고 해결되는 게 아니라,
각 payload가 너무 커지면 오히려 손해라는 걸 확인할 수 있었다.</p>
<ul>
<li><p>Base64 data: URL</p>
<ul>
<li>가장 단순함</li>
<li>적은 수의 이미지에는 괜찮음</li>
<li>하지만 보안/대량 전송 관점에서는 부담</li>
</ul>
</li>
<li><p>커스텀 스킴</p>
<ul>
<li>성능상으로는 매력적임</li>
<li>다만 앱 의존도가 커짐</li>
</ul>
</li>
</ul>
<ul>
<li>[UInt8] 숫자 배열 JSON<ul>
<li>아이디어는 단순했지만 payload 효율이 나빴음</li>
</ul>
</li>
</ul>
<ul>
<li>base64 청크 분할 전송<ul>
<li>웹에서 Blob 재조립</li>
<li>청크 크기는 256KB</li>
</ul>
</li>
</ul>
<hr>
<h2 id="마무리-해보시면서">마무리 해보시면서</h2>
<blockquote>
<p>처음엔 그냥</p>
<p>&quot;이미지를 WebView에 전달하면 되겠네&quot;</p>
</blockquote>
<p>정도로 생각했던 문제였는데,
실제로는 단순 전달 문제가 아니라 아래가 한꺼번에 걸려 있었다.</p>
<blockquote>
<ul>
<li>성능</li>
<li>보안</li>
<li>앱 의존도</li>
<li>웹 구조</li>
<li>대량 이미지 처리</li>
</ul>
<p>이번에 정리한 방식이 절대적인 정답은 아니다.
하지만 이번 조건에서는 충분히 납득 가능한 선택지였다.</p>
<p>다음엔 이 흐름을 바탕으로 아래도 비교해보면 재미있을 것 같다.</p>
</blockquote>
<blockquote>
<p>웹이 직접 </p>
</blockquote>
<pre><code class="language-js">&lt;input type=&quot;file&quot;&gt;</code></pre>
<blockquote>
<p>로 파일을 가지는 구조
네이티브가 이미지 소유권을 계속 갖는 구조... 이방식이 결국 최종 채택되었지만
오늘은 여기까지</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Image Cache - Disk, Memory Storage]]></title>
            <link>https://velog.io/@little_tail/Image-Cache-Disk-Memory-Storage</link>
            <guid>https://velog.io/@little_tail/Image-Cache-Disk-Memory-Storage</guid>
            <pubDate>Thu, 15 Jan 2026 10:50:48 GMT</pubDate>
            <description><![CDATA[<h3 id="시작하기에-앞서">시작하기에 앞서</h3>
<blockquote>
<p>이번시간엔 Disk, Memory 캐시에 핵심 포인트를 정리해 보려고 합니다.</p>
</blockquote>
<h1 id="목표">목표</h1>
<blockquote>
<ul>
<li>네트워크 요청 횟수를 줄이기</li>
<li>이미지 로딩 체감을 개선하기</li>
<li>앱 종료 이후에도 캐시를 유지</li>
<li>메타데이터를 갱신하여 회신화 하기</li>
</ul>
</blockquote>
<h1 id="memory-storage">Memory Storage</h1>
<ul>
<li><code>NSCache</code> 기반의 인메모리 캐시 방식</li>
<li>앱 종료 시 사라지며, 가장 빠른 조회 경로 사용</li>
<li>I/O 비용이 매우 적기 때문에 디코딩 이후 이미지를 바로 반환하기에 적합</li>
<li><code>totalCostLimit</code> 기준으로 시스템에서 자동 제거 -&gt; 메모리 압박 상황관리</li>
<li>NSCache`는 LRU에 가까운 방식으로 정리됨</li>
<li>비용 기준으로 메모리 압박을 완화</li>
</ul>
<pre><code class="language-swift">public init(totalCostLimit: Int = 64 * 1024 * 1024) {
    self.cache = NSCache&lt;NSString, EntryBox&gt;()
    self.cache.totalCostLimit = totalCostLimit
}</code></pre>
<h1 id="disk-storage">Disk Storage</h1>
<ul>
<li><code>FileManager</code> 기반의 디스크 캐시</li>
<li>앱 종료 후에도 유지됨 -&gt; 재실행 시에도 캐시가 남음.</li>
<li>I/O가 상대적으로 부담됨 -&gt; 메모리 캐시 miss 시의 2차 레이어로 사용</li>
<li>디스크 용량 / 유통기한 기준으로 정리하여 캐시의 용량 관리</li>
</ul>
<pre><code class="language-swift">private struct LastPath {
    static let data = &quot;.data&quot; // 이미지 데이터
    static let metadata = &quot;.meta.json&quot; // 갱신정보들
}</code></pre>
<pre><code class="language-swift">public var filenameSafeHash: String {
    let data = Data(rawValue.utf8)
    let digest = SHA256.hash(data: data)
    return digest.map { String(format: &quot;%02x&quot;, $0) }.joined()
}</code></pre>
<h1 id="디스크-저장-구조">디스크 저장 구조</h1>
<ul>
<li>baseURL 하위에 <strong>SHA256</strong> 해시 파일명으로 저장<ul>
<li>이름에서 <strong>&quot;/&quot;</strong> 등과같은 문자에 의한 <strong>경로 손상</strong> 방지, <strong>길이 제한</strong> 등의 의한 이유</li>
</ul>
</li>
<li><code>&lt;hash&gt;.data</code>는 이미지 원본 데이터</li>
<li><code>&lt;hash&gt;.meta.json</code>은 <code>CacheMetadata</code>를 JSON으로 저장</li>
</ul>
<h1 id="metadata">Metadata</h1>
<pre><code class="language-swift">public struct CacheMetadata: CacheSerializer {
    public let originalUrlString: String
    public let createdDate: Date
    public var eTag: ETagCache?
    public var accessCount: Int
    public var lastAccessTime: Date
    public var lastModified: Date?
}</code></pre>
<blockquote>
<p>디스크 캐시에서 접근 정보를 기록하여 pruning 기준으로 사용됩니다.
마지막 접근 시간과 접근 횟수는 LRU, 유통기한을 통해 정리할때 활용됩니다.</p>
</blockquote>
<h1 id="접근-정보-갱신">접근 정보 갱신</h1>
<ul>
<li>디스크에서 읽어올 때마다 <code>touch()</code>를 호출합니다.<ul>
<li>횟수를 통해 덜 썻는지 구분하기 위함.</li>
</ul>
</li>
<li><code>accessCount</code>와 <code>lastAccessTime</code>을 최신화하여 LRU 기준을 유지합니다.<pre><code class="language-swift">public mutating func touch() {
  accessCount += 1
  lastAccessTime = Date()
}</code></pre>
</li>
</ul>
<h1 id="disk-readwrite-포인트">Disk Read/Write 포인트</h1>
<ul>
<li>read 시 <code>metadata.touch()</code>로 접근 정보를 갱신합니다.</li>
<li>write 시 data + metadata를 함께 기록합니다.</li>
<li>요청마다 접근시간/횟수를 업데이트하여 LRU 판단 근거로 사용합니다.</li>
</ul>
<pre><code class="language-swift">private func readEntry(for key: CacheKey) -&gt; CacheEntry&lt;CacheMetadata&gt;? {
    // ... load data/metadata
    metadata.touch()
    // ... re-save metadata
    return CacheEntry(data: data, metadata: metadata)
}</code></pre>
<h1 id="메모리-히트-시-메타데이터-업데이트">메모리 히트 시 메타데이터 업데이트</h1>
<ul>
<li>메모리 히트는 디스크를 즉시 쓰지 않고, <code>DiskAccessRecorder</code>에 쌓아둡니다.</li>
<li>기본 5초 디바운스로 모아서 <code>disk.touch(keys)</code>를 호출합니다.</li>
<li>앱이 백그라운드로 가는 시점에는 즉시 <code>flush()</code>로 반영합니다.</li>
</ul>
<pre><code class="language-swift">public func get(_ key: CacheKey) async -&gt; CacheEntry&lt;CacheMetadata&gt;? {
    if let entry = await memory.get(key) {
        Task { await accessRecorder.record(key) }
        return entry
    }
    // ...
}</code></pre>
<pre><code class="language-swift">func record(_ key: CacheKey) {
    pending.insert(key)
    scheduleFlushIfNeeded()
}</code></pre>
<h1 id="pruning-가지치기">Pruning (가지치기)</h1>
<ul>
<li>오래된 항목 제거 후, 크기 제한 초과 시 LRU 순으로 제거합니다.</li>
</ul>
<pre><code class="language-swift">if now.timeIntervalSince(item.lastAccessTime) &gt; ageLimit {
    try? fileManager.removeItem(at: item.dataURL)
    try? fileManager.removeItem(at: item.metaURL)
}

...

for item in items where totalSize &gt; diskLimit {
    try? fileManager.removeItem(at: item.dataURL)
    try? fileManager.removeItem(at: item.metaURL)
    totalSize -= item.size
    removed += 1
}</code></pre>
<h1 id="캐시-저장-시점">캐시 저장 시점</h1>
<ul>
<li>Memory hit: 즉시 반환합니다.</li>
<li>Disk hit: 메모리에 올려둔 뒤 반환합니다.</li>
<li>Network hit: 메모리와 디스크에 모두 기록합니다.</li>
</ul>
<h1 id="캐시-흐름">캐시 흐름</h1>
<ul>
<li>1차로 메모리 캐시를 조회합니다.</li>
<li>메모리 miss일 경우 디스크 캐시를 조회합니다.</li>
<li>디스크 hit면 메모리에 다시 올립니다. ( 승격 ) 그 후 반환합니다.</li>
<li>디스크 miss이면 네트워크 요청 후 두 캐시에 모두 저장합니다.</li>
<li>메모리 hit는 디스크 메타데이터를 즉시 쓰지 않고 모아둔 후 정리합니다.</li>
</ul>
<h1 id="다이어그램">다이어그램</h1>
<p><img src="https://velog.velcdn.com/images/little_tail/post/01adc37b-acb2-41f4-ad84-ed1b2251f864/image.png" alt="Image Cache Flow"></p>
<h1 id="마치며">마치며</h1>
<blockquote>
<p>이번엔 저번편에 이어서
좀더 구현쪽에 가깝게 정리를 해보았습니다.
전체를 설명하기 보단 핵심 설명과, 흐름을 다시한번 자세히 정리하는게
좋을 것 같다는 생각이 들었어서 이번편은 여기서 마무리 지어보오록 하겠습니다.
모두 고생 하셨습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Image Cache - 개념편]]></title>
            <link>https://velog.io/@little_tail/Image-Cache-%EA%B0%9C%EB%85%90%ED%8E%B8</link>
            <guid>https://velog.io/@little_tail/Image-Cache-%EA%B0%9C%EB%85%90%ED%8E%B8</guid>
            <pubDate>Thu, 01 Jan 2026 17:17:40 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가기에-앞서">들어가기에 앞서</h3>
<blockquote>
<p>해당 편은 이미지 캐시에 대한 이야기를 다루고자 합니다.</p>
</blockquote>
<h1 id="cache">Cache</h1>
<blockquote>
<p>자주 사용하는 데이터, 값을 복사해 놓는 임시 장소 </p>
</blockquote>
<hr>
<h2 id="ios-에서-캐싱-방식">iOS 에서 캐싱 방식</h2>
<h3 id="--disk">- Disk</h3>
<ul>
<li><p>구현: FileManager로 파일 저장/읽기, (메타데이터 JSON/DB로 관리 가능)</p>
</li>
<li><p>특징</p>
<blockquote>
<ul>
<li>( <code>Caches</code> / <code>Documents</code> / <code>App Group</code> )에 저장 → 앱 종료/재실행 후에도 유지</li>
<li>용량이 비교적 크고 대용량 데이터에 유리</li>
<li>I/O 비용이 꽤 큼 -&gt; Memory보다 느림</li>
</ul>
</blockquote>
</li>
<li><p>장점</p>
<blockquote>
<ul>
<li>재실행 후에도 캐시가 남아서 네트워크/처리 비용 절감</li>
<li>메모리 압박과 무관하게 비교적 안정적으로 유지</li>
<li>이미지/비디오 등 큰 데이터 캐싱에 적합</li>
<li>App Group 쓰면 앱+위젯 / 익스텐션 공유 가능</li>
</ul>
</blockquote>
</li>
<li><p>단점</p>
<blockquote>
<ul>
<li>읽기/쓰기 시 디스크 I/O → 빈번하면 성능 저하(스크롤 끊김 등)</li>
<li>정리 정책 필요(용량 제한, 만료 시간, LRU prune) 안 하면 계속 쌓임</li>
<li>메타 갱신(접근 시간 기록 등)을 자주 하면 write 폭증 가능</li>
<li>암호화/백업 제외 등 고려</li>
</ul>
</blockquote>
</li>
</ul>
<h3 id="--memory">- Memory</h3>
<ul>
<li><p>구현: NSCache, URLCache(HTTP 캐시), 또는 직접 메모리 딕셔너리(+eviction)</p>
</li>
<li><p>특징</p>
<blockquote>
<ul>
<li>RAM에 저장 → 앱 종료 시 휘발</li>
<li>접근이 빠름(디스크 대비) → UI 유리</li>
<li>시스템이 메모리 압박 상황이 발생하면 자동으로 비울 수 있음</li>
</ul>
</blockquote>
</li>
<li><p>장점</p>
<blockquote>
<ul>
<li>빠른 캐시</li>
<li>스크롤/재사용 뷰에서 유리</li>
<li>NSCache는 시스템이 필요하면 알아서 지움 (크래시 위험 감소)</li>
</ul>
</blockquote>
</li>
</ul>
<p>단점</p>
<blockquote>
<ul>
<li>앱 꺼지면 사라짐</li>
<li>메모리 용량이 작고 변동적이라 항상 남아있다는 보장 없음</li>
<li>큰 데이터(원본 이미지 Data)를 많이 올리면 메모리 폭증 위험</li>
</ul>
</blockquote>
<h1 id="nscache">NSCache</h1>
<blockquote>
<ul>
<li>key-value 형태</li>
<li>시스템 메모리 자동관리</li>
<li>딕셔너리 처럼 Key 값을 복사해서 가져오는 형태가 아님<ul>
<li>Key 객체 참조를 그대로 사용</li>
<li>Value 가 사라지면 알아서 사라짐</li>
</ul>
</li>
</ul>
</blockquote>
<h1 id="만들어볼-캐싱-그림">만들어볼 캐싱 그림</h1>
<p><img src="https://velog.velcdn.com/images/little_tail/post/0aa780b5-24d4-497c-b9a3-2d90bba8a8d6/image.png" alt=""></p>
<pre><code class="language-text">                    싱글톤 구조인 매니저
                           ↓
            사용자 요구 (메모리, 디스크, 둘다, 안함)
                           ↓
                     둘다에 경우 승격구조 
     ( 메모리 탐색 없으면 -&gt; 디스크 탐색 -&gt; 적중시 메모리 승격 )
( 승격후 디스크 메타 데이터 반영 ( 일정시간 지난 후 혹은 백그라운드 진입 )
                           ↓
                      리사이징 여부
                           ↓
                         이미지</code></pre>
<h1 id="승격에-대한-이야기">승격에 대한 이야기</h1>
<blockquote>
<p>메모리에 캐싱을 하게 되면 일단, 시스템에서 공간이 부족하거나 하였을때
알아서 제거를 해줍니다. 이는 사용자가 다른 앱으로 활동을 할때에
메모리 할당 용량이 줄어들게 되는 구조인거죠. ( 앱을 껏을때도 )</p>
</blockquote>
<blockquote>
<p>만약, 메모리에 올린후 디스크에 반영하지 않았다면
찾지 못하여 새로 API 를 요청해야 합니다.</p>
</blockquote>
<blockquote>
<p>그렇다면, 디스크에서 찾았다면요?
사용자가 UI에서 유리할 수 있도록 메모리로 승격을 하는 구조가 되죠.</p>
</blockquote>
<h2 id="디스크-비움에-대한-기준">디스크 비움에 대한 기준</h2>
<blockquote>
<p>고려해봐야 할 사항중
디스크 이야기를 뺄 수가 없을것 같군요</p>
</blockquote>
<blockquote>
<p>메모리에서 내려온 이미지를 위해 디스크를 활용하게 될텐데
그렇다면 디스크는 언제 제거해야 할까요?</p>
</blockquote>
<h3 id="lru---least-recently-used">LRU - Least Recently Used</h3>
<blockquote>
<p>여러가지 방법이 존재하겠으나
이번 시간에는 LRU 방식을 사용해 보려고 합니다.</p>
</blockquote>
<blockquote>
<p>이름을 해석하면
가장 오래 안쓴것 이죠.
오래 참조하지 않았으니 그만큼 사용자에겐 관심이 없다는 뜻 즉
그 기준으로 제거해 보려고 합니다.</p>
</blockquote>
<h3 id="메모리로-승격후-디스크반영">메모리로 승격후 디스크반영</h3>
<blockquote>
<p>위에서 언급했듯이 결국 디스크에 사용했음을 기록해야합니다.
고민해 봐야 할 부분이죠. 왜냐...</p>
</blockquote>
<blockquote>
<p>I/O 부담이 있으니 디스크 기록 타이밍을 생각해봐야 하기 때문이죠.</p>
</blockquote>
<h3 id="시간-방식">시간 방식</h3>
<blockquote>
<p>이 부분은 이번 파트에선
사용자가 사용한 시점 기준 특정초까지 담아두었다
한번에 반영하는 구조를 구성해보려 합니다.</p>
</blockquote>
<h1 id="etag">ETag</h1>
<blockquote>
<p>모든 서버는 아니지만, 서버 입장에서도 리소스를 아끼기 위해
Etag 라는것을 응답헤더에 달아준다는 사실을 알고 계셨나요?</p>
</blockquote>
<pre><code class="language-http">ETag: &quot;33a64df551425fcc55e4d42a148795d9f25f89d4&quot;
ETag: W/&quot;0815&quot;</code></pre>
<blockquote>
<p>위와 같은 형태로 값을 서버에서 내려주게 되는데
이 값을 저장해 두었다가 서버에게</p>
</blockquote>
<pre><code class="language-text">If-None-Match: &quot;&lt;etag_value&gt;&quot;</code></pre>
<blockquote>
<p>같은 형태로 보내게 된다면
<code>304 - Not Modified</code> 값을 주게 됩니다.
즉 저희는 캐시값을 그대로 쓸 수 있으며, 서버도 리소스를 아낄수 있게 되죠.</p>
</blockquote>
<p>[참조문헌-1_ETag] (<a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Reference/Headers/ETag">https://developer.mozilla.org/ko/docs/Web/HTTP/Reference/Headers/ETag</a>)</p>
<h1 id="last-modified">last-modified</h1>
<blockquote>
<p>모든 서버가 ETag를 주지 않듯이 이친구도 마찬가지 입니다만
Etag 대신 <code>last-modified</code>를 사용하는 서버도 있답니다.</p>
</blockquote>
<pre><code class="language-text">Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT</code></pre>
<blockquote>
<p>위와 같은 형태로 서버가 값을 주는데
날짜 형식이 서버마다 다를까 조사해 보았으나</p>
</blockquote>
<p>GMT
그리니치 표준시. HTTP 날짜는 현지 시각이 아닌, 언제나 GMT로 표현합니다</p>
<blockquote>
<p>라고 적혀 있기에 충분히 활용해 보면 좋을 것 같습니다.</p>
</blockquote>
<p><a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Reference/Headers/Last-Modified">참조문헌-2_last-modified</a></p>
<h2 id="파일명-고려사항">파일명 고려사항</h2>
<ul>
<li><code>/, ?, :, &amp;, %, 유니코드</code> 등 일부 파일명으로써 문제가 발생할 수 있는 상황</li>
<li>파일이름이 무지막지하게 긴 경우</li>
<li>파일명 유출</li>
</ul>
<h3 id="sha256">SHA256</h3>
<blockquote>
<p>해시함수
길이가 고정된 256비트 -&gt; 32바이트 구조</p>
</blockquote>
<ul>
<li>특징<ul>
<li>고정 길이<ul>
<li>해시함수니 같은 인풋은 같은 아웃풋 형태</li>
<li>역으로 해석 불가</li>
<li>이름 충돌 방지 ( 아예 안나는건 아님 확률이 극악 )</li>
</ul>
</li>
</ul>
</li>
</ul>
<blockquote>
<p>위 SHA256 해싱을 통해 이름을 <code>16</code>진수 형태로 바꾸어 연결하여
사용해 보겠습니다.</p>
</blockquote>
<h1 id="github">GitHub</h1>
<blockquote>
<p>참고로 이미 프로젝트는 만들어 놓은 상태입니다.
계속 디벨롭 해봐야 할 것 투성이지만
미리 보고 싶으신분들은 아래 링크를 타시면 되겠습니다.</p>
</blockquote>
<p><a href="https://github.com/Little-tale/SimpleImageCacheSwift">GitHub 먼저 볼래요 - SimpleImageCacheSwift</a></p>
<h3 id="마무리하면서">마무리하면서</h3>
<blockquote>
<p>이번시간은 개념 뼈대와 흐름을 적어 보었습니다.
고려 사항들을 꼭 같이 고민해 주셨으면 합니다.
생각보다 생각할 부분들이 많더라구요.</p>
<p>특히 메모리 승격 부분이 저에게 있어선 좀 난제 였던 것 같습니다.
디스크 I/O 부담을 줄여보겠다고 고민을 많이 했어서</p>
</blockquote>
<blockquote>
<p>아무튼 오늘도 고생하셨습니다.
새해복 많이 받으세요 다들!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[AlarmKit ]]></title>
            <link>https://velog.io/@little_tail/AlarmKit</link>
            <guid>https://velog.io/@little_tail/AlarmKit</guid>
            <pubDate>Tue, 23 Dec 2025 14:26:36 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가기에-앞서">들어가기에 앞서</h2>
<blockquote>
<p>사실 해당 API 는 문서에서 제공하는 데모코드 마저도
삭제된 메서드가 존재하는 만큼 불안정한 API라
언제 다루어 볼까 싶었는데
iOS 26.1 이 넘어가면서 부턴 괜찮아지는 것 도 같아서 다루어 볼까 합니다.</p>
</blockquote>
<p><a href="https://developer.apple.com/documentation/AlarmKit">AlarmKit Docs</a>
<a href="https://github.com/Little-tale/AlarmKit_LittleTale">AlarmKit_LittleTale</a></p>
<p><img src="https://velog.velcdn.com/images/little_tail/post/a0c0c248-99b7-430e-8167-e0c9b490591f/image.png" alt=""></p>
<h1 id="alarmkit">AlarmKit</h1>
<blockquote>
<p>알람킷 이름부터가 네. 알람을 울릴수 있는 도구이죠.
중요한 부분은 <strong>알람은 현재상태(예를들어 무음)를 무시하고 실행</strong> 된다는 점이 중요하죠
아이폰에서 전부터 사용하던 시계의 알람 생태계를 접근할 수 있다가
좀 더 맞는 표현인 것 같습니다.</p>
</blockquote>
<h2 id="nsalarmkitusagedescription">NSAlarmKitUsageDescription</h2>
<blockquote>
<p>알람킷도 권한이 필요합니다.
알람 예약 기능이 필요한 이유에 대해서 <code>NSAlarmKitUsageDescription</code> 키를 추가해야 합니다.</p>
</blockquote>
<p><a href="https://developer.apple.com/documentation/BundleResources/Information-Property-List/NSAlarmKitUsageDescription">NSAlarmKitUsageDescription</a></p>
<h2 id="alarm-manager">Alarm Manager</h2>
<blockquote>
<p>모든 알림을 중앙에서 관리하는 객체입니다.</p>
</blockquote>
<ul>
<li>권한관리</li>
<li>알림관리 (stop, start)</li>
</ul>
<pre><code class="language-swift">// 스케줄 등록
let _ = try await AlarmManager.shared.schedule(id: id, configuration: config)

// 알람 관리
if (isCancelled) {
    try AlarmManager.shared.cancel(id: alarmID) // 취소
} else {
    if (isResumed) {
        try AlarmManager.shared.resume(id: alarmID) // 재게
   } else {
        try AlarmManager.shared.pause(id: alarmID) // 중단
   }
}

// 권한 관리
private func checkAndAuthorize() async throws -&gt; Bool {
    var currentValue = false
    switch AlarmManager.shared.authorizationState {
    case .notDetermined:
        let status = try await AlarmManager.shared.requestAuthorization()
        currentValue = status == .authorized
    case .denied:
        currentValue = false
    case .authorized:
        currentValue = true
    @unknown default:
        fatalError()
    }

    return currentValue
}</code></pre>
<h2 id="alarmconfiguration">AlarmConfiguration</h2>
<blockquote>
<p>알람의 스케줄, 속성, Intent 를 정의하는 하나의 구조체 입니다.</p>
</blockquote>
<pre><code class="language-swift">let attributed = AlarmAttributes&lt;MyAlarmMetaData&gt;(
    presentation: presentation,
    metadata: MyAlarmMetaData(method: .wakeUp),
    tintColor: .orange
)

let schedule = Alarm.Schedule.fixed(store.state.scheduleDate)

let config = AlarmManager.AlarmConfiguration(
    schedule: schedule,
    attributes: attributed,
    secondaryIntent: OpenAppIntents(id: id)
)</code></pre>
<h2 id="alarmpresentation">AlarmPresentation</h2>
<blockquote>
<p>알람의 상태에 따른 UI 구성을 제어합니다.</p>
</blockquote>
<h4 id="문서">문서</h4>
<pre><code class="language-swift">let alert = AlarmPresentation.Alert(title: &quot;Eggs are ready!&quot;,
stopButton: AlarmButton(text: &quot;Stop&quot;, textColor: .blue, systemImageName: &quot;stop.circle&quot;),
secondaryButton: AlarmButton(text: &quot;Repeat&quot;, textColor: .blue, systemImageName: &quot;repeat&quot;),
secondaryButtonBehavior: .countdown)

let countdown = AlarmPresentation.Countdown(title: &quot;Eggs are cooking&quot;)

let paused = AlarmPresentation.Paused(title: &quot;Timer paused&quot;,
resumeButton: AlarmButton(text: &quot;Resume&quot;, textColor: .blue, systemImageName:&quot;play.circle&quot;))

let presentation = AlarmPresentation(alert: alert, countdown: countdown, paused: paused)</code></pre>
<h4 id="실사용시">실사용시</h4>
<pre><code class="language-swift"> /// The appearance of the stop button.
@available(*, deprecated, message: &quot;This property is not used anymore and will be removed.&quot;)
public var stopButton: AlarmButton

/// Creates an alert for an alarm.
/// - Parameters:
///   - title: The title of the alert.
///   - stopButton: The end button for an alarm.
///   - secondaryButton: The customizable second button for an alarm.
///   - secondaryButtonBehavior: The defined behavior of the secondary button.
@available(iOS, deprecated: 26.1, message: &quot;stopButton is deprecated and will no longer be used&quot;)
public init(title: LocalizedStringResource, stopButton: AlarmButton, secondaryButton: AlarmButton? = nil, secondaryButtonBehavior: AlarmPresentation.Alert.SecondaryButtonBehavior? = nil)</code></pre>
<pre><code class="language-swift"> let alert = AlarmPresentation.Alert(
     title: &quot;일어나&quot;,
     secondaryButton: AlarmButton(
     text: &quot;Go To App&quot;,
        textColor: .blue,
        systemImageName: &quot;app.fill&quot;
    ),
    secondaryButtonBehavior: .custom
)
// 위는 UI 구성 요소
let presentation = AlarmPresentation(alert: alert)</code></pre>
<h2 id="alert">Alert</h2>
<blockquote>
<p>알림:
간단한 알림 기능을 제공합니다.</p>
</blockquote>
<h5 id="wwdc25-code">WWDC25 Code</h5>
<pre><code class="language-swift"> func  scheduleAlertOnlyExample () { 
    let alertContent =  AlarmPresentation.Alert ( title: &quot;Wake Up&quot; , stopButton: .stopButton) 

    let attributes =  AlarmAttributes &lt;CookingData&gt; ( presentation : AlarmPresentation (alert: alertContent), 
                                                  tintColor: Color.accentColor ) 

    let alarmConfiguration =  AlarmConfiguration (schedule: .twoMinsFromNow, attributes: attributes) 

    scheduleAlarm(id: UUID (), label: &quot;Wake Up&quot; , alarmConfiguration: alarmConfiguration) 
}</code></pre>
<h2 id="카운트-다운-알람의-경우">카운트 다운 알람의 경우</h2>
<blockquote>
<p>반복 알림 기능이 존재합니다.</p>
</blockquote>
<pre><code class="language-swift"> // 시작값 10 초 -&gt; 다시하기 하면 10
let countDownDuration = Alarm.CountdownDuration(preAlert: 10, postAlert: 10)

let countDownPresentation = AlarmPresentation.Countdown(
    title: &quot;10Sec&quot;,
    pauseButton: AlarmButton(
        text: &quot;pause&quot;,
        textColor: .red,
        systemImageName: &quot;pause.fill&quot;
    ),
)

let pausedPresentation =  AlarmPresentation.Paused(
    title: &quot;Paused&quot;,
    resumeButton: AlarmButton(
        text: &quot;resume&quot;,
        textColor: .green,
        systemImageName: &quot;Play.fill&quot;
    )
)

let presentation = AlarmPresentation(alert: alert, countdown: countDownPresentation, paused: pausedPresentation)

// 위는 UI 구성 요소        
let attributed = AlarmAttributes&lt;MyAlarmMetaData&gt;(presentation: presentation, metadata: MyAlarmMetaData(method: .wakeUp), tintColor: .orange)

let id = UUID()

let config = AlarmManager.AlarmConfiguration(
    countdownDuration: countDownDuration,
    attributes: attributed,
    secondaryIntent: OpenAppIntents(id: id)
)</code></pre>
<h2 id="metadata">MetaData</h2>
<blockquote>
<p>앱과 위젯 확장 간에 공유되는 활동 속성의 메타데이터 
즉, 앱에서 특정 데이터를 실어 보내주고 싶을때 담아서 보내줄 수 있습니다.</p>
</blockquote>
<pre><code class="language-swift"> import AlarmKit

// 앱과 위젯 확장 간에 공유되는 활동 속성의 메타데이터 구조.
struct MyAlarmMetaData: AlarmMetadata {

    let createdAt: Date
    let method: Method?

    init(method: Method? = nil) {
        self.createdAt = Date.now
        self.method = method
    }

    enum Method: String, Codable {
        case wakeUp

        var icon: String {
            switch self {
            case .wakeUp: &quot;flame.fill&quot;
            }
        }
    }
}

// 위젯에서
    func getIcon(attributes: AlarmAttributes&lt;MyAlarmMetaData&gt;) -&gt; some View {
        Group {
            if let icon = attributes.metadata?.method?.icon {
                Image(systemName: icon)
            } else {
                EmptyView()
            }
        }
    }</code></pre>
<h3 id="현재-알람상황-추적">현재 알람상황 추적</h3>
<blockquote>
<p>앱측에서 알람 상태를 감시할 수가 있습니다.
다만, 앱이 실행중 일 때만 알림을 받을 수가 있습니다.</p>
</blockquote>
<pre><code class="language-swift">    private func observeAlarms() {
        Task {
            for await incomingAlarms in alarmManager.alarmUpdates {
                updateAlarmState(with: incomingAlarms)
            }
        }
    }

    private func updateAlarmState(with remoteAlarms: [Alarm]) {
        Task { @MainActor in

            // Update existing alarm states.
            remoteAlarms.forEach { updated in
                alarmsMap[updated.id, default: (updated, &quot;Alarm (Old Session)&quot;)].0 = updated
            }

            let knownAlarmIDs = Set(alarmsMap.keys)
            let incomingAlarmIDs = Set(remoteAlarms.map(\.id))

            // Clean-up removed alarms.
            let removedAlarmIDs = Set(knownAlarmIDs.subtracting(incomingAlarmIDs))
            removedAlarmIDs.forEach {
                alarmsMap[$0] = nil
            }
        }
    }</code></pre>
<table>
<thead>
<tr>
<th align="center">Alarm Setting</th>
<th align="center">Timer Setting</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><picture><img src="https://velog.velcdn.com/images/little_tail/post/97c4cd53-29b5-4589-8a62-b10f68eacf08/image.gif" width="200" height="440"/></picture></td>
<td align="center"><picture><img src="https://velog.velcdn.com/images/little_tail/post/d1e59222-d6d6-4782-b119-0ed80fb467ce/image.gif" width="200" height="440"/></picture></td>
</tr>
</tbody></table>
<h1 id="마무리-하며">마무리 하며</h1>
<blockquote>
<p>핵심적인 부분을 정리 해 본 시간인 것 같습니다.
사실 말로만 쓱 보면 이해가 잘 안되는 스타일이라 저는
Git Hub에다가 어떻게 하면 커스텀 할 수 있는지 </p>
</blockquote>
<blockquote>
<p>라이브 엑티비티 처음해보는데 어떻게 사용하는지 간단하게 다루어 놓았습니다.
한번 사용해보시길 바라면서
다음시간에 뵙겠습니다. 감사합니다. :)</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[iOS 26 SwiftUI UIWindow 터치문제]]></title>
            <link>https://velog.io/@little_tail/iOS-26-SwiftUI-UIWindow-%ED%84%B0%EC%B9%98%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@little_tail/iOS-26-SwiftUI-UIWindow-%ED%84%B0%EC%B9%98%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Sun, 07 Dec 2025 07:40:02 GMT</pubDate>
            <description><![CDATA[<h1 id="passthrough-uiwindow-using-swiftui-in-ios-26">Passthrough UIWindow using SwiftUI in iOS 26</h1>
<blockquote>
<p>한참 SwiftUI에서 발생한 이슈중 하나
UIWindow를 통해 SwiftUIView를 또 하나 띄었을때 위로 띄운 윈도우의
View가 터치가 되지 않는 이슈가 있었습니다.</p>
</blockquote>
<p><a href="https://stackoverflow.com/questions/79768526/passthrough-uiwindow-using-swiftui-in-ios-26">관련 링크 - stackoverflow</a></p>
<blockquote>
<p>저도 이글을 보고 문제를 해결했었는데
꽤 재밌는 문제가 생겨서 공유해 보고자 합니다.</p>
</blockquote>
<h1 id="custom-한-path-를-사용하면-문제가-재발">Custom 한 Path 를 사용하면 문제가 재발</h1>
<blockquote>
<p>네. 제목 그대로 직접만든 Path를 clipShap를 하게 되면
다시 터치가 되지 않는 이슈가 발생합니다.</p>
</blockquote>
<h2 id="window-코드-살펴보기">Window 코드 살펴보기</h2>
<pre><code class="language-swift">final class UIPassthroughWindow: UIWindow {

    private var encounteredEvents = Set&lt;UIEvent&gt;()

    override final func hitTest(_ point: CGPoint, with event: UIEvent?) -&gt; UIView? {
        // If we don&#39;t have a root controller or it does not have a view we are done and can exit
        guard let rootViewController, let rootView = rootViewController.view else { return nil }

        guard let event else {
            assertionFailure(&quot;hit testing without an event is not supported at this time&quot;)
            return super.hitTest(point, with: nil)
        }

        // We next check the base implementation for a hitView, if none is found we are done
        guard let hitView = super.hitTest(point, with: event) else {
            // defensive clearing of encountered events
            encounteredEvents.removeAll()
            return nil
        }
        defer {
            print (encounteredEvents)
            print(rootView.layer.hitTest(point)?.name)
        }
        if encounteredEvents.contains(event) {
            encounteredEvents.removeAll()
            return hitView
        } else if #available(iOS 26, *), rootView.layer.hitTest(point)?.name == nil {

            encounteredEvents.insert(event)
            return hitView
        } else if hitView == rootView {
            return nil
        } else if #available(iOS 18, *) {
            encounteredEvents.insert(event)
            return hitView
        } else {
            return hitView
        }
    }
}</code></pre>
<blockquote>
<p>iOS 26 코드만 요약하면
터치 좌표의 뷰중에서 Layer에 이름이 달려 있지 않다면, 터치할 뷰다. 
라는 이야기 입니다.</p>
</blockquote>
<blockquote>
<p>네, 현재 SwiftUI 에서 UIWindow를 다루는 행위는 사실 도박에 가깝습니다.
왜일까요?</p>
</blockquote>
<h3 id="dts-엔지니어---apple-답변">DTS 엔지니어 - Apple 답변</h3>
<blockquote>
<p> A couple people have asked what implementation details were being depended on which has now resulted in broken behavior in the implementation.</p>
</blockquote>
<blockquote>
<p>Mainly, the assumption that, when you embed a SwiftUI view in a UIHostingController, the SwiftUI Views are represented as a UIView hierarchy. That behavior is not guaranteed for any particular SwiftUI View type, and should not be depended on.</p>
</blockquote>
<blockquote>
<p>There are also assumptions about how UIHostingController&#39;s view implements hitTest, which is also not guaranteed.</p>
</blockquote>
<blockquote>
<p>So, again, my recommendation for anyone interested in implementing this sort of behavior is to please file an enhancement request using Feedback Assistant.
--Greg</p>
</blockquote>
<h3 id="번역본">번역본</h3>
<blockquote>
<p> 몇몇 분들이 구현 세부 사항에 의존한 결과 현재 구현에서 오류가 발생하고 있다고 문의하셨습니다.
주요 원인은 SwiftUI 뷰를 UIHostingController에 임베드할 때 SwiftUI 뷰가 UIView 계층 구조로 표현된다는 가정입니다. </p>
</blockquote>
<blockquote>
<p>이 동작은 특정 SwiftUI 뷰 유형에 대해 보장되지 않으며, 의존해서는 안 됩니다.
 또한 UIHostingController의 뷰가 hitTest를 구현하는 방식에 대한 가정도 존재하는데, 이 역시 보장되지 않습니다.</p>
</blockquote>
<blockquote>
<p>따라서, 이러한 동작을 구현하려는 분들께 다시 한번 권고드립니다: Feedback Assistant를 통해 기능 개선 요청을 제출해 주시기 바랍니다.</p>
</blockquote>
<h3 id="즉-현재-방식은-도박입니다">즉, 현재 방식은 도박입니다.</h3>
<blockquote>
<p>그렇다면 다른 대안이 있을까요?
저도 찾아보고 있지만 현재 마땅한 방법이 없어서
아래와 같은 방법으로 문제를 일단 막을 수는 있습니다.</p>
</blockquote>
<h4 id="1-background-에-눈에-안보이는-정도의-색을-입힌다">1. Background 에 눈에 안보이는 정도의 색을 입힌다.</h4>
<pre><code class="language-swift">func asRoundedCorner(radius: CGFloat, corners: UIRectCorner) -&gt; some View {
        self
            .clipShape(RoundedCornerShape(corners: corners, radius: radius))
            .background {
                Color.black.opacity(0.000001)
            }</code></pre>
<p>위와같이 직접만든 Shape등 터치가 안될때 컬러를 뒤에 두어주면 터치가 또 됩니다. 놀랍죠?</p>
<h4 id="2-uiview-를-가정하지말고-달아주자">2. UIView 를 가정하지말고 달아주자...</h4>
<pre><code class="language-swift">struct WindowTouchView: UIViewRepresentable {

    func makeUIView(context: Context) -&gt; UIView {
        let view = UIView()
        view.backgroundColor = .clear
        return view
    }

    func updateUIView(_ uiView: UIView, context: Context) {
        guard let layer = uiView.layer.sublayers?.first else { return }
        layer.frame = uiView.bounds
    }
}

func asRoundedCorner(radius: CGFloat, corners: UIRectCorner) -&gt; some View {
        self
            .clipShape(RoundedCornerShape(corners: corners, radius: radius))
            .background(WindowTouchView())

} </code></pre>
<p>위와 같이 UIView를 달아주니 또 정상동작 합니다.
다만, SwiftUI 에서 UIKit 를 이런 경우에도 달아주는게 성능상이나, 구조적으로
보기 않좋은 부분이라 생각이 듭니다.</p>
<h3 id="마무리-하며">마무리 하며</h3>
<blockquote>
<p>한참 이슈가 많은 우리 iOS26 참 재밌는 것 같습니다.
하루 빨리 다른 대안이나, 새로운 API 라도 iOS 측에서 제공해주길 바랄 뿐입니다.</p>
</blockquote>
<blockquote>
<p>여담 - Reactor Kit + AlarmKit 과, WebRTC 중에 무엇이 먼저 나갈지 고민입니다.
감사합니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[WebRTC - 개념- 1편]]></title>
            <link>https://velog.io/@little_tail/WebRTC-%EA%B0%9C%EB%85%90-1%ED%8E%B8</link>
            <guid>https://velog.io/@little_tail/WebRTC-%EA%B0%9C%EB%85%90-1%ED%8E%B8</guid>
            <pubDate>Sun, 30 Nov 2025 13:20:10 GMT</pubDate>
            <description><![CDATA[<h1 id="osi-7-layer">OSI 7 Layer</h1>
<p><img src="https://velog.velcdn.com/images/little_tail/post/b60fdd11-d8f8-492d-aefd-71929316f56e/image.png" alt=""></p>
<blockquote>
<p>네트워크 구조를 학습하신 분이라면 지겨운 계층인
OSI 7계층입니다.</p>
</blockquote>
<blockquote>
<p>이중에서 3번째 계층인 네트워크 계층에 집중해보도록 하겠습니다.</p>
</blockquote>
<h2 id="network-layer">Network Layer</h2>
<blockquote>
<p>물리계층부터 간단히 올라가 보죠</p>
</blockquote>
<ul>
<li><p>물리계층</p>
<blockquote>
<p>리피터, 같은 장비들의 물리적인 계층
전기적인, 빛 등의 선로</p>
</blockquote>
</li>
<li><p>데이터 링크 계층</p>
<blockquote>
<p>브릿지 같은 ( 물리적이긴 해요 )
Mac 주소할당, 약간 허브라고 생각하시면 됩니다.</p>
</blockquote>
</li>
</ul>
<blockquote>
<p>대망의 네트워크 계층
쉽게 라우터 계층이라고 전 부르기도 하는데요
아래서부터 회선 깔고 주소, 오류 잡고, 그 신호들이 결국
멀리 멀리 <strong>중계</strong>가 가야 합니다.</p>
</blockquote>
<blockquote>
<p>문제는 뭐냐? 회선들을 여러개 깔았을때 어디가 최단 루트인지,
멀리 신호를 보낼때 손실을 어떻게 해결하는지 알아야 겠죠?</p>
</blockquote>
<blockquote>
<p>이때 라운터가 중폭(신호)도 하고, 어디 어디 라우터를 찍어야 최단 루트이다.
같은 작업을 합니다. 이정도 아시면 사실 개념은 완벅합니다.
즉, <code>라우팅</code>이 핵심이다.</p>
</blockquote>
<h2 id="라우팅">라우팅</h2>
<blockquote>
<p>전기적 신호를 결정하는것이 라우팅</p>
</blockquote>
<ul>
<li>IP를 가지고 경로를 결정하는데, IP는 MAC주소와는 다르게 변경이 가능</li>
<li><blockquote>
<p>즉 고유한 IP를 부여해서 고정해야 함</p>
</blockquote>
</li>
<li>다만 보안에 있어서, 유출되면 위험해지기 때문에 이를 숨겨서 관리해야함 - private</li>
<li>공유기같은 것들은 public IP를 통해 관리를 하지만, </li>
<li>폰, 컴퓨터같은 경우 고유의 IP를 가지게됨.</li>
</ul>
<h2 id="websocket">WebSocket</h2>
<ul>
<li>서버와, 클라이언트가 있을때 3-way-handshaking 을 통해 소통하는 HTTP의 TCP 통신 방식에선<ul>
<li>요청 → 요청확인 → 전송준비(받을 수 있는 상태인지 체크) → 전송 → 받음을 확인하게 flag 과정</li>
<li>UDP: 요청 → 전송</li>
</ul>
</li>
<li>TCP vs UDP<ul>
<li>서버 상태 체크, 받을수 있는 상태인지 체크 등의 체크 과정이 있는걸 TCP</li>
<li>없는걸 UDP</li>
</ul>
</li>
<li>TCP 기반으로 웹소켓을 만듬<ul>
<li>매번 요청/응답의 대한 방식을 매번 하는 것이 아닌 한번만 이루어 지도록 함</li>
<li>HTTP 헤더가 여러번 생기는 방식이 아니다보니, 즉 포장이 여러개가 아니니 무게가 줌</li>
<li>요청할때만 보낼 수 있는게 아닌 서버에서 먼저 푸시가 가능한 형태</li>
</ul>
</li>
</ul>
<blockquote>
<p>포장이 가벼워 짐이 이해가 안갈 수도 있기에 보충설명
HTTP 는 무상태성이죠 즉, 매번 상태를 달아서 요청을 보냅니다.</p>
</blockquote>
<ul>
<li>누가 보낸 건지 → Cookie, Authorization</li>
<li>어떤 형식으로 받고 싶은지 → Accept, Accept-Language</li>
<li>이 리소스가 어디인지 → Host, 요청 라인(GET /something)</li>
<li>캐시, 압축, 인코딩 등 옵션 → Cache-Control, Content-Encoding …</li>
<li>브라우저 정보 → User-Agent</li>
</ul>
<p>즉:</p>
<ul>
<li>일반 HTTP 방식</li>
<li><blockquote>
<p>매 요청마다: HTTP 헤더 + TCP 헤더 + IP 헤더 + ...</p>
</blockquote>
</li>
</ul>
<p>WebSocket 방식</p>
<ul>
<li>처음 1번: HTTP 헤더 + TCP 헤더 + IP 헤더</li>
<li><blockquote>
<p>이후 계속: WebSocket 프레임 헤더(작음) + TCP 헤더 + IP 헤더</p>
</blockquote>
</li>
</ul>
<hr>
<p><img src="https://velog.velcdn.com/images/little_tail/post/e37fb667-fa0e-41f4-824c-9a529cfce9eb/image.png" alt=""></p>
<h1 id="webrtc-개념">WebRTC 개념</h1>
<ul>
<li>중간에 존재하는 서버가 없는 개념</li>
<li>peer - to - peer 통신 방식 ( 단말간 통신 )</li>
<li>클라이언트 쪽에서 어떤 통신을 원하는지 서버에 알려줘야함 ex) VoiceCall, Video</li>
<li>처음에는 서버 중개자가 존재<ul>
<li>서로다른 공유기, 통신망(유플,kt 등), 사내망 등에 의해 직접적인 IP를 모름</li>
<li>NAT 를 뚫어야 함</li>
<li>외의 네트워크는 밖에서 못 들어오도록 막음</li>
<li>서로의 IP를 직접 교환하면 보안에 취약함으로, <code>signaling Server</code> 가 관리해야함</li>
<li>PeerConnection 이라고 함 이를 → 끝나면 진짜 서버없이 연결되는 구조</li>
</ul>
</li>
</ul>
<h2 id="nat-network-address-translation">NAT (Network Address Translation)</h2>
<ul>
<li>Router 는 퍼블릭 IP와 각각의 단말 Private IP를 가짐</li>
<li>단말들에게 Public IP를 알려주는 것을 말하는 용어</li>
</ul>
<h2 id="stun-server">Stun Server</h2>
<ul>
<li>Stun Server<ul>
<li>Public IP 를 저장하고 있고, Peer간 직접 연결을 막는 등의 라우터의 제한을 결정하는 프로토콜</li>
<li>클라 → StunServer 에게 접근 가능한지 요청하는 구조</li>
</ul>
</li>
</ul>
<h2 id="turn-server">Turn Server</h2>
<ul>
<li>돌려주는 서버</li>
<li>P2P연결 시도후 실패한 경우 Turn 서버에서 중개하여 다시 연결할 수 있도록 하는 역활</li>
</ul>
<h2 id="signaling-server">signaling Server</h2>
<ul>
<li>누가 누구랑 어떻게 연결할지에 대한 정보(offer/answer/ICE 등)를 전달해주는 중간 서버</li>
<li>단순 시작전 매칭 연결 도와주는 서버</li>
</ul>
<hr>
<h2 id="마치면서">마치면서</h2>
<blockquote>
<p>이번편은 개념적인 부분을 다루었습니다.
다음편은 핵심 API들 간단히 살펴보고
그 다음편은 WebRTC 를 통해 통신하는 작업을 해보도록 하겠습니다.
감사합니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Match Zoom Transition - 3 - Navigation]]></title>
            <link>https://velog.io/@little_tail/Match-Zoom-Transition-3-Navigation</link>
            <guid>https://velog.io/@little_tail/Match-Zoom-Transition-3-Navigation</guid>
            <pubDate>Mon, 17 Nov 2025 10:44:29 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가기에-앞서">들어가기에 앞서</h2>
<blockquote>
<p>1편부터 보고오셔야 합니다.</p>
</blockquote>
<h1 id="전편에서-빠진-이야기">전편에서 빠진 이야기</h1>
<blockquote>
<p>정작 트렌지션을 만들어 놓고, 적용을 안했더라구요....
어떻게 해야 저 트랜지션을 적용할 수 있는지 간단하게
정리 해보도록 하겠습니다....!</p>
</blockquote>
<h2 id="modalpresentationstyle">modalPresentationStyle</h2>
<blockquote>
<p>ViewController는 모달 프레젠테이션 스타일을 정의할 수 있습니다.
이중에서 저희는 .custom으로 적용하겠습니다.</p>
</blockquote>
<pre><code class="language-swift">vc.modalPresentationStyle = .custom</code></pre>
<blockquote>
<p>커스텀이라고 하였으니 저희가 만든 트랜지션을 적용해야 합니다.</p>
</blockquote>
<pre><code class="language-swift">testDelegate = CustomZoomTransition(referenceView: testButton) { [weak self] in
    guard let self else { return }
    testDelegate = nil
}

vc.transitioningDelegate = self.testDelegate</code></pre>
<blockquote>
<p>마지막으로 모달로 프레젠트하면 됩니다.</p>
</blockquote>
<pre><code class="language-swift">self.present(vc, animated: true)</code></pre>
<h1 id="네비게이션에서도-동작하게">네비게이션에서도 동작하게..!</h1>
<blockquote>
<p>저번에 구성하였던 애니메이터로 돌아가 봅시다.</p>
</blockquote>
<pre><code class="language-swift">CustomDismissalTransitionAnimator: UIViewControllerAnimatedTransitioning

...

func animateTransition(using ctx: UIViewControllerContextTransitioning)
...</code></pre>
<blockquote>
<p>위 함수는 저희가 구성을 하지 않았었는데 구성해보겠습니다.</p>
</blockquote>
<pre><code class="language-swift">/// 인터랙티브 디스미스(모달방식)를 사용하므로 이 메서드에서는 별도 애니메이션을 수행하지 않습니다. 단, 네비게이션 방식에선 사용됩니다.
    func animateTransition(using ctx: UIViewControllerContextTransitioning) {
        // 네비 pop일 때 호출되는 곳
        guard
            let fromVC = ctx.viewController(forKey: .from),
            let toVC   = ctx.viewController(forKey: .to)
        else {
            ctx.completeTransition(false)
            return
        }

        let container = ctx.containerView
        // pop일 땐 toVC를 아래에 깔아야 함
        container.insertSubview(toVC.view, belowSubview: fromVC.view)

        // referenceView의 최종 위치
        let targetFrame = container.convert(referenceView.bounds, from: referenceView)

        let duration = transitionDuration(using: ctx)

        UIView.animate(withDuration: duration,
                       delay: 0,
                       options: [.curveEaseInOut],
                       animations: { [weak self] in
            guard let self else { return }
            // fromVC.view를 버튼 위치만큼 줄이기
            fromVC.view.frame = targetFrame
            fromVC.view.layer.cornerRadius = self.referenceView.layer.cornerRadius
        }, completion: { finished in
            // 끝나면 pop 완료
            let cancelled = ctx.transitionWasCancelled
            if !cancelled {
                fromVC.view.removeFromSuperview()
            }
            ctx.completeTransition(!cancelled)
        })
    }</code></pre>
<h3 id="uinavigationcontrollerdelegate-적용">UINavigationControllerDelegate 적용</h3>
<pre><code class="language-swift">final class CustomZoomTransition: NSObject, UINavigationControllerDelegate
 ...
 func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -&gt; (any UIViewControllerAnimatedTransitioning)? {
        switch operation {
        case .push:
            return presentingTransitionAnimator
        case .pop:
            return dismissalTransitionAnimator
        default:
            return nil
        }
    }

    deinit {
        print(&quot;** DEAD \(Self.description())&quot;)
    }</code></pre>
<pre><code class="language-swift">// 만약 네비게이션 으로 진행하려면
        self.navigationController?.delegate = self.testDelegate
        self.navigationController?.pushViewController(vc, animated: true)</code></pre>
<p><img src="https://velog.velcdn.com/images/little_tail/post/0ed514b7-b19b-46fd-8e8f-f73cbe46e82f/image.gif" alt=""></p>
<h2 id="마무리-하며">마무리 하며</h2>
<blockquote>
<p>다음 시간은 WEB RTC 입니다.
얏호 얏호
다음시간에 뵙겠습니다. 감사합니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Match Zoom Transition - 2 : dismiss]]></title>
            <link>https://velog.io/@little_tail/Match-Zoom-Transition-2-dismiss</link>
            <guid>https://velog.io/@little_tail/Match-Zoom-Transition-2-dismiss</guid>
            <pubDate>Thu, 06 Nov 2025 15:34:07 GMT</pubDate>
            <description><![CDATA[<h1 id="들어가기에-앞서">들어가기에 앞서</h1>
<blockquote>
<p>이전편을 보고 오셔야 합니다.</p>
</blockquote>
<h1 id="뒤로가기에-대한-애니메이션">뒤로가기에 대한 애니메이션</h1>
<pre><code class="language-swift">/// 디스미스 시 사용할 애니메이터 객체를 반환
    func animationController(
        forDismissed dismissed: UIViewController
    ) -&gt; UIViewControllerAnimatedTransitioning? {
        print(&quot;\(#function)&quot;)
        return dismissalTransitionAnimator
    }</code></pre>
<blockquote>
<p>위의서 반환하는 뒤로가기 애니메이터를 제작하겠습니다.</p>
</blockquote>
<h3 id="초기화-구성">초기화 구성</h3>
<pre><code class="language-swift">final class CustomDismissalTransitionAnimator: NSObject {
    private let config: CustomZoomTransitionConfiguration
    private let referenceView: UIView

    // MARK: - 초기화
    init(config: CustomZoomTransitionConfiguration, referenceView: UIView) {
        self.config = config
        self.referenceView = referenceView
    }
}</code></pre>
<blockquote>
<p>주입 받아야 하는 멤버는 다음과 같습니다.</p>
</blockquote>
<ul>
<li>컨피규레이션 </li>
<li>돌아갈 타겟 뷰</li>
</ul>
<blockquote>
<p>구성해야 하는 것은 다음과 같습니다.</p>
</blockquote>
<ul>
<li>UIViewControllerContextTransitioning</li>
</ul>
<h1 id="uiviewcontrollercontexttransitioning">UIViewControllerContextTransitioning</h1>
<blockquote>
<p>전편에서도 잠깐 나왔었는데 이번편에서 설명하겠습니다.</p>
</blockquote>
<blockquote>
<p><strong>UIViewControllerAnimatedTransitioning</strong> 를 채택한 클래스 즉 객체에게
<strong>지금 전환 상황이 어떤지</strong> 알려주는 객체입니다. 프로토콜로 구성되있습니다.</p>
</blockquote>
<pre><code class="language-swift">-- 구성 --
@MainActor public protocol UIViewControllerAnimatedTransitioning : NSObjectProtocol {

    func transitionDuration(using transitionContext: (any UIViewControllerContextTransitioning)?) -&gt; TimeInterval

    func animateTransition(using transitionContext: any UIViewControllerContextTransitioning)

    /// A conforming object implements this method if the transition it creates can
    /// be interrupted. For example, it could return an instance of a
    /// UIViewPropertyAnimator. It is expected that this method will return the same
    /// instance for the life of a transition.
    @available(iOS 10.0, *)
    optional func interruptibleAnimator(using transitionContext: any UIViewControllerContextTransitioning) -&gt; any UIViewImplicitlyAnimating

    optional func animationEnded(_ transitionCompleted: Bool)
}

-- 집중 --
func animateTransition(using transitionContext: UIViewControllerContextTransitioning)</code></pre>
<blockquote>
<p>위 메서드를 통해 해당 객체를 받아올 수 있습니다.</p>
</blockquote>
<h3 id="지금-뷰컨과-다음-뷰컨을-알수가-있다-optional">지금 뷰컨과 다음 뷰컨을 알수가 있다. (Optional)</h3>
<pre><code class="language-swift">let fromVC = transitionContext.viewController(forKey: .from)
let toVC   = transitionContext.viewController(forKey: .to)</code></pre>
<h3 id="애니메이션을-올릴-뷰를-반환-받을-수-있다">애니메이션을 올릴 뷰를 반환 받을 수 있다.</h3>
<pre><code class="language-swift">let containerView = transitionContext.containerView</code></pre>
<hr>
<hr>
<h2 id="디스미스-전환시-소요되는-시간-적용">디스미스 전환시 소요되는 시간 적용</h2>
<pre><code class="language-swift">func transitionDuration(
        using transitionContext: UIViewControllerContextTransitioning?
    ) -&gt; TimeInterval {
        print(&quot;\(#function)&quot;)
        return config.transitionDuration
    }</code></pre>
<h1 id="모달-방식에서의-인터렉티브-애니메이션">모달 방식에서의 인터렉티브 애니메이션</h1>
<h2 id="이해해보기">이해해보기</h2>
<blockquote>
<p>사용자가 뷰를 제스처를 통해서 이전 뷰 위치로 돌아가는 형태입니다.
햇갈리시면 않되는 것이 제스처 도중에는 Dismiss가 된 상황이 아닙니다.
즉 시작하는순간 해당 함수에서 최종으로 내릴건지 다시 원위치 할지를 판단해야 합니다.</p>
</blockquote>
<h2 id="uiviewcontrollerinteractivetransitioning-구현">UIViewControllerInteractiveTransitioning 구현</h2>
<h3 id="startinteractivetransition-_-을-통해">startInteractiveTransition( _:) 을 통해</h3>
<pre><code class="language-swift">func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) {
    print(#function)

    // 1. 전환에 참여하는 뷰컨을 가져온다.
    //   사라질(from) 쪽을 기준으로 (사용자가 드래그 하는 뷰)
    guard let fromVC = transitionContext.viewController(forKey: .from) else {
        return
    }

    // 2. 원래 뷰는 숨김 -&gt; 안하면 돌아가는 애니메이션 중 기존뷰도 잠깐 보이다 사라짐
    fromVC.view.isHidden = true

    // 3. 전환은 항상 containerView 위에서 일어나야 함.
    let containerView = transitionContext.containerView

    // 4. 애니메이션이 시작될 위치/크기 = 현재 from 뷰의 프레임
    let startPoint = CGPoint(x: fromVC.view.frame.minX,
                             y: fromVC.view.frame.minY)
    let startSize = fromVC.view.frame.size

    // 5. 실제로 움직일 임시 뷰(transitionView)를 만든다.
    //    - 시작 프레임은 from 뷰와 동일
    //    - 색상도 from 뷰와 맞춰서 자연스럽게
    //    - cornerRadius는 일단 0에서 시작
    let transitionView = UIView(frame: CGRect(origin: startPoint, size: startSize))
    transitionView.backgroundColor = fromVC.view.backgroundColor
    transitionView.layer.masksToBounds = true
    transitionView.layer.cornerRadius = 0
    containerView.addSubview(transitionView)

    // 6. 애니메이션이 끝났을 때 도달해야 하는 최종 프레임을 구한다.
    //    기준 뷰(referenceView)의 bounds를
    //    containerView 좌표로 변환해서 해당 위치로 가게 하는 구조.
    let finalFrame = containerView.convert(referenceView.bounds, from: referenceView)

    // 7. 스프링 애니메이션으로 뷰를 최종 위치로 늘리면서
    //    cornerRadius도 대상 뷰와 동일하게 맞춰준다.
    UIView.animate(withDuration: config.transitionDuration,
                   delay: 0,
                   usingSpringWithDamping: config.springWithDamping,
                   initialSpringVelocity: config.initialSpringVelocity,
                   options: [.beginFromCurrentState]) { [weak self] in
        guard let self else { return }

        transitionView.frame = finalFrame
        transitionView.layer.cornerRadius = referenceView.layer.cornerRadius
    } completion: { _ in
        // 8. 전환이 끝났다고 시스템에 알려준다.
        // transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        //    처럼 취소 여부를 반영해주는 게 안전하다.
        transitionContext.completeTransition(true)
    }
}</code></pre>
<h1 id="제스처에-따른-뷰-크기-위치-조작">제스처에 따른 뷰 크기 위치 조작</h1>
<pre><code class="language-swift">// MARK: - 제스처 처리
    /// 프레젠트된 화면에서 팬 제스처를 감지해 이동/알파/디스미스를 제어
    @objc
    private func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
        guard let presentedVC = presentationController?.presentedViewController,
              let container = presentedVC.view.superview else {
            return
        }

        guard let targetView = presentedVC.view else { return }

        switch gesture.state {
        case .began:
            targetView.layer.cornerRadius = 45
            // 1) 기존 뷰의 중앙, 포인트를 저장
            originalCenter = targetView.center
            originalAnchor = targetView.layer.anchorPoint

            // 2) 터치 지점을 뷰 좌표로
            let touchInView = gesture.location(in: targetView)

            // 3) anchorPoint로 바꿀 비율 (0~1)
            let newAnchor = CGPoint(
                x: touchInView.x / targetView.bounds.width,
                y: touchInView.y / targetView.bounds.height
            )
            // 0.5, 0.5 (중심) --&gt; 드래그 앵커
            targetView.layer.anchorPoint = newAnchor

            // 4) anchorPoint를 바꾸면 position이 바뀌어버리니, 그만큼 보정
            let oldPos = targetView.layer.position
            print(&quot;began - oldPos: &quot;, oldPos) // began - oldPos:  (220.0, 478.0)
            print(&quot;began - originalAnchor&quot;, originalAnchor) // began - originalAnchor (0.5, 0.5)
            print(&quot;began - newAnchor&quot;, newAnchor) // began - newAnchor (0.02727272727272727, 0.3158995815899582)

            let moveX = (newAnchor.x - originalAnchor.x) // 기준점x 0.47..만큼 왼쪽으로
            let moveY = (newAnchor.y - originalAnchor.y) // 기준점y 0.19..만큼 위로

            // px 로 전환
            let moveXPx = moveX * targetView.bounds.width
            let moveYPx = moveY * targetView.bounds.height

            let newPos = CGPoint(
                x: oldPos.x + moveXPx,
                y: oldPos.y + moveYPx
            )

            targetView.layer.position = newPos

        case .changed:
            let translation = gesture.translation(in: container)
            let dx = translation.x
            let dy = translation.y

            // 전체 이동 거리 (피타고라스)
            let distance = hypot(dx, dy)

            // 얼마나 줄일지
            let maxDrag: CGFloat = 300
            let progress = min(distance / maxDrag, 1)

            let minScale: CGFloat = 0.4
            let scale = 1 - (1 - minScale) * progress

            let scaleTransform = CGAffineTransform(scaleX: scale, y: scale)
            let translateTransform = CGAffineTransform(translationX: dx, y: dy)
            targetView.transform = scaleTransform.concatenating(translateTransform)

            // 투명도도 전체 거리 기준
            targetView.alpha = 1 - 0.4 * progress

        case .ended, .cancelled:
            // 원래대로
            let shouldDismiss = targetView.transform.ty &gt; 150
            if shouldDismiss {
                presentedVC.dismiss(animated: true)
            } else {
                UIView.animate(withDuration: 0.2, animations: { [weak self] in
                    guard let weakSelf = self else { return }
                    targetView.transform = .identity
                    targetView.alpha = 1
                    // anchorPoint도 원래대로
                    targetView.layer.anchorPoint = weakSelf.originalAnchor
                    targetView.center = weakSelf.originalCenter
                    targetView.layer.cornerRadius = 0
                })
            }

        default:
            break
        }
    }</code></pre>
<p><img src="https://velog.velcdn.com/images/little_tail/post/fd2ea82a-fd0f-4c2d-afc6-044ca6908b29/image.gif" alt=""></p>
<h1 id="github">GitHub</h1>
<p><a href="https://github.com/Little-tale/SwiftAnimations">Little-tale/SwiftAnimations</a></p>
<blockquote>
<p>ZoomTransition 4번째 폴더</p>
</blockquote>
<h1 id="마치면서">마치면서</h1>
<blockquote>
<p>이번 편은 정말 꽤 흥미로운 내용이였다고 생각합니다.
아직 끝이 아닌게
네비게이션 방식일땐 지금 같이 해두어도 동작을 안합니다.
다음편은 네비게이션 방식에서도 해보는것, 그리고, 그 다음편은 WebRTC 편을 다루어 보도록 하겠습니다.
감사힙니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Match Zoom Transition - 1 _ 개념 + 등장]]></title>
            <link>https://velog.io/@little_tail/Match-Zoom-Transition-1-%EA%B0%9C%EB%85%90-%EB%93%B1%EC%9E%A5</link>
            <guid>https://velog.io/@little_tail/Match-Zoom-Transition-1-%EA%B0%9C%EB%85%90-%EB%93%B1%EC%9E%A5</guid>
            <pubDate>Mon, 03 Nov 2025 13:25:02 GMT</pubDate>
            <description><![CDATA[<h1 id="match-transition">Match Transition</h1>
<blockquote>
<p><a href="https://developer.apple.com/videos/play/wwdc2024/10145/?time=182">WWDC2024-Match_Zoom_Transition</a>
오늘은 아래와 같은 전환 애니메이션을 구성해 볼겁니다.
SwiftUI 에서도 가능합니다. 다만, SwiftUI는 자료가 많으니 UIKit를 통해 이를 구현해 보겠습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/little_tail/post/0f6fbd8d-4f4b-46cb-8ca8-30a173680fd6/image.gif" alt=""></p>
<h2 id="ios-18">iOS 18+</h2>
<blockquote>
<p>WWDC 영상의 방법은 iOS 18이상에 대한 이야기 입니다.
아직은 iOS 16이상인 기종이 꽤 존재 하기 때문에
간단히 다루고 미만 버전을 다루어 보겠습니다.</p>
</blockquote>
<pre><code class="language-swift">   private func nextView() {
        let vc = TestZoomNextViewController()
        if #available(iOS 18.0, *) {
            vc.preferredTransition = .zoom { [weak self] context in
                guard let self else { return nil }
                return testButton
            }
        }

        self.present(vc, animated: true)
    }</code></pre>
<blockquote>
<p>보시는 거와 같이
preferredTransition 의 zoom을 이용하여 간단하게 
UIKit 에서도 해당 애니메이션을 구현할 수 있습니다.</p>
</blockquote>
<hr>
<h2 id="ios-18-under">iOS 18 under</h2>
<blockquote>
<p>자 그럼 iOS 18 이하는 못하는 걸까요?
아뇨 구현은 가능합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/little_tail/post/5aa140f8-66de-4aa9-aaf9-024e0f1c1a59/image.gif" alt=""></p>
<blockquote>
<p>위 gif 처럼 비슷하게 구현이 가능합니다.</p>
</blockquote>
<h1 id="uiviewcontrollertransitioningdelegate">UIViewControllerTransitioningDelegate</h1>
<blockquote>
<p>A set of methods that vend objects used to manage a fixed-length or interactive transition between view controllers.
뷰 컨트롤러 간의 고정 길이 또는 대화형 전환을 관리하는 데 사용되는 객체를 벤더하는 일련의 방법입니다.</p>
</blockquote>
<blockquote>
<p>말이 참 어렵단 말이죠.
&quot;이 뷰컨을 <code>모달</code>로 띄울 때, 어떤 <code>애니메이션·제스처·레이아웃</code>을 쓸지 정할께요&quot;</p>
</blockquote>
<h2 id="methods">Methods</h2>
<h3 id="animationcontroller-_presented">animationController _presented</h3>
<pre><code class="language-swift">func animationController(
    forPresented presented: UIViewController,
    presenting: UIViewController,
    source: UIViewController
) -&gt; UIViewControllerAnimatedTransitioning?</code></pre>
<blockquote>
<p>모달을 등장할때에 애니메이터를 반환합니다.</p>
</blockquote>
<h3 id="interactioncontrollerforpresentation">interactionControllerForPresentation</h3>
<pre><code class="language-swift">func animationController(
    forDismissed dismissed: UIViewController
) -&gt; UIViewControllerAnimatedTransitioning?</code></pre>
<blockquote>
<p>프렌젠트에서 제스처를 제공할 애니메이터 반환 합니다.</p>
</blockquote>
<h3 id="animationcontroller-_dismiss">animationController _dismiss</h3>
<pre><code class="language-swift">func animationController(
    forDismissed dismissed: UIViewController
) -&gt; UIViewControllerAnimatedTransitioning?</code></pre>
<blockquote>
<p>dismiss 시에 사용할 애니메이터를 반환 합니다.</p>
</blockquote>
<h3 id="interactioncontrollerfordismissal">interactionControllerForDismissal</h3>
<pre><code class="language-swift">func interactionControllerForDismissal(
    using animator: UIViewControllerAnimatedTransitioning
) -&gt; UIViewControllerInteractiveTransitioning?</code></pre>
<blockquote>
<p>드래그를 통한 제스처에 대한 뒤로가기 애니메이션 반환</p>
</blockquote>
<h3 id="presentationcontroller">presentationController</h3>
<pre><code class="language-swift">func presentationController(
    forPresented presented: UIViewController,
    presenting: UIViewController?,
    source: UIViewController
) -&gt; UIPresentationController?</code></pre>
<blockquote>
<p>뷰 계층/딤뷰/코너/시트 높이 같은 레이아웃 관리 컨틀롤러를 반환합니다.</p>
</blockquote>
<blockquote>
<p>위의 메소드들을 통해서 위와 같은 Zoom Transition 을 구성해 보도록 하겠습니다.</p>
</blockquote>
<h2 id="customzoomtransitionconfiguration">CustomZoomTransitionConfiguration</h2>
<blockquote>
<p>기본제공되는 그런건 아닙니다.
애니메이션을 만들때에 사용할 기본적인 구조를 만들어 보겠습니다.</p>
</blockquote>
<pre><code class="language-swift">/// ZOOM 전환(프레젠트/디스미스) 시 사용되는 설정 값 모음
/// - 전환 시간, 스프링 파라미터
/// - 팬 제스처(양축) 기반 알파/디스미스 임계값 계산
struct CustomZoomTransitionConfiguration {
    // MARK: - 애니메이션 기본 파라미터
    /// 전환(프레젠트/디스미스)에 걸리는 총 시간
    let transitionDuration: TimeInterval
    /// 스프링 애니메이션의 감쇠 비율 --&gt; (작을수록 더 출렁임)
    let springWithDamping: CGFloat
    /// 스프링 애니메이션의 초기 속도
    let initialSpringVelocity: CGFloat

    // MARK: - 팬 제스처 관련 파라미터
    /// 이 거리(픽셀) 이상 이동하면 디스미스 처리
    let panGestureDismissThreshold: CGFloat
    /// 팬 제스처 중 최소 알파 값
    let minAlphaDuringPan: CGFloat
    /// 이 거리까지 선형으로 알파가 감소 (0 → minAlphaDuringPan)
    let alphaRangeDistance: CGFloat

    init(
        transitionDuration: TimeInterval = 0.5,
        springWithDamping: CGFloat = 0.85,
        initialSpringVelocity: CGFloat = 0.8,
        panGestureDismissThreshold: CGFloat = 120,
        minAlphaDuringPan: CGFloat = 0.6,
        alphaRangeDistance: CGFloat = 160
    ) {
        self.transitionDuration = transitionDuration
        self.springWithDamping = springWithDamping
        self.initialSpringVelocity = initialSpringVelocity
        self.panGestureDismissThreshold = panGestureDismissThreshold
        self.minAlphaDuringPan = minAlphaDuringPan
        self.alphaRangeDistance = alphaRangeDistance
    }
}</code></pre>
<h2 id="custompresentingtransitionanimator">CustomPresentingTransitionAnimator</h2>
<blockquote>
<p>자 이번엔 뷰가 등장할때에 대한 애니메이션을 정의 보고자 합니다.</p>
</blockquote>
<pre><code class="language-swift">final class CustomPresentingTransitionAnimator: NSObject {
    private let config: CustomZoomTransitionConfiguration
    private let referenceView: UIView
    private var transitionContext: UIViewControllerContextTransitioning?

    // MARK: - 초기화
    init(config: CustomZoomTransitionConfiguration, referenceView: UIView) {
        self.config = config
        self.referenceView = referenceView
    }
}</code></pre>
<pre><code class="language-swift">extension CustomPresentingTransitionAnimator: UIViewControllerAnimatedTransitioning {

    /// 프레젠트 전환에 소요되는 총 시간을 반환
    func transitionDuration(
        using transitionContext: UIViewControllerContextTransitioning?
    ) -&gt; TimeInterval {
        print(&quot;\(#function)&quot;)
        return config.transitionDuration
    }

    /// 버튼 위치/ 크기에서 시작해 화면 전체로 확대되는 프레젠트 애니메이션.
    func animateTransition(
        using transitionContext: UIViewControllerContextTransitioning
    ) {
        print(&quot;\(#function)&quot;)
        guard let presentedViewController = transitionContext.viewController(forKey: .to) else {
            return
        }

        self.transitionContext = transitionContext

        let containerView = transitionContext.containerView

        // 배경 디밍 뷰를 준비하고 컨테이너에 추가
        let backgroundView = makeBackgroundView(in: containerView)

        // 최종 프레임과 버튼(referenceView)의 시작 프레임을 계산
        let finalFrame = containerView.bounds
        let refFrameInContainer = containerView.convert(referenceView.bounds, from: referenceView)

        // 프레젠트될 뷰를 초기 상태(작게/버튼 위치)로 설정
        let presentedView = presentedViewController.view!
        preparePresentedView(presentedView,
                             finalFrame: finalFrame,
                             refFrameInContainer: refFrameInContainer)

        // 컨테이너에 프레젠트 뷰를 추가합니다(디밍 뷰 위).
        containerView.addSubview(presentedView)

        // 확대 애니메이션
        animatePresentation(backgroundView: backgroundView,
                            presentedView: presentedView,
                            finalFrame: finalFrame,
                            transitionContext: transitionContext)
    }

    /// 프레젠트 애니메이션 종료 시 호출됩니다.
    func animationEnded(_ transitionCompleted: Bool) {
        print(&quot;\(#function)&quot;)
        transitionContext = nil
    }
}

private extension CustomPresentingTransitionAnimator {

    /// 컨테이너에 디밍(배경) 뷰를 생성/추가하고 반환
    func makeBackgroundView(in containerView: UIView) -&gt; UIView {
        let backgroundView = UIView(frame: containerView.bounds)
        backgroundView.backgroundColor = containerView.backgroundColor
        backgroundView.alpha = 0
        containerView.addSubview(backgroundView)
        return backgroundView
    }

    /// 프레젠트될 뷰를 버튼 위치/크기에서 시작하도록 초기 상태를 설정
    /// - 최종 프레임, 버튼 프레임을 기반으로 스케일/센터/코너 라운드를 구성
    func preparePresentedView(_ presentedView: UIView,
                              finalFrame: CGRect,
                              refFrameInContainer: CGRect) {
        // 최종 프레임을 기준으로 레이아웃 설정
        presentedView.frame = finalFrame

        // 확대/축소 중 코너 라운드가 잘 보이도록 마스킹 활성화
        presentedView.layer.masksToBounds = true
        let referenceCorner = referenceView.layer.cornerRadius

        // 버튼 대비 전체 화면의 스케일 비율 계산
        let minScale: CGFloat = 0.001 // 0 스케일 방지
        let scaleX = max(refFrameInContainer.width / max(finalFrame.width, 1), minScale)
        let scaleY = max(refFrameInContainer.height / max(finalFrame.height, 1), minScale)

        // 스케일 상태에서 버튼 코너와 유사하게 보이도록 초기 코너 보정
        let effectiveScale = max(min(scaleX, scaleY), 0.001)
        let initialCorner = referenceCorner / effectiveScale
        presentedView.layer.cornerRadius = initialCorner

        // 버튼 중심 위치에서 작게 시작하도록 설정
        presentedView.transform = CGAffineTransform(scaleX: scaleX, y: scaleY)
        presentedView.center = CGPoint(x: refFrameInContainer.midX, y: refFrameInContainer.midY)
    }

    /// 디밍 알파/스케일/센터/코너 라운드를 애니메이션하여 화면 전체로 확대
    func animatePresentation(backgroundView: UIView,
                             presentedView: UIView,
                             finalFrame: CGRect,
                             transitionContext: UIViewControllerContextTransitioning) {
        UIView.animate(
            withDuration: config.transitionDuration,
            delay: 0,
            usingSpringWithDamping: config.springWithDamping,
            initialSpringVelocity: config.initialSpringVelocity,
            options: [.beginFromCurrentState]
        ) {
            backgroundView.alpha = 1
            presentedView.transform = .identity
            presentedView.center = CGPoint(x: finalFrame.midX, y: finalFrame.midY)
            presentedView.layer.cornerRadius = 0
        } completion: { _ in
            backgroundView.removeFromSuperview()
            transitionContext.completeTransition(transitionContext.transitionWasCancelled == false)
        }
    }
}</code></pre>
<h3 id="전환될-뷰컨을-반환받기">전환될 뷰컨을 반환받기</h3>
<pre><code class="language-swift">guard let presentedViewController = transitionContext.viewController(forKey: .to) else {
      return
 }</code></pre>
<p><img src="https://velog.velcdn.com/images/little_tail/post/47a058ec-217f-4ae8-b945-237b861b26ea/image.gif" alt=""></p>
<h3 id="1편을-마치며">1편을 마치며</h3>
<blockquote>
<p>기본적인 개념과, 등장 애니메이션을 만들어 보았습니다.
다음 편은 사라질때에 대한 애니메이션을 구성해보겠습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[UITest]]></title>
            <link>https://velog.io/@little_tail/UITest</link>
            <guid>https://velog.io/@little_tail/UITest</guid>
            <pubDate>Mon, 27 Oct 2025 16:06:48 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가기에-앞서">들어가기에 앞서</h2>
<blockquote>
<p>이전편 Unit Test를 보신후 참고하시면 좋습니다.</p>
</blockquote>
<h1 id="uitest">UITest</h1>
<blockquote>
<p>말 그래도 UI가 정상 동작하는가 즉
예측한 대로 동작하는가에 관한 자동화 테스트를 말함</p>
</blockquote>
<h2 id="syntax">Syntax</h2>
<pre><code class="language-swift">.accessibilityIdentifier(&lt;#T##identifier: String##String#&gt;)
   --- or ---
   accessibilityIdentifier = &quot;Identifier&quot;</code></pre>
<blockquote>
<p>위 같은 식별자를 통해 UI가 정상 동작하는지 테스트 하는 원리</p>
</blockquote>
<h2 id="xctassert">XCTAssert</h2>
<pre><code class="language-swift">
let app = XCUIApplication()

// okay button이 있는 경우 성공
XCTAssert(app.buttons[&quot;okay&quot;].exists)

// &quot;Hello World&quot; 라는 문자열이 있는 경우 성공
XCTAssert(app.staticTexts[&quot;Hello World&quot;].exists)

// 2초 내로 &quot;TEST2&quot; 라는 문자열이 보여지는 경우 성공
XCTAssert(app.staticTexts[&quot;TEST2&quot;].waitForExistence(timeout: 2))

// image1 이라는 이미지가 있는 경우 성공
XCTAssert(app.images[&quot;image1&quot;].exists, &quot;There is no image1 image&quot;)

// 실패
XCTFail()</code></pre>
<h2 id="action">Action</h2>
<pre><code class="language-swift">let app = XCUIApplication()
app.buttons[&quot;button&quot;].tap()
app.buttons[&quot;doubleTapButton&quot;].doubleTap()</code></pre>
<blockquote>
<p>식별자를 통해 버튼을 찾고 액션을 준다.</p>
</blockquote>
<pre><code class="language-swift">let pointX = app.frame.width/2
let pointY = app.frame.height/2

let pointLocation = CGVector(dx: pointX, dy: pointY)
let normalizedLocation = app.coordinate(withNormalizedOffset: CGVector(dx:0, dy: 0))

let tapLocation = normalizedLocation.withOffset(pointLocation)
tapLocation.tap()</code></pre>
<blockquote>
<p>좌표를 통해 특정 지점을 탭 해도록 한다.</p>
</blockquote>
<h2 id="예시">예시</h2>
<pre><code class="language-swift">import XCTest
@testable import Testable

@MainActor
final class TestableUITests: XCTestCase {

    override func setUpWithError() throws {
        // Put setup code here. This method is called before the invocation of each test method in the class.

        // In UI tests it is usually best to stop immediately when a failure occurs.
        continueAfterFailure = false

        // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
    }

    override func tearDownWithError() throws {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
    }

    func testTappedStartButton() {
        let app = XCUIApplication()
        let id = TestIdentifiers.contentViewStartButton.identifier
        let startButton = app.buttons[id]

        guard startButton.exists else {
            XCTFail(&quot;Lost Start Button&quot;)
            return
        }

        startButton.tap()
    }

    func testStartButtonExistsAndTap() {
        let app = XCUIApplication()
        app.launch()
        let startButton = app.buttons[TestIdentifiers.contentViewStartButton.identifier]
        XCTAssertTrue(startButton.waitForExistence(timeout: 3), &quot;Start button should exist&quot;)
        startButton.tap()
    }

    func testSumTextExists() {
        let app = XCUIApplication()
        app.launch()
        let sumText = app.staticTexts[TestIdentifiers.contentViewSumText.identifier]
        XCTAssertTrue(sumText.waitForExistence(timeout: 3), &quot;Sum label should exist&quot;)
    }

    func testStartThenSumChanges() {
        let app = XCUIApplication()
        app.launch()
        let startButton = app.buttons[TestIdentifiers.contentViewStartButton.identifier]
        let sumText = app.staticTexts[TestIdentifiers.contentViewSumText.identifier]

        XCTAssertTrue(startButton.waitForExistence(timeout: 3), &quot;Start button should exist&quot;)
        XCTAssertTrue(sumText.waitForExistence(timeout: 3), &quot;Sum label should exist&quot;)

        let initialValue = sumText.label
        startButton.tap()

        let predicate = NSPredicate { _, _ in sumText.exists &amp;&amp; sumText.label != initialValue }
        let exp = expectation(for: predicate, evaluatedWith: nil, handler: nil)
        wait(for: [exp], timeout: 5.0)

        XCTAssertNotEqual(sumText.label, initialValue, &quot;Sum label should change after loading numbers&quot;)
    }
}
</code></pre>
<h1 id="추가설명">추가설명</h1>
<pre><code class="language-swift">let p = NSPredicate { _, _ in sumText.exists &amp;&amp; sumText.label != initialValue }</code></pre>
<blockquote>
<p>참 거짓 구분위한 객체 구성</p>
</blockquote>
<h2 id="expectation---xctestexpection">expectation -&gt; XCTestExpection</h2>
<blockquote>
<p>비동기 조건이 충족될 때가지 가디리는 방식
일정 시간 동안 재시도</p>
</blockquote>
<pre><code class="language-swift">let exp = expectation(for: predicate, evaluatedWith: nil, handler: nil)</code></pre>
<blockquote>
<p>Expection 정의</p>
</blockquote>
<pre><code class="language-swift">wait(for: [exp], timeout: 5.0)</code></pre>
<blockquote>
<p>5초까지 비동기(expectation) 종료를 대기</p>
</blockquote>
<h3 id="마무리-하며">마무리 하며</h3>
<blockquote>
<p>이번엔 UITest를 다루어 보았습니다. 
사실 이것 외에도 더 많은 메서드나 방식이 존재하는데
그런 부분은 찾아보면서 하는게 더 맞는 방법인 것 같아
여기까지 정리 해보도록 하겠습니다.
아직 Testable 에 대해서 다루지 않아서 다시 다루긴 할 겁니다.</p>
</blockquote>
<h3 id="예고">예고</h3>
<blockquote>
<p>다음 시간은 아마 <code>Transition Animation</code> 에 대해서 다루어 보도록 하겠습니다.
WWDC 2024 글을 참고할 예정이니 많은 관심 부탁 드립니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Unit Test]]></title>
            <link>https://velog.io/@little_tail/Unit-Test</link>
            <guid>https://velog.io/@little_tail/Unit-Test</guid>
            <pubDate>Mon, 20 Oct 2025 13:20:31 GMT</pubDate>
            <description><![CDATA[<h1 id="unit-test">Unit Test</h1>
<blockquote>
<p>단위 테스트
특정 유닛이 정상 동작하는지 체크하기 위한 테스트</p>
</blockquote>
<h2 id="first">FIRST</h2>
<h3 id="--f-ast">- <code>F</code> ast</h3>
<ul>
<li>여러 테스트를 빠르게 동작하도록<h3 id="--i-solated">- <code>I</code> solated</h3>
</li>
<li>독립적으로 진행되도록<h3 id="--r-epeatable">- <code>R</code> epeatable</h3>
</li>
<li>반복적으로 가능하게<h3 id="--s-elf-automation">- <code>S</code> elf-Automation</h3>
</li>
<li>자동화<h3 id="--t-imely">- <code>T</code> imely</h3>
</li>
<li>수정사항 발생시 수정사항에 대한 여파 확인</li>
</ul>
<hr>
<h2 id="given-when-then">Given, When, Then</h2>
<h3 id="--given">- Given</h3>
<blockquote>
<p>테스트 목적의 상황 설명 ex) 3 을 받고 난수 생성</p>
</blockquote>
<h3 id="--when">- When</h3>
<blockquote>
<p>테스트 발생 시점 ex) 버튼 클릭 이벤트</p>
</blockquote>
<h3 id="--then">- Then</h3>
<blockquote>
<p>예상결과, 결과 그 후이 처리</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/little_tail/post/4fac2d71-058a-4a96-a7e1-a4d5bd9748bd/image.png" alt=""></p>
<hr>
<h2 id="xctest-메서드들">XCTest 메서드들</h2>
<ul>
<li><p>XCTAssertTrue(<em>:) / XCTAssertFalse(</em>:)</p>
<pre><code class="language-swift">XCTAssertTrue(isEnabled)
XCTAssertFalse(items.isEmpty == false)</code></pre>
</li>
<li><p>XCTAssertEqual(<em>:</em>:) / XCTAssertNotEqual(<em>:</em>:)
(Float/Double은 accuracy: 지원)</p>
</li>
</ul>
<pre><code class="language-swift">XCTAssertEqual(user.name, &quot;Lee&quot;)
XCTAssertEqual(distance, 1.0, accuracy: 0.0001)</code></pre>
<ul>
<li><p>XCTAssertNil(<em>:) / XCTAssertNotNil(</em>:)</p>
<pre><code class="language-swift">XCTAssertNil(cache[&quot;missing&quot;])
XCTAssertNotNil(image)</code></pre>
</li>
<li><p>XCTAssertGreaterThan(<em>:</em>:) / XCTAssertLessThan(<em>:</em>:)
&gt; =, &lt;= 도 각각 제공</p>
<pre><code class="language-swift">XCTAssertGreaterThan(score, 80)
XCTAssertLessThan(latency, 100)</code></pre>
</li>
<li><p>XCTAssertIdentical(<em>:</em>:) / XCTAssertNotIdentical(<em>:</em>:)
(두 객체 참조가 같은 인스턴스인지)</p>
<pre><code class="language-swift">XCTAssertIdentical(a as AnyObject, b as AnyObject)</code></pre>
</li>
<li><p>XCTFail(_:)
(명시적으로 실패 처리)</p>
<pre><code class="language-swift">if unsupported { XCTFail(&quot;미지원 경로&quot;) }</code></pre>
</li>
<li><p>XCTUnwrap(_:)
(옵셔널이 nil이면 즉시 실패)</p>
<pre><code class="language-swift">let data = try XCTUnwrap(response.data)</code></pre>
</li>
</ul>
<hr>
<h2 id="테스트-해보기">테스트 해보기</h2>
<h3 id="numberhelper-비즈니스-로직">NumberHelper (비즈니스 로직)</h3>
<pre><code class="language-swift">import Foundation

protocol NumberService: Sendable {
    func fetchNumbers() async throws -&gt; [Int]
    func sumEvenNumbers(_ numbers: [Int]) -&gt; Int
}

extension NumberService {
    func sumEvenNumbers(_ numbers: [Int]) -&gt; Int {
        numbers.filter { $0 % 2 == 0 }.reduce(0, +)
    }
}

enum NumberServiceError: Error, LocalizedError {
    case network
    case malformed

    var errorDescription: String? {
        switch self {
        case .network: return &quot;네트워크 오류가 발생했습니다.&quot;
        case .malformed: return &quot;데이터 형식 오류가 발생했습니다.&quot;
        }
    }
}

final class TestableHelper: NumberService {

    // 임의의 딜레이
    private let delay: UInt64 = 700_000_000

    func fetchNumbers() async throws -&gt; [Int] {
        try await Task.sleep(nanoseconds: delay)

        return (1...10).map{ _ in Int.random(in: 1...30) }
    }
}</code></pre>
<h2 id="xctest-타겟-생성-및-연결-코드">XCTest 타겟 생성 및 연결, 코드</h2>
<blockquote>
<p>테스트 하고자할 프로젝트나 모듈에 <code>AddTarget</code> 을 진행후
아래와 같이 테스트 해보겠습니다.</p>
</blockquote>
<pre><code class="language-swift">import XCTest
@testable import Testable // 현재 프로젝트 ( 만약 Tuist 같은 멀티모듈 환경시 해당 모듈 ).

@MainActor
final class TestableTests: XCTestCase {
    // Test 대상
    private var service: NumberService!

    // 테스트에 필요한 설정값
    override func setUpWithError() throws {
        // Put setup code here. This method is called before the invocation of each test method in the class.
        try super.setUpWithError()
        self.service = TestableHelper()
    }

    // Test 마치며 정리할 것들 정리 -&gt; 격리성 보장
    override func tearDownWithError() throws {
        try super.tearDownWithError()
        service = nil
    }


    /// func fetchNumbers() async throws -&gt; [Int] Test Code
    func testFetchNumber() async throws {
        // given : 필요한 값 세팅
        // -&gt; 현재는 없음

        // when : 테스트 코드 실행
        let number = try await service.fetchNumbers()

        // then : 결과 비교
        XCTAssertEqual(number.count, 10, &quot;The number Count should be 10&quot;)
    }

    // sumEvenNumbers(_ numbers: [Int]) -&gt; Int TestCode
    func testSumEvenNumbers() {
        // given None

        // when
        let result = service.sumEvenNumbers([1,2,3,4,5,5,6,7,8,9,10,20])

        // then
        XCTAssertEqual(result, 50, &quot;The sum of even numbers should be 50&quot;)
    }

}</code></pre>
<h2 id="비동기-테스트">비동기 테스트</h2>
<h3 id="expectation-fulfill-wait">expectation(), fulfill(), wait()</h3>
<ul>
<li><p>충족되거나 시간 초과 까지, 둘 중 먼저 발생하는 시점까지 계속 진행</p>
<pre><code class="language-swift">func testFetchCallBack() throws {
      let manager = FakeNetworkManager(mode: .success([1,2,3,4,5,6]), delay: 100_000) // much smaller
      let exp = expectation(description: &quot;fetch Done&quot;) // 뭘 충족할지

      manager.fetchNumbers { result in
          let nums = try? result.get()
          XCTAssertEqual(nums, [1,2,3,4,5,6])
          exp.fulfill() // 충족함 
      }

      wait(for: [exp], timeout: 2.0)
  }</code></pre>
</li>
</ul>
<h3 id="마무리-하며">마무리 하며</h3>
<blockquote>
<p>오랜만에 다시 글을 올립니다.
개인사정으로 인해... 몇일간 맥북을 못 만지는 상황이 발생해서..!
다음시간에는 UITest 로 찾아 뵙겠습니다. 감사합니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[RIBs - 뷰가 없는 RIB (3)]]></title>
            <link>https://velog.io/@little_tail/RIBs-%EB%B7%B0%EA%B0%80-%EC%97%86%EB%8A%94-RIB-3</link>
            <guid>https://velog.io/@little_tail/RIBs-%EB%B7%B0%EA%B0%80-%EC%97%86%EB%8A%94-RIB-3</guid>
            <pubDate>Wed, 01 Oct 2025 11:40:00 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가기에-앞서">들어가기에 앞서</h3>
<blockquote>
<p>이번시간에는 저번시간에 이어서 프로젝트를 이어가 보곘습니다.
저번시간에 <code>로그인</code> 기능을 가지고 뷰를 가진 RIB 을 구성하였습니다.
이번에는 <code>HomeRIB</code> 과 <code>RandomRIB</code> 이 둘을 중계하는 <code>Flow RIB</code>을 구성 해보겠습니다. </p>
</blockquote>
<h1 id="home-rib">Home RIB</h1>
<blockquote>
<p>빠른 진행을 위해 UI는 간단하게 진행합니다.</p>
</blockquote>
<h2 id="homeviewcontroller">HomeViewController</h2>
<pre><code class="language-swift">final class HomeViewController: UIViewController, HomePresentable, HomeViewControllable {

    weak var listener: HomePresentableListener?
    /// Root -&gt; Login -&gt; Name Label
    private let nameLabel = UILabel().after {
        $0.font = .systemFont(ofSize: 20, weight: .bold)
        $0.textColor = .black
        $0.translatesAutoresizingMaskIntoConstraints = false
    }

    private let backLoginButton = UIButton().after {
        $0.setTitle(&quot;Go Back&quot;, for: .normal)
        $0.setTitleColor(.red, for: .normal)
        $0.translatesAutoresizingMaskIntoConstraints = false
    }

    private let randomColorMoveButton = UIButton().after {
        $0.setTitle(&quot;NEXT&quot;, for: .normal)
        $0.setTitleColor(.black, for: .normal)
        $0.translatesAutoresizingMaskIntoConstraints = false
    }

    override func loadView() {
        super.loadView()
        self.view.backgroundColor = .blue
        setViewHierarchy()
        setUI()
        subscribe()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        nameLabel.text = listener?.getName()
    }

    func pushViewController(_ viewController: ViewControllable) {
        self.navigationController?.pushViewController(viewController.uiviewController, animated: true)
    }

    func popViewController(animated: Bool) {
        self.navigationController?.popViewController(animated: animated)
    }
}

extension HomeViewController {

    private func setViewHierarchy() {
        view.addSubview(nameLabel)
        view.addSubview(backLoginButton)
        view.addSubview(randomColorMoveButton)
    }

    private func setUI() {
        NSLayoutConstraint.activate([
            nameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            nameLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),

            backLoginButton.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 10),
            backLoginButton.widthAnchor.constraint(equalToConstant: 120),
            backLoginButton.heightAnchor.constraint(equalToConstant: 40),

            randomColorMoveButton.topAnchor.constraint(equalTo: backLoginButton.bottomAnchor, constant: 10),
            randomColorMoveButton.centerXAnchor.constraint(equalTo: backLoginButton.centerXAnchor)
        ])
    }
}

extension HomeViewController {

    private func subscribe() {
        backLoginButton.rx.tap
            .bind(with: self) { owner, _ in
                owner.listener?.goBackToLogin()
            }
            .disposed(by: rx.disposeBag)

        randomColorMoveButton.rx.tap
            .bind(with: self) { owner, _ in
                owner.listener?.moveToRandomView()
            }
            .disposed(by: rx.disposeBag)
    }
}
</code></pre>
<h3 id="presentablelistener">PresentableListener</h3>
<pre><code class="language-swift">protocol HomePresentableListener: AnyObject {
    func getName() -&gt; String

    func goBackToLogin()

    func moveToRandomView()
}</code></pre>
<h2 id="home-router">Home Router</h2>
<pre><code class="language-swift">protocol HomeInteractable: Interactable, RandomListener {
    var router: HomeRouting? { get set }
    var listener: HomeListener? { get set }
}

protocol HomeViewControllable: ViewControllable {
    // TODO: Declare methods the router invokes to manipulate the view hierarchy.
    func pushViewController(_ viewController: ViewControllable)

    func popViewController(animated: Bool)
}

final class HomeRouter: ViewableRouter&lt;HomeInteractable, HomeViewControllable&gt;, HomeRouting {

    private let randomColorBuilder: RandomBuilder
    private var randomRouting: RandomRouting?

    // TODO: Constructor inject child builder protocols to allow building children.
    init(interactor: HomeInteractable,
         viewController: HomeViewControllable,
         randomColorBuilder: RandomBuilder
    ) {
        self.randomColorBuilder = randomColorBuilder
        super.init(interactor: interactor, viewController: viewController)
        interactor.router = self
    }

    func routeToRandomColorView() {
        let child = randomColorBuilder.build(withListener: interactor)
        attachChild(child)
        self.randomRouting = child
        viewController.pushViewController(child.viewControllable)
    }

    func backToHome() {
        guard let child = randomRouting else { return }
        viewController.popViewController(animated: true)
        detachChild(child)
        randomRouting = nil
    }
}
</code></pre>
<h3 id="home-interactor">Home Interactor</h3>
<pre><code class="language-swift">protocol HomeRouting: ViewableRouting {
    // TODO: Declare methods the interactor can invoke to manage sub-tree via the router.
    func routeToRandomColorView()

    func backToHome()
}

protocol HomePresentable: Presentable {
    var listener: HomePresentableListener? { get set }
    // TODO: Declare methods the interactor can invoke the presenter to present data.
}

protocol HomeListener: AnyObject {
    // TODO: Declare methods the interactor can invoke to communicate with other RIBs.

    func didRequestLogin()
}

final class HomeInteractor: PresentableInteractor&lt;HomePresentable&gt;, HomeInteractable, HomePresentableListener {

    weak var router: HomeRouting?
    weak var listener: HomeListener?
    private let name: String

    // TODO: Add additional dependencies to constructor. Do not perform any logic
    // in constructor.
    init(presenter: HomePresentable, name: String) {
        self.name = name
        super.init(presenter: presenter)
        presenter.listener = self
    }

    override func didBecomeActive() {
        super.didBecomeActive()
        // TODO: Implement business logic here.
    }

    override func willResignActive() {
        super.willResignActive()
        // TODO: Pause any business logic.
    }

    func getName() -&gt; String {
        return self.name
    }

    func goBackToLogin() {
        listener?.didRequestLogin()
    }

    func moveToRandomView() {
        router?.routeToRandomColorView()
    }

    func randomBack() {
        router?.backToHome()
    }
}
</code></pre>
<h2 id="homebuilder">HomeBuilder</h2>
<pre><code class="language-swift">protocol HomeDependency: Dependency {
    // TODO: Declare the set of dependencies required by this RIB, but cannot be
    // created by this RIB.
}

final class HomeComponent: Component&lt;HomeDependency&gt; {

    // TODO: Declare &#39;fileprivate&#39; dependencies that are only used by this RIB.

    let name: String

    init(dependency: HomeDependency, name: String) {
        self.name = name
        super.init(dependency: dependency)
    }
}

extension HomeComponent: RandomDependency {

}

// MARK: - Builder

protocol HomeBuildable: Buildable {
    func build(withListener listener: HomeListener, name: String) -&gt; HomeRouting
}

final class HomeBuilder: Builder&lt;HomeDependency&gt;, HomeBuildable {

    override init(dependency: HomeDependency) {
        super.init(dependency: dependency)
    }

    func build(withListener listener: HomeListener, name: String) -&gt; HomeRouting {
        let component = HomeComponent(dependency: dependency, name: name)
        let viewController = HomeViewController()
        let interactor = HomeInteractor(presenter: viewController, name: name)

        let randomBuilder = RandomBuilder(dependency: component)

        interactor.listener = listener
        return HomeRouter(interactor: interactor, viewController: viewController, randomColorBuilder: randomBuilder)
    }
}</code></pre>
<blockquote>
<p>자 저번 시간에 각각의 역활을 다루면서 하였기에 설명은 생략하겠습니다.
중요한 핵심은 뷰 이동을 수행하는 역활은 이친구가 하긴 합니다.
그 판단은 여기서 하지 않습니다. </p>
</blockquote>
<h1 id="random-rib">Random RIB</h1>
<blockquote>
<p>Rib 구성은 길어짐으로 생략하고 UI 만 작성해 놓겠습니다.
한번 직접 구성해 보시는 것을 추천합니다.</p>
</blockquote>
<pre><code class="language-swift">import UIKit
import RxSwift
import RxCocoa

final class RandomView: UIView {

    private let colorChangeButton = UIButton().after { b in
        b.setTitle(&quot;색상 바꾸기&quot;, for: .normal)
        b.setTitleColor(.cyan, for: .normal)
        b.backgroundColor = .lightGray
        b.titleLabel?.font = .boldSystemFont(ofSize: 24)
        b.translatesAutoresizingMaskIntoConstraints = false
    }

    private let backButton = UIButton().after {
        $0.setTitle(&quot;BACK&quot;, for: .normal)
        $0.backgroundColor = .lightGray
        $0.setTitleColor(.red, for: .normal)
        $0.translatesAutoresizingMaskIntoConstraints = false
    }

    // MARK: Internal
    enum RandomViewEvent {
        case colorChange
        case backButtonTapped
    }

    let viewEventStream = PublishRelay&lt;RandomViewEvent&gt; ()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setUI()
        subscribeUIEvent()
    }

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

// MARK: UI
extension RandomView {

    private func setUI() {
        self.backgroundColor = .random
        self.addSubview(backButton)
        self.addSubview(colorChangeButton)

        NSLayoutConstraint.activate([
            colorChangeButton.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -40),
            colorChangeButton.centerXAnchor.constraint(equalTo: centerXAnchor),
            colorChangeButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 44),

            backButton.topAnchor.constraint(equalTo: colorChangeButton.bottomAnchor, constant: 20),
            backButton.centerXAnchor.constraint(equalTo: centerXAnchor),
        ])
    }
}

// MARK: UI Event
extension RandomView {

    private func subscribeUIEvent() {
        colorChangeButton.addAction(UIAction(handler: { [weak self] _ in
            guard let owner = self else { return }
            owner.viewEventStream.accept(.colorChange)
        }), for: .touchDown)

        backButton.rx.tap
            .bind(with: self) { owner, _ in
                owner.viewEventStream.accept(.backButtonTapped)
            }
            .disposed(by: rx.disposeBag)
    }
}</code></pre>
<h1 id="flow-rib-핵심">Flow RIB (핵심)</h1>
<blockquote>
<p>지금은 뷰 구조가 단순하니까 Flow RIB 형태가 필요 하지 않습니다.
다만 뷰가 많다고 가정해 보겠습니다.
뷰 이동 관련해서 저희는 복잡한 뷰이동은 다른 파일에서 관리 했었을 겁니다.</p>
</blockquote>
<blockquote>
<p>코디네이터 패턴을 쓰거나, 라우터 패턴을 쓰거나 해서 관리를 하였는데
이것도 비슷한 방법이다 라고 생각하시면서
핵심은 뷰를 가지고 있는 않는 RIB을 통해 관리할 예정이니 집중 바랍니다.</p>
</blockquote>
<h2 id="home-flow-viewcontroller---x">Home Flow ViewController? -&gt; X</h2>
<blockquote>
<p>없습니다.!!</p>
</blockquote>
<h2 id="home-flow-interactor">Home Flow Interactor</h2>
<blockquote>
<p>뷰가 없는데 뷰모델이 존재합니다 만 거의 안씁니다.
하지만 각 하위의 리스너를 감지하게 해야 합니다.</p>
</blockquote>
<pre><code class="language-swift">final class HomeFlowInteractor: Interactor, HomeFlowInteractable {

    weak var router: HomeFlowRouting?
    weak var listener: HomeFlowListener?

    // TODO: Add additional dependencies to constructor. Do not perform any logic
    // in constructor.
    override init() {}

    override func didBecomeActive() {
        super.didBecomeActive()
        // TODO: Implement business logic here.
    }

    override func willResignActive() {
        super.willResignActive()
    }
}

extension HomeFlowInteractor: HomeListener {
    func didRequestLogin() {
        listener?.moveToLogin()
    }
}

extension HomeFlowInteractor: RandomListener {
    func randomBack() {
        router?.routeToRandomColor()
    }
}

protocol HomeFlowRouting: Routing {
    func start(name: String)
    func routeToRandomColor()
    func routeBackToHome()
}

protocol HomeFlowListener: AnyObject {
    // TODO: Declare methods the interactor can invoke to communicate with other RIBs.

    func moveToLogin()
}</code></pre>
<h2 id="home-flow-router">Home Flow Router</h2>
<blockquote>
<p>RIB이 직접 뷰컨트롤러를 가지진 않지만 주입을 받을 겁니다.</p>
</blockquote>
<pre><code class="language-swift">protocol HomeFlowInteractable: Interactable, HomeListener, RandomListener {
    var router: HomeFlowRouting? { get set }
    var listener: HomeFlowListener? { get set }
}

final class HomeFlowRouter: Router&lt;HomeFlowInteractable&gt;, HomeFlowRouting {

    // MARK: - Private
    private let rootVc: RootViewControllable
    private let homeBuilder: HomeBuildable
    private let randomColorBuilder: RandomBuildable

    private var homeRouting: HomeRouting?
    private var randomColorRouting: RandomRouting?

    init(interactor: HomeFlowInteractable, rootVc: RootViewControllable, homeBuilder: HomeBuildable, randomColorBuilder: RandomBuildable, homeRouting: HomeRouting? = nil, randomColorRouting: RandomRouting? = nil) {
        self.rootVc = rootVc
        self.homeBuilder = homeBuilder
        self.randomColorBuilder = randomColorBuilder
        self.homeRouting = homeRouting
        self.randomColorRouting = randomColorRouting
        super.init(interactor: interactor)
        interactor.router = self
    }

    func start(name: String) {
        let home = homeBuilder.build(withListener: interactor, name: name)
        attachChild(home)

        homeRouting = home
        rootVc.setRoot(home.viewControllable, animated: false)
    }

    func routeToRandomColor() {
        guard homeRouting != nil else { return }
        let random = randomColorBuilder.build(withListener: interactor)
        attachChild(random)
        randomColorRouting = random
        rootVc.push(random.viewControllable, animated: true)
    }

    func routeBackToHome() {
        guard let random = self.randomColorRouting else { return }
        rootVc.pop(animated: true)
        detachChild(random)
        randomColorRouting = nil
    }

}</code></pre>
<h2 id="home-flow-builder">Home Flow Builder</h2>
<pre><code class="language-swift">protocol HomeFlowDependency: Dependency {
    var rootVC: RootViewControllable { get }
    var homeBuilder: HomeBuildable { get }
    var randomBuilder: RandomBuildable { get }
}

final class HomeFlowComponent: Component&lt;HomeFlowDependency&gt; {

}

// MARK: - Builder
protocol HomeFlowBuildable: Buildable {
    func build(withListener listener: HomeFlowListener) -&gt; HomeFlowRouting
}

final class HomeFlowBuilder: Builder&lt;HomeFlowDependency&gt;, HomeFlowBuildable {

    override init(dependency: HomeFlowDependency) {
        super.init(dependency: dependency)
    }

    func build(withListener listener: HomeFlowListener) -&gt; HomeFlowRouting {
        let component = HomeFlowComponent(dependency: dependency)
        let interactor = HomeFlowInteractor()
        interactor.listener = listener

//        return HomeFlowRouter(interactor: interactor, viewController: component.HomeFlowViewController)
        return HomeFlowRouter(interactor: interactor, rootVc: dependency.rootVC, homeBuilder: dependency.homeBuilder, randomColorBuilder: dependency.randomBuilder)
    }
}</code></pre>
<blockquote>
<p>다음과 같이 의존성을 두어서 진행하겠습니다.</p>
</blockquote>
<h3 id="root-builder">Root Builder</h3>
<pre><code class="language-swift">final class RootBuilder: Builder&lt;RootDependency&gt;, RootBuildable {

    override init(dependency: RootDependency) {
        super.init(dependency: dependency)
    }

    func build() -&gt; LaunchRouting {
        let viewController = RootViewController()
        // 이부분에서 
        let component = RootComponent(dependency: dependency, rootVc: viewController)

        let interactor = RootInteractor(presenter: viewController)

        let loginBuilder    = LoginBuilder(dependency: component)
        let homeFlowBuilder = HomeFlowBuilder(dependency: component)

        return RootRouter(
            interactor: interactor,
            viewController: viewController,
            loginBuilder: loginBuilder,
            homeFlowBuilder: homeFlowBuilder
        )
    }
}</code></pre>
<blockquote>
<p>RootViewController 에서 push, pop만 구성해놓으면 (프로토콜에 의해서)
구조가 잡히게 됩니다.</p>
</blockquote>
<h3 id="마무리-하면서">마무리 하면서</h3>
<blockquote>
<p>RIBs 편이 마무리가 되었습니다.
글이 조금 부실하게 느껴질거라 생각합니다.
코드로 설명할게 많다 보니 오히려 말로 풀어내기가 애매하다고 생각을 합니다.</p>
</blockquote>
<blockquote>
<p>제가 작성한 코드를 복붙 하지 마시고 하나하나  적으면서 생각하시면 좋지 않을까 생각이 듭니다.
사실 말로 설명은 1편에서 개념편에서 한번에 정리한게 끝입니다.
어떻게 활용하냐에서는 코드가 많은게 더 좋을 것 같아서 순서 정도만 잡으면서 작성한 것 같구요</p>
</blockquote>
<blockquote>
<p>RIBs 도 상당히 아이디어는 좋은 아키텍처라고 생각합니다.
하지만 난이도와 ASAP 한 환경을 생각해본다면 비추가 되는 아키텍처라고 생각을 합니다.</p>
</blockquote>
<blockquote>
<p>정말 고생많으셨고 다음시간은 
XCTest 를 다루어 볼까 합니다. ( 다룬줄 알았는데 아니더라구요 ) 
XCTest -&gt; TCA 와 Testable 형태로 작성할 수도 있고
Unity 여정 을 작성할 수도 있습니다....!</p>
</blockquote>
<blockquote>
<p>Unity도 공부해보면 좋을 것 같아서...!
댓글로 원하시는거 적으시면 반영 하겠습니다...!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[RIBs - 프로젝트 구성편 - 2,  간단 로그인 뷰 연결]]></title>
            <link>https://velog.io/@little_tail/RIBs-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%84%B1%ED%8E%B8-2-%EA%B0%84%EB%8B%A8-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%B7%B0-%EC%97%B0%EA%B2%B0</link>
            <guid>https://velog.io/@little_tail/RIBs-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%84%B1%ED%8E%B8-2-%EA%B0%84%EB%8B%A8-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%B7%B0-%EC%97%B0%EA%B2%B0</guid>
            <pubDate>Mon, 22 Sep 2025 12:05:41 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가기에-앞서">들어가기에 앞서</h3>
<blockquote>
<p>꼭 1편과 2편을 보고 글을 읽어주시면 좋겠습니다.</p>
</blockquote>
<h1 id="login-ribs-세팅">Login RIBs 세팅</h1>
<p><img src="https://velog.velcdn.com/images/little_tail/post/24becaac-7a59-44a9-8817-802d60dc0fed/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/little_tail/post/395d411a-b858-466b-b0ae-dcdca4a461d4/image.png" alt=""></p>
<blockquote>
<p>이전 시간에 배운 방법으로 똑같이 템플릿을 통해서
Login Ribs를 세팅해주시면 됩니다.
View가 존재하는 Rib이기 때문에 첫번째를 체크하고 진행 하셔야 합니다.</p>
</blockquote>
<h3 id="ui-세팅">UI 세팅</h3>
<blockquote>
<p>빠른 진행을 위해 간단하게 설계 했습니다.</p>
</blockquote>
<pre><code class="language-swift">import UIKit

final class LoginView: UIView {
    // 시작 버튼
    let button = UIButton().after { // after 는 Then 라이브러리 같이 설계되어 있습니다.
        var config = UIButton.Configuration.filled()
        config.title = &quot;로그인&quot;
        config.contentInsets = NSDirectionalEdgeInsets(top: 12, leading: 20, bottom: 12, trailing: 20)
        $0.configuration = config
        $0.translatesAutoresizingMaskIntoConstraints = false
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setUI()
    }

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

// MARK: UI Layer
extension LoginView {

    private func setUI() {
        self.addSubview(button)

        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: self.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: self.centerYAnchor),
        ])
    }
}</code></pre>
<h2 id="ui-event-연결해보기">UI Event 연결해보기</h2>
<blockquote>
<p>UI 를 구성했으니 위와같은 버튼을 <strong>Interactor</strong> 에게 알려주어야 하겠죠?
RIBs 내부에 RxSwift가 있어서 RxSwift 를 사용해 보겠습니다.</p>
</blockquote>
<h3 id="listener-setting">Listener Setting</h3>
<pre><code class="language-swift">protocol LoginPresentableListener: AnyObject {
    // TODO: Declare properties and methods that the view controller can invoke to perform
    // business logic, such as signIn(). This protocol is implemented by the corresponding
    // interactor class.

      // 로그인 버튼 클릭
    func touchToLogin(name: String)
}</code></pre>
<h3 id="viewcontroller-setting">ViewController Setting</h3>
<pre><code class="language-swift">final class LoginViewController: UIViewController, LoginPresentable, LoginViewControllable {

    private let baseView = LoginView()

    weak var listener: LoginPresentableListener?

    override func loadView() {
        super.loadView()
        self.view = baseView
        subscribe()
    }
}

extension LoginViewController {

    private func subscribe() {
        baseView.button.rx.tap
            .bind(with: self) { owner, _ in
                owner.listener?.touchToLogin(name: &quot;테스트를 위함&quot;)
            }
            .disposed(by: rx.disposeBag)
    }
}</code></pre>
<h2 id="interactor-setting">Interactor Setting</h2>
<blockquote>
<p>자 프로토콜을 통해 저희는 저런 이벤트를 구성하겠다고 약속을 한거죠?
그것을 구현해야 합니다.</p>
</blockquote>
<h3 id="interactor-listener-setting">Interactor Listener Setting</h3>
<blockquote>
<p>로그인이 완료 되면 상위에게 뷰를 옮겨달라 라는 메시지를 전달하고자 합니다.
일단은요. -&gt; 다음편에서는 구조가 바뀔 예정입니다.</p>
</blockquote>
<pre><code class="language-swift">protocol LoginListener: AnyObject {
    // TODO: Declare methods the interactor can invoke to communicate with other RIBs.

    func didLogin(name: String)
}

final class LoginInteractor: PresentableInteractor&lt;LoginPresentable&gt;, LoginInteractable, LoginPresentableListener {

    weak var router: LoginRouting?
    weak var listener: LoginListener?

    // TODO: Add additional dependencies to constructor. Do not perform any logic
    // in constructor.
    override init(presenter: LoginPresentable) {
        super.init(presenter: presenter)
        presenter.listener = self
    }

    override func didBecomeActive() {
        super.didBecomeActive()
        // TODO: Implement business logic here.
    }

    override func willResignActive() {
        super.willResignActive()
        // TODO: Pause any business logic.
    }

    // 로그인 버튼 클릭시
    func touchToLogin(name: String) {
        let name = name
        // 비즈니스 로직 거친후 (생략) -&gt; Root 에게 Home으로 가라고 명령
        listener?.didLogin(name: name)
    }
}</code></pre>
<h2 id="loginbuilder-setting">LoginBuilder Setting</h2>
<blockquote>
<p>자 이제 구성한 이벤트들이나, 의존성등을 구성하는 친구를 구현을 해야합니다.
그래야 Init() 을 해서 사용할 수 있겠죠?</p>
</blockquote>
<pre><code class="language-swift">protocol LoginBuildable: Buildable {
    func build(withListener listener: LoginListener) -&gt; LoginRouting
}

final class LoginBuilder: Builder&lt;LoginDependency&gt;, LoginBuildable {

    override init(dependency: LoginDependency) {
        super.init(dependency: dependency)
    }

    func build(withListener listener: LoginListener) -&gt; LoginRouting {
        let component = LoginComponent(dependency: dependency)
        let viewController = LoginViewController()
        let interactor = LoginInteractor(presenter: viewController)
        interactor.listener = listener
        return LoginRouter(interactor: interactor, viewController: viewController)
    }
}</code></pre>
<h2 id="root에서-login-이벤트-감지하기">Root에서 Login 이벤트 감지하기</h2>
<blockquote>
<p>자 위에서 말했듯이 상위에게 이벤트를 전달하고자
Listener를 세팅했죠?
상위가 바로 루트이기 때문에 다음과 같이 이벤트를 감지 하고자 합니다.</p>
</blockquote>
<pre><code class="language-swift">protocol RootInteractable: Interactable, LoginListener { // &lt;- 이부분
    var router: RootRouting? { get set }
    var listener: RootListener? { get set }
}</code></pre>
<pre><code class="language-swift">final class RootInteractor: PresentableInteractor&lt;RootPresentable&gt;, RootInteractable, RootPresentableListener {

    weak var router: RootRouting?
    weak var listener: RootListener?

    // TODO: Add additional dependencies to constructor. Do not perform any logic
    // in constructor.
    override init(presenter: RootPresentable) {
        super.init(presenter: presenter)
        presenter.listener = self
    }

   ...

    func didLogin(name: String) { // &lt;- 이부분
        router?.routToHome(name: name)
    }

}</code></pre>
<h3 id="root-route-setting">Root Route Setting</h3>
<pre><code class="language-swift">protocol RootRouting: ViewableRouting {
    // TODO: Declare methods the interactor can invoke to manage sub-tree via the router.
    func routToLogin()
}</code></pre>
<pre><code class="language-swift">final class RootRouter: LaunchRouter&lt;RootInteractable, RootViewControllable&gt;, RootRouting {

    private let loginBuilder: LoginBuildable
    private let homeBuilder: HomeBuildable

    private var currentChild: ViewableRouting?
    private var loginRouter: LoginRouting?
    private var homeRouter: HomeRouting?

    init(interactor: RootInteractable,
         viewController: RootViewControllable,
         loginBuilder: LoginBuildable,
         homeBuilder: HomeBuildable) {
        self.loginBuilder = loginBuilder
        self.homeBuilder = homeBuilder
        super.init(interactor: interactor, viewController: viewController)
        interactor.router = self
    }

    override func didLoad() {
        super.didLoad()

        routToLogin()
    }

    func routToLogin() {
        if let child = currentChild {
            detachChild(child)
        }

        let login = loginBuilder.build(withListener: interactor)

        attachChild(login)
        currentChild = login
        loginRouter = login

        viewController.setRoot(login.viewControllable, animated: true)
    }

    func routToHome(name: String) { // 다음 시간에 
        if let child = currentChild {
            detachChild(child)
        }

        let home = homeBuilder.build(withListener: interactor, name: name)

        attachChild(home)
        currentChild = home
        homeRouter = home

        viewController.setRoot(home.viewControllable, animated: true)
    }
}</code></pre>
<h3 id="root-viewcontroller-수정">Root ViewController 수정</h3>
<pre><code class="language-swift">final class RootViewController: UIViewController, RootPresentable, RootViewControllable {

    private let container = UIView()
    private weak var current: UINavigationController?

    weak var listener: RootPresentableListener?

    init() {
        super.init(nibName: nil, bundle: nil)
    }

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

    override func loadView() {
        super.loadView()
        view = container
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
    }

    func setRoot(_ vc: ViewControllable, animated: Bool) {
        let rootVC = vc.uiviewController
        let nav = UINavigationController()
        nav.setViewControllers([rootVC], animated: false) // 초기 세팅은 false 권장

        // 기존 제거
        if let current {
            current.willMove(toParent: nil)
            current.view.removeFromSuperview()
            current.removeFromParent()
        }

        addChild(nav)
        container.addSubview(nav.view)
        nav.view.frame = container.bounds
        nav.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        nav.didMove(toParent: self)
        current = nav

        // 페이드 인
        if animated {
            container.alpha = 0
            UIView.animate(withDuration: 0.2) { [weak self] in
                self?.container.alpha = 1
            }
        }
    }
}
</code></pre>
<h3 id="마치면서">마치면서</h3>
<blockquote>
<p>이번편은 자식 RIBs가 하나 생겨서 연결하는 작업을 했습니다.
사실 Home뷰도 만들어서 왔다갔다 하는거 까지를 이번편으로 하려 했는데
사실 Home같은 경우는 Home 안에서도 뷰가 많겠죠? 이동하는 뷰들이</p>
</blockquote>
<blockquote>
<p>그래서 HomeFlow 라고 해서 View가 없는 RIB을 생성해서 Home의 뷰들을 통제하는
중계소? 같은 것으로 구성을 또 하게 되서 루즈해지니
Login만 설명을 했구요 다음편은 HomeView 빠르게 뷰와 라우터 만 설명을 한후
FlowRIB 설명을 끝으로 RIBs 편을 마무리 할까 고민중입니다.</p>
</blockquote>
<blockquote>
<p>궁금하신 사항 있으시면 꼭 말씀 부탁드립니다.
감사합니다.</p>
</blockquote>
<h3 id="github">github</h3>
<p><a href="https://github.com/Little-tale/HowRIbs">Little-Tale/HowRibs</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RIBs - 프로젝트 구성편 - 1]]></title>
            <link>https://velog.io/@little_tail/RIBs-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%84%B1%ED%8E%B8-1</link>
            <guid>https://velog.io/@little_tail/RIBs-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%84%B1%ED%8E%B8-1</guid>
            <pubDate>Tue, 16 Sep 2025 08:15:36 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가기에-앞서">들어가기에 앞서</h3>
<blockquote>
<p>이전 개념편에 이어서
프로젝트 구성편으로 넘어 왔습니다.
이전편을 보지 않고 해당 편을 보아도 프로젝트는 구성이 가능합니다...만
개념을 알고 보시는게 도움이 되니 꼭 이전 개념편을 봐주시길 권장합니다.</p>
</blockquote>
<h2 id="템플릿-세팅">템플릿 세팅</h2>
<ol>
<li>빈 폴더 하나를 만들어 두세요</li>
<li>해당 폴더에 <a href="https://github.com/uber/ribs-ios">RIBs</a> 을 클론 해주세요</li>
<li>터미널을 열어주세요</li>
<li><strong>cd RIBs/ios/tooling</strong> 으로 이동합니다.</li>
<li><strong>/install-xcode-template.sh</strong> 을 통해 쉘을 실행 시켜주세요</li>
<li>아래 사진과 같이 RIB 템플릿이 설치 되었는지 확인해주세요
<img src="https://velog.velcdn.com/images/little_tail/post/2c574a2e-5c2f-4353-99d9-c319047d1cd0/image.png" alt=""></li>
</ol>
<h1 id="시작">시작</h1>
<h2 id="root-구성">Root 구성</h2>
<p><img src="https://velog.velcdn.com/images/little_tail/post/ab1db7c4-afe4-4322-8904-4b5d1fc1a514/image.png" alt=""></p>
<blockquote>
<p>위 사진처럼 Root를 구성해 보려고 합니다.
템플릿을통해 Root를 구성해 봅시다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/little_tail/post/6403333e-ae2c-47fd-a2f9-15d81a3a20e7/image.png" alt=""></p>
<blockquote>
<p>위 사진처럼 알아서 만들어 줄거에요
템플릿 세팅하실때 체크 박스중 View 여부에 따라 템플릿이 달라지는 체크박스가 있습니다.
View가 있음을 체크하고 만들어 주세요!</p>
</blockquote>
<h3 id="routing-변경">Routing 변경</h3>
<blockquote>
<p><strong>ViewableRouter</strong> 
아마 라우터가 해당 프로토콜을 채택하고 있을거에요
이는 루트 라우터에서는 사용하지 않을 것이기에
LaunchRouter로 변경해주세요!</p>
</blockquote>
<pre><code class="language-swift">// 바꾸신후 RootBuilder 에서도 변경작업이 필요합니다.

// MARK: - Builder
protocol RootBuildable: Buildable {
    func build() -&gt; LaunchRouting
}

final class RootBuilder: Builder&lt;RootDependency&gt;, RootBuildable {

    override init(dependency: RootDependency) {
        super.init(dependency: dependency)
    }

    func build() -&gt; LaunchRouting {
        let viewController = RootViewController()
        let component = RootComponent(dependency: dependency, rootVc: viewController)

        let interactor = RootInteractor(presenter: viewController)

        // MARK: 이부분은 무시하셔도 됩니다.
        // 후에 하게 되요... ㅎㅎ
        let loginBuilder    = LoginBuilder(dependency: component)
        let homeFlowBuilder = HomeFlowBuilder(dependency: component)

        return RootRouter(
            interactor: interactor,
            viewController: viewController,
            loginBuilder: loginBuilder,
            homeFlowBuilder: homeFlowBuilder
        )
    }
}</code></pre>
<h3 id="appcomponent">AppComponent</h3>
<pre><code class="language-swift">import RIBs

class AppComponent: Component&lt;EmptyComponent&gt;, RootDependency {

    init() {
        super.init(dependency: EmptyComponent())
    }
}</code></pre>
<h3 id="scenedelegate-세팅">SceneDelegate 세팅</h3>
<pre><code class="language-swift">class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?
    // 런치라우터를 변수선언
    private var launchRouter: LaunchRouting?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let scene = (scene as? UIWindowScene) else { return }
        let _window = UIWindow(windowScene: scene)
        self.window = _window

        // _window.makeKeyAndVisible() // 해당 함수를 실행하지 않아도 됩니다.

        let appComponent = AppComponent()
        let _launchRouter = RootBuilder(dependency: appComponent).build()
        self.launchRouter = _launchRouter
        _launchRouter.launch(from: _window)
    }


// MARK: RIBs LaunchRouter 내부 코드중

/// The application root router base class, that acts as the root of the router tree.
open class LaunchRouter&lt;InteractorType, ViewControllerType&gt;: ViewableRouter&lt;InteractorType, ViewControllerType&gt;, LaunchRouting {

    /// Initializer.
    ///
    /// - parameter interactor: The corresponding `Interactor` of this `Router`.
    /// - parameter viewController: The corresponding `ViewController` of this `Router`.
    public override init(interactor: InteractorType, viewController: ViewControllerType) {
        super.init(interactor: interactor, viewController: viewController)
    }

    /// Launches the router tree.
    ///
    /// - parameter window: The window to launch the router tree in.
    public final func launch(from window: UIWindow) {
        window.rootViewController = viewControllable.uiviewController
        window.makeKeyAndVisible()

        interactable.activate()
        load()
    }
}
</code></pre>
<blockquote>
<p>func launch(from window: UIWindow) 함수를 통해
내부에서 window.makeKeyAndVisible() 함수를 실행시키기 때문에
위와 같은 방식으로 SceneDelegate 를 구성합니다.</p>
</blockquote>
<blockquote>
<p>실행 시켜 보시면 넵 빈화면이 나오겠죠..?
이번시간에는 하지 않겠지만
로그인RIB 과 HomeRIB을 구성해서 로그인 화면으로 시작하여 Home으로 
그 반대로도 구성해 보겠습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/little_tail/post/a9e44fee-499b-407c-bbc2-18de4b0140fe/image.png" alt=""></p>
<h1 id="마무리-하며">마무리 하며</h1>
<blockquote>
<p>이번시간에는 Root를 구성하는 과정을 글로 옮겨 보았습니다.
다음 시간부터 좀더 머리가 아파지기 시작합니다.</p>
</blockquote>
<blockquote>
<p>Login을 먼저 구성을 하구요
Root와 통신을 해서 Home으로 이동시키고
Home에선 로그아웃 버튼을 만들어서 Login 화면으로 이동하게 구성해보겠습니다.
오늘도 고생 하셨습니다.</p>
</blockquote>
<h3 id="프로젝트-코드-미리보기">프로젝트 코드 미리보기</h3>
<p><a href="https://github.com/Little-tale/HowRIbs">git-hub - LittleTale_HowRibs</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RIBs - 개념편]]></title>
            <link>https://velog.io/@little_tail/RIBs-%EA%B0%9C%EB%85%90%ED%8E%B8</link>
            <guid>https://velog.io/@little_tail/RIBs-%EA%B0%9C%EB%85%90%ED%8E%B8</guid>
            <pubDate>Wed, 10 Sep 2025 14:20:46 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가기에-앞서">들어가기에 앞서</h3>
<blockquote>
<p>이번편을 작성하는데 있어서
좀 시간이 필요했습니다.... 
Pin + Flex Layout 편을 작성하고 있었는데
Ribs 라는 친구가 생각보다 재밌어 보이드라구용
그래서 중간에 갈아타는 바람에 걸린 것두 있고
사용법 보다는 개념을 좀더 집중해서
이해도를 높히고 들어가고 싶은 마음도 컷기에
좀더 확실하게
알아보고 시도해보고 적용해보면서 시간이 걸렸던 것 같습니다...
자 시작해보죠.</p>
</blockquote>
<h2 id="ribs가-왜-나온-것인가">RIBs가 왜 나온 것인가</h2>
<blockquote>
<p>MVC 를 유지보수 할때에 새로운 기능이 늘어날때마다 복잡성이 증가</p>
</blockquote>
<ul>
<li>모듈 증가할 수록 테스트 복잡</li>
<li>ViewController 평균 코드 줄이 3000줄</li>
</ul>
<h3 id="viper-아키텍처란-무엇인가">VIPER 아키텍처란 무엇인가</h3>
<ul>
<li>View              : UI</li>
<li>Interactor        : 비즈니스 로직 + Network + Data Layer 수행</li>
<li>Presenter         : UI의 상호작용 -&gt; Interactor의 Data를 요청후 View 반영 담당</li>
<li>Entity            : UI에 사용할 모델</li>
<li>Router            : 화면 전환 로직</li>
</ul>
<h3 id="viper-장점">Viper 장점</h3>
<ul>
<li>MVC 대비 추상적</li>
<li>각각의 책임 분담으로 인한 유지보수 도움</li>
<li>테스트 용이 단 Presenter, Interactor 한</li>
</ul>
<h2 id="ribs는-왜">Ribs는 왜?</h2>
<ul>
<li>View, Business 트리와 밀접 -&gt; 한쌍을 이루어야 함이 거슬림 -&gt; 단독 노드 구성 가능하게</li>
<li>View 트리가 중심인점</li>
</ul>
<h3 id="viper-아키텍처">VIPER 아키텍처</h3>
<pre><code class="language-swift">View &lt;-&gt; Presenter &lt;-&gt; Interactor -&gt; Entity
            |-&gt; Router</code></pre>
<h3 id="ribs">RIBs</h3>
<pre><code class="language-swift">Builder &lt;-&gt; Component
   ↓
┌──────────────────────────────────────────────┐
│ Router &lt;- Interactor &lt;-&gt; Presenter &lt;-&gt; View  │
└──────────────────────────────────────────────┘</code></pre>
<h1 id="ribs-의-정의">RIBs 의 정의</h1>
<blockquote>
<p>UI가 중심이 아닌 Business Logic 중심으로 하겠다!
State 변화 감지는 Business 단에서 관리 하겠다!
L &quot;&quot; 는 Scope ( 격리된거죠 ) 를 통해 관리 하겠다!
하나의 화면엔 여러 VC 가 이루어질 수 있다!</p>
</blockquote>
<h2 id="ribs-필수적-요소">RIBs 필수적 요소</h2>
<ul>
<li>Router: Attach (뷰 추가) Detach (뷰 제거)</li>
<li>Interactor: 비즈니스 로직 -&gt; Rib 들을 Attach, Detach할지 명령</li>
<li>Builder: Rib 생성</li>
</ul>
<h2 id="옵션-적인-요소">옵션 적인 요소</h2>
<ul>
<li>UI:          Layout, Animation</li>
<li>Presenter:   Translation Logic - Interactor &lt;-&gt; View</li>
</ul>
<h3 id="scope">Scope</h3>
<blockquote>
<p>상속처럼 자식에서 부모의 내용을 받아서 사용 가능
AuthToken 같은거 생각
State관리는 이친구가 관리</p>
</blockquote>
<ul>
<li>Child Rib에서는 Parent Rib의 내용을 이미 가지고 있다고 가정</li>
</ul>
<h3 id="ribs-life-cycle">RIBs Life cycle</h3>
<blockquote>
<p>Attach, Detach 상태가 존재하는데
Router에서 애니메이션 설정이 필요함
Router에 Will, did 라이프 사이클을 추가하여 사용하면 편리</p>
</blockquote>
<h3 id="ribs-단점">RIBs 단점</h3>
<blockquote>
<p>하나의 기능에 많은 클래수 수와 하나의 파일에 여러가지 클래스가 존재
Ribs를 하나의 프로젝트에 전체를 종속 시켜야 하는 문제
Ribs 내부에 RxSwift가 내장되어 있는데 이것 또한 문제</p>
</blockquote>
<h2 id="parent-와-자식-간의-데이터-교환시">Parent 와 자식 간의 데이터 교환시</h2>
<blockquote>
<p>Interface Listener 사용하여 소통함</p>
</blockquote>
<h1 id="ribs-생성-방법">RIBs 생성 방법</h1>
<ul>
<li><p>Rib 생성</p>
</li>
<li><p>부모의 Router에 프로퍼티 적용: 생성된 Child Builder 프로퍼티 추가</p>
</li>
<li><p>Dependency적용: Builder를 만들떄에 Componenet 주입</p>
<pre><code>           컴포넌트는 부모의 컴포넌트 속성을 따름</code></pre></li>
<li><p>Builder 에 변경된 Router 내용 적용</p>
<h2 id="ribs-구성-상세">RIBs 구성 상세</h2>
<h3 id="interactor">Interactor</h3>
<blockquote>
<p>비즈니스 로직을 포함
해당 클래스에서 Rx 구독을 수행해야함
상태를 변경하는 결정을 수행
그외에 다른 Rib을 자식으로 연결할지 결정함</p>
</blockquote>
<h3 id="router">Router</h3>
<blockquote>
<p>Interactor의 요청을 수신 받음
자식 Rib의 연결 혹은 분리</p>
</blockquote>
<ol>
<li>라우터는 자식 Interactor를 모의하거나 그 존재에 신경 쓸 필요 없이
복잡한 Interactor 논리를 쉽게 테스트할 수 있게 해주는
Humble Objects 역할을 합니다.</li>
<li>라우터는 부모 인터랙터와 자식 인터랙터 사이에 추가적인 추상화 계층을 생성합니다.
이로 인해 인터랙터 간의 동기식 통신이 다소 어려워지고,
RIB 간의 직접 결합 대신 반응형 통신을 채택하는 것이 용이해집니다.</li>
<li>라우터는 인터랙터에서 구현해야 하는 간단하고 반복적인 라우팅 로직을 포함합니다.
이러한 보일러플레이트 코드를 제거하면 인터랙터의 크기를 줄이고
RIB에서 제공하는 핵심 비즈니스 로직에 더 집중할 수 있습니다.</li>
</ol>
</li>
</ul>
<h3 id="router-종류">Router 종류</h3>
<ul>
<li><p>Router<Interactable></p>
<blockquote>
<p>기본 라우터
뷰가 존재 하지 않는 경우에 사용</p>
</blockquote>
<pre><code class="language-swift">ViewableRouter&lt;Interactable, ViewControllable&gt;</code></pre>
<blockquote>
<p>뷰가 존재할 경우에 사용해야하는 라우터
즉 이 RIB이 ViewController를 가지고 있어야 함
자식의 화면을 Present/ Dismiss 하거나 교체 하여야 함</p>
</blockquote>
<pre><code class="language-swift">LaunchRouter&lt;Interactable, ViewControllable&gt;</code></pre>
<blockquote>
<p>앱 시작 지점 전용
AppDelegate 에서 Launch 로 앱을 띄울때 쓰이는 루트 라우터</p>
</blockquote>
</li>
</ul>
<h3 id="builder">Builder</h3>
<blockquote>
<p>RIB의 모든 구성 클래스, 각 자식에 대한 빌더를 인스턴스화</p>
</blockquote>
<h3 id="presenter">Presenter</h3>
<blockquote>
<p>비즈니스 모델을 뷰 모델로 변환 혹은
반대로 변환하는 상태를 저장하지 않는 클래스
해당 클래스는 생략이 가능하며 뷰모델 변환은 뷰컨트롤러 혹은 인터랙터 책임</p>
</blockquote>
<h3 id="viewcontroller">View(Controller)</h3>
<blockquote>
<p>UI 업데이트 혹은 빌드</p>
</blockquote>
<h3 id="component">Component</h3>
<blockquote>
<p>Rib 종속성 관리
Rib을 구성하는 다른 유닛들을 인스턴스화 할 수 있도록 지원
외부 종속성에 대한 접근을 제공
부모 Rib의 컴포넌트는 일반적으로 자식 RIB의 빌더에 주입되어 자식 RIB이
부모 Rib의 종속성에 접근하도록 함</p>
</blockquote>
<h3 id="마무리-하면서">마무리 하면서</h3>
<blockquote>
<p>개념을 다루면서 제가 궁금했던 점을 정리한 내용입니다.
TCA 편을 보셨다면 꽤 비슷한 흐름이 보이실 겁니다.
어느 부분이냐 바로 트리 구조라는 흐름이 포인트일 것 같습니다.</p>
</blockquote>
<blockquote>
<p>TCA도 상위 Reducer 가 하위 리듀서를 감지할 수 있듯이
RIBs 도 그런 형태를 취합니다.</p>
</blockquote>
<blockquote>
<p>다만 RIBs는 제가 개인적으로 느끼기에는
많이 복잡한 구조를 가지고 있습니다.
다음편인 프로젝트 작성하기 편에서 느끼실 수 있을 것 같아요
모두 고생하셨구 다음편에서 뵙겠습니다....
감사합니다.</p>
</blockquote>
<blockquote>
<p>바로 프로젝트 코드를 보고싶다면
<a href="https://github.com/Little-tale/HowRIbs">https://github.com/Little-tale/HowRIbs</a>
를 참고해주세용 스타도 주시면 감사합니당</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[깜작 스낵 알고리즘! - 백트래킹]]></title>
            <link>https://velog.io/@little_tail/%EA%B9%9C%EC%9E%91-%EC%8A%A4%EB%82%B5-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%B0%B1%ED%8A%B8%EB%9E%98%ED%82%B9</link>
            <guid>https://velog.io/@little_tail/%EA%B9%9C%EC%9E%91-%EC%8A%A4%EB%82%B5-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%B0%B1%ED%8A%B8%EB%9E%98%ED%82%B9</guid>
            <pubDate>Tue, 19 Aug 2025 11:38:57 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가기에-앞서">들어가기에 앞서</h2>
<blockquote>
<p>사실 알고리즘을 블로그 글을 
핀 레이아웃편을 준비 하고 있었는데
준비하면서 알고리즘도 같이 공부 하고 있었습니다.
이때 백트래킹이 정말 인상 깊었어서 잠깐 다루는 시간을 갖도록 하겠습니다.</p>
</blockquote>
<h1 id="백트래킹">백트래킹</h1>
<blockquote>
<p>사전적으로는
<code>&quot;가능한 해를 점진적으로 찾다가 해가 아니라면 되돌아가는 깊이 우선 탐색 + 가지치기 기법이다.&quot;</code>
라고 정의가 되어 있기는 합니다.
사실 말로만 들으면 뭔말인지 저어어언혀 이해가 안갔어요 저는
저는 이정의를 좀더 심플하게 정의 해보고자 합니다.
<code>반복해서 해를 찾는데 아니다 싶으면 되돌리기</code></p>
</blockquote>
<h2 id="예시-문제">예시 문제</h2>
<blockquote>
<p>백준 문제에서 괜찮다 싶은 문제 2개를 가져와 보았습니다.</p>
</blockquote>
<h3 id="n과-m-6번째-문제">N과 M 6번째 문제</h3>
<blockquote>
<p>문제: N개의 자연수와 자연수 M이 주어졌을 때, 아래 조건을 만족하는 
길이가 M인 수열을 모두 구하는 프로그램을 작성하시오. </p>
</blockquote>
<ul>
<li><p>N개의 자연수는 모두 다른 수이다.</p>
</li>
<li><p>N개의 자연수 중에서 M개를 고른 수열은 오름차순이어야 한다.</p>
</li>
<li><p>예시 입력</p>
<pre><code class="language-text">3 1
4 5 2</code></pre>
</li>
<li><p>예시 출력</p>
<pre><code class="language-text">2
4
5</code></pre>
</li>
</ul>
<blockquote>
<p>위에서 언급했다 싶이 조건을 만들고 조건이 맞다 싶으면 결과를 쌓을것이며 아니다 싶으면 돌아 가겠습니다.</p>
</blockquote>
<pre><code class="language-swift">let nm = readLine()!.split(separator: &quot; &quot;).map{ Int($0)! }
let n = nm[0] // 총 갯수
let m = nm[1] // 고를수있는 갯수

let numbers = readLine()!.split(separator: &quot; &quot;).map { Int($0)! }.sorted()
var visited = Array(repeating: false, count: n)
var temp: [Int] = []
var answer: String = &quot;&quot;


func solution(start: Int) {
    if temp.count == m {
        answer += temp.map{ String($0) }.joined(separator: &quot; &quot;) + &quot;\n&quot;
        return
    }

    for i in start..&lt;n {
        temp.append(numbers[i])
        solution(start: i + 1)
        temp.removeLast()
    }
}

solution(start: 0)
print(answer, terminator: &quot;&quot;)</code></pre>
<blockquote>
<p>코드를 유심히 보시면
솔류션 함수에서 숫자를 검사합니다. 길이가 m과 같은지 
같다면 돌아가는 형태죠?</p>
</blockquote>
<blockquote>
<p>그아래에 for문을 보시면
솔류션 함수를 다시 호출하고 있습니다.
재귀 함수구나! 를 접근이 가능하죠</p>
</blockquote>
<blockquote>
<p>이말은 즉 1,2,3,4,5 가 있다면 1,2,3,4,5 - 1,2,3,5,4.... 같이 모오든 경우의 수를 다 보겠다 라는 의미입니다.</p>
</blockquote>
<h3 id="로또-문제">로또 문제</h3>
<blockquote>
<p>독일 로또는 {1, 2, ..., 49}에서 수 6개를 고른다.
로또 번호를 선택하는데 사용되는 가장 유명한 전략은 </p>
</blockquote>
<p>49가지 수 중 k(k&gt;6)개의 수를 골라 집합 S를 만든 다음 그 수만 가지고 번호를 선택하는 것이다.</p>
<blockquote>
</blockquote>
<p>예를 들어, k=8, S={1,2,3,5,8,13,21,34}인 경우 
이 집합 S에서 수를 고를 수 있는 경우의 수는 총 28가지이다. </p>
<blockquote>
</blockquote>
<p>([1,2,3,5,8,13], [1,2,3,5,8,21], [1,2,3,5,8,34], [1,2,3,5,13,21], ..., [3,5,8,13,21,34])</p>
<blockquote>
</blockquote>
<p>집합 S와 k가 주어졌을 때, 수를 고르는 모든 방법을 구하는 프로그램을 작성하시오.</p>
<blockquote>
<p>해당 문제는 위 문제보다 좀더 백트래킹에 대해 이해 할수 있을것 같아서 가져와 보았습니다.</p>
</blockquote>
<pre><code class="language-swift">var text: String = &quot;&quot;

while true {
    let numbers = readLine()!.split(separator: &quot; &quot;).map { Int($0)! }
    let k = numbers[0]
    if k == 0 {
        break
    } else if !text.isEmpty {
        text += &quot;\n&quot;
    }
    let lottoNumbers = Array(numbers[1...])
    var temp: [Int] = []
    solution(originalNumbers: lottoNumbers, current: &amp;temp, start: 0, text: &amp;text)
}

func solution(originalNumbers: [Int], current: inout [Int], start: Int, text: inout String) {
    if current.count == 6 {
        text += current.map { String($0) }.joined(separator: &quot; &quot;) + &quot;\n&quot;
        return
    }

    for i in start..&lt;originalNumbers.count {
        current.append(originalNumbers[i])
        solution(originalNumbers: originalNumbers, current: &amp;current, start: i + 1, text: &amp;text)
        current.removeLast()
    }
}

print(text, terminator: &quot;&quot;)</code></pre>
<blockquote>
<p>반복문을 잘 보시면
넣었다가 빼고 하는 과정이 있습니다.</p>
</blockquote>
<blockquote>
<p>이 부분이 햇갈려 하실 수 도 있으신데
넣으면 1 -&gt; 넣고 2 -&gt; 쭉 가서 조건인 6개까지 모였는가 모이면 결과를 쌓고 돌아가는 형태죠?</p>
</blockquote>
<h3 id="마무리-하며">마무리 하며</h3>
<blockquote>
<p>간단히 짤막한 스낵 알고리즘 - 백트래킹 편이였습니다.
처음엔 상당히 이해가 안되었는데 계속 하다보면 이해가 되드라구요
모두 수고 하셨습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Tuist 마지막편 - RunScript With Firebase]]></title>
            <link>https://velog.io/@little_tail/Tuist-%EB%A7%88%EC%A7%80%EB%A7%89%ED%8E%B8-RunScript-With-Firebase</link>
            <guid>https://velog.io/@little_tail/Tuist-%EB%A7%88%EC%A7%80%EB%A7%89%ED%8E%B8-RunScript-With-Firebase</guid>
            <pubDate>Thu, 24 Jul 2025 07:15:05 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가기에-앞서">들어가기에 앞서</h3>
<blockquote>
<p>드디어 마지막 편으로 돌아온것 같아요
저번 시간에 <code>RunScript</code> 관련해서 글을 작성하겠다고 
했었는데 이번편을 <code>Tuist</code>는 마무리 지어보고자 합니다.
마지막 편을 다 보시고 나서도 이해가 되지 않는 부분들은
댓글로 알려주시면 따로 연락 드리거나 글을 작성하겠습니다.</p>
</blockquote>
<h1 id="run-script">Run Script</h1>
<blockquote>
<p>일단은 이친구가 뭔지를 이해하고 넘어갈 필요가 있다고 생각을 해요
물론 요즘은 서드파티 문서에 <code>복붙</code>하면 문제가 생기지 않겠지만
<code>Tuist</code>처럼 멀티 모듈화를 진행하게 되면
문제가 발생하게 되요 
즉 우리는 이 RunScript를 이해할 필요가 있는거죠</p>
</blockquote>
<h2 id="개념">개념</h2>
<blockquote>
<p>Xcode의 <code>BuildPhase</code> 중 <code>Run Script Phase</code> 가 존재합니다.
해당 단계는 <code>쉘 명령을 빌드 도중에 동작하도록 해주는 단계</code> 라고 생각해 주시면
될 것 같습니다.</p>
</blockquote>
<p>예를 들어보면</p>
<pre><code class="language-swift">echo &quot;빌드 시작&quot;
swiftlint // 코드스타일
echo &quot;빌드 끝&quot;</code></pre>
<p>해당하는 코드처럼 작성을 하게 될 거에요.</p>
<blockquote>
<p>자 저희가 집중해서 볼 부분은 해당 파일을 지정해서 동작해줄 수 있도록 해야한다는 거죠.</p>
</blockquote>
<h2 id="firebase-문서">FireBase 문서</h2>
<ul>
<li>링크: <a href="https://firebase.google.com/docs/crashlytics/get-started?hl=ko&amp;platform=ios">https://firebase.google.com/docs/crashlytics/get-started?hl=ko&amp;platform=ios</a></li>
</ul>
<blockquote>
<p>해당 문서를 참고해서 보시면</p>
</blockquote>
<pre><code class="language-shell"># 이 스크립트는 프로젝트의 dSYM 파일을 처리하고 파일을 Crashlytics에 업로드합니다.
&quot;${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run&quot;

# Input Files(입력 파일) 섹션에서 다음 파일 위치의 경로를 추가합니다.
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist
$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist
$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}.debug.dylib</code></pre>
<blockquote>
<p>위와 같은 경로를 지정해 달라고 합니다.</p>
</blockquote>
<ul>
<li>위 경로가 필요한 이유는 <code>빌드중에 지정된 파일을 사용할 수 있는지를 확인</code>하고, <code>dSYM파일</code>을 처리하기 위함입니다.</li>
</ul>
<h2 id="문제의-상황">문제의 상황</h2>
<blockquote>
<p>일반적으로는 진짜 저렇게 복붙을 하고 작업하더라도 문제가 발생하지 않습니다.
경로는 <code>고정</code>이였을 것이니까요.</p>
</blockquote>
<blockquote>
<p>단, 현재 멀티 모듈화를 하게 되면 해당 경로는 당연히 다를 겁니다.
즉, 크래시가 발생하면 <code>dSYM 파일</code>이 처리되지 못 하여 수동으로 제공해 달라 라는 
문제가 발생합니다.</p>
</blockquote>
<h1 id="tuist--runscript">Tuist + RunScript</h1>
<blockquote>
<p>이 부분은 정말 귀찮고, 번거롭겠지만 Tuist 환경에서는 다음과 같이 해결이 필요합니다.
<code>직접 경로 세팅</code>이 말이죠...!</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/little_tail/post/6a27deb5-3fb3-44e3-b124-d9f0486db73a/image.png" alt=""></p>
<blockquote>
<p>자 Tuist는 다음과 같이 라이브러리들을 관리하고 있습니다.
위에서 봤던 스크립트 코드를 한번 다시 보죠.</p>
</blockquote>
<pre><code class="language-shell">&quot;${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run&quot;</code></pre>
<blockquote>
<p>해당 경로를 다음과 같이 수정하겠습니다.
제 파일 구조와 다를 수 있으니 <code>참고</code>하시면 좋겠습니다. + <code>Manifests</code>에서 작업 했습니다.</p>
</blockquote>
<pre><code class="language-swift">private let fireBaseRunScript = &quot;&quot;&quot;
#!/bin/sh

echo &quot;[Script] CHECKING dSYM: ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}&quot;
ls -l &quot;${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}&quot;

&quot;${PROJECT_DIR}/../../Tuist/.build/checkouts/firebase-ios-sdk/Crashlytics/run&quot;
&quot;&quot;&quot;</code></pre>
<blockquote>
<p>두번째로, 예를 들어 <code>InfoPlist</code>가 여러개다 가정하고 이를 관리할 수 있도록 하는 스크립트를 
작성하겠습니다.</p>
</blockquote>
<pre><code class="language-swift">let fireBaseInfoPlistScript = &quot;&quot;&quot;
#!/bin/sh

PATH_TO_GOOGLE_PLISTS=&quot;${PROJECT_DIR}/../../AppSettingFiles/InfoPlist&quot;

echo &quot;[Script] Checking Dev plist path: $PATH_TO_GOOGLE_PLISTS/GoogleService-Dev-Info.plist&quot;
echo &quot;[Script] Checking BUILT_PRODUCTS_DIR: ${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}&quot;

case &quot;${CONFIGURATION}&quot; in
   &quot;Dev&quot;)
    echo &quot;[Script] Checking Dev MODE&quot;
     cp &quot;$PATH_TO_GOOGLE_PLISTS/GoogleService-Dev-Info.plist&quot; &quot;${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist&quot;
     ;;
   *)
    echo &quot;[Script] Checking LIVE MODE&quot;
     cp &quot;$PATH_TO_GOOGLE_PLISTS/GoogleService-Live-Info.plist&quot; &quot;${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist&quot;
     ;;
esac
&quot;&quot;&quot;</code></pre>
<h2 id="tuist-적용">Tuist 적용</h2>
<blockquote>
<p>자 위와 같이 문제점을 알았고 해결방법도 알았으니 이제 적용을 해보고자 합니다.
좀 더 간편히 쓸 수 있도록 <code>TargetScript</code>를 <code>확장</code>해서 작성하였습니다.</p>
</blockquote>
<pre><code class="language-swift">extension TargetScript {
    public static let fireBase = Self.pre(
        script: fireBaseInfoPlistScript,
        name: &quot;FirebaseInfoPlistScript&quot;,
        basedOnDependencyAnalysis: false
    )

    public static let fireBaseCrashlyticsRun = Self.post(
        script: fireBaseRunScript,
        name: &quot;Firebase Crashlytics&quot;,
        inputPaths: [
            &quot;${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}&quot;,
            &quot;${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}&quot;,
            &quot;${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist&quot;,
            &quot;$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist&quot;,
            &quot;$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)&quot;
        ],
        basedOnDependencyAnalysis: false
    )
}

 // Target 적용
    .target(
        ...
        scripts: [.firebase, .fireBaseCrashlyticsRun]
    )
</code></pre>
<blockquote>
<p>하단에 <code>.target</code> 코드가 존재하는데 위와같이 타겟에 최종적으로 적용하셔야 동작합니다.
다른 스크립트에서 <code>특정 경로</code>가 필요한 경우라면 위와같이 해결하시면 됩니다...!</p>
</blockquote>
<hr>
<h3 id="마무리하며">마무리하며...</h3>
<blockquote>
<p>드디어 <code>Tuist</code>편이 마무리 되었습니다.
Tuist도 TCA처럼 <code>업데이트가 빈번한</code> 라이브러리라 
<code>특정 버전</code>을 고정해서 사용하시면 좋을 것 같습니다.
TCA나 Rx도 잠깐 다루었던 것 같은데 Tuist가 가장 오래 다룬 편인 것 같습니다.
그만큼 내용이 방대하고 핵심만 뽑아서 블로그에 녹이려다 보니 시간이 꽤 걸렸던 것 같습니다.
다음편은 고민중인게 <code>Fastlane</code> 혹은 <code>Pin, Flex Layout</code> 둘중에 무엇을 다루는게 좋을지
고민입니다. 혹시 <code>원하는 것</code> 있으시면 <code>그것</code>부터 작성해 보도록 하겠습니다.</p>
</blockquote>
<h4 id="고생하셨습니다-모두">고생하셨습니다. 모두!</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[Tuist_What_The...  - TCA With Preview 편]]></title>
            <link>https://velog.io/@little_tail/TuistWhatThe...-TCA-With-Preview-%ED%8E%B8</link>
            <guid>https://velog.io/@little_tail/TuistWhatThe...-TCA-With-Preview-%ED%8E%B8</guid>
            <pubDate>Mon, 30 Jun 2025 12:53:37 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가기에-앞서">들어가기에 앞서</h3>
<blockquote>
<p>Tuist 편중 Why How 편을 모두 마치고
What The 편으로 돌아 왔습니다.</p>
</blockquote>
<blockquote>
<p>이번 시간에는 Tuist 의 고질병이라고 해야 할지 모르겠는데
Mecro 기능이 있는 라이브러리에서 Preview 와 충돌이 있는지
Preview가 동작하지 않는 이슈가 있어서
왜 이런 현상이 발생하는지
어떻게 해결하는지 알아보도록 하겠습니다.</p>
</blockquote>
<h1 id="static-framework-dynamic-framework">Static Framework, Dynamic Framework</h1>
<h2 id="framework">Framework</h2>
<blockquote>
<p>프레임워크는 여러 프로젝트에서 사용할 기능을 하나의 모듈로 만들어 둔 후 
사용할 수 있도록 하는 캡슐화 계층 구조 파일 디렉토리입니다.</p>
</blockquote>
<blockquote>
<p>정적 프레임워크(Static Framework)와 동적 프레임워크(Dynamic Framework)로 나뉘며, 둘 중 어떤방식으로 할지에 따라 빌드 시간과 메모리 사용량, 앱 성능 등을 개선시킬 수 있습니다. 다만, 이를 잘못 구성하면 런타임 충돌과 같은 문제를 일으킬 수 있지요.</p>
</blockquote>
<h2 id="static-framework">Static Framework</h2>
<blockquote>
<p>정적 라이브러리(static library, .a파일)와 리소스를 포함하는 프레임워크 입니다. 
컴파일 시점에 실행파일에 포함됩니다. 즉 앱이 실행될 때에 로딩 과정 없이 바로 사용할 수 있는거죠.</p>
</blockquote>
<blockquote>
<p>정적 라이브러리 코드가 Heap 메모리에 올라가게 됩니다.
만약 여러 타켓에서 사용하게되면 코드 중복현상이 발생 하겠죠. ( 용량 증가 )
컴파일러가 정리를 해줍니다...만 Tuist 를 쓰면 이게 정리가 잘 안됩니다.
그래서 이 부분을 잘 판단해서 사용해야 합니다.</p>
</blockquote>
<h2 id="dynamic-framework">Dynamic Framework</h2>
<blockquote>
<p>동적 라이브러리(.dylib)와 리소스를 포함하는 프레임워크입니다. 
앱 실행 파일에 직접 포함되지 않고 앱 실행 시 동적으로 로드됩니다.</p>
</blockquote>
<blockquote>
<p>빌드 시에는 링크 되지만 실행 파일에는 포함되지 않습니다.
Dynamic Library Reference가 포함되며, 이를 통해 모듈 호출 시에 시에 
Stack에 있는 라이브러리가 로드 되어 메모리에 올라갑니다.</p>
</blockquote>
<blockquote>
<p>다시 정리하면, 
필요할때 메모리에 올라갑니다 ( Stack 에 따라 )
동적이다보니 앱 실행 속도 저하가 생길 수 있다.</p>
</blockquote>
<h1 id="잠깐-왜-이걸-설명하나요">잠깐 왜 이걸 설명하나요?</h1>
<blockquote>
<p>이유는 정적 라이브러리는 Preview에서 동작하지 않습니다.</p>
</blockquote>
<p align="center">
    <img src="https://velog.velcdn.com/images/little_tail/post/72d911dc-04d0-4c9e-bf77-7afa760293db/image.png" width="300" height="300"/>
</p>

<blockquote>
<p><code>TCA</code>나 <code>TCACoordinator</code> 등 내부 내부적으로 스태틱한 부속 라이브러리가 존재합니다.
이를 동적으로 바꿔줘야 하는 문제가 생긴거죠</p>
</blockquote>
<blockquote>
<p>사실 이부분은 TCA 측에서도 알고 있고, 해결한 문제였기도 합니다.
다만, 현재 제가 Tuist 버전을 <code>4.51.1</code>을 사용하고 있고
TCA는 <code>1.17.1</code> 버전을 사용중인데 싱글 모듈 형태일때는 
정상적으로 동작하였습니다.</p>
</blockquote>
<blockquote>
<p>다만 여러 타켓을 만들고 연결하고나서 테스트를 하게되면 
문제가 발생하는 거죠.</p>
</blockquote>
<blockquote>
<p>내부 코드를 다 보지는 못했지만 TCA Mecro 쪽에서 문제가 발생하는 것으로 
보이드라구요</p>
</blockquote>
<h3 id="swift-macro">Swift macro</h3>
<blockquote>
<p>macro는 Swift 컴파일러가 빌드 타임에 코드를 생성하기 위한 도구
macro 정의가 포함된 모듈이 현재 타겟에 포함되어 있어야 합니다.</p>
</blockquote>
<blockquote>
<p>즉 다시말해, Macro가 실행되려면, 그 macro를 제공하는 모듈이 
compiler plugin으로 런타임처럼 실행될 수 있어야 합니다.</p>
</blockquote>
<h3 id="내가-생각하는-원인-결론">내가 생각하는 원인 결론</h3>
<blockquote>
<p>다중 모듈/타겟에서는 macro symbol이 다른 타겟으로 전파되지 않아서 문제가 발생했고
다이나믹 프레임워크로 동작하게 해서 외부 타겟에서도 명시적으로 링크되고 탐색 가능하게 
바꾼다.</p>
</blockquote>
<h1 id="해결법">해결법</h1>
<blockquote>
<p>해결법도 상당히 간단합니다.
내부 라이브러리를 다이나믹하게 하면 해결이 됩니다.
다만 DrivedData한번 밀어버리시고, tuist install후 테스트 해보시면
대부분 되는데 안될때가 있어요 그럴땐 DrivedData 내부에 실행한 시뮬레이터(Preview)
폴더만 제거하고 다시 해보시면 동작 합니다. </p>
</blockquote>
<pre><code class="language-swift">private let tcaDynamics: [String : Product] = [
    &quot;ComposableArchitecture&quot;: .framework,
    &quot;Dependencies&quot;: .framework,
    &quot;CombineSchedulers&quot;: .framework,
    &quot;Sharing&quot;: .framework,
    &quot;SwiftUINavigation&quot;: .framework,
    &quot;UIKitNavigation&quot;: .framework,
    &quot;UIKitNavigationShim&quot;: .framework,
    &quot;ConcurrencyExtras&quot;: .framework,
    &quot;Clocks&quot;: .framework,
    &quot;CustomDump&quot;: .framework,
    &quot;IdentifiedCollections&quot;: .framework,
    &quot;XCTestDynamicOverlay&quot;: .framework,
    &quot;IssueReporting&quot;: .framework,
    &quot;_CollectionsUtilities&quot;: .framework,
    &quot;PerceptionCore&quot;: .framework,
    &quot;Perception&quot;: .framework,
    &quot;OrderedCollections&quot;: .framework,
    &quot;CasePaths&quot;: .framework,
    &quot;DependenciesMacros&quot;: .framework,
    &quot;FlowStacks&quot;: .framework // TCA Coordinator
]</code></pre>
<h3 id="마치면서">마치면서...</h3>
<blockquote>
<p>Tuist 편이 거의 마무리 되어가고 있는거 같아요
제가 Tuist를 사용하면서 문제가 생겼던 부분들을 What_The 편에서 다룰 계획인데
다음 편은 파이어 베이스 크래시틱스나, 애널리틱스 같은 라이브러리에서 
RunScript 동작 시키는 방법을 해보도록 하겠습니다. (Tuist에서)
모두 고생하셨습니다.</p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>