<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>NamS의 iOS일기</title>
    <link>https://nsios.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 10 Apr 2026 22:38:18 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>냄수</managingEditor>
    <image>
      <title>NamS의 iOS일기</title>
      <url>https://tistory1.daumcdn.net/tistory/3197485/attach/8628db088d9448cd8a4e15cd5e46e408</url>
      <link>https://nsios.tistory.com</link>
    </image>
    <item>
      <title>[iOS] AI 기본 개념부터 온디바이스까지 정리해보기</title>
      <link>https://nsios.tistory.com/256</link>
      <description>&lt;p data-end=&quot;901&quot; data-start=&quot;867&quot; data-ke-size=&quot;size16&quot;&gt;AI를 공부하려고 하면 생각보다 용어가 한번에 많이 나옵니다.&lt;/p&gt;
&lt;p data-end=&quot;969&quot; data-start=&quot;903&quot; data-ke-size=&quot;size16&quot;&gt;머신러닝, 딥러닝, 모델, 추론, Transformer, 양자화, Core ML, Foundation Models...&lt;/p&gt;
&lt;p data-end=&quot;1033&quot; data-start=&quot;971&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1110&quot; data-start=&quot;1035&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서는 기본개념을 알아보고 iOS에는 어떻게 연결되고 &lt;br /&gt;앞으로는 어떻게 쓰면좋을지 고민과정을 작성해보려합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1323&quot; data-start=&quot;1217&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1227&quot; data-start=&quot;1217&quot; data-section-id=&quot;2yfql1&quot;&gt;AI 핵심 개념과 키워드&lt;/li&gt;
&lt;li data-end=&quot;1243&quot; data-start=&quot;1228&quot; data-section-id=&quot;suv3hu&quot;&gt;모델은 어떻게 사용하는가&lt;/li&gt;
&lt;li data-end=&quot;1243&quot; data-start=&quot;1228&quot; data-section-id=&quot;suv3hu&quot;&gt;iOS에서는 어떻게 연결되는가&lt;/li&gt;
&lt;li data-end=&quot;1243&quot; data-start=&quot;1228&quot; data-section-id=&quot;suv3hu&quot;&gt;Python&amp;nbsp;방식과&amp;nbsp;Create&amp;nbsp;ML&amp;nbsp;방식&lt;/li&gt;
&lt;li data-end=&quot;1284&quot; data-start=&quot;1263&quot; data-section-id=&quot;1dhif74&quot;&gt;성능최적화&lt;/li&gt;
&lt;li data-end=&quot;1307&quot; data-start=&quot;1285&quot; data-section-id=&quot;h3k0uv&quot;&gt;나는 지금 AI를 어떻게 쓰고 있는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(아래부터 글은 제가 정리한 자료조사를 기반으로 AI가 작성해준 글입니다.... )&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;1341&quot; data-start=&quot;1330&quot; data-section-id=&quot;1na2k60&quot; data-ke-size=&quot;size26&quot;&gt;AI 핵심 개념&lt;/h2&gt;
&lt;h3 data-end=&quot;1370&quot; data-start=&quot;1343&quot; data-section-id=&quot;3ay3pb&quot; data-ke-size=&quot;size23&quot;&gt;머신러닝 (Machine Learning)&lt;/h3&gt;
&lt;p data-end=&quot;1405&quot; data-start=&quot;1371&quot; data-ke-size=&quot;size16&quot;&gt;데이터를 보고 패턴을 학습해서 예측하거나 분류하는 방법입니다.&lt;/p&gt;
&lt;p data-end=&quot;1466&quot; data-start=&quot;1407&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;br /&gt;메일이 스팸인지 분류하거나&lt;br /&gt;사용자에게 상품을 추천하는 문제를 풀 때 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;1530&quot; data-start=&quot;1468&quot; data-ke-size=&quot;size16&quot;&gt;보통 전통적인 머신러닝에서는&lt;br /&gt;사람이 어떤 특징(feature)을 넣을지 설계하는 비중이 상대적으로 컸습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;1555&quot; data-start=&quot;1532&quot; data-section-id=&quot;adz8lv&quot; data-ke-size=&quot;size23&quot;&gt;딥러닝 (Deep Learning)&lt;/h3&gt;
&lt;p data-end=&quot;1575&quot; data-start=&quot;1556&quot; data-ke-size=&quot;size16&quot;&gt;딥러닝은 머신러닝의 한 종류입니다.&lt;/p&gt;
&lt;p data-end=&quot;1631&quot; data-start=&quot;1577&quot; data-ke-size=&quot;size16&quot;&gt;대량의 데이터와 많은 연산을 이용해서&lt;br /&gt;모델이 복잡한 패턴을 스스로 학습하도록 하는 방식입니다.&lt;/p&gt;
&lt;p data-end=&quot;1690&quot; data-start=&quot;1633&quot; data-ke-size=&quot;size16&quot;&gt;이미지 인식, 음성 인식, 번역, LLM 같은 생성형 AI도&lt;br /&gt;대부분 딥러닝 기반이라고 보면 됩니다.&lt;/p&gt;
&lt;h3 data-end=&quot;1706&quot; data-start=&quot;1692&quot; data-section-id=&quot;5gbumw&quot; data-ke-size=&quot;size23&quot;&gt;모델 (Model)&lt;/h3&gt;
&lt;p data-end=&quot;1725&quot; data-start=&quot;1707&quot; data-ke-size=&quot;size16&quot;&gt;모델은 학습이 끝난 결과물입니다.&lt;/p&gt;
&lt;p data-end=&quot;1762&quot; data-start=&quot;1727&quot; data-ke-size=&quot;size16&quot;&gt;조금 단순하게 말하면&lt;br /&gt;AI의 &amp;ldquo;뇌&amp;rdquo;처럼 생각할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;1821&quot; data-start=&quot;1764&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 사진을 넣었을 때&lt;br /&gt;&amp;ldquo;이건 고양이일 확률이 90%다&amp;rdquo;&lt;br /&gt;같은 결과를 내놓는게 모델입니다.&lt;/p&gt;
&lt;h3 data-end=&quot;1859&quot; data-start=&quot;1823&quot; data-section-id=&quot;1t15r0k&quot; data-ke-size=&quot;size23&quot;&gt;학습 vs 추론 (Training vs Inference)&lt;/h3&gt;
&lt;h4 data-end=&quot;1868&quot; data-start=&quot;1861&quot; data-ke-size=&quot;size20&quot;&gt;학습&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1933&quot; data-start=&quot;1869&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1897&quot; data-start=&quot;1869&quot; data-section-id=&quot;uu9ly9&quot;&gt;데이터를 이용해서 모델 파라미터를 조정하는 과정&lt;/li&gt;
&lt;li data-end=&quot;1917&quot; data-start=&quot;1898&quot; data-section-id=&quot;11b4fvx&quot;&gt;시간이 오래 걸리고 연산량이 큼&lt;/li&gt;
&lt;li data-end=&quot;1933&quot; data-start=&quot;1918&quot; data-section-id=&quot;19470xp&quot;&gt;쉽게 말하면 배우는 단계&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;1942&quot; data-start=&quot;1935&quot; data-ke-size=&quot;size20&quot;&gt;추론&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2023&quot; data-start=&quot;1943&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1977&quot; data-start=&quot;1943&quot; data-section-id=&quot;11chxuk&quot;&gt;학습이 끝난 모델로 실제 입력에 대한 결과를 반환하는 과정&lt;/li&gt;
&lt;li data-end=&quot;2000&quot; data-start=&quot;1978&quot; data-section-id=&quot;1dzo3a9&quot;&gt;서비스에서 우리가 주로 사용하는 단계&lt;/li&gt;
&lt;li data-end=&quot;2023&quot; data-start=&quot;2001&quot; data-section-id=&quot;1u0622n&quot;&gt;예: 사용자의 음성을 텍스트로 바꾸기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2069&quot; data-start=&quot;2025&quot; data-ke-size=&quot;size16&quot;&gt;iOS 개발자 입장에서는 대부분 &lt;b&gt;학습보다 추론&lt;/b&gt;을 더 많이 다루게 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;2138&quot; data-start=&quot;2071&quot; data-ke-size=&quot;size16&quot;&gt;앱에서는 이미 만들어진 모델을 넣고&lt;br /&gt;그 모델이 실제 입력에 대해 어떤 결과를 내는지 사용하는 경우가 많기 때문입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;2157&quot; data-start=&quot;2145&quot; data-section-id=&quot;yxgmfp&quot; data-ke-size=&quot;size26&quot;&gt;관련 키워드 개념&lt;/h2&gt;
&lt;h3 data-end=&quot;2183&quot; data-start=&quot;2159&quot; data-section-id=&quot;ltmzfc&quot; data-ke-size=&quot;size23&quot;&gt;Neural Network (신경망)&lt;/h3&gt;
&lt;p data-end=&quot;2210&quot; data-start=&quot;2184&quot; data-ke-size=&quot;size16&quot;&gt;사람 뇌의 뉴런 연결 방식을 흉내낸 구조입니다.&lt;/p&gt;
&lt;p data-end=&quot;2232&quot; data-start=&quot;2212&quot; data-ke-size=&quot;size16&quot;&gt;딥러닝의 기본 뼈대라고 보면 됩니다.&lt;/p&gt;
&lt;h3 data-end=&quot;2261&quot; data-start=&quot;2234&quot; data-section-id=&quot;1ddg5d8&quot; data-ke-size=&quot;size23&quot;&gt;CNN / RNN / Transformer&lt;/h3&gt;
&lt;h4 data-end=&quot;2271&quot; data-start=&quot;2263&quot; data-ke-size=&quot;size20&quot;&gt;CNN&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2330&quot; data-start=&quot;2272&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2287&quot; data-start=&quot;2272&quot; data-section-id=&quot;it14c5&quot;&gt;이미지 처리에 강한 구조&lt;/li&gt;
&lt;li data-end=&quot;2307&quot; data-start=&quot;2288&quot; data-section-id=&quot;pef6y0&quot;&gt;사진의 패턴이나 형태를 잘 잡음&lt;/li&gt;
&lt;li data-end=&quot;2330&quot; data-start=&quot;2308&quot; data-section-id=&quot;1to6t4y&quot;&gt;이미지 분류, 객체 탐지에 많이 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;2340&quot; data-start=&quot;2332&quot; data-ke-size=&quot;size20&quot;&gt;RNN&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2426&quot; data-start=&quot;2341&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2365&quot; data-start=&quot;2341&quot; data-section-id=&quot;119epq9&quot;&gt;순서가 중요한 데이터를 다루기 위한 구조&lt;/li&gt;
&lt;li data-end=&quot;2397&quot; data-start=&quot;2366&quot; data-section-id=&quot;718o29&quot;&gt;이전 정보를 어느 정도 기억하면서 다음 데이터를 처리&lt;/li&gt;
&lt;li data-end=&quot;2426&quot; data-start=&quot;2398&quot; data-section-id=&quot;1w4sjmz&quot;&gt;문장, 음성 같은 순차 데이터에서 많이 다뤄졌음&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;2444&quot; data-start=&quot;2428&quot; data-ke-size=&quot;size20&quot;&gt;Transformer&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2529&quot; data-start=&quot;2445&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2471&quot; data-start=&quot;2445&quot; data-section-id=&quot;1yosrbg&quot;&gt;현재 자연어 처리와 생성형 AI의 핵심 구조&lt;/li&gt;
&lt;li data-end=&quot;2500&quot; data-start=&quot;2472&quot; data-section-id=&quot;14u5tpd&quot;&gt;문장 전체 관계를 잘 파악하고 병렬 처리에 유리&lt;/li&gt;
&lt;li data-end=&quot;2529&quot; data-start=&quot;2501&quot; data-section-id=&quot;pv1brd&quot;&gt;번역, 요약, 질의응답, LLM 등에 널리 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;2559&quot; data-start=&quot;2531&quot; data-section-id=&quot;1vcd0gr&quot; data-ke-size=&quot;size23&quot;&gt;Input / Output / Feature&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2630&quot; data-start=&quot;2560&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2582&quot; data-start=&quot;2560&quot; data-section-id=&quot;10sk03r&quot;&gt;input: 모델에 넣는 입력 데이터&lt;/li&gt;
&lt;li data-end=&quot;2603&quot; data-start=&quot;2583&quot; data-section-id=&quot;145tbq&quot;&gt;output: 모델이 내놓는 결과&lt;/li&gt;
&lt;li data-end=&quot;2630&quot; data-start=&quot;2604&quot; data-section-id=&quot;ce5wku&quot;&gt;feature: 데이터를 설명하는 특징 정보&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2750&quot; data-start=&quot;2632&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;br /&gt;집 사진을 넣어서 인테리어 스타일을 분류한다면&lt;br /&gt;사진은 input,&lt;br /&gt;&amp;ldquo;모던 스타일&amp;rdquo; 같은 결과는 output,&lt;br /&gt;사진 안의 패턴이나 색감, 구조 같은 정보는 feature로 볼 수 있습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;2775&quot; data-start=&quot;2752&quot; data-section-id=&quot;u0rror&quot; data-ke-size=&quot;size23&quot;&gt;Loss / Optimization&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2853&quot; data-start=&quot;2776&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2807&quot; data-start=&quot;2776&quot; data-section-id=&quot;181pxoq&quot;&gt;loss: 모델 예측이 얼마나 틀렸는지를 나타내는 값&lt;/li&gt;
&lt;li data-end=&quot;2853&quot; data-start=&quot;2808&quot; data-section-id=&quot;10fsmbt&quot;&gt;optimization: 그 loss를 줄이도록 모델 파라미터를 조정하는 과정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2894&quot; data-start=&quot;2855&quot; data-ke-size=&quot;size16&quot;&gt;학습은 결국&lt;br /&gt;&amp;ldquo;틀린 정도를 줄여가는 과정&amp;rdquo;이라고 이해하면 편합니다.&lt;/p&gt;
&lt;h3 data-end=&quot;2928&quot; data-start=&quot;2896&quot; data-section-id=&quot;1fvdn74&quot; data-ke-size=&quot;size23&quot;&gt;Overfitting / Generalization&lt;/h3&gt;
&lt;h4 data-end=&quot;2946&quot; data-start=&quot;2930&quot; data-ke-size=&quot;size20&quot;&gt;Overfitting&lt;/h4&gt;
&lt;p data-end=&quot;2997&quot; data-start=&quot;2947&quot; data-ke-size=&quot;size16&quot;&gt;학습 데이터는 지나치게 잘 맞추는데&lt;br /&gt;처음 보는 데이터에서는 성능이 떨어지는 상태입니다.&lt;/p&gt;
&lt;p data-end=&quot;3037&quot; data-start=&quot;2999&quot; data-ke-size=&quot;size16&quot;&gt;문제를 이해한게 아니라&lt;br /&gt;학습 데이터를 외워버린 상태에 가깝습니다.&lt;/p&gt;
&lt;h4 data-end=&quot;3058&quot; data-start=&quot;3039&quot; data-ke-size=&quot;size20&quot;&gt;Generalization&lt;/h4&gt;
&lt;p data-end=&quot;3084&quot; data-start=&quot;3059&quot; data-ke-size=&quot;size16&quot;&gt;처음 보는 데이터에도 잘 동작하는 능력입니다.&lt;/p&gt;
&lt;p data-end=&quot;3131&quot; data-start=&quot;3086&quot; data-ke-size=&quot;size16&quot;&gt;좋은 모델은 결국&lt;br /&gt;훈련셋이 아니라 실제 사용자 입력에서도 잘 동작해야 합니다.&lt;/p&gt;
&lt;h3 data-end=&quot;3152&quot; data-start=&quot;3133&quot; data-section-id=&quot;1afmcv1&quot; data-ke-size=&quot;size23&quot;&gt;Dataset / Label&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3207&quot; data-start=&quot;3153&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3184&quot; data-start=&quot;3153&quot; data-section-id=&quot;s9609s&quot;&gt;dataset: 학습이나 평가에 사용하는 데이터 모음&lt;/li&gt;
&lt;li data-end=&quot;3207&quot; data-start=&quot;3185&quot; data-section-id=&quot;qk070c&quot;&gt;label: 데이터에 붙는 정답 정보&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3270&quot; data-start=&quot;3209&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 고양이 사진 1만장이 dataset이고,&lt;br /&gt;각 사진에 붙은 &amp;ldquo;고양이&amp;rdquo;라는 정답이 label입니다.&lt;/p&gt;
&lt;h3 data-end=&quot;3282&quot; data-start=&quot;3272&quot; data-section-id=&quot;qx6u3b&quot; data-ke-size=&quot;size23&quot;&gt;Tensor&lt;/h3&gt;
&lt;p data-end=&quot;3321&quot; data-start=&quot;3283&quot; data-ke-size=&quot;size16&quot;&gt;머신러닝과 딥러닝에서 데이터를 담는 기본적인 다차원 배열 단위입니다.&lt;/p&gt;
&lt;p data-end=&quot;3395&quot; data-start=&quot;3323&quot; data-ke-size=&quot;size16&quot;&gt;이미지, 음성, 텍스트도&lt;br /&gt;결국은 연산 가능한 숫자 구조로 바뀌어서 다뤄지는데&lt;br /&gt;그때 자주 등장하는 표현이 tensor입니다.&lt;/p&gt;
&lt;p data-end=&quot;3395&quot; data-start=&quot;3323&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;3418&quot; data-start=&quot;3402&quot; data-section-id=&quot;1i08ov3&quot; data-ke-size=&quot;size26&quot;&gt;모델은 어떻게 사용하는가&lt;/h2&gt;
&lt;p data-end=&quot;3477&quot; data-start=&quot;3420&quot; data-ke-size=&quot;size16&quot;&gt;앱 개발에서는 보통 모델을 직접 학습시키기보다&lt;br /&gt;이미 학습된 모델을 가져와 사용하는 경우가 많습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;3496&quot; data-start=&quot;3479&quot; data-section-id=&quot;1tevarb&quot; data-ke-size=&quot;size23&quot;&gt;Core ML 모델 포맷&lt;/h3&gt;
&lt;p data-end=&quot;3564&quot; data-start=&quot;3497&quot; data-ke-size=&quot;size16&quot;&gt;애플 생태계에서는 .mlmodel, .mlpackage 형태의 모델을&lt;br /&gt;Xcode에 넣어서 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;3624&quot; data-start=&quot;3566&quot; data-ke-size=&quot;size16&quot;&gt;이미지 분류, 텍스트 처리, 음성, 예측 등&lt;br /&gt;여러 작업을 Core ML 기반으로 연결할 수 있습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;3635&quot; data-start=&quot;3626&quot; data-section-id=&quot;5mq29a&quot; data-ke-size=&quot;size23&quot;&gt;모델 변환&lt;/h3&gt;
&lt;p data-end=&quot;3723&quot; data-start=&quot;3636&quot; data-ke-size=&quot;size16&quot;&gt;실제로는 Python 환경에서 모델을 학습한 뒤&lt;br /&gt;coremltools 같은 도구를 사용해서&lt;br /&gt;Core ML 형식으로 변환하는 흐름이 자주 나옵니다.&lt;/p&gt;
&lt;p data-end=&quot;3727&quot; data-start=&quot;3725&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;p data-end=&quot;3778&quot; data-start=&quot;3729&quot; data-ke-size=&quot;size16&quot;&gt;Python에서 학습&lt;br /&gt;-&amp;gt; 모델 변환&lt;br /&gt;-&amp;gt; Xcode에 포함&lt;br /&gt;-&amp;gt; 앱에서 추론&lt;/p&gt;
&lt;p data-end=&quot;3788&quot; data-start=&quot;3780&quot; data-ke-size=&quot;size16&quot;&gt;이런 식입니다.&lt;/p&gt;
&lt;h3 data-end=&quot;3798&quot; data-start=&quot;3790&quot; data-section-id=&quot;yng435&quot; data-ke-size=&quot;size23&quot;&gt;ONNX&lt;/h3&gt;
&lt;p data-end=&quot;3840&quot; data-start=&quot;3799&quot; data-ke-size=&quot;size16&quot;&gt;ONNX는 모델을 다른 환경으로 옮기기 쉽게 하려는 중간 표준 형식입니다.&lt;/p&gt;
&lt;p data-end=&quot;3912&quot; data-start=&quot;3842&quot; data-ke-size=&quot;size16&quot;&gt;모델 학습 프레임워크와 실행 환경이 다를 수 있기 때문에&lt;br /&gt;중간 포맷을 거쳐 Core ML 쪽으로 가져오는 경우도 있습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;3933&quot; data-start=&quot;3914&quot; data-section-id=&quot;12eujap&quot; data-ke-size=&quot;size23&quot;&gt;TensorFlow Lite&lt;/h3&gt;
&lt;p data-end=&quot;4012&quot; data-start=&quot;3934&quot; data-ke-size=&quot;size16&quot;&gt;크로스플랫폼이나 이미 TFLite 모델을 쓰고 있는 환경이라면&lt;br /&gt;TensorFlow Lite 기반으로 앱에 넣어 사용하는 방법도 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;3395&quot; data-start=&quot;3323&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;4038&quot; data-start=&quot;4019&quot; data-section-id=&quot;1rqr2zr&quot; data-ke-size=&quot;size26&quot;&gt;iOS에서는 어떻게 연결되는가&lt;/h2&gt;
&lt;p data-end=&quot;4144&quot; data-start=&quot;4069&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;4160&quot; data-start=&quot;4146&quot; data-section-id=&quot;1sk6zhz&quot; data-ke-size=&quot;size23&quot;&gt;Core ML이란?&lt;/h3&gt;
&lt;p data-end=&quot;4208&quot; data-start=&quot;4161&quot; data-ke-size=&quot;size16&quot;&gt;Core ML은&lt;br /&gt;앱이 모델을 사용할 수 있도록 해주는 상위 ML 프레임워크입니다.&lt;/p&gt;
&lt;p data-end=&quot;4251&quot; data-start=&quot;4210&quot; data-ke-size=&quot;size16&quot;&gt;입출력 관리, 모델 로드, 컴파일, 실행 경로 선택 같은 부분을 맡습니다.&lt;/p&gt;
&lt;p data-end=&quot;4321&quot; data-start=&quot;4253&quot; data-ke-size=&quot;size16&quot;&gt;즉&lt;br /&gt;모델 자체를 만드는 도구라기보다&lt;br /&gt;&lt;b&gt;앱에서 모델을 안정적으로 실행하게 해주는 런타임 인터페이스&lt;/b&gt;에 가깝습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;4344&quot; data-start=&quot;4323&quot; data-section-id=&quot;1b1f8c1&quot; data-ke-size=&quot;size23&quot;&gt;Foundation Models&lt;/h3&gt;
&lt;p data-end=&quot;4417&quot; data-start=&quot;4345&quot; data-ke-size=&quot;size16&quot;&gt;최근 애플은 Foundation Models 프레임워크를 통해&lt;br /&gt;온디바이스 언어 모델에 접근하는 인터페이스를 제공하고 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;4588&quot; data-start=&quot;4419&quot; data-ke-size=&quot;size16&quot;&gt;이 프레임워크는 Apple Intelligence를 지원하는 기기와 설정 여부에 따라 사용 가능 여부가 달라지고,&lt;br /&gt;세션 기반으로 모델을 호출하며, 생성 옵션이나 구조화된 응답 형태를 다룰 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;4823&quot; data-start=&quot;4590&quot; data-ke-size=&quot;size16&quot;&gt;개발자 입장에서는&lt;br /&gt;기존처럼 직접 모델 파일을 넣어 쓰는 방식과는 조금 다르게,&lt;br /&gt;&lt;b&gt;애플이 제공하는 시스템 모델 능력을 앱 기능으로 연결하는 방식&lt;/b&gt;으로 볼 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-end=&quot;4844&quot; data-start=&quot;4825&quot; data-section-id=&quot;q6ww3s&quot; data-ke-size=&quot;size23&quot;&gt;CPU / GPU / ANE&lt;/h3&gt;
&lt;p data-end=&quot;4925&quot; data-start=&quot;4845&quot; data-ke-size=&quot;size16&quot;&gt;CoreML은 조건에따라 적절한 장치를 사용합니다&lt;/p&gt;
&lt;p data-end=&quot;4934&quot; data-start=&quot;4927&quot; data-ke-size=&quot;size16&quot;&gt;간단히 보면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;5000&quot; data-start=&quot;4935&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4959&quot; data-start=&quot;4935&quot; data-section-id=&quot;1jolxn4&quot;&gt;CPU: 제어 흐름, 범용 처리, 호환성&amp;nbsp;&lt;/li&gt;
&lt;li data-end=&quot;4976&quot; data-start=&quot;4960&quot; data-section-id=&quot;1l8vu4x&quot;&gt;GPU: 병렬 연산에 유리&lt;/li&gt;
&lt;li data-end=&quot;5000&quot; data-start=&quot;4977&quot; data-section-id=&quot;1o3svk8&quot;&gt;ANE: AI 추론에 특화된 연산 가속&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;5044&quot; data-start=&quot;5002&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;5055&quot; data-start=&quot;5046&quot; data-section-id=&quot;ho1nsy&quot; data-ke-size=&quot;size23&quot;&gt;전체 흐름&lt;/h3&gt;
&lt;p data-end=&quot;5074&quot; data-start=&quot;5056&quot; data-ke-size=&quot;size16&quot;&gt;대략적인 구조는 이런 느낌입니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;div&gt;&lt;span&gt;Your App&lt;/span&gt;&lt;br /&gt;&lt;span&gt;-&amp;gt; Vision / Natural Language / Speech / SoundAnalysis&lt;/span&gt;&lt;br /&gt;&lt;span&gt;-&amp;gt; Core ML&lt;/span&gt;&lt;br /&gt;&lt;span&gt; -&amp;gt; .mlmodel / .mlpackage&lt;/span&gt;&lt;br /&gt;&lt;span&gt; -&amp;gt; MLModel runtime&lt;/span&gt;&lt;br /&gt;&lt;span&gt; -&amp;gt; Input / Output abstraction&lt;/span&gt;&lt;br /&gt;&lt;span&gt; -&amp;gt; Compute planning&lt;/span&gt;&lt;br /&gt;&lt;span&gt; -&amp;gt; CPU / GPU / Neural Engine execution&lt;/span&gt;&lt;br /&gt;&lt;span&gt;-&amp;gt; Low-level foundations&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&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;p data-end=&quot;5437&quot; data-start=&quot;5336&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;5470&quot; data-start=&quot;5444&quot; data-section-id=&quot;3wigqw&quot; data-ke-size=&quot;size26&quot;&gt;Python 방식과 Create ML 방식&lt;/h2&gt;
&lt;h3 data-end=&quot;5488&quot; data-start=&quot;5472&quot; data-section-id=&quot;1tcf64b&quot; data-ke-size=&quot;size23&quot;&gt;Python 중심 방식&lt;/h3&gt;
&lt;p data-end=&quot;5530&quot; data-start=&quot;5489&quot; data-ke-size=&quot;size16&quot;&gt;모델 설계부터 학습, 평가, 변환, 배포까지 전부 직접 다루는 흐름입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;5589&quot; data-start=&quot;5532&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;5539&quot; data-start=&quot;5532&quot; data-section-id=&quot;nygn7l&quot;&gt;문제 정의&lt;/li&gt;
&lt;li data-end=&quot;5551&quot; data-start=&quot;5540&quot; data-section-id=&quot;1d85o0z&quot;&gt;데이터 수집/정리&lt;/li&gt;
&lt;li data-end=&quot;5559&quot; data-start=&quot;5552&quot; data-section-id=&quot;cy8ko5&quot;&gt;모델 선택&lt;/li&gt;
&lt;li data-end=&quot;5567&quot; data-start=&quot;5560&quot; data-section-id=&quot;1olu9px&quot;&gt;학습 설정&lt;/li&gt;
&lt;li data-end=&quot;5572&quot; data-start=&quot;5568&quot; data-section-id=&quot;yih75t&quot;&gt;평가&lt;/li&gt;
&lt;li data-end=&quot;5580&quot; data-start=&quot;5573&quot; data-section-id=&quot;1grqom2&quot;&gt;저장/변환&lt;/li&gt;
&lt;li data-end=&quot;5589&quot; data-start=&quot;5581&quot; data-section-id=&quot;1jaevzx&quot;&gt;배포 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;5612&quot; data-start=&quot;5591&quot; data-ke-size=&quot;size16&quot;&gt;자유도는 높지만&lt;br /&gt;그만큼 복잡합니다.&lt;/p&gt;
&lt;h3 data-end=&quot;5627&quot; data-start=&quot;5614&quot; data-section-id=&quot;y46ndv&quot; data-ke-size=&quot;size23&quot;&gt;Create ML&lt;/h3&gt;
&lt;p data-end=&quot;5658&quot; data-start=&quot;5628&quot; data-ke-size=&quot;size16&quot;&gt;Create ML은 많은 부분을 자동화해주는 편입니다.&lt;/p&gt;
&lt;p data-end=&quot;5700&quot; data-start=&quot;5660&quot; data-ke-size=&quot;size16&quot;&gt;복잡한 연구보다&lt;br /&gt;기능 구현이 목적일 때 빠르게 시도해보기에 좋습니다.&lt;/p&gt;
&lt;p data-end=&quot;5704&quot; data-start=&quot;5702&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;5744&quot; data-start=&quot;5705&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;5726&quot; data-start=&quot;5705&quot; data-section-id=&quot;18aczt4&quot;&gt;내가 AI 연구자가 되려는건 아니고&lt;/li&gt;
&lt;li data-end=&quot;5744&quot; data-start=&quot;5727&quot; data-section-id=&quot;1yfizp&quot;&gt;특정 기능을 앱에 넣고 싶다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;5781&quot; data-start=&quot;5746&quot; data-ke-size=&quot;size16&quot;&gt;이런 상황에서는 Create ML이 훨씬 실용적일 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;5805&quot; data-start=&quot;5788&quot; data-section-id=&quot;m38jr1&quot; data-ke-size=&quot;size26&quot;&gt;성능 최적화&lt;/h2&gt;
&lt;p data-end=&quot;5824&quot; data-start=&quot;5807&quot; data-ke-size=&quot;size16&quot;&gt;온디바이스 AI는 매력적입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;5888&quot; data-start=&quot;5826&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;5835&quot; data-start=&quot;5826&quot; data-section-id=&quot;6ibhos&quot;&gt;응답이 빠르고&lt;/li&gt;
&lt;li data-end=&quot;5856&quot; data-start=&quot;5836&quot; data-section-id=&quot;5cpm1m&quot;&gt;네트워크가 없어도 동작할 수 있고&lt;/li&gt;
&lt;li data-end=&quot;5888&quot; data-start=&quot;5857&quot; data-section-id=&quot;fzpj1l&quot;&gt;개인정보를 서버로 보내지 않아도 되는 경우가 많습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;5944&quot; data-start=&quot;5890&quot; data-ke-size=&quot;size16&quot;&gt;하지만 그만큼&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;모델 크기, 메모리, 발열, 배터리, 앱 용량 같은 현실적인 문제&lt;/span&gt;가 따라옵니다.&lt;/p&gt;
&lt;p data-end=&quot;5978&quot; data-start=&quot;5946&quot; data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;온디바이스 AI는 결국 최적화가 핵심&lt;/b&gt;입니다.&lt;/p&gt;
&lt;h3 data-end=&quot;125&quot; data-start=&quot;103&quot; data-section-id=&quot;192now3&quot; data-ke-size=&quot;size23&quot;&gt;Quantization (양자화)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;651&quot; data-start=&quot;126&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;286&quot; data-start=&quot;126&quot; data-section-id=&quot;gum112&quot;&gt;개념
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;286&quot; data-start=&quot;133&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;161&quot; data-start=&quot;133&quot; data-section-id=&quot;y8goje&quot;&gt;모델 숫자표현을 더 작은 정밀도로 변경하는 방법&lt;/li&gt;
&lt;li data-end=&quot;203&quot; data-start=&quot;163&quot; data-section-id=&quot;1d8r6dq&quot;&gt;float32 -&amp;gt; int8, float16처럼 더 작은형식으로 변경&lt;/li&gt;
&lt;li data-end=&quot;237&quot; data-start=&quot;205&quot; data-section-id=&quot;19yiq0j&quot;&gt;목적: 모델크기감소, 메모리 사용량감소, 추론속도 향상&lt;/li&gt;
&lt;li data-end=&quot;286&quot; data-start=&quot;239&quot; data-section-id=&quot;1v2hsb9&quot;&gt;온디바이스에서는 제한된 자원 안에서 모델을 실행해야하므로 매우 중요한 최적화 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;472&quot; data-start=&quot;287&quot; data-section-id=&quot;gywgzw&quot;&gt;원리
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;472&quot; data-start=&quot;294&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;328&quot; data-start=&quot;294&quot; data-section-id=&quot;ma54ht&quot;&gt;모델 가중치와 활성값은 정밀한 실수로 저장되는 경우가 많음&lt;/li&gt;
&lt;li data-end=&quot;365&quot; data-start=&quot;330&quot; data-section-id=&quot;cu2hp0&quot;&gt;하지만 실제 추론에서는 항상 높은 정밀도가 필요한 것은 아님&lt;/li&gt;
&lt;li data-end=&quot;424&quot; data-start=&quot;367&quot; data-section-id=&quot;1st3179&quot;&gt;따라서 더 적은 비트로 표현해도 성능 손실은 작게 유지하면서 계산량과 메모리 사용량을 줄일 수 있음&lt;/li&gt;
&lt;li data-end=&quot;472&quot; data-start=&quot;426&quot; data-section-id=&quot;qxy9h4&quot;&gt;저장해야 하는 데이터 크기가 줄어들기 때문에 앱 용량과 로딩 비용에도 도움이 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;651&quot; data-start=&quot;473&quot; data-section-id=&quot;pg2gto&quot;&gt;활용
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;651&quot; data-start=&quot;479&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;518&quot; data-start=&quot;479&quot; data-section-id=&quot;qiiw5d&quot;&gt;스마트폰, 임베디드 기기 등 자원이 제한된 환경에서 매우 자주 사용&lt;/li&gt;
&lt;li data-end=&quot;563&quot; data-start=&quot;520&quot; data-section-id=&quot;18359gl&quot;&gt;transformer, cnn처럼 큰 모델을 기기에서 실행할 때 특히 중요&lt;/li&gt;
&lt;li data-end=&quot;602&quot; data-start=&quot;565&quot; data-section-id=&quot;11x7pyx&quot;&gt;추론 속도 개선, 전력 소모 감소, 메모리 절약 측면에서 효과적&lt;/li&gt;
&lt;li data-end=&quot;651&quot; data-start=&quot;604&quot; data-section-id=&quot;12j42ek&quot;&gt;다만 양자화를 너무 강하게하면 정확도 저하가 발생할 수 있어서 적절한 균형이 중요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;671&quot; data-start=&quot;653&quot; data-section-id=&quot;377bjq&quot; data-ke-size=&quot;size23&quot;&gt;Pruning (가지치기)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1152&quot; data-start=&quot;672&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;805&quot; data-start=&quot;672&quot; data-section-id=&quot;15kpcsm&quot;&gt;개념
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;805&quot; data-start=&quot;679&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;720&quot; data-start=&quot;679&quot; data-section-id=&quot;ug3yum&quot;&gt;모델 안에서 중요도가 낮은 연결, 가중치, 레이어 일부를 제거하는 방법&lt;/li&gt;
&lt;li data-end=&quot;757&quot; data-start=&quot;722&quot; data-section-id=&quot;1v911pp&quot;&gt;덜 중요한 부분을 잘라내서 모델을 더 가볍고 효율적으로 만듬&lt;/li&gt;
&lt;li data-end=&quot;805&quot; data-start=&quot;759&quot; data-section-id=&quot;ks5wcn&quot;&gt;불필요한 계산을 줄여서 온디바이스 환경에 맞게 단순화하는 작업이라고 볼 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;988&quot; data-start=&quot;806&quot; data-section-id=&quot;f40dze&quot;&gt;원리
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;988&quot; data-start=&quot;812&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;859&quot; data-start=&quot;812&quot; data-section-id=&quot;186021u&quot;&gt;실제로 학습이 끝난 모델에는 결과에 큰 영향을 주지 않는 가중치가 존재할 수 있음&lt;/li&gt;
&lt;li data-end=&quot;899&quot; data-start=&quot;861&quot; data-section-id=&quot;mplhlb&quot;&gt;이런 요소를 제거해도 전체 성능이 크게 떨어지지 않는 경우가 있음&lt;/li&gt;
&lt;li data-end=&quot;945&quot; data-start=&quot;901&quot; data-section-id=&quot;1bdiqem&quot;&gt;pruning 이후 필요하면 다시 fine-tuning을 해서 성능을 회복함&lt;/li&gt;
&lt;li data-end=&quot;988&quot; data-start=&quot;947&quot; data-section-id=&quot;1vnhdj1&quot;&gt;즉, 모델이 가진 여유분을 줄여 더 효율적인 구조로 다시 정리하는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1152&quot; data-start=&quot;989&quot; data-section-id=&quot;17j444i&quot;&gt;활용
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1152&quot; data-start=&quot;995&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1035&quot; data-start=&quot;995&quot; data-section-id=&quot;18jetgp&quot;&gt;구조적으로 잘라낸 pruning은 실제 기기 속도개선에 더 도움되는편&lt;/li&gt;
&lt;li data-end=&quot;1064&quot; data-start=&quot;1037&quot; data-section-id=&quot;1mi6nzg&quot;&gt;모델 크기축소, 계산량감소, 메모리절약에 사용&lt;/li&gt;
&lt;li data-end=&quot;1103&quot; data-start=&quot;1066&quot; data-section-id=&quot;1we39z8&quot;&gt;온디바이스에서는 발열과 배터리 부담을 줄이는 데에도 의미가 있음&lt;/li&gt;
&lt;li data-end=&quot;1152&quot; data-start=&quot;1105&quot; data-section-id=&quot;1b09dq&quot;&gt;단순히 작게 만드는 것보다 실제 실행 효율이 좋아지는 방향으로 적용하는 것이 중요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1175&quot; data-start=&quot;1154&quot; data-section-id=&quot;6c2js7&quot; data-ke-size=&quot;size23&quot;&gt;Model Compression&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1557&quot; data-start=&quot;1176&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1303&quot; data-start=&quot;1176&quot; data-section-id=&quot;16mkigb&quot;&gt;개념
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1303&quot; data-start=&quot;1182&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1210&quot; data-start=&quot;1182&quot; data-section-id=&quot;e30u37&quot;&gt;모델을 더 작고 효율적으로 만드는 전체기법 묶음&lt;/li&gt;
&lt;li data-end=&quot;1256&quot; data-start=&quot;1212&quot; data-section-id=&quot;y3uv63&quot;&gt;양자화, pruning, distillation 등의 방식이 포함될 수 있음&lt;/li&gt;
&lt;li data-end=&quot;1303&quot; data-start=&quot;1258&quot; data-section-id=&quot;1ugir0x&quot;&gt;온디바이스에서 실행 가능한 수준으로 모델을 다듬는 전체 전략이라고 볼 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1435&quot; data-start=&quot;1304&quot; data-section-id=&quot;54i6u&quot;&gt;원리
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1435&quot; data-start=&quot;1310&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1352&quot; data-start=&quot;1310&quot; data-section-id=&quot;fs2hws&quot;&gt;원본 모델 성능을 최대한 유지하면서 용량, 연산량을 줄이는 방향으로 변형&lt;/li&gt;
&lt;li data-end=&quot;1382&quot; data-start=&quot;1354&quot; data-section-id=&quot;1owxh3y&quot;&gt;같은 일을 더 적은 자원으로 수행하게 만드는 것&lt;/li&gt;
&lt;li data-end=&quot;1435&quot; data-start=&quot;1384&quot; data-section-id=&quot;jvidfa&quot;&gt;단순히 모델을 줄이는 것이 아니라, 속도&amp;middot;메모리&amp;middot;전력&amp;middot;정확도 사이에서 균형을 맞추는 과정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1557&quot; data-start=&quot;1436&quot; data-section-id=&quot;1xuwqrz&quot;&gt;활용
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1557&quot; data-start=&quot;1442&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1456&quot; data-start=&quot;1442&quot; data-section-id=&quot;ehb964&quot;&gt;온디바이스에서 필수개념&lt;/li&gt;
&lt;li data-end=&quot;1486&quot; data-start=&quot;1458&quot; data-section-id=&quot;7h3ibs&quot;&gt;앱크기제한, 메모리제한, 발열문제 줄이는데 중요&lt;/li&gt;
&lt;li data-end=&quot;1509&quot; data-start=&quot;1488&quot; data-section-id=&quot;y5cubz&quot;&gt;대형모델 -&amp;gt; 소형모델 변경시 핵심&lt;/li&gt;
&lt;li data-end=&quot;1557&quot; data-start=&quot;1511&quot; data-section-id=&quot;1w7o0bd&quot;&gt;실제 서비스에서는 사용자 경험을 유지하면서 배포 가능한 형태로 만드는 데 사용됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1592&quot; data-start=&quot;1559&quot; data-section-id=&quot;1xugqpe&quot; data-ke-size=&quot;size23&quot;&gt;Latency vs Accuracy trade-off&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1847&quot; data-start=&quot;1593&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1659&quot; data-start=&quot;1593&quot; data-section-id=&quot;ijqj4i&quot;&gt;더 빠르게 응답할수록 정확도가 떨어질수도있고, 높은 정확도를 추구할수록 모델이 무거워 느려질수 있는 트레이드오프관계&lt;/li&gt;
&lt;li data-end=&quot;1696&quot; data-start=&quot;1660&quot; data-section-id=&quot;agpemg&quot;&gt;온디바이스에서는 특히 응답속도가 사용자 경험에 직접 영향을 줌&lt;/li&gt;
&lt;li data-end=&quot;1753&quot; data-start=&quot;1697&quot; data-section-id=&quot;ib3ys6&quot;&gt;예를 들어 카메라 객체인식이나 음성인식은 정확도도 중요하지만, 늦게 반응하면 사용성이 크게 떨어짐&lt;/li&gt;
&lt;li data-end=&quot;1790&quot; data-start=&quot;1754&quot; data-section-id=&quot;15kd7it&quot;&gt;따라서 여러 모델후보를 비교해서 적정 균형점을 찾는 것이 중요&lt;/li&gt;
&lt;li data-end=&quot;1847&quot; data-start=&quot;1791&quot; data-section-id=&quot;c8jrjt&quot;&gt;서비스에서는 항상 최고 정확도보다, 충분히 좋은 정확도와 빠른 응답을 함께 만족하는 지점을 찾게됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1881&quot; data-start=&quot;1849&quot; data-section-id=&quot;edloiu&quot; data-ke-size=&quot;size23&quot;&gt;Batch vs Real-time inference&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2167&quot; data-start=&quot;1882&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1985&quot; data-start=&quot;1882&quot; data-section-id=&quot;1evr79l&quot;&gt;batch 추론 - 여러 입력을 한번에 묶어서 처리
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1985&quot; data-start=&quot;1914&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1948&quot; data-start=&quot;1914&quot; data-section-id=&quot;wo0zgg&quot;&gt;서버로그분석, 대용량 이미지분류, 야간 일괄처리 등에 적합&lt;/li&gt;
&lt;li data-end=&quot;1985&quot; data-start=&quot;1950&quot; data-section-id=&quot;1isrc4u&quot;&gt;처리 효율은 좋지만 즉각적인 응답이 필요한 상황과는 맞지않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2089&quot; data-start=&quot;1986&quot; data-section-id=&quot;vb5tbu&quot;&gt;real time 추론 - 입력이 들어올때마다 즉시처리
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2089&quot; data-start=&quot;2020&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2051&quot; data-start=&quot;2020&quot; data-section-id=&quot;1d56pm1&quot;&gt;카메라 객체인식, 음성비서, 키보드 자동완성등에 사용&lt;/li&gt;
&lt;li data-end=&quot;2089&quot; data-start=&quot;2053&quot; data-section-id=&quot;v3hac&quot;&gt;사용자 입력에 바로 반응해야하므로 속도와 지연시간이 매우 중요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2120&quot; data-start=&quot;2090&quot; data-section-id=&quot;12jcryl&quot;&gt;온디바이스는 대체로 real-time 이 많이사용됨&lt;/li&gt;
&lt;li data-end=&quot;2167&quot; data-start=&quot;2121&quot; data-section-id=&quot;73q7ag&quot;&gt;그래서 모델 정확도뿐 아니라 실행속도, 메모리 사용량, 발열까지 함께 고려해야함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;6713&quot; data-start=&quot;6643&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;6745&quot; data-start=&quot;6720&quot; data-section-id=&quot;1fqh71z&quot; data-ke-size=&quot;size26&quot;&gt;그래서 이 시점의 나는 지금 AI를 어떻게 사용하고 있는가&lt;/h2&gt;
&lt;p data-end=&quot;6811&quot; data-start=&quot;6747&quot; data-ke-size=&quot;size16&quot;&gt;크게 3가지로 나누어서 사용하고있습니다&lt;/p&gt;
&lt;p data-end=&quot;6811&quot; data-start=&quot;6747&quot; data-ke-size=&quot;size16&quot;&gt;개발영역과 생산성영역과 검색영역입니다.&lt;/p&gt;
&lt;h3 data-end=&quot;6819&quot; data-start=&quot;6813&quot; data-section-id=&quot;1hrke6e&quot; data-ke-size=&quot;size23&quot;&gt;개발&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;6908&quot; data-start=&quot;6820&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;6853&quot; data-start=&quot;6834&quot; data-section-id=&quot;tq4ihk&quot;&gt;에이전트를 역할별로 나눠서 사용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;6853&quot; data-start=&quot;6834&quot; data-section-id=&quot;tq4ihk&quot;&gt;UI 구현&lt;/li&gt;
&lt;li data-end=&quot;6853&quot; data-start=&quot;6834&quot; data-section-id=&quot;tq4ihk&quot;&gt;아키텍처, 성능, 보안, 스타일 검사&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;6908&quot; data-start=&quot;6894&quot; data-section-id=&quot;9yyast&quot;&gt;스킬&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;6908&quot; data-start=&quot;6894&quot; data-section-id=&quot;9yyast&quot;&gt;모듈 추가&lt;/li&gt;
&lt;li data-end=&quot;6908&quot; data-start=&quot;6894&quot; data-section-id=&quot;9yyast&quot;&gt;코드 구현&lt;/li&gt;
&lt;li data-end=&quot;6908&quot; data-start=&quot;6894&quot; data-section-id=&quot;9yyast&quot;&gt;코드 검사&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;6997&quot; data-start=&quot;6987&quot; data-section-id=&quot;rf21bu&quot; data-ke-size=&quot;size23&quot;&gt;개발 생산성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;7064&quot; data-start=&quot;6998&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;7009&quot; data-start=&quot;6998&quot; data-section-id=&quot;f5vo5s&quot;&gt;작업 스케줄 관리 - 옵시디언 연동&lt;/li&gt;
&lt;li data-end=&quot;7020&quot; data-start=&quot;7010&quot; data-section-id=&quot;id056w&quot;&gt;문서 작성 - 작업기반으로 작성&lt;/li&gt;
&lt;li data-end=&quot;7044&quot; data-start=&quot;7021&quot; data-section-id=&quot;1ip8fyt&quot;&gt;AI 에이전트가 수행한 작업을 md 파일로 정리&lt;/li&gt;
&lt;li data-end=&quot;7064&quot; data-start=&quot;7045&quot; data-section-id=&quot;162wxzk&quot;&gt;웹훅을 통해 메신저로 요약 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;7144&quot; data-start=&quot;7138&quot; data-section-id=&quot;1hrkftb&quot; data-ke-size=&quot;size23&quot;&gt;검색&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;7191&quot; data-start=&quot;7145&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;7152&quot; data-start=&quot;7145&quot; data-section-id=&quot;1tle3d9&quot;&gt;개념 검색&lt;/li&gt;
&lt;li data-end=&quot;7165&quot; data-start=&quot;7153&quot; data-section-id=&quot;1ofa72e&quot;&gt;정보 분석 및 정리&lt;/li&gt;
&lt;li data-end=&quot;7180&quot; data-start=&quot;7166&quot; data-section-id=&quot;1lz23m4&quot;&gt;필요한 코드 초안 작성&lt;/li&gt;
&lt;li data-end=&quot;7191&quot; data-start=&quot;7181&quot; data-section-id=&quot;blahgl&quot;&gt;버그 원인 탐색&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;7245&quot; data-start=&quot;7193&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-section-id=&quot;1fqh71z&quot; data-start=&quot;6720&quot; data-end=&quot;6745&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&amp;nbsp;Next...&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;26년 최근들어 하네스 라는 방식의 에이전트 팀을 이용한 &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;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;middot;조율하는 아키텍처 도구&lt;br /&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;/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;리드 에이전트 -(명령)-&amp;gt;&amp;nbsp; 개발 에이전트&amp;nbsp; &amp;nbsp;-&amp;gt;&amp;nbsp; 작업&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; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; -(명령)-&amp;gt;&amp;nbsp; 문서 에이전트&amp;nbsp; &amp;nbsp;-&amp;gt;&amp;nbsp; 작업&lt;/p&gt;
&lt;p data-end=&quot;7245&quot; data-start=&quot;7193&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; -(명령)&lt;/span&gt;-&amp;gt;&amp;nbsp; 검증 에이전트&amp;nbsp; &amp;nbsp;-&amp;gt;&amp;nbsp; 작업&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;7245&quot; data-start=&quot;7193&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;7245&quot; data-start=&quot;7193&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이런 과정을 알아서 할 수 있는 환경을 만들어보고싶네요&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;7245&quot; data-start=&quot;7193&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이 과정에서&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;7245&quot; data-start=&quot;7193&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;주의해야할점은 AI를 어디까지 믿어야하는가가 될거같아요&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;7245&quot; data-start=&quot;7193&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;완전한 신뢰는 할 수 없으니 검증은 어떻게 해야할지에 대한 고민도 필요할것 같아요&lt;/span&gt;&lt;/p&gt;</description>
      <category>iyOmSd/Title: Swift</category>
      <author>냄수</author>
      <guid isPermaLink="true">https://nsios.tistory.com/256</guid>
      <comments>https://nsios.tistory.com/256#entry256comment</comments>
      <pubDate>Tue, 31 Mar 2026 21:22:27 +0900</pubDate>
    </item>
    <item>
      <title>[Swift] FoundationModels</title>
      <link>https://nsios.tistory.com/255</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;OnDevice LLM 프레임워크인 FoundationModels에 대해서 알아보려합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 LLM은 통신을 통한다던가 혹은 기기내에서 사용하도록 OnDevice형식을 취하려면 보안과 앱용량의 trade-off를 고민했어야했는데&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;apple의 FoundationModels는 운영체제에 이미 존재해서 앱용량이 늘어나지 않는다고하네요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심타입들을 알아보고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;냉장고 내에있는 재료를 통해 어떤 음식을 만들수잇는지 요청하는 데모앱을 만들어보려합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FoundationModels?&lt;/h3&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;대규모&amp;nbsp;언어&amp;nbsp;모델에&amp;nbsp;대한&amp;nbsp;접근을&amp;nbsp;제공하여&amp;nbsp;사용자의&amp;nbsp;특정&amp;nbsp;사용&amp;nbsp;사례에&amp;nbsp;맞는&amp;nbsp;지능형&amp;nbsp;작업을&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;사용자가&amp;nbsp;요청한&amp;nbsp;내용에&amp;nbsp;적합한&amp;nbsp;새로운&amp;nbsp;텍스트를&amp;nbsp;생성할&amp;nbsp;수&amp;nbsp;있으며,&amp;nbsp;특수한&amp;nbsp;작업을&amp;nbsp;수행하기&amp;nbsp;위해&amp;nbsp;사용자가&amp;nbsp;작성한&amp;nbsp;코드를&amp;nbsp;호출하는&amp;nbsp;결정을&amp;nbsp;내릴&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;파운데이션 모델 프레임워크는 두 가지 기본 안전 계층을 갖추고 있으며, 프레임워크는 다음을 활용합니다&lt;br /&gt;- 민감한 주제를 신중하게 처리하도록 훈련된 온디바이스 언어 모델.&lt;br /&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;내&amp;nbsp;언어&amp;nbsp;모델은&amp;nbsp;모든&amp;nbsp;요청을&amp;nbsp;처리하기에&amp;nbsp;적합하지&amp;nbsp;않을&amp;nbsp;수&amp;nbsp;있으며&amp;nbsp;특정&amp;nbsp;주제에&amp;nbsp;대한&amp;nbsp;요청을&amp;nbsp;거부할&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;문자열&amp;nbsp;응답을&amp;nbsp;생성할&amp;nbsp;때&amp;nbsp;모델이&amp;nbsp;요청을&amp;nbsp;거부하면&amp;nbsp;&quot;죄송합니다,&amp;nbsp;도와드릴&amp;nbsp;수&amp;nbsp;없습니다&quot;와&amp;nbsp;같이&amp;nbsp;거절로&amp;nbsp;시작하는&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;모델 사용 가능 여부는 다음과 같은 기기 요소에 따라 달라집니다:&lt;br /&gt;- 기기가 Apple Intelligence를 지원해야 합니다.&lt;br /&gt;- 기기의 설정에서 Apple Intelligence가 켜져 있어야 합니다.&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;SystemLanguageModel.default.availability 를 이용해서 사용가능/불가능 케이스 대응가능합니다&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;LanguageModelSession&lt;/h3&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;위해&amp;nbsp;LanguageModelSession&amp;nbsp;객체를&amp;nbsp;생성합니다.&amp;nbsp;&lt;br /&gt;단일&amp;nbsp;회화&amp;nbsp;상호작용의&amp;nbsp;경우&amp;nbsp;모델을&amp;nbsp;호출할&amp;nbsp;때마다&amp;nbsp;새&amp;nbsp;세션을&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;프레임워크를 사용하는 앱을 테스트할 때는 Xcode Instruments를 활용하여 요청 수행 시간 등 수행한 요청에 대한 정보를 더 자세히 파악할 수 있습니다.&lt;br /&gt;요청을&amp;nbsp;수행할&amp;nbsp;때&amp;nbsp;LanguageModelSession&amp;nbsp;동안&amp;nbsp;모델이&amp;nbsp;수행한&amp;nbsp;작업을&amp;nbsp;설명하는&amp;nbsp;기록&amp;nbsp;항목에&amp;nbsp;접근할&amp;nbsp;수&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;br /&gt;&lt;b&gt;GenerationOptions&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;b&gt;UseCase&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;general - 기본 모델은 질문 답변 및 창의적 콘텐츠 생성 등 광범위한 텍스트 기반 작업, 일반적 상황에서 최상의 성능을 내도록 최적화됨&lt;br /&gt;contentTagging - 주제 태그 생성, 콘텐츠 분류, 텍스트에서 특정 엔티티(예: 인물, 장소) 추출 등의 텍스트의 구조를 분석하고 특정 정보를 추출하거나 분류하는데 특화된 모델&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;GenerationOptions&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;sampling&lt;/b&gt; - 출력토큰 선택하는 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;greedy선택한 경우 같은세션상태에서 같은프롬프트 입력시 항상 같은 출력(같은 버전 온디바이스 모델이면)&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;temperature&lt;/b&gt; - 출력의 다양성 조정 (높을수록 다른 결과 생성)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;maximumResponseTokens&lt;/b&gt; - 너무 긴 응답 제한&lt;/p&gt;
