<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>김경록의 앱 개발 여정</title>
    <link>https://roks-apps.tistory.com/</link>
    <description>iOS및 Swift를 주로 다룹니다.</description>
    <language>ko</language>
    <pubDate>Thu, 9 Apr 2026 00:08:12 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Kim Roks</managingEditor>
    <item>
      <title>디자이너와 협업하기 - 반응형 UI 및 머터리얼 디자인 가이드라인(feat flutter)</title>
      <link>https://roks-apps.tistory.com/90</link>
      <description>&lt;h3&gt;1. 반응형 레이아웃(Responsive Layout)의 정의&lt;/h3&gt;
&lt;p&gt;반응형 레이아웃이란 단일한 코드 베이스가 다양한 화면 크기, 해상도, 기기 방향(가로/세로)에 맞춰 UI 요소를 유동적으로 재배치하고 최적화하는 설계 방식을 의미한다. 단순히 화면을 늘리는 &amp;#39;확대(Scaling)&amp;#39;가 아니라, 기기별 특성에 맞춰 사용자 경험(UX)을 &amp;#39;적응(Adapting)&amp;#39;시키는 것이 핵심이다.&lt;/p&gt;
&lt;h3&gt;2. 머티리얼 디자인(Material Design) 채택의 이점&lt;/h3&gt;
&lt;p&gt;Flutter 개발 환경에서 머티리얼 디자인 가이드를 준수하는 것은 단순히 미적인 선택이 아니라 개발 효율과 기기 대응력을 위한 전략적 선택이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;기기 파편화 통제 (목적: 예측 가능한 UI 유지):&lt;/strong&gt; 갤럭시 폴드의 좁은 외부 디스플레이부터 고해상도 태블릿까지, 극단적인 화면 비율 차이를 8dp 그리드 시스템이라는 수학적 규칙으로 통제한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;프레임워크 최적화 (목적: 구현 난이도 하락):&lt;/strong&gt; Flutter의 핵심 위젯들(Scaffold, Row, Column 등)은 머티리얼 수치를 기본값으로 가진다. 가이드를 따르면 별도의 커스텀 없이도 시스템과 조화로운 UI 구현이 가능하다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;객관적 협업 표준 (목적: 의사결정 비용 감소):&lt;/strong&gt; 주관적인 느낌 대신 &amp;quot;600dp 중단점 기준 4단 그리드 적용&amp;quot;과 같은 명확한 수치를 사용하여 디자이너와 개발자 간의 소통 오류를 제거한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 반응형 디자인 설계 시 주의점 및 필요성&lt;/h3&gt;
&lt;p&gt;반응형을 설계할 때는 단순히 &amp;#39;보이는 것&amp;#39;을 넘어, 기기 확장 시 발생할 수 있는 논리적 왜곡을 방지하는 데 집중해야 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;비율 설계의 왜곡 방지 (주의점: 상대 수치의 함정):&lt;/strong&gt; 아이콘 등을 &amp;#39;화면 너비의 20%&amp;#39; 같은 상대 비율로만 설정할 경우, 대화면 기기에서 요소가 비정상적으로 거대해지는 현상이 발생한다. 반드시 최대 크기(Max-width) 제한이 병행되어야 한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;정보 계층의 재구성 (주의점: 가독성 저하 방지):&lt;/strong&gt; 화면이 커질 때 요소를 키우는 것이 아니라, 남는 여백에 새로운 정보를 노출하거나 요소를 가로로 재배치(Reflow)하여 대화면의 공간 활용도를 높여야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 중단점(Breakpoints)과 폰트 방어 전략&lt;/h3&gt;
&lt;p&gt;반응형 UI의 핵심은 화면 크기가 변하는 &amp;#39;임계점&amp;#39;을 설정하고, 그 안에서 콘텐츠(특히 텍스트)의 파손을 막는 것이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;중단점 설정 (설계 기준):&lt;/strong&gt; 가로 폭 600dp를 기준으로 모바일(Compact)과 태블릿(Regular)을 구분하여 레이아웃 로직을 분기한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;12sp 하한선 고수 (가독성 방어):&lt;/strong&gt; 12sp 미만의 폰트는 접근성을 파괴한다. 작은 화면 대응 시 글자 크기를 줄이는 대신 UI 구조를 변경(말줄임, 세로 쌓기, 아이콘 대체)하여 최소한의 읽기 권리를 보장한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;가변 폰트 적용 (유연성 확보):&lt;/strong&gt; 16sp에서 12sp 사이의 구간은 기기 폭에 따라 유동적으로 조절하되, 12sp라는 최후의 방어선을 설정하여 레이아웃 붕괴를 막는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. 실무 적용을 위한 역할별 학습 과제&lt;/h3&gt;
&lt;h4&gt;앱 개발자 (구현 관점)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Constraints(제약 조건):&lt;/strong&gt; 부모가 자식에게 전달하는 크기 제한 범위(BoxConstraints) 이해 및 적용.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LayoutBuilder:&lt;/strong&gt; 현재 위젯에 할당된 실제 너비를 실시간으로 파악하여 UI 분기 로직 작성.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexible Layout:&lt;/strong&gt; 고정 크기를 지양하고 Expanded, Flexible, Wrap 위젯을 통한 유동적 배치 숙달.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;앱 디자이너 (설계 관점)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Figma Auto Layout:&lt;/strong&gt; 개발의 Flexbox 모델과 동일한 오토 레이아웃 기능을 활용하여 가변적인 컴포넌트 설계.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge-to-Edge 마진:&lt;/strong&gt; 어떤 극한의 환경에서도 콘텐츠 보호를 위해 최소 16dp의 좌우 여백(Margin)을 고정값으로 설정.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;텍스트 정책 명시:&lt;/strong&gt; 좁은 화면에서 발생할 텍스트 초과 상황에 대해 &amp;#39;줄바꿈&amp;#39;, &amp;#39;축소&amp;#39;, &amp;#39;말줄임&amp;#39; 중 하나를 명확히 정의.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;관련 링크&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://m2.material.io/design/layout/responsive-layout-grid.html#grid-customization&quot;&gt;https://m2.material.io/design/layout/responsive-layout-grid.html#grid-customization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://m2.material.io/design/layout/understanding-layout.html#principles&quot;&gt;https://m2.material.io/design/layout/understanding-layout.html#principles&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>Kim Roks</author>
      <guid isPermaLink="true">https://roks-apps.tistory.com/90</guid>
      <comments>https://roks-apps.tistory.com/90#entry90comment</comments>
      <pubDate>Wed, 18 Mar 2026 01:05:32 +0900</pubDate>
    </item>
    <item>
      <title>[Swift UIKit] Popover 드롭다운 버튼 화면 전환 버그 수정 과정</title>
      <link>https://roks-apps.tistory.com/89</link>
      <description>&lt;h2 data-end=&quot;77&quot; data-start=&quot;72&quot; data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://roks-apps.tistory.com/88&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://roks-apps.tistory.com/88&lt;/a&gt;&lt;/p&gt;
&lt;p data-end=&quot;253&quot; data-start=&quot;78&quot; data-ke-size=&quot;size16&quot;&gt;이전 포스팅에서&amp;nbsp;&lt;b&gt;UIPopover + UITableView&lt;/b&gt;로 드롭다운 버튼을 만들었다가, 드롭다운이 &lt;b&gt;펼쳐진 상태에서 좌측 엣지 스와이프(인터랙티브 뒤로가기)&lt;/b&gt; 를 하면 화면이 얼어붙는 이슈가 생겼다&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Sep-26-2025 10-19-51.gif&quot; data-origin-width=&quot;330&quot; data-origin-height=&quot;714&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0Hm7o/btsQQioDIVQ/KJVTxazvZkjmSbCke9HVbk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0Hm7o/btsQQioDIVQ/KJVTxazvZkjmSbCke9HVbk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0Hm7o/btsQQioDIVQ/KJVTxazvZkjmSbCke9HVbk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/0Hm7o/btsQQioDIVQ/KJVTxazvZkjmSbCke9HVbk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;330&quot; height=&quot;714&quot; data-filename=&quot;Sep-26-2025 10-19-51.gif&quot; data-origin-width=&quot;330&quot; data-origin-height=&quot;714&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 드롭다운을 연 상태에서 뒤로가기 제스쳐를 할 경우 네비게이션 타이틀만 바뀌고 화면이 멈추는 버그&lt;/p&gt;
&lt;hr data-end=&quot;258&quot; data-start=&quot;255&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;265&quot; data-start=&quot;260&quot; data-ke-size=&quot;size26&quot;&gt;증상&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;379&quot; data-start=&quot;266&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;289&quot; data-start=&quot;266&quot;&gt;드롭다운 버튼 탭 &amp;rarr; 팝오버로 드롭다운 표시&lt;/li&gt;
&lt;li data-end=&quot;335&quot; data-start=&quot;290&quot;&gt;드롭다운이 열린 상태에서 &lt;b&gt;좌측 엣지 스와이프&lt;/b&gt;로 뒤로가기 제스처 시작&lt;/li&gt;
&lt;li data-end=&quot;379&quot; data-start=&quot;336&quot;&gt;네비게이션 바 타이틀은 바뀌지만 뷰는 그대로, 터치도 먹지 않는 상태 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;384&quot; data-start=&quot;381&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;391&quot; data-start=&quot;386&quot; data-ke-size=&quot;size26&quot;&gt;원인&lt;/h2&gt;
&lt;p data-end=&quot;564&quot; data-start=&quot;392&quot; data-ke-size=&quot;size16&quot;&gt;원인 추적은 금방 가능했다 아마 화면 전환에서 문제가 생긴듯했다.&lt;/p&gt;
&lt;p data-end=&quot;564&quot; data-start=&quot;392&quot; data-ke-size=&quot;size16&quot;&gt;presentedViewController(팝오버)가 떠 있는 동안 UINavigationController의 &lt;b&gt;인터랙티브 pop 전환&lt;/b&gt;이 시작되면, 두 전환이 동시에 유지되면서 내부 전환 코디네이터가 꼬인다. 결과적으로 화면이 멈춘 것처럼 보이는 상태가 된다는 결론에 도달했다.&lt;/p&gt;
&lt;p data-end=&quot;564&quot; data-start=&quot;392&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;564&quot; data-start=&quot;392&quot; data-ke-size=&quot;size16&quot;&gt;결국&lt;/p&gt;
&lt;blockquote data-end=&quot;622&quot; data-start=&quot;578&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;622&quot; data-start=&quot;580&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;뒤로가기 제스처가 시작되기 전에 떠 있는 팝오버를 먼저 닫아야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-end=&quot;627&quot; data-start=&quot;624&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;634&quot; data-start=&quot;629&quot; data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;h3 data-end=&quot;672&quot; data-start=&quot;636&quot; data-ke-size=&quot;size23&quot;&gt;1) 제스처 시작 직전에 팝오버 닫고, 이번 제스처는 취소&lt;/h3&gt;
&lt;p data-end=&quot;823&quot; data-start=&quot;673&quot; data-ke-size=&quot;size16&quot;&gt;interactivePopGestureRecognizer의 delegate에서 &lt;b&gt;팝오버가 떠 있으면 즉시 dismiss&lt;/b&gt; 하고, &lt;b&gt;이번 제스처는 false로 취소&lt;/b&gt;한다. 사용자는 자연스럽게 한 번 더 스와이프하면 정상적으로 뒤로가기 된다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1758849719280&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class BaseViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        hookInteractivePopGesture()
    }

    // 일반 전환(pop/push) 중에도 안전하게 정리
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        if isMovingFromParent || navigationController?.transitionCoordinator != nil {
            presentedViewController?.dismiss(animated: false)
        }
    }

    private func hookInteractivePopGesture() {
        guard let nc = navigationController,
              let popGR = nc.interactivePopGestureRecognizer else { return }
        popGR.delegate = self
        popGR.isEnabled = (nc.viewControllers.count &amp;gt; 1)
    }
}

