<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>NEON의 이것저것</title>
    <link>https://neoself.tistory.com/</link>
    <description>비빔면이랑 피아노 좋아하는 앱 개발자입니다.</description>
    <language>ko</language>
    <pubDate>Thu, 16 Apr 2026 16:02:44 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Neoself</managingEditor>
    <image>
      <title>NEON의 이것저것</title>
      <url>https://tistory1.daumcdn.net/tistory/7111943/attach/fbdabc7667a741e0974a349f0527f576</url>
      <link>https://neoself.tistory.com</link>
    </image>
    <item>
      <title>[2025년 9월 기준] 다가구주택 전세보증보험 준비서류 정리</title>
      <link>https://neoself.tistory.com/97</link>
      <description>&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;아래 사진은 제가 2025년 9월 23일 기준 서울 허그 보증보험지사에 직접 방문하여 찍은 준비서류 항목 리스트 입니다! 다만 허그보증보험지사 내부적으로 서류 조건이 바뀔 수 있기 때문에, 직접 보증보험 지사를 방문하시거나 전화를 통해 문의하시는 게 제일 정확할거에요! 참고만 해주시면 감사하겠습니다!!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2250&quot; data-origin-height=&quot;3000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T02cg/dJMcaaKy8X8/ykIOSSjyytC9XfaKqd1V0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T02cg/dJMcaaKy8X8/ykIOSSjyytC9XfaKqd1V0k/img.png&quot; data-alt=&quot;완비여부에는 그냥 해당 서류 지참여부에 대한 X,O만 있습니다... 부끄러워서 모자이크하였습니더...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T02cg/dJMcaaKy8X8/ykIOSSjyytC9XfaKqd1V0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT02cg%2FdJMcaaKy8X8%2FykIOSSjyytC9XfaKqd1V0k%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;640&quot; height=&quot;853&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2250&quot; data-origin-height=&quot;3000&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;완비여부에는 그냥 해당 서류 지참여부에 대한 X,O만 있습니다... 부끄러워서 모자이크하였습니더...&lt;/figcaption&gt;
&lt;/figure&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;u&gt;다가구주택 임차인&lt;/u&gt;이며 &lt;u&gt;대출을 HF 기관을 통해 진행&lt;/u&gt;하였으며, &lt;u&gt;임대인이 법인이 아니며,&lt;/u&gt; &lt;u&gt;건물에 상가가 없었던&lt;/u&gt;&amp;nbsp;제 경우는 크게 아래 서류 준비가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 확정일자부 임대차 계약서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 전세보증금 지급 증빙&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 전입세대확인서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 건축물대장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 부동산 등기부등본&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 신분증 사본&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7. 타 전세계약체결내역 확인서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8. 확정일자 부여현황&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9. 중개사무소등록증(필요시)&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;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 건축물대장&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부동산 중개인분께 저는 요청하여 프린트물로 받을 수 있었는데, 정보 24 홈페이지 통해서도 무료발급이 언제든지 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 부동산 등기부등본&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것 또한 계약한 주택의 주소만 알고 있다면 인터넷 등기소에서 700원 가량의 결제와 함께 온라인 발급이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 위 서류와 같이 &lt;u&gt;&lt;b&gt;&quot;건물&quot;과 &quot;토지&quot; 2개&lt;/b&gt;&lt;/u&gt;에 대해 모두 등기부등본 발급이 필요합니다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 신분증 사본&lt;/b&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;u&gt;&lt;b&gt;전세 계약서&lt;/b&gt;&lt;/u&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;size14&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;여기서 행정복지센터는 반드시 계약하고자 하는 &lt;u&gt;건물이 속한 지역을 담당하는 행정복지센터&lt;/u&gt;를 방문해야합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 확정일자부 임대차 계약서&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전세계약서 + 신분증 행정복지센터에 보여드리고 임대차 계약 신고를 완료할 경우, 주택 임대차 계약 신고필증을 발급받을 수 있는데, 보증보험지사에 앞서 발급받은&amp;nbsp;&lt;u&gt;&lt;b&gt;전세계약서와 신고필증&lt;/b&gt;&lt;/u&gt;을 제출하면 효력이 인정됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 전입세대확인서&lt;/b&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;해당 서류는 일반적으로 &quot;주택&quot; 단위로 조회 및 발급이 가능하나, 다가구 주택의 경우 세대 간 구분 없는 만큼 보증보험지사에서도 전체 건물에 대한 전입세대 확인서를 요구합니다. 때문에, 행정복지센터 방문 시, &lt;u&gt;&lt;b&gt;&quot;건물&quot;&lt;/b&gt;&lt;/u&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;size18&quot;&gt;&lt;b&gt;3. 확정일자 부여현황&lt;/b&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;여기서 주의해야할 점은 크게 2가지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 반드시 &lt;u&gt;&lt;b&gt;&quot;지번&quot;에 대한 확정일자 부여현황과 &quot;도로명&quot;에 대한 확정일자 부여현황 2개를 모두 발급&lt;/b&gt;&lt;/u&gt;받아야한다는 점&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 서류에 포함시킬 &lt;u&gt;&lt;b&gt;기간 범위를 10년으로 설정&lt;/b&gt;&lt;/u&gt;해야한다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;위 특이사항을 별도로 전달하지 않으면, 일반적으로 도로명에 대한 확정일자 부여현황만 발급해주기 때문에, 위 2가지를 꼭 발급 간 전달해야합니다.&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;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 타 전세계약체결내역 확인서 + 중개사무소등록증(중개사가 직접 해당 서류를 작성하는 경우 필요)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저의 경우, 부동산 중개인 분께서 직접 작성하여 제게 전달해주셨습니다. 이는 전세계약 현황을 보다 상세히 확인하기 위해 추가 작성되는 서류로, 보증보험 지사에서도 해당 서류의 중요성을 크게 강조주셨습니다. 특히 기타 란에 확정일자가 부여된 날짜, 월세 계약 여부 등, 최대한 각 임차인에 대한 특이사항을 상세히 기재해야 합니다. 보증보험지사 측에서 해당 확인서의 내용과 확정일자 부여현황 서류 2개를 비교대조하며, 부적절한 임차인이 있는지 여부를 확인하는 과정을 거치기 때문에, 확인서 내용이 보증보험지사에 서류를 제출하는 시점으로 최신화되어있는지 집주인 및 중개사에게 꼼꼼히 확인받는 것이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 중개사가 직접 해당 확인서를 작성하는 주체일 경우, 중개사가 실제 중개사 자격을 갖고 있는지에 대한 증빙 또한 필요하기 때문에, 중개사 측에 중개사무소 등록증 서류를 요청해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ko_1616563910655_755459_4.jpg&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RVyVH/dJMcadtSI0e/bkaZcOQXrgrKYB8MCRNqh1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RVyVH/dJMcadtSI0e/bkaZcOQXrgrKYB8MCRNqh1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RVyVH/dJMcadtSI0e/bkaZcOQXrgrKYB8MCRNqh1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRVyVH%2FdJMcadtSI0e%2FbkaZcOQXrgrKYB8MCRNqh1%2Fimg.jpg&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;600&quot; height=&quot;450&quot; data-filename=&quot;ko_1616563910655_755459_4.jpg&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 전세보증금 지급 증빙&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상으로 HF전세자금대출을 받은 상황에서 HUG 보증보험을 위해 필요로 하는 서류였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다.&lt;/p&gt;</description>
      <category>일상</category>
      <author>Neoself</author>
      <guid isPermaLink="true">https://neoself.tistory.com/97</guid>
      <comments>https://neoself.tistory.com/97#entry97comment</comments>
      <pubDate>Tue, 6 Jan 2026 16:41:42 +0900</pubDate>
    </item>
    <item>
      <title>[2025년 9월 기준] 다가구 주택 전세보증보험 가입 후기(나름의 노하우?)</title>
      <link>https://neoself.tistory.com/96</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 서론 제치고, 본론부터 말씀드리자면 2025년 9월 중순 기준 저는 &lt;b&gt;HF 전세자금대출&lt;/b&gt; + &lt;b&gt;HUG 전세보증보험 &lt;/b&gt;조합으로 다가구 주택을 전세거래하였습니다! 요즘 서울의 월세가격이 살벌한데, 전세는 다가구주택 위주 매물밖에 없다보니,,, 다가구주택 전세 거래에 관심있으신 분들에게 조금이나마 도움이 되었으면 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Snapshot_20251208_203651.png&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;1188&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z8Xjt/dJMcabvWsbd/KsWiTzWWk1BPShdMmZdzT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z8Xjt/dJMcabvWsbd/KsWiTzWWk1BPShdMmZdzT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z8Xjt/dJMcabvWsbd/KsWiTzWWk1BPShdMmZdzT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz8Xjt%2FdJMcabvWsbd%2FKsWiTzWWk1BPShdMmZdzT0%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;760&quot; height=&quot;1188&quot; data-filename=&quot;KakaoTalk_Snapshot_20251208_203651.png&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;1188&quot;/&gt;&lt;/span&gt;&lt;/figure&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;b&gt;2025.08.06&lt;/b&gt;: 가계약금 입금&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2025.09.12&lt;/b&gt;: 잔금 입금 및 전입신고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2025.09.12&lt;/b&gt;: 전세보증보험지사 방문(하지만, 빠꾸먹음..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2025.09.23&lt;/b&gt;: 전세보증보험지사 재방문 및 전세보증금반환보증보험 접수 성공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;2025.11.06&lt;/b&gt;:&lt;span&gt; 44일만에, 심사 통과 및 보증료 납부&lt;/span&gt;&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 중, 가장 발목을 잡는 기준이 바로 &lt;u&gt;&lt;b&gt;선순위채권&lt;/b&gt;&lt;/u&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 보증 신청인, 즉 제가 지불해야하는 전세 보증금 + 저보다 먼저 들어온 다른 세입자들의 전세보증금의 합계가 주택가격 * 126%의 금액보다 더 적어야 한다는 조건입니다.&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;하지만 현실적으로 발품을 팔 때에는, 위 조건을 확인하는 것이 사실상 불가능하다고 봐야 합니다. 이에 대한 이유는 크게 3가지 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 건물 내부 거주하는 다른 세입자들의 전세보증금을 파악하기 위해선 전입세대확인서를 주민센터로부터 발급받아야 하는데, 해당 서류는 전입신고를 해야만 조회 및 발급이 가능합니다.&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;2. 제가 보험 가입여부를 여쭤보러 허그 보증보험 지사를 방문했을 당시에는 주택가격의 측정 기준이나 정확한 수치를 공유주시지 않았었습니다. 아마 내부적으로 상황에 따라 변동이 좀 있어서 그렇지 않을까? 개인적인 추측을 해보았는데, 때문에 열심히 현재 주택건물 가격과 어찌 다른 세입자들의 전세보증금들 리스트를 파악하여 가입 여부를 계산할 수 있었다해도 심사 빠꾸를 때리실 수도 있습니다.&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;3. 다가구주택의 경우 전세대출기관에서 병행해서 진행하는 보증보험 가입이 불가능합니다(2025년 9월 기준). 때문에 전세 거래부동산이 속한 위치의 HUG보증보험지사를 별도로 방문해야만 하는데, 보증보험 가입 사전심사를 안해주실 가능성이 큽니다. 저 또한 전입신고, 확정일자 부여가 완료되고 난 후에서야, 보증보험 심사를 시작해주셨기 때문에, 사실상 전세거래 -&amp;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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 특약을 통해 최악의 상황 미연에 방지하기&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 임대인은 임차인의 대항력이 발생되기 전까지 근저당권 등 추가등기를 설정하지 않기로 한다.&lt;br /&gt;2. 본 계약은 전세자금 대출로 진행하며, (건축물 상의 문제로)대출 승인 불가시 임대인은 계약금 전액을 반환한다.&lt;br /&gt;3. 보증보험가입 등 임차인의 보증금 보호를 위한 행위에 임대인은 적극 협조하며, 보증보험이 불가능할 경우, 계약금 및 전세보증금을 반환한다.&lt;br /&gt;4. 임대인은 임차인의 보증보험이 완료되어 보증서가 발급되기 전까지 근저당권 등 기타제한물건을 설정하지 않는다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;일단 전세 계약서 작성 시에는 꼭, 위 특약을 기입해주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 2번과 3번 특약을 기입함으로써, 전입신고까지 완료하고 들어간 상황에서 보증보험 가입이 불허된 최악의 상황에서 계약금과 보증금을 모두 반환받을 수 있습니다. 물론 다시 처음부터 발품을 팔아야하는 만큼 시간의 손실은 생기지만,,, 돈은 돌려받을 수 있잖아요...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 참고로 중도퇴실의 경우에는 제가 다음 임차인을 직접 구해야한다는 항목이 있었는데, &quot;보증보험 가입 실패로 인한 퇴실은 중도퇴실로 간주하지 않는다&quot;와 같은 항목을 같이 추가하면 어땠을까... 라는 생각이 듭니다. 이처럼 전입신고 이후 보증보험 가입을 실패하는 경우에 대비할 수 있는 특약을 최대한 기입해주시는 것이 좋아요!&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 집품이나 중개인을 통해 기존에 전세보증보험이 통과된 이력이 있는지 확인하기!&lt;br /&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 중개인으로부터 이전 임차인이 보증보험 가입에 성공했었다 내용을 전달받고 전입신고를 한 케이스인데요. 솔직히 저는 중개인 분께서 전달주신 내용을 100% 믿지는 못했습니다(죄송합니다,,). 때문에, 동일한 건물에 거주했었던 사람들의 후기를 접할 수 있는 플랫폼인 &quot;집품&quot;에서 교차검증을 했어요. 다른 사람후기를 쓰려면 본인도 자신이 살았던 집에 대해 후기를 1개 작성해야만 해서, 저는 본가 후기 냅다 작성했었습니다... 만일 해당 주택에 대해 보증보험 가입이 가능하다는 후기가 존재한다면, 높은 확률로 보증보험 심사 통과가 가능한 매물이다로 봐도 되겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://zippoom.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://zippoom.com/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765194480990&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;집품 - 집에 대한 모든 정보&quot; data-og-description=&quot;국토교통부 기반으로 매일 업데이트되는 실거래가, 시세와 인증된 실제 거주 후기까지 부동산에 대한 정보를 한눈에!&quot; data-og-host=&quot;zippoom.com&quot; data-og-source-url=&quot;https://zippoom.com/&quot; data-og-url=&quot;https://zippoom.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dotfU0/hyZOM1TjPR/gFC8tUEZPEEP6vEgso5MyK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/R1AwB/hyZOLPqmjc/VNTFZnwzb0hRBxUjGst1P0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://zippoom.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://zippoom.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dotfU0/hyZOM1TjPR/gFC8tUEZPEEP6vEgso5MyK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/R1AwB/hyZOLPqmjc/VNTFZnwzb0hRBxUjGst1P0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;집품 - 집에 대한 모든 정보&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;국토교통부 기반으로 매일 업데이트되는 실거래가, 시세와 인증된 실제 거주 후기까지 부동산에 대한 정보를 한눈에!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;zippoom.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;집품 플랫폼을 홍보할 의도는 하나도 없습니다ㅏ...&lt;/s&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 방 개수를 확인해보기!&lt;/b&gt;&lt;/h4&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;저의 경우, 서울시 금천구에 위치한 4층 높이 빌라형 건물이였는데, 특이하게 4층은 집주인분께서 전체를 사용하고 있다보니 일반적인 빌라형 주택 대비 방이 적은 구조였습니다. 2층에 4개, 3층에 4개 해서 총 8개 방이 모두 전세로 임대된 건물이였으며, 해당 건물을 보증보험지사에서 심사할 당시 몇천만원 차이로 아슬아슬하게 세이프라고 말씀주셨습니다!&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;물론 건물 내부 모든 방이 다 전세로 거래되지 않고 반전세나 월세 형태로 거래가 되어있을 경우에는 그만큼 전세보증금 합계는 같이 줄어들테니, 방이 9개 이상이라고 무조건 안되지는 않겠습니다..! 때문에, 집주인이나 중개인한테 해당 건물 내부 전세 및 월세 비중을 한번 물어보는 것도 좋을 것 같아요!&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. 건물 내부에 집주인이 거주한다면 플러스&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 전세사기 사례들을 보면 임대인이 여러 건물에 전세를 둔 케이스들이 많았습니다. 하지만, 그런 다가구 주택 케이스들 중에서도 한 건물에 대해서만 임대를 놓는 다가구 주택은 상대적으로 고위험 수준으로 간주하지 않는 것 같아요! 특히 집주인이 건물 내부에 거주하고 있다는 사실이 등기부등본을 통해 기입이 되어있다면, 통과될 가능성이 높아집니다!&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5. 보증보험 지사 방문 시에는, 최대한 공손하게&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 사실 좀 당연한 얘기라고도 생각되실수 있는데요. 보증보험 지사를 흔히 저희가 접하는 서비스 종사자 분들이라고 생각하시면 안됩니다...! 그 분들은 다가구 주택에 대한 보증보험 심사를 안 받으시려는 제스처를 굉장히 직설적으로 표현하실 겁니다. 만에 하나 전세사기가 발생하게 된다면, 세금을 통해 매꿔지게 될테니까요! 그래서 아무리 최대한 서류를 꼼꼼히 챙겨가셔도, 1~2번은 심사 서류 미비나 업무시간 등을 이유로 심사 불응을 하실 가능성이 있어요! 보통 다가구 주택 시에는 4~5번 보증보험 지사 방문이 필요하시다라고 심사 담당자께서도 말씀주셨습니다!&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;물론 지사 바이 지사겠지만, 제가 방문했을 시에는 혼잣말 아닌 혼잣말로 이런 매물을 왜 신청하려 오셨냐는 얘기와 함께 오후 4시에 방문했을 때에는, 너무 늦은 시간에 왔다고 오늘 안으로는 심사 신청이 불가하니 다른 날에 오라고도 말씀주셨어요.(오후 2시 즈음에 오는 것이 베스트라고 하셨습니다.) 하지만, 실제로 전세사기의 8~90%는 다가구 주택으로 인해 발생하니 어쩔 수 없는 상황이라고 생각합니다! 때문에, 철저한 을의 포지션에서 얘기하시면서 필요로하는 서류 및 미비사항을 최대한 꼼꼼히 듣는 것이 필수입니다!&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;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6. 심사에 필요한 서류는 틈틈히 재요청하여 갱신하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한번의 보증보험 지사 방문 시에 지참한 서류들이 모두 충족되면 제일 베스트이겠지만! &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;전세보증보험 심사, 특히 다가구 주택의 경우에는 요청 서류 조건이 까다로운만큼, 지사 재방문 요청으로 예상보다 일정이 미뤄질 수가 있습니다! 이렇게 미뤄지면서 본래 유효기간이 남아있던 서류가 쓸모없어질 수도 있어요..! 특히 서류 대부분의 유효기간이 1개월이기에, 특정 서류 발급을 위해 행정복지센터를 방문해야할 일이 생긴다면, 가는 김에 나머지 서류들도 한번에 재발급 요청하여 유효기간을 하루라도 더 늘려야 합니다!&lt;/span&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;필요로 하는 서류들은 아래 블로그 글에 따로 정리해놓았습니다!&amp;nbsp; 참고해주세요!&lt;/span&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;&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;</description>
      <category>일상</category>
      <category>HUG보증보험지사</category>
      <category>다가구 전세</category>
      <category>다가구 전세보증보험</category>
      <category>다가구주택</category>
      <category>전세보증보험</category>
      <category>허그보증보험</category>
      <author>Neoself</author>
      <guid isPermaLink="true">https://neoself.tistory.com/96</guid>
      <comments>https://neoself.tistory.com/96#entry96comment</comments>
      <pubDate>Mon, 8 Dec 2025 20:53:12 +0900</pubDate>
    </item>
    <item>
      <title>Hilt 라이브러리 정리 (ft. Annotation Processing)</title>
      <link>https://neoself.tistory.com/95</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Android Developers에서는 Hilt를 아래와 같이 설명하고 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;종속 항목 수동 삽입을 실행하는 상용구를 줄이는 Android용 종속 항목 삽입(DI) 라이브러리&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hilt는 안드로이드 컴포넌트(Activity/Fragment/Compose NavHost/ ViewModel 등)의 생명주기에 맞춰 의존성 그래프를 자동 생성하고, Gradle 플러그인 및 Annotation processing을 통해 Factory 코드를 만들어 보일러 플레이트를 줄여줍니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Annotation Processing: 소스코드 상단의 어노테이션(ex. @Composable)을 컴파일 시점에 스캔하여, 추가 소스/ 클래스/메타 데이터를 자동 생성하는 컴파일 확장 단계. &lt;br /&gt;JSR 269 표준(플러그인 API) 위에서 동작하며, Dagger/Hilt, Room, Glide 등이 이 메커니즘으로 팩토리/바인딩 코드를 생성&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hilt 라이브러리가 보일러 플레이트를 줄여주는 과정을 자세히 설명하기 위해서는 안드로이드 프로젝트가 빌드되는 과정부터 설명이 필요한데요.&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;1. Kotlin 컴파일 전 전처리: KAPT면 스텁 생성, KSP면 심벌 분석.&lt;br /&gt;1.1 KAPT(Java annotation processor를 Kotlin에서 사용 가능하게 함)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;1.&amp;nbsp;kotlinc가&amp;nbsp;프로세서가&amp;nbsp;읽을&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;Kotlin&amp;nbsp;소스를&amp;nbsp;Java&amp;nbsp;스텁으로&amp;nbsp;변환&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;2.&amp;nbsp;Javac가&amp;nbsp;스텁을&amp;nbsp;기반으로&amp;nbsp;Annotation&amp;nbsp;Processor를&amp;nbsp;돌려&amp;nbsp;새&amp;nbsp;소스(주로&amp;nbsp;.java)를&amp;nbsp;생성하고,&amp;nbsp;그&amp;nbsp;생성물을&amp;nbsp;포함해&amp;nbsp;컴파일&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;3.&amp;nbsp;이후&amp;nbsp;kotlinc가&amp;nbsp;원본&amp;nbsp;Kotlin&amp;nbsp;소스(+&amp;nbsp;생성된&amp;nbsp;Kotlin&amp;nbsp;소스가&amp;nbsp;있다면&amp;nbsp;그것까지)를&amp;nbsp;컴파일&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;4.&amp;nbsp;최종적으로&amp;nbsp;모두&amp;nbsp;.class&amp;nbsp;바이트코드로&amp;nbsp;합쳐짐&lt;br /&gt;1.2 KSP&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;1.&amp;nbsp;kotlinc가&amp;nbsp;Kotlin&amp;nbsp;심벌을&amp;nbsp;직접&amp;nbsp;읽고,&amp;nbsp;프로세서가&amp;nbsp;Kotlin/Java&amp;nbsp;소스를&amp;nbsp;생성&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;2.&amp;nbsp;생성된&amp;nbsp;Kotlin&amp;nbsp;소스(.kt)는&amp;nbsp;kotlinc가,&amp;nbsp;생성된&amp;nbsp;Java&amp;nbsp;소스가&amp;nbsp;있다면&amp;nbsp;Javac가&amp;nbsp;각각&amp;nbsp;컴파일&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;3.&amp;nbsp;최종적으로&amp;nbsp;모두&amp;nbsp;.class&amp;nbsp;바이트코드로&amp;nbsp;합쳐짐&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;비교: KAPT는 스텁 생성 + Javac 라운드를 거쳐야 해 오버헤드가 있고, 증분/멀티플랫폼 제약이 있습니다. KSP는 스텁 없이 심벌을 직접 읽어 일반적으로 더 빠르고 증분/멀티플랫폼 지원이 낫습니다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 이후 단계: 클래스 머징 &amp;rarr; D8/DEX 변환 &amp;rarr; 리소스 병합/패키징(APK/AAB) 등으로 빌드 마무리&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;여기서 Kapt 기준 2단계, KSP 기준 1단계에서 Hilt 라이브러리는 Factory 코드를 생성하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1764824033874&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 실제 Hilt 라이브러리의 Annotation이 기입된 소스코드