&lt;p 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;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Generable&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델이 프롬프트에 응답할 때 사용하는 프로토콜 타입&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swift 구조체나 열거형에 Generable 매크로를 주석 처리하여 모델이 프롬프트에 응답할 때 해당 유형의 인스턴스를 생성할 수 있도록 합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과로 비구조적 자연어를 출력하는데 사람은 읽기쉽지만 뷰에 적용하기힘들어서 Json, csv 형식으로 변환후 사용해서&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;h3 data-ke-size=&quot;size23&quot;&gt;GenerationGuide&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Generable&amp;nbsp;유형의&amp;nbsp;속성&amp;nbsp;허용&amp;nbsp;값에&amp;nbsp;영향을&amp;nbsp;미칠&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Guide 매크로로 사용&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;/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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 예제를 만들어봅시다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;냉장고에 있는 재료들로 레시피를 추천해주는 기능입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Session을 만들어주고&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;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;response(to: )&lt;/span&gt;&lt;/b&gt; 한번에 완성된 응답받기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;streamResponse(to: )&lt;/b&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;이때 @Generable을 사용하면 원하는 타입의 구조체로 받을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래코드는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델을 정의하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션을 설정하고&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;pre id=&quot;code_1771919130226&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Generable(description: &quot;식재료&quot;)
struct Ingredient {
    let name: String
}