extension BaseViewController: UIGestureRecognizerDelegate {
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -&amp;gt; Bool {
        if gestureRecognizer === navigationController?.interactivePopGestureRecognizer,
           presentedViewController != nil {
            // 팝오버 먼저 닫고, 이번 제스처는 취소
            presentedViewController?.dismiss(animated: false)
            return false
        }
        return true
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-end=&quot;2014&quot; data-start=&quot;1996&quot; data-ke-size=&quot;size23&quot;&gt;2) 전환 직전 추가 방어&lt;/h3&gt;
&lt;p data-end=&quot;2157&quot; data-start=&quot;2015&quot; data-ke-size=&quot;size16&quot;&gt;viewWillDisappear(_:)에서 presentedViewController?.dismiss(animated: false)를 한 번 더 호출해 두면, 일반적인 pop/push에서도 잔여 프레젠테이션이 남지 않아 안전하다. (위 코드 포함)&lt;/p&gt;
&lt;hr data-end=&quot;2709&quot; data-start=&quot;2706&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2719&quot; data-start=&quot;2711&quot; data-ke-size=&quot;size26&quot;&gt;체크리스트&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2946&quot; data-start=&quot;2720&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2788&quot; data-start=&quot;2720&quot;&gt;제스처 시작 전에 presentedViewController 있으면 &lt;b&gt;dismiss 후 제스처 취소&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;2851&quot; data-start=&quot;2789&quot;&gt;viewWillDisappear에서도 &lt;b&gt;한 번 더 dismiss&lt;/b&gt;로 잔여 프레젠테이션 제거&lt;/li&gt;
&lt;li data-end=&quot;2902&quot; data-start=&quot;2852&quot;&gt;팝오버의 passthroughViews = []로 바깥 터치 전달 최소화&lt;/li&gt;
&lt;li data-end=&quot;2946&quot; data-start=&quot;2903&quot;&gt;가능하면 UIButton.menu 등 &lt;b&gt;시스템 풀다운&lt;/b&gt; 고려&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;2951&quot; data-start=&quot;2948&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2959&quot; data-start=&quot;2953&quot; data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Sep-26-2025 10-23-52.gif&quot; data-origin-width=&quot;330&quot; data-origin-height=&quot;714&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UQf2o/btsQOdoeopK/ajaDCKa36jNVvUkBIkhjt0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UQf2o/btsQOdoeopK/ajaDCKa36jNVvUkBIkhjt0/img.gif&quot; data-alt=&quot;드롭다운이 먼저 dismiss된 후 화면 전환이 잘 작동&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UQf2o/btsQOdoeopK/ajaDCKa36jNVvUkBIkhjt0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/UQf2o/btsQOdoeopK/ajaDCKa36jNVvUkBIkhjt0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;330&quot; height=&quot;714&quot; data-filename=&quot;Sep-26-2025 10-23-52.gif&quot; data-origin-width=&quot;330&quot; data-origin-height=&quot;714&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;드롭다운이 먼저 dismiss된 후 화면 전환이 잘 작동&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;3110&quot; data-start=&quot;2960&quot; data-ke-size=&quot;size16&quot;&gt;네비게이션 타이틀이 변화하는걸 확인해서 다행히 원인을 빨리 찾고 해결할 수 있었다.&lt;br /&gt;뒤로가기 제스처가 시작되기 전에 &lt;b&gt;항상 떠 있는 팝오버를 닫는&lt;/b&gt; 흐름으로 바꾸니, 화면 멈춤 증상은 재현되지 않았다.&lt;/p&gt;
&lt;p data-end=&quot;3110&quot; data-start=&quot;2960&quot; data-ke-size=&quot;size16&quot;&gt;드롭다운을 팝오버로 구현해야 하는 상황이라면 위 가드 코드들을 반드시 넣어두는 걸 추천한다.&lt;/p&gt;</description>
      <category>Trouble Shooting</category>
      <category>popover</category>
      <category>SWIFT</category>
      <category>UIKit</category>
      <category>버그수정</category>
      <author>Kim Roks</author>
      <guid isPermaLink="true">https://roks-apps.tistory.com/89</guid>
      <comments>https://roks-apps.tistory.com/89#entry89comment</comments>
      <pubDate>Fri, 26 Sep 2025 10:25:58 +0900</pubDate>
    </item>
    <item>
      <title>[Swift UIKit] 유저 사용성을 고려한 UI의 재설계 과정 (드롭 다운 버튼)</title>
      <link>https://roks-apps.tistory.com/88</link>
      <description>&lt;h2 data-end=&quot;193&quot; data-start=&quot;188&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;352&quot; data-start=&quot;194&quot; data-ke-size=&quot;size16&quot;&gt;신규 프로젝트에서 &lt;b&gt;사유 선택을 위한 드롭다운 버튼&lt;/b&gt;이 필요했다.&lt;br /&gt;드롭다운 버튼이라는 게 사실 앱보다는 웹 환경에서 더 자주 쓰이는 UI 같지만,&lt;br /&gt;이번 프로젝트의 디자인은 &lt;b&gt;웹 UI를 앱에 맞게 리디자인&lt;/b&gt;하는 과정이었기 때문에&lt;br /&gt;가능한 한 동기화를 시키려고 했다.&lt;/p&gt;
&lt;p data-end=&quot;483&quot; data-start=&quot;354&quot; data-ke-size=&quot;size16&quot;&gt;처음 해보는 작업이라 구글링을 해봤는데, 대부분 서드파티 라이브러리를 이용해서 해결하고 있었다.&lt;br /&gt;하지만 이번 프로젝트는 &lt;b&gt;SnapKit 외에는 서드파티를 쓰지 않는 것&lt;/b&gt;을 희망해서 배제하기로 했다&lt;/p&gt;
&lt;p data-end=&quot;534&quot; data-start=&quot;485&quot; data-ke-size=&quot;size16&quot;&gt;우선 가장 간단해 보이는 방법을 찾았고, iOS 기본 컴포넌트인 UIMenu로 구현을 시작했다.&lt;/p&gt;
&lt;hr data-end=&quot;539&quot; data-start=&quot;536&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;555&quot; data-start=&quot;541&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;UIMenu가 뭐지?&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;1832&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdpTg0/btsQIYiITpK/fVK6Nii86BcLOovwoZCs51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdpTg0/btsQIYiITpK/fVK6Nii86BcLOovwoZCs51/img.png&quot; data-alt=&quot;사진첩에서 흔히 볼 수 있는 UI&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdpTg0/btsQIYiITpK/fVK6Nii86BcLOovwoZCs51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdpTg0%2FbtsQIYiITpK%2FfVK6Nii86BcLOovwoZCs51%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;359&quot; height=&quot;777&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;1832&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;사진첩에서 흔히 볼 수 있는 UI&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;704&quot; data-start=&quot;577&quot; data-ke-size=&quot;size16&quot;&gt;UIMenu는 버튼을 탭 했을 때 &lt;b&gt;메뉴 리스트&lt;/b&gt;를 보여주는 iOS 기본 컴포넌트다.&lt;br /&gt;&lt;b&gt;iOS 14 이후부터는 버튼과 쉽게 결합할 수 있도록 API가 강화되어서,&lt;/b&gt;&lt;br /&gt;옵션을 나열하는 경우 꽤 간단하게 적용할 수 있었다.&lt;/p&gt;
&lt;p data-end=&quot;728&quot; data-start=&quot;706&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;831&quot; data-start=&quot;730&quot; data-ke-size=&quot;size16&quot;&gt;공식 문서 설명에 따르면, UIMenu는 &lt;b&gt;액션 모음&lt;/b&gt;을 정의하고 표시하는 방식이다.&lt;br /&gt;덕분에 별도의 커스텀 뷰 없이도 빠르게 드롭다운과 유사한 UI를 구성할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;865&quot; data-start=&quot;833&quot; data-ke-size=&quot;size16&quot;&gt;간단히 말해, 이런 식으로 버튼에 붙여서 사용할 수 있다:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1758274952929&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let items = [
    (&quot;육아휴직&quot;, NonWorkingType.parentalLeave),
    (&quot;출산전후휴가&quot;, NonWorkingType.maternityLeave),
    (&quot;배우자출산휴가&quot;, NonWorkingType.spouseMaternityLeave)
]

let actions = items.map { item in
    UIAction(title: item.0) { _ in
        print(&quot;Selected \(item.1)&quot;)
    }
}

let button = UIButton(type: .system)
button.menu = UIMenu(children: actions)
button.showsMenuAsPrimaryAction = true&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;1275&quot; data-start=&quot;1272&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1294&quot; data-start=&quot;1277&quot; data-ke-size=&quot;size26&quot;&gt;UIMenu로 만든 결과물&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-11 오후 8.11.34 (1).png&quot; data-origin-width=&quot;475&quot; data-origin-height=&quot;839&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uzcPe/btsQGiccJZe/slvxnh7jlDFD2pYYtKnuZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uzcPe/btsQGiccJZe/slvxnh7jlDFD2pYYtKnuZk/img.png&quot; data-alt=&quot;너무 많은 선택지..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uzcPe/btsQGiccJZe/slvxnh7jlDFD2pYYtKnuZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuzcPe%2FbtsQGiccJZe%2Fslvxnh7jlDFD2pYYtKnuZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;332&quot; height=&quot;586&quot; data-filename=&quot;스크린샷 2025-09-11 오후 8.11.34 (1).png&quot; data-origin-width=&quot;475&quot; data-origin-height=&quot;839&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;너무 많은 선택지..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;1382&quot; data-start=&quot;1315&quot; data-ke-size=&quot;size16&quot;&gt;처음 만든 결과물은 동작 자체는 잘 됐다.&lt;br /&gt;리스트 항목을 누르면 값이 잘 선택되고, 서버와의 매핑도 문제없었다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1960&quot; data-start=&quot;1907&quot; data-ke-size=&quot;size16&quot;&gt;스크린샷만 봐도 알 수 있듯이, &lt;b&gt;메뉴 항목이 많은 환경에서는 한눈에 보기 불편했다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-end=&quot;1960&quot; data-start=&quot;1907&quot; data-ke-size=&quot;size16&quot;&gt;일반적인 화면에선 스크롤 없이 리스트들이 나열됐고&lt;br /&gt;작은 화면(iPhone SE 같은 극단적인 케이스)에서는 스크롤을 지원했다.&lt;br /&gt;&lt;b&gt;메뉴 자체의 높이를 줄여 공통적으로 스크롤을 유도&lt;/b&gt;하면 깔끔할 것 같았지만 그 부분은 구현에 실패했다.&lt;/p&gt;
&lt;p data-end=&quot;933&quot; data-start=&quot;845&quot; data-ke-size=&quot;size16&quot;&gt;즉, UIMenu는 단순히 항목이 몇 개 없는 상황이라면 충분히 쓸 만했지만,&lt;br /&gt;&lt;b&gt;항목이 많아지는 순간 UX가 급격히 떨어지는 한계를 드러냈다&lt;/b&gt;.&lt;/p&gt;
&lt;hr data-end=&quot;1965&quot; data-start=&quot;1962&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1974&quot; data-start=&quot;1967&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개선하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;2065&quot; data-start=&quot;1975&quot; data-ke-size=&quot;size16&quot;&gt;그래서 다른 접근을 고민하다가,&lt;/p&gt;
&lt;p data-end=&quot;2065&quot; data-start=&quot;1975&quot; data-ke-size=&quot;size16&quot;&gt;아마 보통 iPad에서 많이 쓰이는 UI인 걸로 알고 있지만&lt;/p&gt;
&lt;p data-end=&quot;2065&quot; data-start=&quot;1975&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UIPopoverPresentationController + UITableView&lt;/b&gt; 조합을 선택했다.&lt;/p&gt;
&lt;p data-end=&quot;2065&quot; data-start=&quot;1975&quot; data-ke-size=&quot;size16&quot;&gt;애플의 HIG에서도 팝오버는 &lt;b&gt;큰 화면에서 주변 맥락을 유지하면서 가볍게 보여주는 UI&lt;/b&gt;로 소개되고 있다.&lt;br /&gt;보통 iPhone 같은 Compact 환경에서는 보통 &lt;b&gt;풀스크린이나 시트로 대체되는 것&lt;/b&gt;이 자연스럽다는 얘기다.&lt;/p&gt;
&lt;p data-end=&quot;670&quot; data-start=&quot;450&quot; data-ke-size=&quot;size16&quot;&gt;그래서 그런가?&lt;br /&gt;기본 동작은 Compact width에서 자동으로 풀스크린 적응(adaptive)이 되어버린다.&lt;br /&gt;이 적응을 막고 싶다면 UIPopoverPresentationControllerDelegate에서&lt;br /&gt;아래처럼 .none을 반환해 주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1758275976808&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func adaptivePresentationStyle(
    for controller: UIPresentationController,
    traitCollection: UITraitCollection
) -&amp;gt; UIModalPresentationStyle {
    return .none
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;2065&quot; data-start=&quot;1975&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2114&quot; data-start=&quot;2067&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2293&quot; data-start=&quot;2116&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2167&quot; data-start=&quot;2116&quot;&gt;&lt;b&gt;Popover&lt;/b&gt;를 사용하면 메뉴가 화면을 가득 채우지 않고 자연스럽게 떠오른다.&lt;/li&gt;
&lt;li data-end=&quot;2222&quot; data-start=&quot;2168&quot;&gt;그 안에 &lt;b&gt;TableView&lt;/b&gt;를 넣어주면, 스크롤 가능하고 체크마크도 표시할 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;2293&quot; data-start=&quot;2223&quot;&gt;게다가 maxHeightFraction 같은 방식을 활용해서, 화면 크기에 맞춰 &lt;b&gt;동적 높이 조절&lt;/b&gt;도 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 최종적으로 아래와 같이 너무 마음에 들게 개선할 수 있었다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;simulator_screenshot_361BC04A-4653-4970-B1E2-AF21307DB3F7.png&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;1334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0yaeK/btsQG2Ug6y0/sLANinQ1RSpEkM1dnX6bOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0yaeK/btsQG2Ug6y0/sLANinQ1RSpEkM1dnX6bOk/img.png&quot; data-alt=&quot;개선된 버튼 스타일&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0yaeK/btsQG2Ug6y0/sLANinQ1RSpEkM1dnX6bOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0yaeK%2FbtsQG2Ug6y0%2FsLANinQ1RSpEkM1dnX6bOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;369&quot; height=&quot;656&quot; data-filename=&quot;simulator_screenshot_361BC04A-4653-4970-B1E2-AF21307DB3F7.png&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;1334&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;개선된 버튼 스타일&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;혹시 코드가 궁금하신분들이 있을까봐 내가 작성했던 드롭다운 버튼의 코드를 활용해 조금 더 범용성있게 서브클래싱 해봤다&lt;/div&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1758276748707&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import UIKit

final class DropdownButton: UIButton {
    
    // Public item representation (ID is Hashable for external mapping)
    public struct Item: Hashable {
        public let id: AnyHashable
        public let title: String
        
        public init(id: AnyHashable, title: String) {
            self.id = id
            self.title = title
        }
    }
    
    // MARK: Public State / Callbacks
    
    public private(set) var selectedTitle: String?
    public private(set) var selectedId: AnyHashable?
    
    /// Called whenever a user selects an item.
    public var onSelect: ((Item) -&amp;gt; Void)?
    
    /// Accessibility and layout customization
    public var placeholder: String = &quot;선택&quot; {
        didSet { applyTitleIfNeeded() }
    }
    public var maxHeightFraction: CGFloat = 0.4    // e.g., 0.4 == 40% of screen height
    public var rowHeight: CGFloat = 48
    public var minimumPopoverWidth: CGFloat = 230
    public var cellNumberOfLines: Int = 1
    
    // MARK: Internal Data
    
    public private(set) var items: [Item] = [] {
        didSet { isEnabled = !items.isEmpty }
    }
    
    // MARK: Lifecycle
    
    public override init(frame: CGRect) {
        super.init(frame: frame)
        configureButtonAppearance()
        applyTitleIfNeeded()
        addTarget(self, action: #selector(didTap), for: .touchUpInside)
        isAccessibilityElement = true
        accessibilityTraits.insert(.button)
    }
    
    public required init?(coder: NSCoder) {
        super.init(coder: coder)
        configureButtonAppearance()
        applyTitleIfNeeded()
        addTarget(self, action: #selector(didTap), for: .touchUpInside)
        isAccessibilityElement = true
        accessibilityTraits.insert(.button)
    }
    
    // MARK: Public API
    
    /// Replace entire data source.
    public func setItems(_ newItems: [Item]) {
        items = newItems
        // If the previous selection no longer exists, reset to placeholder
        if let selectedId = selectedId, items.contains(where: { $0.id == selectedId }) == false {
            resetSelection()
        } else {
            // Keep the current title if still valid
            applyTitleIfNeeded()
        }
    }
    
    /// Preselect by item identifier.
    public func preselect(id: AnyHashable) {
        guard let found = items.first(where: { $0.id == id }) else { return }
        applySelection(item: found)
    }
    
    /// Preselect by display title.
    public func preselect(title: String) {
        guard let found = items.first(where: { $0.title == title }) else { return }
        applySelection(item: found)
    }
    
    /// Reset selection and show placeholder.
    public func resetSelection() {
        selectedTitle = nil
        selectedId = nil
        applyTitle(placeholder)
    }
    
    // MARK: Appearance
    
    private func configureButtonAppearance() {
        var configuration = UIButton.Configuration.filled()
        configuration.baseBackgroundColor = .systemGray6
        configuration.baseForegroundColor = .label
        configuration.cornerStyle = .medium
        configuration.titleLineBreakMode = .byTruncatingTail
        
        let symbolConfiguration = UIImage.SymbolConfiguration(pointSize: 12, weight: .medium)
        configuration.image = UIImage(systemName: &quot;chevron.down&quot;, withConfiguration: symbolConfiguration)
        configuration.imagePlacement = .trailing
        configuration.imagePadding = 6
        
        configuration.attributedTitle = AttributedString(placeholder, attributes: AttributeContainer([
            .font: UIFont.preferredFont(forTextStyle: .body)
        ]))
        configuration.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12)
        
        self.configuration = configuration
        accessibilityLabel = placeholder
    }
    
    private func applyTitleIfNeeded() {
        applyTitle(selectedTitle ?? placeholder)
    }
    
    private func applyTitle(_ title: String) {
        guard var buttonConfiguration = configuration else { return }
        buttonConfiguration.attributedTitle = AttributedString(title, attributes: AttributeContainer([
            .font: UIFont.preferredFont(forTextStyle: .body)
        ]))
        configuration = buttonConfiguration
        accessibilityLabel = title
    }
    
    // MARK: Selection Logic
    
    private func applySelection(item: Item) {
        selectedTitle = item.title
        selectedId = item.id
        applyTitle(item.title)
        onSelect?(item)
    }
    
    // MARK: Actions
    
    @objc private func didTap() {
        guard let host = parentViewController() else { return }
        
        let controller = DropdownPopoverController(
            items: items,
            selectedTitle: selectedTitle,
            rowHeight: rowHeight,
            cellNumberOfLines: cellNumberOfLines
        )
        controller.onSelect = { [weak self] item in
            self?.applySelection(item: item)
        }
        
        controller.modalPresentationStyle = .popover
        if let popover = controller.popoverPresentationController {
            popover.sourceView = self
            popover.sourceRect = bounds
            popover.permittedArrowDirections = [.up, .down]
            popover.delegate = controller
        }
        
        let screenHeight = UIScreen.main.bounds.height
        let maximumHeight = screenHeight * maxHeightFraction
        let totalHeight = CGFloat(items.count) * rowHeight
        let finalHeight = min(maximumHeight, totalHeight)
        let width = max(bounds.width, minimumPopoverWidth)
        
        controller.preferredContentSize = CGSize(width: width, height: finalHeight)
        host.present(controller, animated: true)
    }
}

// MARK: - Popover Controller

private final class DropdownPopoverController: UIViewController {
    
    var onSelect: ((DropdownButton.Item) -&amp;gt; Void)?
    
    private let items: [DropdownButton.Item]
    private let selectedTitle: String?
    private let rowHeight: CGFloat
    private let cellNumberOfLines: Int
    
    private let tableView = UITableView(frame: .zero, style: .plain)
    
    init(items: [DropdownButton.Item],
         selectedTitle: String?,
         rowHeight: CGFloat,
         cellNumberOfLines: Int) {
        self.items = items
        self.selectedTitle = selectedTitle
        self.rowHeight = rowHeight
        self.cellNumberOfLines = cellNumberOfLines
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        return nil
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: &quot;Cell&quot;)
        tableView.dataSource = self
        tableView.delegate = self
        tableView.rowHeight = rowHeight
        tableView.alwaysBounceVertical = true
        tableView.tableFooterView = UIView()
        tableView.separatorInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
        
        view.addSubview(tableView)
        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
}

// MARK: - UIPopoverPresentationControllerDelegate

extension DropdownPopoverController: UIPopoverPresentationControllerDelegate {
    func adaptivePresentationStyle(for controller: UIPresentationController,
                                   traitCollection: UITraitCollection) -&amp;gt; UIModalPresentationStyle {
        return .none
    }
}

// MARK: - UITableViewDataSource

extension DropdownPopoverController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -&amp;gt; Int {
        return items.count
    }
    
    func tableView(_ tableView: UITableView,
                   cellForRowAt indexPath: IndexPath) -&amp;gt; UITableViewCell {
        let item = items[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: &quot;Cell&quot;, for: indexPath)
        
        var content = cell.defaultContentConfiguration()
        content.text = item.title
        content.textProperties.numberOfLines = cellNumberOfLines
        content.textProperties.font = UIFont.preferredFont(forTextStyle: .body)
        cell.contentConfiguration = content
        
        cell.accessoryType = (item.title == selectedTitle) ? .checkmark : .none
        cell.isAccessibilityElement = true
        cell.accessibilityTraits = .button
        cell.accessibilityLabel = item.title
        
        return cell
    }
}

// MARK: - UITableViewDelegate

extension DropdownPopoverController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        onSelect?(items[indexPath.row])
        dismiss(animated: true)
    }
}