@Singleton
class AuthManager @Inject constructor(
    @ApplicationContext private val context: Context
) {
	...
}

// 컴파일 단계에서 Annotation Processing으로 생성된 Factory 코드
@ScopeMetadata(&quot;javax.inject.Singleton&quot;)
@QualifierMetadata(&quot;dagger.hilt.android.qualifiers.ApplicationContext&quot;)
@DaggerGenerated
@Generated(
    value = &quot;dagger.internal.codegen.ComponentProcessor&quot;,
    comments = &quot;https://dagger.dev&quot;
)
@SuppressWarnings({
    &quot;unchecked&quot;,
    &quot;rawtypes&quot;,
    &quot;KotlinInternal&quot;,
    &quot;KotlinInternalInJava&quot;
})
public final class AuthManager_Factory implements Factory&amp;lt;AuthManager&amp;gt; {
  // @ApplicationContext로 한정된 Context를 Dagger가 공급
  private final Provider&amp;lt;Context&amp;gt; contextProvider;

  public AuthManager_Factory(Provider&amp;lt;Context&amp;gt; contextProvider) {
    this.contextProvider = contextProvider;
  }

  @Override
  public AuthManager get() {
    // contextProvider.get()으로 실제 Context를 받아 newInstance를 호출
    return newInstance(contextProvider.get());
  }

  public static AuthManager_Factory create(Provider&amp;lt;Context&amp;gt; contextProvider) {
    return new AuthManager_Factory(contextProvider);
  }

