<?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>Thu, 18 Jun 2026 00:30:37 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[[짤막지식] Web]]></title>
            <link>https://velog.io/@little_tail/%EC%A7%A4%EB%A7%89%EC%A7%80%EC%8B%9D-Web</link>
            <guid>https://velog.io/@little_tail/%EC%A7%A4%EB%A7%89%EC%A7%80%EC%8B%9D-Web</guid>
            <pubDate>Thu, 18 Jun 2026 00:30:37 GMT</pubDate>
            <description><![CDATA[<h1 id="1-react-로-짜면-봇이-못읽는다-">1. React 로 짜면 봇이 못읽는다 ???</h1>
<blockquote>
<p>React 웹은 화면을 만드는 방식에 따라 검색엔진이 내용을 볼 수도 있고 못 볼 수도 있음</p>
</blockquote>
<pre><code class="language-jsx">서버가 빈 HTML 전달
→ 브라우저가 JS 다운로드
→ React 실행
→ 화면 생성</code></pre>
<blockquote>
<p>React 앱은 처음 HTML이 거의 비어 있고, 
브라우저에서 JavaScript가 실행된 뒤에 화면이 만듬 </p>
</blockquote>
<p>이걸 <strong>CSR</strong>, Client-Side Rendering이라함</p>
<blockquote>
</blockquote>
<pre><code class="language-jsx">SSR: 서버에서 HTML을 미리 만들어서 보냄
SSG: 빌드 시점에 HTML을 미리 만들어 둠
Prerender: 크롤봇용 HTML을 미리 생성</code></pre>
<p>즉 검색엔진이 JavaScript를 실행 하지 않아도 본문을 읽을 수 있게 만드는 것임</p>
<h3 id="next로-해결하면">Next로 해결하면?</h3>
<pre><code class="language-jsx">Next.js SSR/SSG
→ 처음부터 본문이 들어간 HTML 제공
→ 크롤봇이 JS 실행 안 해도 읽을 수 있음</code></pre>
<p>요즘 검색엔진/봇 들이 발전하면서 리엑트여도 가능하다라는 말이 있음</p>
<h1 id="2-react가-빠른-이유는-virtual-dom-때문일까">2. React가 빠른 이유는 Virtual DOM 때문일까?</h1>
<blockquote>
<p>흔히 React가 빠른 이유를 Virtual DOM이라고 설명한다.</p>
</blockquote>
<p>하지만 실제로는</p>
<ul>
<li>컴포넌트 구조</li>
<li>상태 관리</li>
<li>변경된 부분만 업데이트</li>
</ul>
<p>가 더 중요한 이유다.</p>
<ul>
<li>Virtual DOM 자체가 무조건 빠른 것은 아니다.</li>
</ul>
<h1 id="3-spa는-왜-새로고침이-빠를까">3. SPA는 왜 새로고침이 빠를까?</h1>
<blockquote>
<p>React는 보통 SPA(Single Page Application) 구조를 사용한다.</p>
</blockquote>
<p>페이지 이동 시</p>
<ul>
<li>HTML 전체를 다시 받지 않음</li>
<li>필요한 데이터만 API로 요청</li>
<li>변경된 화면만 갱신</li>
</ul>
<p>그래서 앱처럼 부드럽게 동작한다.</p>
<h1 id="4-nextjs는-왜-react인데-seo에-유리하다는거야">4. Next.js는 왜 React인데 SEO에 유리하다는거야?</h1>
<blockquote>
<p>일단 Next.js는 페이지를 만드는 방식을 선택 가능</p>
</blockquote>
<ul>
<li>CSR: 브라우저에서 화면 생성</li>
<li>SSR: 요청이 들어올 때 서버에서 HTML 생성</li>
<li>SSG: 빌드할 때 HTML을 미리 생성</li>
</ul>
<blockquote>
<p>React도 쓰면서, 처음부터 내용이 들어있는 HTML을 줄수도 있다.
즉, 봇들이 읽고 가기 좋은 환경이다</p>
</blockquote>
<h2 id="hydration">Hydration</h2>
<ol>
<li>서버 → HTML 만들어 브라우저에</li>
<li>화면을 빠르겐 볼 수 있음 ( 다만 껍데기 )</li>
<li>브라우저에서 React JS 실행</li>
</ol>
<blockquote>
<p>위 과정을 Hydration 이라고 함.</p>
</blockquote>
<h1 id="5-nextjs에서-use-client는-무슨-뜻일까">5. Next.js에서 &quot;use client&quot;는 무슨 뜻일까?</h1>
<blockquote>
<p>Next.js App Router에서는 기본적으로 컴포넌트가 서버에서 실행된다.
그런데 useState, useEffect, onClick 같은 브라우저 기능이 필요하면
클라이언트에서 실행해야 한다.</p>
</blockquote>
<blockquote>
<p>이때 파일 맨 위에 적는 것이 &quot;use client&quot; 이다. </p>
</blockquote>
<h1 id="필요-개념">필요 개념</h1>
<hr>
<h3 id="csr">CSR</h3>
<blockquote>
<p>서버에서 거의 빈 HTML → Browser 가 JavaScript를 실행 하여 화면 구성</p>
</blockquote>
<h3 id="spa">SPA</h3>
<blockquote>
<p>페이지가 여러개 처럼 보이게만
사실 HTML 하나에서 화면만 바뀌는 웹 앱</p>
</blockquote>
<h3 id="virtual-dom">Virtual DOM</h3>
<blockquote>
<p>React가 화면을 바로 고치기 전에 사용하는 가짜 화면 설계도</p>
</blockquote>
<ul>
<li>다시말해, 필요한 부분만 바꿔 동작하는 방식<blockquote>
</blockquote>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[DevOps 3편 : Terraform 해보기]]></title>
            <link>https://velog.io/@little_tail/DevOps-3%ED%8E%B8-Terraform-%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@little_tail/DevOps-3%ED%8E%B8-Terraform-%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Tue, 19 May 2026 07:30:15 GMT</pubDate>
            <description><![CDATA[<h2 id="준비물">준비물</h2>
<ul>
<li>AWS 계정</li>
<li>Terraform CLI 설치</li>
<li>AWS CLI 설치<ul>
<li><a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html">https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html</a></li>
</ul>
</li>
</ul>
<h2 id="iam-사용자-1명--access-key-1개-만들어-보자">IAM 사용자 1명 + Access Key 1개 만들어 보자</h2>
<blockquote>
<p>루트 에서 바로 하는 것 보단 AWS 권한 분리를 위해 분리해 보려고 한다.</p>
</blockquote>
<ol>
<li><p>루트 계정으로 AWS 콘솔에 로그인</p>
</li>
<li><p>상단 검색창에 IAM 입력 후 IAM 서비스로</p>
</li>
<li><p>왼쪽 메뉴 Users(사용자)</p>
</li>
<li><p>Create user (사용자 만들기)</p>
</li>
<li><p>Create group
5.1. AmazonS3FullAccess 활성화
5.2. SignInLocalDevelopmentAccess 활성화 (CLI 로그인)
AmazonS3FullAccess: S3 권한 = S3 버킷생성, 버킷 삭제, 버킷 목록조회, 객체 업로드/다운로드/삭제, 버킷정책 일부 관리</p>
</li>
<li><p>사용자 -&gt; 보안 자격 증명 -&gt; 엑세스키 -&gt; 엑세스키 만들기</p>
</li>
<li><p>aws 상태 확인해 보기
7.1 유저 정보</p>
<pre><code class="language-bash">aws sts get-caller-identity</code></pre>
<p>7.2 유저, 엑세스키, secret_key, region</p>
<pre><code class="language-bash">aws configure list</code></pre>
</li>
<li><p>Terraform 상태 확인 하기 ( 버전 )</p>
<pre><code class="language-bash"> terraform version</code></pre>
</li>
</ol>
<h2 id="이제-코드로-작성해보자">이제 코드로 작성해보자</h2>
<blockquote>
<p>작업할 폴더에서 providers.tf 를 생성</p>
</blockquote>
<pre><code class="language-hcl"># Terraform 전체 설정 블록
# 어떤 Provider를 사용할지, 버전은 몇으로 할지 정의
terraform {
  # 필수 프로파이더를 설정
  required_providers {
    # aws 변수 선언 -&gt; asw provider를 만들겠다. 
    aws = {
      # provider source 세팅
      source = &quot;hashicorp/aws&quot; # hashicorp에서 제공하는 aws 프로바이더 사용하겠다

      # provider 버전 세팅
      version = &quot;~&gt; 5.0&quot; # 5.x 버전 사용하겠다
    }
  }
}


# aws 프로바이더 설정
provider &quot;aws&quot; {
  # region 설정 서울 (ap-northeast-2)
  region = &quot;ap-northeast-2&quot;
}</code></pre>
<blockquote>
<p>main.tf</p>
</blockquote>
<pre><code class="language-hcl"># 실제로 만들 AWS 리소스 정의
# 리소스 타입 ex) aws_s3_bucket
# 리소스를 테라폼 내부에서 부를 이름 example
resource &quot;aws_s3_bucket&quot; &quot;example&quot; {
  bucket = &quot;littletale-terraform-study-001&quot; # 주의사항: AWS 전체에서 고유해야함!!
}</code></pre>
<hr>
<blockquote>
<p>현재는 </p>
<ul>
<li>providers.tf</li>
<li>main.tf</li>
</ul>
</blockquote>
<p> 이 두 구조로 진행하지만 실무에서는 좀더 나눈다고 한다.</p>
<blockquote>
<ul>
<li>providers.tf: 버전과 provider</li>
<li>variables.tf: 입력값 선언</li>
<li>terraform.tfvars: 실제 값</li>
<li>main.tf: 리소스 정의</li>
</ul>
</blockquote>
<h2 id="테라폼안에서의-동작-순서-aws-안넘어감">테라폼안에서의 동작 순서 (aws 안넘어감)</h2>
<h3 id="초기화">초기화</h3>
<pre><code class="language-bash">terraform init</code></pre>
<p><img src="https://velog.velcdn.com/images/little_tail/post/f59920f1-d7c6-4497-b68e-d9a44306564a/image.png" alt=""></p>
<blockquote>
<p>required_providers 의 값을 읽고 세팅을 한다.</p>
</blockquote>
<h3 id="린트-정리--포멧-">린트 정리 ( 포멧 )</h3>
<blockquote>
<p>Terraform 코드 스타일을 표준 형태로 자동 정리</p>
</blockquote>
<pre><code class="language-bash">terraform fmt</code></pre>
<h3 id="validate--컴파일-">validate ( 컴파일 )</h3>
<blockquote>
<p>현재 작성한 Terraform 설정이 문법적으로 맞는가 보는 단계</p>
</blockquote>
<pre><code class="language-bash">terraform validate</code></pre>
<p><img src="https://velog.velcdn.com/images/little_tail/post/68e324fa-9a3a-4211-ac64-abc51361d88e/image.png" alt=""></p>
<p>더 자세히는</p>
<ul>
<li>괄호/블록 문법 오류</li>
<li>잘못된 속성명</li>
<li>provider 설정 구조 문제</li>
</ul>
<h3 id="terraform-plan">terraform plan</h3>
<blockquote>
<p>이게 진짜 신기할 것 같은데
Terraform 이 현재 코드와 실제 인프라 상태를 비교
새로 만들것, 바꿀것, 지울것들을 미리 보여준다고 함</p>
</blockquote>
<p>예측 동작</p>
<ul>
<li>현재 AWS에 이 버킷 없음</li>
<li>코드에는 버킷 만들라고 되어 있음</li>
<li>그래서 1 to add</li>
</ul>
<p><img src="https://velog.velcdn.com/images/little_tail/post/6c2ca98f-e6af-443e-9916-79b898b90e83/image.png" alt=""></p>
<blockquote>
<p>당연한 이야기겠지만 프로젝트 파일에서 진행 해야한다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/little_tail/post/385e4fd6-1fe3-453b-8734-ea5c571fd24d/image.png" alt=""></p>
<blockquote>
<p>이번엔 AWS 로그인 정보를 읽지 못했다.</p>
</blockquote>
<h4 id="terraform의-aws-provider는-보통-이런-순서로-자격-증명을-찾음">terraform의 AWS provider는 보통 이런 순서로 자격 증명을 찾음</h4>
<ul>
<li>환경변수</li>
<li>AWS shared config/profile</li>
<li>credentials file</li>
<li>EC2 role 등</li>
</ul>
<p>즉 aws login 값을 못 가져오는 것 같음 -&gt; 엑세스 키가 필요</p>
<ol>
<li>IAM 사용자에서 Access Key 생성</li>
<li>터미널에서: aws configure</li>
<li>입력:</li>
</ol>
<ul>
<li>AWS Access Key ID</li>
<li>AWS Secret Access Key</li>
<li>Default region name: ap-northeast-2</li>
<li>Default output format: json</li>
</ul>
<pre><code class="language-bash">terraform plan

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_s3_bucket.example will be created
  + resource &quot;aws_s3_bucket&quot; &quot;example&quot; {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = &quot;littletale-terraform-study-20260415-01&quot;
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags_all                    = (known after apply)
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + cors_rule (known after apply)

      + grant (known after apply)

      + lifecycle_rule (known after apply)

      + logging (known after apply)

      + object_lock_configuration (known after apply)

      + replication_configuration (known after apply)

      + server_side_encryption_configuration (known after apply)

      + versioning (known after apply)

      + website (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.</code></pre>
<blockquote>
<h3 id="여기서-말하는-핵심은-간단하다">여기서 말하는 핵심은 간단하다</h3>
<ol>
<li>aws_s3_bucket.example 이라는 리소스를 AWS 에 새로 하나 추가한다.</li>
<li><strong>Plan: 1 to add, 0 to change, 0 to destroy.</strong> 이부분이 핵심</li>
<li><code>will be created</code>: 아직 안 만들었고, apply 하면 만들어진다는 뜻</li>
</ol>
</blockquote>
<h4 id="이제-apply-할-차례다">이제 Apply 할 차례다</h4>
<pre><code class="language-bash">terraform apply</code></pre>
<p><img src="https://velog.velcdn.com/images/little_tail/post/0e07494c-8e0d-458c-a4d6-83275e6a49ee/image.png" alt=""></p>
<pre><code class="language-bash">aws s3 ls
---
2026-04-16 09:45:47 littletale-terraform-study-20260415-01</code></pre>
<h4 id="테라폼-입장에서도-확인이-가능하다">테라폼 입장에서도 확인이 가능하다.</h4>
<pre><code class="language-bash">terraform state list
---
aws_s3_bucket.example</code></pre>
<h2 id="s3란-무엇이지">S3란 무엇이지?</h2>
<blockquote>
<p>AWS의 파일 저장 서비스
Simple Storage Service 의 줄임말!
이미지, 동영상, JSON, 로그, 백업 파일등</p>
</blockquote>
<h2 id="버킷은-무엇이지">버킷은 무엇이지?</h2>
<blockquote>
<p>S3 안에서 파일을 담는 <code>최상위 저장 공간</code>
<code>창고</code> 라고 생각하자.</p>
</blockquote>
<ul>
<li>S3 = 창고 서비스 자체</li>
<li>bucket = 창고 하나</li>
<li>버킷 안의 파일들 = 그 창고 안에 들어 있는 물건들</li>
</ul>
<p>exam</p>
<pre><code class="language-text">S3
└── bucket
    ├── file1.jpg
    ├── data.json
    └── logs.txt</code></pre>
<h1 id="변수를-사용해보자">변수를 사용해보자</h1>
<blockquote>
<p>당연하겠지만 이친구도 결국 Code 이기에 변수가 있다.</p>
</blockquote>
<p>variables.tf</p>
<pre><code class="language-hcl"># Terraform 입력 변수 선언
# 이 파일은 &quot;어떤 값을 외부에서 받을지&quot;를 정의합니다.
variable &quot;bucket_name&quot; {
  # 이 변수의 의미를 설명합니다.
  description = &quot;S3 bucket name&quot;

  # 문자열(string) 타입만 받도록 제한합니다.
  type = string
}</code></pre>
<p>terraform.tfvars</p>
<pre><code class="language-hcl"># 변수에 실제 값을 넣는 파일입니다.
# variables.tf 에서 선언한 bucket_name 변수에 값을 할당합니다.
bucket_name = &quot;littletale-terraform-study-20260415-01&quot;</code></pre>
<p>main.tf</p>
<pre><code class="language-hcl"># 실제로 만들 AWS 리소스 정의
# aws_s3_bucket = AWS S3 버킷 리소스 타입
# example = Terraform 내부에서 이 리소스를 식별하는 이름
resource &quot;aws_s3_bucket&quot; &quot;example&quot; {
  # 기존 방식: 버킷 이름을 코드에 직접 하드코딩
  # 이 방식은 간단하지만, 값 변경 시 코드 자체를 수정해야 해서 재사용성이 떨어집니다.
  # bucket = &quot;littletale-terraform-study-20260415-01&quot;

  # 현재 방식: 변수(variable)로 버킷 이름을 받아서 사용
  # variables.tf 에서 선언한 bucket_name 값을 참조합니다.
  bucket = var.bucket_name
}
</code></pre>
<h1 id="결과를-볼-수-있게">결과를 볼 수 있게</h1>
<blockquote>
<p>outputs.tf는 Terraform이 작업을 끝낸 뒤, 결과로 보여줄 값을 정의하는 파일</p>
</blockquote>
<p>outputs.tf</p>
<pre><code class="language-hcl"># Terraform 출력값 선언
# 이 파일은 &quot;Terraform이 성공적으로 생성한 리소스의 정보를 외부로 보여줄지&quot;를 정의합니다.
output &quot;bucket_name&quot; {
  # 이 출력값의 의미를 설명합니다.
  description = &quot;Created S3 bucket name&quot;

  # aws_s3_bucket.example 리소스에서 bucket 속성(attribute)의 값을 가져옵니다.
  value = aws_s3_bucket.example.bucket
}</code></pre>
<pre><code class="language-bash"> jaehyungkim  ~/Desktop/Terraform  terraform apply 
aws_s3_bucket.example: Refreshing state... [id=littletale-terraform-study-20260415-01]

Changes to Outputs:
  + bucket_name = &quot;littletale-terraform-study-20260415-01&quot;

You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only &#39;yes&#39; will be accepted to approve.

  Enter a value: yes


Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

bucket_name = &quot;littletale-terraform-study-20260415-01&quot;</code></pre>
<h3 id="와닿지-않는다">와닿지 않는다.</h3>
<ul>
<li>S3 버킷 생성</li>
<li>그 버킷 ARN을 IAM 정책에서 써야 함</li>
<li>그 버킷 이름을 다른 모듈에서 써야 함</li>
<li>생성된 URL을 출력해서 바로 확인해야 함</li>
</ul>
<p>이럴 때 output이 없으면 매번 AWS 콘솔 들어가서 찾아야 한다.</p>
<ul>
<li>variable = 내가 Terraform에 넘기는 값</li>
<li>output = Terraform이 나에게 돌려주는 값</li>
</ul>
<blockquote>
<p>일단 이정도로 정리하고 다시 다루어 보면 좋을 것 같음</p>
</blockquote>
<h1 id="tags">tags</h1>
<blockquote>
<p>AWS 리소스에 붙이는 <code>메타데이터</code></p>
</blockquote>
<ul>
<li>이 리소스 이름이 뭔지</li>
<li>누가 만들었는지</li>
<li>어떤 환경인지</li>
<li>무슨 용도인지</li>
</ul>
<blockquote>
<p>리소스가 많아지면 관리하기가 까다로워질 뿐더러, 관리자가 누구였는지 등 정보가 필요해 진다. </p>
</blockquote>
<ul>
<li>리소스 용도 파악</li>
<li>비용 추적</li>
<li>운영/정리 기준 통일</li>
<li>콘솔에서 검색/필터링 쉬움</li>
</ul>
<p>main.tf</p>
<pre><code># 실제로 만들 AWS 리소스 정의
# aws_s3_bucket = AWS S3 버킷 리소스 타입
# example = Terraform 내부에서 이 리소스를 식별하는 이름
resource &quot;aws_s3_bucket&quot; &quot;example&quot; {
  # 기존 방식: 버킷 이름을 코드에 직접 하드코딩
  # 이 방식은 간단하지만, 값 변경 시 코드 자체를 수정해야 해서 재사용성이 떨어집니다.
  # bucket = &quot;littletale-terraform-study-20260415-01&quot;

  # 현재 방식: 변수(variable)로 버킷 이름을 받아서 사용
  # variables.tf 에서 선언한 bucket_name 값을 참조합니다.
  bucket = var.bucket_name

  # 태그는 key-value 형태로 작성
  tags = {
    Name        = &quot;littletale-study-bucket&quot; # 버킷의 이름
    Environment = &quot;dev&quot;                     # 개발 환경
    Project     = &quot;terraform-study&quot;         # 프로젝트 이름
    Owner       = &quot;jaehyungkim&quot;             # 소유자
  }
}
</code></pre><ul>
<li>진짜 버킷 이름: littletale-terraform-study-20260415-01</li>
<li>태그의 Name 값: littletale-study-bucket</li>
</ul>
<h4 id="태그-name은-관리용-라벨이라-더-자유롭게-쓸-수-있습니다">태그 Name은 관리용 라벨이라 더 자유롭게 쓸 수 있습니다.</h4>
<blockquote>
<p>bucket = AWS가 인식하는 진짜 고유 이름
tags.Name = 사람이 보기 쉽게 붙이는 별칭 같은 것</p>
</blockquote>
<h2 id="이름-헷갈리지-말기">이름 헷갈리지 말기</h2>
<pre><code class="language-hcl">resource &quot;aws_s3_bucket&quot; &quot;example&quot; 
example = Terraform 코드 내부에서만 쓰는 이름, terraform이 리소스를 참조할 때 사용
----------------

bucket = var.bucket_name
AWS에 실제로 생성되는 S3 버킷의 진짜 이름, 유일무이
----------------

tags = {
  Name = &quot;s3-for-terraform-study&quot;
}
AWS 리소스에 붙이는 관리용 라벨</code></pre>
<h2 id="이제-적용해-보자">이제 적용해 보자</h2>
<pre><code class="language-bash">terraform fmt 

terraform validate
Success! The configuration is valid.

terraform plan</code></pre>
<p>plan 을 돌리고 나면 나온 출력은 다음과 같다</p>
<pre><code class="language-bash">aws_s3_bucket.example: Refreshing state... [id=littletale-terraform-study-20260415-01]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # aws_s3_bucket.example will be updated in-place
  ~ resource &quot;aws_s3_bucket&quot; &quot;example&quot; {
        id                          = &quot;littletale-terraform-study-20260415-01&quot;
      ~ tags                        = {
          + &quot;Environment&quot; = &quot;dev&quot;
          + &quot;Name&quot;        = &quot;littletale-study-bucket&quot;
          + &quot;Owner&quot;       = &quot;jaehyungkim&quot;
          + &quot;Project&quot;     = &quot;terraform-study&quot;
        }
      ~ tags_all                    = {
          + &quot;Environment&quot; = &quot;dev&quot;
          + &quot;Name&quot;        = &quot;littletale-study-bucket&quot;
          + &quot;Owner&quot;       = &quot;jaehyungkim&quot;
          + &quot;Project&quot;     = &quot;terraform-study&quot;
        }
        # (12 unchanged attributes hidden)

        # (3 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.</code></pre>
<p>기존 S3 버킷에 작성한 태그만 추가라고 잘 뜹니다.</p>
<pre><code class="language-bash">terraform apply</code></pre>
<p><img src="https://velog.velcdn.com/images/little_tail/post/40b1cc2f-5080-4159-885a-5ce22c2fac1d/image.png" alt=""></p>
<p>cli 로도 확인이 가능하다</p>
<pre><code class="language-bash">aws s3api get-bucket-tagging --bucket littletale-terraform-study-20260415-01</code></pre>
<pre><code>{
    &quot;TagSet&quot;: [
        {
            &quot;Key&quot;: &quot;Project&quot;,
            &quot;Value&quot;: &quot;terraform-study&quot;
        },
        {
            &quot;Key&quot;: &quot;Environment&quot;,
            &quot;Value&quot;: &quot;dev&quot;
        },
        {
            &quot;Key&quot;: &quot;Owner&quot;,
            &quot;Value&quot;: &quot;jaehyungkim&quot;
        },
        {
            &quot;Key&quot;: &quot;Name&quot;,
            &quot;Value&quot;: &quot;littletale-study-bucket&quot;
        }
    ]
}</code></pre><h1 id="destroy">Destroy</h1>
<p>(plan, apply)</p>
<ul>
<li>생성 </li>
<li>수정</li>
<li>확인 </li>
</ul>
<blockquote>
<p>위 단계처럼 현재까지 진행했다.
이제 삭제를 진행해 보자</p>
</blockquote>
<blockquote>
<p>Destroy 이름 부터가 부셔버리겠다인데
말 그대로다. terraform에서 관리하던 인프라 전부를 부시겠다이기 때문이다.
일단 해보자</p>
</blockquote>
<pre><code class="language-bash"> terraform plan -destroy

 terraform destroy

 ----

 aws_s3_bucket.example: Destroying... [id=littletale-terraform-study-20260415-01]
aws_s3_bucket.example: Destruction complete after 1s

Destroy complete! Resources: 1 destroyed.</code></pre>
<p><img src="https://velog.velcdn.com/images/little_tail/post/9456bc19-0c0d-4d8c-9ac5-e5b730beb0cd/image.png" alt=""></p>
<p>만든 버킷이 없어졌다.</p>
<blockquote>
<p>전부를 없애는 거다 보니 상당히 위험하게 느껴진다.</p>
</blockquote>
<h3 id="특정-리소스만-부시려면">특정 리소스만 부시려면?</h3>
<blockquote>
<p>특정 리소스만 없애기 위해 아래와 같이 작성한다.</p>
</blockquote>
<pre><code class="language-bash">terraform destroy -target=aws_s3_bucket.example</code></pre>
<p><img src="https://velog.velcdn.com/images/little_tail/post/63d294b9-c501-4e82-be47-3e4bd2df205f/image.png" alt=""></p>
<blockquote>
<p>경고가 떳다
&quot;인프라 전체를 종합적으로 보지 않고, 특정 리소스를 찝어서 처리하고 있다&quot;
&quot;권장하지 않는다&quot;</p>
</blockquote>
<p>즉, 해당 방식도 위험이 따른다. 저걸 삭제 했을때 어떤 영향이 일어날지 미지수 이기 때문이다.</p>
<blockquote>
<p>그래도 한번 해보자</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/little_tail/post/63d294b9-c501-4e82-be47-3e4bd2df205f/image.png" alt=""></p>
<p>이것도 마찬가지로 잘 지워진 모습이다.</p>
<h3 id="그렇다면-어떻게-해야-안전하게-할까">그렇다면 어떻게 해야 안전하게 할까?</h3>
<blockquote>
<p>바로, 코드로 지우거나, 주석 처리하는 것</p>
</blockquote>
<pre><code class="language-bash"># resource &quot;aws_s3_bucket&quot; &quot;example&quot; {
#   # 기존 방식: 버킷 이름을 코드에 직접 하드코딩
#   # 이 방식은 간단하지만, 값 변경 시 코드 자체를 수정해야 해서 재사용성이 떨어집니다.
#   # bucket = &quot;littletale-terraform-study-20260415-01&quot;

#   # 현재 방식: 변수(variable)로 버킷 이름을 받아서 사용
#   # variables.tf 에서 선언한 bucket_name 값을 참조합니다.
#   bucket = var.bucket_name

#   # 태그는 key-value 형태로 작성
#   tags = {
#     Name        = &quot;littletale-study-bucket&quot; # 버킷의 이름
#     Environment = &quot;dev&quot;                     # 개발 환경
#     Project     = &quot;terraform-study&quot;         # 프로젝트 이름
#     Owner       = &quot;jaehyungkim&quot;             # 소유자
#   }
# }




# output &quot;bucket_arn&quot; {
#   description = &quot;ARN of the S3 bucket&quot;
#   value       = aws_s3_bucket.example.arn
# }

# output &quot;bucket_name&quot; {
#   description = &quot;Name of the S3 bucket&quot;
#   value       = aws_s3_bucket.example.bucket
# }</code></pre>
<p>plan, apply 돌리면 또 잘 지워져 나온다.</p>
<h4 id="지금까지의-과정들">지금까지의 과정들</h4>
<ul>
<li>provider 설정</li>
<li>resource 생성</li>
<li>variable 분리</li>
<li>tags 추가</li>
<li>plan으로 변경 확인</li>
<li>apply로 반영</li>
<li>destroy로 삭제</li>
</ul>
<h1 id="state">State</h1>
<blockquote>
<p>상태 
Terraform 에서도 상태라는 개념이 존재하다.
<strong>Terraform이 관리하는 실제 인프라의 현재 상태를 기록해 둔 파일</strong></p>
</blockquote>
<ul>
<li><code>plan</code>에서 무엇을 만들지 예측</li>
<li><code>apply</code>에서 실제 반영</li>
<li><code>destroy</code>에서 무엇을 지울지 판단</li>
<li>다음 <code>plan</code>에서 변경점을 비교</li>
</ul>
<blockquote>
<p>이 들이 가능한 이유가 <code>state</code> 가 있기 때문</p>
</blockquote>
<ul>
<li><code>*.tf 파일</code> = 내가 원하는 목표 상태</li>
<li><code>terraform.tfstate</code> = Terraform이 알고 있는 현재 상태</li>
</ul>
<blockquote>
<p>이 두개의 값을 통해 Terraform은 판단함</p>
</blockquote>
<h2 id="what-if">What If?</h2>
<p>예를 들어 지금 네가 이런 코드를 썼다고 하자</p>
<pre><code class="language-bash">resource &quot;aws_s3_bucket&quot; &quot;example&quot; {
  bucket = var.bucket_name
}</code></pre>
<blockquote>
<p>처음엔 state에 아무것도 없을 수 있음
그러면 Terraform은 이렇게 판단</p>
</blockquote>
<ul>
<li>코드에는 버킷이 있어야 한다고 적혀 있음</li>
<li>그런데 state에는 없음 </li>
<li>그럼 새로 만들어야겠다</li>
</ul>
<h2 id="무슨-상태값들을">무슨 상태값들을?</h2>
<ul>
<li>Terraform 버전</li>
<li>리소스 목록</li>
<li>각 리소스의 실제 ID</li>
<li>속성값</li>
<li>output 값</li>
</ul>
<h2 id="조심할-점">조심할 점.</h2>
<blockquote>
<p>state에는 리소스 정보가 많이 들어간다.
민감한 정보도 들어있다.</p>
</blockquote>
<ul>
<li>git에 올리지 않기</li>
<li>팀이 공유 가능한 원격 backend 사용</li>
<li>lock 고려</li>
<li>접근 권한 고려</li>
</ul>
<h3 id="공유하지-말라는건가---x-공유해야한다">공유하지 말라는건가? -&gt; X 공유해야한다.</h3>
<blockquote>
<p>terraform.tfstate를 각자 노트북에 따로 들고 있으면 문제가 발생</p>
</blockquote>
<ul>
<li>A가 apply 해서 실제 AWS는 바뀜</li>
<li>그런데 B 로컬 state는 예전 상태</li>
<li>그러면 B가 plan/apply 할 때 잘못된 판단 가능</li>
<li>동시에 작업하면 state 충돌도 남</li>
</ul>
<blockquote>
<p>하나의 서버에 State를 넣어 관리하는게 정석</p>
</blockquote>
<h3 id="동시-접근은">동시 접근은?</h3>
<blockquote>
<p>한 사람이 먼저 state lock을 잡으면
다른 사람의 작업은 대기하거나 실패 하게</p>
</blockquote>
<h1 id="aws-기존에-있던걸-terraform-이-알아서-인식할까">AWS 기존에 있던걸 Terraform 이 알아서 인식할까?</h1>
<blockquote>
<p>놀랍게도 그건 무리다.
기존 AWS 리소스가 있어도 state에 없으면 Terraform은 
“내가 관리 중인 리소스”로 인식하지 않는다.</p>
</blockquote>
<p>이때 선택지는 보통 2개</p>
<ol>
<li><p>새로 만들 수 있는 건 Terraform으로 새로 구축
기존 것을 건드리지 않고 Terraform 관리 대상은 새로 만든 것부터 시작</p>
</li>
<li><p>기존 리소스를 Terraform state에 편입 ( import )</p>
<ol>
<li>예를들어 aws애 있던 resource들은 하나 하나 적어서 반영하기</li>
</ol>
</li>
</ol>
<h2 id="시도해보기---import">시도해보기 - import</h2>
<ol>
<li>테라폼 상태 확인해보기</li>
</ol>
<pre><code class="language-bash">terraform state list
aws_s3_bucket.example</code></pre>
<ol>
<li><p>AWS 콘솔에서 기존에 없던 S3 버킷 littletale-terraform-study-20260416-01를 만듬</p>
</li>
<li><p>Terraform은 이 버킷을 자동으로 알지 못한다
그래서 <code>plan</code>만으로는 “기존 버킷을 가져오기”가 아니라 “새로 만들기”로 이해할 수 있다</p>
</li>
<li><p>기존 AWS 리소스를 Terraform이 관리하게 만들려면 <code>import</code>가 필요하다</p>
</li>
</ol>
<pre><code class="language-bash">terraform import aws_s3_bucket.imported littletale-terraform-study-20260416-01</code></pre>
<ul>
<li><code>aws_s3_bucket.imported</code>: Terraform 코드 안의 리소스 주소</li>
<li><code>littletale-terraform-study-20260416-01</code> : AWS에 이미 존재하는 실제 버킷 이름</li>
</ul>
<blockquote>
<p>“이미 AWS에 있는 이 버킷을, Terraform의 aws_s3_bucket.imported 리소스로 등록해라”</p>
</blockquote>
<p>처음엔 aws_s3_bucket.example로 import를 시도했는데, 
그 주소는 이미 다른 버킷을 관리 중이어서 충돌이 났었다.</p>
<p><img src="https://velog.velcdn.com/images/little_tail/post/1b52f969-8e23-46ce-b8d5-ba1161750f1e/image.png" alt=""></p>
<h2 id="import-후-해야할-일">import 후 해야할 일</h2>
<blockquote>
<p>바로 실제 AWS와 상태가 일치 하는가 이다.</p>
</blockquote>
<pre><code class="language-bash"> terraform plan                                                                
aws_s3_bucket.example: Refreshing state... [id=littletale-terraform-study-20260415-01]
aws_s3_bucket.imported: Refreshing state... [id=littletale-terraform-study-20260416-01]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.</code></pre>
<p>현재는 간단히 하고 있어서 바꿀게 없다고 뜨지만</p>
<ul>
<li>0 to add인지</li>
<li>0 to change인지</li>
<li>아니면 태그나 속성 차이 때문에 1 to change가 나오는지 등</li>
</ul>
<p>이러한 부분들을 신경써야 한다.</p>
<h1 id="localstf">Locals.tf</h1>
<blockquote>
<p>현재는 태그가 리소스마다 직접 박혀 있음
이런 값들을 local로 묶어서 재사용해야함 Common 느낌</p>
</blockquote>
<p>현재</p>
<pre><code class="language-hcl">tags = {
  Name        = &quot;littletale-study-bucket&quot;
  Environment = &quot;dev&quot;
  Project     = &quot;terraform-study&quot;
  Owner       = &quot;jaehyungkim&quot;
}</code></pre>
<p>locals.tf</p>
<pre><code class="language-hcl">locals {
  common_tags = {
    Environment = &quot;dev&quot;             # 개발 환경
    Project     = &quot;terraform-study&quot; # 프로젝트 이름
    Owner       = &quot;jaehyungkim&quot;     # 소유자
  }
}</code></pre>
<p>main.tf</p>
<pre><code class="language-hcl"># 로컬로 따로 뺴서 연결
tags = merge(local.common_tags, {
    Name = &quot;littletale-study-bucket&quot;
})</code></pre>
<h3 id="단순-코드-가독성-때문일까">단순 코드 가독성 때문일까?</h3>
<blockquote>
<p>그렇지 않다.</p>
</blockquote>
<ul>
<li>코드 가독성 (중복 제거)</li>
<li>재사용성</li>
<li><strong>통제성</strong> -&gt; 변경 범위가 줄고, 범위 예측이 가능</li>
</ul>
<h3 id="variable과-local의-차이가-뭔데">variable과 local의 차이가 뭔데?</h3>
<ul>
<li><p>외부에서 주입받아야 하면 variable</p>
</li>
<li><p>코드 내부에서 정해도 되면 local</p>
</li>
<li><p>재사용성이 있는가 없는가로 생각해보자</p>
</li>
</ul>
<pre><code class="language-hcl">variable &quot;bucket_name&quot; {
  description = &quot;S3 bucket name&quot;
  type = string
}

variable &quot;bucket_name2&quot; {
  description = &quot;S3 bucket name&quot;
  type = string
}</code></pre>
<p>버킷 이름을 변수로 두는게 맞는지 고민해보자</p>
<ul>
<li>버킷 이름은 환경마다 달라질 수 있음</li>
<li>전역 유일해야 해서 매번 바뀔 수도 있음</li>
<li>코드에 박아두면 재사용성이 떨어짐</li>
</ul>
<pre><code class="language-hcl">bucket_name = &quot;littletale-terraform-study-20260415-01&quot;
bucket_name2 = &quot;littletale-terraform-study-20260416-01&quot;</code></pre>
<p>즉 변수로 두는게 맞다</p>
<pre><code class="language-hcl">locals {
  common_tags = {
    Environment = &quot;dev&quot;             # 개발 환경
    Project     = &quot;terraform-study&quot; # 프로젝트 이름
    Owner       = &quot;jaehyungkim&quot;     # 소유자
  }
}</code></pre>
<p>Environment = &quot;dev&quot; 이값도 변수로 빼는게 맞아 보인다.</p>
<pre><code class="language-hcl">locals {
  common_tags = {
    Environment = var.environment   # 개발 환경
    Project     = &quot;terraform-study&quot; # 프로젝트 이름
    Owner       = &quot;jaehyungkim&quot;     # 소유자
  }
}</code></pre>
<blockquote>
<p>다음과 같이 코드를 변경 하였다.</p>
</blockquote>
<h1 id="outputstf">Outputs.tf</h1>
<p>위에서 잠깐 다루었지만 이번에 좀 더 다루어 보자.</p>
<h2 id="arn">ARN</h2>
<blockquote>
<p><code>Amazon Resource Name</code> 의 약자</p>
</blockquote>
<ul>
<li><p>AWS 리소스를 식별하는 주소</p>
</li>
<li><p>사람에겐 버킷 이름이</p>
</li>
<li><p>AWS 시스템에선 ARN 같은 정확한 참조값 ex) 식별자
ex) <code>arn:aws:s3:::littletale-terraform-study-20260415-01</code></p>
</li>
</ul>
<ul>
<li>arn: → ARN 형식 시작</li>
<li>aws: → AWS</li>
<li>s3: → S3 서비스</li>
<li>::~ → 리소스 이름</li>
</ul>
<h2 id="output">output</h2>
<blockquote>
<p><code>terraform이 작업을 끝낸 뒤, 밖으로 보여줄 값</code>을 정의하는 파일</p>
</blockquote>
<p>만약 버킷을 만들었다면 Terraform은 버킷의 관련 값들을 알고 있음</p>
<ul>
<li>버킷 이름</li>
<li>버킷 ARN</li>
<li>도메인 이름 등</li>
</ul>
<p>→ 그중 <code>중요한 값</code>을 <code>output으로 선언</code>
→ <code>apply</code> or <code>terraform output</code> 명령으로 확인 가능</p>
<p><code>“다른 코드/모듈이 이 값을 쓰게 만드는 인터페이스”</code></p>
<h3 id="그럼-지금-상태에서-추가한-리소스에-관한-output을-짠다면-어떻게-할까">그럼 지금 상태에서 추가한 리소스에 관한 output을 짠다면 어떻게 할까?</h3>
<pre><code class="language-hcl">resource &quot;aws_s3_bucket&quot; &quot;imported&quot; {
  bucket = var.bucket_name2

  tags = merge(local.common_tags, {
    Name = &quot;littletale-imported-bucket&quot;
  })
}

output &quot;imported_bucket_name&quot; {
  description = &quot;Name of the imported S3 bucket&quot;
  value       = aws_s3_bucket.imported.bucket
}

output &quot;imported_bucket_arn&quot; {
  description = &quot;ARN of the imported S3 bucket&quot;
  value       = aws_s3_bucket.imported.arn
}</code></pre>
<h1 id="versioning">versioning</h1>
<blockquote>
<p>aws s3 버킷의 버전관리 기능
마치 npm, fvm, spm 등 과 같은 느낌으로 생각하면 된다.</p>
</blockquote>
<ul>
<li>ex) 이미지들어있음 -&gt; 수정함 -&gt; 버전별로 관리해서 이전 이미지도 아는</li>
</ul>
<h2 id="중요한-포인트">중요한 포인트</h2>
<blockquote>
<p>버킷 안에서 관리 X -&gt; 별도 리소스로 다루기</p>
</blockquote>
<ul>
<li>aws_s3_bucket
버킷 자체</li>
<li>aws_s3_bucket_versioning
버킷의 versioning 설정</li>
</ul>
<p>버킷 차제</p>
<pre><code class="language-hcl">resource &quot;aws_s3_bucket&quot; &quot;imported&quot; {
  ...
}</code></pre>
<p>버킷에 Versioning 적용</p>
<pre><code class="language-hcl">resource &quot;aws_s3_bucket_versioning&quot; &quot;imported&quot; {
  bucket = aws_s3_bucket.imported.id

  versioning_configuration {
    status = &quot;Enabled&quot;
  }
}</code></pre>
<blockquote>
<p>오해 금물 : 버킷을 새로 하나 더파서 하는 개념 X
-&gt; 기존것에 설정을 추가한것임
즉 리소스는 추가 O 버킷추가? X</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/little_tail/post/23f384b5-7571-47f4-ae1b-66f8a0034458/image.png" alt=""></p>
<p>사진과 같이 버전 관리가 활성화 된 모습이다.</p>
<h1 id="public-acess-block">public acess block</h1>
<blockquote>
<p>이번엔 Versioning 이랑 비슷하지만 다른 거다.
목적은 <code>보안</code> 이다.</p>
</blockquote>
<pre><code class="language-hcl"># S3 버킷 public access block 설정 리소스입니다.
resource &quot;aws_s3_bucket_public_access_block&quot; &quot;imported&quot; {
  bucket = aws_s3_bucket.imported.id

  # 버킷 퍼블릭 액세스 차단 설정
  block_public_acls       = true # 퍼블릭 ACL 차단
  block_public_policy     = true # 퍼블릭 정책 차단
  ignore_public_acls      = true # 퍼블릭 ACL 무시
  restrict_public_buckets = true # 퍼블릭 버킷 제한
}</code></pre>
<h2 id="s3에서-공개-여부를-제어하는-대표수단">S3에서 공개 여부를 제어하는 대표수단</h2>
<ul>
<li>ACL</li>
<li>Bucket Policy</li>
</ul>
<h3 id="acl---access-control-list">ACL - Access Control List</h3>
<ul>
<li>private</li>
<li>public-read</li>
<li>public-read-write</li>
</ul>
<p>ex) 어떤 객체에 public-read -&gt; 인터넷 누구나 읽을 수 있음</p>
<h3 id="bucket-policy">Bucket Policy</h3>
<blockquote>
<p>버킷에 붙는 <code>Json</code>
ACL보다 더 구조적, 세밀하게 권한 제어</p>
</blockquote>
<ul>
<li>특정 IAM 사용자만 접근 허용</li>
<li>특정 VPC에서만 접근 허용</li>
<li>특정 prefix 경로만 읽기 허용</li>
<li>모든 사람(*)에게 읽기 허용</li>
</ul>
<h3 id="간단-정리">간단 정리</h3>
<p>ACL = 리소스 권한 
Bucket Policy = 버킷 권한 규칙 문서</p>
<pre><code class="language-bash">  block_public_acls       = true # 퍼블릭 ACL 차단
  -&gt; 버킷에 퍼블릭 AWS가 ACL 설정 차단

  block_public_policy     = true # 퍼블릭 정책 차단
  /*
      Principal *
    Action s3:GetObject
    Resource 버킷 객체 전체

    이런 퍼블릭 버킷 정책을 붙이려는 시도를 막음
  */

  ignore_public_acls      = true # 퍼블릭 ACL 무시
  -&gt; 이미 퍼블릭 ACL이 있더라도, 그 ACL을 무시함

  restrict_public_buckets = true # 퍼블릭 버킷 제한</code></pre>
<p><a href="https://docs.aws.amazon.com/sdk-for-swift/latest/api/awss3/documentation/awss3/s3clienttypes/publicaccessblockconfiguration/restrictpublicbuckets/">참고링크: restrict_public_buckets</a></p>
<ul>
<li>Amazon S3에서 이 버킷에 대한 공개 버킷 정책을 제한할지 여부입니다. 기본값은 false</li>
<li>이 설정을 활성화해도 이전에 저장된 버킷 정책에는 영향을 미치지 않지만, 
공개 버킷 정책 내의 공개 및 계정 간 액세스(특정 계정에 대한 비공개 위임 포함)는 차단됩니다.</li>
</ul>
<p><a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block">문서: aws_s3_bucket_public_access_block</a></p>
<p><img src="https://velog.velcdn.com/images/little_tail/post/0098f5cf-af54-4802-917e-9bc4fd7e97b3/image.png" alt=""></p>
<h3 id="경로는-하나가-아니다">경로는 하나가 아니다.</h3>
<p>위에서 4개의 값을 true 둔 것을 이해하면 알 듯이
S3가 공개되는 경로가 하나가 아니다.</p>
<ul>
<li>ACL 로 공개</li>
<li>Bucket Policy</li>
<li>이전 설정값에 의한</li>
</ul>
<hr>
<h1 id="data-source">Data source</h1>
<blockquote>
<p>테라폼이 기존에 있는 <code>정보</code> <code>리소스</code> <code>조회만</code> 하는 방법</p>
</blockquote>
<ul>
<li>resource = 만들고 관리함</li>
<li>data = 읽기만 함</li>
</ul>
<p>다시 정리하면</p>
<blockquote>
<p>지금 Terraform이 어떤 AWS 계정/사용자로 인증되어 있는지 그 정보를 읽어오는 것</p>
</blockquote>
<blockquote>
<p>** “Terraform이 만들지는 않지만, 알아야 하는 정보를 읽어오는 장치” **
<strong>&quot;기존 AWS 정보도 Terraform에서 참조할 수 있다&quot;</strong></p>
</blockquote>
<p>main.tf</p>
<pre><code class="language-hcl"># 데이터 소스
# 현재 로그인된 AWS 계정 정보를 가져옵니다.
data &quot;aws_caller_identity&quot; &quot;current&quot; {}
# 현재 사용 중인 AWS 리전을 가져옵니다.
data &quot;aws_region&quot; &quot;current&quot; {}</code></pre>
<p>output.tf</p>
<pre><code class="language-hcl"># 데이터 소스 출력값
output &quot;current_account_id&quot; {
  description = &quot;AWS account ID currently used by Terraform&quot;
  value       = data.aws_caller_identity.current.account_id
}

# 데이터 소스 출력값
output &quot;current_region&quot; {
  description = &quot;AWS region currently used by Terraform&quot;
  value       = data.aws_region.current.name
}</code></pre>
<p>Plan 출력 값</p>
<pre><code class="language-hcl">data.aws_caller_identity.current: Reading...
data.aws_region.current: Reading...
data.aws_region.current: Read complete after 0s [id=ap-northeast-2]
data.aws_caller_identity.current: Read complete after 0s [id=xxxxxxxxxx]</code></pre>
<p>locals.tf</p>
<pre><code>locals {
  common_tags = {
    Environment = var.environment   # 개발 환경
    Project     = &quot;terraform-study&quot; # 프로젝트 이름
    Owner       = &quot;jaehyungkim&quot;     # 소유자
    Region      = data.aws_region.current.name
  }
}</code></pre><p>위와 같이 적용해보고 plan</p>
<pre><code class="language-hcl"> ...
 + &quot;Region&quot;      = &quot;ap-northeast-2&quot;
 ...
 + &quot;Region&quot;      = &quot;ap-northeast-2&quot;
...
Plan: 0 to add, 2 to change, 0 to destroy.</code></pre>
<p><img src="https://velog.velcdn.com/images/little_tail/post/0a37093c-7071-4573-9a7f-bef1c23fc4c4/image.png" alt=""></p>
<p>잘 반영 된 것을 확인 할 수가 있습니다.</p>
<h1 id="잠깐-정리하기">잠깐 정리하기</h1>
<ul>
<li>provider</li>
<li>resource</li>
<li>variable</li>
<li>local</li>
<li>output</li>
<li>data</li>
<li>import</li>
<li>plan/apply/destroy</li>
</ul>
<h1 id="module">Module</h1>
<blockquote>
<p>프로그래밍을 해본사람들은 지겹게 들은 단어 모듈
즉, 재사용 단위로 묶는것을 뜻함</p>
</blockquote>
<h2 id="상황">상황</h2>
<blockquote>
<p>예를들어 아래와 같이 상황을 두어보자</p>
</blockquote>
<ul>
<li>버킷 여러 개</li>
<li>VPC 여러 개</li>
<li>환경 여러 개</li>
<li>프로젝트 여러 개</li>
</ul>
<blockquote>
<p>이때 매번 코드를 새로 쓰면
중복, 실수 가능성 이 커짐</p>
</blockquote>
<p>-&gt; 공통 패턴을 모듈로 묶자</p>
<h2 id="s3-구성법을-모듈화-하자">S3 구성법을 모듈화 하자</h2>
<p>main.tf</p>
<pre><code class="language-hcl"># 이 모듈 안에서 공통으로 쓰는 태그 맵을 만듭니다.
# 루트 모듈에서 받은 environment, owner, project_name, region 값을 한 번 묶어서
# 여러 리소스에서 반복 사용하기 쉽게 정리합니다.
locals {
  common_tags = {
    # 배포 환경 구분용 태그입니다.
    Environment = var.environment

    # 리소스 소유자 식별용 태그입니다.
    Owner       = var.owner

    # 프로젝트 식별용 태그입니다.
    Project     = var.project_name

    # 현재 AWS 리전 태그입니다.
    Region      = var.region
  }
}

# 이 모듈의 핵심 리소스인 S3 버킷 본체입니다.
resource &quot;aws_s3_bucket&quot; &quot;this&quot; {
  # 루트 모듈에서 전달받은 실제 버킷 이름을 그대로 사용합니다.
  bucket = var.bucket_name

  # 공통 태그(local.common_tags)에 버킷별 Name 태그를 추가해서 최종 tags 맵을 만듭니다.
  # 이렇게 하면 Environment/Owner/Project/Region은 공통 유지,
  # Name만 버킷마다 다르게 줄 수 있습니다.
  tags = merge(local.common_tags, {
    Name = var.name_tag
  })
}

# versioning 설정은 선택적으로 생성합니다.
# enable_versioning이 true면 count = 1이 되어 리소스가 생성되고,
# false면 count = 0이 되어 이 설정 리소스가 아예 생기지 않습니다.
resource &quot;aws_s3_bucket_versioning&quot; &quot;this&quot; {
  # 필요할 때만 리소스를 생성하기 위한 count 조건입니다.
  count = var.enable_versioning ? 1 : 0

  # versioning 설정이 적용될 대상 버킷 ID입니다.
  bucket = aws_s3_bucket.this.id

  versioning_configuration {
    # 객체를 덮어써도 이전 버전을 남기도록 버전 관리를 활성화합니다.
    status = &quot;Enabled&quot;
  }
}</code></pre>
<p>outputs.tf</p>
<pre><code class="language-hcl"># 이 파일은 모듈 내부에서 만든 버킷의 주요 값을 모듈 밖으로 내보냅니다.
# 루트 모듈은 이 output들을 받아 다시 자신의 output으로 노출할 수 있습니다.

# 이 모듈이 만든 버킷의 ARN입니다.
output &quot;bucket_arn&quot; {
  description = &quot;ARN of the S3 bucket&quot;
  value       = aws_s3_bucket.this.arn
}

# 이 모듈이 만든 버킷의 실제 이름입니다.
output &quot;bucket_name&quot; {
  description = &quot;Name of the S3 bucket&quot;
  value       = aws_s3_bucket.this.bucket
}</code></pre>
<p>valiable.tf</p>
<pre><code class="language-hcl"># 이 파일은 s3_bucket 모듈이 루트 모듈로부터 받아야 하는 입력값들을 정의합니다.

# 실제 S3 버킷 이름입니다.
variable &quot;bucket_name&quot; {
  description = &quot;Name of the S3 bucket&quot;
  type        = string
}

# versioning 설정을 생성할지 여부입니다.
variable &quot;enable_versioning&quot; {
  description = &quot;Whether to enable S3 bucket versioning&quot;
  type        = bool
  default     = false
}

# 공통 태그에 들어갈 배포 환경값입니다.
variable &quot;environment&quot; {
  description = &quot;Deployment environment&quot;
  type        = string
}

# 공통 태그와 별도로 버킷별 Name 태그에 들어갈 값입니다.
variable &quot;name_tag&quot; {
  description = &quot;Value for the Name tag&quot;
  type        = string
}

# 공통 태그에 들어갈 Owner 값입니다.
variable &quot;owner&quot; {
  description = &quot;Owner tag value&quot;
  type        = string
}

# 공통 태그에 들어갈 Project 값입니다.
variable &quot;project_name&quot; {
  description = &quot;Project tag value&quot;
  type        = string
}

# 공통 태그에 들어갈 AWS 리전 값입니다.
variable &quot;region&quot; {
  description = &quot;AWS region tag value&quot;
  type        = string
}</code></pre>
<p><img src="https://velog.velcdn.com/images/little_tail/post/cf802183-abca-4bc2-bb1e-a8c4538bc577/image.png" alt=""></p>
<p>위 사진과 같이 모듈화를 해놓았다</p>
<h4 id="maintf-변경점-1-module">main.tf 변경점 1. module</h4>
<pre><code class="language-hcl"># example 버킷용 S3 모듈 호출입니다.
# 루트 모듈은 &quot;어떤 값을 넘길지&quot;만 결정하고,
# 실제 버킷 리소스, 태그 조합, versioning 구현은 modules/s3_bucket 안에 둡니다.
module &quot;example&quot; {
  # 이 모듈 코드가 있는 로컬 경로입니다.
  source = &quot;./modules/s3_bucket&quot;

  # 실제 S3 버킷 이름입니다.
  bucket_name = var.bucket_name

  # example 버킷은 versioning을 켜지 않도록 false를 전달합니다.
  enable_versioning = false

  # 공통 태그에 들어갈 환경값입니다.
  environment = var.environment

  # 버킷별 Name 태그 값입니다.
  name_tag = &quot;littletale-study-bucket&quot;

  # 공통 태그에 들어갈 Owner 값입니다.
  owner = var.owner

  # 공통 태그에 들어갈 Project 값입니다.
  project_name = var.project_name

  # data source로 읽어온 현재 AWS 리전을 태그 값으로 넘깁니다.
  region = data.aws_region.current.name
}

# imported 버킷용 S3 모듈 호출입니다.
# 이 모듈은 example 모듈과 같은 구현을 재사용하지만,
# versioning을 켜고 Name 태그만 다르게 넘겨서 동작 차이를 만듭니다.
module &quot;imported&quot; {
  # 같은 모듈을 재사용합니다.
  source = &quot;./modules/s3_bucket&quot;

  # imported 버킷의 실제 S3 이름입니다.
  bucket_name = var.bucket_name2

  # imported 버킷은 versioning을 활성화하도록 true를 전달합니다.
  enable_versioning = true

  # 공통 태그에 들어갈 환경값은 example과 동일하게 사용합니다.
  environment = var.environment

  # imported 버킷 전용 Name 태그 값입니다.
  name_tag = &quot;littletale-imported-bucket&quot;

  # 공통 태그에 들어갈 Owner 값입니다.
  owner = var.owner

  # 공통 태그에 들어갈 Project 값입니다.
  project_name = var.project_name

  # data source로 읽어온 현재 AWS 리전을 태그 값으로 넘깁니다.
  region = data.aws_region.current.name
}</code></pre>
<blockquote>
<p>위에서 재사용 가능한 방식으로 변경 했었기 때문에
그들을 사용하기 위해 <code>resource</code> -&gt; <code>module</code> 로 변경하였다.</p>
</blockquote>
<h4 id="maintf-변경점-2-moved">main.tf 변경점 2. moved</h4>
<blockquote>
<p>테라폼이 갖고 있는 즉 state 에서 이미 예전 리소스를 물고 있기 때문에
새로 만든 모듈 것을 쓰도록 옮기기 위해서</p>
</blockquote>
<p>main.tf</p>
<pre><code class="language-hcl"># 예전에 루트 모듈에서 직접 관리하던 example 버킷을,
# 이제는 module.example 안의 aws_s3_bucket.this로 옮겼다고 Terraform state에 알려줍니다.
# 이 선언이 없으면 Terraform은 &quot;기존 리소스를 삭제하고 module 안 리소스를 새로 만든다&quot;고 오해할 수 있습니다.
moved {
  from = aws_s3_bucket.example
  to   = module.example.aws_s3_bucket.this
}

# 예전에 루트 모듈에서 직접 관리하던 imported 버킷을,
# 이제는 module.imported 안의 aws_s3_bucket.this로 옮겼다고 Terraform state에 알려줍니다.
moved {
  from = aws_s3_bucket.imported
  to   = module.imported.aws_s3_bucket.this
}

# imported 버킷의 versioning 설정도 루트 리소스에서 module 내부 리소스로 주소를 이전합니다.
# count가 켜져 있으므로 module 쪽 주소는 this[0] 형태가 됩니다.
moved {
  from = aws_s3_bucket_versioning.imported
  to   = module.imported.aws_s3_bucket_versioning.this[0]
}</code></pre>
<p>outputs.tf</p>
<pre><code class="language-hcl"># 루트 모듈에서 확인하고 싶은 값들을 밖으로 노출하는 파일입니다.
# 각 output은 module 내부 리소스를 직접 보는 대신, module이 내보낸 값을 다시 노출합니다.

# example 모듈이 만든 버킷의 ARN입니다.
output &quot;bucket_arn&quot; {
  description = &quot;ARN of the S3 bucket&quot;
  value       = module.example.bucket_arn
}

# example 모듈이 만든 버킷의 실제 이름입니다.
output &quot;bucket_name&quot; {
  description = &quot;Name of the S3 bucket&quot;
  value       = module.example.bucket_name
}

# imported 모듈이 관리하는 버킷의 ARN입니다.
output &quot;imported_bucket_arn&quot; {
  description = &quot;ARN of the imported S3 bucket&quot;
  value       = module.imported.bucket_arn
}

# imported 모듈이 관리하는 버킷의 실제 이름입니다.
output &quot;imported_bucket_name&quot; {
  description = &quot;Name of the imported S3 bucket&quot;
  value       = module.imported.bucket_name
}

# 현재 Terraform이 인증된 AWS 계정 ID입니다.
# data source에서 읽은 값을 output으로 확인할 수 있게 노출합니다.
output &quot;current_account_id&quot; {
  description = &quot;AWS account ID currently used by Terraform&quot;
  value       = data.aws_caller_identity.current.account_id
}

# 현재 provider가 사용 중인 AWS 리전입니다.
# data source에서 읽은 값을 output으로 확인할 수 있게 노출합니다.
output &quot;current_region&quot; {
  description = &quot;AWS region currently used by Terraform&quot;
  value       = data.aws_region.current.name
}</code></pre>
<blockquote>
<p>outputs 에서도 이젠 모듈에 있는 값을 쓸 수 있도록 변경</p>
</blockquote>
<p>variables.tf</p>
<pre><code class="language-hcl"># 루트 모듈에서 관리하는 첫 번째 버킷의 실제 이름입니다.
# 전역 유일해야 하므로 보통 terraform.tfvars에서 환경별로 다르게 넣습니다.
variable &quot;bucket_name&quot; {
  # 이 값이 어떤 의미인지 Terraform 문서/에러 메시지에 함께 보여줍니다.
  description = &quot;S3 bucket name for the example bucket&quot;

  # S3 버킷 이름은 문자열이므로 string 타입을 사용합니다.
  type = string
}

# 루트 모듈에서 관리하는 두 번째 버킷(imported)의 실제 이름입니다.
variable &quot;bucket_name2&quot; {
  # 이 값이 어떤 의미인지 Terraform 문서/에러 메시지에 함께 보여줍니다.
  description = &quot;S3 bucket name for the imported bucket&quot;

  # S3 버킷 이름은 문자열이므로 string 타입을 사용합니다.
  type = string
}

# 공통 태그에 넣을 배포 환경값입니다.
# 예: dev, staging, prod
variable &quot;environment&quot; {
  description = &quot;Deployment environment&quot;
  type        = string
}

# 공통 태그에 넣을 Owner 값입니다.
# 이 프로젝트에서는 버킷 둘 다 같은 소유자 태그를 사용합니다.
variable &quot;owner&quot; {
  description = &quot;Owner tag value&quot;
  type        = string
}

# 공통 태그에 넣을 Project 값입니다.
# 이 프로젝트에서는 버킷 둘 다 같은 프로젝트 태그를 사용합니다.
variable &quot;project_name&quot; {
  description = &quot;Project tag value&quot;
  type        = string
}</code></pre>
<blockquote>
<p>위 같이 모듈화를 진행 하였다. 상당히 난이도가 복잡한 이야기 들이다.</p>
</blockquote>
<h1 id="backend">Backend</h1>
<blockquote>
<p>프론트엔드, 백엔드 할때 그 백엔드.....가 아니다.
<strong>Terraform state를 어디에 저장할지 정하는 저장 방식</strong> </p>
</blockquote>
<h2 id="provider-vs-backend">Provider Vs Backend</h2>
<ul>
<li>provider = Terraform이 어느 서비스와 통신할지 선택</li>
<li>backend = 어디에 저장할지 (state 를 어디에?)</li>
</ul>
<h2 id="backend의-중요성">BackEnd의 중요성</h2>
<blockquote>
<p>본인이 가진 state와 동료의 state가 불일치</p>
</blockquote>
<ul>
<li>state를 S3에 저장</li>
<li>lock은 DynamoDB로 분리 관리</li>
</ul>
<pre><code class="language-hcl"># state를 팀원과 공유 목적 (Backend)
terraform {
  backend &quot;s3&quot; {
    bucket = &quot;terraform-state-bucket&quot;
    key    = &quot;study/terraform.tfstate&quot;
    region = data.aws_region.current.name
  }
}</code></pre>
<p>이렇게 작성해보았었다.
다만 지적 사항이 존재한다.</p>
<ol>
<li><code>data.aws_region.current.name를 쓰면 안 됩니다.</code></li>
</ol>
<p>이유는 좀 재미있다.</p>
<ul>
<li>backend는 Terraform이 data source를 읽기 전에 먼저 초기화</li>
</ul>
<ol start="2">
<li>bucket = &quot;terraform-state-bucket&quot;는 실제로 존재하는 S3 버킷이어야</li>
</ol>
<ul>
<li>생성하는게 아니기 때문에 존재 하여야만 한다.</li>
</ul>
<pre><code class="language-hcl"> backend &quot;s3&quot; {
    bucket = &quot;terraform-state-bucket&quot;
    key    = &quot;study/terraform.tfstate&quot;
    region = &quot;ap-northeast-2&quot;
  }</code></pre>
<p>다음과 같이 하드하게 작성하도록 수정하였다.</p>
<blockquote>
<p>2번째 문제를 해결하고자 resouce를 선언하면 어떻게될까 고민을 해보았다.
결론은 안된다.</p>
</blockquote>
<ol>
<li>Terraform 시작</li>
<li>먼저 backend 초기화 필요</li>
<li>state를 어디서 읽고 쓸지 결정</li>
<li>그다음에야 리소스들을 해석하고 plan/apply 가능</li>
</ol>
<blockquote>
<p>그럼 이 문제를 해결하려면 단순히 aws에 직접 생성하면 될까?</p>
</blockquote>
<ul>
<li>빠르게 해결은 됨 다만 유지보수 애매</li>
</ul>
<p>-&gt; backend 전용 Terraform을 구성하면 될듯</p>
<blockquote>
<p>Backend 프로젝트 따로 만들고 연결</p>
</blockquote>
<pre><code class="language-hcl"># Terraform state를 로컬 파일 대신 S3에 저장하는 backend 설정입니다.
  # bucket은 state 파일이 저장될 S3 버킷 이름입니다.
  # key는 그 버킷 안에서 state 파일이 놓일 경로입니다.
  # region은 backend 버킷이 존재하는 AWS 리전입니다.
  #
  # 주의:
  # backend 블록은 Terraform 초기화 단계에서 먼저 읽히므로
  # data.aws_region.current.name 같은 data source 참조를 사용할 수 없습니다.
  # 따라서 region은 문자열로 직접 적어야 합니다.
  backend &quot;s3&quot; {
    bucket         = &quot;littletale-terraform-state-20260419&quot;
    key            = &quot;study/terraform.tfstate&quot;
    region         = &quot;ap-northeast-2&quot;
    dynamodb_table = &quot;terraform-state-lock&quot;
  }

  bucket: state 파일 저장 버킷
  key: 그 버킷 안의 state 경로
  region: backend 버킷 리전
  dynamodb_table: state lock 테이블</code></pre>
<blockquote>
<p>메인 프로젝트 루트에서 마이그레이션을 진행해야 함.</p>
</blockquote>
<pre><code class="language-hcl">terraform init -migrate-state</code></pre>
<p><img src="https://velog.velcdn.com/images/little_tail/post/081dbeda-b487-4022-9018-5dc869838530/image.png" alt="">
<img src="https://velog.velcdn.com/images/little_tail/post/07279ebc-9ce2-4dba-aac4-4ef8850507b3/image.png" alt=""></p>
<blockquote>
<p>위 사진 처럼 정상적으로 State에 올라간 것이 보인다</p>
</blockquote>
<p>명령어 로도 가능하다</p>
<pre><code class="language-hcl">&gt; aws s3 ls s3://littletale-terraform-state-20260419/study/
2026-04-21 10:34:31       8793 terraform.tfstate</code></pre>
<h1 id="dynamodb란">DynamoDB란?</h1>
<blockquote>
<p>AWS의 NoSQL 완전관리형 데이터 베이스</p>
</blockquote>
<ul>
<li>서버 직접 설치/운영 안 함 (AWS가 관리)</li>
<li>키-값 형태나 문서 형태처럼 빠르게 읽쓰 가능</li>
</ul>
<h1 id="dynamodb-lock">DynamoDB Lock?</h1>
<ul>
<li>지금 State 작업중인지 기록</li>
<li>lock 흭득</li>
<li>작업 끝내면 lock 해제</li>
</ul>
<p>--- 먼저 시도 ---</p>
<ol>
<li>Terraform Apply</li>
<li>DynamoDB 테이블에 내가 지금 lock 기록 시도</li>
<li>성공시 작업 진행</li>
<li>끝나면 lock 삭제</li>
</ol>
<p>--- 후에 시도 ---</p>
<ol>
<li>Terraform Apply</li>
<li>이미 lock 있어서 실패</li>
<li>대기 혹은 실패</li>
</ol>
<h1 id="환경-분리-dev-prod">환경 분리 (Dev, Prod)</h1>
<blockquote>
<p>이유는 뭐... 실제 운영중인 곳에 영향이 없어야 겠죠?</p>
</blockquote>
<p>폴더 방식과, 워크스페이스 방식이 있다. 흥미로운 사실은 폴더 방식을 주로 사용하다는것 (실수방지)
ex) </p>
<pre><code class="language-hcl">envs/
  dev/
  prod/
modules/
  s3_bucket/</code></pre>
<p><img src="https://velog.velcdn.com/images/little_tail/post/b97c527b-96fc-41c8-ad1d-d21aa33cd718/image.png" alt=""></p>
<p>사진과 같이 변경 구조를 바꾸었다.</p>
<h2 id="흐름">흐름</h2>
<h3 id="root">Root</h3>
<ul>
<li>envs/dev</li>
<li>envs/prod</li>
</ul>
<pre><code class="language-hcl">cd /Users/jaehyungkim/Desktop/Terraform/envs/dev
terraform plan</code></pre>
<h3 id="file-load">File Load</h3>
<ul>
<li>envs/dev/terraform.tf (line 1)</li>
<li>envs/dev/providers.tf (line 1)</li>
<li>envs/dev/main.tf (line 1)</li>
<li>envs/dev/locals.tf (line 1)</li>
<li>envs/dev/outputs.tf (line 1)</li>
</ul>
<h3 id="backend-load">Backend Load</h3>
<h3 id="provider-load">Provider Load</h3>
<h3 id="local-load--localstf-">local load ( locals.tf )</h3>
<ul>
<li>버킷 이름</li>
<li>환경 이름</li>
<li>owner</li>
<li>project_name</li>
<li>Name 태그</li>
<li>versioning on/off</li>
</ul>
<pre><code class="language-hcl"># dev 환경 값을 한 객체로 묶어 관리합니다.
# 이렇게 하면 env 루트가 &quot;환경 설정 파일&quot;처럼 보이고,
# 비슷한 변수 선언 파일을 환경마다 반복해서 둘 필요가 줄어듭니다.
locals {
  config = {
    bucket_name                = &quot;littletale-terraform-study-20260415-01&quot;
    imported_bucket_name       = &quot;littletale-terraform-study-20260416-01&quot;
    environment                = &quot;dev&quot;
    owner                      = &quot;jaehyungkim&quot;
    project_name               = &quot;terraform-study&quot;
    example_name_tag           = &quot;littletale-study-bucket&quot;
    imported_name_tag          = &quot;littletale-imported-bucket&quot;
    example_enable_versioning  = false
    imported_enable_versioning = true
  }
}</code></pre>
<h3 id="maintf-가-상위-환경-모듈-호출">main.tf 가 상위 환경 모듈 호출</h3>
<pre><code class="language-hcl">module &quot;environment&quot; {
  source = &quot;../../modules/s3_environment&quot;
  config = local.config
}</code></pre>
<ul>
<li>modules/s3_environment를 호출하고</li>
<li>dev용 설정 객체를 넘깁니다</li>
</ul>
<ul>
<li>현재 AWS 계정/리전 조회<pre><code class="language-hcl">data &quot;aws_caller_identity&quot; &quot;current&quot; {}
data &quot;aws_region&quot; &quot;current&quot; {}</code></pre>
</li>
</ul>
<ul>
<li><p>버킷 2개 조립</p>
<pre><code class="language-hcl">module &quot;example&quot; { ... }
module &quot;imported&quot; { ... }</code></pre>
</li>
<li><p>output 준비</p>
<pre><code class="language-hcl"># dev example 버킷의 ARN입니다.
output &quot;bucket_arn&quot; {
description = &quot;ARN of the dev example bucket&quot;
value       = module.environment.bucket_arn
}
</code></pre>
</li>
</ul>
<h1 id="dev-example-버킷의-실제-이름입니다">dev example 버킷의 실제 이름입니다.</h1>
<p>output &quot;bucket_name&quot; {
  description = &quot;Name of the dev example bucket&quot;
  value       = module.environment.bucket_name
}</p>
<h1 id="dev-imported-버킷의-arn입니다">dev imported 버킷의 ARN입니다.</h1>
<p>output &quot;imported_bucket_arn&quot; {
  description = &quot;ARN of the dev imported bucket&quot;
  value       = module.environment.imported_bucket_arn
}
...</p>
<pre><code>
## 요약 흐름

1. envs/dev/terraform.tf
   backend/state 결정
2. envs/dev/providers.tf
AWS 연결 결정
3. envs/dev/locals.tf
dev 환경값 준비
4. envs/dev/main.tf
modules/s3_environment 호출
5. modules/s3_environment
환경 단위 조립
6. modules/s3_bucket
실제 S3 버킷 리소스 구현
7. output이 다시 위로 올라와 최종 노출





### 마치며
&gt; 오우 너무 길었어요
&gt; 잘라서 가겠습니다.







</code></pre>]]></description>
        </item>
        <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>
    </channel>
</rss>