// MARK: - UIView helper (parent view controller lookup)

private extension UIView {
    func parentViewController() -&amp;gt; UIViewController? {
        var responder: UIResponder? = self
        while let current = responder {
            if let controller = current as? UIViewController {
                return controller
            }
            responder = current.next
        }
        return nil
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-end=&quot;4616&quot; data-start=&quot;4613&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;4623&quot; data-start=&quot;4618&quot; data-ke-size=&quot;size26&quot;&gt;후기&lt;/h2&gt;
&lt;p data-end=&quot;4649&quot; data-start=&quot;4624&quot; data-ke-size=&quot;size16&quot;&gt;이번 작업을 통해 느낀 점은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4825&quot; data-start=&quot;4651&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4686&quot; data-start=&quot;4651&quot;&gt;UIMenu는 항목이 적을 때는 간단하게 쓰기 좋다.&lt;/li&gt;
&lt;li data-end=&quot;4758&quot; data-start=&quot;4687&quot;&gt;하지만 항목이 많아질 가능성이 있다면, 다른 방법을&amp;nbsp;고려하는 게 더 낫다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4758&quot; data-start=&quot;4687&quot;&gt;나는 UIMenu자체를 수정하여 높이를 최적화할 방법을 결국 못 찾았다&lt;/li&gt;
&lt;li data-end=&quot;4758&quot; data-start=&quot;4687&quot;&gt;시트 디자인은 원하지 않았다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;iPhone환경에서 popOver를 구현하는 것이 옳은가에 대한 의문은 아직 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그럼에도 당장에 더 좋은 UX를 위해 택했다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;4943&quot; data-start=&quot;4827&quot; data-ke-size=&quot;size16&quot;&gt;결국 이번 케이스는 &amp;ldquo;일반적인 iPhone UX 패턴&amp;rdquo;보다는 &lt;b&gt;앵커 기반의 드롭다운 경험&lt;/b&gt;을 살리는 게 더 중요했고,&lt;br /&gt;그 목표를 충족시켜 줄 수 있는 해법이 Popover였다고 생각한다.&lt;/p&gt;</description>
      <category>Trouble Shooting</category>
      <category>dropdownbutton</category>
      <category>SWIFT</category>
      <category>UIKit</category>
      <category>드롭다운</category>
      <category>드롭다운 버튼</category>
      <author>Kim Roks</author>
      <guid isPermaLink="true">https://roks-apps.tistory.com/88</guid>
      <comments>https://roks-apps.tistory.com/88#entry88comment</comments>
      <pubDate>Fri, 19 Sep 2025 19:23:52 +0900</pubDate>
    </item>
    <item>
      <title>[CS] 애플 로그인을 구현할 때 왜 서버가 필요할까?</title>
      <link>https://roks-apps.tistory.com/87</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 스위프트 오픈 카톡방에서 '애플 로그인을 구현할때 서버가 필요한가요?' 라는 질문을 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이 질문은 단순히 기능 구현의 문제가 아니라, 소셜 로그인 방식의 작동 원리와 보안 구조를 제대로 이해하고 있는지를 보여주는 중요한 질문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;소셜 로그인은 OAuth2.0 으로 지원된다.&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소셜 로그인(Google, naver, kakao 등)은 보통 OAuth 2.0으로 제공됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth는 무엇일까요?&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;OAuth는 사용자가 아이디와 비밀번호를 직접 입력하지 않고도, 제 3의 앱이나 서비스가 권한 있는 방식으로 사용자 정보를 얻을 수 있도록 허용하는 표준 프로토콜 입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 우리가 어떤 앱에서 Apple로 로그인을 누르면 Apple은 인증을 수행한 뒤 Auth를 사용하는 서비스(이하 '앱')에 &lt;b&gt;인증 결과 토큰(JWT 형식)을 전달&lt;/b&gt;합니다.&lt;br /&gt;앱은 이 토큰을 바탕으로 사용자의 로그인을 처리합니다. &lt;b&gt;사용자의 비밀번호는 우리 앱엔 직접적으로 전달되지 않습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;왜 OAuth 같은 방식이 필요할까요?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 날이 갈수록 보안에 대한 인식이 점차 강화되고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 일반적인 사이드프로젝트나, 소규모 회사등의 서비스에선 유저의 정보를 직접 저장하고 관리하는것이 법적/ 기술적으로 부담이 될 수 있는 실정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 얘기한것 처럼 사용자의 예민한 정보를 우리 앱에 직접적으로 저장하지 않기때문에, 이러한 보안 리스크가 훨씬 적어지고, 로그인 기능을 빠르게 추가할수도 있겠죠&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더붙여 사용자 입장에서도 장점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 계정을 만들 필요가 없고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 익숙한 플랫폼 계정으로 로그인을 할 수 있으니 새로운 서비스 사용에 대한 부담이 줄어들겠죠&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;그럼 OAuth는 어떤 방식으로 동작할까요?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비유하자면 &lt;b&gt;토큰 이라는 임시 열쇠&lt;/b&gt;를 통해 간접적으로 권한을 받는겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단히 말해, 소셜 로그인에서 앱이 사용자 정보를 직접 얻는 게 아니라&lt;br /&gt;&lt;b&gt;토큰(Token)&lt;/b&gt;이라는 임시 열쇠를 통해 간접적으로 권한을 받습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;1375&quot; data-start=&quot;1362&quot; data-ke-size=&quot;size23&quot;&gt;  비유하자면:&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;앱은 사용자에게 직접 집 열쇠를 받는 게 아니라,&lt;br /&gt;사용자 본인이 인증기관에서 발급받은 '&lt;b&gt;임시' 열쇠&lt;/b&gt;를 앱에게 넘기는 구조입니다.&lt;br /&gt;앱은 이 열쇠(Access Token)를 갖고 사용자 정보를 제한적으로 열람할 수 있습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1593&quot; data-start=&quot;1520&quot; data-ke-size=&quot;size16&quot;&gt;이 구조 덕분에 앱이 사용자 비밀번호를 다룰 일이 없어지고,&lt;br /&gt;토큰의 범위와 유효시간이 제한되므로 &lt;b&gt;보안적으로도 안전합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;그럼 OAuth는 보안적으로 더할 나위 없이 아주 훌륭할까요?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth는 &lt;b&gt;권한 위임&lt;/b&gt;만을 처리합니다. (이를 &lt;b&gt;Delegated Authorization&lt;/b&gt;라고 표현)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 누간가의 정보를 접근해도 된다 정도의 허락을 주는것이지&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;'이 사용자가 진짜 누구임?'&lt;/b&gt; 까지의 문제는 해결해주지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 등장하게 되는것이 &lt;b&gt;OpenID Connect (OIDC)입니다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;애플 로그인은 OIDC 기반으로 작동한다&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 OAuth는 열쇠를 제공받는다고 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 어떻게 보면 열쇠만 있으면 내가 그 사람이 누릴 수 있는 혜택을 똑같이 누릴 수 있다는 이야기가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 OIDC가 조금 더 강화된 보안을 제공합니다. OIDC가 뭘까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OIDC는 &lt;b&gt;Auth 2.0 프로토콜을 기반&lt;/b&gt;으로 한 인증 프로토콜입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OIDC는 Auth 2.0 + ID 확인 인증 레이어를 추가한 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더욱 강화된 보안성을 제공해줄 수 있겠죠&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 열쇠에 비유했으니 이건 열쇠와 더불어 신분증 인증까지 하는 개념이라고 할 수 있을것 같습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적인 부분 보다도 이번 포스팅에서 결론적으로 얘기하고 싶은 내용은&amp;nbsp; &lt;br /&gt;&lt;b&gt;애플로그인은 OIDC 방식을 채택하고 있으며 이는 Auth 2.0 기반으로 동작한다 &lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;그래서 서버가 왜 필요하다고?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'애플로그인 구현에 서버가 필요한가요?'라는 질문은 곧&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OAuth 2.0, OIDC 구현에 왜 서버가 필요한가요? 랑 같은 내용으로 귀결&lt;/b&gt;됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;342&quot; data-start=&quot;303&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&amp;nbsp;1. 민감 정보 보호 (Client Secret &amp;amp; 토큰)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;579&quot; data-start=&quot;343&quot; data-ke-size=&quot;size16&quot;&gt;OAuth 2.0에서는 클라이언트 앱이 서비스에 접근할 수 있도록 client_id와 client_secret이라는 고유한 정보가 발급됩니다.&lt;/p&gt;
&lt;p data-end=&quot;579&quot; data-start=&quot;343&quot; data-ke-size=&quot;size16&quot;&gt;이 중 client_secret은 &lt;b&gt;절대로 클라이언트 앱 내부에 포함되어서는 안 됩니다&lt;/b&gt;. &lt;br /&gt;누구나 디컴파일하거나 네트워크 트래픽을 분석해 시크릿을 탈취할 수 있기 때문이죠.&lt;br /&gt;따라서, 이 정보를 안전하게 저장하고 사용하는 역할은&lt;b&gt;&amp;nbsp;서버가 전담&lt;/b&gt;해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;620&quot; data-start=&quot;586&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. Authorization Code 교환 단계&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;674&quot; data-start=&quot;621&quot; data-ke-size=&quot;size16&quot;&gt;많이 사용하는 인증 방식인 &lt;b&gt;Authorization Code Flow&lt;/b&gt;는 두 단계로 나뉩니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;751&quot; data-start=&quot;676&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;711&quot; data-start=&quot;676&quot;&gt;사용자 인증 &amp;rarr; Authorization Code 획득&lt;/li&gt;
&lt;li data-end=&quot;751&quot; data-start=&quot;712&quot;&gt;Authorization Code &amp;rarr; Access Token 교환&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;876&quot; data-start=&quot;753&quot; data-ke-size=&quot;size16&quot;&gt;두 번째 단계에서 &lt;b&gt;client_secret이 필요&lt;/b&gt;하기 때문에, 이 교환 작업은 &lt;b&gt;서버에서 안전하게 처리되어야 합니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-end=&quot;876&quot; data-start=&quot;753&quot; data-ke-size=&quot;size16&quot;&gt;클라이언트에서 처리하면 인증 정보가 유출될 가능성이 생기므로 보안적으로 권장되지 않습니다.&lt;/p&gt;
&lt;p data-end=&quot;876&quot; data-start=&quot;753&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;1201&quot; data-start=&quot;1175&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 사용자 정보 요청과 세션 유지&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;1287&quot; data-start=&quot;1202&quot; data-ke-size=&quot;size16&quot;&gt;OAuth로 인증이 완료된 후에도 우리는 사용자의 정보(예: 이메일, 프로필 등)를 불러오거나, 앱 내에서 로그인 상태를 유지해야 합니다. 이때 서버는:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1371&quot; data-start=&quot;1289&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1319&quot; data-start=&quot;1289&quot;&gt;Access Token을 이용해 사용자 정보를 요청&lt;/li&gt;
&lt;li data-end=&quot;1347&quot; data-start=&quot;1320&quot;&gt;사용자 세션을 유지 (쿠키 or JWT 기반)&lt;/li&gt;
&lt;li data-end=&quot;1371&quot; data-start=&quot;1348&quot;&gt;추가적인 사용자 데이터와 연결 (DB)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1426&quot; data-start=&quot;1373&quot; data-ke-size=&quot;size16&quot;&gt;하는 등의 역할을 수행합니다. &lt;b&gt;클라이언트만으로는 이 흐름을 안전하게 유지하기 어렵습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1426&quot; data-start=&quot;1373&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1426&quot; data-start=&quot;1373&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 쉽게 한줄로 표현하면 'OIDC, OAtuh 가 그걸 요구하니까' 라고 대답할 수 있겠지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 발자국 더 들어가 그니까 그걸 왜 요구하는데? 까지 알아봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 외에도 OAuth, OIDC 구현에 서버가 필요한 이유는 이 외에도 많이 있겠지만&amp;nbsp; 큰 내용들을 정리해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어쩌면 민감 정보를 클라이언트가 아닌 서버 사이드에서 처리해야 하는 보안원칙과 대동소이 하다고 할 수&amp;nbsp; 있을것 같네요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에 소셜로그인은 포함되지 않았지만 추후 진행하게 된다면 더 수월하게 진행할 수 있을거 같네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>apple login</category>
      <category>OAtuh 2.0</category>
      <category>OIDC</category>
      <category>SWIFT</category>
      <author>Kim Roks</author>
      <guid isPermaLink="true">https://roks-apps.tistory.com/87</guid>
      <comments>https://roks-apps.tistory.com/87#entry87comment</comments>
      <pubDate>Fri, 25 Jul 2025 17:08:11 +0900</pubDate>
    </item>
    <item>
      <title>[Swift] 타입 추상화(associated, Opaque, Existential, Generic)</title>
      <link>https://roks-apps.tistory.com/86</link>
      <description>&lt;p data-end=&quot;289&quot; data-start=&quot;174&quot; data-ke-size=&quot;size16&quot;&gt;Swift는 &lt;b&gt;타입 안정성&lt;/b&gt;이 아주 강력한 언어입니다.&lt;br /&gt;특히 &lt;b&gt;타입 추상화(type abstraction)&lt;/b&gt;와 관련된 기능들이 다양해서,&lt;/p&gt;
&lt;p data-end=&quot;289&quot; data-start=&quot;174&quot; data-ke-size=&quot;size16&quot;&gt;제대로 활용하면 코드의 유연성과 재사용성이 크게 높아지죠.&lt;/p&gt;
&lt;p data-end=&quot;343&quot; data-start=&quot;291&quot; data-ke-size=&quot;size16&quot;&gt;오늘은 그중에서도 뭔가 비슷한 것 같으면서도 분명히 다른 네 가지 기능을 정리해보려고 합니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;449&quot; data-start=&quot;345&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;373&quot; data-start=&quot;345&quot;&gt;연관 타입 (associatedtype)&lt;/li&gt;
&lt;li data-end=&quot;393&quot; data-start=&quot;374&quot;&gt;제네릭 (Generic)&lt;/li&gt;
&lt;li data-end=&quot;420&quot; data-start=&quot;394&quot;&gt;불명확 타입 (Opaque Type)&lt;/li&gt;
&lt;li data-end=&quot;449&quot; data-start=&quot;421&quot;&gt;실존 타입 (Existential Type)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;501&quot; data-start=&quot;451&quot; data-ke-size=&quot;size16&quot;&gt;어디에 어떻게 써야 할지, 어떤 차이가 있는지 살펴보고 그래서 '이게 왜 존재해야하는가'에 대해 개인적인 생각을 담아봤습니다.&lt;/p&gt;
&lt;blockquote data-end=&quot;549&quot; data-start=&quot;503&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;549&quot; data-start=&quot;505&quot; data-ke-size=&quot;size16&quot;&gt;  한글 용어는 야곰님의 『스위프트 프로그래밍 4판』을 기준으로 사용했습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-end=&quot;554&quot; data-start=&quot;551&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;607&quot; data-start=&quot;556&quot; data-ke-size=&quot;size26&quot;&gt;1.   연관 타입 (associatedtype) &amp;mdash; 프로토콜 내부에서의 타입 추상화&lt;/h2&gt;
&lt;h3 data-end=&quot;618&quot; data-start=&quot;609&quot; data-ke-size=&quot;size23&quot;&gt;  개념&lt;/h3&gt;
&lt;p data-end=&quot;726&quot; data-start=&quot;620&quot; data-ke-size=&quot;size16&quot;&gt;associatedtype은 &lt;b&gt;프로토콜 내부에서 타입을 추상화&lt;/b&gt;하는 기능이에요.&lt;br /&gt;즉, 프로토콜을 따르는 타입이 &lt;b&gt;어떤 구체 타입을 쓸지는 채택한 쪽에서 나중에 정하게&lt;/b&gt; 됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1752667256004&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protocol Container { 
  associatedtype Item
  func append(_ item: Item)
  func get(index: Int) -&amp;gt; Item 
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-end=&quot;861&quot; data-start=&quot;852&quot; data-ke-size=&quot;size23&quot;&gt;  목적&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;982&quot; data-start=&quot;863&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;901&quot; data-start=&quot;863&quot;&gt;프로토콜이 &lt;b&gt;행동만 정의&lt;/b&gt;하고, 구체적인 타입은 구현체가 지정&lt;/li&gt;
&lt;li data-end=&quot;929&quot; data-start=&quot;902&quot;&gt;다양한 타입에 대해 &lt;b&gt;유연한 추상화 가능&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;982&quot; data-start=&quot;930&quot;&gt;Swift의 &lt;b&gt;Protocol-Oriented Programming&lt;/b&gt; 스타일에 잘 맞음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;993&quot; data-start=&quot;984&quot; data-ke-size=&quot;size23&quot;&gt;⚠️ 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1137&quot; data-start=&quot;995&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1094&quot; data-start=&quot;995&quot;&gt;associatedtype이 있는 프로토콜은 &lt;b&gt;그 자체로 타입으로 사용할 수 없습니다.&lt;/b&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1137&quot; data-start=&quot;1095&quot;&gt;대신 제네릭이나 불명확 타입(some)과 &lt;b&gt;함께 사용해야&lt;/b&gt; 해요.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1752667317487&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let c: Container // ❌ 에러&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-end=&quot;1142&quot; data-start=&quot;1139&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1182&quot; data-start=&quot;1144&quot; data-ke-size=&quot;size26&quot;&gt;2.   제네릭 (Generic) &amp;mdash; 타입과 함수에서의 추상화&lt;/h2&gt;
&lt;h3 data-end=&quot;1193&quot; data-start=&quot;1184&quot; data-ke-size=&quot;size23&quot;&gt;  개념&lt;/h3&gt;
&lt;p data-end=&quot;1276&quot; data-start=&quot;1195&quot; data-ke-size=&quot;size16&quot;&gt;제네릭은 타입을 &lt;b&gt;외부에서 주입받는 형태&lt;/b&gt;의 추상화입니다.&lt;br /&gt;함수, 구조체, 클래스 등에서 사용할 수 있습니다. 이건 많이들 접하셨을거 같아요&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1752667356021&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct Stack&amp;lt;T&amp;gt; {
  var items = [T]()
  mutating func push(_ item: T) {
    items.append(item)
  } 
  mutating func pop() -&amp;gt; T {
    return items.removeLast() 
  } 
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #000000; font-size: 1.44em; letter-spacing: -1px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;  목적&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1567&quot; data-start=&quot;1491&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1524&quot; data-start=&quot;1491&quot;&gt;&lt;b&gt;다양한 타입에 대해 재사용&lt;/b&gt;할 수 있는 유연한 코드&lt;/li&gt;
&lt;li data-end=&quot;1551&quot; data-start=&quot;1525&quot;&gt;&lt;b&gt;성능 최적화에 유리&lt;/b&gt; (정적 디스패치)&lt;/li&gt;
&lt;li data-end=&quot;1567&quot; data-start=&quot;1552&quot;&gt;&lt;b&gt;타입 안정성&lt;/b&gt; 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1572&quot; data-start=&quot;1569&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1624&quot; data-start=&quot;1574&quot; data-ke-size=&quot;size26&quot;&gt;3.   불명확 타입 (Opaque Type, some) &amp;mdash; 타입을 감추되 고정&lt;/h2&gt;
&lt;h3 data-end=&quot;1635&quot; data-start=&quot;1626&quot; data-ke-size=&quot;size23&quot;&gt;  개념&lt;/h3&gt;
&lt;p data-end=&quot;1713&quot; data-start=&quot;1637&quot; data-ke-size=&quot;size16&quot;&gt;some 키워드는 &lt;b&gt;내부 구현 타입은 숨기고, 외부엔 프로토콜만 노출&lt;/b&gt;하는 방식이에요.&lt;br /&gt;SwiftUI에서 많이 보셨을 겁니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1752667403556&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func makeView() -&amp;gt; some View { 
  Text(&quot;Hello&quot;) 
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1829&quot; data-start=&quot;1780&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1829&quot; data-start=&quot;1780&quot;&gt;이 함수는 Text를 반환하지만, 외부에서는 View라는 프로토콜만 보입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1840&quot; data-start=&quot;1831&quot; data-ke-size=&quot;size23&quot;&gt;  목적&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1969&quot; data-start=&quot;1842&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1886&quot; data-start=&quot;1842&quot;&gt;associatedtype이 있는 프로토콜을 반환할 때 쓸 수 있는 방법&lt;/li&gt;
&lt;li data-end=&quot;1922&quot; data-start=&quot;1887&quot;&gt;&lt;b&gt;타입을 감추면서도 정적 디스패치&lt;/b&gt;를 유지 (성능 좋음)&lt;/li&gt;
&lt;li data-end=&quot;1969&quot; data-start=&quot;1923&quot;&gt;복잡한 내부 구현을 숨기고 싶을 때 유용 (예: SwiftUI의 View 반환)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1974&quot; data-start=&quot;1971&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2030&quot; data-start=&quot;1976&quot; data-ke-size=&quot;size26&quot;&gt;4.   실존 타입 (Existential Type, any) &amp;mdash; 다형성을 위한 추상화&lt;/h2&gt;
&lt;h3 data-end=&quot;2041&quot; data-start=&quot;2032&quot; data-ke-size=&quot;size23&quot;&gt;  개념&lt;/h3&gt;
&lt;p data-end=&quot;2135&quot; data-start=&quot;2043&quot; data-ke-size=&quot;size16&quot;&gt;실존 타입이란 &lt;b&gt;프로토콜을 타입처럼 사용하는 것&lt;/b&gt;입니다.&lt;br /&gt;Swift 5.7부터 any Protocol 문법이 도입되었고, &lt;b&gt;6부터는 반드시 명시적으로 써야 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;2135&quot; data-start=&quot;2043&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1752667453159&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protocol Drawable { func draw() }

final class Circle: Drawable { 
  func draw() {
    print(&quot;그림 그려용&quot;) 
  }
} 

let shape: any Drawable = Circle() shape.draw() // ✅ 동적 디스패치&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;2135&quot; data-start=&quot;2043&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;2388&quot; data-start=&quot;2333&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;2388&quot; data-start=&quot;2335&quot; data-ke-size=&quot;size16&quot;&gt;any Drawable은 &lt;b&gt;여러 타입을 하나의 변수로 묶기 위한 런타임 컨테이너&lt;/b&gt;입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-end=&quot;2393&quot; data-start=&quot;2390&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-end=&quot;2419&quot; data-start=&quot;2395&quot; data-ke-size=&quot;size20&quot;&gt;❗ 비교: 실존 타입 없이 쓴 경우&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1752667467762&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let shape = Circle() shape.draw() // ✅ 정적 디스패치&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2565&quot; data-start=&quot;2482&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2546&quot; data-start=&quot;2482&quot;&gt;이건 shape의 타입이 Circle로 &lt;b&gt;정적으로 확정되기 때문에&lt;/b&gt;, 디스패치도 정적으로 이뤄집니다.&lt;/li&gt;
&lt;li data-end=&quot;2565&quot; data-start=&quot;2547&quot;&gt;성능 최적화가 더 잘 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;2570&quot; data-start=&quot;2567&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;2581&quot; data-start=&quot;2572&quot; data-ke-size=&quot;size23&quot;&gt;  목적&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2657&quot; data-start=&quot;2583&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2603&quot; data-start=&quot;2583&quot;&gt;&lt;b&gt;다형성&lt;/b&gt;을 극대화할 수 있음&lt;/li&gt;
&lt;li data-end=&quot;2630&quot; data-start=&quot;2604&quot;&gt;여러 타입을 하나의 타입처럼 다루고 싶을 때&lt;/li&gt;
&lt;li data-end=&quot;2657&quot; data-start=&quot;2631&quot;&gt;런타임에서 타입이 결정되는 유연한 코드 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;2668&quot; data-start=&quot;2659&quot; data-ke-size=&quot;size23&quot;&gt;⚠️ 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2757&quot; data-start=&quot;2670&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2698&quot; data-start=&quot;2670&quot;&gt;&lt;b&gt;동적 디스패치&lt;/b&gt;를 사용 &amp;rarr; 성능 손해 가능&lt;/li&gt;
&lt;li data-end=&quot;2720&quot; data-start=&quot;2699&quot;&gt;&lt;b&gt;타입 인라인 최적화가 어려움&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;2757&quot; data-start=&quot;2721&quot;&gt;Swift 6부터는 any 키워드를 &lt;b&gt;반드시 써야 함&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;2757&quot; data-start=&quot;2721&quot;&gt;&lt;b&gt;실존 타입은 변수 활용에 유연성을 줄 수 있지만, 성능의 아쉬움이 있을 수 있으니 사용시 고려를 해봐야합니다&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;2762&quot; data-start=&quot;2759&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2783&quot; data-start=&quot;2764&quot; data-ke-size=&quot;size26&quot;&gt;  네 가지 방식 비교 정리&lt;/h2&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 150px;&quot; border=&quot;1&quot; data-end=&quot;3174&quot; data-start=&quot;2785&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;associatedtype&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;제네릭&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;opaque&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Existential&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;2982&quot; data-start=&quot;2932&quot;&gt;
&lt;td style=&quot;height: 20px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;2940&quot; data-start=&quot;2932&quot;&gt;사용 위치&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2950&quot; data-start=&quot;2940&quot; data-col-size=&quot;sm&quot;&gt;프로토콜 내부&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2961&quot; data-start=&quot;2950&quot; data-col-size=&quot;sm&quot;&gt;타입/함수 선언&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2969&quot; data-start=&quot;2961&quot; data-col-size=&quot;sm&quot;&gt;반환 타입&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2982&quot; data-start=&quot;2969&quot; data-col-size=&quot;sm&quot;&gt;변수/매개변수 등&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;3024&quot; data-start=&quot;2983&quot;&gt;
&lt;td style=&quot;height: 20px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;2994&quot; data-start=&quot;2983&quot;&gt;타입 결정 시점&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;3001&quot; data-start=&quot;2994&quot; data-col-size=&quot;sm&quot;&gt;채택 시&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;3009&quot; data-start=&quot;3001&quot; data-col-size=&quot;sm&quot;&gt;컴파일 시&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;3017&quot; data-start=&quot;3009&quot; data-col-size=&quot;sm&quot;&gt;컴파일 시&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;3024&quot; data-start=&quot;3017&quot; data-col-size=&quot;sm&quot;&gt;런타임&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot; data-end=&quot;3068&quot; data-start=&quot;3025&quot;&gt;
&lt;td style=&quot;height: 25px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;3034&quot; data-start=&quot;3025&quot;&gt;다형성 지원&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; data-end=&quot;3041&quot; data-start=&quot;3034&quot; data-col-size=&quot;sm&quot;&gt;✅ 가능&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; data-end=&quot;3049&quot; data-start=&quot;3041&quot; data-col-size=&quot;sm&quot;&gt;❌ 제한적&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; data-end=&quot;3057&quot; data-start=&quot;3049&quot; data-col-size=&quot;sm&quot;&gt;❌ 제한적&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; data-end=&quot;3068&quot; data-start=&quot;3057&quot; data-col-size=&quot;sm&quot;&gt;✅ 완전 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot; data-end=&quot;3116&quot; data-start=&quot;3069&quot;&gt;
&lt;td style=&quot;height: 25px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;3079&quot; data-start=&quot;3069&quot;&gt;디스패치 방식&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; data-end=&quot;3089&quot; data-start=&quot;3079&quot; data-col-size=&quot;sm&quot;&gt;정적 + 간접&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; data-end=&quot;3096&quot; data-start=&quot;3089&quot; data-col-size=&quot;sm&quot;&gt;✅ 정적&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; data-end=&quot;3103&quot; data-start=&quot;3096&quot; data-col-size=&quot;sm&quot;&gt;✅ 정적&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; data-end=&quot;3116&quot; data-start=&quot;3103&quot; data-col-size=&quot;sm&quot;&gt;❌ 동적 (느림)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot; data-end=&quot;3142&quot; data-start=&quot;3117&quot;&gt;
&lt;td style=&quot;height: 25px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;3125&quot; data-start=&quot;3117&quot;&gt;타입 감춤&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; data-end=&quot;3129&quot; data-start=&quot;3125&quot; data-col-size=&quot;sm&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; data-end=&quot;3133&quot; data-start=&quot;3129&quot; data-col-size=&quot;sm&quot;&gt;❌&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; data-end=&quot;3137&quot; data-start=&quot;3133&quot; data-col-size=&quot;sm&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; data-end=&quot;3142&quot; data-start=&quot;3137&quot; data-col-size=&quot;sm&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot; data-end=&quot;3174&quot; data-start=&quot;3143&quot;&gt;
&lt;td style=&quot;height: 25px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;3154&quot; data-start=&quot;3143&quot;&gt;배열 등에 담기&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; data-end=&quot;3158&quot; data-start=&quot;3154&quot; data-col-size=&quot;sm&quot;&gt;❌&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; data-end=&quot;3162&quot; data-start=&quot;3158&quot; data-col-size=&quot;sm&quot;&gt;❌&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; data-end=&quot;3166&quot; data-start=&quot;3162&quot; data-col-size=&quot;sm&quot;&gt;❌&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; data-end=&quot;3174&quot; data-start=&quot;3166&quot; data-col-size=&quot;sm&quot;&gt;✅ 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;3179&quot; data-start=&quot;3176&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;3201&quot; data-start=&quot;3181&quot; data-ke-size=&quot;size26&quot;&gt;  언제 어떤 걸 써야 할까?&lt;/h2&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;3502&quot; data-start=&quot;3203&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;상황&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;적합한 방식&lt;span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3297&quot; data-start=&quot;3242&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3268&quot; data-start=&quot;3242&quot;&gt;다양한 타입을 하나의 추상화로 다루고 싶다&lt;/td&gt;
&lt;td data-end=&quot;3297&quot; data-start=&quot;3268&quot; data-col-size=&quot;sm&quot;&gt;associatedtype + Opaque&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3332&quot; data-start=&quot;3298&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3325&quot; data-start=&quot;3298&quot;&gt;재사용 가능한 자료구조, 알고리즘이 필요하다&lt;/td&gt;
&lt;td data-end=&quot;3332&quot; data-start=&quot;3325&quot; data-col-size=&quot;sm&quot;&gt;제네릭&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3386&quot; data-start=&quot;3333&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3376&quot; data-start=&quot;3333&quot;&gt;프로토콜 반환이 필요한데 associatedtype이 있어서 제한된다&lt;/td&gt;
&lt;td data-end=&quot;3386&quot; data-start=&quot;3376&quot; data-col-size=&quot;sm&quot;&gt;Opaque&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3421&quot; data-start=&quot;3387&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3412&quot; data-start=&quot;3387&quot;&gt;여러 타입을 하나의 배열 등에 담고 싶다&lt;/td&gt;
&lt;td data-end=&quot;3421&quot; data-start=&quot;3412&quot; data-col-size=&quot;sm&quot;&gt;Existential&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3460&quot; data-start=&quot;3422&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3451&quot; data-start=&quot;3422&quot;&gt;런타임에 타입이 결정돼야 하고 유연성이 필요하다&lt;/td&gt;
&lt;td data-end=&quot;3460&quot; data-start=&quot;3451&quot; data-col-size=&quot;sm&quot;&gt;Existential&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3502&quot; data-start=&quot;3461&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3485&quot; data-start=&quot;3461&quot;&gt;성능이 중요하고 타입도 정적이어야 한다&lt;/td&gt;
&lt;td data-end=&quot;3502&quot; data-start=&quot;3485&quot; data-col-size=&quot;sm&quot;&gt;제네릭 또는 Opaque&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;3507&quot; data-start=&quot;3504&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;3531&quot; data-start=&quot;3509&quot; data-ke-size=&quot;size26&quot;&gt;✅ 마치며, 결국, 이 모든 기능의 목적은?&lt;/h2&gt;
&lt;blockquote data-end=&quot;331&quot; data-start=&quot;283&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;331&quot; data-start=&quot;285&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 모든 기능은 결국 &amp;ldquo;좋은 소프트웨어 아키텍처&amp;rdquo;를 만들기 위해 존재한다.&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;404&quot; data-start=&quot;333&quot; data-ke-size=&quot;size16&quot;&gt;라고 생각합니다.&lt;/p&gt;
&lt;p data-end=&quot;404&quot; data-start=&quot;333&quot; data-ke-size=&quot;size16&quot;&gt;이런 추상화 기능들은 각각의 방식으로 우리가 &lt;b&gt;더 유연하고, 안전하며, 유지보수가 쉬운 코드를 작성&lt;/b&gt;할 수 있게 도와줍니다.&lt;/p&gt;
&lt;h3 data-end=&quot;438&quot; data-start=&quot;411&quot; data-ke-size=&quot;size23&quot;&gt;  추상화를 통해 우리가 얻게 되는 것들&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;748&quot; data-start=&quot;440&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;480&quot; data-start=&quot;440&quot;&gt;✅ &lt;b&gt;유연한 설계&lt;/b&gt;: 타입을 고정하지 않고 바꿔 쓸 수 있는 구조&lt;/li&gt;
&lt;li data-end=&quot;530&quot; data-start=&quot;481&quot;&gt;✅ &lt;b&gt;구현과 인터페이스의 분리&lt;/b&gt;: 내부 구현을 몰라도 외부에서 쓸 수 있는 캡슐화&lt;/li&gt;
&lt;li data-end=&quot;571&quot; data-start=&quot;531&quot;&gt;✅ &lt;b&gt;타입 안전성&lt;/b&gt;: 컴파일 타임에 오류를 잡아낼 수 있는 강력함&lt;/li&gt;
&lt;li data-end=&quot;611&quot; data-start=&quot;572&quot;&gt;✅ &lt;b&gt;재사용성&lt;/b&gt;: 반복되는 패턴 없이 다양한 상황에서 활용 가능&lt;/li&gt;
&lt;li data-end=&quot;654&quot; data-start=&quot;612&quot;&gt;✅ &lt;b&gt;결합도 감소&lt;/b&gt;: 모듈 간 의존을 줄이고 테스트/확장이 쉬운 구조&lt;/li&gt;
&lt;li data-end=&quot;696&quot; data-start=&quot;655&quot;&gt;✅ &lt;b&gt;테스트 용이성&lt;/b&gt;: 추상화 덕분에 mocking, DI가 쉬워짐&lt;/li&gt;
&lt;li data-end=&quot;748&quot; data-start=&quot;697&quot;&gt;✅ &lt;b&gt;성능과 구조 간의 균형 조율&lt;/b&gt;: 런타임 유연성과 정적 최적화 사이에서 선택 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;753&quot; data-start=&quot;750&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;782&quot; data-start=&quot;755&quot; data-ke-size=&quot;size23&quot;&gt;  아키텍처 관점에서 본 각 기능의 역할&lt;/h3&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;1042&quot; data-start=&quot;784&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;기능&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;아키텍처적 가치&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;880&quot; data-start=&quot;830&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;849&quot; data-start=&quot;830&quot;&gt;associatedtype&lt;/td&gt;
&lt;td data-end=&quot;880&quot; data-start=&quot;849&quot; data-col-size=&quot;md&quot;&gt;구현체에 의존하지 않고 유연한 프로토콜 설계 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;919&quot; data-start=&quot;881&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;887&quot; data-start=&quot;881&quot;&gt;제네릭&lt;/td&gt;
&lt;td data-end=&quot;919&quot; data-start=&quot;887&quot; data-col-size=&quot;md&quot;&gt;다양한 타입에 대해 재사용 가능, 타입 안정성 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;980&quot; data-start=&quot;920&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;938&quot; data-start=&quot;920&quot;&gt;오파큐 타입 (some)&lt;/td&gt;
&lt;td data-end=&quot;980&quot; data-start=&quot;938&quot; data-col-size=&quot;md&quot;&gt;구현을 감추면서도 성능을 확보 (SwiftUI에서 핵심적으로 사용됨)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1042&quot; data-start=&quot;981&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;997&quot; data-start=&quot;981&quot;&gt;실존 타입 (any)&lt;/td&gt;
&lt;td data-end=&quot;1042&quot; data-start=&quot;997&quot; data-col-size=&quot;md&quot;&gt;다형성과 런타임 유연성 확보, UI 컴포넌트/플러그인/전략 패턴 등에 유용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전엔 &amp;ldquo;왜 이렇게 복잡하게 만들었지?&amp;rdquo; 싶었던 기능들도,&lt;br /&gt;이제는 코드의 구조적인 개선과 실전 유지보수의 편안함을 위해 마련된 장치라는 걸 느끼게 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;227&quot; data-start=&quot;130&quot; data-ke-size=&quot;size16&quot;&gt;아직 실질적으로 구조화해서 본격적으로 사용할 기회는 많지 않았지만,&lt;br /&gt;앞으로 다가올 상황을 위해 이 개념들을 이해할 필요가 있다고 느꼈습니다.&lt;/p&gt;</description>
      <category>TIL</category>
      <category>SWIFT</category>
      <category>swift associated type</category>
      <category>swift existential type</category>
      <category>swift opaque type</category>
      <author>Kim Roks</author>
      <guid isPermaLink="true">https://roks-apps.tistory.com/86</guid>
      <comments>https://roks-apps.tistory.com/86#entry86comment</comments>
      <pubDate>Wed, 16 Jul 2025 21:14:14 +0900</pubDate>
    </item>
    <item>
      <title>[Swift UIKit] CornerRadius 적용시 의문점 정리</title>
      <link>https://roks-apps.tistory.com/85</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt;  cornerRadius란?&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;cornerRadius는 iOS의 모든 UIView가 갖고 있는 CALayer의 속성 중 하나로, 뷰의 모서리를 둥글게 만들어줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; 숫자로 지정한 반지름(radius)만큼 모서리가 잘려 둥근 사각형 형태로 렌더링되죠. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어, 버튼이나 카드 뷰에 부드러운 곡선을 주고 싶을 때 유용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;let roundedImageView: UIImageView = {
	let imageView = UIImageView()
    imageView.backgroundColor = .systemBlue
    imageView.layer.cornerRadius = 12  // 모서리를 12pt 만큼 둥글게
    imageView.clipsToBounds = true    // 내부 콘텐츠가 넘어가지 않도록 마스킹
    
    return imageView
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;위 코드는 &lt;/span&gt;&lt;span&gt;UIView&lt;/span&gt;&lt;span&gt;를 생성한 뒤, &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;cornerRadius&lt;/span&gt;&lt;span&gt;를 설정하고 &lt;/span&gt;&lt;b&gt;&lt;span&gt;clipsToBounds = true&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;b&gt;를 추가&lt;/b&gt;하여 뷰 안의 자식 뷰나 이미지가 경계를 넘어 그려지지 않도록 처리합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt;  그런데 가끔 왜 clipsToBounds를 true로 하지 않아도 됐는가?&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;CornerRadius를 사용하고, &lt;b&gt;`clipToBounds`&lt;/b&gt; 속성을 수정하지않았는데도 테두리가 둥글어진 경험이 있지않나요?(저는 있..)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;UIView는 자체적으로 CALayer를 가지고 있습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 &lt;/span&gt;&lt;span&gt;layer.cornerRadius&lt;/span&gt;&lt;span&gt;만 설정해도 &lt;b&gt;뷰의 배경색(backgroundColor)&lt;/b&gt;과 &lt;b&gt;테두리(border)&lt;/b&gt;는 해당 레이어 형태 그대로 둥글게 그려집니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이때 자식 뷰나 이미지 콘텐츠가 뷰 경계 밖으로 그려지지 않는 한, 마스크 없이도 외형상 둥근 모서리가 적용된 것처럼 보이기 때문에 &lt;/span&gt;&lt;span&gt;clipsToBounds&lt;/span&gt;&lt;span&gt;를 생략해도 기능상 큰 문제가 없다고 느껴질 때가 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉, &lt;/span&gt;&lt;span&gt;cornerRadius&lt;/span&gt;&lt;span&gt;는 레이어의 &quot;모양(shape)&quot;을 변경하는 것이고, &lt;/span&gt;&lt;span&gt;clipsToBounds&lt;/span&gt;&lt;span&gt;는 &lt;/span&gt;&lt;span&gt;&lt;b&gt;하위 콘텐츠를 잘라낼지 여부&lt;/b&gt;&lt;/span&gt;&lt;span&gt;만 제어할 뿐이어서, 콘텐츠가 경계를 침범하지 않는 경우라면 생략해도 시각적으로는 둥근 모서리가 유지됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt;☝️ layer.masksToBounds 속성으로도 가능하던데?&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;clipsToBounds&lt;/span&gt;&lt;span&gt;는 UIView 레벨에서 동작하는 반면, &lt;/span&gt;&lt;span&gt;masksToBounds&lt;/span&gt;&lt;span&gt;는 CALayer 속성입니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;두 속성은 내부적으로 동일한 역할(&lt;/span&gt;&lt;span&gt;layer.masksToBounds = true&lt;/span&gt;&lt;span&gt;)을 수행하지만, 적용 대상이 다릅니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;view.clipsToBounds = true&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;뷰의 하위 뷰(subviews)와 콘텐츠만 잘라냅니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;view.layer.masksToBounds = true&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;레이어 단위로, 그림자(shadow), 그라디언트, 서브레이어까지 모두 마스킹합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;특히 그림자 효과를 동시에 쓰고 싶다면, &lt;/span&gt;&lt;span&gt;masksToBounds = true&lt;/span&gt;&lt;span&gt;로 설정하면 그림자가 잘려 나갈 수 있으므로, 뷰 레벨에서 &lt;/span&gt;&lt;span&gt;clipsToBounds&lt;/span&gt;&lt;span&gt;만 사용하는 것이 좋습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;용도에 따라 두 속성을 정확히 구분해 사용여부를 결정하면 UI 품질과 성능을 모두 만족시키는 결과를 얻을 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;166&quot; data-start=&quot;124&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✅ clipsToBounds vs masksToBounds 정리&lt;/b&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;451&quot; data-start=&quot;168&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;속성&lt;/td&gt;
&lt;td&gt;정의&lt;/td&gt;
&lt;td&gt;대상&lt;/td&gt;
&lt;td&gt;용도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;341&quot; data-start=&quot;252&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;271&quot; data-start=&quot;252&quot;&gt;clipsToBounds&lt;/td&gt;
&lt;td data-end=&quot;286&quot; data-start=&quot;271&quot; data-col-size=&quot;sm&quot;&gt;UIView의 속성&lt;/td&gt;
&lt;td data-end=&quot;319&quot; data-start=&quot;286&quot; data-col-size=&quot;sm&quot;&gt;뷰(View) 자체의 &lt;b&gt;서브뷰(하위 뷰)&lt;/b&gt; 잘라내기&lt;/td&gt;
&lt;td data-end=&quot;341&quot; data-start=&quot;319&quot; data-col-size=&quot;sm&quot;&gt;일반적으로 &lt;b&gt;iOS에서 사용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;451&quot; data-start=&quot;342&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;361&quot; data-start=&quot;342&quot;&gt;masksToBounds&lt;/td&gt;
&lt;td data-end=&quot;377&quot; data-start=&quot;361&quot; data-col-size=&quot;sm&quot;&gt;CALayer의 속성&lt;/td&gt;
&lt;td data-end=&quot;418&quot; data-start=&quot;377&quot; data-col-size=&quot;sm&quot;&gt;레이어(Layer)의 &lt;b&gt;하위 레이어&lt;/b&gt;나 &lt;b&gt;그림자/모서리&lt;/b&gt; 처리&lt;/td&gt;
&lt;td data-end=&quot;451&quot; data-start=&quot;418&quot; data-col-size=&quot;sm&quot;&gt;&lt;b&gt;Core Animation 관련 처리 시 사용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;</description>
      <category>TIL</category>
      <category>cornerRadius</category>
      <category>SWIFT</category>
      <category>UIKit</category>
      <author>Kim Roks</author>
      <guid isPermaLink="true">https://roks-apps.tistory.com/85</guid>
      <comments>https://roks-apps.tistory.com/85#entry85comment</comments>
      <pubDate>Mon, 19 May 2025 16:14:05 +0900</pubDate>
    </item>
    <item>
      <title>[Swift] iOS 18.4 를 앞두고 알아야 할 SceneDelegate 기반 앱 구조 전환</title>
      <link>https://roks-apps.tistory.com/84</link>
      <description>&lt;p data-end=&quot;258&quot; data-start=&quot;158&quot; data-ke-size=&quot;size16&quot;&gt;iOS 앱 개발자라면 누구나 익숙한 AppDelegate 방식. 앱 생명주기의 중심이었죠.&lt;/p&gt;
&lt;p data-end=&quot;258&quot; data-start=&quot;158&quot; data-ke-size=&quot;size16&quot;&gt;그런데 이제는&amp;hellip; &lt;b&gt;Apple이 SceneDelegate를 강제하려고 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;314&quot; data-start=&quot;260&quot; data-ke-size=&quot;size16&quot;&gt;정말 AppDelegate는 사라지는 걸까요?&lt;br /&gt;그리고 우리는 지금 무엇을 준비해야 할까요?&lt;/p&gt;
&lt;hr data-end=&quot;319&quot; data-start=&quot;316&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;346&quot; data-start=&quot;321&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  SceneDelegate가 뭐길래?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;469&quot; data-start=&quot;348&quot; data-ke-size=&quot;size16&quot;&gt;iOS 13부터 도입된 &lt;b&gt;Scene 기반 Life Cycle&lt;/b&gt;은 앱을 하나 이상의 &quot;Scene&quot;으로 나누어 관리할 수 있게 해줍니다.&lt;br /&gt;간단히 말하면, &lt;b&gt;앱의 UI 상태를 독립적으로 관리할 수 있는 구조&lt;/b&gt;죠.&lt;/p&gt;
&lt;p data-end=&quot;469&quot; data-start=&quot;348&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;예를 들어 iPad에서 여러 개의 앱 창(윈도우)을 동시에 띄울 수 있는 것도 Scene 구조 덕분입니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-end=&quot;563&quot; data-start=&quot;539&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  그런데 왜 지금 이렇게 중요한가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;600&quot; data-start=&quot;565&quot; data-ke-size=&quot;size16&quot;&gt;Apple은 iOS 18.4부터 다음 조건을 &lt;b&gt;강제&lt;/b&gt;합니다&lt;/p&gt;
&lt;p data-end=&quot;600&quot; data-start=&quot;565&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;✅ Info.plist에 UIApplicationSceneManifest 키가 반드시 있어야 함&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;700&quot; data-start=&quot;604&quot; data-ke-size=&quot;size16&quot;&gt;✅ 앱은 최소 하나 이상의 UIScene을 선언해야 함&lt;/p&gt;
&lt;p data-end=&quot;753&quot; data-start=&quot;702&quot; data-ke-size=&quot;size16&quot;&gt;즉, SceneDelegate를 &lt;b&gt;도입하지 않으면 앱이 실행되지 않을 수 있습니다&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;753&quot; data-start=&quot;702&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1115&quot; data-start=&quot;1089&quot; data-ke-size=&quot;size26&quot;&gt; ️ 기존 프로젝트는 어떻게 해야 할까?&lt;/h2&gt;
&lt;h3 data-end=&quot;1153&quot; data-start=&quot;1117&quot; data-ke-size=&quot;size23&quot;&gt;1. SceneDelegate.swift 파일 추가&lt;/h3&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;pre id=&quot;code_1747098875189&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // UIWindow 설정 및 초기 뷰컨트롤러 연결
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-end=&quot;1464&quot; data-start=&quot;1430&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-end=&quot;1464&quot; data-start=&quot;1430&quot; data-ke-size=&quot;size23&quot;&gt;2. Info.plist에 Scene 설정 추가&lt;/h3&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h3 data-end=&quot;2041&quot; data-start=&quot;2010&quot; data-ke-size=&quot;size23&quot;&gt;3. AppDelegate.swift 정리&lt;/h3&gt;
&lt;p data-end=&quot;2109&quot; data-start=&quot;2042&quot; data-ke-size=&quot;size16&quot;&gt;application(_:configurationForConnecting:) 메서드를 구현해 Scene을 구성합니다.&lt;/p&gt;
&lt;h2 data-end=&quot;2141&quot; data-start=&quot;2116&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✨ SwiftUI는 이미 Scene 기반&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;2284&quot; data-start=&quot;2143&quot; data-ke-size=&quot;size16&quot;&gt;SwiftUI 앱은 이미 @main과 WindowGroup 등을 통해 Scene 구조를 사용 중입니다.&lt;br /&gt;따라서 SwiftUI 앱이라면 걱정할 필요는 없습니다. 다만 일부 UIKit 연동 시 Scene 구조를 명확히 이해해 두는 것이 좋습니다.&lt;/p&gt;
&lt;p data-end=&quot;2284&quot; data-start=&quot;2143&quot; data-ke-size=&quot;size16&quot;&gt;SwiftUI의 표준화는 좀 더 걸릴거라고 생각중이었는데 이것도..?&lt;/p&gt;
&lt;p data-end=&quot;2284&quot; data-start=&quot;2143&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;2311&quot; data-start=&quot;2291&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✅ 마무리: 지금 바로 점검하자&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;2405&quot; data-start=&quot;2313&quot; data-ke-size=&quot;size16&quot;&gt;이제 더 이상 &lt;b&gt;SceneDelegate는 선택이 아닙니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2500&quot; data-start=&quot;2407&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2437&quot; data-start=&quot;2407&quot;&gt;SceneDelegate.swift 도입&lt;/li&gt;
&lt;li data-end=&quot;2474&quot; data-start=&quot;2438&quot;&gt;Info.plist에 SceneManifest 설정&lt;/li&gt;
&lt;li data-end=&quot;2500&quot; data-start=&quot;2475&quot;&gt;AppDelegate에서 역할 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2558&quot; data-start=&quot;2502&quot; data-ke-size=&quot;size16&quot;&gt;  아래 문서 원문에서 더 자세한 내용을 다루고 있습니다 마이그레이션 가이드 또한 제공하니 확인하세요&lt;/p&gt;
&lt;p data-end=&quot;2558&quot; data-start=&quot;2502&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/technotes/tn3187-migrating-to-the-uikit-scene-based-life-cycle&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/documentation/technotes/tn3187-migrating-to-the-uikit-scene-based-life-cycle&lt;/a&gt;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>scene delegate</category>
      <category>SWIFT</category>
      <author>Kim Roks</author>
      <guid isPermaLink="true">https://roks-apps.tistory.com/84</guid>
      <comments>https://roks-apps.tistory.com/84#entry84comment</comments>
      <pubDate>Tue, 13 May 2025 10:17:41 +0900</pubDate>
    </item>
    <item>
      <title>[Swift UIKit] Button 터치 피드백이 안보이는 경우</title>
      <link>https://roks-apps.tistory.com/83</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  개요&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱을 만들 때 Button은 필수적이고, 자동으로 터치 피드백을 제공합니다.(눌렸을 때 색깔이 바뀐다던가)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 종종 버튼을 눌렀을 때 &lt;b&gt;아무런 반응이 없는 것처럼 보이는 문제&lt;/b&gt;를 겪게 되죠.&lt;/p&gt;
&lt;p data-end=&quot;313&quot; data-start=&quot;290&quot; data-ke-size=&quot;size16&quot;&gt;그럴 땐 대부분 아래 이유 중 하나입니다.&lt;/p&gt;
&lt;hr data-end=&quot;318&quot; data-start=&quot;315&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;363&quot; data-start=&quot;320&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  1. AttributedString에서 색상을 직접 지정한 경우(configuraton 사용 시)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;396&quot; data-start=&quot;365&quot; data-ke-size=&quot;size16&quot;&gt;configuration 사용 시 폰트를 지정할 때 AttriibutedString을 사용하게 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;396&quot; data-start=&quot;365&quot; data-ke-size=&quot;size16&quot;&gt;그때 foregroundColor 또한 지정이 가능하며 이것으로 글자색을 바꿀 수도 있습니다.&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;pre id=&quot;code_1746178471315&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let attributedTitle = AttributedString(
    &quot;이미지&quot;,
    attributes: AttributeContainer([
        .font: UIFont.et_pretendard(style: .medium, size: 18),
        .foregroundColor: UIColor.et_textColor5
    ])
)
configuration.attributedTitle = attributedTitle&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;757&quot; data-start=&quot;668&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;757&quot; data-start=&quot;668&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 하면 터치됐을 때 버튼의 상태가. highlighted로 바뀌어도 텍스트 색상은 그대로입니다.&lt;/p&gt;
&lt;p data-end=&quot;757&quot; data-start=&quot;668&quot; data-ke-size=&quot;size16&quot;&gt;시스템이 자동으로 상태별 색을 조정하지 못해요&lt;/p&gt;
&lt;h3 data-end=&quot;770&quot; data-start=&quot;759&quot; data-ke-size=&quot;size23&quot;&gt;✅ 해결 방법&lt;/h3&gt;
&lt;pre id=&quot;code_1746178543323&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var attributedTitle = AttributedString(&quot;이미지&quot;)
attributedTitle.font = UIFont....
configuration.attributedTitle = attributedTitle
configuration.baseForegroundColor = UIColor...&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AttributedStrign이 아닌, configuration의 컬러를 바꿔줌으로 해결&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1004&quot; data-start=&quot;1001&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-end=&quot;1339&quot; data-start=&quot;1296&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1386&quot; data-start=&quot;1346&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  2. 버튼 위에 다른 뷰가 덮여 있는 경우 (터치 전달 문제)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1462&quot; data-start=&quot;1388&quot; data-ke-size=&quot;size16&quot;&gt;가장 흔하지만 간과되는 문제입니다. 버튼은 잘 만들어졌지만, &lt;b&gt;다른 뷰가 버튼 위에 있어서 터치 이벤트가 가로 차일 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-end=&quot;1476&quot; data-start=&quot;1464&quot; data-ke-size=&quot;size23&quot;&gt;  체크리스트&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1689&quot; data-start=&quot;1478&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1572&quot; data-start=&quot;1478&quot;&gt;버튼 위에 UIView, UILabel, UIImageView 등이 isUserInteractionEnabled = true로 되어 있고, 투명한 경우&lt;/li&gt;
&lt;li data-end=&quot;1622&quot; data-start=&quot;1573&quot;&gt;오토레이아웃 제약이 잘못돼서 버튼이 실제로는 안 보이는 곳에 있거나, 작게 눌려 있음&lt;/li&gt;
&lt;li data-end=&quot;1689&quot; data-start=&quot;1623&quot;&gt;clipsToBounds, alpha, hidden 속성으로 인해 버튼이 뷰 계층상 보이지만 동작 안 함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1702&quot; data-start=&quot;1691&quot; data-ke-size=&quot;size23&quot;&gt;✅ 해결 방법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1914&quot; data-start=&quot;1704&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1767&quot; data-start=&quot;1704&quot;&gt;&lt;b&gt;Xcode의 Debug View Hierarchy&lt;/b&gt;를 열어서 버튼 위에 어떤 뷰가 있는지 시각적으로 확인&lt;/li&gt;
&lt;li data-end=&quot;1818&quot; data-start=&quot;1768&quot;&gt;덮는 뷰가 있다면 isUserInteractionEnabled = false로 설정&lt;/li&gt;
&lt;li data-end=&quot;1914&quot; data-start=&quot;1819&quot;&gt;필요시 버튼을 superview의 가장 앞쪽으로 올림:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1746178636666&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;view.bringSubviewToFront(addLinkButton)&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-end=&quot;1919&quot; data-start=&quot;1916&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1961&quot; data-start=&quot;1921&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  3. 버튼 자체가 지나치게 작거나 레이아웃이 어긋남&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;2020&quot; data-start=&quot;1963&quot; data-ke-size=&quot;size16&quot;&gt;버튼 레이아웃이 너무 타이트하면 눌렀을 때 효과도 작게 느껴지거나 아예 반응이 없어 보일 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;2020&quot; data-start=&quot;1963&quot; data-ke-size=&quot;size16&quot;&gt;또한 레이아웃이 완벽히 잡혀있지 않으면 버튼 피드백이 보이지 않을 수 있습니다.&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-start=&quot;1691&quot; data-end=&quot;1702&quot;&gt;✅ 해결 방법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;레이아웃 점검하기!&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;2222&quot; data-start=&quot;2219&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2257&quot; data-start=&quot;2224&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  정리: 버튼 터치 피드백이 안 보일 때 체크리스트&lt;/b&gt;&lt;/h2&gt;
&lt;div&gt;&lt;span data-state=&quot;closed&quot;&gt;&lt;/span&gt;
&lt;div&gt;&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 100px;&quot; border=&quot;1&quot; data-end=&quot;2764&quot; data-start=&quot;2259&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;2764&quot; data-start=&quot;2369&quot;&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;2437&quot; data-start=&quot;2369&quot;&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2408&quot; data-start=&quot;2369&quot;&gt;.attributedTitle에서 색상 지정&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2437&quot; data-start=&quot;2408&quot;&gt;baseForegroundColor만 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;2511&quot; data-start=&quot;2438&quot;&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2476&quot; data-start=&quot;2438&quot;&gt;배경 피드백 없음&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2511&quot; data-start=&quot;2476&quot;&gt;backgroundColorTransformer 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;2619&quot; data-start=&quot;2512&quot;&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2549&quot; data-start=&quot;2512&quot;&gt;다른 뷰가 덮고 있음&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2619&quot; data-start=&quot;2549&quot;&gt;Debug View Hierarchy 확인, 덮는 뷰 isUserInteractionEnabled = false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;2691&quot; data-start=&quot;2620&quot;&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2653&quot; data-start=&quot;2620&quot;&gt;버튼이 너무 작거나 인셋 없음&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2691&quot; data-start=&quot;2653&quot;&gt;contentInsets, imagePadding 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;2764&quot; data-start=&quot;2692&quot;&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2720&quot; data-start=&quot;2692&quot;&gt;버튼이 안 보이는 계층 구조에 위치해 있음&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2764&quot; data-start=&quot;2720&quot;&gt;bringSubviewToFront(button) 또는 레이아웃 수정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-end=&quot;2769&quot; data-start=&quot;2766&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2779&quot; data-start=&quot;2771&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✨ 마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;2963&quot; data-start=&quot;2899&quot; data-ke-size=&quot;size16&quot;&gt;버튼이 눌리는 동작은 정상적으로 수행되지만, &lt;b&gt;터치 피드백이 사용자에게 보이지 않는 경우&lt;/b&gt;가 있습니다.&lt;br /&gt;이러한 시각적 피드백은 &lt;b&gt;사용자 경험(UX)에 있어 매우 중요한 요소&lt;/b&gt;입니다.&lt;br /&gt;버튼을 눌렀을 때 아무런 변화가 없다면, 사용자 입장에서는 &quot;정말 눌린 게 맞나?&quot; 하는 불안감을 느낄 수 있기 때문이죠.&lt;/p&gt;
&lt;p data-end=&quot;409&quot; data-start=&quot;234&quot; data-ke-size=&quot;size16&quot;&gt;기존에 잘 작동하던 버튼이 갑자기 피드백이 보이지 않게 되었다면, &lt;b&gt;어딘가 실수가 있었던 것&lt;/b&gt;일 가능성이 높습니다.&lt;br /&gt;이럴 때는 단순히 &quot;작동하니까 괜찮다&quot;는 생각보다, &lt;b&gt;왜 피드백이 사라졌는지 하나씩 체크해 보는 습관&lt;/b&gt;을 들이는 것이 좋습니다.&lt;br /&gt;작은 디테일이 전체 사용자 경험의 완성도를 결정짓습니다.&lt;/p&gt;</description>
      <category>Trouble Shooting</category>
      <category>SWIFT</category>
      <category>UIKit</category>
      <category>버튼 피드백</category>
      <author>Kim Roks</author>
      <guid isPermaLink="true">https://roks-apps.tistory.com/83</guid>
      <comments>https://roks-apps.tistory.com/83#entry83comment</comments>
      <pubDate>Fri, 2 May 2025 18:42:47 +0900</pubDate>
    </item>
    <item>
      <title>[Swift Concurrency] 구조적 동시성의 취소(feat: 명시,암시적 취소전파)</title>
      <link>https://roks-apps.tistory.com/82</link>
      <description>&lt;h2 data-end=&quot;275&quot; data-start=&quot;256&quot; data-ke-size=&quot;size26&quot;&gt;✅ 구조적 동시성과 작업 취소&lt;/h2&gt;
&lt;h3 data-end=&quot;291&quot; data-start=&quot;277&quot; data-ke-size=&quot;size23&quot;&gt;구조적 동시성이란?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;440&quot; data-start=&quot;292&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;352&quot; data-start=&quot;292&quot;&gt;부모 Task가 종료되기 전까지, 그 아래에서 생성된 자식 Task들도 &lt;b&gt;모두 종료되어야 하는&lt;/b&gt; 규칙&lt;/li&gt;
&lt;li data-end=&quot;408&quot; data-start=&quot;353&quot;&gt;Swift의 async let, withTaskGroup 이 대표적인 구조적 동시성 도구&lt;/li&gt;
&lt;li data-end=&quot;440&quot; data-start=&quot;409&quot;&gt;취소는 &lt;b&gt;위에서 아래로 전파&lt;/b&gt;됨 (부모 &amp;rarr; 자식)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-end=&quot;480&quot; data-start=&quot;442&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;480&quot; data-start=&quot;444&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;즉, 부모가 취소되면, 구조적으로 연결된 자식들도 자동으로 취소됨&lt;/b&gt;&lt;/u&gt;&lt;br /&gt;구조적 동시성에 관한 이전 글 ) &lt;a href=&quot;https://roks-apps.tistory.com/76&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://roks-apps.tistory.com/76&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-end=&quot;485&quot; data-start=&quot;482&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;510&quot; data-start=&quot;487&quot; data-ke-size=&quot;size26&quot;&gt;✅ async let의 작업 취소&lt;/h2&gt;
&lt;h3 data-end=&quot;518&quot; data-start=&quot;512&quot; data-ke-size=&quot;size23&quot;&gt;특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;671&quot; data-start=&quot;519&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;572&quot; data-start=&quot;519&quot;&gt;async let 은 선언된 순간 비동기 작업을 시작하고, 자동으로 취소 전파 대상이 됨&lt;/li&gt;
&lt;li data-end=&quot;623&quot; data-start=&quot;573&quot;&gt;await 하기 전에 부모 Task가 취소되면, async let 작업도 취소됨&lt;/li&gt;
&lt;li data-end=&quot;671&quot; data-start=&quot;624&quot;&gt;특별히 .cancel() 하지 않아도 &lt;b&gt;자동으로 정리(cleanup)&lt;/b&gt; 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;679&quot; data-start=&quot;673&quot; data-ke-size=&quot;size23&quot;&gt;예시&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1744962872462&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func loadData() async {
    async let user = fetchUser()
    async let posts = fetchPosts()

    do {
        let result = try await (user, posts)
        print(&quot;결과: \(result)&quot;)
    } catch is CancellationError {
        print(&quot;작업이 취소되었습니다.&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1067&quot; data-start=&quot;946&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1011&quot; data-start=&quot;946&quot;&gt;위에서 loadData() 가 취소되면 fetchUser() 와 fetchPosts() 도 같이 취소됨&lt;/li&gt;
&lt;li data-end=&quot;1067&quot; data-start=&quot;1012&quot;&gt;try await 하는 시점에서 취소되었으면, CancellationError 가 던져짐&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1072&quot; data-start=&quot;1069&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1097&quot; data-start=&quot;1074&quot; data-ke-size=&quot;size26&quot;&gt;✅ TaskGroup의 작업 취소&lt;/h2&gt;
&lt;h3 data-end=&quot;1105&quot; data-start=&quot;1099&quot; data-ke-size=&quot;size23&quot;&gt;특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1315&quot; data-start=&quot;1106&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1185&quot; data-start=&quot;1106&quot;&gt;withTaskGroup 또는 withThrowingTaskGroup 내에서 만든 Task들은 &lt;b&gt;부모 TaskGroup에 소속&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1241&quot; data-start=&quot;1186&quot;&gt;부모가 cancelAll() 호출하면, &lt;b&gt;그 시점 이후 추가된/미완료된 작업들이 취소됨&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1315&quot; data-start=&quot;1242&quot;&gt;작업 내부에서는 Task.isCancelled 이나 Task.checkCancellation()로 반응하도록 구현해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1323&quot; data-start=&quot;1317&quot; data-ke-size=&quot;size23&quot;&gt;예시&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1984&quot; data-start=&quot;1876&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1919&quot; data-start=&quot;1876&quot;&gt;group.cancelAll()은 &lt;b&gt;이미 끝난 작업에는 영향 없음&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1984&quot; data-start=&quot;1920&quot;&gt;각 작업 내부에서 &lt;b&gt;직접적으로 취소에 반응하도록 처리&lt;/b&gt;해야 함 (Task.isCancelled 등 사용)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1744962722099&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func processTasks() async {
    await withTaskGroup(of: Void.self) {
        group in for i in 0..&amp;lt;5 {
            group.addTask {
                try? await Task.sleep(nanoseconds: UInt64(i) * 500_000_000)
                if Task.isCancelled {
                    print(&quot;Task \(i) 취소됨&quot;)
                    return
                }
                print(&quot;Task \(i) 완료&quot;)
            }
        } // 1초 후 취소 try? await Task.sleep(nanoseconds: 1_000_000_000) group.cancelAll() // -&amp;gt; 아직 끝나지 않은 Task 들에 취소 요청 }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-end=&quot;1989&quot; data-start=&quot;1986&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2001&quot; data-start=&quot;1991&quot; data-ke-size=&quot;size26&quot;&gt;✅ 비교 정리&lt;/h2&gt;
&lt;div&gt;&lt;span data-state=&quot;closed&quot;&gt;&lt;/span&gt;
&lt;div&gt;항목async letTaskGroup
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;2311&quot; data-start=&quot;2003&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;2311&quot; data-start=&quot;2075&quot;&gt;
&lt;tr data-end=&quot;2094&quot; data-start=&quot;2075&quot;&gt;
&lt;td data-end=&quot;2085&quot; data-start=&quot;2075&quot;&gt;구조적 동시성&lt;/td&gt;
&lt;td data-end=&quot;2089&quot; data-start=&quot;2085&quot;&gt;✅&lt;/td&gt;
&lt;td data-end=&quot;2094&quot; data-start=&quot;2089&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2146&quot; data-start=&quot;2095&quot;&gt;
&lt;td data-end=&quot;2103&quot; data-start=&quot;2095&quot;&gt;취소 전파&lt;/td&gt;
&lt;td data-end=&quot;2119&quot; data-start=&quot;2103&quot;&gt;부모 취소 시 자동 전파&lt;/td&gt;
&lt;td data-end=&quot;2146&quot; data-start=&quot;2119&quot;&gt;cancelAll() 호출로 수동 전파&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2209&quot; data-start=&quot;2147&quot;&gt;
&lt;td data-end=&quot;2161&quot; data-start=&quot;2147&quot;&gt;자식 작업 취소 처리&lt;/td&gt;
&lt;td data-end=&quot;2166&quot; data-start=&quot;2161&quot;&gt;자동&lt;/td&gt;
&lt;td data-end=&quot;2209&quot; data-start=&quot;2166&quot;&gt;수동 (isCancelled, checkCancellation)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2271&quot; data-start=&quot;2210&quot;&gt;
&lt;td data-end=&quot;2218&quot; data-start=&quot;2210&quot;&gt;예외 전파&lt;/td&gt;
&lt;td data-end=&quot;2234&quot; data-start=&quot;2218&quot;&gt;가능 (자동 throw)&lt;/td&gt;
&lt;td data-end=&quot;2271&quot; data-start=&quot;2234&quot;&gt;가능 (withThrowingTaskGroup 사용 시)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2311&quot; data-start=&quot;2272&quot;&gt;
&lt;td data-end=&quot;2280&quot; data-start=&quot;2272&quot;&gt;결과 수집&lt;/td&gt;
&lt;td data-end=&quot;2294&quot; data-start=&quot;2280&quot;&gt;튜플 등으로 간단하게&lt;/td&gt;
&lt;td data-end=&quot;2311&quot; data-start=&quot;2294&quot;&gt;루프나 배열로 직접 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;2316&quot; data-start=&quot;2313&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;2330&quot; data-start=&quot;2318&quot; data-ke-size=&quot;size23&quot;&gt;  요약&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2471&quot; data-start=&quot;2332&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2375&quot; data-start=&quot;2332&quot;&gt;async let: &lt;b&gt;간결&lt;/b&gt;, 취소 자동 처리, 에러 자동 전파&lt;/li&gt;
&lt;li data-end=&quot;2425&quot; data-start=&quot;2376&quot;&gt;TaskGroup: &lt;b&gt;유연&lt;/b&gt;, 작업 수동 취소 가능, 복잡한 컨트롤에 적합&lt;/li&gt;
&lt;li data-end=&quot;2471&quot; data-start=&quot;2426&quot;&gt;둘 다 &lt;b&gt;부모 취소 시 자식도 함께 취소됨&lt;/b&gt; &amp;rarr; 결국 이게 구조적 동시성의 핵심&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 data-end=&quot;211&quot; data-start=&quot;161&quot; data-ke-size=&quot;size26&quot;&gt;✅ 명시적 취소 전파 (Explicit Cancellation Propagation)&lt;/h2&gt;
&lt;h3 data-end=&quot;218&quot; data-start=&quot;213&quot; data-ke-size=&quot;size23&quot;&gt;뜻&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;276&quot; data-start=&quot;219&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;276&quot; data-start=&quot;219&quot;&gt;개발자가 &lt;b&gt;직접 코드로 취소를 요청하거나&lt;/b&gt;&lt;br /&gt;&lt;b&gt;직접 자식 작업에게 신호를 보내는 방식&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;284&quot; data-start=&quot;278&quot; data-ke-size=&quot;size23&quot;&gt;예시&lt;/h3&gt;
&lt;h4 data-end=&quot;340&quot; data-start=&quot;286&quot; data-ke-size=&quot;size20&quot;&gt;1. Task.cancel(), group.cancelAll() 같은 메서드 호출&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1744963355757&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let task = Task { // 작업 내용 } task.cancel() //   명시적으로 취소 전파&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-end=&quot;454&quot; data-start=&quot;420&quot; data-ke-size=&quot;size20&quot;&gt;2. TaskGroup 내에서 수동으로 취소 요청&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;swift&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&lt;span data-state=&quot;closed&quot;&gt;복사&lt;/span&gt;&lt;span data-state=&quot;closed&quot;&gt;편집&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1744963399563&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;await withTaskGroup(of: Void.self) { group in
    group.addTask { await someWork() }
    group.cancelAll() //   명시적 취소 요청
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-end=&quot;637&quot; data-start=&quot;594&quot; data-ke-size=&quot;size20&quot;&gt;3. 내부 로직에서 명시적으로 Task.isCancelled 체크&lt;/h4&gt;
&lt;pre id=&quot;code_1744963415235&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if Task.isCancelled {
    //   명시적으로 체크하고 정리
    return
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-end=&quot;714&quot; data-start=&quot;711&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;766&quot; data-start=&quot;716&quot; data-ke-size=&quot;size26&quot;&gt;✅ 암시적 취소 전파 (Implicit Cancellation Propagation)&lt;/h2&gt;
&lt;h3 data-end=&quot;773&quot; data-start=&quot;768&quot; data-ke-size=&quot;size23&quot;&gt;뜻&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;869&quot; data-start=&quot;774&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;814&quot; data-start=&quot;774&quot;&gt;Swift의 &lt;b&gt;구조적 동시성에 의해 자동으로 일어나는 취소 전파&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;869&quot; data-start=&quot;815&quot;&gt;&lt;b&gt;부모 Task가 취소되면&lt;/b&gt;, 구조 내 자식 Task들도 &lt;b&gt;자동으로 취소 요청을 받음&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;877&quot; data-start=&quot;871&quot; data-ke-size=&quot;size23&quot;&gt;예시&lt;/h3&gt;
&lt;h4 data-end=&quot;918&quot; data-start=&quot;879&quot; data-ke-size=&quot;size20&quot;&gt;1. async let 이 포함된 부모 Task가 취소되면&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1744963438117&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func parentTask() async {
    async let user = fetchUser()
    async let posts = fetchPosts()
    
    // 이 Task 자체가 취소되면 user, posts 도 암시적으로 취소됨
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-end=&quot;1132&quot; data-start=&quot;1081&quot; data-ke-size=&quot;size20&quot;&gt;2. withTaskGroup 안의 작업들도 부모 Task가 취소되면 자동 취소&lt;/h4&gt;
&lt;pre id=&quot;code_1744963447111&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func parent() async {
    await withTaskGroup(of: Void.self) { group in
        group.addTask {
            await longRunningWork()
        }
        // 이 함수 전체가 취소되면, group의 자식 작업도 암시적으로 취소됨
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-end=&quot;1390&quot; data-start=&quot;1347&quot; data-ke-size=&quot;size20&quot;&gt;3. try await 로 취소에 대응하는 시스템 API 사용 시&lt;/h4&gt;
&lt;pre id=&quot;code_1744963461223&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try await URLSession.shared.data(from: url)
// Task가 취소되면 암시적으로 이 API도 중단됨&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-end=&quot;1483&quot; data-start=&quot;1480&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1496&quot; data-start=&quot;1485&quot; data-ke-size=&quot;size26&quot;&gt;  비교 정리&lt;/h2&gt;
&lt;div&gt;&lt;span data-state=&quot;closed&quot;&gt;&lt;/span&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 233px;&quot; border=&quot;1&quot; data-end=&quot;1931&quot; data-start=&quot;1498&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;명시적 취소 전파&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;암시적 취소 전파&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;1655&quot; data-start=&quot;1576&quot;&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;1581&quot; data-start=&quot;1576&quot;&gt;&lt;b&gt;정의&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;1624&quot; data-start=&quot;1581&quot;&gt;개발자가 직접 .cancel(), cancelAll() 등을 호출&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;1655&quot; data-start=&quot;1624&quot;&gt;구조적 동시성 또는 시스템 동작에 의해 자동 발생&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;1730&quot; data-start=&quot;1656&quot;&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;1661&quot; data-start=&quot;1656&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;1700&quot; data-start=&quot;1661&quot;&gt;task.cancel(), group.cancelAll()&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;1730&quot; data-start=&quot;1700&quot;&gt;부모 Task 취소 &amp;rarr; 자식 Task 자동 취소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;1841&quot; data-start=&quot;1731&quot;&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;1744&quot; data-start=&quot;1731&quot;&gt;&lt;b&gt;Task 내부 대응&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;1794&quot; data-start=&quot;1744&quot;&gt;보통 Task.isCancelled, checkCancellation() 사용&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;1841&quot; data-start=&quot;1794&quot;&gt;대부분 CancellationError throw 혹은 시스템 API 취소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;1881&quot; data-start=&quot;1842&quot;&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;1851&quot; data-start=&quot;1842&quot;&gt;&lt;b&gt;코드 관여도&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;1867&quot; data-start=&quot;1851&quot;&gt;높음 (수동 처리 필요)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;1881&quot; data-start=&quot;1867&quot;&gt;낮음 (자동 전파)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;1931&quot; data-start=&quot;1882&quot;&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;1891&quot; data-start=&quot;1882&quot;&gt;&lt;b&gt;유용한 상황&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;1913&quot; data-start=&quot;1891&quot;&gt;조건부 취소, 복잡한 제어 필요 시&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;1931&quot; data-start=&quot;1913&quot;&gt;단순 구조, 자동 관리 시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>swift concurrency</category>
      <author>Kim Roks</author>
      <guid isPermaLink="true">https://roks-apps.tistory.com/82</guid>
      <comments>https://roks-apps.tistory.com/82#entry82comment</comments>
      <pubDate>Fri, 18 Apr 2025 16:55:13 +0900</pubDate>
    </item>
    <item>
      <title>[Swift Concurrency] Task의 취소</title>
      <link>https://roks-apps.tistory.com/81</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;작업의 즉시 멈춤이 아닌, &amp;lsquo;취소를 전파&amp;rsquo;하는 개념&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Swift의 Task는 취소를 요청받더라도, &lt;b&gt;즉시 중단되는 것이 아니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;대신, &lt;b&gt;&amp;ldquo;취소되었다는 신호&amp;rdquo;&lt;/b&gt; 를 내부적으로 가지고 있고, 작업 내에서 그 신호를 &lt;b&gt;직접 확인하고 적절히 대응&lt;/b&gt;해야 함.&lt;/li&gt;
&lt;li&gt;이걸 &quot;&lt;b&gt;cooperative cancellation&lt;/b&gt;&quot; 이라고도 함. 작업이 스스로 확인하고 정리하는 방식.(협동적 취소)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;부모-자식 관계와 취소 전파&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Swift의 structured concurrency에서는 부모 Task가 있고, 그 안에 자식 Task가 만들어질 수 있어.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;부모 Task가 취소되면, 자식 Task에게도 취소가 전파됨.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;하지만! 자식 작업이 &lt;b&gt;완전히 끝날 때까지&lt;/b&gt; (심지어 에러가 발생했더라도), 부모 작업은 기다림.&lt;/li&gt;
&lt;li&gt;즉, 취소되더라도 자식 작업은 graceful하게 끝나야 하며, 그동안 부모는 대기 상태.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;취소 상태 확인: Task.isCancelled&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작업 내부에서 현재 취소 상태인지 확인하고 싶을 때 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;swift&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;
func someTask() async {
    if Task.isCancelled {
        print(&quot;취소 요청이 들어왔습니다.&quot;)
        return
    }

    // 작업 수행
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;반복문이나 긴 작업 처리 중간중간에 Task.isCancelled를 확인하는 것이 일반적인 패턴.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;try Task.checkCancellation()&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 함수는 Task.isCancelled를 체크하고, &lt;b&gt;취소되었다면 에러를 던짐.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;에러 타입은 CancellationError&lt;/li&gt;
&lt;li&gt;Task가 취소되었을 때는 일반적인 에러 처리 흐름을 따름&lt;/li&gt;
&lt;li&gt;try 구문에서 Task.checkCancellation() 호출 시 CancellationError가 throw됨 &amp;rarr; 이를 do-catch로 처리 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항목 설명&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;취소 방식&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;즉시 멈추는 게 아닌, 신호를 전달&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;부모-자식 구조&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;부모 취소 &amp;rarr; 자식에게 전파되지만, 자식이 끝날 때까지 기다림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;상태 확인&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Task.isCancelled 로 수동 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;취소 체크 및 에러 던지기&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;try Task.checkCancellation()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;에러 처리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;CancellationError 로 잡아서 분기 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 중첩된 Task와 취소 전파&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. &lt;b&gt;중첩된 Task는 별도의 컨텍스트로 동작&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Swift에서 Task는 기본적으로 &lt;b&gt;새로운 독립적인 실행 컨텍스트&lt;/b&gt;를 만든다.&lt;/li&gt;
&lt;li&gt;즉, 상위 Task 내에서 Task { }로 생성한 Task는 &lt;b&gt;부모와 별개&lt;/b&gt;임.&lt;/li&gt;
&lt;li&gt;그래서 &lt;b&gt;부모 Task가 취소되더라도, 내부에서 만든 Task에는 취소가 전파되지 않는다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;swift
복사편집
func parentTask() async {
    Task {
        // 이 Task는 부모와 별개
        // 부모가 취소돼도 여기까지는 취소가 전파되지 않음
        await doWork()
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 예제에서 내부 Task는 **&amp;ldquo;중첩되어 있지만 독립적&amp;rdquo;**인 구조.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;취소 전파가 안 되는 이유는 같은 구조적 컨텍스트가 아니기 때문.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 구조적 동시성에서는 취소가 제대로 전파됨&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  예시 1: async let 사용 시&lt;/h3&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;swift
복사편집
func example() async {
    async let result1 = doSomething()
    async let result2 = doSomethingElse()

    // 부모가 취소되면 result1, result2 도 함께 취소됨
    let _ = await (try? result1, try? result2)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;async let은 &lt;b&gt;parent-child 관계가 유지되며&lt;/b&gt;, &lt;b&gt;구조적으로 연결되어 있음&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;부모가 취소되면 자식도 취소됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  예시 2: withTaskGroup&lt;/h3&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;swift
복사편집
func example() async {
    await withTaskGroup(of: Void.self) { group in
        group.addTask {
            await workA()
        }

        group.addTask {
            await workB()
        }

        group.cancelAll() // &amp;rarr; 모든 작업에 취소가 전파됨
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 직접적인 취소 처리를 구현하지 않아도 되는 경우&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;취소에 대응하는 API는 자동으로 취소됨&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 URLSession.shared.data(from:) 같은 API는 내부적으로 &lt;b&gt;CancellationError를 던질 준비가 되어 있는&lt;/b&gt; 구조야.&lt;/li&gt;
&lt;li&gt;즉, 우리가 따로 Task.isCancelled를 확인하지 않아도, Task가 취소되면 이 함수가 &lt;b&gt;자동으로 취소되고 에러를 throw&lt;/b&gt;함.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;func fetchData(from url: URL) async throws -&amp;gt; Data {
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 작업이 포함된 Task가 취소되면, data(from:)가 취소되고 &lt;b&gt;CancellationError가 throw됨&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;별도의 checkCancellation() 필요 없음.&lt;/li&gt;
&lt;li&gt;Task.sleep 도 이런 경우&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>TIL</category>
      <category>swift concurrency</category>
      <category>task cancel</category>
      <category>task cancellation</category>
      <author>Kim Roks</author>
      <guid isPermaLink="true">https://roks-apps.tistory.com/81</guid>
      <comments>https://roks-apps.tistory.com/81#entry81comment</comments>
      <pubDate>Fri, 18 Apr 2025 15:12:24 +0900</pubDate>
    </item>
  </channel>
</rss>