@Generable(description: &quot;AI가 제안할 결과 레시피&quot;)
struct Recipe: Codable {
    @Guide(description: &quot;요리이름&quot;)
    let title: String
    @Guide(description: &quot;냉장고에 없는 추가로 필요한 재료만 리스트업&quot;)
    let ingredientsNeeded: [String]
    let instructions: [String]
    @Guide(description: &quot;쉬움, 보통, 어려움 중 하나 선택&quot;)
    let difficulty: String
    let estimatedTime: Int // 분 단위
}

let instructions = &quot;&quot;&quot;
    당신은 냉장고에 있는 재료로 요리를 추천해주는 전문 셰프 어시스턴트입니다.
        
        역할:
        - 사용자가 제공한 재료만으로 만들 수 있는 현실적인 레시피를 추천합니다
        - 소금, 후추, 식용유, 간장, 된장 등 기본 양념은 항상 있다고 가정합니다
        - 가정집 주방 기준으로 실현 가능한 레시피만 제안합니다
        - 조리 단계는 초보자도 따라할 수 있도록 구체적으로 설명합니다
        - 한국어로 응답합니다
        
        레시피 선정 기준:
        - 다양한 난이도(Easy / Medium / Hard)의 레시피를 균형있게 추천합니다
        - 재료 낭비 없이 가능한 많은 재료를 활용합니다
        - 영양 균형을 고려합니다
        - 요리이름에 냉장고 단어를 제외합니다.
        - 냉장고 속에 없는건 사용하지 않습니다.
    &quot;&quot;&quot;