  public static AuthManager newInstance(Context context) {
    // AuthManager의 @Inject constructor(@ApplicationContext Context)를 그대로 사용
    return new AuthManager(context);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시는 Hilt 라이브러리의 @Singleton 어노테이션을 코드에 기입할 때, 컴파일 과정 자동생성되는 코드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Factory 코드는 AuthManager 생성자 호출을 감싸며, Dagger가 DI 그래프를 조립할 때 AuthManager의 생성자에 필요한 Context를 주입해 새 인스턴스를 생성하는 역할을 수행합니다. 이로써, 런타임에 리플렉션 없이 빠른 객체 공급이 가능해집니다.&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;Hilt/ Dagger로 설계되는 의존성 방향 중 하나의 예시로 아래와 같은 상황 및 코드가 있습니다.&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;0. 의존성 방향&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@HiltAndroidApp&amp;nbsp;&amp;rarr;&amp;nbsp;모듈(Repo/UseCase/Network)&amp;nbsp;&amp;rarr;&amp;nbsp;@HiltViewModel&amp;nbsp;&amp;rarr;&amp;nbsp;Compose&amp;nbsp;hiltViewModel()/EntryPoint&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;1. 루트&amp;nbsp;그래프&amp;nbsp;선언&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764825007829&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 루트 그래프 선언
@HiltAndroidApp
class ExampleApplication : Application() {
    // Inject 어노테이션을 통해 클래스를 접근하여 루트 그래프에서 사용할 수 있도록 의존성 설계
    @Inject lateinit var exampleManager: ExampleManager

    override fun onCreate() {
        super.onCreate()
        exampleManager.exampleFunc() 
    }
}&lt;/code&gt;&lt;/pre&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;2. 의존성 정의 모듈&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764825131504&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;abstract class RepositoryModule {
    // 도메인 인터페이스를 @Binds @SingleTon으로 구현체에 연결
    @Binds
    @Singleton
    abstract fun bindExampleRepository(
        exampleRepositoryImpl: ExampleRepositoryImpl
    ): ExampleRepository

    ...
}&lt;/code&gt;&lt;/pre&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;3. 화면/상태 계층 주입&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764825279285&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// @HiltViewModel + @Inject constructor로 상위 클래스를 주입받아 사용
@HiltViewModel
class ExampleViewModel @Inject constructor(
    private val exampleManager: ExampleManager
) {
    fun test() {
        exampleManager.exampleFunc()
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1764825335083&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun ExampleView(
    // Compose 네비게이션에서 hiltViewModel()로 주입된 ViewModel 사용
    viewModel: ExampleViewModel = hiltViewModel()
) {
    ...&lt;/code&gt;&lt;/pre&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;4. EntryPoint&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@EntryPoint
@InstallIn(SingletonComponent::class)
interface AuthManagerEntryPoint {
    fun authManager(): AuthManager
}

// EntryPoint를 통해 불러올 수 있도록 인터페이스를 사전에 정의
@EntryPoint
@InstallIn(SingletonComponent::class)
interface PopupManagerEntryPoint {
    fun popupManager(): PopupManager
}

@Composable
fun HumaniaView(humaniaViewModel: HumaniaViewModel) {
    val toastManager: ToastManager = humaniaViewModel.toastManager

    val context = LocalContext.current
    // EntryPointAccessors.fromApplication으로 상위 Manager 클래스들을 꺼내, UI 컨테이너에 연결
    val popupManager: PopupManager = remember {
        EntryPointAccessors.fromApplication(context, PopupManagerEntryPoint::class.java).popupManager()
    }
    val authManager = EntryPointAccessors.fromApplication(context, AuthManagerEntryPoint::class.java).authManager()
    val navController = rememberNavController()
    
    ...
    // 컴포저블 정의 섹션
    Box {
        ...
        PopupContainer(popupManager = popupManager)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;루트 Compose에서 EntryPointAccessors.fromApplication으로 ToastManager, PopupManager, AuthManager 등을 꺼내 UI 컨테이너에 연결합니다.&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;</description>
      <category>개발지식 정리/Kotlin</category>
      <author>Neoself</author>
      <guid isPermaLink="true">https://neoself.tistory.com/95</guid>
      <comments>https://neoself.tistory.com/95#entry95comment</comments>
      <pubDate>Thu, 4 Dec 2025 14:24:05 +0900</pubDate>
    </item>
    <item>
      <title>플레이 스토어 앱 업데이트 실패 해결 과정(certificate mismatch)</title>
      <link>https://neoself.tistory.com/94</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;인앱 업데이트 로직을 통해 플레이스토어로 리다이렉트 및 업데이트 시도 시, 아래 팝업과 함께 앱 설치가 불가하다는 문제를 겪었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-03 오후 1.39.30.png&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bckJHF/dJMcab3Ktt4/9f4HipjslTGWgQghUF8DAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bckJHF/dJMcab3Ktt4/9f4HipjslTGWgQghUF8DAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bckJHF/dJMcab3Ktt4/9f4HipjslTGWgQghUF8DAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbckJHF%2FdJMcab3Ktt4%2F9f4HipjslTGWgQghUF8DAK%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;1032&quot; height=&quot;516&quot; data-filename=&quot;스크린샷 2025-12-03 오후 1.39.30.png&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;516&quot;/&gt;&lt;/span&gt;&lt;/figure&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;이를 해결하고자 먼저, 해당 이슈가 발견된 기기를 연결하여 LogCat를 통해 원인을 파악하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1764730846834&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[414] AU2: Failure History successfully updated for package, attempting to upgrade to version 7 with install reason SINGLE_INSTALL and status_code 6004
[410] IQ: start evaluating install requests.
[403] com.packageName.projectName is installed but certificate mismatch // 서명 파일 불일치!
[410] IQ: There are 0 installs pending for job 34988: []
[410] IQ: No matching installs to run for jobs: [34988]
[410] IQ: 0 scheduled install statuses found to proceed.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 업데이트 전인 버전 A와 업데이트를 하고자 하는 최신 버전 B 간 사용된 서명 파일이 불일치하기에 업데이트를 하여도, 동일한 앱으로 간주되지 않아, 업데이트가 실패되고 있다는 것을 의미합니다.&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;실제로 버전 A에서 사용된 서명 파일의 경우 CI/CD 구축 과정에서 다른 서명파일로 교체를 한 후, 버전 B에 대한 배포가 이루어졌기 때문에 이에 대한 검증을 하고자 버전 A에 대한 아카이브 파일을 다시 Android Studio로 연 후, &quot;Generate Signed App Bundle or APK&quot;를 통해 버전 B 배포에 사용한 업로드 키 인증서(jks)로 서명하여 apk로 추출한 후, 테스트 기기에 설치하여 확인해보기로 했습니다.&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;그 이유는 업로드에 사용된 서명 키가 실제 playStore에서 관리하고 있는 서명키와 달랐기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Playstore는 &lt;b&gt;업로드 키&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;이는 Google Play Console -&amp;gt; 앱 대시보드 -&amp;gt; 테스트 및 출시 -&amp;gt; 앱 무결성 탭에서 Play 앱 서명 섹션의 설정 버튼을 누르면 확인이 가능합니다. 우측 사진에서 보시는 바와 같이 앱 번들을 업로드할 때 사용되는 &lt;b&gt;업로드 키 인증서&lt;/b&gt;와 앱의 출시버전 서명에 사용되는 &lt;b&gt;앱 서명 키 인증서&lt;/b&gt;를 별도로 구분하고 있죠. 여기서 앱 서명 키 인증서는 업로드 키 인증서와 달리 개발자가 직접 접근할 수는 없으며 Google 서버에서 자체 보관합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FLe75/dJMcahJFJAp/eJgaQHJrGMDg3p51pK1z0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FLe75/dJMcahJFJAp/eJgaQHJrGMDg3p51pK1z0k/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1711&quot; data-origin-height=&quot;811&quot; data-filename=&quot;blob&quot; style=&quot;width: 59.1345%; margin-right: 10px;&quot; data-widthpercent=&quot;59.83&quot; id=&quot;kEditorPhotosEditingImage-1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FLe75/dJMcahJFJAp/eJgaQHJrGMDg3p51pK1z0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFLe75%2FdJMcahJFJAp%2FeJgaQHJrGMDg3p51pK1z0k%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;1711&quot; height=&quot;811&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nGjEu/dJMcafZnrhw/5tXUKVB4kAMNfyfk3XqOX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nGjEu/dJMcafZnrhw/5tXUKVB4kAMNfyfk3XqOX0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;850&quot; data-filename=&quot;blob&quot; style=&quot;width: 39.7027%;&quot; data-widthpercent=&quot;40.17&quot; id=&quot;kEditorPhotosEditingImage-2&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nGjEu/dJMcafZnrhw/5tXUKVB4kAMNfyfk3XqOX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnGjEu%2FdJMcafZnrhw%2F5tXUKVB4kAMNfyfk3XqOX0%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;1204&quot; height=&quot;850&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Frame 184.png&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;568&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btI2ux/dJMcachhqoy/Ekwr0HQ3CbM1YeeioaIrTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btI2ux/dJMcachhqoy/Ekwr0HQ3CbM1YeeioaIrTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btI2ux/dJMcachhqoy/Ekwr0HQ3CbM1YeeioaIrTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtI2ux%2FdJMcachhqoy%2FEkwr0HQ3CbM1YeeioaIrTk%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;806&quot; height=&quot;568&quot; data-filename=&quot;Frame 184.png&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;568&quot;/&gt;&lt;/span&gt;&lt;/figure&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;
&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;이는 Google Play Console에서 조회 및 다운로드가 가능합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3ZGOj/dJMcaaqeBHc/PeWgLTIL6civJ4KXxkHlv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3ZGOj/dJMcaaqeBHc/PeWgLTIL6civJ4KXxkHlv0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1681&quot; data-origin-height=&quot;643&quot; data-filename=&quot;blob&quot; style=&quot;width: 56.0378%; margin-right: 10px;&quot; data-widthpercent=&quot;56.7&quot; id=&quot;kEditorPhotosEditingImage-4&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3ZGOj/dJMcaaqeBHc/PeWgLTIL6civJ4KXxkHlv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3ZGOj%2FdJMcaaqeBHc%2FPeWgLTIL6civJ4KXxkHlv0%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;1681&quot; height=&quot;643&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8mGKs/dJMb995WeWc/2oIBKDhuwIKtd1p53cflck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8mGKs/dJMb995WeWc/2oIBKDhuwIKtd1p53cflck/img.png&quot; data-filename=&quot;blob&quot; data-origin-height=&quot;606&quot; data-origin-width=&quot;1210&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;43.3&quot; style=&quot;width: 42.7994%;&quot; id=&quot;kEditorPhotosEditingImage-5&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8mGKs/dJMb995WeWc/2oIBKDhuwIKtd1p53cflck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8mGKs%2FdJMb995WeWc%2F2oIBKDhuwIKtd1p53cflck%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;1210&quot; height=&quot;606&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 디버깅 및 리서치 경험을 통해 Google Play에서 출시된 앱 버전들에 대해 어떻게 서명을 관리하고 있는지를 알아볼 수 있었습니다.&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;</description>
      <category>개발지식 정리/Kotlin</category>
      <category>안드로이드</category>
      <category>앱 서명 키 인증서</category>
      <category>업데이트 실패</category>
      <category>업로드 키 인증서</category>
      <author>Neoself</author>
      <guid isPermaLink="true">https://neoself.tistory.com/94</guid>
      <comments>https://neoself.tistory.com/94#entry94comment</comments>
      <pubDate>Wed, 3 Dec 2025 13:41:24 +0900</pubDate>
    </item>
    <item>
      <title>[FCM] iOS에서 Firebase 푸시알림 구현하기</title>
      <link>https://neoself.tistory.com/93</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;모바일 앱 사용자 경험을 향상시키는 핵심 요소 중 하나는 적절한 시점에 전달되는 푸시알림입니다.&lt;br /&gt;iOS 플랫폼에서 Firebase Cloud Messaging(FCM)을 활용하면 비교적 쉽게 푸시알림 기능을 구축할 수 있지만, 플랫폼 특성상 몇 가지 중요한 환경설정을 반드시 선행해야 합니다.&amp;nbsp;&lt;br /&gt;이 글에서는 iOS 앱에 Firebase 푸시알림 기능을 구현하기 위해 필요한 초기 환경설정을 단계별로 정리해보았습니다.&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;0. 앱 &amp;amp; 앱 식별자 추가하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 Apple Developer 플랫폼에서 푸시알림을 연결하고자 하는 앱을 추가해줘야 합니다. 이미 앱이 추가되어있는 상황이면 해당 절차를 무시하셔도 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Group 5.png&quot; data-origin-width=&quot;571&quot; data-origin-height=&quot;587&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2FUXy/btsONvYjF16/lXAYsziYNrfEKAAUFZro7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2FUXy/btsONvYjF16/lXAYsziYNrfEKAAUFZro7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2FUXy/btsONvYjF16/lXAYsziYNrfEKAAUFZro7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2FUXy%2FbtsONvYjF16%2FlXAYsziYNrfEKAAUFZro7K%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;571&quot; height=&quot;587&quot; data-filename=&quot;Group 5.png&quot; data-origin-width=&quot;571&quot; data-origin-height=&quot;587&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 법인 개발자의 경우 SKU 필드가 추가로 생성되는데, 이는 Apple 내부적으로 사용되지 않고, 앱 개발사(법인)에서 자체적으로 관리용으로 사용하는 식별 코드입니다. 때문에 {회사명}_yyyy_mm과 같이 앱 자체 식별이 가능한 문자열로 설정해두면 좋습니다.&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;여기서 다른 필드와 달리 번들 ID는 다른 창에서 따로 추가가 필요한 값입니다. 이는 Apple 내부적으로 관리하기 위해 사용하는 식별 코드이기 때문이죠. 때문에, 번들 ID 드롭다운 창 내부 +버튼 클릭을 통해 번들 ID 추가 창으로 이동하여 아래와 같이 식별자값을 추가합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Group 4.png&quot; data-origin-width=&quot;890&quot; data-origin-height=&quot;342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/trkvr/btsONLtdHGt/dVsngjZrMArrepY3Tk4nf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/trkvr/btsONLtdHGt/dVsngjZrMArrepY3Tk4nf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/trkvr/btsONLtdHGt/dVsngjZrMArrepY3Tk4nf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftrkvr%2FbtsONLtdHGt%2FdVsngjZrMArrepY3Tk4nf0%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;890&quot; height=&quot;342&quot; data-filename=&quot;Group 4.png&quot; data-origin-width=&quot;890&quot; data-origin-height=&quot;342&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. Firebase 인증을 위한 APNs 키 생성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS 플랫폼의 경우, Firebase에서는 Apple Developer 플랫폼에서 발급할 수 있는 인증서 파일(p8)을 통해 앱 개발자 인증을 수행합니다. 해당 키는 본래 APN(Apple Push Notification) 기능을 위한 키인데요. 애플 서버에서 푸시알림 요청이 악성 해커가 아닌 해당 앱개발자가 보낸 것인지 확인하기 위해 사용되는 인증수단입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Group 7.png&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MlrqW/btsOPvP5594/SmiGikKhXKzXdYSOmGFOWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MlrqW/btsOPvP5594/SmiGikKhXKzXdYSOmGFOWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MlrqW/btsOPvP5594/SmiGikKhXKzXdYSOmGFOWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMlrqW%2FbtsOPvP5594%2FSmiGikKhXKzXdYSOmGFOWk%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;1143&quot; height=&quot;466&quot; data-filename=&quot;Group 7.png&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;466&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좌측 메뉴의 Keys 탭을 선택할 경우, 키에 대한 생성을 할 수 있는데요. 위와 같이 Key name을 임의로 설정한 후(저는 &quot;{앱이름} APN&quot;으로 설정하였습니다. 하단에 키가 보유하게될 권한 선택지들 중, Apple Push Notifications service 항목을 체크합니다. 이때 키 관련 설정이 선행된다고 경고 메세지가 표시되는데요.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-20 오후 3.43.06 1.png&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/prY0P/btsOObLOyc3/qM5cW13kACFaKKlDpSAYK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/prY0P/btsOObLOyc3/qM5cW13kACFaKKlDpSAYK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/prY0P/btsOObLOyc3/qM5cW13kACFaKKlDpSAYK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FprY0P%2FbtsOObLOyc3%2FqM5cW13kACFaKKlDpSAYK1%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;1143&quot; height=&quot;488&quot; data-filename=&quot;스크린샷 2025-06-20 오후 3.43.06 1.png&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;488&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 키가 효력을 같는 범위에 대한 설정을 마치고 Save를 누르면 설정과정이 완료되며, 아래와 같이 키 등록 버튼이 활성화됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Group 8.png&quot; data-origin-width=&quot;1149&quot; data-origin-height=&quot;326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8Cfpe/btsOOJnF6c7/NVzgog0gazv6qwZW7cPHO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8Cfpe/btsOOJnF6c7/NVzgog0gazv6qwZW7cPHO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8Cfpe/btsOOJnF6c7/NVzgog0gazv6qwZW7cPHO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8Cfpe%2FbtsOOJnF6c7%2FNVzgog0gazv6qwZW7cPHO0%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;1149&quot; height=&quot;326&quot; data-filename=&quot;Group 8.png&quot; data-origin-width=&quot;1149&quot; data-origin-height=&quot;326&quot;/&gt;&lt;/span&gt;&lt;/figure&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;blockquote data-ke-style=&quot;style3&quot;&gt;사실 이와 유사한 역할을 하는 또다른 인증수단이 있는데요. 이는 인증서(.cer)입니다. 기존에는 .cer 방식을 주로 사용해왔지만, 2023년부터 Firebase에서는 p8을 사용하는 것을 권장하고 있습니다.&lt;br /&gt;&lt;b&gt;APNs 키(.p8)&lt;/b&gt;&amp;nbsp;:&amp;nbsp;서버용으로 한 번 발급하면 유지 가능 (최대 2개까지 생성 가능)&lt;br /&gt;&lt;b&gt;인증서(.cer)&lt;/b&gt;&amp;nbsp;:&amp;nbsp;앱별로 유효 기간 존재, 갱신 필요&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. Firebase 프로젝트 등록&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, Firebase 콘솔을 접속하여 로그인을 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://console.firebase.google.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://console.firebase.google.com&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1750721242392&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;로그인 - Google 계정&quot; data-og-description=&quot;이메일 또는 휴대전화&quot; data-og-host=&quot;accounts.google.com&quot; data-og-source-url=&quot;https://console.firebase.google.com/&quot; data-og-url=&quot;https://accounts.google.com/v3/signin/identifier?continue=https%3A%2F%2Fconsole.firebase.google.com%2F&amp;amp;followup=https%3A%2F%2Fconsole.firebase.google.com%2F&amp;amp;ifkv=AdBytiNsEnJWo0XqbUlGwAtmyj5zWIQMvINgSHaWSQV4zCjSlIbqbQbsT2f1ytpBI62F9dDAxyUXQA&amp;amp;osid=1&amp;amp;passive=1209600&amp;amp;flowName=WebLiteSignIn&amp;amp;flowEntry=ServiceLogin&amp;amp;dsh=S-802097954%3A1750721240998689&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://console.firebase.google.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://console.firebase.google.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;로그인 - Google 계정&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이메일 또는 휴대전화&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;accounts.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, 아래 프로젝트 시작하기 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Group 10.png&quot; data-origin-width=&quot;985&quot; data-origin-height=&quot;679&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcDYmB/btsOMWILGf5/AbhNZKhfHioxTiljoksEQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcDYmB/btsOMWILGf5/AbhNZKhfHioxTiljoksEQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcDYmB/btsOMWILGf5/AbhNZKhfHioxTiljoksEQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcDYmB%2FbtsOMWILGf5%2FAbhNZKhfHioxTiljoksEQ0%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;420&quot; height=&quot;290&quot; data-filename=&quot;Group 10.png&quot; data-origin-width=&quot;985&quot; data-origin-height=&quot;679&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 프로젝트 이름 설정&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1570&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cs1Bw5/btsON82MikD/HK4Z8r1Nyl1zhmPlOKOMHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cs1Bw5/btsON82MikD/HK4Z8r1Nyl1zhmPlOKOMHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cs1Bw5/btsON82MikD/HK4Z8r1Nyl1zhmPlOKOMHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcs1Bw5%2FbtsON82MikD%2FHK4Z8r1Nyl1zhmPlOKOMHK%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;3000&quot; height=&quot;1570&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1570&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해당 프로젝트 이름은 firebase 플랫폼에서 앱을 식별하기 위해 사용되는 값이기에, 고유한 값을 입력해야합니다. 또한, 한번 설정한 이름은 설정에 사용했던 프로젝트가 삭제되더라도 더이상 사용이 불가하니, 주의에 주의를 가합시다...!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-24 오전 8.33.25.png&quot; data-origin-width=&quot;3282&quot; data-origin-height=&quot;1704&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/J5sfB/btsOOu5kK5S/GihmCKXPzXxq2gdKMBnhtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/J5sfB/btsOOu5kK5S/GihmCKXPzXxq2gdKMBnhtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/J5sfB/btsOOu5kK5S/GihmCKXPzXxq2gdKMBnhtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJ5sfB%2FbtsOOu5kK5S%2FGihmCKXPzXxq2gdKMBnhtK%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;3282&quot; height=&quot;1704&quot; data-filename=&quot;스크린샷 2025-06-24 오전 8.33.25.png&quot; data-origin-width=&quot;3282&quot; data-origin-height=&quot;1704&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 Google Analytics를 희망하시는 분은 사용 설정을 진행하여줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Group 12.png&quot; data-origin-width=&quot;985&quot; data-origin-height=&quot;537&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mVhEG/btsONDWnhoA/BAr7tvlwFl8UuuDHxMHWy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mVhEG/btsONDWnhoA/BAr7tvlwFl8UuuDHxMHWy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mVhEG/btsONDWnhoA/BAr7tvlwFl8UuuDHxMHWy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmVhEG%2FbtsONDWnhoA%2FBAr7tvlwFl8UuuDHxMHWy1%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;985&quot; height=&quot;537&quot; data-filename=&quot;Group 12.png&quot; data-origin-width=&quot;985&quot; data-origin-height=&quot;537&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 프로젝트에 iOS 앱 추가&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트 설정 완료 이후에는, iOS 앱을 추가합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;iOS 앱 추가 자체에 대한 과정은 Google Firebase 자체 공식문서가 굉장히 설명이 잘 되어있기에 해당 링크로 대체합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://firebase.google.com/docs/ios/setup?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://firebase.google.com/docs/ios/setup?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1750721929966&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Apple 프로젝트에 Firebase 추가 &amp;nbsp;|&amp;nbsp; Firebase for Apple platforms&quot; data-og-description=&quot;새로운 Firebase Studio 기능부터 AI 통합 방법까지 I/O에서 발표된 모든 내용을 확인해 보세요. 블로그 읽기 의견 보내기 Apple 프로젝트에 Firebase 추가 컬렉션을 사용해 정리하기 내 환경설정을 기준&quot; data-og-host=&quot;firebase.google.com&quot; data-og-source-url=&quot;https://firebase.google.com/docs/ios/setup?hl=ko&quot; data-og-url=&quot;https://firebase.google.com/docs/ios/setup?hl=ko&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://firebase.google.com/docs/ios/setup?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://firebase.google.com/docs/ios/setup?hl=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Apple 프로젝트에 Firebase 추가 &amp;nbsp;|&amp;nbsp; Firebase for Apple platforms&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;새로운 Firebase Studio 기능부터 AI 통합 방법까지 I/O에서 발표된 모든 내용을 확인해 보세요. 블로그 읽기 의견 보내기 Apple 프로젝트에 Firebase 추가 컬렉션을 사용해 정리하기 내 환경설정을 기준&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;firebase.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;br /&gt;2. iOS 앱에 발급받은 인증키 파일(p8) 업로드&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS 앱 추가가 완료된 이후에는 아래와 같은 프로젝트 설정 창에서 p8 파일 업로드를 통해 앱구성 과정을 마칠 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Group 9.png&quot; data-origin-width=&quot;1146&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/djLzUZ/btsOMqchnmZ/lmKsClZAkuK2xzMHoQQUkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djLzUZ/btsOMqchnmZ/lmKsClZAkuK2xzMHoQQUkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djLzUZ/btsOMqchnmZ/lmKsClZAkuK2xzMHoQQUkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdjLzUZ%2FbtsOMqchnmZ%2FlmKsClZAkuK2xzMHoQQUkk%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;1146&quot; height=&quot;559&quot; data-filename=&quot;Group 9.png&quot; data-origin-width=&quot;1146&quot; data-origin-height=&quot;559&quot;/&gt;&lt;/span&gt;&lt;/figure&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;</description>
      <category>개발지식 정리/Swift</category>
      <author>Neoself</author>
      <guid isPermaLink="true">https://neoself.tistory.com/93</guid>
      <comments>https://neoself.tistory.com/93#entry93comment</comments>
      <pubDate>Tue, 24 Jun 2025 08:40:39 +0900</pubDate>
    </item>
    <item>
      <title>[SwiftUI] 단일 TextField로 여러 정보 연속적으로 입력하기</title>
      <link>https://neoself.tistory.com/92</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 구현 목표&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 플로우에서 사용자가 화면을 터치하지 않고도 키보드의 &quot;다음&quot; 버튼만으로 모든 정보를 순차적으로 입력할 수 있는 UX를 구현하고자 했습니다. 이 과정에서 일반 텍스트 입력, 그리고 비밀번호 입력이 모두 필요했기에, 평문 입력을 위한 TextField와 입력 내용을 점으로 표기해주는 SecureField 간의 자연스러운 전환이 핵심 요구사항이었습니다.&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;span style=&quot;color: #000000;&quot;&gt;1. TextField와 SecureField 간 전환 시 키보드가 내려가지 않을 것&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. 입력 정보 종류에 따라 적절한 키보드 타입(.default, .asciiCapable, .numberPad)이 즉시 적용될 것&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 시행착오 과정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1차 시도: 조건부 렌더링&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1750242753156&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if type.isSecure &amp;amp;&amp;amp; isPasswordHidden {
    SecureField(...)
        .focused($isFocused)
} else {
    TextField(...)
        .focused($isFocused)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Simulator Screen Recording - iPhone 16 - 2025-06-18 at 19.58.34.gif&quot; data-origin-width=&quot;295&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cE7qW9/btsOIoQ7pxh/330OVxdAZvQGXP2QFVpRnk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cE7qW9/btsOIoQ7pxh/330OVxdAZvQGXP2QFVpRnk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cE7qW9/btsOIoQ7pxh/330OVxdAZvQGXP2QFVpRnk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cE7qW9/btsOIoQ7pxh/330OVxdAZvQGXP2QFVpRnk/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;240&quot; height=&quot;521&quot; data-filename=&quot;Simulator Screen Recording - iPhone 16 - 2025-06-18 at 19.58.34.gif&quot; data-origin-width=&quot;295&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과:&lt;/b&gt;&amp;nbsp;❌ 실패&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째&amp;nbsp;시도에서는&amp;nbsp;조건부 렌더링을 통해 구현을&amp;nbsp;시도했습니다. 하지만&amp;nbsp;뷰가&amp;nbsp;교체되는 순간&amp;nbsp;FocusState&amp;nbsp;추적이&amp;nbsp;끊어져 키보드가 내려가는 현상이 발생했습니다. 이 과정에서 SwiftUI의&amp;nbsp;FocusState가 키보드 표시 여부와&amp;nbsp;직결되어 있지만, 세밀한 제어가 불가능한&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;size18&quot;&gt;&lt;b&gt;2차&amp;nbsp;시도: ZStack과&amp;nbsp;opacity 활용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 시도에서는 ZStack과 opacity를 활용하여 접근했습니다.&amp;nbsp;이 방식으로 키보드가&amp;nbsp;내려가지 않고&amp;nbsp;포커스 상태를 유지할 수는&amp;nbsp;있었지만, 키보드 타입이 변경되지 않는 새로운 문제가 발생했습니다.&amp;nbsp;원인을 분석해보니&amp;nbsp;뷰 자체의 재평가가&amp;nbsp;발생하지 않아 키보드 타입&amp;nbsp;갱신이 되지 않았던&amp;nbsp;것이었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750242820419&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ZStack(alignment: .leading) {
    SecureField(...)
        .opacity(type.isSecure &amp;amp;&amp;amp; isPasswordHidden ? 1 : 0)
    TextField(...)
        .opacity(type.isSecure &amp;amp;&amp;amp; isPasswordHidden ? 0 : 1)
}
.keyboardType(type.keyboardType)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Simulator Screen Recording - iPhone 16 - 2025-06-18 at 20.00.15.gif&quot; data-origin-width=&quot;295&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KoqxK/btsOHBKb7IV/iYlHkeXzIHWxeApnZFYKN0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KoqxK/btsOHBKb7IV/iYlHkeXzIHWxeApnZFYKN0/img.gif&quot; data-alt=&quot;비밀번호 입력에 사용한 키보드가 유지되며 닉네임 입력 간 한글 입력 불가&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KoqxK/btsOHBKb7IV/iYlHkeXzIHWxeApnZFYKN0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/KoqxK/btsOHBKb7IV/iYlHkeXzIHWxeApnZFYKN0/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;240&quot; height=&quot;521&quot; data-filename=&quot;Simulator Screen Recording - iPhone 16 - 2025-06-18 at 20.00.15.gif&quot; data-origin-width=&quot;295&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;비밀번호 입력에 사용한 키보드가 유지되며 닉네임 입력 간 한글 입력 불가&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과:&lt;/b&gt;&amp;nbsp;부분 성공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;✅ 키보드가 내려&lt;/span&gt;&lt;span&gt;가지 않고 포커스 상태&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&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;보드 타입이 변&lt;/span&gt;&lt;span&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3차 시도: 명시적 뷰 정체성 부여&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째&amp;nbsp;시도에서는 뷰에&amp;nbsp;명시적 정체성을 부여하는&amp;nbsp;방식을&amp;nbsp;시도했습니다. 이 방식으로 대부분의 키보드 타입 전환에는 성공했으나,&amp;nbsp;.default&amp;nbsp;&amp;harr;&amp;nbsp;.asciiCapable&amp;nbsp;간 전환만 실패하는 현상이&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;이 과정에서 .id 수정자의 위치가 .focused 수정자보다 선행되어야 안정적으로 동작하는 것을 확인했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750243021447&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// MARK: - MyTextField
HStack(alignment: .center) {
    ZStack(alignment: .leading) {
        SecureField(...)
        .opacity(type.isSecure &amp;amp;&amp;amp; isPasswordHidden ? 1 : 0)
    
        TextField(...)
        .opacity(type.isSecure &amp;amp;&amp;amp; isPasswordHidden ? 0 : 1)
    }
}
.keyboardType(type.keyboardType)
.id(&quot;\(type)&quot;) // focused 뷰 수정자 적용 이후 id값 적용
.focused($isFocused) // focused 뷰 수정자&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 만일 .focused 뷰 수정자 위에 명시적 정체성을 부여할 경우, focused 수정자가 적용된 후, 뷰의 ID가 변경됨에 따라 SwiftUI가 뷰를 새로 생성하는 것으로 판단해 isFocused 상태와의 연결이 끊깁니다.&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;때문에, 아래와 같이 명시적 정체성을 부여한 후에 뷰의 focus 상태 제어 뷰 수정자가 동작하도록 순서를 지정해야, Focus 상태 추적이 가능해집니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750243221039&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// MARK: - MyTextField
HStack(alignment: .center) {
    ZStack(alignment: .leading) {
        SecureField(...)
        .opacity(type.isSecure &amp;amp;&amp;amp; isPasswordHidden ? 1 : 0)
    
        TextField(...)
        .opacity(type.isSecure &amp;amp;&amp;amp; isPasswordHidden ? 0 : 1)
    }
}
.keyboardType(type.keyboardType)
.focused($isFocused) // focused 뷰 수정자
.id(&quot;\(type)&quot;) // focused 뷰 수정자 적용 이후 id값 적용&lt;/code&gt;&lt;/pre&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;span style=&quot;color: #333333;&quot;&gt;✅ 대부분의 키보드 타입 전환 성공&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;❌&amp;nbsp;.default&amp;nbsp;&amp;harr;&amp;nbsp;.asciiCapable&amp;nbsp;간 전환만 실패&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;발견: .id 수정자의 위치가 .focused 수정자보다 밑에 위치해야 안정적으로 동작&lt;/span&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;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;최종 해결: 컴포넌트 내부&amp;nbsp;포커스 상태 관리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로는 컴포넌트 내부에서 포커스 상태를&amp;nbsp;직접 관리하는 방식을&amp;nbsp;채택했습니다. 이를 통해&amp;nbsp;컴포넌트 내부에서&amp;nbsp;포커스&amp;nbsp;대상을 명확히 지정할&amp;nbsp;수 있었고, 키보드 타입과&amp;nbsp;포커스&amp;nbsp;상태를 독립적으로 관리할 수 있게 되었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750243304483&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct MyTextField: View {
    @FocusState private var focusedField: Field?
    
    var body: some View {
        ZStack(alignment: .leading) {
                SecureField(&quot;&quot;, text: $value, prompt: prompt)
                    .focused($focusedField, equals: .secure)
                    .opacity(type.isSecure &amp;amp;&amp;amp; isPasswordHidden ? 1 : 0)
                
                TextField(&quot;&quot;, text: $value, prompt: prompt)
                    .focused($focusedField, equals: .normal)
                    .opacity(type.isSecure &amp;amp;&amp;amp; isPasswordHidden ? 0 : 1)
            }
            .autocapitalization(.none)
            .keyboardType(type.keyboardType)
            .onChange(of: isFocused) { newValue in
                if newValue {
                    focusedField = type.isSecure ? .secure : .normal
                }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Simulator Screen Recording - iPhone 16 - 2025-06-18 at 20.03.17.gif&quot; data-origin-width=&quot;295&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IeLro/btsOHIvGmAD/BikEgLJpUKqlh0VZfxU0YK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IeLro/btsOHIvGmAD/BikEgLJpUKqlh0VZfxU0YK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IeLro/btsOHIvGmAD/BikEgLJpUKqlh0VZfxU0YK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/IeLro/btsOHIvGmAD/BikEgLJpUKqlh0VZfxU0YK/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;240&quot; height=&quot;521&quot; data-filename=&quot;Simulator Screen Recording - iPhone 16 - 2025-06-18 at 20.03.17.gif&quot; data-origin-width=&quot;295&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;✅&lt;span&gt; &lt;/span&gt;&lt;/span&gt;컴포넌트 내부에서&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;포커스 대상을 명&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 style=&quot;color: #333333; text-align: start;&quot;&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;키&lt;/span&gt;&lt;span&gt;보드 타입과&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;포커스 상태&lt;/span&gt;&lt;span&gt;를 독립적으로&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;관리 가능&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;배운&amp;nbsp;점&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SwiftUI의&amp;nbsp;뷰 업데이트 메커니즘&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 구현을&amp;nbsp;통해 SwiftUI의&amp;nbsp;뷰 업데이트 메커니즘에 대해&amp;nbsp;깊이 이해할 수 있었습니다. 단순한&amp;nbsp;opacity&amp;nbsp;변경만으로는 실질적인 뷰 재평가가 발생하지 않으며,&amp;nbsp;뷰의 정체성(id)을&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;뷰 수정자의 순서&amp;nbsp;중요성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI에서 뷰 수정자의 순서가 실행 결과에 큰 영향을 미친다는 점도 배웠습니다. 특히 .id 수정자는 .focused 수정자보다 먼저 적용되어야 안정적으로 동작한다는 것을 확인했으며, 이를 통해 SwiftUI의 뷰 수정자가 선언 순서에 따라 적용된다는 중요한 특성을 다시 체감하게 되었습니다.&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;마지막으로, 복잡한 입력 폼에서의 포커스&amp;nbsp;상태 관리&amp;nbsp;전략에 대해 배웠습니다. 컴포넌트 내부에서 포커스 상태를 관리하면 더&amp;nbsp;세밀한 제어가&amp;nbsp;가능하며, 외부 상태 변화를 감지하여&amp;nbsp;내부 상태를 적절히 조정하는 패턴이 효과적이라는 것을 확인했습니다.&lt;/p&gt;
&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;SwiftUI에서 복잡한 입력&amp;nbsp;폼을&amp;nbsp;구현할 때는 프레임워크의 특성을 이해하고 이를&amp;nbsp;고려한&amp;nbsp;설계가 필요합니다. 특히 포커스 상태와 키보드&amp;nbsp;타입 관리와 같은 세부적인&amp;nbsp;UX 요소들은 프레임워크의&amp;nbsp;제약 사항을 파악하고, 이를 우회하거나 보완하는 전략이 중요하다는 것을&amp;nbsp;이번 구현을 통해&amp;nbsp;배울 수&amp;nbsp;있었습니다.&lt;/p&gt;</description>
      <category>개발지식 정리/Swift</category>
      <author>Neoself</author>
      <guid isPermaLink="true">https://neoself.tistory.com/92</guid>
      <comments>https://neoself.tistory.com/92#entry92comment</comments>
      <pubDate>Wed, 18 Jun 2025 20:04:50 +0900</pubDate>
    </item>
    <item>
      <title>[SwiftUI] 화면별 제스처를 통한 뒤로가기 활성화 여부 제어하기</title>
      <link>https://neoself.tistory.com/91</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 개요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS 앱 개발에서 화면 전환 시 탭바의 표시/숨김과 스와이프 백 제스처의 활성화/비활성화는 중요한 UX 요소입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI를 활용해 앱을 고도화하게 될 경우, 대부분은 SwiftUI 프레임워크에서 기본으로 제공하는 navigationBar 대신 커스텀 제작한 헤더를 통해 화면전환을 제어하게 될 것입니다. 하지만 이의 경우 아래 화면과 같이 SwiftUI의 기본 네비게이션 헤더와 중복되는 상황이 발생하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RERkk/btsOF6DfA00/gIKp6DCa2dhKFICHHhLLjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RERkk/btsOF6DfA00/gIKp6DCa2dhKFICHHhLLjK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;1686&quot; data-filename=&quot;스크린샷 2025-06-17 오후 9.46.19.png&quot; data-widthpercent=&quot;49.5&quot; style=&quot;width: 48.9217%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RERkk/btsOF6DfA00/gIKp6DCa2dhKFICHHhLLjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRERkk%2FbtsOF6DfA00%2FgIKp6DCa2dhKFICHHhLLjK%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;832&quot; height=&quot;1686&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cquYky/btsOFYr1zKS/LfRPMtjpuT6bOKA4Yf8LH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cquYky/btsOFYr1zKS/LfRPMtjpuT6bOKA4Yf8LH0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;863&quot; data-origin-height=&quot;1714&quot; data-filename=&quot;스크린샷 2025-06-17 오후 9.46.08.png&quot; style=&quot;width: 49.9155%;&quot; data-widthpercent=&quot;50.5&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cquYky/btsOFYr1zKS/LfRPMtjpuT6bOKA4Yf8LH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcquYky%2FbtsOFYr1zKS%2FLfRPMtjpuT6bOKA4Yf8LH0%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;863&quot; height=&quot;1714&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;(좌) .navigationBarBackButtonHidden(false) / (우) .navigationBarBackButtonHidden(true)&lt;/figcaption&gt;
&lt;/figure&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;span style=&quot;color: #000000;&quot;&gt;navigationBarBackButtonHidden(true)&lt;/span&gt;를 통해 기본 네비게이션 헤더 가리기를 수행하면 1차적으로 위 문제를 해결할 수 있습니다. 하지만, 해당 뷰 수정자를 적용시키면, 화면 좌측 끝에서 우측으로 스와이프할때 이전 화면으로 전환되는 PopGesture가 동작하지 않게 됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;왜죠?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI의 NavigationView와 NavigationStack은 내부적으로 UIKit의 UINavigationController를 사용하기 때문에, 내부구현코드를 통해 원인을 분석해보고자 했으나, 이는 공개되지 않았기에 파악이 불가했습니다. 다만 백 버튼이 숨겨진 상황은 뒤로 가면 안되는 상황임을 전달하기 위한 애플의 설계철학이 반영된 결정임을 추론해볼 수 있었습니다.&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;PopGesture의 무조건적 차단은 사용자 경험 측면에서는 바람직하지 않을 수 있습니다. &lt;span&gt;회원가입이나 결제와 같은 중요한 프로세스에서는 치명적인 오류로 이어질 수 있는 뒤로가기를 차단하기 위해 스와이프 백을 비활성화해야 하지만, 일반적인 사용 플로우에서는 스와이프 백 제스처를 유지하면서 뒤로가기 버튼만 숨기고 싶은 경우가 있습니다. &lt;/span&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;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;2. View Modifier의 개념&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;SwiftUI의 View Modifier는 뷰의 동작과 외관을 수정하는 재사용 가능한 컴포넌트입니다. Modifier는 다음과 같은 특징을 가집니다:&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;&lt;span&gt;1. 체이닝 가능&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 여러 Modifier 연속적 적용&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;&lt;span&gt;2. 재사용성&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 동일한 수정 여러 뷰에 적용&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;&lt;span&gt;3. 캡슐화&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 관련된 로직 하나의 컴포넌트로 추상화&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;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt;3. 스와이프 백 제어 구현&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span&gt;3.1 PopGestureManager 구현&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스와이프 백 제스처의 상태를 관리합니다. 싱글톤 패턴을 사용해 앱 전체에서 일관된 상태를 유지하고자 했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750157485879&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@MainActor
final class PopGestureManager {
    static let shared = PopGestureManager()
    
    private init() {}
    
    private(set) var isSwipeBackDisabled = false
    
    func updateSwipeBackDisabled(to bool: Bool) {
        isSwipeBackDisabled = bool
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span&gt;3.2 SwipeBackDisabledViewModifier 구현&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 SwiftUI의 뷰 생명주기를 접근하기 위해 ViewModifier를 새로 생성한 후, onAppear 뷰 수정자에 앞서 생성한 PopGestureManager을 호출해, 뷰가 나타날 때 ViewModifier를 통해 주입한 제스처의 활성화 여부값으로 동적으로 변경되게끔 설계했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750157649015&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct SwipeBackDisabledViewModifier: ViewModifier {
    let isDisabled: Bool
    
    func body(content: Content) -&amp;gt; some View {
        content
            .onAppear {
                PopGestureManager.shared.updateSwipeBackDisabled(to: isDisabled)
            }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span&gt;3.3 UINavigationController 확장&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p 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;실제 스와이핑 제스처에 대한 차단 여부를 조작하기 위해 UINavigationController의 확장을 새로 구현했습니다. 해당 확장에서는 UINavigationController의 interactivePopGestureRecognizer 속성의 delegate를 자신으로 설정한 후, UIGestureRecognizerDelegate 프로토콜의 gestureRecognizerShouldBegin 메서드를 구현하여 제스처 시작 여부를 제어했습니다. 이 메서드 내부에서는 PopGestureManager에서 관리하는 isSwipeBackDisabled 불린값과 현재 네비게이션 스택의 뷰컨트롤러 개수를 확인하여, 스와이프 백 제스처 활성화 여부를 화면 단위로 동적 변경할 수 있게 구현했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-test-render-count=&quot;1&quot;&gt;
&lt;div data-is-streaming=&quot;false&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1750157758583&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;extension UINavigationController: ObservableObject, UIGestureRecognizerDelegate {
    open override func viewDidLoad() {
        super.viewDidLoad()
        interactivePopGestureRecognizer?.delegate = self
    }

    public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -&amp;gt; Bool {
        return !PopGestureManager.shared.isSwipeBackDisabled &amp;amp;&amp;amp; viewControllers.count &amp;gt; 1
    }
}&lt;/code&gt;&lt;/pre&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;size18&quot;&gt;&lt;b&gt;3.4. View 확장자에 ViewModifier 연결&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1750160498324&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import SwiftUI
import UIKit

extension View {
    func swipeBackDisabled(_ isDisabled: Bool) -&amp;gt; some View {
        modifier(SwipeBackDisabledViewModifier(isDisabled: isDisabled))
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, SwiftUI의 View 확장을 통해 커스텀 ViewModifier를 편리하게 사용할 수 있는 헬퍼 메서드를 구현해, SwiftUI 뷰에서 메서드 체이닝 방식으로 간편하게 스와이프 백 제스처 비활성화 기능을 적용할 수 있게 되었습니다.&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;pre id=&quot;code_1750163607477&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct ExampleView: View {
    
    var body: some View {
        VStack {
            headerSection
            ...
        }
        .swipeBackDisabled(true) // 해당화면에서는 스와이핑 동작을 통한 뒤로가기 불가
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 프로젝트 규모가 커짐에 따라 화면이 늘어날때마다 각 화면에 대한 구현 파일에서 해당 뷰 수정자 상태를 일일히 변경하는 것은 불필요한 리소스 낭비로 이어질 것이라 판단했고, 휴먼에러 발생 가능성과도 이어질 수 있을 것이라 생각했습니다. 때문에, 이전에 글로 공유드렸던 Navigation Router 패턴에 이를 조합하여, 화면 별 백스와이핑 제어를 중앙화하는 구조를 고안하게 되었습니다. &lt;br /&gt;&lt;br /&gt;특히 전환대상 화면에 대한 정보를 담는 Route Enum에서 disableSwipeBack이라는 연관 속성을 필수로 포함시키게끔 구조를 설계해, 새로운 화면이 추가될 때마다 해당 화면의 Route를 정의하면서 자연스럽게 백 제스처에 대한 설정값도 포함시킬 수 있도록 했습니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt;4. 라우팅 시스템과의 통합&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1750164095432&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import SwiftUI

struct MainNavigationStack: View {
    @StateObject private var router = AppRouter()
    
    var body: some View {
        NavigationStack(path: $router.path) {
            MainTabView()
                .navigationDestination(for: AppRoute.self) { route in
                    destinationView(for: route)
                        .swipeBackDisabled(route.disableSwipeBack) // 스와이프 백 제스처 제어
                }
        }
        .environmentObject(router)
    }
    
    @ViewBuilder
    private func destinationView(for route: AppRoute) -&amp;gt; some View {
        switch route {
        case .homeDetail:
            HomeDetailView()
        case .profileEdit:
            ProfileEditView()
        case .settings:
            SettingsView()
        case .listView:
            ExampleListView()
        case .detailView(let id):
            ExampleDetailView(id: id)
        case .formView:
            ExampleFormView()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 정의했던 NavigationStack 구조체에 정의된 navigationDestination 내부에 뷰 수정자를 추가함으로써, 파일에 대한 이동 없이 단일 파일에서 백 제스처에 대한 제어를 쉽게 할 수 있게 됩니다.&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;</description>
      <category>개발지식 정리/Swift</category>
      <author>Neoself</author>
      <guid isPermaLink="true">https://neoself.tistory.com/91</guid>
      <comments>https://neoself.tistory.com/91#entry91comment</comments>
      <pubDate>Tue, 17 Jun 2025 21:43:24 +0900</pubDate>
    </item>
    <item>
      <title>[SwiftUI] Navigation Router 패턴: 복잡한 네비게이션 플로우 관리하기</title>
      <link>https://neoself.tistory.com/90</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;최근 Sw&lt;/span&gt;&lt;span&gt;iftUI 프로젝트에서 복잡한 네비게이션 플로우를 구현하면&lt;/span&gt;&lt;span&gt;서 Navigation Router 패턴을 도&lt;/span&gt;&lt;span&gt;입하게 되었습니다.&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;기존의 NavigationView/NavigationStack&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;기반 접근법으로는 다&lt;/span&gt;&lt;span&gt;단계 회원가입 플&lt;/span&gt;&lt;span&gt;로우와 같은 복잡한&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;네비게이션을&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;관리하기 어려웠&lt;/span&gt;&lt;span&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;*해당 네비게이션 방식은 iOS 16버전부터 지원되는 방식입니다. 때문에, 최소버전이 15이하일 경우에는 도움이 되지 않을 수 있습니다!&lt;/span&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;&lt;span&gt;0. 기&lt;/span&gt;&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;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span&gt;1. Navig&lt;/span&gt;&lt;span&gt;ationStack 중첩 문제&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처음에는&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;각 View에&lt;/span&gt;&lt;span&gt;서 독립적으로 NavigationStack&lt;/span&gt;&lt;span&gt;을 관리하고자 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749635432442&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct ExampleApp: App {
    var body: some Scene {
        WindowGroup {
            NavigationStack {  // 첫 번째 NavigationStack
                OnboardingView()
            }
        }
    }
}

// CreateAccountView.swift 
struct CreateAccountView: View {
    @StateObject private var viewModel = CreateAccountViewModel()
    
    var body: some View {
        NavigationStack(path: $viewModel.navigationPath) {  // 두 번째 NavigationStack (중첩!)
            // ... view content
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span&gt;&lt;span&gt;문제점&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;- Navig&lt;/span&gt;&lt;span&gt;ationStack이 중첩되어 네&lt;/span&gt;&lt;span&gt;비게이션이 예&lt;/span&gt;&lt;span&gt;측 불가능하&lt;/span&gt;&lt;span&gt;게 동작&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;- 각 View&lt;/span&gt;&lt;span&gt;가 자체 네비게이션&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;상태를 관리하여&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&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;스처와 네비게이션&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;바 버튼이 올&lt;/span&gt;&lt;span&gt;바르게 작동하지&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;않음&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span&gt;2.&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;상태 관리의 복&lt;/span&gt;&lt;span&gt;잡성&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749635514793&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// CreateAccountViewModel.swift - 기존 방식
@MainActor
final class CreateAccountViewModel: ObservableObject {
    @Published var navigationPath = NavigationPath()  // 분산된 네비게이션 상태
    @Published var currentStepIndex: Int = 0
    
    func proceedToNextStep() {
        // 어떤 화면으로 이동할지 복잡한 로직
        switch currentStepIndex {
        case 0: navigationPath.append(&quot;accountForm&quot;)
        case 1: navigationPath.append(&quot;endCreateAccount&quot;)
        // ...
        }
    }
}&lt;/code&gt;&lt;/pre&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;span&gt;&lt;span&gt;문제점&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;각 ViewModel이 네&lt;/span&gt;&lt;span&gt;비게이션 로직을 중&lt;/span&gt;&lt;span&gt;복 구현&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;- 화면 간 데이터 전&lt;/span&gt;&lt;span&gt;달이 복잡&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;- 테스트하기 어려&lt;/span&gt;&lt;span&gt;운 구조&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt;1. &lt;/span&gt;&lt;span&gt;Navigation Router 패턴 선정&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해당 패턴을 택하게 된 이유는 아래와 같이 정리해볼 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;1. &lt;b&gt;단일 책임&lt;/b&gt;&lt;/span&gt;&lt;b&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;원칙&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: 네&lt;/span&gt;&lt;span&gt;비게이션 관&lt;/span&gt;&lt;span&gt;리만을 담당하는 Router&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;클래스&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;2. &lt;b&gt;타입 안전성&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;: Enum 기반 Route&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;정의로&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;컴파일 타임 오&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;3.&lt;b&gt; 중앙화&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;앱 전체의 네비게&lt;/span&gt;&lt;span&gt;이션 플로우를 한&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;곳에서 관리&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;4. &lt;b&gt;확장성&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;새로운 화&lt;/span&gt;&lt;span&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;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;특히, 현재 프로젝트의 경우 화면마다 선택적으로 적용되어야 하는 하기 커스텀 뷰 수정자들이 존재했었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749635916657&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ExampleView()
    .isTabHidden(_ isHidden: Bool) // 하단 네비게이션 탭 표시여부 제어
    .isBackSwipeDisabled(_ isDisabled: Bool) // 스와이핑을 통한 화면 뒤로가기 제어&lt;/code&gt;&lt;/pre&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;span&gt;때문에, 모든 화면들에 대한 뷰 수정자 적용 여부를 한곳에서 확인 및 수정할 수 있게 된다는 점이 제게 가장 큰 장점으로 다가왔습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt;2.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Navigation Router 패턴 도&lt;/span&gt;&lt;span&gt;입&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span&gt;1. Route&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;프로토콜 정&lt;/span&gt;&lt;span&gt;의&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;참고한 블로그의 경우 프로젝트 내부 모든 화면을 단일 Route로 관리하고 있었지만, 저희 앱에서는 네비게이션 스택이 완전히 분리되어야 하는 2개의 플로우가 존재했기 때문에, 2개 이상의 Route 및 Router 인스턴스가 필요했습니다. 때문에, 모든 Route가 공통적으로 필요로 하는 요구사항을 Protocol로 정의해 Route 구현 간 중복코드를 제거하고, Route 추가에 드는 리소스를 절감하고자 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;여기서 Route를 열거형화하면서, 필수정보인 id 속성뿐만 아니라 화면마다 매핑되는 메타데이터를 최대한 정의하여 타입 안정성 및 유지보수성을 높이고자 했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749636186327&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Route.swift
import Foundation

protocol Route: Hashable, CaseIterable {
    var id: String { get } // NavigationPath에서 사용되는 키값
    var analyticsName: String { get } // GoogleFirebase Analytics 로깅에 사용되는 이름
    var disableSwipeBack: Bool { get } // 스와이핑을 통한 뒤로가기 지원 여부
    var hidesTabBar: Bool { get } // 커스텀 하단 네비게이션 바 표시 여부
}

// extension으로 Hashable 프로토콜의 필수구현사항을 구현하여 중복 코드를 최소화합니다.
extension Route {
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
    
    static func == (lhs: Self, rhs: Self) -&amp;gt; Bool {
        return lhs.id == rhs.id
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;CaseIterable&lt;/b&gt;: allCases 프로퍼티를 자동으로 제공받을 수 있는 프로토콜&lt;br /&gt;&lt;b&gt;Hashable:&lt;/b&gt; NavigationPath에서 Route를 키로 사용하기 위해 필수로 채택되어야 하는 프로토콜&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span&gt;2. 도메인별&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Route 구현&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이후, 각 도메인 별로&lt;/span&gt;&lt;span&gt;&amp;nbsp;Route를&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;정의하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749636368212&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// AuthRoute.swift
import Foundation

enum AuthRoute: Route {
    case createAccount
    case accountForm
    case endCreateAccount
    
    var id: String {
        switch self {
        case .createAccount: return &quot;createAccount&quot;
        case .accountForm: return &quot;accountForm&quot;
        case .endCreateAccount: return &quot;endCreateAccount&quot;
        }
    }
    
    var analyticsName: String {
        switch self {
        case .createAccount: return &quot;create_account_start_screen&quot;
        case .accountForm: return &quot;create_account_form_screen&quot;
        case .endCreateAccount: return &quot;account_creation_complete_screen&quot;
        }
    }
    
    var disableSwipeBack: Bool {
        switch self {
        case .accountForm: return true  // 단계별 진행이므로 스와이프 뒤로가기 비활성화
        default: return false
        }
    }
    
    var hidesTabBar: Bool {
        return false  // 인증 플로우에서는 탭바가 없음
    }
}&lt;/code&gt;&lt;/pre&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span&gt;3. Router 클래스&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;구현&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 각 도메인마다 화면 전환 로직을 담당하는 Router 클래스를 정의하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749636598891&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// AuthRouter.swift
import SwiftUI
import Combine

@MainActor
final class AuthRouter: ObservableObject {
    @Published var path = NavigationPath()
    
    func push(to route: AuthRoute) {
        path.append(route)
    }
    
    func pop() {
        guard !path.isEmpty else { return }
        path.removeLast()
    }
    
    func reset() {
        path = NavigationPath()
    }
    
    func popTo(count: Int) {
        guard count &amp;gt; 0, count &amp;lt;= path.count else { return }
        path.removeLast(count)
    }
    
    func replace(with route: AuthRoute) {
        path = NavigationPath()
        path.append(route)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. NavigationStack 래퍼 구현&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 navigationDestination에 대한 처리로직을 구현하고, 화면별로 매핑되는 뷰 수정자 적용여부를 관리하기 위한 래퍼 구조체를 새로 생성하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749636842120&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// AuthNavigationStack.swift
import SwiftUI

struct AuthNavigationStack: View {
    @StateObject private var authRouter = AuthRouter()
    
    var body: some View {
        NavigationStack(path: $authRouter.path) {
            OnboardingView()
                .navigationDestination(for: AuthRoute.self) { route in
                    destinationView(for: route)
                        .swipeBackDisabled(route.disableSwipeBack)
                        .onAppear {
                            // 분석 이벤트 로깅
                            print(&quot;  Auth Analytics: \(route.analyticsName)&quot;)
                        }
                }
        }
        .environmentObject(authRouter)
    }
    
    @ViewBuilder
    private func destinationView(for route: AuthRoute) -&amp;gt; some View {
        switch route {
        case .createAccount:
            CreateAccountView()
        case .accountForm:
            AccountFormView()
        case .endCreateAccount:
            EndCreateAccountView()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보시는 것과 같이 레퍼 구조체 내부에서 Route 열거형에 정의된 모든 화면에 접근이 가능하기 때문에, 위 코드처럼 swipeBackDisabled 뷰 수정자를 동적으로 삽입할 수 있습니다. 또한 화면전환마다 onAppear 수정자가 호출되기에, 이벤트 로깅을 수행하기에도 적합합니다.&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;이때, navigationDestination 뷰 수정자의 클로저 내부 구현체를 별도 메서드로 분리하였는데요. 이는 전환될 수 있는 화면 개수가 많아질 경우, 코드 가독성이 저하되는 것을 방지하기 위함입니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span&gt;5. 메&lt;/span&gt;&lt;span&gt;인 앱 구조 통&lt;/span&gt;&lt;span&gt;합&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 도메인별로 Router를 분리하였다면, 앱 최상단인 App 구조체에서 이를 분기해 네비게이션 구현이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 각 플로우가 독립적인 NavigationStack을 보유하게 됨에 따라, 화면 간 의존성이 저하되어 유지보수가 용이해집니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749637097047&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ExampleApp.swift
@main
struct ExampleApp: App {
    @StateObject private var authManager = AuthManager.shared
    
    var body: some Scene {
        WindowGroup {
            if authManager.isLoggedIn {
                MainNavigationStack()  // 로그인 후 메인 플로우
            } else {
                AuthNavigationStack()  // 인증 플로우
            }
        }
    }
}&lt;/code&gt;&lt;/pre&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;size18&quot;&gt;&lt;b&gt;6. Router 사용&lt;/b&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;MVVM 아키텍처를 기준으로 ViewModel과 View 두가지 선택지가 크게 있을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 ViewModel은 순수 비즈니스로직만을 담당해야하기 때문에, View파일에서 해당 네비게이션 로직을 호출하도록 설계했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749637386332&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// OnboardingView.swift
struct OnboardingView: View {
    @StateObject private var viewModel = OnboardingViewModel()
    @EnvironmentObject private var authRouter: AuthRouter // Router 환경객체 구독
    
    var body: some View {
        VStack(spacing: 0) {
            // ... UI 컴포넌트들

            Button(action:{
                authRouter.push(to: .createAccount)  // 간단한 네비게이션
            }) {
                Text(&quot;회원가입 화면으로 전환하기&quot;)
            }
        }
    }
}

// CreateAccountView.swift
struct CreateAccountView: View {
    @EnvironmentObject private var authRouter: AuthRouter
    
    var body: some View {
        VStack {
            Text(&quot;본인인증이 필요합니다&quot;)

            Spacer()

            Button(&quot;시작하기&quot;) {
                authRouter.push(to: .accountForm)  // 타입 안전한 네비게이션
            }
        }   
    }
}&lt;/code&gt;&lt;/pre&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt;3.&lt;span&gt;&amp;nbsp;결론&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Navigation Router 패턴을 도입한 결과 아래 장점을 체감할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 코드 품질 향상&lt;/b&gt;: 네비게이션&amp;nbsp;로직의 중앙화로&amp;nbsp;유지보수성 증대&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 개발 생산성 향상&lt;/b&gt;: 타입 안전성으로 런타임 오류 감소&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 사용자 경험 개선&lt;/b&gt;: 일관된 네비게이션 동작과 제스처&amp;nbsp;제어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 확장성 확보&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 하단 네비게이션 탭 표시 제어 로직&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 스와이핑을 통한 뒤로가기 제어 로직&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 과정을 다루겠습니다.&lt;/p&gt;</description>
      <category>개발지식 정리/Swift</category>
      <author>Neoself</author>
      <guid isPermaLink="true">https://neoself.tistory.com/90</guid>
      <comments>https://neoself.tistory.com/90#entry90comment</comments>
      <pubDate>Wed, 11 Jun 2025 19:31:07 +0900</pubDate>
    </item>
    <item>
      <title>[SwiftUI] 바텀시트 외부영역의 어두워짐 효과 제거하기</title>
      <link>https://neoself.tistory.com/89</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI에서는 바텀시트 UI를 위해 .sheet() 뷰 수정자를 제공하고 있습니다. 개발자는 해당 수정자에 SwiftUI 뷰를 클로저 내부에 선언함으로써,&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;자연스러운 애니메이션 효과와 함께&lt;/span&gt; 원하는 뷰를 부모 뷰 상단에 표시할 수 있게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-04 오후 3.19.34.png&quot; data-origin-width=&quot;632&quot; data-origin-height=&quot;1300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pRzDj/btsOpOiKGFa/q3jQLUjxqnGgGl2NxJq1kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pRzDj/btsOpOiKGFa/q3jQLUjxqnGgGl2NxJq1kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pRzDj/btsOpOiKGFa/q3jQLUjxqnGgGl2NxJq1kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpRzDj%2FbtsOpOiKGFa%2Fq3jQLUjxqnGgGl2NxJq1kk%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;320&quot; height=&quot;658&quot; data-filename=&quot;스크린샷 2025-06-04 오후 3.19.34.png&quot; data-origin-width=&quot;632&quot; data-origin-height=&quot;1300&quot;/&gt;&lt;/span&gt;&lt;/figure&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;하지만, .sheet 뷰 수정자의 기본형태를 사용할 경우, 바텀시트 표시와 함께 바텀시트 외부 배경또한 어두워지게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749018287266&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.sheet(isPresented: $isDetailPresent) {
    ExampleBottomSheet()
        .presentationBackgroundInteraction(.enabled)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 부모 뷰에 대한 상호작용 가능여부를 조정하는 위 뷰 수정자를 .enabled로 설정하게 되면, 외부영역에 대한 상호작용이 가능해질 뿐만 아니라, 아래와 같이 바텀시트 외부영역에 대해 Dimming 처리되지 않게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-04 오후 3.19.44.png&quot; data-origin-width=&quot;660&quot; data-origin-height=&quot;1310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eliZk4/btsOoE9f28L/KPzCbJI16kbcWJkUXZtv00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eliZk4/btsOoE9f28L/KPzCbJI16kbcWJkUXZtv00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eliZk4/btsOoE9f28L/KPzCbJI16kbcWJkUXZtv00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeliZk4%2FbtsOoE9f28L%2FKPzCbJI16kbcWJkUXZtv00%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;320&quot; height=&quot;635&quot; data-filename=&quot;스크린샷 2025-06-04 오후 3.19.44.png&quot; data-origin-width=&quot;660&quot; data-origin-height=&quot;1310&quot;/&gt;&lt;/span&gt;&lt;/figure&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Reference&lt;/b&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.appcoda.com/swiftui-bottom-sheet-background/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.appcoda.com/swiftui-bottom-sheet-background/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1749018503165&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Customizing SwiftUI Bottom Sheet's Background and Scrolling Behaviour&quot; data-og-description=&quot;Since the release of iOS 16, it&amp;rsquo;s easy to create an interactive bottom sheet using SwiftUI. All you need to do is to embed a modifier called presentationDetents in a Sheet view. Earlier, we published a detailed tutorial to walk you through the API. Howev&quot; data-og-host=&quot;www.appcoda.com&quot; data-og-source-url=&quot;https://www.appcoda.com/swiftui-bottom-sheet-background/&quot; data-og-url=&quot;https://www.appcoda.com/swiftui-bottom-sheet-background/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/qNIUO/hyY5eKzJUl/uZQzhWgP1xGvCwyt8Wbiuk/img.jpg?width=1600&amp;amp;height=900&amp;amp;face=0_0_1600_900,https://scrap.kakaocdn.net/dn/dlXdmy/hyY360iYUI/GYFJUnN0z3cCI9lGRr6gH1/img.jpg?width=1600&amp;amp;height=900&amp;amp;face=0_0_1600_900,https://scrap.kakaocdn.net/dn/bw0SOk/hyY06AFJn7/5QfjmTwaqhTFhFbu7UMQrK/img.png?width=1706&amp;amp;height=948&amp;amp;face=0_0_1706_948&quot;&gt;&lt;a href=&quot;https://www.appcoda.com/swiftui-bottom-sheet-background/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.appcoda.com/swiftui-bottom-sheet-background/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/qNIUO/hyY5eKzJUl/uZQzhWgP1xGvCwyt8Wbiuk/img.jpg?width=1600&amp;amp;height=900&amp;amp;face=0_0_1600_900,https://scrap.kakaocdn.net/dn/dlXdmy/hyY360iYUI/GYFJUnN0z3cCI9lGRr6gH1/img.jpg?width=1600&amp;amp;height=900&amp;amp;face=0_0_1600_900,https://scrap.kakaocdn.net/dn/bw0SOk/hyY06AFJn7/5QfjmTwaqhTFhFbu7UMQrK/img.png?width=1706&amp;amp;height=948&amp;amp;face=0_0_1706_948');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Customizing SwiftUI Bottom Sheet's Background and Scrolling Behaviour&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Since the release of iOS 16, it&amp;rsquo;s easy to create an interactive bottom sheet using SwiftUI. All you need to do is to embed a modifier called presentationDetents in a Sheet view. Earlier, we published a detailed tutorial to walk you through the API. Howev&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.appcoda.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발지식 정리/Swift</category>
      <category>BottomSheet</category>
      <category>Dimming</category>
      <category>presentationbackgroundinteraction</category>
      <category>SwiftUI</category>
      <category>배경</category>
      <author>Neoself</author>
      <guid isPermaLink="true">https://neoself.tistory.com/89</guid>
      <comments>https://neoself.tistory.com/89#entry89comment</comments>
      <pubDate>Wed, 4 Jun 2025 15:29:05 +0900</pubDate>
    </item>
    <item>
      <title>Swift Concurrency: Behind the scenes 정리</title>
      <link>https://neoself.tistory.com/88</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-10 오후 8.53.32.png&quot; data-origin-width=&quot;2938&quot; data-origin-height=&quot;1412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tYzNc/btsNgxvNnty/d7Ju0JMsTZrnP73l7VRz20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tYzNc/btsNgxvNnty/d7Ju0JMsTZrnP73l7VRz20/img.png&quot; data-alt=&quot;아래 도식 흐름에 대한 코드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tYzNc/btsNgxvNnty/d7Ju0JMsTZrnP73l7VRz20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtYzNc%2FbtsNgxvNnty%2Fd7Ju0JMsTZrnP73l7VRz20%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;2938&quot; height=&quot;1412&quot; data-filename=&quot;스크린샷 2025-04-10 오후 8.53.32.png&quot; data-origin-width=&quot;2938&quot; data-origin-height=&quot;1412&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;아래 도식 흐름에 대한 코드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqb1yq/btsNfdyAdFD/8A20V6oW7daov3I3zLgTOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqb1yq/btsNfdyAdFD/8A20V6oW7daov3I3zLgTOK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2520&quot; data-origin-height=&quot;1480&quot; data-filename=&quot;스크린샷 2025-04-10 오후 8.41.30.png&quot; style=&quot;width: 49.7157%; margin-right: 10px;&quot; data-widthpercent=&quot;50.3&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqb1yq/btsNfdyAdFD/8A20V6oW7daov3I3zLgTOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbqb1yq%2FbtsNfdyAdFD%2F8A20V6oW7daov3I3zLgTOK%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;2520&quot; height=&quot;1480&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mhuEl/btsNe3iIa3e/DhQrQx5tOfG9834LGfWJJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mhuEl/btsNe3iIa3e/DhQrQx5tOfG9834LGfWJJK/img.png&quot; data-origin-width=&quot;2574&quot; data-origin-height=&quot;1530&quot; data-is-animation=&quot;false&quot; data-filename=&quot;스크린샷 2025-04-10 오후 8.41.12.png&quot; data-widthpercent=&quot;49.7&quot; style=&quot;width: 49.1215%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mhuEl/btsNe3iIa3e/DhQrQx5tOfG9834LGfWJJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmhuEl%2FbtsNe3iIa3e%2FDhQrQx5tOfG9834LGfWJJK%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;2574&quot; height=&quot;1530&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&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;기존 Grand CentalDispath에서 사용자가 구독한 뉴스를 URLSession으로 불러와 사용자 뉴스 피드에 보여주고자 할때, 위와 같은 도식의 흐름이 필요했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 스레드는 사용자 입력에 대한 반응성을 유지하도록 하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상호배제를 보장하는 직렬큐를 통해 추후 URLSession으로 부터 받는 결과들을 관리함으로써, 데이터 베이스 접근 간 상호 배제를 보장할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같은 구조에서 네트워크 요청 결과가 도착하면, URLSession의 콜백이 병렬 큐에서 호출되어 UI를 새로 고치게 될 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 것은 GCD에서 작업이 대기열에 등록되면, &lt;u&gt;&lt;b&gt;스레드가 새로 생성&lt;/b&gt;&lt;/u&gt;되어 작업을 처리한다는 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-10 오후 8.47.27.png&quot; data-origin-width=&quot;3322&quot; data-origin-height=&quot;1322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lIgsd/btsNho6v1j5/DNTaodMnpYMyYk9h0eWih0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lIgsd/btsNho6v1j5/DNTaodMnpYMyYk9h0eWih0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lIgsd/btsNho6v1j5/DNTaodMnpYMyYk9h0eWih0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlIgsd%2FbtsNho6v1j5%2FDNTaodMnpYMyYk9h0eWih0%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;3322&quot; height=&quot;1322&quot; data-filename=&quot;스크린샷 2025-04-10 오후 8.47.27.png&quot; data-origin-width=&quot;3322&quot; data-origin-height=&quot;1322&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 모든 여유 CPU 코어를 점유할때까지  시스템은 새로 생성한 스레드들을 할당할 것이며, 네트워크 및 I/O 작업, 락과 같은 동기화 매커니즘 사용으로 인해 스레드가 블록될 경우, GCD는 추가 스레드를 생성해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 남은 작업을 처리하게끔 설계하여 blocked된 스레드가 다시 unblock되기 위해 필요로 하는 추가 작업을 수행할 수 있도록 하거나,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.스레드의 자원 대기 문제를 해결하게끔 합니다.&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;하지만, 설령 사용자가 백개의 피드를 업데이트하는 것과 같이 많은 비동기작업 요청이 발생하면, 각 데이터 작업은 완료 이후 UI 업데이트라는 후속작업을 실행시켜야 하기 때문에, 동시 큐 내부에서 모두 completion block을 갖게 되며, 스레드가 과도하게 생성될 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-10 오후 9.00.36.png&quot; data-origin-width=&quot;3392&quot; data-origin-height=&quot;1340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/H7y1n/btsNhJCBnYB/zO8312XZG75GDeRdL47TV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/H7y1n/btsNhJCBnYB/zO8312XZG75GDeRdL47TV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/H7y1n/btsNhJCBnYB/zO8312XZG75GDeRdL47TV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FH7y1n%2FbtsNhJCBnYB%2FzO8312XZG75GDeRdL47TV1%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;3392&quot; height=&quot;1340&quot; data-filename=&quot;스크린샷 2025-04-10 오후 9.00.36.png&quot; data-origin-width=&quot;3392&quot; data-origin-height=&quot;1340&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국, 시스템이 CPU 코어수(A17 기준 6코어)보다 더 많은 스레드를 처리하게 되며, 스레드 폭발 현상이 발생합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;스레드 폭발&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 자원을 고갈시켜, 성능이 급격히 저하되는 현상&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 각 스레드는 약 &lt;u&gt;&lt;b&gt;1MB의 스택 메모리와 커널 리소스를 소비&lt;/b&gt;&lt;/u&gt;하기 때문에 현재 실행중인 스레드에 필요한 &lt;u&gt;&lt;b&gt;리소스가 고갈&lt;/b&gt;&lt;/u&gt;될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 새로운 스레드 생성 시, CPU는 기존 스레드에서 벗어나 새로운 스레드를 실행하기 위해 &lt;u&gt;&lt;b&gt;스레드 컨텍스트 전환&lt;/b&gt;&lt;/u&gt;을 수행해야합니다 .&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-10 오후 9.03.20.png&quot; data-origin-width=&quot;3128&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s1RiB/btsNfPR1vKM/EgimkE6aWsotb45mFCHcok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s1RiB/btsNfPR1vKM/EgimkE6aWsotb45mFCHcok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s1RiB/btsNfPR1vKM/EgimkE6aWsotb45mFCHcok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs1RiB%2FbtsNfPR1vKM%2FEgimkE6aWsotb45mFCHcok%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;3128&quot; height=&quot;374&quot; data-filename=&quot;스크린샷 2025-04-10 오후 9.03.20.png&quot; data-origin-width=&quot;3128&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 차단된 스레드가 다시 실행가능해지면, 스케줄러는 CPU에서 스레드를 타임쉐어함으로써, 모든 스레드가 계속 진행될 수 있도록 해야하지만, 스레드 폭발 상황에서는, 수백개의 스레드를 타임쉐어하기 때문에 과도한 컨텍스트 전환이 발생하게 됩니다. 이는 CPU 효율성을 큰 폭으로 저하시킵니다.&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;&quot;Continuation&quot;&lt;/b&gt;이라는 경량 객체를 사용해 차지하는 메모리와 리소스를 줄인다는 차이점을 갖고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-10 오후 9.06.42.png&quot; data-origin-width=&quot;3366&quot; data-origin-height=&quot;1332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bydX5A/btsNfPEAqnc/QMCnwntIe01FHritVoqCv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bydX5A/btsNfPEAqnc/QMCnwntIe01FHritVoqCv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bydX5A/btsNfPEAqnc/QMCnwntIe01FHritVoqCv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbydX5A%2FbtsNfPEAqnc%2FQMCnwntIe01FHritVoqCv0%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;3366&quot; height=&quot;1332&quot; data-filename=&quot;스크린샷 2025-04-10 오후 9.06.42.png&quot; data-origin-width=&quot;3366&quot; data-origin-height=&quot;1332&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조적 동시성 아래 작업을 수행하게 될 경우, 완전한 스레드 컨텍스트 스위칭 대신, &lt;u&gt;&lt;b&gt;Continuation 간의 전환만&lt;/b&gt;&lt;/u&gt; 수행하기에, 메서드 호출 비용만 소모하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국, 구조적 동시성은 런타임동안 CPU 코어수만큼만 스레드를 생성하고, 스레드가 블록될 경우 작업 아이템(Work Items)들 간 적은 비용으로 전환될 수 있게끔 보장합니다.&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;await 키워드를 사용할 경우 &lt;u&gt;&lt;b&gt;비동기 대기&lt;/b&gt;&lt;/u&gt;를 진행하게 되면서, 비동기 함수를 기다릴 동안 &lt;u&gt;&lt;b&gt;현재 스레드를 차단하지 않고&lt;/b&gt;&lt;/u&gt;, 일시 중단(스레드 해제)되며 다른 작업이 실행될 수 있도록 열어놓습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Swift 비동기 및 동시성 처리 모델&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-10 오후 9.19.40.png&quot; data-origin-width=&quot;3404&quot; data-origin-height=&quot;1216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1y7ac/btsNhQobOAm/GA6kKwDvfgKOkezEEojg51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1y7ac/btsNhQobOAm/GA6kKwDvfgKOkezEEojg51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1y7ac/btsNhQobOAm/GA6kKwDvfgKOkezEEojg51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1y7ac%2FbtsNhQobOAm%2FGA6kKwDvfgKOkezEEojg51%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;3404&quot; height=&quot;1216&quot; data-filename=&quot;스크린샷 2025-04-10 오후 9.19.40.png&quot; data-origin-width=&quot;3404&quot; data-origin-height=&quot;1216&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 Stack Frame은 연기 지점(await 키워드 위치) 전체에서 불필요한 지역변수를 저장합니다. 설령, 위 코드의 id와 article은 정의되자마자 for 반복문 내부에서 사용되기 때문에, StackFrame에 저장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와는 별개로 updateDatabase와 add 비동기 메서드를 위한 async frame이 Heap영역에 존재하며, 해당 비동기 프레임은 일시중단 지점 전체에서 사용할 수 있어야 하는 정보를 저장합니다. 설령, newArticles 변수는 일시중단 지점 전후에 모두 사용되어야 하기 때문에, newArticles 상태 추적을 위해 비동기 프레임에 저장되어야 합니다.&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;이후 스레드가 계속 실행될 경우 add 메서드 내부 database.save 메서드를 호출하게 되는데, 이때 Stack Frame은 새로 생성되어 push되는 것이 아니고, &lt;u&gt;&lt;b&gt;대체&lt;/b&gt;&lt;/u&gt;됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 설계의 이유는 향후 필요한 변수들이 모두 이미 비동기 프레임에 저장되어 있기 때문입니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-10 오후 9.25.10.png&quot; data-origin-width=&quot;3432&quot; data-origin-height=&quot;1206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wRqKW/btsNfI5UVb8/Etr0RkGeMZaxLlbfi7r501/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wRqKW/btsNfI5UVb8/Etr0RkGeMZaxLlbfi7r501/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wRqKW/btsNfI5UVb8/Etr0RkGeMZaxLlbfi7r501/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwRqKW%2FbtsNfI5UVb8%2FEtr0RkGeMZaxLlbfi7r501%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;3432&quot; height=&quot;1206&quot; data-filename=&quot;스크린샷 2025-04-10 오후 9.25.10.png&quot; data-origin-width=&quot;3432&quot; data-origin-height=&quot;1206&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 새로 호출된 save 메서드는 비동기 메서드이기에 이를 위한 새로운 비동기 프레임이 생성됩니다.&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;만일 이 단계에서, save 메서드가 장기간 Suspend 될 경우, 스레드는 블록되는 대신 다른 작업 수행을 위해 재사용되며, 비동기 프레임에서 일시 중단 지점에 대한 정보가 힙 영역에 보관되고 있기 때문에, 언제든지 실행을 재개할 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이 비동기 프레임 목록은 Continuation에 대한 런타임 표현이 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-10 오후 9.27.16.png&quot; data-origin-width=&quot;3116&quot; data-origin-height=&quot;1274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGI59U/btsNfFWwoEv/PYuwG6wtatLj2w5Tkq5Mkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGI59U/btsNfFWwoEv/PYuwG6wtatLj2w5Tkq5Mkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGI59U/btsNfFWwoEv/PYuwG6wtatLj2w5Tkq5Mkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGI59U%2FbtsNfFWwoEv%2FPYuwG6wtatLj2w5Tkq5Mkk%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;3116&quot; height=&quot;1274&quot; data-filename=&quot;스크린샷 2025-04-10 오후 9.27.16.png&quot; data-origin-width=&quot;3116&quot; data-origin-height=&quot;1274&quot;/&gt;&lt;/span&gt;&lt;/figure&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수는 await 지점에서 Continuation으로 분할될 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/He0Jx/btsNhiMjgOr/gBqDH1Hv3kMnfJrZgstIM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/He0Jx/btsNhiMjgOr/gBqDH1Hv3kMnfJrZgstIM0/img.png&quot; data-origin-width=&quot;3136&quot; data-origin-height=&quot;1750&quot; data-is-animation=&quot;false&quot; data-filename=&quot;스크린샷 2025-04-10 오후 9.41.22.png&quot; style=&quot;width: 46.9529%; margin-right: 10px;&quot; data-widthpercent=&quot;47.51&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/He0Jx/btsNhiMjgOr/gBqDH1Hv3kMnfJrZgstIM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHe0Jx%2FbtsNhiMjgOr%2FgBqDH1Hv3kMnfJrZgstIM0%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;3136&quot; height=&quot;1750&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bq1N5P/btsNhJbzW7r/7l3uKB93OG7Lg9fM0T82I0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bq1N5P/btsNhJbzW7r/7l3uKB93OG7Lg9fM0T82I0/img.png&quot; data-origin-width=&quot;3002&quot; data-origin-height=&quot;1516&quot; data-is-animation=&quot;false&quot; data-filename=&quot;스크린샷 2025-04-10 오후 9.43.16.png&quot; style=&quot;width: 51.8843%;&quot; data-widthpercent=&quot;52.49&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bq1N5P/btsNhJbzW7r/7l3uKB93OG7Lg9fM0T82I0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbq1N5P%2FbtsNhJbzW7r%2F7l3uKB93OG7Lg9fM0T82I0%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;3002&quot; height=&quot;1516&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좌측 코드는 비동기 메서드인 URLSession DataTask와 Continuation인 나머지 작업으로 분할될 수 있으며, 이 Continuation 작업은 비동기 메서드가 종료된 후에만 실행될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우측 코드는 withThrowingTaskGroup을 기점으로 부모 작업이 정의되며, 그 내부 group.async 클로저를 통해 여러 자식 작업이 생성되고 있는데, 여기서도 마찬가지로 여러 자식 작업들이 종료되어야 부모 작업이 진행될 수 있습니다.&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;이러한 코드의 종속성은 Swift의 동시성 런타임(Concurrency Runtime)에서 알수 있으며, 코드를 통해서도 scope를 통해 명시적으로 파악할 수 있습니다.&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;즉, Swift 언어기능을 사용해 대기 중에 작업을 일시중단할 수 있으며, 이때 실행중인 스레드는 작업 종속성을 추론하고 대신 다른 작업을 선택할 수 있습니다.&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;u&gt;&lt;b&gt;협동 스레드 풀&lt;/b&gt;&lt;/u&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;이는 CPU 코어 수만큼 스레드를 생성하기에, 시스템에 과도한 부담을 주지 않으며, 작업 항목이 차단되면 더 많은 스레드를 생성하던 GCD의 Cocurrent 큐와 달리 항상 진행할 수 있기 때문에, 런타임은 추후 생성되는 스레드 수를 신중하게 제어할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스레드 풀&lt;/b&gt;: 마치 자전거 대여소같이 &lt;span style=&quot;background-color: #ffffff; color: #09090b; text-align: start;&quot;&gt; &lt;/span&gt;&lt;b&gt;미리 생성된 스레드&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #09090b; text-align: start;&quot;&gt;들을 모아 놓은 영역입니다. 필요할 때마다 스레드를 생성하고 제거하는 대신, 풀에서 스레드를 가져와 사용하고 다시 반환합니다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #09090b; text-align: start;&quot;&gt;&lt;/span&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;span style=&quot;background-color: #ffffff; color: #09090b; text-align: start;&quot;&gt;하지만, 구조적 동시성을 도입하는 것이 항상 좋지만은 않습니다.&lt;/span&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;span style=&quot;background-color: #ffffff; color: #09090b; text-align: start;&quot;&gt;1. 성능 저하&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-10 오후 9.54.28.png&quot; data-origin-width=&quot;1654&quot; data-origin-height=&quot;914&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyyi2H/btsNgau2gnr/ZkcUqDbK8KFGB3QnobIHEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyyi2H/btsNgau2gnr/ZkcUqDbK8KFGB3QnobIHEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyyi2H/btsNgau2gnr/ZkcUqDbK8KFGB3QnobIHEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcyyi2H%2FbtsNgau2gnr%2FZkcUqDbK8KFGB3QnobIHEk%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;1654&quot; height=&quot;914&quot; data-filename=&quot;스크린샷 2025-04-10 오후 9.54.28.png&quot; data-origin-width=&quot;1654&quot; data-origin-height=&quot;914&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async let으로 선언된 isThumbnailView 속성은 매번 호출될때마다 비동기 작업을 수행합니다. 이는 매 호출마다 디스크에서 값을 읽어올 수 있게 됨에 따라 소요시간이 증가될 수 있으며, 현재 컨텍스트가 일시중단됨에 따라 데이터의 일관성이 저해될 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1744289877845&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 앱 시작 시 한 번만 읽고 메모리에 저장
let isThumbnailView = UserDefaults.standard.bool(forKey: &quot;ViewType&quot;)

// 필요할 때 동기적으로 사용
if isThumbnailView {
    // Perform thumbnail view layout
} else {
    // Perform list view layout
}&lt;/code&gt;&lt;/pre&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;2. 데이터 원자성(안정성) 저하&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Await 사용시, 실행했던 스레드가 Continuation을 실행할 것이라는 보장이 없을뿐더러, 일시중단될 동안 앱 전역 상태가 변경될 수 있게 됨에 따라 일관성 또한 준수되지 않게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에, await 이전에 lock을 유지할 경우, 다음 스레드가 이전에 실행된 스레드가 아닐 경우, 스레드가 계속 Block될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드별 데이터는 await에서도 보존되지 않습니다.&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;3. 런타임 계약 미준수 시 fatal 에러&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조적 동시성 사용시, 런타임 계약을 유지하여야 협력적 스레드 풀이 최적의 상태에서 동작하며 스레드를 지속적으로 수행할 수 있게 됩니다. 다만, 이때 Swift 런타임에 알려진 작업(Continuation, Child Task)들만 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;협력적 스레드 풀에서 대기할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이러한 런타임 계약을 유지하기 위해선 코드에서 명시될 수 있는 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;await, Actors, TaskGroup과 같은 구조적 동시성의 기본 요소를 사용해야합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-10 오후 10.08.12.png&quot; data-origin-width=&quot;3058&quot; data-origin-height=&quot;1140&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzCqfs/btsNhr9HNIi/adkIBfJyr73U9YkZ5isH9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzCqfs/btsNhr9HNIi/adkIBfJyr73U9YkZ5isH9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzCqfs/btsNhr9HNIi/adkIBfJyr73U9YkZ5isH9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzCqfs%2FbtsNhr9HNIi%2FadkIBfJyr73U9YkZ5isH9k%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;3058&quot; height=&quot;1140&quot; data-filename=&quot;스크린샷 2025-04-10 오후 10.08.12.png&quot; data-origin-width=&quot;3058&quot; data-origin-height=&quot;1140&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 NSLock과 같은 기본 요소는 사용이 가능합니다만 주의가 필요합니다. 이는 짧은 시간동안 스레드를 차단할 수 있지만 전진 진행이라는 런타임계약을 위반하지 않습니다. 다만, 올바른 사용을 돕는 컴파일러 지원이 없기에 개발자의 책임이 중요합니다. 하지만, DispatchSemaphore와 같은 요소는 런타임에서 의존성 정보를 숨기게 되면서 런타임이 올바른 스케줄링 결정을 내리고 해결할 수 없기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히&amp;nbsp; 구조화되지 않은 작업을 생성한 다음, 세마포어를 통해 작업 경계를 넘어 의존성을 소급적으로 도입하면, 다른 스레드가 세마포어를 해제할 수 있을때까지 스레드가 무기한으로 세마포어에 대해 차단될 수도 있게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-10 오후 10.13.57.png&quot; data-origin-width=&quot;3400&quot; data-origin-height=&quot;1774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5DHg1/btsNhqcgyb6/mffK8gfJFFE1LJtt8yWSpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5DHg1/btsNhqcgyb6/mffK8gfJFFE1LJtt8yWSpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5DHg1/btsNhqcgyb6/mffK8gfJFFE1LJtt8yWSpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5DHg1%2FbtsNhqcgyb6%2FmffK8gfJFFE1LJtt8yWSpK%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;3400&quot; height=&quot;1774&quot; data-filename=&quot;스크린샷 2025-04-10 오후 10.13.57.png&quot; data-origin-width=&quot;3400&quot; data-origin-height=&quot;1774&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swift는 Swift 런타임이 인식할 수 있는 자식 작업이나 Continuation에 대해서만 비동기 대기를 수행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Swift의 액터와 동기화&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;액터 타입은 동시에 하나의 메서드 호출만 실행할 수 있는 상호 배제를 보장하며, 이로 인해 데이터 레이스를 방지할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-10 오후 10.25.08.png&quot; data-origin-width=&quot;3394&quot; data-origin-height=&quot;1020&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pioGV/btsNfSnOSIX/mOm5B0pJu5p53agZnmUvEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pioGV/btsNfSnOSIX/mOm5B0pJu5p53agZnmUvEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pioGV/btsNfSnOSIX/mOm5B0pJu5p53agZnmUvEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpioGV%2FbtsNfSnOSIX%2FmOm5B0pJu5p53agZnmUvEK%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;3394&quot; height=&quot;1020&quot; data-filename=&quot;스크린샷 2025-04-10 오후 10.25.08.png&quot; data-origin-width=&quot;3394&quot; data-origin-height=&quot;1020&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GCD에서의 직렬화 큐의 경우, 큐가 이미 실행중이면 경쟁상태에 있음을 의미하며 호출 스레드는 블록됩니다. 다만 이는 스레드 폭발을 유발하게 됩니다. 이를 위해 dispatch async가 도입되면서 스레드폭발의 원인인 스레드 차단은 방지할 수 있으나, 경합(Contention)이 없을때, Dispatch가 비동기 작업을 수행할 새 스레드를 요청해야 한다는 것입니다. 이는 과도한 컨텍스트 전환으로 이어집니다.&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;u&gt;&lt;b&gt;Non-blocking 방식&lt;/b&gt;&lt;/u&gt;으로 작업을 처리하기에 스레드 폭발을 예방하며, 실행중이 아닌 액터에서 메서드를 호출하면 호출하는 &lt;u&gt;&lt;b&gt;스레드를 재사용&lt;/b&gt;&lt;/u&gt;하며 메서드 호출을 실행할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sEjCF/btsNhmurtfr/v19u3GXgN603MatLqkGOdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sEjCF/btsNhmurtfr/v19u3GXgN603MatLqkGOdK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2550&quot; data-origin-height=&quot;1492&quot; data-filename=&quot;스크린샷 2025-04-10 오후 10.31.21.png&quot; style=&quot;width: 40.7449%; margin-right: 10px;&quot; data-widthpercent=&quot;41.22&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sEjCF/btsNhmurtfr/v19u3GXgN603MatLqkGOdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsEjCF%2FbtsNhmurtfr%2Fv19u3GXgN603MatLqkGOdK%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;2550&quot; height=&quot;1492&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccZejh/btsNf9bOcCv/bqHALO3xoVw4h6G4vBUK9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccZejh/btsNf9bOcCv/bqHALO3xoVw4h6G4vBUK9k/img.png&quot; data-origin-width=&quot;3392&quot; data-origin-height=&quot;1392&quot; data-is-animation=&quot;false&quot; data-filename=&quot;스크린샷 2025-04-10 오후 10.32.35.png&quot; data-widthpercent=&quot;58.78&quot; style=&quot;width: 58.0923%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccZejh/btsNf9bOcCv/bqHALO3xoVw4h6G4vBUK9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccZejh%2FbtsNf9bOcCv%2FbqHALO3xoVw4h6G4vBUK9k%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;3392&quot; height=&quot;1392&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;액터는 협력적 스레드 풀을 활용해 스케줄링을 제공하며, 필요에 따라 호출 스레드를 재사용합니다. 여기서 액터는 다른 액터 간 상호작용할 수 있으며(Actor hopping), 전환 시에도 실행중인 작업을 차단하지 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 도식과 같이 비동기 작업이 많고 경쟁이 치열하면, 시스템은 어떤 작업이 더 중요한지에 따라 트레이드 오프를 해야합니다. 설령, 사용자 인터랙션과 관련된 고우선순위 작업이 백그라운드 작업보다 우선시될 수 있을 것입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;액터의 재진입성(Reentrancy) &lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-11 오후 5.46.45.png&quot; data-origin-width=&quot;3350&quot; data-origin-height=&quot;1548&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pTxNW/btsNhsByVsS/0XL4EomU03Td3qXDKxNSwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pTxNW/btsNhsByVsS/0XL4EomU03Td3qXDKxNSwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pTxNW/btsNhsByVsS/0XL4EomU03Td3qXDKxNSwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpTxNW%2FbtsNhsByVsS%2F0XL4EomU03Td3qXDKxNSwk%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;3350&quot; height=&quot;1548&quot; data-filename=&quot;스크린샷 2025-04-11 오후 5.46.45.png&quot; data-origin-width=&quot;3350&quot; data-origin-height=&quot;1548&quot;/&gt;&lt;/span&gt;&lt;/figure&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;Database actor가 await 지점에 도달하여 일시중단되고, Sports Actor가 해당 스레드에서 작업을 이어 실행하고 있으며, 잠시후, Sports Actor가 Database Actor를 호출하여 기사를 저장한다고 가정합니다. 이때 Database Actor는 상호배제를 유지하고 있기 때문에 경쟁상태가 아니며, 바로 Database Actor 접근이 가능해집니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Database Actor는 이후, 저장작업 수행을 위해 새로운 작업 항목을 생성하게 되는데 이 작업항목은 이전 작업 항목이 일시중단된 동안에도 진행될 수 있습니다. 이러한 개념이 바로 액터의 재진입성을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주의해야할 점은 재진입으로 새로 생성된 작업항목이 이전에 일시중단된 작업보다 먼저 실행을 완료할 수 있다는 점입니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XCyfp/btsNjkQrifj/Q26Qw1hss8PRvmKYE5AMbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XCyfp/btsNjkQrifj/Q26Qw1hss8PRvmKYE5AMbk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;3458&quot; data-origin-height=&quot;1144&quot; data-filename=&quot;스크린샷 2025-04-11 오후 5.55.50.png&quot; style=&quot;width: 48.0597%; margin-right: 10px;&quot; data-widthpercent=&quot;48.63&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XCyfp/btsNjkQrifj/Q26Qw1hss8PRvmKYE5AMbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXCyfp%2FbtsNjkQrifj%2FQ26Qw1hss8PRvmKYE5AMbk%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;3458&quot; height=&quot;1144&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKOiXB/btsNiQh3jkM/tU9nWlDf3ouKGs3FpF2k6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKOiXB/btsNiQh3jkM/tU9nWlDf3ouKGs3FpF2k6K/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;3430&quot; data-origin-height=&quot;1074&quot; data-filename=&quot;스크린샷 2025-04-11 오후 5.56.29.png&quot; style=&quot;width: 50.7775%;&quot; data-widthpercent=&quot;51.37&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKOiXB/btsNiQh3jkM/tU9nWlDf3ouKGs3FpF2k6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKOiXB%2FbtsNiQh3jkM%2FtU9nWlDf3ouKGs3FpF2k6K%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;3430&quot; height=&quot;1074&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좌측의 GCD 직렬큐의 경우 높은 우선순위를 가지는 UI 관련 작업이 낮은 우선순위의 Background 작업 사이에 배치되었기에, UI 반응이 지연되는 상황이 발생할 수 있다는 것을 의미합니다. 하지만, Actor의 경우 런타임이 우선순위가 낮은 항목보다 우선순위가 높은 항목을 큐 앞으로 이동시켜 먼저 실행되게 할 수 있습니다.&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;이는 시스템의 메인스레드에 대한 개념을 추상화하여 다른 특성을 지니고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-11 오후 5.59.30.png&quot; data-origin-width=&quot;2610&quot; data-origin-height=&quot;1514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/S6ejx/btsNiqkdDAM/22l96ibWZ4z1k6mOfHaT81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/S6ejx/btsNiqkdDAM/22l96ibWZ4z1k6mOfHaT81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S6ejx/btsNiqkdDAM/22l96ibWZ4z1k6mOfHaT81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FS6ejx%2FbtsNiqkdDAM%2F22l96ibWZ4z1k6mOfHaT81%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;2610&quot; height=&quot;1514&quot; data-filename=&quot;스크린샷 2025-04-11 오후 5.59.30.png&quot; data-origin-width=&quot;2610&quot; data-origin-height=&quot;1514&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매인 액터는 협력적 스레드 풀의 스레드와 분리되어있기 때문에, 문맥 전환을 필요로 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-11 오후 6.05.04.png&quot; data-origin-width=&quot;3454&quot; data-origin-height=&quot;1506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZfjMC/btsNi0Sf2gt/PO7RROlF6KrOtIHh8sO2s0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZfjMC/btsNi0Sf2gt/PO7RROlF6KrOtIHh8sO2s0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZfjMC/btsNi0Sf2gt/PO7RROlF6KrOtIHh8sO2s0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZfjMC%2FbtsNi0Sf2gt%2FPO7RROlF6KrOtIHh8sO2s0%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;3454&quot; height=&quot;1506&quot; data-filename=&quot;스크린샷 2025-04-11 오후 6.05.04.png&quot; data-origin-width=&quot;3454&quot; data-origin-height=&quot;1506&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 위 코드와 같이 글로벌 스레드에서 기사를 로드한 후, 메인액터에서 이를 update하는 로직을 수행하기 위해 루프의 각 반복은 최소&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-11 오후 6.06.30.png&quot; data-origin-width=&quot;3488&quot; data-origin-height=&quot;1406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d7IOJA/btsNjGThQJK/QotodZ2ekNpRGmK77kKip1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d7IOJA/btsNjGThQJK/QotodZ2ekNpRGmK77kKip1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d7IOJA/btsNjGThQJK/QotodZ2ekNpRGmK77kKip1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd7IOJA%2FbtsNjGThQJK%2FQotodZ2ekNpRGmK77kKip1%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;3488&quot; height=&quot;1406&quot; data-filename=&quot;스크린샷 2025-04-11 오후 6.06.30.png&quot; data-origin-width=&quot;3488&quot; data-origin-height=&quot;1406&quot;/&gt;&lt;/span&gt;&lt;/figure&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;&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;</description>
      <category>개발지식 정리/WWDC 정리</category>
      <author>Neoself</author>
      <guid isPermaLink="true">https://neoself.tistory.com/88</guid>
      <comments>https://neoself.tistory.com/88#entry88comment</comments>
      <pubDate>Fri, 11 Apr 2025 18:07:44 +0900</pubDate>
    </item>
  </channel>
</rss>