func makeSession() async {
    let session = LanguageModelSession(instructions: instructions)
    session.prewarm()
    self.session = session
    
    let stream = session.streamResponse(
        to: &quot;냉장고에 있는 재료로 만들 수 있는 요리는?&quot;,
        generating: Recipe.self
    )
    do {
        for try await token in stream {
            print(&quot;token.content&quot;, token.content)
            self.displayText = token.content.instructions ?? []
            self.title = token.content.title ?? &quot;&quot;
        }
    } catch {
        print(error)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행결과를 보면&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1771919909168&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;(id: FoundationModels.GenerationID(value: &quot;0A90B228-5192-4EE1-B9AE-FF96715453CF&quot;), title: Optional(&quot;간장 볶음밥&quot;), ingredientsNeeded: nil, instructions: nil, difficulty: nil, estimatedTime: nil)
(id: FoundationModels.GenerationID(value: &quot;0A90B228-5192-4EE1-B9AE-FF96715453CF&quot;), title: Optional(&quot;간장 볶음밥&quot;), ingredientsNeeded: Optional([&quot;밥&quot;]), instructions: nil, difficulty: nil, estimatedTime: nil)
(id: FoundationModels.GenerationID(value: &quot;0A90B228-5192-4EE1-B9AE-FF96715453CF&quot;), title: Optional(&quot;간장 볶음밥&quot;), ingredientsNeeded: Optional([&quot;밥&quot;]), instructions: Optional([&quot;냉장고에서 밥&quot;]), difficulty: nil, estimatedTime: nil)
(id: FoundationModels.GenerationID(value: &quot;0A90B228-5192-4EE1-B9AE-FF96715453CF&quot;), title: Optional(&quot;간장 볶음밥&quot;), ingredientsNeeded: Optional([&quot;밥&quot;]), instructions: Optional([&quot;냉장고에서 밥을 꺼내 놓는다.&quot;, &quot;밥 위에 간장과&quot;]), difficulty: nil, estimatedTime: nil)
(id: FoundationModels.GenerationID(value: &quot;0A90B228-5192-4EE1-B9AE-FF96715453CF&quot;), title: Optional(&quot;간장 볶음밥&quot;), ingredientsNeeded: Optional([&quot;밥&quot;]), instructions: Optional([&quot;냉장고에서 밥을 꺼내 놓는다.&quot;, &quot;밥 위에 간장과 식용유를 두르고 잘 섞는다.&quot;]), difficulty: nil, estimatedTime: nil)
(id: FoundationModels.GenerationID(value: &quot;0A90B228-5192-4EE1-B9AE-FF96715453CF&quot;), title: Optional(&quot;간장 볶음밥&quot;), ingredientsNeeded: Optional([&quot;밥&quot;]), instructions: Optional([&quot;냉장고에서 밥을 꺼내 놓는다.&quot;, &quot;밥 위에 간장과 식용유를 두르고 잘 섞는다.&quot;, &quot;밥을 냄비에 담고 끓인다&quot;]), difficulty: nil, estimatedTime: nil)
(id: FoundationModels.GenerationID(value: &quot;0A90B228-5192-4EE1-B9AE-FF96715453CF&quot;), title: Optional(&quot;간장 볶음밥&quot;), ingredientsNeeded: Optional([&quot;밥&quot;]), instructions: Optional([&quot;냉장고에서 밥을 꺼내 놓는다.&quot;, &quot;밥 위에 간장과 식용유를 두르고 잘 섞는다.&quot;, &quot;밥을 냄비에 담고 끓인다.&quot;, &quot;밥이 익으면 소금과 후추&quot;]), difficulty: nil, estimatedTime: nil)
(id: FoundationModels.GenerationID(value: &quot;0A90B228-5192-4EE1-B9AE-FF96715453CF&quot;), title: Optional(&quot;간장 볶음밥&quot;), ingredientsNeeded: Optional([&quot;밥&quot;]), instructions: Optional([&quot;냉장고에서 밥을 꺼내 놓는다.&quot;, &quot;밥 위에 간장과 식용유를 두르고 잘 섞는다.&quot;, &quot;밥을 냄비에 담고 끓인다.&quot;, &quot;밥이 익으면 소금과 후추로 간을 맞춘다.&quot;]), difficulty: Optional(&quot;&quot;), estimatedTime: nil)
(id: FoundationModels.GenerationID(value: &quot;0A90B228-5192-4EE1-B9AE-FF96715453CF&quot;), title: Optional(&quot;간장 볶음밥&quot;), ingredientsNeeded: Optional([&quot;밥&quot;]), instructions: Optional([&quot;냉장고에서 밥을 꺼내 놓는다.&quot;, &quot;밥 위에 간장과 식용유를 두르고 잘 섞는다.&quot;, &quot;밥을 냄비에 담고 끓인다.&quot;, &quot;밥이 익으면 소금과 후추로 간을 맞춘다.&quot;]), difficulty: Optional(&quot;Easy&quot;), estimatedTime: Optional(10))&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-origin-width=&quot;350&quot; data-origin-height=&quot;352&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BEvzZ/dJMcagEABwa/UzY3KonTijSmIUIG9DSFp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BEvzZ/dJMcagEABwa/UzY3KonTijSmIUIG9DSFp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BEvzZ/dJMcagEABwa/UzY3KonTijSmIUIG9DSFp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBEvzZ%2FdJMcagEABwa%2FUzY3KonTijSmIUIG9DSFp0%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;350&quot; height=&quot;352&quot; data-origin-width=&quot;350&quot; data-origin-height=&quot;352&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;또다른 결과로는 냉장고에 없는 재료를 마음대로 추가해서 작성하는 &lt;span style=&quot;color: #ee2323;&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tool프로토콜을 준수해서 정의해줄 수 있습니다.&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;&amp;nbsp;Tool&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI모델이 사용자 요청분석후 답변 작성대신 요청해결을 위해서 도구가 필요하다고 판단하는 과정입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1726&quot; data-origin-height=&quot;1032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAvmB5/dJMcai3pbZ9/okFDAzvvB06fGVxEaUMYUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAvmB5/dJMcai3pbZ9/okFDAzvvB06fGVxEaUMYUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAvmB5/dJMcai3pbZ9/okFDAzvvB06fGVxEaUMYUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAvmB5%2FdJMcai3pbZ9%2FokFDAzvvB06fGVxEaUMYUK%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;1726&quot; height=&quot;1032&quot; data-origin-width=&quot;1726&quot; data-origin-height=&quot;1032&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;&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;모델: 정의된 tool 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도구: 냉장고에 있는 재료반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델: 요리 추천 및 부족한 재료 파악&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도구를 만들어서 넣어주면 필요할때 가져다 쓴다라고 했지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각보다 도구가 호출이 항상되지 않더라구요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조를 변경하고 조건을 변경해서 적용한 코드입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1771987001720&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let instructions = &quot;&quot;&quot;
    당신은 요리 레시피를 추천해주는 전문 셰프 어시스턴트입니다.

    반드시 지켜야 할 규칙:
    1. 레시피 추천 전 반드시 get_ingredients 도구를 호출하여 사용 가능한 재료를 먼저 확인하세요.
    2. get_ingredients 도구가 반환한 재료만 사용합니다.
    3. 요리 이름(title)에 반드시 실제 요리 이름을 사용하세요 (예: 감자볶음, 순두부찌개, 햄볶음밥).
    4. 조리 단계는 초보자도 따라할 수 있도록 구체적으로 설명합니다.
    5. 한국어로 응답합니다.
    6. 만들 수 있는 요리가 없다면 현재 재료로는 만들 수 없다고 말해줍니다.
    &quot;&quot;&quot;


let prompt = &quot;사용 가능한 재료를 먼저 확인한 후, 그 재료들로만 만들 수 있는 요리를 하나 추천해줘&quot;


@Generable(description: &quot;식재료&quot;)
struct Ingredient: Hashable {
    let name: String
}

@Generable(description: &quot;AI가 제안할 결과 레시피&quot;)
struct Recipe {
    @Guide(description: &quot;실제 요리 이름을 사용. 예: 감자볶음, 순두부찌개, 햄볶음밥&quot;)
    let title: String
    @Guide(description: &quot;요리에 필요한 재료&quot;)
    let ingredients: [String]
    @Guide(description: &quot;조리 단계 목록. get_ingredients 도구로 확인한 재료만 사용&quot;)
    let instructions: [String]
    @Guide(description: &quot;쉬움, 보통, 어려움 중 하나 선택&quot;)
    let difficulty: String
}&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;응답으로 사용할 구조체타입들도 더 상세하게 정의했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1771987061694&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct FridgeIngredientsTool: Tool {
    var name: String { &quot;get_ingredients&quot; }
    var description: String {
        &quot;사용 가능한 재료 목록을 조회합니다. 레시피 추천 전 반드시 먼저 호출해야 하며, 이 도구가 반환한 재료만 레시피에 사용할 수 있습니다.&quot;
    }

    // 앱의 재료 저장소를 참조
    var ingredients: Set&amp;lt;String&amp;gt;
    
    init(ingredients: [Ingredient]) {
        self.ingredients = Set(ingredients.map { $0.name })
    }

    @Generable
    struct Arguments {
        @Guide(description: &quot;레시피로 만들 요리&quot;)
        let food: String
    }

    // 언어 모델은 이 도구를 활용하고자 할 때 이 메서드를 호출합니다.
    func call(arguments: Arguments) async throws -&amp;gt; String {
        let list = ingredients.map { &quot;\($0)&quot; }.joined(separator: &quot;,&quot;)
        print(&quot; &quot;, #function, &quot;\nlist&quot;, arguments)
        return &quot;&quot;&quot;
            사용 가능한 재료 목록 (이 재료만 레시피에 사용 가능):
            \(list)

            위 목록에 없는 재료는 레시피에 포함하지 마세요.
            &quot;&quot;&quot;
    }
}&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;이렇게 정의를해도 없는 재료를 넣는과정이있는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도구가 호출되면 정상적으로 정의된 재료내에서 요리를하는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도구가 호출되지 않으면 없는재료를 추가해서 만드는것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;써본결과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;지침과 프롬프트를 잘 다듬어서 테스트하면서 기대하는 대답이 나오는지 확인이&lt;/b&gt;&lt;/span&gt; 필요한것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;도구가 호출되지 않는다면 Arguments 가이드와 설명을 좀더 다듬어 보는걸 추천&lt;/b&gt;&lt;/span&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정시 &quot;절대 하지마세요&quot;, &quot;주의&quot;와 같이 강제로 부정적인 느낌의 명령은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apple FoundationModels의 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;안전 가드레일(guardrail)이 발동되서 에러를 발생&lt;/span&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;&lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;Arguments&lt;/span&gt;&lt;/b&gt;의 정의에 따라서도 응답의 질이 달라지고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;call 함수&lt;/span&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;지금까지 적용한 동작흐름은 아래와 같습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;모델지침으로 세션 생성&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;⬇️&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;세션 prewarm&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;⬇️&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;세션에 프롬프트 입력&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;⬇️&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;AI가 필요한 도구선택후 호출 (Argument로 AI가 선택한 요리 값 전달)&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;⬇️&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;call함수에서 현재 냉장고(뷰모델 배열 데이터)에 있는 재료들 반환 및 추가적인 프롬프트 적용&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;⬇️&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;도구에서 반환받은 재료를 바탕으로 Recipe타입에 맞게 모델화&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;⬇️&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;stream으로 Recipe모델 받아옴&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;⬇️&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; 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&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/4il0u/dJMcad16qhQ/OWP83w05fVE13GD8ahx7dK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4il0u/dJMcad16qhQ/OWP83w05fVE13GD8ahx7dK/img.png&quot; data-origin-width=&quot;328&quot; data-origin-height=&quot;441&quot; data-is-animation=&quot;false&quot; style=&quot;width: 35.5789%; margin-right: 10px;&quot; data-widthpercent=&quot;36.43&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4il0u/dJMcad16qhQ/OWP83w05fVE13GD8ahx7dK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4il0u%2FdJMcad16qhQ%2FOWP83w05fVE13GD8ahx7dK%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;328&quot; height=&quot;441&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qeDYB/dJMcafZW9KI/MLrFizWBspBvMRtnK8J3H0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qeDYB/dJMcafZW9KI/MLrFizWBspBvMRtnK8J3H0/img.png&quot; data-origin-width=&quot;259&quot; data-origin-height=&quot;406&quot; data-is-animation=&quot;false&quot; style=&quot;width: 30.5162%; margin-right: 10px;&quot; data-widthpercent=&quot;31.24&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qeDYB/dJMcafZW9KI/MLrFizWBspBvMRtnK8J3H0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqeDYB%2FdJMcafZW9KI%2FMLrFizWBspBvMRtnK8J3H0%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;259&quot; height=&quot;406&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kDBWt/dJMcabcbvhz/nKu6PtTKOjNZhzQdoqKMf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kDBWt/dJMcabcbvhz/nKu6PtTKOjNZhzQdoqKMf0/img.png&quot; data-origin-width=&quot;338&quot; data-origin-height=&quot;512&quot; data-is-animation=&quot;false&quot; style=&quot;width: 31.5794%;&quot; data-widthpercent=&quot;32.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kDBWt/dJMcabcbvhz/nKu6PtTKOjNZhzQdoqKMf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkDBWt%2FdJMcabcbvhz%2FnKu6PtTKOjNZhzQdoqKMf0%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;338&quot; height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;/div&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;만능소스를 추가해줬더니 더맛잇게 만드네요  &lt;br /&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;잘다듬어서 Tool로 정의한 도구가 정상적으로 호출되고 가지고있는 재료들만으로 요리를 만들지만&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;decodingFailure(FoundationModels.LanguageModelSession.GenerationError.Context(debugDescription: &quot;Failed to extract content&quot;, underlyingErrors: []))&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;하지만 재실행하면 정상적인 결과를 얻을 수 있습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;374&quot; data-origin-height=&quot;202&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dubOgo/dJMcag5E0W0/Ppmh2kKeJU4AknH4BxUGb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dubOgo/dJMcag5E0W0/Ppmh2kKeJU4AknH4BxUGb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dubOgo/dJMcag5E0W0/Ppmh2kKeJU4AknH4BxUGb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdubOgo%2FdJMcag5E0W0%2FPpmh2kKeJU4AknH4BxUGb0%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;374&quot; height=&quot;202&quot; data-origin-width=&quot;374&quot; data-origin-height=&quot;202&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;347&quot; data-origin-height=&quot;529&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FmRK9/dJMcagR7StC/qkNa9DphC1HBPM68NaQkik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FmRK9/dJMcagR7StC/qkNa9DphC1HBPM68NaQkik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FmRK9/dJMcagR7StC/qkNa9DphC1HBPM68NaQkik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFmRK9%2FdJMcagR7StC%2FqkNa9DphC1HBPM68NaQkik%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;347&quot; height=&quot;529&quot; data-origin-width=&quot;347&quot; data-origin-height=&quot;529&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;/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;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;pre id=&quot;code_1772004884261&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  call(arguments:) 
list Arguments(food: &quot;김치찌개&quot;)
  call(arguments:) 
list Arguments(food: &quot;순두부김밥&quot;)
  call(arguments:) 
list Arguments(food: &quot;김밥&quot;)
  call(arguments:) 
list Arguments(food: &quot;김밥&quot;)
content: PartiallyGenerated(id: FoundationModels.GenerationID(value: &quot;E9D6948E-1AF8-400D-88EA-D33014AB36E3&quot;), title: Optional(&quot;김밥&quot;), ingredients: Optional([&quot;&quot;]), instructions: nil, difficulty: nil)
content: PartiallyGenerated(id: FoundationModels.GenerationID(value: &quot;E9D6948E-1AF8-400D-88EA-D33014AB36E3&quot;), title: Optional(&quot;김밥&quot;), ingredients: Optional([&quot;밥&quot;, &quot;김&quot;]), instructions: Optional([&quot;&quot;]), difficulty: nil)
content: PartiallyGenerated(id: FoundationModels.GenerationID(value: &quot;E9D6948E-1AF8-400D-88EA-D33014AB36E3&quot;), title: Optional(&quot;김밥&quot;), ingredients: Optional([&quot;밥&quot;, &quot;김&quot;]), instructions: Optional([&quot;밥을 김에 말아주세요.&quot;, &quot;햄&quot;]), difficulty: nil)
content: PartiallyGenerated(id: FoundationModels.GenerationID(value: &quot;E9D6948E-1AF8-400D-88EA-D33014AB36E3&quot;), title: Optional(&quot;김밥&quot;), ingredients: Optional([&quot;밥&quot;, &quot;김&quot;]), instructions: Optional([&quot;밥을 김에 말아주세요.&quot;, &quot;햄을 넣어주세요.&quot;]), difficulty: Optional(&quot;&quot;))
content: PartiallyGenerated(id: FoundationModels.GenerationID(value: &quot;E9D6948E-1AF8-400D-88EA-D33014AB36E3&quot;), title: Optional(&quot;김밥&quot;), ingredients: Optional([&quot;밥&quot;, &quot;김&quot;]), instructions: Optional([&quot;밥을 김에 말아주세요.&quot;, &quot;햄을 넣어주세요.&quot;]), difficulty: Optional(&quot;쉬움&quot;))&lt;/code&gt;&lt;/pre&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-origin-width=&quot;1174&quot; data-origin-height=&quot;139&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MbbNE/dJMcafMsTRv/jsFStTNSgWzA0ef7CCvvik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MbbNE/dJMcafMsTRv/jsFStTNSgWzA0ef7CCvvik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MbbNE/dJMcafMsTRv/jsFStTNSgWzA0ef7CCvvik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMbbNE%2FdJMcafMsTRv%2FjsFStTNSgWzA0ef7CCvvik%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;1174&quot; height=&quot;139&quot; data-origin-width=&quot;1174&quot; data-origin-height=&quot;139&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;그와중에 김밥에 김과 김치를 사용하는군요...&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;정리하면 FoundationModel을 사용하기위해서 아래는 기본적으로 필요합니다.&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;LLM&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- LanguageModelSession의 지침 정의&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Tool 정의(Arguments, call함수)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 응답 모델로 사용할 Generable과 Guide정의&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- response방식 선택(stream, 단일)&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;간단하게 FoundationModel을 사용해봤는데요&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;gt; 프롬프트와 지침으로 도구사용을 유도할 수 있도록 잘 작성해야할 것 같아요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 다른 데이터로 명령을 수행함 -&amp;gt; ai가 상상해서 행동하지않도록 좀더 자세한 예시와 명령을 해야할 것 같아요&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;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;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;</description>
      <author>냄수</author>
      <guid isPermaLink="true">https://nsios.tistory.com/255</guid>
      <comments>https://nsios.tistory.com/255#entry255comment</comments>
      <pubDate>Thu, 26 Feb 2026 21:47:51 +0900</pubDate>
    </item>
    <item>
      <title>[Swift] Core Image Filter</title>
      <link>https://nsios.tistory.com/254</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이미지에 필터를 적용하는 작업을 해볼겁니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Core Image를 사용합니다.&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;br /&gt;(이건 UIKit에서도 제공해서 적용하긴쉽긴하지만 커스텀하기엔 더 low level인 Core Image 좋습니다.)&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;h3 data-ke-size=&quot;size23&quot;&gt;Core Image란?&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;519&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cs3Gbe/dJMcaaYo2qk/zd9qmWtWWRnqe1cKMVUJdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cs3Gbe/dJMcaaYo2qk/zd9qmWtWWRnqe1cKMVUJdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cs3Gbe/dJMcaaYo2qk/zd9qmWtWWRnqe1cKMVUJdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcs3Gbe%2FdJMcaaYo2qk%2Fzd9qmWtWWRnqe1cKMVUJdK%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;1280&quot; height=&quot;519&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;519&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Core&amp;nbsp;Image는&amp;nbsp;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;정지&amp;nbsp;영상&amp;nbsp;및&amp;nbsp;동영상에&amp;nbsp;대한&amp;nbsp;실시간에&amp;nbsp;가까운&amp;nbsp;처리를&amp;nbsp;제공하도록&amp;nbsp;설계된&amp;nbsp;이미지&amp;nbsp;처리&amp;nbsp;및&amp;nbsp;분석&amp;nbsp;기술&lt;/b&gt;&lt;/span&gt;입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Core&amp;nbsp;Graphics,&amp;nbsp;Core&amp;nbsp;Video,&amp;nbsp;Image&amp;nbsp;I/O&amp;nbsp;프레임워크의&amp;nbsp;이미지&amp;nbsp;데이터&amp;nbsp;유형을&amp;nbsp;기반으로&amp;nbsp;작동하며,&amp;nbsp;GPU&amp;nbsp;또는&amp;nbsp;CPU&amp;nbsp;렌더링&amp;nbsp;경로를&amp;nbsp;사용합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Core&amp;nbsp;Image는&amp;nbsp;사용하기&amp;nbsp;쉬운&amp;nbsp;애플리케이션&amp;nbsp;프로그래밍&amp;nbsp;인터페이스(API)를&amp;nbsp;제공함으로써&amp;nbsp;저수준&amp;nbsp;그래픽&amp;nbsp;처리의&amp;nbsp;세부&amp;nbsp;사항을&amp;nbsp;숨깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPU의 성능을 활용하기 위해 OpenGL, OpenGL ES 또는 Metal의 세부 사항을 알 필요도 없으며, 멀티코어 처리의 이점을 얻기 위해 Grand Central Dispatch(GCD)에 대해 알 필요도 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;Core Image가 세부 사항을 처리해 줍니다.&lt;/b&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;앞으로 사용하게될 CIImage는 이미지가 아니라 이미지를 생성하는데 필요한 정보입니다.&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;a href=&quot;https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/CoreImaging/ci_intro/ci_intro.html#//apple_ref/doc/uid/TP30001185&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/CoreImaging/ci_intro/ci_intro.html#//apple_ref/doc/uid/TP30001185&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1769416847088&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;About Core Image&quot; data-og-description=&quot;About Core Image Core Image is an image processing and analysis technology designed to provide near real-time processing for still and video images. It operates on image data types from the Core Graphics, Core Video, and Image I/O frameworks, using either &quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/CoreImaging/ci_intro/ci_intro.html#//apple_ref/doc/uid/TP30001185&quot; data-og-url=&quot;https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/CoreImaging/ci_intro/ci_intro.html#//apple_ref/doc/uid/TP30001185&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/CoreImaging/ci_intro/ci_intro.html#//apple_ref/doc/uid/TP30001185&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/CoreImaging/ci_intro/ci_intro.html#//apple_ref/doc/uid/TP30001185&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;About Core Image&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;About Core Image Core Image is an image processing and analysis technology designed to provide near real-time processing for still and video images. It operates on image data types from the Core Graphics, Core Video, and Image I/O frameworks, using either&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/library/archive/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html#//apple_ref/doc/filter/ci/CIBoxBlur&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/library/archive/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html#//apple_ref/doc/filter/ci/CIBoxBlur&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1769416910264&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;Core Image Filter Reference&quot; data-og-description=&quot;Core Image Filter Reference&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/library/archive/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html#//apple_ref/doc/filter/ci/CIBoxBlur&quot; data-og-url=&quot;https://developer.apple.com/library/archive/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html#//apple_ref/doc/filter/ci/CIBoxBlur&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developer.apple.com/library/archive/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html#//apple_ref/doc/filter/ci/CIBoxBlur&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/library/archive/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html#//apple_ref/doc/filter/ci/CIBoxBlur&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;Core Image Filter Reference&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Core Image Filter Reference&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.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;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터 정보나 Core Image 가이드는 여기서 확인하세요!&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;br /&gt;CIFilter이란..?&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;CIImage &amp;rarr; CIFilter &amp;rarr; output CIImage &amp;rarr; CIFilter &amp;rarr; output CIImage &amp;rarr; &amp;hellip; 방식으로&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;CIImage에 효과를 적용하는데 사용하는 타입입니다.&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&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/b0jtam/dJMcaajNswI/2sPZRDSGYhpzLxEbkgRKk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0jtam/dJMcaajNswI/2sPZRDSGYhpzLxEbkgRKk0/img.png&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;606&quot; data-is-animation=&quot;false&quot; style=&quot;width: 45.1347%; margin-right: 10px;&quot; data-widthpercent=&quot;45.67&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0jtam/dJMcaajNswI/2sPZRDSGYhpzLxEbkgRKk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0jtam%2FdJMcaajNswI%2F2sPZRDSGYhpzLxEbkgRKk0%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;498&quot; height=&quot;606&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUzPOj/dJMcafMb0V2/qXkiTErkbvEXwj9IqJEynk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUzPOj/dJMcafMb0V2/qXkiTErkbvEXwj9IqJEynk/img.png&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;1440&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;54.33&quot; style=&quot;width: 53.7025%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUzPOj/dJMcafMb0V2/qXkiTErkbvEXwj9IqJEynk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUzPOj%2FdJMcafMb0V2%2FqXkiTErkbvEXwj9IqJEynk%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;1408&quot; height=&quot;1440&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;CIFilter를 만드는 방법은 2가지가 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1769417112830&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import CoreImage.CIFilterBuiltins

func applyBlur(originImage: CIImage) {
    let blurFilter = CIFilter.boxBlur()
    blurFilter.inputImage = originImage
    blurFilter.radius = 10
}

func applyBlur2(originImage: CIImage) {
    let filter = CIFilter(name: &quot;CIBoxBlur&quot;)
    filter?.setValue(originImage, forKey: kCIInputImageKey) // 적용할 이미지
    filter?.setValue(10, forKey: kCIInputRadiusKey) // 강도
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;b&gt;import CoreImage.CIFilterBuiltins&lt;/b&gt;를 통해 CIFilter의 타입값으로 사용하는 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 직접 필터 문자열을 적용해서 kCI 상수값을 사용하는방법&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;691&quot; data-origin-height=&quot;259&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dBUl2O/dJMcacBVpL6/NnGi5smJkk36dSTCc5rKr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dBUl2O/dJMcacBVpL6/NnGi5smJkk36dSTCc5rKr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dBUl2O/dJMcacBVpL6/NnGi5smJkk36dSTCc5rKr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdBUl2O%2FdJMcacBVpL6%2FNnGi5smJkk36dSTCc5rKr0%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;691&quot; height=&quot;259&quot; data-origin-width=&quot;691&quot; data-origin-height=&quot;259&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;체이닝을 통해서 적용 하면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2210&quot; data-origin-height=&quot;1560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNBfv9/dJMcahb8PJT/wfKUjDys9J01cSP5kMYdE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNBfv9/dJMcahb8PJT/wfKUjDys9J01cSP5kMYdE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNBfv9/dJMcahb8PJT/wfKUjDys9J01cSP5kMYdE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNBfv9%2FdJMcahb8PJT%2FwfKUjDys9J01cSP5kMYdE1%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;2210&quot; height=&quot;1560&quot; data-origin-width=&quot;2210&quot; data-origin-height=&quot;1560&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;Core&amp;nbsp;Image&amp;nbsp;필터는&amp;nbsp;출력&amp;nbsp;CIImage&amp;nbsp;객체를&amp;nbsp;생성하므로,&amp;nbsp;이&amp;nbsp;객체를&amp;nbsp;다른&amp;nbsp;필터의&amp;nbsp;입력으로&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를&amp;nbsp;들어,&amp;nbsp;그림&amp;nbsp;1-1에&amp;nbsp;표시된&amp;nbsp;필터&amp;nbsp;시퀀스는&amp;nbsp;이미지에&amp;nbsp;색상&amp;nbsp;효과를&amp;nbsp;적용한&amp;nbsp;다음,&amp;nbsp;광채&amp;nbsp;효과를&amp;nbsp;추가하고,&amp;nbsp;마지막으로&amp;nbsp;결과물에서&amp;nbsp;일부&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;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;Core&amp;nbsp;Image는&amp;nbsp;이와&amp;nbsp;같은&amp;nbsp;필터&amp;nbsp;체인의&amp;nbsp;적용을&amp;nbsp;최적화하여&amp;nbsp;결과를&amp;nbsp;빠르고&amp;nbsp;효율적으로&amp;nbsp;렌더링합니다.&lt;/b&gt;&lt;/span&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체인&amp;nbsp;내&amp;nbsp;각&amp;nbsp;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;CIImage&amp;nbsp;객체는&amp;nbsp;완전히&amp;nbsp;렌더링된&amp;nbsp;이미지가&amp;nbsp;아니라,&amp;nbsp;단순히&amp;nbsp;렌더링을&amp;nbsp;위한&amp;nbsp;&quot;레시피&quot;&lt;/b&gt;&lt;/span&gt;에&amp;nbsp;불과합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Core&amp;nbsp;Image는&amp;nbsp;각&amp;nbsp;필터를&amp;nbsp;개별적으로&amp;nbsp;실행하여&amp;nbsp;볼&amp;nbsp;수&amp;nbsp;없는&amp;nbsp;중간&amp;nbsp;픽셀&amp;nbsp;버퍼를&amp;nbsp;렌더링하는&amp;nbsp;데&amp;nbsp;시간과&amp;nbsp;메모리를&amp;nbsp;낭비할&amp;nbsp;필요가&amp;nbsp;없습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신&amp;nbsp;Core&amp;nbsp;Image는&amp;nbsp;필터를&amp;nbsp;단일&amp;nbsp;작업으로&amp;nbsp;결합하며,&amp;nbsp;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;다른&amp;nbsp;순서로&amp;nbsp;적용해도&amp;nbsp;동일한&amp;nbsp;결과를&amp;nbsp;더&amp;nbsp;효율적으로&amp;nbsp;생성할&amp;nbsp;수&amp;nbsp;있다면&amp;nbsp;필터의&amp;nbsp;순서를&amp;nbsp;재구성할&amp;nbsp;수도&amp;nbsp;있습니다&lt;/b&gt;&lt;/span&gt;.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림&amp;nbsp;1-2는&amp;nbsp;그림&amp;nbsp;1-1의&amp;nbsp;예시&amp;nbsp;필터&amp;nbsp;체인을&amp;nbsp;더&amp;nbsp;정확하게&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;1-2에서&amp;nbsp;크롭&amp;nbsp;작업이&amp;nbsp;마지막에서&amp;nbsp;첫&amp;nbsp;번째로&amp;nbsp;이동했음을&amp;nbsp;확인하십시오.&amp;nbsp;이&amp;nbsp;필터는&amp;nbsp;원본&amp;nbsp;이미지의&amp;nbsp;넓은&amp;nbsp;영역이&amp;nbsp;최종&amp;nbsp;출력에서&amp;nbsp;잘려나가는&amp;nbsp;결과를&amp;nbsp;초래합니다.&amp;nbsp;따라서&amp;nbsp;해당&amp;nbsp;픽셀에는&amp;nbsp;색상&amp;nbsp;및&amp;nbsp;선명도&amp;nbsp;필터를&amp;nbsp;적용할&amp;nbsp;필요가&amp;nbsp;없습니다.&amp;nbsp;크롭&amp;nbsp;작업을&amp;nbsp;먼저&amp;nbsp;수행함으로써&amp;nbsp;Core&amp;nbsp;Image는&amp;nbsp;비용이&amp;nbsp;많이&amp;nbsp;드는&amp;nbsp;이미지&amp;nbsp;처리&amp;nbsp;작업이&amp;nbsp;최종&amp;nbsp;출력에&amp;nbsp;표시될&amp;nbsp;픽셀에만&amp;nbsp;적용되도록&amp;nbsp;보장합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든영역을 필터처리하는 연산이 비싸기때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크롭을 먼저 적용하고 필터처리해주는 작업을 개발자가 신경쓰지않아도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;비용이 적은방향으로 자동으로 최적화가 이뤄진다고합니다&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 영역에 필터를 적용해보는 연습을위해서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블러필터를 구현해보고&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지의 반만 크롭해서 적용후에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교를위해서 가운데에 선을 그려보는 구현까지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작해보겠습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1769496942643&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    
    private func applyBlur(image: UIImage) -&amp;gt; UIImage {
        /// 기기 사이즈에 대응하는 이미지 가로길이
        let imageWidth: CGFloat = UIDevice.screenWidth
        /// 기기 사이즈에 대응하는 이미지 세로길이
        let imageHeight: CGFloat = imageWidth * image.size.height / image.size.width
        /// 이미지뷰의 사이즈
        let imageViewSize: CGSize = .init(width: imageWidth, height: imageHeight)
        /// 이미지 해상도
        let imageSize: CGSize = image.size
        let render = UIGraphicsImageRenderer(size: .init(width: imageWidth, height: imageHeight))
        
        let lineWidth: CGFloat = 3
        let lineColor = CGColor(red: 1, green: 0, blue: 0, alpha: 0.6)
        
        let newImage = render.image { context in
            image.draw(in: .init(origin: .zero, size: imageViewSize))
            /// 가로 반만 필터적용
            let halfWidthPixelRect: CGRect = .init(
                origin: .zero,
                size: .init(width: imageSize.width/2, height: imageSize.height)
            )
            let cropImage = blurWithCrop(
                image: image,
                cropPixel: halfWidthPixelRect
            )
            cropImage.draw(
                in: .init(
                    origin: .zero,
                    size: .init(width: imageWidth/2, height: imageHeight)
                )
            )
            
            // 필터적용 구분선
            context.cgContext.setStrokeColor(lineColor)
            context.cgContext.setLineWidth(lineWidth)
            context.cgContext.setLineDash(phase: 1, lengths: [10,10])
            context.cgContext.move(to: .init(x: imageWidth / 2, y: 0))
            context.cgContext.addLine(to: .init(x: imageWidth / 2, y: imageHeight))
            context.cgContext.strokePath()
        }
        
        return newImage
    }
    
    private func blurWithCrop(image: UIImage, cropPixel cropRect: CGRect) -&amp;gt; UIImage {
        if let cropImage = image.cgImage?.cropping(to: cropRect) {
            let blurImage = filterFactory.applyBlur(originImage: CIImage(cgImage: cropImage))
            return blurImage
        }
        return image
    }&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1769497017584&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ImageFilter.swift 

final class ImageFilter {
    let ciContext: CIContext = .init()
    
    func applyBlur(originImage: CIImage) -&amp;gt; UIImage {
        let blurFilter = CIFilter.boxBlur()
        blurFilter.inputImage = originImage
        blurFilter.radius = 10
        
        if let ciImage = blurFilter.outputImage,
           let cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent) {
            return UIImage(cgImage: cgImage)
        }
        
        return UIImage(ciImage: originImage)
    }
}&lt;/code&gt;&lt;/pre&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/djyhhI/dJMcabXjAYm/I3fLplwfqTYBLxZQwlxGL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djyhhI/dJMcabXjAYm/I3fLplwfqTYBLxZQwlxGL0/img.png&quot; data-origin-width=&quot;433&quot; data-origin-height=&quot;880&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djyhhI/dJMcabXjAYm/I3fLplwfqTYBLxZQwlxGL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdjyhhI%2FdJMcabXjAYm%2FI3fLplwfqTYBLxZQwlxGL0%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;433&quot; height=&quot;880&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCyhyA/dJMb99LW3yI/HsE8VjCyp2LzRZnCFJ7qX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCyhyA/dJMb99LW3yI/HsE8VjCyp2LzRZnCFJ7qX1/img.png&quot; data-origin-width=&quot;433&quot; data-origin-height=&quot;880&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCyhyA/dJMb99LW3yI/HsE8VjCyp2LzRZnCFJ7qX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCyhyA%2FdJMb99LW3yI%2FHsE8VjCyp2LzRZnCFJ7qX1%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;433&quot; height=&quot;880&quot;/&gt;&lt;/span&gt;&lt;/div&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;이번엔 모자이크필터와 색상필터를 구현해서 적용해볼게요&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/MAK8n/dJMcab34RoP/kUgnmMtNCRsyD4DkXgYQFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MAK8n/dJMcab34RoP/kUgnmMtNCRsyD4DkXgYQFK/img.png&quot; data-origin-width=&quot;433&quot; data-origin-height=&quot;880&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MAK8n/dJMcab34RoP/kUgnmMtNCRsyD4DkXgYQFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMAK8n%2FdJMcab34RoP%2FkUgnmMtNCRsyD4DkXgYQFK%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;433&quot; height=&quot;880&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ul0NR/dJMcab34RwD/T23NfuDWkbeCx6CFr1nrv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ul0NR/dJMcab34RwD/T23NfuDWkbeCx6CFr1nrv1/img.png&quot; data-origin-width=&quot;433&quot; data-origin-height=&quot;880&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;50&quot; style=&quot;width: 49.4186%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ul0NR/dJMcab34RwD/T23NfuDWkbeCx6CFr1nrv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ful0NR%2FdJMcab34RwD%2FT23NfuDWkbeCx6CFr1nrv1%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;433&quot; height=&quot;880&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;pre id=&quot;code_1769499612464&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ViewModel.swift

@Observable
final class ViewModelOnlyFilter {
    enum FilterOption {
        case sepia
        case mosaic
        case blur
    }
    
    var maskImages: [UIImage] = []
    private var images: [UIImage] = []
   
    private let filterFactory = ImageFilter()
    
    init() {
        setupImage()
        maskImages = images.map {
            // 원하는 필터적용부분
            applyFilter(image: $0, filter: .blur)
        }
    }
    
    private func setupImage() {
        self.images = [
            UIImage(resource: ._8),
            UIImage(resource: ._10),
            UIImage(resource: .이미지6),
            UIImage(resource: .이미지1),
            UIImage(resource: .이미지2),
        ]
    }
    
    private func applyFilter(image: UIImage, filter: FilterOption) -&amp;gt; UIImage {
        /// 기기 사이즈에 대응하는 이미지 가로길이
        let imageWidth: CGFloat = UIDevice.screenWidth
        /// 기기 사이즈에 대응하는 이미지 세로길이
        let imageHeight: CGFloat = imageWidth * image.size.height / image.size.width
        /// 이미지뷰의 사이즈
        let imageViewSize: CGSize = .init(width: imageWidth, height: imageHeight)
        /// 이미지 해상도
        let imageSize: CGSize = image.size
        let render = UIGraphicsImageRenderer(size: .init(width: imageWidth, height: imageHeight))
        
        let lineWidth: CGFloat = 3
        let lineColor = CGColor(red: 1, green: 0, blue: 0, alpha: 0.6)
        
        /// 가로 반만 필터적용
        let halfWidthPixelRect: CGRect = .init(
            origin: .zero,
            size: .init(width: imageSize.width/2, height: imageSize.height)
        )
        let cropImage: UIImage = applyFilterWithCrop(
            image: image,
            cropPixel: halfWidthPixelRect,
            filter: filter
        )
        let newImage = render.image { context in
            image.draw(in: .init(origin: .zero, size: imageViewSize))
            cropImage.draw(
                in: .init(
                    origin: .zero,
                    size: .init(width: imageWidth/2, height: imageHeight)
                )
            )
            // 중앙 세로선 그리기
            context.cgContext.setStrokeColor(lineColor)
            context.cgContext.setLineWidth(lineWidth)
            context.cgContext.setLineDash(phase: 1, lengths: [10,10])
            context.cgContext.move(to: .init(x: imageWidth / 2, y: 0))
            context.cgContext.addLine(to: .init(x: imageWidth / 2, y: imageHeight))
            context.cgContext.strokePath()
        }
        
        return newImage
    }
    
    private func applyFilterWithCrop(
        image: UIImage,
        cropPixel cropRect: CGRect,
        filter: FilterOption
    ) -&amp;gt; UIImage {
        /// 필요영역만 크롭후 필터적용해서 그려주는 방법
        if let cropImage = image.cgImage?.cropping(to: cropRect) {
            /**
             UIKit, CoreImage 좌표다름 주의
             cgContext이용
             해상도는 원본이미지 Pixel사이즈기준으로 크롭해야해서 해상도 변경전 rect으로해야함 주의
             그릴땐 해상도 변경후 테두리좌표에 그려야함 주의
             */
            switch filter {
            case .sepia:
                return filterFactory.applySepiaTone(image: CIImage(cgImage: cropImage))
            case .mosaic:
                return filterFactory.applyPixelate(image: CIImage(cgImage: cropImage))
            case .blur:
                return filterFactory.applyBlur(image: CIImage(cgImage: cropImage))
            }
        }
        return image
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1769499700058&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ImageFilter.swift

final class ImageFilter {
    let ciContext: CIContext = .init()
    
    func applySepiaTone(image: CIImage) -&amp;gt; UIImage {
        let filter = CIFilter.sepiaTone()
        filter.inputImage = image
        filter.intensity = 0.8
        
        if let ciImage = filter.outputImage,
           let cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent) {
            return UIImage(cgImage: cgImage)
        }
        
        return UIImage(ciImage: image)
    }
    
    func applyPixelate(image: CIImage) -&amp;gt; UIImage {
        let pixelateFilter = CIFilter.pixellate()
        pixelateFilter.inputImage = image
        pixelateFilter.scale = 7
        
        if let ciImage = pixelateFilter.outputImage,
           let cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent) {
            return UIImage(cgImage: cgImage)
        }
        
        return UIImage(ciImage: image)
    }
    
    func applyBlur(image: CIImage) -&amp;gt; UIImage {
        let blurFilter = CIFilter.boxBlur()
        blurFilter.inputImage = image
        blurFilter.radius = 10
        
        if let ciImage = blurFilter.outputImage,
           let cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent) {
            return UIImage(cgImage: cgImage)
        }
        
        return UIImage(ciImage: image)
    }
}&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;</description>
      <category>iyOmSd/Title: Swift</category>
      <author>냄수</author>
      <guid isPermaLink="true">https://nsios.tistory.com/254</guid>
      <comments>https://nsios.tistory.com/254#entry254comment</comments>
      <pubDate>Tue, 27 Jan 2026 22:46:12 +0900</pubDate>
    </item>
    <item>
      <title>2025 회고</title>
      <link>https://nsios.tistory.com/253</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;25년 한 해를 돌아보면서 정리하는 시간을 가져보려합니다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;# 월별 기록&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;1월&lt;/b&gt;&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;일이 적적한 시즌이라 스스로 일을 만들어내면서 했어요&lt;/span&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;앱테스트시 컨트롤쉽도록 개발했어요&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;&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;웨이팅을 몇시간전에 미리걸어두고 갔던 기억이나네요&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;&quot;&gt;&lt;b&gt;2월&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;CRM도구를 연결하는 일을했어요&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;&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;color: #333333;&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;color: #333333;&quot;&gt;&lt;b&gt;3월&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;광고 SDK를 전체적으로 변경하는 일을했어요&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;&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;이 나이에..? 1달에 엠티를 2번이나 가게됬네요&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;&quot;&gt;WWDC 스터디와 모듈화 스터디를 진행했어요&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;&quot;&gt;&lt;b&gt;4월&lt;/b&gt;&lt;/span&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;&amp;nbsp;&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;전회사보다 검진종목이 많이없어서 아쉬웠어요&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;&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;color: #333333;&quot;&gt;&lt;b&gt;5월&lt;/b&gt;&lt;/span&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;정말 제니 - like JENNIE 춤을 배우고 추게됬어요&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;&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;본격적으로 뭔가 재밌고 흥미가 붙어서 자주하게 됬어요&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;&quot;&gt;&lt;b&gt;6월&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;광고 이슈와 crash 이슈들을 해결했어요&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;&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;color: #333333;&quot;&gt;WWDC25가 있던 달이에요 영상들을 챙겨보기 시작했어요&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;&quot;&gt;VisionOCR을 학습하면서 데모앱을 만들었어요&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;&quot;&gt;&lt;b&gt;7월&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;WWDC가 나오고 새버전이 올라갈 예정임에따라서 미니멈타겟 상향을 고려하고&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;변경시 대응해야할 이슈들을 체크했어요&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;&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;color: #333333;&quot;&gt;&lt;b&gt;8월&amp;nbsp;&lt;/b&gt;&lt;/span&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;청모를 가기 시작했어요&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;&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;이때 인식이 좀 안좋기도했고 성수기 전이라 그런지 사람이 생각보다 없고 휑 했던거같아요(개꿀이였어요)&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;&quot;&gt;&lt;b&gt;9월&lt;/b&gt;&lt;/span&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;&amp;nbsp;&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;한번쯤 도전해보고싶어서 살면서 처음 가본 페스티벌이였어요&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;&quot;&gt;DeviceActivity에 대해서 학습하면서 적용해볼수 있는 아이디어를 고민했어요&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;클라이밍 첫핑크를 풀었어요(1~11단계중 7단계)&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;&quot;&gt;&lt;b&gt;10월&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;KISA에서 소명서가 날라와서 대응하는 작업을했어요&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;iOS26 디자인 대응을 했어요&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;&quot;&gt;흑백요리사에 나왔던 포노 부오노, 티엔미미 를 가봤어요&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;지출이 조금 크지만 후회없이 맛있게 먹은 좋은 경험이였어요&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;&quot;&gt;&lt;b&gt;11월&lt;/b&gt;&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;크라임씬 방탈출을 했어요 재밌어서 1번 더 예약해서 1달에 2번에나 했어요&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;&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;color: #333333;&quot;&gt;렛스25에 갔다왔어요&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;확실히 컨퍼런스들이 죽어가고있는게 느껴졌어요..ㅠ&lt;/span&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;하지만 적당히 있어서 쾌적한부분도 좋긴해요&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;&quot;&gt;30년인생 처음으로 파마를 해봤어요&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;&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;12월&lt;/b&gt;&lt;/span&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;기획과 디자인 모두 주도적으로 참여한 경험이에요&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;&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;color: #333333;&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;color: #333333;&quot;&gt;매쉬업에서 첫 송년회를 했어요&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;준비를 너무잘해줘서 좋은 추억을 남긴거같아요&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;&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;이케아를 갔어요&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;클라이밍 핑크깬지 3개월만에 첫보라를 풀었어요&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;# 커리어 회고&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;도전적인 일&lt;/b&gt;&lt;/span&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;Core Image의 CIFilter의 기본개념과 적용을 할 수 있게됬어요&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;온디바이스 AI를 적용할 수있는 기술들을 학습했어요&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Family Controls를 학습하고 앱잠금 및 스크린타임 차트를 구현했어요&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;&quot;&gt;&lt;b&gt;아쉬운 일&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;올해도 회사에서 iOS 영향력이 너무 적다고 느껴졌어요&lt;/span&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;일이 줄어들다보니 나태해지는 경우가 있었어요&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;&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;# 인생 회고&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;경제&lt;/b&gt;&lt;/span&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;더 공부하고 직접 임장을 다니면서 미래에 내집을 고르거나 투자안목을 길러보고 싶어요&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;&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;내 돈을 잃지않도록 잘 관리하고 공부해야 할필요성을 많이느꼇어요&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;&lt;/span&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;&amp;nbsp;&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;이를 주식과 비교했을때 맞을때도 아닐때도 있었어요&lt;/span&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;&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;취미&lt;/b&gt;&lt;/span&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;올해초만해도 파랑(5단계)였던거같은데 어느덧 꿀 보라(8단계)라는 걸 해보려고 탐내고있어요&lt;/span&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;지금까지 크게기록을 하지않았는데 내년부터는 기록을 남겨보려고해요&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;&quot;&gt;밴드가 올해에 5곡을 합주했어요&lt;/span&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;유다빈밴드 - 사포닌 같은 너&lt;/span&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;음율 - 피차일반&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;한로로 - 사랑하게 될거야&amp;nbsp;&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;어렵기도했고 쉽기도했는데 예전에 비해서 의욕이 조금 떨어져서 &lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;연습량이 줄어든것을&lt;/span&gt;&amp;nbsp;느꼈어요&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;동기부여라던가 으쌰으쌰 분위기가 없어서 쳐질때도있는데 그래도 열정 있는 사람들이 있어서&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;다시 마음을 고쳐잡고 열심히 해야할 것같아요&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;&quot;&gt;&lt;b&gt;미래&lt;/b&gt;&lt;/span&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;이런 과정을 보면서 회사에 의존된 상태라면&lt;/span&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;개발자를 하다보니 많은 다양한 분야가 있음에도 생각하지 못해서 시야가 좁다는것도 느꼈고&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;# 2026 목표&lt;/span&gt;&lt;/h2&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;'무탈하다'라는 말이 정말 좋게 와닿았어서 인상적이였어요&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;26년을 무탈하게 보내면 좋을 것 같아요&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;&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;color: #333333;&quot;&gt;25년 자산을 정리하고 26년 자산을 비교했을때 최소 10% 불어난 상태로 만들고싶어요&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;&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;color: #333333;&quot;&gt;새로운 일들에 대해서 겁먹지말고 도전해보고 많은 경험을 해보고싶어요&lt;/span&gt;&lt;/p&gt;</description>
      <author>냄수</author>
      <guid isPermaLink="true">https://nsios.tistory.com/253</guid>
      <comments>https://nsios.tistory.com/253#entry253comment</comments>
      <pubDate>Wed, 31 Dec 2025 23:13:59 +0900</pubDate>
    </item>
    <item>
      <title>[Swift] Vision FaceDetect</title>
      <link>https://nsios.tistory.com/252</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;CoreImage의 CIDetector와 Vision의 VNDetectFaceRectanglesRequest를 사용하는 2가지 방법이있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS11부터 도입된 후자를 권장하고 더 강력한 기능을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이번엔 VNDetectFaceRectanglesRequest를 이용한 코딩을 할예정입니다.&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;VNDetectFaceRectanglesRequest란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지에서 얼굴을 찾도록 요청하는 타입입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vision을 쓰다보면 000Reqeust형식을 많이보게됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 요청중 하나입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1764387035329&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final class FaceDetector {
    func detectFaceRectangles(image: UIImage) throws -&amp;gt; [VNFaceObservation]? {
        guard let image = image.cgImage else { return nil }
        let request = VNDetectFaceRectanglesRequest()
        
        request.revision = VNDetectFaceRectanglesRequestRevision3
        
        let handler = VNImageRequestHandler(cgImage: image)
        try handler.perform([request])
        let results = request.results
        return results
    }
}&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과값으로 VNFaceObservation을 반환받습니다.&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;VNFaceObservation란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지에서 감지하는 얼굴 또는 얼굴 특징 정보입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1764387148287&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let observation = try detector.detectFaceRectangles(image: image)
var boundingBoxes: [CGRect] = []
observation?.forEach {
    print(&quot;description&quot;, $0.description)
    print(&quot;boundingBox&quot;, $0.boundingBox)
    print(&quot;faceCaptureQuality&quot;, $0.faceCaptureQuality)
    print(&quot;confidence&quot;, $0.confidence)
    print(&quot;landmarks&quot;, $0.landmarks)
    print(&quot;pitch&quot;, $0.pitch)
    print(&quot;roll&quot;, $0.roll)
    print(&quot;timeRange&quot;, $0.timeRange)
    print(&quot;uuid&quot;, $0.uuid)
    print(&quot;yaw&quot;, $0.yaw)
    
    boundingBoxes.append($0.boundingBox)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1764387164488&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;description &amp;lt;VNFaceObservation: 0x15ceac800&amp;gt; 484BBF54-1E67-404A-B45B-20BB5A7257BF VNDetectFaceRectanglesRequestRevision3 confidence=0.683225 boundingBox=[0.613351, 0.484306, 0.140112, 0.270106]

boundingBox (0.6133507490158081, 0.4843055307865143, 0.14011242985725403, 0.27010563015937805)

faceCaptureQuality nil

confidence 0.68322533

landmarks nil

pitch Optional(-0.317534)

roll Optional(1.027767)

timeRange CMTimeRange(start: __C.CMTime(value: 0, timescale: 1, flags: __C.CMTimeFlags(rawValue: 1), epoch: 0), duration: __C.CMTime(value: 0, timescale: 1, flags: __C.CMTimeFlags(rawValue: 1), epoch: 0))

uuid 484BBF54-1E67-404A-B45B-20BB5A7257BF

yaw Optional(-0.638136)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;description&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;b&gt;boundingBox&lt;/b&gt; - 감지된 얼굴의 위치와 크기를 나타내는 정규화된 좌표, 얼굴영역표시에 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CGRect좌표값으로 이미지의 상대적인 위치를 나타냄&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;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Vision은 왼쪽하단이 원점(0,0)이고 UIKit/SwiftUI는 왼쪽상단이 원점이므로 Y축을 뒤집어서 사용&lt;/b&gt;&lt;/span&gt;해야함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;boundingBox가 (0.6, 0.4, 0.1, 0.2)일때 &amp;lt;&amp;lt;&amp;lt; (x위치, y위치, 가로길이, 세로길이)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 가로길이가 100, 세로길이가 100이라면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가로로 0.6지점인 60포인트부터 길이가 0.1인 10에 얼굴이 인식됫다는 것을 알려줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 60~70 포인트에 얼굴이 포함됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세로는 0.4지점 40부터 세로길이 0.2인 20에 얼굴이 인식됬습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 40~60입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정보를 종합하면 (60, 40), (70, 40), (70, 60), (60, 60)&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;525&quot; data-origin-height=&quot;328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HT6Re/dJMcaajrtTk/BDLmZ52lfFDjTDXFrTd291/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HT6Re/dJMcaajrtTk/BDLmZ52lfFDjTDXFrTd291/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HT6Re/dJMcaajrtTk/BDLmZ52lfFDjTDXFrTd291/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHT6Re%2FdJMcaajrtTk%2FBDLmZ52lfFDjTDXFrTd291%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;525&quot; height=&quot;328&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;525&quot; data-origin-height=&quot;328&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이해를 돕기위해 그림을 그려봤어요 ㅎ...(그림에서 Y축 반전은 우선 신경쓰지 마세요 이해용입니다!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoreImage를 이용한 처리에는 좌표를 그대로 써도되서 무관하지만 보통 &lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;CGImage나 View로 그리기때문에 Y축은 각별히 신경&lt;/b&gt;&lt;/span&gt;써야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1764389208035&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    func convert(boundingBox: CGRect, to bounds: CGRect) -&amp;gt; CGRect {
        let imageWidth = bounds.width
        let imageHeight = bounds.height
        
        let frame = CGRect(
            origin: CGPoint(
                x: boundingBox.origin.x * imageWidth,
                y: (1 - boundingBox.maxY) * imageHeight
            ),
            size: .init(
                width: boundingBox.width * imageWidth,
                height: boundingBox.height * imageHeight
            )
        )
        
        return frame
    }
    
    // CIImage용
    func ciImageRect(boundingBox: CGRect, to bounds: CGRect) -&amp;gt; CGRect {
        let imageWidth = bounds.width
        let imageHeight = bounds.height
        
        let frame = CGRect(
            origin: CGPoint(
                x: boundingBox.origin.x * imageWidth,
                y: boundingBox.origin.y * imageHeight
            ),
            size: .init(
                width: boundingBox.width * imageWidth,
                height: boundingBox.height * imageHeight
            )
        )
        
        return frame
    }&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;x축은 비례로 좌표를 변환하고 y축은 뒤집어서 비례로 좌표를 변환했습니다.&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;faceCaptureQuality&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;face capture quality request시 제공&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;confidence&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;landmarks&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;face landmark request시에만 제공됨&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;pitch&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼굴의 세로 회전각 (x축 기준으로 머리를 위아래로 움직일때 각도)&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;roll&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼굴의 회전정도 (z축)&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;timeRange&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼굴인식이 적용되는 비디오구간 정보(이미지는 존재x)&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;yaw&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼굴의 수평방향 회전 (y축, 고개를 좌우로 돌리는각도)&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;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-origin-width=&quot;1179&quot; data-origin-height=&quot;2556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btyoj5/dJMcafLOVLt/rrMklBr15ntwACKxk5tZcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btyoj5/dJMcafLOVLt/rrMklBr15ntwACKxk5tZcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btyoj5/dJMcafLOVLt/rrMklBr15ntwACKxk5tZcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbtyoj5%2FdJMcafLOVLt%2FrrMklBr15ntwACKxk5tZcK%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;367&quot; height=&quot;796&quot; data-origin-width=&quot;1179&quot; data-origin-height=&quot;2556&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>iyOmSd/Title: Swift</category>
      <author>냄수</author>
      <guid isPermaLink="true">https://nsios.tistory.com/252</guid>
      <comments>https://nsios.tistory.com/252#entry252comment</comments>
      <pubDate>Sat, 29 Nov 2025 13:18:19 +0900</pubDate>
    </item>
    <item>
      <title>[Swift] Screen Time API(DeviceActivity)</title>
      <link>https://nsios.tistory.com/251</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 게시물에서 정리하지않은 Screen Time관련 기능중&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DeviceActivity를 정리하려합니다.&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;1. DeviceActivity를 사용해서 특정시간에 코드를 실행시키는 방법과&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DeviceActivity란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이 실행되지않아도 코드를 실행 할 수 있는 방법을 제공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 기기를 모니터링하고 조건 충족시 자동으로 코드실행 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 특정 시간대에 코드실행 가능&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;Device Activity Monitor Extension과 Device Activity Report Extension으로 구분됩니다.&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;Device Activity Monitor Extension&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 기기사용을 모니터링 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 조건시 이벤트를 트리거합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;File -&amp;gt; New -&amp;gt; Target에서&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;150&quot; data-origin-height=&quot;114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CBqlM/dJMcaksDC1K/4m9UPBZSCpoweCjzoK1RIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CBqlM/dJMcaksDC1K/4m9UPBZSCpoweCjzoK1RIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CBqlM/dJMcaksDC1K/4m9UPBZSCpoweCjzoK1RIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCBqlM%2FdJMcaksDC1K%2F4m9UPBZSCpoweCjzoK1RIK%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;150&quot; height=&quot;114&quot; data-origin-width=&quot;150&quot; data-origin-height=&quot;114&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;DeviceActivityMonitor를 준수해서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현할 수 있는 함수는 아래와 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1761547872088&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/// 활동 시작전에 startMonitoring의 warningTime분 전에 알림 (시작 n분전, 종료 n분전)
override func intervalWillStartWarning(for activity: DeviceActivityName)
override func intervalWillEndWarning(for activity: DeviceActivityName)

/// 장치 활동 간격이 시작 (startMonitoring의 intervalStart 시간에 해당시 호출됨)
override func intervalDidStart(for activity: DeviceActivityName) 
/// 장치 활동 간격이 끝 (startMonitoring의 intervalEnd 시간에 해당시 호출됨)
override func intervalDidEnd(for activity: DeviceActivityName)

/// 활동이 지정한 임계값에 도달할 예정일때 호출됨
override func eventWillReachThresholdWarning(_ event: DeviceActivityEvent.Name, activity: DeviceActivityName)
/// 활동이 지정한 임계값에 도달하면 호출됨
override func eventDidReachThreshold(_ event: DeviceActivityEvent.Name, activity: DeviceActivityName)&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;위의 함수를 실행시키기위해선 DeviceActivityCenter 인스턴스를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;deviceActivityCenter.startMonitoring를 호출해서 모니터링을 등록합니다&lt;/p&gt;
&lt;pre id=&quot;code_1761547929767&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let deviceActivityCenter = DeviceActivityCenter()
// 이벤트 이름에 맞는 이벤트 생성
private let events: [DeviceActivityEvent.Name: DeviceActivityEvent] = [
    .encouraged: .init(threshold: DateComponents(minute: 1))
]
var startDate: Date = .now
var startDateComponent: DateComponents { DateComponents(from: startDate) }
var endDate: Date = .now
var endDateComponent: DateComponents { DateComponents(from: endDate) }

func startMonitor() {
    do {
        try deviceActivityCenter.startMonitoring(
            .testName,
            during: .init(
                intervalStart: startDateComponent,
                intervalEnd: endDateComponent,
                repeats: true,
                warningTime: .init(minute: 1) // 1분전
            ),
            events: events
        )
        syncActivityList()
        print(&quot;설정완료&quot;)
    } catch {
        print(error)
    }
}

// MARK: - 이름정의

extension DeviceActivityName {
    static let testName = Self(&quot;testName&quot;)
}

extension DeviceActivityEvent.Name {
    static let encouraged = Self(&quot;encouraged&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 직접 테스트해본결과 start와 end시간 사이에 최소 15분 이상 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;15분내 간격일때 너무 짧다고 에러를 발생시킵니다&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;intervalStart값을 넣기위해 DateComponents타입생성시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Calendar&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;current&lt;span&gt;.&lt;/span&gt;dateComponents&lt;span&gt;([.&lt;/span&gt;year&lt;span&gt;, .&lt;/span&gt;month&lt;span&gt;, .&lt;/span&gt;day&lt;span&gt;, .&lt;/span&gt;hour&lt;span&gt;, .&lt;/span&gt;minute&lt;span&gt;, .&lt;/span&gt;second&lt;span&gt;], &lt;/span&gt;from&lt;span&gt;: &lt;/span&gt;date&lt;span&gt;) 이런식으로&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;년월일시간분초 값이 모두 존재해야 정상적으로 등록되서 이벤트발생합니다&lt;/span&gt;&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;/p&gt;
&lt;pre id=&quot;code_1761549068552&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let authCenter = AuthorizationCenter.shared

func checkAuth() {
    Task {
        do {
            try await authCenter.requestAuthorization(for: .individual)
        } catch {
            print(&quot;Fail: \(error)&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;/p&gt;
&lt;pre id=&quot;code_1761549189568&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var allActivieString: [String] = []

private func syncActivityList() {
    allActivieString = deviceActivityCenter.activities.map { activity in
        &quot;\(activity.rawValue) &quot; + startToEndTimeString(name: activity)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;514&quot; data-origin-height=&quot;1038&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DMGcp/dJMcagqeOem/A96grmEabR7npKSqTknYk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DMGcp/dJMcagqeOem/A96grmEabR7npKSqTknYk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DMGcp/dJMcagqeOem/A96grmEabR7npKSqTknYk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDMGcp%2FdJMcagqeOem%2FA96grmEabR7npKSqTknYk0%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;283&quot; height=&quot;572&quot; data-origin-width=&quot;514&quot; data-origin-height=&quot;1038&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞 게시글에서 정리한 ManagedSetting을 이용한다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정시간에 앱을 차단하는 기능을 구현할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1761549406982&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class DeviceActivityMonitorExtension: DeviceActivityMonitor {
    let store = ManagedSettingsStore()

    /// 장치 활동 간격이 시작되었음을 나타냅니다
    override func intervalDidStart(for activity: DeviceActivityName) {
        super.intervalDidStart(for: activity)
        
        if let selection = loadSelectedApps() {
            print(&quot;앱 목록 로드성공&quot;)
            blockSelectedApps(selection)
        }
    }

    private func loadSelectedApps() -&amp;gt; FamilyActivitySelection? {
        let userDefaults = UserDefaults(suiteName: &quot;group.test.familyControl&quot;)
        guard let data = userDefaults?.data(forKey: &quot;testKey&quot;) else {
            return nil
        }
        
        do {
            let model = try JSONDecoder().decode(AppModel.self, from: data)
            return model.selection
        } catch {
            print(&quot;앱 목록 로드 실패: \(error)&quot;)
            return nil
        }
    }
    
    private func blockSelectedApps(_ selection: FamilyActivitySelection) {
        // 앱 차단 설정
        store.shield.applications = selection.applicationTokens.isEmpty ?
            nil : selection.applicationTokens
        
        // 카테고리 차단 설정
        store.shield.applicationCategories = selection.categoryTokens.isEmpty
        ? nil
        : .specific(selection.categoryTokens)
        
        // 웹 도메인 차단 설정 (필요한 경우)
        store.shield.webDomains = selection.webDomainTokens.isEmpty
        ? nil
        : selection.webDomainTokens
    }
       
}&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;loadSelectedApps()를 통해서 활동을 선택한 앱목록을 가져와서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;blockSelectedApps()함수로 전달해서 선택된 앱들을 차단합니다.&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;deviceActivityCenter&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stopMonitoring&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;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_1761549551061&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    func unblockApps() {
        // 모든 차단 해제
        store.shield.applications = nil
        store.shield.applicationCategories = nil
        store.shield.webDomains = nil
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nil을 넣어주면 해제됩니다&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;h3 data-ke-size=&quot;size23&quot;&gt;Device Activity Report Extension&lt;/h3&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;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;File -&amp;gt; New -&amp;gt; Target에서&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;161&quot; data-origin-height=&quot;119&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boSqpO/dJMcaiBABEB/Sm0VHysDfYfMa0cFwht4yK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boSqpO/dJMcaiBABEB/Sm0VHysDfYfMa0cFwht4yK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boSqpO/dJMcaiBABEB/Sm0VHysDfYfMa0cFwht4yK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboSqpO%2FdJMcaiBABEB%2FSm0VHysDfYfMa0cFwht4yK%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;161&quot; height=&quot;119&quot; data-origin-width=&quot;161&quot; data-origin-height=&quot;119&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;h4 data-ke-size=&quot;size20&quot;&gt;DeviceActivityReport.Context&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DeviceActivity 데이터를 기반으로 그릴 View 유형을 보고서에 알려주는 사용자 지정 가능한 유형입니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1761549735943&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public extension DeviceActivityReport.Context {
    // If your app initializes a DeviceActivityReport with this context, then the system will use
    // your extension's corresponding DeviceActivityReportScene to render the contents of the
    // report.
    static let totalActivity = Self(&quot;Total Activity&quot;)
}&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;이 타입은 어떤 DeviceActivityReportScene타입을 부를것인지 선택하는 이름입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1761549864075&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public protocol DeviceActivityReportScene : AppExtensionScene {

    /// The context of the scene.
    ///
    /// When your app creates a `DeviceActivityReport` with this context, the
    /// system uses this scene to render the report's content.
    var context: DeviceActivityReport.Context { get }
}&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;기본적으로 context를 작성해야하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 정의한 context가 앱타겟에서 불러올 ReportScene이 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1761549888595&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct TotalActivityReport: DeviceActivityReportScene {
    // Define which context your scene will represent.
    // DeviceActivityReport호출시 해당 컨텍스트에 해당하는 Report 불러옴
    let context: DeviceActivityReport.Context = .totalActivity  
    
    // Define the custom configuration and the resulting view for this report.
    let content: ([AppUsage]) -&amp;gt; TotalActivityView&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;여러 Report를 구현할 수도 있으므로 각 범위에 따라서 다르게 선택할 수 있습니다.&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_1761550061994&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct DeviceReportView: View {
    var body: some View {
        VStack {
            Text(&quot;사용량 통계&quot;)
            DeviceActivityReport(context, filter: filter)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DeviceActivityReport 뷰타입에 넣어줄 context가 여기에 해당합니다.&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;span data-token-index=&quot;0&quot;&gt;DeviceActivityResults&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;필터링된 장치 활동 결과의 비동기 시퀀스&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;for await in 으로 요소에 접근가능합니다. &lt;/span&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;DeviceActivityData&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS16+&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;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;주요 속성&amp;nbsp;(Instance Properties)&lt;/span&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;&lt;span&gt;1&lt;/span&gt;. &lt;span&gt;activitySegments&lt;/span&gt;: 사용자의&amp;nbsp;활동을 세그먼트별로 구분한&amp;nbsp;데이터&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;2&lt;/span&gt;. &lt;span&gt;device&lt;/span&gt;: 활동&amp;nbsp;보고서와 연관된 디바이스 정보&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;3&lt;/span&gt;. &lt;span&gt;lastUpdatedDate&lt;/span&gt;: 시스템이&amp;nbsp;이&amp;nbsp;디바이스의&amp;nbsp;데이터를 마지막으로 업데이트한 날짜&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;4&lt;/span&gt;. &lt;span&gt;segmentInterval&lt;/span&gt;: 각&amp;nbsp;활동 세그먼트의&amp;nbsp;간격&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;5&lt;/span&gt;. &lt;span&gt;user&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;span&gt;1&lt;/span&gt;&lt;span&gt;. &lt;/span&gt;DeviceActivityData&lt;span&gt;.&lt;/span&gt;ActivitySegment&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;간격&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;- &lt;span&gt;categories&lt;/span&gt;: 활동 세그먼트&amp;nbsp;동안의 카테고리별&amp;nbsp;디바이스 활동&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span&gt;dateInterval&lt;/span&gt;: 활동 세그먼트의 날짜 간격&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span&gt;firstPickup&lt;/span&gt;: 활동 세그먼트&amp;nbsp;동안&amp;nbsp;사용자가 처음 디바이스를&amp;nbsp;집어든 시간&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span&gt;longestActivity&lt;/span&gt;: 활동 세그먼트 동안 가장&amp;nbsp;긴 활동 세션의 날짜&amp;nbsp;간격&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span&gt;totalActivityDuration&lt;/span&gt;: 활동 세그먼트 동안의&amp;nbsp;총 활동 시간&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;- &lt;/span&gt;totalPickupsWithoutApplicationActivity&lt;span&gt;:&amp;nbsp;앱을 사용하지 않고&amp;nbsp;디바이스를&amp;nbsp;집어든&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;span&gt;2&lt;/span&gt;&lt;span&gt;. &lt;/span&gt;DeviceActivityData&lt;span&gt;.&lt;/span&gt;Device&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;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;span&gt;model&lt;/span&gt;: 디바이스&amp;nbsp;모델 (iPhone, iPad, &lt;span&gt;Mac&lt;/span&gt; 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span&gt;name&lt;/span&gt;: 사용자가 설정한&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;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;. &lt;/span&gt;DeviceActivityData&lt;span&gt;.&lt;/span&gt;User&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;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;span&gt;appleID&lt;/span&gt;: 사용자의&amp;nbsp;Apple &lt;span&gt;ID&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;- &lt;/span&gt;nameComponents&lt;span&gt;: 사용자&amp;nbsp;이름&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span&gt;role&lt;/span&gt;: 사용자&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;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;. &lt;/span&gt;DeviceActivityData&lt;span&gt;.&lt;/span&gt;CategoryActivity&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;앱&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;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span&gt;applications&lt;/span&gt;: 이 카테고리에 기여한 앱 활동&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span&gt;category&lt;/span&gt;: 활동의 카테고리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;- &lt;/span&gt;totalActivityDuration&lt;span&gt;:&amp;nbsp;이 카테고리의&amp;nbsp;총 활동 시간&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span&gt;webDomains&lt;/span&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;pre id=&quot;code_1761550435350&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private func printLog(data: DeviceActivityResults&amp;lt;DeviceActivityData&amp;gt;) async -&amp;gt; [AppUsage] {
    let formatter = DateComponentsFormatter()
    formatter.allowedUnits = [.day, .hour, .minute, .second]
    formatter.unitsStyle = .abbreviated
    formatter.zeroFormattingBehavior = .dropAll
    let thisDevice = UIDevice.current.model
    
    var appUsages: [AppUsage] = []
    for await value in data {
        print(&quot; device.name&quot;,value.device.name) // 연결된 모든 디바이스 (아이폰, 아이패드) NS, NS iPad
        print(&quot; lastUpdatedDate&quot;,formatter.string(for: value.lastUpdatedDate))
        print(&quot; segmentInterval&quot;, value.segmentInterval) // 측정 간격
        print(&quot; name&quot;, value.user.nameComponents) // 사용자이름 ( given: 남수 family: 김)
        print(&quot; user apple id&quot;, value.user.appleID) // apple id
        print(&quot; user role&quot;, value.user.role) // individual
        
        
        for await activity in value.activitySegments {
            print(&quot; activity dateInterval&quot;, activity.dateInterval) // 측정 간격
            print(&quot; activity firstPickup&quot;, activity.firstPickup) // 처음 폰든시간 9/21 오후 11시2분2초
            print(&quot; activity longestActivity&quot;, activity.longestActivity) // 제일긴 활동시간 Date to Date
            print(&quot; activity totalActivityDuration&quot;, activity.totalActivityDuration) // 3072초
            print(&quot; activity totalPickupsWithoutApplicationActivity&quot;, activity.totalPickupsWithoutApplicationActivity)
            
            for await category in activity.categories {
                print(&quot; category.localizedDisplayName&quot;, category.category.localizedDisplayName) // social, Other, 큰카테고리
                print(&quot; category.debugDescription&quot;, category.category.token.debugDescription) // 128 bytes, 토큰 해독불가
                print(&quot; category.totalActivityDuration&quot;, category.totalActivityDuration) // 1668초, 690초

                for await application in category.applications {
                    print(&quot; app localizedDisplayName&quot;, application.application.localizedDisplayName) // instagram, 카카오톡
                    print(&quot; app bundleIdentifier&quot;, application.application.bundleIdentifier) // com.burbn.instagram. com.iwilab.KakaoTalk
                    print(&quot; app numberOfNotifications&quot;, application.numberOfNotifications) // 0, 10
                    print(&quot; app numberOfPickups&quot;, application.numberOfPickups) // 1, 5
                    print(&quot; app totalActivityDuration&quot;, application.totalActivityDuration) // 1228초, 430초
                    let usage = AppUsage(
                        title: application.application.localizedDisplayName?.lowercased() ?? &quot;&quot;,
                        time: Int(application.totalActivityDuration)
                    )
                    appUsages.append(usage)
                }
                
                for await webDomain in category.webDomains {
                    print(&quot;⚫️web domain&quot;, webDomain.webDomain.domain) // weather.naver.com, naver.com
                    print(&quot;⚫️web totalActivityDuration&quot;, webDomain.totalActivityDuration) // 5초
                    print(&quot;⚫️web debugDescription&quot;, webDomain.webDomain.token.debugDescription) // 128bytes
                }
            }
            
        }
    }
    print(&quot;------------------&quot;)
    return appUsages
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1761550562116&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  device.name Optional(&quot;NS&quot;)
  lastUpdatedDate nil
  segmentInterval daily(during: 2025-09-21 15:00:00 +0000 to 2025-09-22 15:00:00 +0000)
  name Optional(givenName: 남수 familyName: 김 )
  user apple id Optional(&quot;ㅁㅁㅁㅁ@naver.com&quot;)
  user role individual
  activity dateInterval 2025-09-21 15:00:00 +0000 to 2025-09-22 15:00:00 +0000
  activity firstPickup Optional(2025-09-21 23:02:02 +0000)
  activity longestActivity Optional(2025-09-22 01:14:31 +0000 to 2025-09-22 01:24:45 +0000)
  activity totalActivityDuration 3072.8046337366104
  activity totalPickupsWithoutApplicationActivity 2
  category.localizedDisplayName Optional(&quot;Social&quot;)
  category.debugDescription Optional(ActivityCategoryToken(data: 128 bytes: &amp;auml;&amp;eacute;&amp;icirc;/&amp;Ntilde;I&amp;copy;&amp;eacute;jMM&amp;ocirc;
 j7;&amp;ordf;$.&amp;egrave;&amp;yacute;&amp;ocirc;`g&amp;oslash;c&amp;Oslash;~v.&amp;iexcl;&amp;Agrave;0&amp;Otilde;&amp;yen;
 &amp;cent;2&amp;macr;&amp;laquo;&amp;ucirc;^
 S&amp;aring;Rg&amp;Acirc;&amp;AElig;^8&amp;pound;&amp;plusmn;j&amp;oacute;&amp;Auml;&amp;laquo;&amp;Acirc;&amp;egrave;,&amp;middot;&amp;Ucirc;&amp;shy;9&amp;pound;:&amp;Aring;&amp;Auml;&amp;auml;&amp;atilde;CV&amp;ordf;&amp;para;&amp;Ugrave;B&amp;ccedil;&amp;acirc;6d&amp;nbsp;&amp;Aring;&quot;g/))
  category.totalActivityDuration 1668.274539232254
  app localizedDisplayName Optional(&quot;Instagram&quot;)
  app bundleIdentifier Optional(&quot;com.burbn.instagram&quot;)
  app bundleIdentifier Optional(&quot;com.burbn.instagram&quot;)
  app numberOfNotifications 0
  app numberOfPickups 1
  app totalActivityDuration 1228.57754611969
  app localizedDisplayName Optional(&quot;카카오톡&quot;)
  app bundleIdentifier Optional(&quot;com.iwilab.KakaoTalk&quot;)
  app bundleIdentifier Optional(&quot;com.iwilab.KakaoTalk&quot;)
  app numberOfNotifications 10
  app numberOfPickups 5
  app totalActivityDuration 430.60449719429016
  category.localizedDisplayName Optional(&quot;Productivity &amp;amp; Finance&quot;)
  category.debugDescription Optional(ActivityCategoryToken(data: 128 bytes: &amp;auml;&amp;eacute;&amp;icirc;/&amp;Ntilde;I&amp;copy;&amp;eacute;jMM&amp;ocirc;
 j7;&amp;ordf;$.&amp;egrave;&amp;yacute;&amp;ocirc;`g&amp;oslash;c&amp;Oslash;~v.&amp;iexcl;&amp;Agrave;0&amp;Otilde;&amp;yen;
 &amp;cent;2&amp;macr;&amp;laquo;&amp;ucirc;^
 S&amp;aring;Rg&amp;Acirc;&amp;AElig;^8&amp;pound;&amp;plusmn;j&amp;oacute;&amp;Auml;&amp;laquo;&amp;Acirc;&amp;egrave;,&amp;middot;&amp;Ucirc;&amp;shy;9&amp;pound;:&amp;Aring;&amp;Auml;&amp;auml;&amp;atilde;CV&amp;ordf;&amp;para;*L&amp;yacute;&amp;igrave;&amp;Oacute;)&amp;ucirc;ZD&amp;sect;9))
  category.totalActivityDuration 417.86487793922424
  app localizedDisplayName Optional(&quot;토스&quot;)
  app bundleIdentifier Optional(&quot;com.vivarepublica.cash&quot;)
  app bundleIdentifier Optional(&quot;com.vivarepublica.cash&quot;)
  app numberOfNotifications 6
  app numberOfPickups 4
  app totalActivityDuration 408.36211693286896
  app localizedDisplayName Optional(&quot;모바일출입카드&quot;)
  app bundleIdentifier Optional(&quot;kr.co.adcaps.mobilecard&quot;)
  app bundleIdentifier Optional(&quot;kr.co.adcaps.mobilecard&quot;)
  app numberOfNotifications 0
  app numberOfPickups 0
  app totalActivityDuration 9.502761006355286
  category.localizedDisplayName Optional(&quot;Entertainment&quot;)
  category.debugDescription Optional(ActivityCategoryToken(data: 128 bytes: &amp;auml;&amp;eacute;&amp;icirc;/&amp;Ntilde;I&amp;copy;&amp;eacute;jMM&amp;ocirc;
 j7;&amp;ordf;$.&amp;egrave;&amp;yacute;&amp;ocirc;`g&amp;oslash;c&amp;Oslash;~v.&amp;iexcl;&amp;Agrave;0&amp;Otilde;&amp;yen;&amp;cent;2&amp;macr;&amp;laquo;&amp;ucirc;^
 S&amp;aring;Rg&amp;Acirc;&amp;AElig;^8&amp;pound;&amp;plusmn;j&amp;oacute;&amp;Auml;&amp;laquo;&amp;Acirc;&amp;egrave;,&amp;middot;&amp;Ucirc;&amp;shy;9&amp;pound;:&amp;Aring;&amp;Auml;&amp;auml;&amp;atilde;CV&amp;ordf;&amp;para;~&amp;sup2;=&amp;euml;&amp;iexcl;&amp;Euml;&amp;Atilde;A(C&amp;Iuml;&amp;Yacute;))
  category.totalActivityDuration 31.47321093082428
  app localizedDisplayName Optional(&quot;네이버 웹툰&quot;)
  app bundleIdentifier Optional(&quot;com.nhncorp.NaverWebtoon&quot;)
  app bundleIdentifier Optional(&quot;com.nhncorp.NaverWebtoon&quot;)
  app numberOfNotifications 0
  app numberOfPickups 0
  app totalActivityDuration 22.481863021850586
  app localizedDisplayName Optional(&quot;YouTube&quot;)
  app bundleIdentifier Optional(&quot;com.google.ios.youtube&quot;)
  app bundleIdentifier Optional(&quot;com.google.ios.youtube&quot;)
  app numberOfNotifications 1
  app numberOfPickups 0
  app totalActivityDuration 2.891474962234497
 ⚫️web domain Optional(&quot;weather.naver.com&quot;)
 ⚫️web totalActivityDuration 5.285457968711853
 ⚫️web debugDescription Optional(WebDomainToken(data: 128 bytes: &amp;auml;&amp;eacute;&amp;icirc;/&amp;Ntilde;I&amp;copy;&amp;eacute;jMM&amp;ocirc;
 j7;&amp;ordf;$.&amp;uuml;&amp;euml;0&amp;uuml;bw&amp;szlig;cg&amp;amp;&amp;icirc;&amp;Oslash;e&amp;frac12;&amp;acute;&amp;Ucirc;&amp;eth;J&amp;reg;&amp;ucirc;&amp;frac34;&amp;egrave;
 P
 q&amp;Acirc;&amp;cent;&amp;plusmn;&amp;iuml;w&amp;acute;7&amp;Igrave;&amp;AElig;&amp;oacute;&amp;Auml;&amp;laquo;&amp;Acirc;&amp;egrave;,&amp;middot;&amp;Ucirc;&amp;shy;9&amp;pound;:&amp;Aring;&amp;Auml;&amp;auml;&amp;atilde;CV&amp;ordf;&amp;para;&amp;divide;&amp;auml;]r    &amp;amp;vm&amp;frac14;&amp;Ograve;))
 ⚫️web domain Optional(&quot;naver.com&quot;)
 ⚫️web totalActivityDuration 0.8144149780273438
 ⚫️web debugDescription Optional(WebDomainToken(data: 128 bytes: &amp;auml;&amp;eacute;&amp;icirc;/&amp;Ntilde;I&amp;copy;&amp;eacute;jMM&amp;ocirc;
 j7;&amp;ordf;$.&amp;uuml;&amp;euml;0&amp;uuml;bw&amp;szlig;cg&amp;amp;&amp;icirc;&amp;Oslash;|&amp;sup1;&amp;pound;&amp;Aacute;&amp;raquo;[&amp;iuml;&amp;lt;&amp;eth;&amp;divide;&amp;cedil;JY&amp;euml;:&amp;Acirc;&amp;egrave;&amp;ograve;&amp;AElig;^8&amp;pound;&amp;plusmn;j&amp;oacute;&amp;Auml;&amp;laquo;&amp;Acirc;&amp;egrave;,&amp;middot;&amp;Ucirc;&amp;shy;9&amp;pound;:&amp;Aring;&amp;Auml;&amp;auml;&amp;atilde;CV&amp;ordf;&amp;para;0?&amp;Atilde;3&amp;atilde;(&amp;yacute;&amp;auml;R0))
  device.name Optional(&quot;NS iPad&quot;)
  lastUpdatedDate nil
  segmentInterval daily(during: 2025-09-21 15:00:00 +0000 to 2025-09-22 15:00:00 +0000)
  name Optional(givenName: 남수 familyName: 김 )
  user apple id Optional(&quot;ㅁㅁㅁㅁ@naver.com&quot;)
  user role individual&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;/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;h3 data-ke-size=&quot;size23&quot;&gt;차트구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Scene타입의 content에 사용할 타입을 먼저 정의할겁니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 타이틀과 시간을 저장할 구조체도 같이 정의합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1761551045141&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let content: (String) -&amp;gt; TotalActivityView // 기본 생성시 

let content: ([AppUsage]) -&amp;gt; TotalActivityView // &amp;lt;&amp;lt; 수정


struct AppUsage {
    let title: String
    let time: Int
}&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;이제 앱타겟에서 사용할 뷰를 정의합니다&lt;/p&gt;
&lt;pre id=&quot;code_1761551645677&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct DeviceReportView: View {
    @State private var context: DeviceActivityReport.Context = .totalActivity
    @State private var filter: DeviceActivityFilter = .init(
        segment: .daily(
            during: .init(
                start: .now.addingTimeInterval(-3600),
                end: .now.addingTimeInterval(3600)
            )
        ),
        devices: .all
    )
    var body: some View {
        VStack {
            Text(&quot;사용량 통계&quot;)
            DeviceActivityReport(context, filter: filter)
        }
    }
}&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그부분은 다제거하고 필요한 코드만 작성했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1761551742136&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct TotalActivityReport: DeviceActivityReportScene {
    // Define which context your scene will represent.
    let context: DeviceActivityReport.Context = .totalActivity  // DeviceActivityReport호출시 해당 컨텍스트에 해당하는 Report 불러옴
    
    // Define the custom configuration and the resulting view for this report.
    let content: ([AppUsage]) -&amp;gt; TotalActivityView
    
    func makeConfiguration(representing data: DeviceActivityResults&amp;lt;DeviceActivityData&amp;gt;) async -&amp;gt; [AppUsage] {
        // Reformat the data into a configuration that can be used to create
        // the report's view.
        let formatter = DateComponentsFormatter()
        formatter.allowedUnits = [.day, .hour, .minute, .second]
        formatter.unitsStyle = .abbreviated
        formatter.zeroFormattingBehavior = .dropAll
        
        return await printLog(data: data)
    }
    
    func containsIPhone(_ name: String) -&amp;gt; Bool {
      return name.range(of: &quot;iPhone&quot;, options: .caseInsensitive) != nil
    }
    
    private func printLog(data: DeviceActivityResults&amp;lt;DeviceActivityData&amp;gt;) async -&amp;gt; [AppUsage] {
        let formatter = DateComponentsFormatter()
        formatter.allowedUnits = [.day, .hour, .minute, .second]
        formatter.unitsStyle = .abbreviated
        formatter.zeroFormattingBehavior = .dropAll
        let thisDevice = UIDevice.current.model
        
        var appUsages: [AppUsage] = []
        for await value in data {
            for await activity in value.activitySegments {
                for await category in activity.categories {
                    for await application in category.applications {
                        let usage = AppUsage(
                            title: application.application.localizedDisplayName?.lowercased() ?? &quot;&quot;,
                            time: Int(application.totalActivityDuration)
                        )
                        appUsages.append(usage)
                    }
                }
                
            }
        }

        return appUsages
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4중 for문이라니 ,,, (가능하다면 flatMap으로 배열 평탄화 가공하셔도 좋을거같네요)&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_1761551857942&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;
import SwiftUI
import Charts

struct AppUsage {
    let title: String
    let time: Int
}

struct TotalActivityView: View {
    let totalActivity: String
    let total: Int
    let data: [AppUsage]
    @State private var selectedAngle: Int?
    private let dataRange: [Range&amp;lt;Int&amp;gt;]
    // 선택한 차트 각도에 해당하는 선택된 아이템반환
    private var selectedItem: AppUsage? {
        guard let selectedAngle else { return nil }
        if let selectedIndex = dataRange.firstIndex(where: { range in
            range.contains(selectedAngle)
        }) {
            return data[selectedIndex]
        }
        return nil
    }
    
    init(data: [AppUsage]) {
        self.data = data
        var total: Int = 0
        self.dataRange = data.map {
            let newTotal = total + $0.time
            let result = total..&amp;lt;newTotal
            total = newTotal
            return result
       }
        self.totalActivity = &quot;\(total)s&quot;
        self.total = total
    }
    
    var body: some View {
        Text(&quot;전체 \(totalActivity)&quot;)
        
        Chart(data, id: \.title) { value in
            SectorMark(
                angle: .value(&quot;appName&quot;, value.time),
                innerRadius: .ratio(0.618), // 황금비율 (안쪽원)
                outerRadius: .ratio(1), // 전체원의 크기
                angularInset: 3 // 차트 데이터간 간격
            )
            .cornerRadius(10)
            .opacity(selectedItem?.title == value.title ? 1 : 0.5) // 선택된아이템 강조
            .foregroundStyle(by: .value(&quot;color&quot;, value.title))
        }
        .chartLegend(.visible) // 범주 노출
        .chartAngleSelection(value: $selectedAngle) // 선택된 각도 바인딩 (Double 혹은 Int 타입만 가능)
        .chartBackground { proxy in
            GeometryReader { geometry in
                if let plotFrame = proxy.plotFrame { // 차트가 그려진프레임
                    let frame = geometry[plotFrame] // Ahchor&amp;lt;CGRect&amp;gt; -&amp;gt; CGRect 형변환
                    VStack {
                        Text(&quot;사용량&quot;)
                        if let selectedItem {
                            Text(selectedItem.title)
                                .bold()
                            Text(&quot;time: \(selectedItem.time)s&quot;)
                            Text(&quot;\(Double(selectedItem.time) / Double(total) * 100)%&quot;)
                        }
                    }
                    .position(x: frame.midX, y: frame.midY) // 차트프레임 좌표 적용시켜야 중앙에뜸
                }
            }
        }
        .scaledToFit()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1179&quot; data-origin-height=&quot;2556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tqpQA/dJMcaj8kR5y/jHR4V5QES69t8W0kgXnTUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tqpQA/dJMcaj8kR5y/jHR4V5QES69t8W0kgXnTUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tqpQA/dJMcaj8kR5y/jHR4V5QES69t8W0kgXnTUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtqpQA%2FdJMcaj8kR5y%2FjHR4V5QES69t8W0kgXnTUk%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;375&quot; height=&quot;813&quot; data-origin-width=&quot;1179&quot; data-origin-height=&quot;2556&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;</description>
      <category>iyOmSd/Title: Swift</category>
      <author>냄수</author>
      <guid isPermaLink="true">https://nsios.tistory.com/251</guid>
      <comments>https://nsios.tistory.com/251#entry251comment</comments>
      <pubDate>Mon, 27 Oct 2025 22:08:18 +0900</pubDate>
    </item>
    <item>
      <title>[Swift] Screen Time API(FamilyControls, ManagedSettings)</title>
      <link>https://nsios.tistory.com/250</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Screen Time API중 Family Controls과 ManagedSettings에 대해서 알아보려고합니다.&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-origin-width=&quot;516&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cS0vqU/btsQQ0BLoD0/EhP5ohqjF5RIDiQfzDw1o1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cS0vqU/btsQQ0BLoD0/EhP5ohqjF5RIDiQfzDw1o1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cS0vqU/btsQQ0BLoD0/EhP5ohqjF5RIDiQfzDw1o1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcS0vqU%2FbtsQQ0BLoD0%2FEhP5ohqjF5RIDiQfzDw1o1%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;516&quot; height=&quot;218&quot; data-origin-width=&quot;516&quot; data-origin-height=&quot;218&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Managed Setting 에서 보호자는 앱을 통해 계정 잠금, 웹 트래픽 필터링, 미디어 접근 제한 등의 작업을 수행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자녀가 기기에서 앱을 실행할 이유가 전혀 없을 수 있으므로, 앱을 실행하지 않고도 기기에서 코드를 실행할 수 있도록 기기 Device Activity 프레임워크를 활용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인정보 보호를 위해 Family Control 프레임워크는 가족 공유를 통해 앱을 승인하기 위해 보호자의 동의를 요구합니다.&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;Managed Setting - 계정잠금, 트래픽 필터링, 앱차단 등 기기사용을 보다 세밀하게 제어가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Family Control - 승인 접근, 앱, 웹 활동제한을 위한 토큰 제공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Device Activity - 앱이 실행되지않아도 코드를 실행시킬 수 있는 방법 제공, 모니터링&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 FamilyControl 프레임워크에서 앱 카테고리 토큰을 받아서&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;br /&gt;구현시 주의사항으로&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;실제 디바이스 비번 설정이 안되있다면&amp;nbsp;&lt;/b&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 style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;authenticationMethodUnavailable 에러를&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;만나볼 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&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;size16&quot;&gt;해결방안으로는 아래와같이 진행하면 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. 설정 &amp;gt; Face ID 및 패스코드 - 패스코드 설정 또는 변경 - Face ID/Touch ID 재설정&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. 설정 &amp;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 data-ke-size=&quot;size23&quot;&gt;Family Controls&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FamilyControl사용을 위해서는&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Capability에서 Family Control 을 먼저 추가한뒤&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드적으로도 사용자 승인을 받아야합니다&lt;/p&gt;
&lt;pre id=&quot;code_1758871492256&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    let authCenter = AuthorizationCenter.shared

    func checkAuth() {
        Task {
            do {
                try await authCenter.requestAuthorization(for: .individual)
            } catch {
                print(&quot;Fail: \(error)&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;FamilyActivitySelection&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.familyActivityPicker 모디파이어를 사용하여 기기내의 앱 리스트를 보여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;차단할 카테고리 세부앱들은 시뮬레이터에서는 확인이 불가능&lt;/span&gt;하여 카테고리 전체만 차단가능하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;실기기에선 각 앱별로 선택하여 차단할 수&lt;/span&gt;도 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1758871866630&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import FamilyControls
                
struct SomeView: View {
    @State var selection = FamilyActivitySelection()
    @State var isPresented = false
    
    var body: some View {
        SomeView2()
            .familyActivityPicker(isPresented: $isPresented, selection: $selection)
            .onChange(of: selection) { old, new in
                let applications = new.applications
                let categories = new.categories
                let webDomains = new.webDomains
                print(applications, categories, webDomains)
            }
    }
}&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-origin-width=&quot;573&quot; data-origin-height=&quot;1163&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kAnhh/btsQQhLgQkT/uaW3550ioimqdUaoLw3lmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kAnhh/btsQQhLgQkT/uaW3550ioimqdUaoLw3lmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kAnhh/btsQQhLgQkT/uaW3550ioimqdUaoLw3lmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkAnhh%2FbtsQQhLgQkT%2FuaW3550ioimqdUaoLw3lmk%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;312&quot; height=&quot;633&quot; data-origin-width=&quot;573&quot; data-origin-height=&quot;1163&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 개인 정보를 보호하기 위해 FamilyActivitySelection은 사용자가 선택한 범주, 애플리케이션 및 웹 도메인을 나타내는 불투명한 값을 보유합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 다음 이러한 불투명한 값을 ManagedSettings 및 DeviceActivity 프레임워크의 인스턴스와 메서드에 전달하여 보호 기능을 설정하고 관리할 수 있습니다.&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;extension에서 사용할 것이라면 appGroup을 이용해서 이값을 저장하고 읽어오면 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;UserDefault에 타입을 바로 저장할수는 없으니 Data형식으로 인코딩해서 저장하고 디코딩해서 읽는 방식을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인코딩 디코딩 하는 예시코드&lt;/p&gt;
&lt;pre id=&quot;code_1758872876408&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct AppModel: Codable {
    init(selection: FamilyActivitySelection) {
        self.selection = selection
    }
    
    let selection: FamilyActivitySelection
}

func setShieldRestrictions() {
    let applications = selectionToDiscourage
    
    do {
        let model = AppModel(selection: applications)
        let appData = try JSONEncoder().encode(model)
        userDefaults.set(appData, forKey: &quot;testKey&quot;)
    } catch {
        print(error)
    }
}
    

func loadSelectedApps() -&amp;gt; FamilyActivitySelection? {
    let userDefaults = UserDefaults(suiteName: &quot;group.test.familyControl&quot;)
    guard let data = userDefaults?.data(forKey: &quot;testKey&quot;) else {
        return nil
    }
    
    do {
        let model = try JSONDecoder().decode(AppModel.self, from: data)
        print(model)
        return model.selection
    } catch {
        print(&quot;앱 목록 로드 실패: \(error)&quot;)
        return nil
    }
}&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 data-ke-size=&quot;size23&quot;&gt;Managed Settings&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ManagedSettingsStore&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 그룹은 관련 설정(예: 기본값, 가능한 최소값 및 최대값)에 대한 관련 데이터를 포함합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FamilyActivitySelection타입으로 선택한값을 ManagedSettingsStore에 전달합니다&lt;/p&gt;
&lt;pre id=&quot;code_1758872139313&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import ManagedSettings

@Observable
final class ManagedModel {
    private let store = ManagedSettingsStore()
    var selectionToDiscourage: FamilyActivitySelection = .init()
    
    func setShieldRestrictions() {
        let applications = selectionToDiscourage
        
        store.shield.applications = applications.applicationTokens.isEmpty ? nil : applications.applicationTokens
        store.shield.applicationCategories = applications.categoryTokens.isEmpty
        ? nil
        : .specific(applications.categoryTokens)
    }
}&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;store.shield(ShieldSettings타입) 을통해서 앱이나 웹사이트를 제한 할 수 있습니다.&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;Shield&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이 차단되는 화면을 커스텀하고싶으면 addTarget에서 아래중 원하는것을 선택하면됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;542&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mlY1x/btsQREeqL6h/0qe01qnkAkTtPvU5NBFfk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mlY1x/btsQREeqL6h/0qe01qnkAkTtPvU5NBFfk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mlY1x/btsQREeqL6h/0qe01qnkAkTtPvU5NBFfk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmlY1x%2FbtsQREeqL6h%2F0qe01qnkAkTtPvU5NBFfk1%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;542&quot; height=&quot;248&quot; data-origin-width=&quot;542&quot; data-origin-height=&quot;248&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;Shield Configuration&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- ShieldConfigurationDataSource&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;차단시 UI설정&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;pre id=&quot;code_1758872401147&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    override func configuration(shielding application: Application) -&amp;gt; ShieldConfiguration {
        // Customize the shield as needed for applications.
        ShieldConfiguration(
            backgroundBlurStyle: .dark,
            backgroundColor: .black,
            icon: nil,
            title: .init(text: &quot;앱 차단⚠️!!&quot;, color: .systemPink),
            subtitle: .init(text: &quot;서브타이틀임&quot;, color: .systemPink),
            primaryButtonLabel: .init(text: &quot;primary버튼&quot;, color: .systemPink),
            primaryButtonBackgroundColor: .black,
            secondaryButtonLabel: .init(text: &quot;secondary버튼&quot;, color: .systemPink)
        )
    }
    
    override func configuration(shielding application: Application, in category: ActivityCategory) -&amp;gt; ShieldConfiguration {
        // Customize the shield as needed for applications shielded because of their category.
        ShieldConfiguration(
            backgroundBlurStyle: .dark,
            backgroundColor: .black,
            icon: nil,
            title: .init(text: &quot;카테고리 차단⚠️!!&quot;, color: .systemPink),
            subtitle: .init(text: &quot;서브타이틀임\(category.localizedDisplayName)&quot;, color: .systemPink),
            primaryButtonLabel: .init(text: &quot;primary버튼&quot;, color: .systemPink),
            primaryButtonBackgroundColor: .black,
            secondaryButtonLabel: .init(text: &quot;secondary버튼&quot;, color: .systemPink)
        )
    }&lt;/code&gt;&lt;/pre&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&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/nU1AS/btsQQX6bvmi/fzoDa4lxMqelYBcPr62tV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nU1AS/btsQQX6bvmi/fzoDa4lxMqelYBcPr62tV0/img.png&quot; data-origin-width=&quot;1179&quot; data-origin-height=&quot;2556&quot; data-is-animation=&quot;false&quot; style=&quot;width: 47.7901%; margin-right: 10px;&quot; data-widthpercent=&quot;48.35&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nU1AS/btsQQX6bvmi/fzoDa4lxMqelYBcPr62tV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnU1AS%2FbtsQQX6bvmi%2FfzoDa4lxMqelYBcPr62tV0%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;1179&quot; height=&quot;2556&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmDZ42/btsQP5cBKNg/7Ph1JXFmLTn5VRNLY0udZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmDZ42/btsQP5cBKNg/7Ph1JXFmLTn5VRNLY0udZk/img.png&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;2330&quot; data-is-animation=&quot;false&quot; style=&quot;width: 51.0471%;&quot; data-widthpercent=&quot;51.65&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmDZ42/btsQP5cBKNg/7Ph1JXFmLTn5VRNLY0udZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdmDZ42%2FbtsQP5cBKNg%2F7Ph1JXFmLTn5VRNLY0udZk%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;1148&quot; height=&quot;2330&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;CGV앱을 제한걸고(시뮬에서 테스트불가능해서 실제폰에서 테스트) CGV앱을 실행시키면 앱 차단이 노출됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이와 다르게 생산성 및 금융이라는 카테고리를 제한시키고 앱을 실행시키면 카테고리 차단이 노출됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;카테고리가 더 상위개념으로&amp;nbsp;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;카테고리를 차단하면 앱차단보다 우선순위로 노출됩니다&lt;/b&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;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;size16&quot;&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;Shield Action Extension&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- ShieldActionDelegate&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;차단시 각 버튼에 따른 이벤트 처리&lt;/p&gt;
&lt;pre id=&quot;code_1758872522577&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    override func handle(action: ShieldAction, for application: ApplicationToken, completionHandler: @escaping (ShieldActionResponse) -&amp;gt; Void) {
        // Handle the action as needed.
        switch action {
        case .primaryButtonPressed:
            completionHandler(.close)
        case .secondaryButtonPressed:
            completionHandler(.defer)
        @unknown default:
            fatalError()
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p 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;size16&quot;&gt;defer은 버튼을 눌러도 아무동작이 없습니다.&amp;nbsp; 참고하세요&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;</description>
      <category>iyOmSd/Title: Swift</category>
      <author>냄수</author>
      <guid isPermaLink="true">https://nsios.tistory.com/250</guid>
      <comments>https://nsios.tistory.com/250#entry250comment</comments>
      <pubDate>Fri, 26 Sep 2025 22:55:44 +0900</pubDate>
    </item>
    <item>
      <title>[Swift] WWDC25 Explore concurrency in SwiftUI</title>
      <link>https://nsios.tistory.com/249</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Swift6.2에서 달라진내용과 앱의 응답성을 효율적으로 만드는 내용입니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Concurrency를 이용해서 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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swift 6.2에는 새 언어 모드가 도입됐습니다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tO6TK/btsQcJsJinL/vRdO20IXrJB4KRek1YHk4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tO6TK/btsQcJsJinL/vRdO20IXrJB4KRek1YHk4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tO6TK/btsQcJsJinL/vRdO20IXrJB4KRek1YHk4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtO6TK%2FbtsQcJsJinL%2FvRdO20IXrJB4KRek1YHk4K%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈의 모든 타입에 암묵적이 @MainActor주석을 표시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;투어에서 배우는 모든 내용은 새 모드와 상관없이 적용됩니다&lt;br /&gt;&lt;br /&gt;투어에는 3가지 명소가 등장합니다(WWDC에서 각 목차를 투어에 비유함)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 액터의 아름다운 메도우에서 시작하여 SwiftUI가 메인 액터를 애플리케이션 컴파일 시점과 런타임 기본값으로 처리하는 방법을 살펴봅니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 다음 동시성 절벽을 방문해 SwiftUI가 메인 스레드에서 작업을 분리하여 앱 UI 지연을 방지하는 방법과 데이터 레이스 버그 방지 방법을 살펴봅니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막에는 캠프에 도착해 동시 코드와 SwiftUI API 간의 관계에 대해 생각해 봅니다&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;Main-actor Meadows&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6WZgX/btsQaQmiPh3/etLTZDE8KRDNOUYBitNIn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6WZgX/btsQaQmiPh3/etLTZDE8KRDNOUYBitNIn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6WZgX/btsQaQmiPh3/etLTZDE8KRDNOUYBitNIn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6WZgX%2FbtsQaQmiPh3%2FetLTZDE8KRDNOUYBitNIn0%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View는 @MainActor에 격리되며 구조체를 View에 맞게 조정함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 ColorExtractorView는 @MainActor로 격리됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 점선은 추론된 격리를 나타냄&lt;br /&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;@MainActor에 격리된 전체 타입은 모든 멤버도 암시적으로 격리됩니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View의 요구 사항을 구현하는 본문 프로퍼티뿐만 아니라&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@State 변수 같은 선언한 다른 멤버도 포함됩니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델의 스키마나 colorCount에 바인딩된 속성 같은 다른 멤버 속성을 참조시 공유된 @MainActor 격리가 액세스 안전을 보장하기 때문에 컴파일러에서 허용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;@MainActor는 SwiftUI의 컴파일 타임 기본값입니다&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;따라서 대부분의 시간을 앱 기능 개발에 집중하고 동시성 문제를 고민할 필요가 없습니다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1WEbx/btsQcI8q25N/E6vrju8d5THOxN5ZFJvki0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1WEbx/btsQcI8q25N/E6vrju8d5THOxN5ZFJvki0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1WEbx/btsQcI8q25N/E6vrju8d5THOxN5ZFJvki0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1WEbx%2FbtsQcI8q25N%2FE6vrju8d5THOxN5ZFJvki0%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;색상 추출 함수는 비동기식이므로 비동기 컨텍스트로 전환하기 위해 Task로 함수를 호출합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰 body가 @MainActor로 격리되기 때문에 이 &lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;Task에 전달한 클로저도 메인 스레드에서 실행&lt;/span&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;[훨씬 더 실용적인 이유]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- AppKit 및 UIKit의 API는 @MainActor에만 격리됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- SwiftUI는 이러한 프레임워크와 원활하게 상호작용함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@MainActor로 주석을 달지 않아도 됩니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI는 API에 @MainActor 주석을 답니다 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;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXQvc7/btsP8NxotvX/CcpF9WJxQKpr3cNl4VE0i1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXQvc7/btsP8NxotvX/CcpF9WJxQKpr3cNl4VE0i1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXQvc7/btsP8NxotvX/CcpF9WJxQKpr3cNl4VE0i1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXQvc7%2FbtsP8NxotvX%2FCcpF9WJxQKpr3cNl4VE0i1%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Concurrency Cliffs&lt;/h3&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-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dN3d3e/btsQaQNnf9D/LhgKgH8TtuQYgbwDaZGTF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dN3d3e/btsQaQNnf9D/LhgKgH8TtuQYgbwDaZGTF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dN3d3e/btsQaQNnf9D/LhgKgH8TtuQYgbwDaZGTF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdN3d3e%2FbtsQaQNnf9D%2FLhgKgH8TtuQYgbwDaZGTF1%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isLoading 속성에 반응하는 scaleEffect를 사용하고 있습니다 &lt;br /&gt;애니메이션 값에는 복잡한 수학이 동반됩니다 &lt;br /&gt;프레임 단위로 계산하면 비용이 많이 들 수 있습니다 &lt;br /&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;SwiftUI는 이 계산을 백그라운드 스레드에서 수행&lt;/b&gt;&lt;/span&gt;하여 메인 스레드가 다른 작업에 더 집중하게 합니다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZVc2o/btsP9MktdJC/xhwwcKHeceW1sbx3ErrLm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZVc2o/btsP9MktdJC/xhwwcKHeceW1sbx3ErrLm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZVc2o/btsP9MktdJC/xhwwcKHeceW1sbx3ErrLm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZVc2o%2FbtsP9MktdJC%2FxhwwcKHeceW1sbx3ErrLm0%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI는 선언형입니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UIView와 달리 View 프로토콜을 준수하는 구조체는 메모리에서 고정된 위치를 차지하는 객체가 아닙니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;런타임에 SwiftUI는 View에 대한 별도의 표현을 만듭니다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Tpqdp/btsQaKl9Hsl/1xSdkyqw0gUr1CkwLSlOCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Tpqdp/btsQaKl9Hsl/1xSdkyqw0gUr1CkwLSlOCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Tpqdp/btsQaKl9Hsl/1xSdkyqw0gUr1CkwLSlOCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTpqdp%2FbtsQaKl9Hsl%2F1xSdkyqw0gUr1CkwLSlOCK%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&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;이 표현은 다양한 최적화 기회를 제공합니다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;중요한 점은 뷰 표현의 일부를 백그라운드 스레드에서 평가하는 것&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI는 사용자를 대신해 많은 계산을 수행할 때 이 기법을 사용함&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;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;Shape&lt;/b&gt;&lt;/span&gt; 프로토콜이 대표적인 예입니다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/waZJz/btsQbZQmJJv/O9J18fnyIsh1Phj1FtJWZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/waZJz/btsQbZQmJJv/O9J18fnyIsh1Phj1FtJWZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/waZJz/btsQbZQmJJv/O9J18fnyIsh1Phj1FtJWZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwaZJz%2FbtsQbZQmJJv%2FO9J18fnyIsh1Phj1FtJWZk%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웨지 모양이 움직이면 작성한 path 메서드는 백그라운드 스레드에서 호출을 받습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰 수정자 &lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;visualEffect&lt;/b&gt;&lt;/span&gt;는 대상 뷰(즉 텍스트)에 적용될 효과를 정의하는 클로저를 허용합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시각 효과는 화려하지만 렌더링 비용은 비쌉니다 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;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rXshl/btsQa5creS3/KHmWAUVQlWKL7kHouIni5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rXshl/btsQa5creS3/KHmWAUVQlWKL7kHouIni5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rXshl/btsQa5creS3/KHmWAUVQlWKL7kHouIni5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrXshl%2FbtsQa5creS3%2FKHmWAUVQlWKL7kHouIni5K%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;Layout&lt;/span&gt;&lt;/b&gt; 프로토콜은 메인 스레드 에서 요구 사항 메서드를 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;visualEffect처럼 &lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;onGeometryChange&lt;/b&gt;&lt;/span&gt;의 최초 인수는 백그라운드 스레드에서도 호출될 수 있는 클로저입니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI는 Sendable 주석을 사용하여 이 런타임 동작 또는 의미론을 컴파일러와 사용자에게 전달합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서도 SwiftUI의 동시성 주석은 런타임 의미론을 표현합니다&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-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dZRr6L/btsQckzXBqF/ALal3Ikh1OhFmlQpKEBAk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dZRr6L/btsQckzXBqF/ALal3Ikh1OhFmlQpKEBAk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dZRr6L/btsQckzXBqF/ALal3Ikh1OhFmlQpKEBAk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdZRr6L%2FbtsQckzXBqF%2FALal3Ikh1OhFmlQpKEBAk0%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sendable 키워드는 @MainActor에서 데이터를 공유할 때 발생하는 데이터 레이스 조건을 알려 줍니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sendable은 절벽길 경고 표지판과 같습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swift는 코드 내의 레이스 조건을 안정적으로 탐지하고 컴파일러 오류로 이를 알립니다&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;/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-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceRPdP/btsQbw8M8y3/wDSYFSSodjGvgYhutB4kUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceRPdP/btsQbw8M8y3/wDSYFSSodjGvgYhutB4kUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceRPdP/btsQbw8M8y3/wDSYFSSodjGvgYhutB4kUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceRPdP%2FbtsQbw8M8y3%2FwDSYFSSodjGvgYhutB4kUk%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SchemeContentView에서 상태 pulse 가 이 visualEffect에 필요함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swift는 데이터 레이스 조건이 있다고 에러뜸&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pulse 변수는 self.pulse의 약자입니다 @MainActor 격리 변수를 전송 가능 클로저에서 공유할 때 자주 발생하는 상황임&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Self는 뷰입니다 메인 액터에 격리되어 있습니다&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/bTWCQf/btsP99zU2Ew/dZJttJbFi3yJ2ENKMApI30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTWCQf/btsP99zU2Ew/dZJttJbFi3yJ2ENKMApI30/img.png&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTWCQf/btsP99zU2Ew/dZJttJbFi3yJ2ENKMApI30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTWCQf%2FbtsP99zU2Ew%2FdZJttJbFi3yJ2ENKMApI30%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;1184&quot; height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxrzGs/btsP9DOKVmg/MLLEKbVxkaGHUWUe01Zhj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxrzGs/btsP9DOKVmg/MLLEKbVxkaGHUWUe01Zhj0/img.png&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxrzGs/btsP9DOKVmg/MLLEKbVxkaGHUWUe01Zhj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxrzGs%2FbtsP9DOKVmg%2FMLLEKbVxkaGHUWUe01Zhj0%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;1184&quot; height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 목표는 전송 가능 클로저에서 pulse 변수에 접근하는 것입니다 이렇게 하려면 두 가지 일이 일어나야 합니다&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ltOpH/btsQbx7F2G7/jWjy4kthSO34uorgzWV7ZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ltOpH/btsQbx7F2G7/jWjy4kthSO34uorgzWV7ZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ltOpH/btsQbx7F2G7/jWjy4kthSO34uorgzWV7ZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FltOpH%2FbtsQbx7F2G7%2FjWjy4kthSO34uorgzWV7ZK%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&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;Swift에서는 이를 변수 자체의 백그라운드 스레드 전송이라 부릅니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하려면 self의 유형이 Sendable이어야 합니다&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-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brf5GW/btsQaayC7C4/KZS1y8dZUMrK6mijKAFEWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brf5GW/btsQaayC7C4/KZS1y8dZUMrK6mijKAFEWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brf5GW/btsQaayC7C4/KZS1y8dZUMrK6mijKAFEWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbrf5GW%2FbtsQaayC7C4%2FKZS1y8dZUMrK6mijKAFEWk%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;self가 적절한 위치에 있으니 이 비격리 영역에서 관련 속성 pulse를 읽겠습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일러는 속성 pulse가 어떤 액터에도 격리되지 않은 경우에만 이를 허용합니다&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-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/McEbH/btsQcLc1HHh/jvcKWBWKSuKAcVhP59jMP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/McEbH/btsQcLc1HHh/jvcKWBWKSuKAcVhP59jMP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/McEbH/btsQcLc1HHh/jvcKWBWKSuKAcVhP59jMP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMcEbH%2FbtsQcLc1HHh%2FjvcKWBWKSuKAcVhP59jMP1%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 다시 살펴보면 self는 View이므로 @MainActor로 보호됩니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 컴파일러는 이를 Sendable로 간주합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Swift는 이 self 참조가 @MainActor 격리 영역에서 Sendable 클로저로 넘어가도록 허용함&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-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQ4uMJ/btsP919G8l5/EgaT4KsrJa2lxXikAie6mk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQ4uMJ/btsP919G8l5/EgaT4KsrJa2lxXikAie6mk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQ4uMJ/btsP919G8l5/EgaT4KsrJa2lxXikAie6mk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQ4uMJ%2FbtsP919G8l5%2FEgaT4KsrJa2lxXikAie6mk%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일러는 self 전달은 가능하지만 @MainActor에 격리된 속성 pulse 접근은 안전하지 않다고 말합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일 오류를 수정하려면 View를 참조하여 속성 읽기를 방지해야 합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성 중인 visualEffect에는 뷰의 전체 값이 필요 없습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pulse가 true인지 false인지 알기만 하면 됩니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클로저의 캡처 목록에 있는 pulse 변수의 복사본을 만들어 대신 참조할 수 있습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 self가 클로저에 전송되지 않습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bool은 단순 값 유형이므로 전송 가능 pulse의 복사본을 전송함&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-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OxgYm/btsP95EjyGy/w9RqjKjr4X2S81ZO0ZLtJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OxgYm/btsP95EjyGy/w9RqjKjr4X2S81ZO0ZLtJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OxgYm/btsP95EjyGy/w9RqjKjr4X2S81ZO0ZLtJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOxgYm%2FbtsP95EjyGy%2Fw9RqjKjr4X2S81ZO0ZLtJk%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sYtPf/btsQahLiJHT/VWnxNjTcO4kzfEKEK1V3Gk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sYtPf/btsQahLiJHT/VWnxNjTcO4kzfEKEK1V3Gk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sYtPf/btsQahLiJHT/VWnxNjTcO4kzfEKEK1V3Gk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsYtPf%2FbtsQahLiJHT%2FVWnxNjTcO4kzfEKEK1V3Gk%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Code Camp&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 Button은 비동기 클로저를 받지 않을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기식 업데이트는 좋은 사용자 경험의 필수 요소입니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱에 장기 실행 작업이 있으며 사용자가 결과물을 기다려야 할 때는 더 중요하죠&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI의 액션 콜백은 제 로딩 상태 같은 UI 업데이트 설정에 필요한 동기식 클로저를 허용함&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqSTZF/btsP9D85v6x/WjOlgDHKIuiFVC1ovqkRAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqSTZF/btsP9D85v6x/WjOlgDHKIuiFVC1ovqkRAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqSTZF/btsP9D85v6x/WjOlgDHKIuiFVC1ovqkRAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqSTZF%2FbtsP9D85v6x%2FWjOlgDHKIuiFVC1ovqkRAk%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&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;UI 프레임워크로서 &lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;프레임마다 부드러운 상호작용을 구현하려면 SwiftUI는 기기가 요구하는 특정 화면 재생 빈도를 고려&lt;/b&gt;&lt;/span&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLdSjW/btsP80XNPzO/kL8NFkdvAZuENf2cIxiVJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLdSjW/btsP80XNPzO/kL8NFkdvAZuENf2cIxiVJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLdSjW/btsP80XNPzO/kL8NFkdvAZuENf2cIxiVJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLdSjW%2FbtsP80XNPzO%2FkL8NFkdvAZuENf2cIxiVJk%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;녹색 삼각형을 이용해 SwiftUI가 onScrollVisibilityChange를 호출하는 시점을 표시하겠습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파란색 원은 상태 변환을 통해 애니메이션을 트리거하는 순간을 표시합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정에서는 변환이 제스처 콜백과 동일한 프레임에서 발생하느냐에 따라 시각적으로 완전히 달라질 수 있습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 애니메이션 변환 전에 비동기 작업을 추가하려 합니다 비동기 작업이 시작되는 시점을 주황 선으로 표시하고 대기합니다&lt;/p&gt;
&lt;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-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1Andg/btsQb2zzWkJ/WzxrPY1W48aEAam509Kkyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1Andg/btsQb2zzWkJ/WzxrPY1W48aEAam509Kkyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1Andg/btsQb2zzWkJ/WzxrPY1W48aEAam509Kkyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1Andg%2FbtsQb2zzWkJ%2FWzxrPY1W48aEAam509Kkyk%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Task는 async 함수를 인수로 허용합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;await를 목격하면 컴파일러는 비동기 함수를 두 개로 나눕니다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blEOdj/btsP9FluCEf/xJNCHcDuaYA7Qij0kbrJHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blEOdj/btsP9FluCEf/xJNCHcDuaYA7Qij0kbrJHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blEOdj/btsP9FluCEf/xJNCHcDuaYA7Qij0kbrJHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblEOdj%2FbtsP9FluCEf%2FxJNCHcDuaYA7Qij0kbrJHk%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 부분을 실행하면 Swift 런타임은 함수를 일시 중지하고 CPU에서 다른 작업을 수행할 수 있습니다&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-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sIGwT/btsP9FFL6ra/Uq16rdj22QkkbfFbO2qpK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sIGwT/btsP9FFL6ra/Uq16rdj22QkkbfFbO2qpK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sIGwT/btsP9FFL6ra/Uq16rdj22QkkbfFbO2qpK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsIGwT%2FbtsP9FFL6ra%2FUq16rdj22QkkbfFbO2qpK1%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D8XRd/btsQaUWxNwv/zY9vF5oeOZWEIvEKdrsBI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D8XRd/btsQaUWxNwv/zY9vF5oeOZWEIvEKdrsBI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D8XRd/btsQaUWxNwv/zY9vF5oeOZWEIvEKdrsBI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FD8XRd%2FbtsQaUWxNwv%2FzY9vF5oeOZWEIvEKdrsBI0%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&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;그래서 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;애니메이션이 버벅거리는 것처럼 보입니다 &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 비동기 함수 변형은 목표 달성에 도움이 되지 않죠&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;애니메이션 같은 시간에 민감한 로직은 SwiftUI의 입력과 출력이 동기화되어야 합니다&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Observable 속성의 동기화된 변환과 동기화된 콜백은 프레임워크와의 상호작용에서 가장 자연스러운 타입입니다&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-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjGZMu/btsQb3L0ZuD/rH81YEth0tPC0WffuTTjm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjGZMu/btsQb3L0ZuD/rH81YEth0tPC0WffuTTjm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjGZMu/btsQb3L0ZuD/rH81YEth0tPC0WffuTTjm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjGZMu%2FbtsQb3L0ZuD%2FrH81YEth0tPC0WffuTTjm1%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이 많은 동시 작업을 수행한다면 UI 코드와 비 UI 코드 사이의 경계를 찾아보세요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;비동기 작업의 로직을 뷰 로직에서 분리하세요&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 일부를 브리지로 사용할 수 있습니다 상태는 UI 코드와 비동기 코드를 분리합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;비동기 작업이 완료되면 상태에 동기식 변경을 수행하여 UI가 이 변경에 반응해 업데이트되게 합니다&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 방식으로 UI 로직은 대부분 동기식이 됩니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 비동기 코드에 대한 테스트 작성이 더 쉬워집니다 UI 로직과 독립적으로 작동하기 때문이죠&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-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bf8MC/btsP9MriVOe/ujmKQpT4akZEgSoqweoMHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bf8MC/btsP9MriVOe/ujmKQpT4akZEgSoqweoMHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bf8MC/btsP9MriVOe/ujmKQpT4akZEgSoqweoMHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBf8MC%2FbtsP9MriVOe%2FujmKQpT4akZEgSoqweoMHk%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;1184&quot; height=&quot;696&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰는 여전히 Task를 이용해 비동기 컨텍스트로 전환 가능하죠&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 코드를 최대한 간단하게 유지해야 합니다 UI 이벤트를 모델에 알리는 역할을 합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI 코드 중 시간에 민감한 변경이 많이 필요한 부분과 장기 실행되는 비동기 로직 간의 경계를 찾으면 앱 구조를 개선할 수 있습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰의 동기성과 반응성 유지에도 도움이 됩니다 UI가 아닌 코드를 잘 정리하는 것도 중요합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swift 6.2는 우수한 기본 액터 격리 설정을 제공합니다 기존 앱이 있다면 시도해 보세요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 @MainActor 주석을 삭제할 수 있습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mutex는 클래스를 전송 가능으로 만드는 중요 도구입니다&lt;/p&gt;</description>
      <category>iyOmSd/Title: Swift</category>
      <author>냄수</author>
      <guid isPermaLink="true">https://nsios.tistory.com/249</guid>
      <comments>https://nsios.tistory.com/249#entry249comment</comments>
      <pubDate>Thu, 28 Aug 2025 20:05:17 +0900</pubDate>
    </item>
    <item>
      <title>[Swift] WWDC25 Wake up to the AlarmKit API</title>
      <link>https://nsios.tistory.com/248</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 알람관련기능을 개발하면서 알람은 왜지원을 안해주는건가&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많이 불편했었는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제야 지원을 해주네요??&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시대를 빨리태어난 요구사항이 돼버렷습니다..&lt;/p&gt;
&lt;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;오늘은 WWDC25주제중 AlarmKit에 관련된 내용을 정리해보려합니다&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고정되거나 예정된 시간에 발생하는 일에 대한 두드러진 알림&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일정이나 카운트기반으로 작동&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;무음모드와 현재 포커스상태를 무시하고 실행&lt;/span&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;이게 중요한거같아요&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bF9L4O/btsPtWU37py/FjGxnrDPWe0qGE68NkfG8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bF9L4O/btsPtWU37py/FjGxnrDPWe0qGE68NkfG8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bF9L4O/btsPtWU37py/FjGxnrDPWe0qGE68NkfG8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbF9L4O%2FbtsPtWU37py%2FFjGxnrDPWe0qGE68NkfG8k%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;1958&quot; height=&quot;1160&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 앱 인텐트로 정의된 동작을 가진 커스텀 버튼 혹은 미루기 버튼&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 갖는 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Authorization관련해서는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 앱내에서 권한을 동의해야 사용가능하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수동으로 요청하거나 첫알람 생성시 자동으로 요청되도록 가능하다고하네요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언제든지 설정앱에서 권한상태를 변경할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 권한들처럼 앱의 info.plist에 &lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;NSAlarmKitUsageDescription&lt;/b&gt;&lt;/span&gt;를 추가하여 알람사용 사례를 설명하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수동으로 권한요청시 &lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;requestAuthorization&lt;/b&gt;&lt;/span&gt;를 사용하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한 상태 확인시 AlarmManager 클래스의&amp;nbsp;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;authorizationState&lt;/b&gt;&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;알람생성시 필요한 주요요소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Countdown duration: 카운트다운 시간&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Schedule: 특정날짜나 반복패턴 일정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Appearance: 알람표시 맞춤화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Action: 알람동작 맞춤화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Sound: 알람 관련소리&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;이렇게 5가지가 주요요소입니다.&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;Countdown duration&lt;/h4&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/tpd82/btsPs3tN3be/vzVVCvX2jI5LvZODbLrlmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tpd82/btsPs3tN3be/vzVVCvX2jI5LvZODbLrlmK/img.png&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tpd82/btsPs3tN3be/vzVVCvX2jI5LvZODbLrlmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftpd82%2FbtsPs3tN3be%2FvzVVCvX2jI5LvZODbLrlmK%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;1958&quot; height=&quot;1160&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQvEIj/btsPsS0iXrc/qpku4mGZYJh62ZHkkUVOs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQvEIj/btsPsS0iXrc/qpku4mGZYJh62ZHkkUVOs0/img.png&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQvEIj/btsPsS0iXrc/qpku4mGZYJh62ZHkkUVOs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQvEIj%2FbtsPsS0iXrc%2Fqpku4mGZYJh62ZHkkUVOs0%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;1958&quot; height=&quot;1160&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;알림이 처음 예약되면 알람전 카운트다운 시간에 표시될 카운트다운 UI가 표시됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알람 시간이 경과하면 알람이 발동되고 구성에 따라서 커스텀된 알람UI가 표시됨&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-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nyeuE/btsPuEMY5UZ/pg1TV6eLJickP37sy1rGJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nyeuE/btsPuEMY5UZ/pg1TV6eLJickP37sy1rGJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nyeuE/btsPuEMY5UZ/pg1TV6eLJickP37sy1rGJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnyeuE%2FbtsPuEMY5UZ%2Fpg1TV6eLJickP37sy1rGJ0%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;1958&quot; height=&quot;1160&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 알람에대해 다시 알림이 선택되면 다음알람 간격동안 카운트다운 UI가 다시 표시됨&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1272&quot; data-origin-height=&quot;380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4CfnS/btsPuFrD80B/zAZ0GBfTAFS72DTVZ2AIyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4CfnS/btsPuFrD80B/zAZ0GBfTAFS72DTVZ2AIyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4CfnS/btsPuFrD80B/zAZ0GBfTAFS72DTVZ2AIyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4CfnS%2FbtsPuFrD80B%2FzAZ0GBfTAFS72DTVZ2AIyk%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;1272&quot; height=&quot;380&quot; data-origin-width=&quot;1272&quot; data-origin-height=&quot;380&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의코드는 10분후 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;알람&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;타이머를 설정하는 코드&lt;span style=&quot;color: #666666;&quot;&gt;(여기서는 알람 전 10분이라고 설명합니다 그래서 preAlert 라고하나보네요)&lt;/span&gt;&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;&amp;nbsp;&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;Schedule&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;일정은 절대적이고 기기 시간대가 바뀌어도 변경되지 않음&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AvhEC/btsPuNbPSam/lzumxfzrpJiP8C9537zwb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AvhEC/btsPuNbPSam/lzumxfzrpJiP8C9537zwb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AvhEC/btsPuNbPSam/lzumxfzrpJiP8C9537zwb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAvhEC%2FbtsPuNbPSam%2FlzumxfzrpJiP8C9537zwb0%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;1958&quot; height=&quot;1160&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의코드는 Date만들어서 Alarm셋팅하는 코드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Alarm.Schedule.fixed&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[상대시간]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상대적인 일정을 지정할 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에는 시간이 포함되고, 주간 반복 패턴을 추가할 수도 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;상대적인 일정은 시간대의 변화를 고려함&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Omkw9/btsPt1Pxkyj/mZu0brKZywYaaCUFiVVLu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Omkw9/btsPt1Pxkyj/mZu0brKZywYaaCUFiVVLu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Omkw9/btsPt1Pxkyj/mZu0brKZywYaaCUFiVVLu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOmkw9%2FbtsPt1Pxkyj%2FmZu0brKZywYaaCUFiVVLu1%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;1958&quot; height=&quot;1160&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;&lt;b&gt;Alarm.Schedule.Relative&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Appearance&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AlarmButton타입을 이용해서 알람버튼을 구현가능&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-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnldJ0/btsPtDaqegF/t3q4fMkgxcVHQ7BnO2j8s0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnldJ0/btsPtDaqegF/t3q4fMkgxcVHQ7BnO2j8s0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnldJ0/btsPtDaqegF/t3q4fMkgxcVHQ7BnO2j8s0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnldJ0%2FbtsPtDaqegF%2Ft3q4fMkgxcVHQ7BnO2j8s0%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;1958&quot; height=&quot;1160&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;496&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l1hUW/btsPtjpATam/0PQgjkngwMmGl7EHiL1PM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l1hUW/btsPtjpATam/0PQgjkngwMmGl7EHiL1PM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l1hUW/btsPtjpATam/0PQgjkngwMmGl7EHiL1PM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl1hUW%2FbtsPtjpATam%2F0PQgjkngwMmGl7EHiL1PM1%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;496&quot; height=&quot;176&quot; data-origin-width=&quot;496&quot; data-origin-height=&quot;176&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;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;AlarmPresentation.Alert&lt;/b&gt;&lt;/span&gt;: 알람 제목, stop버튼 설정가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;AlarmAttributes&lt;/b&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;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;카운트다운UI를 표시하려면 아래와같이 설정해야함&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-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baqHMF/btsPthee0Fk/H0qEKxZQvddK5KlKnkqNFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baqHMF/btsPthee0Fk/H0qEKxZQvddK5KlKnkqNFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baqHMF/btsPthee0Fk/H0qEKxZQvddK5KlKnkqNFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaqHMF%2FbtsPthee0Fk%2FH0qEKxZQvddK5KlKnkqNFK%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;1958&quot; height=&quot;1160&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dhgZpR/btsPu7nwxxx/LBk2wt9i0FH8gCKaJWsy01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dhgZpR/btsPu7nwxxx/LBk2wt9i0FH8gCKaJWsy01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dhgZpR/btsPu7nwxxx/LBk2wt9i0FH8gCKaJWsy01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdhgZpR%2FbtsPu7nwxxx%2FLBk2wt9i0FH8gCKaJWsy01%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;1958&quot; height=&quot;1160&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&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;AlarmPresentation.Alert에 보조 버튼으로 Repeat버튼을 만들어서 넣어줌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;secondaryButtonBehavior&lt;/b&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;이제 카운트다운 UI를 구현하기위해서는 Live Activity(실시간현황)를 구현해줘야함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카운트다운은 잠금화면, Dynamic Island, 스탠바이에서 표시됨&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;Live Activity를 앱의 위젯 확장프로그램에 추가하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ActivityConfiguration을 설정하고&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AlarmAttributes를 메타데이터 유형으로 지정하면 됨&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caHXJP/btsPutdLfVl/ySop1Z8GFc6VkKQhJtXr2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caHXJP/btsPutdLfVl/ySop1Z8GFc6VkKQhJtXr2k/img.png&quot; data-alt=&quot;라이브 액티비티 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caHXJP/btsPutdLfVl/ySop1Z8GFc6VkKQhJtXr2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaHXJP%2FbtsPutdLfVl%2FySop1Z8GFc6VkKQhJtXr2k%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;1958&quot; height=&quot;1160&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;라이브 액티비티 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tOBZh/btsPuSxm0RT/oGpSL72V8pxZYvZuZAwlo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tOBZh/btsPuSxm0RT/oGpSL72V8pxZYvZuZAwlo1/img.png&quot; data-alt=&quot;다이나믹 아일랜드 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tOBZh/btsPuSxm0RT/oGpSL72V8pxZYvZuZAwlo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtOBZh%2FbtsPuSxm0RT%2FoGpSL72V8pxZYvZuZAwlo1%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;1958&quot; height=&quot;1160&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;다이나믹 아일랜드 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qTWk9/btsPs0Rkbbh/JqkudkEJy7BQzZ1GnwlmGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qTWk9/btsPs0Rkbbh/JqkudkEJy7BQzZ1GnwlmGK/img.png&quot; data-alt=&quot;다이나믹 아일랜드 디자인의 확장 영역 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qTWk9/btsPs0Rkbbh/JqkudkEJy7BQzZ1GnwlmGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqTWk9%2FbtsPs0Rkbbh%2FJqkudkEJy7BQzZ1GnwlmGK%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;1958&quot; height=&quot;1160&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&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;&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/pIL3g/btsPuF6d2rd/CXE9Kw7MmOZWYvfuwHguVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pIL3g/btsPuF6d2rd/CXE9Kw7MmOZWYvfuwHguVk/img.png&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pIL3g/btsPuF6d2rd/CXE9Kw7MmOZWYvfuwHguVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpIL3g%2FbtsPuF6d2rd%2FCXE9Kw7MmOZWYvfuwHguVk%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;1958&quot; height=&quot;1160&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A0V4d/btsPtDheuOy/pXSpFpQEGY91NuTkC39vn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A0V4d/btsPtDheuOy/pXSpFpQEGY91NuTkC39vn0/img.png&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A0V4d/btsPtDheuOy/pXSpFpQEGY91NuTkC39vn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA0V4d%2FbtsPtDheuOy%2FpXSpFpQEGY91NuTkC39vn0%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;1958&quot; height=&quot;1160&quot;/&gt;&lt;/span&gt;&lt;/div&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;AlarmAttributes을 생성할 때 이 커스텀된 메타데이터를 포함하면됨&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;그런데 기기가 재시동되고 처음으로 잠금해제가 되기전처럼 실시간 현황이 표시 불가능한 상황도 있음&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buR2vS/btsPuLLVeqY/nvVtfZ0t8NIm1B3gzknSi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buR2vS/btsPuLLVeqY/nvVtfZ0t8NIm1B3gzknSi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buR2vS/btsPuLLVeqY/nvVtfZ0t8NIm1B3gzknSi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuR2vS%2FbtsPuLLVeqY%2FnvVtfZ0t8NIm1B3gzknSi1%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;1958&quot; height=&quot;1160&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠금화면에서는 보조버튼의 아이콘, 알림제목, 카운트다운에 tintColor가 사용됨&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-origin-width=&quot;516&quot; data-origin-height=&quot;188&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m5sFx/btsPt9s7CXh/wskEpLTRtvQft8KFX1WEi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m5sFx/btsPt9s7CXh/wskEpLTRtvQft8KFX1WEi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m5sFx/btsPt9s7CXh/wskEpLTRtvQft8KFX1WEi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm5sFx%2FbtsPt9s7CXh%2FwskEpLTRtvQft8KFX1WEi1%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;516&quot; height=&quot;188&quot; data-origin-width=&quot;516&quot; data-origin-height=&quot;188&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;h4 data-ke-size=&quot;size20&quot;&gt;Action&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AlarmKit은 사용자가 알람버튼을 탭할때 자체코드를 실행하는 기능도 제공함&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-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvyMbx/btsPuI2QwRY/UwkwKwZOka08m3NMI6uknk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvyMbx/btsPuI2QwRY/UwkwKwZOka08m3NMI6uknk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvyMbx/btsPuI2QwRY/UwkwKwZOka08m3NMI6uknk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvyMbx%2FbtsPuI2QwRY%2FUwkwKwZOka08m3NMI6uknk%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;1958&quot; height=&quot;1160&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맞춤형 동작을 실행하도록 보조버튼 동작을 custom으로 설정합니다&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/Sxotp/btsPtAdEKbL/l4S8A800P6q4iP7ouS2le0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Sxotp/btsPtAdEKbL/l4S8A800P6q4iP7ouS2le0/img.png&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Sxotp/btsPtAdEKbL/l4S8A800P6q4iP7ouS2le0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSxotp%2FbtsPtAdEKbL%2Fl4S8A800P6q4iP7ouS2le0%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;1958&quot; height=&quot;1160&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXdar3/btsPt8nsLkT/LdnGCsHyykD9TS7KK8ACS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXdar3/btsPt8nsLkT/LdnGCsHyykD9TS7KK8ACS0/img.png&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXdar3/btsPt8nsLkT/LdnGCsHyykD9TS7KK8ACS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXdar3%2FbtsPt8nsLkT%2FLdnGCsHyykD9TS7KK8ACS0%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;1958&quot; height=&quot;1160&quot;/&gt;&lt;/span&gt;&lt;/div&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;OpenInApp타입인 인텐트(LiveActivityIntent프로토콜)의 인스턴스를 생성하고&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알람의 고유 식별자를 전달합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보조버튼이 탭될 때 이 인텐트를 실행하라고 시스템에 알립니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Sound&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l8rX3/btsPvaLlnuP/8vK9vfWXOMe2IIWl9bBKG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l8rX3/btsPvaLlnuP/8vK9vfWXOMe2IIWl9bBKG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l8rX3/btsPvaLlnuP/8vK9vfWXOMe2IIWl9bBKG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl8rX3%2FbtsPvaLlnuP%2F8vK9vfWXOMe2IIWl9bBKG1%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;1958&quot; height=&quot;1160&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AlertSound구조체를 사용해서 커스텀 소리를 정의할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이&lt;b&gt; 파일은 앱의 메인 번들이나 앱 데이터 컨테이너의 Library/Sounds 폴더&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Life Cycle&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GQDzl/btsPuYjWNkU/dc14T5GJAKXYUFYlGSNLxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GQDzl/btsPuYjWNkU/dc14T5GJAKXYUFYlGSNLxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GQDzl/btsPuYjWNkU/dc14T5GJAKXYUFYlGSNLxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGQDzl%2FbtsPuYjWNkU%2Fdc14T5GJAKXYUFYlGSNLxk%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;1958&quot; height=&quot;1160&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1160&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;예약하려면 알람의 고유식별자와 이전에 생성한 configuration을 사용합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알람 라이프사이클을 완전히 제어할 수 있고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카운트다운 상태로 전환하거나, 취소, 중지, 일시정지, 다시시작이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>iyOmSd/Title: Swift</category>
      <author>냄수</author>
      <guid isPermaLink="true">https://nsios.tistory.com/248</guid>
      <comments>https://nsios.tistory.com/248#entry248comment</comments>
      <pubDate>Tue, 22 Jul 2025 18:14:28 +0900</pubDate>
    </item>
    <item>
      <title>[Swift] Vision OCR 인식</title>
      <link>https://nsios.tistory.com/247</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Vision Framework를 사용해서 이미지속의 텍스트를 인식할 수 있는 OCR 기능을 구현해보려합니다&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;OCR 이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;광학 문자 인식 Optical&amp;nbsp;Character&amp;nbsp;Recognition의 약자로 이미지나 스캔 문서에 있는 텍스트를 기계가 읽을 수 있는 텍스트로 변환하는 기술입니다.&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;2가지 방법으로 이용합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fast path방식과 accurate path방식이 있습니다.&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;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;Fast&lt;/b&gt;&lt;/span&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;이 접근방식은 기존 OCR과 유사함&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: #8a3db6;&quot;&gt;&lt;b&gt;Accurate (default)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신경망을 이용해서 문자열과 줄로 이루어진 텍스트를 찾은다음 추가 분석을 수행하여 개별 단어와 문장을 찾음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 접근방식은 사람이 텍스트를 읽는 방식과 훨씬 더 일치함&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1482&quot; data-origin-height=&quot;944&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PJFwT/btsOPV9Tj1M/DP1ZvoQP84dGRJ0NLAXUGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PJFwT/btsOPV9Tj1M/DP1ZvoQP84dGRJ0NLAXUGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PJFwT/btsOPV9Tj1M/DP1ZvoQP84dGRJ0NLAXUGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPJFwT%2FbtsOPV9Tj1M%2FDP1ZvoQP84dGRJ0NLAXUGk%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;1482&quot; height=&quot;944&quot; data-origin-width=&quot;1482&quot; data-origin-height=&quot;944&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;VNRequestTextRecognitionLevel타입으로 제공되고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VNRecognizeTextRequest의 recognitionLevel옵션을 변경하면 적용됩니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1750752105383&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let textRequest = VNRecognizeTextRequest()
textRequest.recognitionLevel = .accurate&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 data-ke-size=&quot;size23&quot;&gt;구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 OCR인식 코드구현은 간단합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하기 편리하게 제공된거같네요&lt;/p&gt;
&lt;pre id=&quot;code_1750752237299&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;guard let image = uiImage.cgImage else {
    print(&quot;cgImage nil!!&quot;)
    return
}

let textRequest = VNRecognizeTextRequest()
textRequest.revision = VNRecognizeTextRequestRevision3
textRequest.recognitionLanguages = [&quot;ko-KR&quot;, &quot;en-US&quot;]
textRequest.usesLanguageCorrection = false
textRequest.minimumTextHeight = 0.1
textRequest.recognitionLevel = .accurate

let handler = VNImageRequestHandler(cgImage: image)
do {
    // 요청실행
    try handler.perform([textRequest])
    
    // 요청값 반환
    let text = textRequest.results?.compactMap { $0.topCandidates(1).first?.string }.joined(separator: &quot;\n&quot;) ?? &quot;&quot;
    print(text)
}&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;Request 옵션을 하나씩알아볼게요&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;revision&lt;/b&gt; - 엔진 버전 선택(최신이 좋아요 iOS지원 버전에맞게 선택하세요!)&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;recognitionLanguages&lt;/b&gt; - 인식할 언어&lt;/p&gt;
&lt;pre id=&quot;code_1750752579429&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;textRequest.supportedRecognitionLanguages()&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;인식할 언어 ko-KR를 안넣어주면 한국어가 있어도 한국어로 인식하지않아요&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;usesLanguageCorrection&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;b&gt;minimumTextHeight&lt;/b&gt; - 이미지 높이에 상대적인 크기의 텍스트를 인식하는 옵션이에요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 이미지높이의 절반인 텍스트로 인식을 제한하려면 0.5를 넣어요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크게크게 보기때문에 메모리 사용량이 줄고, 인식속도가 빨라지지만 작은크기 텍스트는 무시합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본값은 0.03125 라고하네요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(테스트할때 0.1~0.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;b&gt;recognitionLevel&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;b&gt;VNImageRequestHandler의 perform()&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;이때 인식이 시작됩니다&lt;/p&gt;
&lt;pre id=&quot;code_1750753034501&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try handler.perform([textRequest])&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;&lt;b&gt;VNRecognizeTextRequest.results&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;perform을 했을때 VNRecognizedTextObservation타입의 결과값 배열이 반환되요&lt;/p&gt;
&lt;pre id=&quot;code_1750753013952&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    let text = textRequest.results?.compactMap { $0.topCandidates(1).first?.string }.joined(separator: &quot;\n&quot;) ?? &quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$0.topCandidates(1) - 인식결과값이 여러개인데 그중 우선순위 최상위 1개만 받겠다 라는 옵션이에요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인식한 문자 하나하나의 결과값을 joined시켜서 하나의 문장으로 만들어주는 코드에요&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;/h3&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-origin-width=&quot;1691&quot; data-origin-height=&quot;733&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d1tfrN/btsOPnGiHY9/Ot5m89redPfYDEXwhkPT5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d1tfrN/btsOPnGiHY9/Ot5m89redPfYDEXwhkPT5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d1tfrN/btsOPnGiHY9/Ot5m89redPfYDEXwhkPT5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd1tfrN%2FbtsOPnGiHY9%2FOt5m89redPfYDEXwhkPT5K%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;1691&quot; height=&quot;733&quot; data-origin-width=&quot;1691&quot; data-origin-height=&quot;733&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;우선 처음테스트는 &lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;정확도우선의 옵션&lt;/b&gt;&lt;/span&gt;을 설정했어요&lt;/p&gt;
&lt;pre id=&quot;code_1750813428690&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let textRequest = VNRecognizeTextRequest()
textRequest.revision = VNRecognizeTextRequestRevision3
textRequest.recognitionLanguages = [&quot;ko-KR&quot;, &quot;en-US&quot;]
textRequest.usesLanguageCorrection = true
textRequest.recognitionLevel = .accurate&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;/p&gt;
&lt;pre id=&quot;code_1750813452469&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;12 point 포인트
14 point 포인트
16 point 포인트
20 point 포인트
24 point 포인트
32 point 포인트
36 point 포인트
40 point 포인트
48 point 포인트
0123456789 0123456789
01234567890123456789
0123456789 0123456789
0123456790123456789
01234567890123456789
01234567890123456789
01234567890123456789
01234567890123456789
01234567890123456789
64 point 포인트
01234567890123456789
1 장, 0.800754084 seconds&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;하지만 이때 고려해야할게 하나 있죠 바로 시간입니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1장을 인식하는데 0.8초라 여러장의 사진을 인식하려한다면 고려해야할 부분이 많아지겠죠?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으론&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;빠름우선의 옵션&lt;/b&gt;&lt;/span&gt;을 설정해볼게요&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1750813568785&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let textRequest = VNRecognizeTextRequest()
textRequest.revision = VNRecognizeTextRequestRevision3
textRequest.recognitionLanguages = [&quot;ko-KR&quot;, &quot;en-US&quot;]
textRequest.usesLanguageCorrection = false
//        textRequest.minimumTextHeight = 0.1  &amp;lt;&amp;lt; fast일때 설정하면 아무결과를 받지못합니다
textRequest.recognitionLevel = .fast&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;/p&gt;
&lt;pre id=&quot;code_1750813594569&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;12 point &amp;amp;oI_
01234587890123456789
14 point EOI
01234567890123456789
16 point EOIE
01234567890123456789
20 point &amp;pound;oI E
01234567890123456789
24 point &amp;pound;oI E
01234567890123456789
32 point &amp;amp;OI E
01234567890123456789
36 point &amp;amp;oIE
01234567890123456789
40 point &amp;pound;OIE
01234567890123456789
48 point lOI E
01234567890123456789
64 point lOI E
01234567890123456789
1 장, 0.077625583 seconds&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;12point 테스트케이스인&amp;nbsp;&lt;/span&gt;1번째줄은 6과 8을 정확하게 인식하지못햇네요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나머지 숫자와 영어는 잘인식되늰데 한글을 인식하지못하네요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 위코드에서 보이듯이 minimumTextHeight를 fast옵션과 함께주면 텍스트인식을 못해서 아무결과값을 받지못해요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간도보면 0.07초로 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;10배차이&lt;/span&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;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;시간과 성능의 트레이드오프인 옵션들이라 테스트하면서 잘 선택하면 좋을 것 같아요&lt;/b&gt;&lt;/span&gt;!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 이젠 여러장을 인식할때 &lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;async로 동시에 돌리고싶을때&lt;/b&gt;&lt;/span&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;pre id=&quot;code_1750813900686&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private func excuteRequest(uiImage: UIImage) async -&amp;gt; [VNRecognizedTextObservation] {
    guard let image = uiImage.cgImage else {
        print(&quot;cgImage nil!!&quot;)
        return []
    }
    let textRequest = VNRecognizeTextRequest()
    textRequest.revision = VNRecognizeTextRequestRevision3
    textRequest.recognitionLanguages = [&quot;ko-KR&quot;, &quot;en-US&quot;]
    textRequest.usesLanguageCorrection = false
//        textRequest.minimumTextHeight = 0.1
    textRequest.recognitionLevel = .fast
    
    let handler = VNImageRequestHandler(cgImage: image)
    
    return await withUnsafeContinuation { continuation in
        do {
            try handler.perform([textRequest])
            let observations = textRequest.results ?? []
            continuation.resume(returning: observations)
        } catch {
            print(error)
            continuation.resume(returning: [])
        }
    }
}&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;/p&gt;
&lt;pre id=&quot;code_1750814219445&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func recognizeText() async {
    let images: [UIImage] = [
        .폰트테스트png,
        .폰트테스트png,
    ]
    
    let time = await clock.measure {
        // 이미지 총갯수를 4개의 이미지그룹으로 분리
        let imageGroups = images.splitFourStride()
        
        let recognizationString = await withTaskGroup(of: String.self) { group in
            imageGroups.forEach { imageGroup in
                group.addTask {
                    await self.excute(imageGroup: imageGroup)
                }
            }
            var result: String = &quot;&quot;
            for await value in group {
                result += value
            }
            return result
        }
        
        // 종합된 결과 텍스트
        print(recognizationString)
    }
    
    // 인식한 장수, 인식까지 걸린시간
    print(images.count, &quot;장&quot;, time)
}

// 각 그룹별 인식결과 반환
private func excute(imageGroup: [UIImage]) async -&amp;gt; String {
    var totalString = &quot;&quot;
    for (i, image) in imageGroup.enumerated() {
        let results = await self.excuteRequest(uiImage: image)
        let text = results.compactMap { $0.topCandidates(1).first?.string }.joined(separator: &quot;\n&quot;)
        totalString += text
    }
    return totalString
}&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;10개의 이미지를 4개의 그룹으로 실행시킨 결과 아래와같이 동시에 잘 진행되는걸 확인할 수 있어요&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1744&quot; data-origin-height=&quot;528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AEtDR/btsOPVihbF3/V8s45K2n3ymhtJpQ2KR1p0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AEtDR/btsOPVihbF3/V8s45K2n3ymhtJpQ2KR1p0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AEtDR/btsOPVihbF3/V8s45K2n3ymhtJpQ2KR1p0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAEtDR%2FbtsOPVihbF3%2FV8s45K2n3ymhtJpQ2KR1p0%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;1744&quot; height=&quot;528&quot; data-origin-width=&quot;1744&quot; data-origin-height=&quot;528&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1748&quot; data-origin-height=&quot;246&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sJ6WQ/btsOPKA0JMb/D3PGiQLpK4Yd8d3Lr4zQe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sJ6WQ/btsOPKA0JMb/D3PGiQLpK4Yd8d3Lr4zQe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sJ6WQ/btsOPKA0JMb/D3PGiQLpK4Yd8d3Lr4zQe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsJ6WQ%2FbtsOPKA0JMb%2FD3PGiQLpK4Yd8d3Lr4zQe0%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;1748&quot; height=&quot;246&quot; data-origin-width=&quot;1748&quot; data-origin-height=&quot;246&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>iyOmSd/Title: Swift</category>
      <author>냄수</author>
      <guid isPermaLink="true">https://nsios.tistory.com/247</guid>
      <comments>https://nsios.tistory.com/247#entry247comment</comments>
      <pubDate>Wed, 25 Jun 2025 21:34:20 +0900</pubDate>
    </item>
  </channel>
</rss>