<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Clamp</title>
    <link>https://clamp-coding.tistory.com/</link>
    <description>주니어 iOS개발자의 발악!!!!!!!</description>
    <language>ko</language>
    <pubDate>Thu, 13 Aug 2026 06:51:29 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>clamp</managingEditor>
    <image>
      <title>Clamp</title>
      <url>https://tistory1.daumcdn.net/tistory/5225653/attach/3942c0319bb445db950bb65caa6156fe</url>
      <link>https://clamp-coding.tistory.com</link>
    </image>
    <item>
      <title>[Xcode] SPM Package 내부에서 CustomFont 사용하기</title>
      <link>https://clamp-coding.tistory.com/526</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;SDK작업중 Font를 SPM Package에서 사용하기 위한 방법과 삽질을 남겨봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPM Package의 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Package하위의 여러 모듈중 SDKCommon이라는 모듈의 일부입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;527&quot; data-origin-height=&quot;247&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bU0wgk/btsKY3dceK2/6AE6oUk5I8w9BvkIaG26t0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bU0wgk/btsKY3dceK2/6AE6oUk5I8w9BvkIaG26t0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bU0wgk/btsKY3dceK2/6AE6oUk5I8w9BvkIaG26t0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbU0wgk%2FbtsKY3dceK2%2F6AE6oUk5I8w9BvkIaG26t0%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;527&quot; height=&quot;247&quot; data-origin-width=&quot;527&quot; data-origin-height=&quot;247&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;h2 data-ke-size=&quot;size26&quot;&gt;SDK 구조 가정&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;ClampSDK&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;- ClampSDKAuth // 모듈&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;- ClampSDKChat&amp;nbsp;//&amp;nbsp;모듈&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;- ClampSDKCommon //&amp;nbsp;모듈&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;ㄴ Resources&amp;nbsp; &amp;nbsp; &amp;nbsp;// ClampCommon모듈의 하위 폴더&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;ㄴ Fonts&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// Resources 폴더의 하위 폴더&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;ㄴ ttf파일&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Package 파일에 Resources를 정의해줍시다&lt;/h4&gt;
&lt;pre id=&quot;code_1732706773555&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    targets: [
        .target(
            name: &quot;ClampSDKCommon&quot;,
            resources: [
                .process(&quot;Resources/Fonts&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;이렇게만 정의해주면 SPM은 재귀적으로 하위 파일들을 찾아나갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPM 공식문서 참고&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;2. 일반 프로젝트에서는 Font를 info.plist파일에 등록해주지만 코드로도 등록할 수 있습니다.&lt;/h4&gt;
&lt;pre id=&quot;code_1732706895933&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Foundation

struct SDKCustomFontName {
    static let kantumRuyProBold =  &quot;KantumruyPro-Bold&quot;
    static let kantumRuyProMedium =  &quot;KantumruyPro-Medium&quot;
    static let kantumRuyProRegular =  &quot;KantumruyPro-Regular&quot;
    static let kantumRuyProSemiBold =  &quot;KantumruyPro-SemiBold&quot;
    
    static var all: [String] {
        [kantumRuyProBold, kantumRuyProMedium, kantumRuyProRegular, kantumRuyProSemiBold]
    }
}

public struct SDKFonts {
    public static func registerCustomFonts() {
        SDKCustomFontName.all.forEach { font in
            guard let url = Bundle.module.url(forResource: font, withExtension: &quot;ttf&quot;) else {
                return
            }
            CTFontManagerRegisterFontsForURL(url as CFURL, .process, 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;p data-ke-size=&quot;size16&quot;&gt;만약 SDK가 앱에서 사용된다면 앱에서 registerCutomFonts()를 SDK사용 전에 호출해주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppDelegate에서 하면 되겠죠&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Preview에서 보고싶다면?&lt;/p&gt;
&lt;pre id=&quot;code_1732706976426&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public extension View {
    func loadCustomFonts() -&amp;gt; some View {
        SDKFonts.registerCustomFonts()
        return self
    }
}

#Preview {
    MainView()
        .loadCustomFonts()
}&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;h2 data-ke-size=&quot;size26&quot;&gt;삽질... 그리고 기록&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폰트를 찾지 못한 이유&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;url이 항상 nil로 출력되는 문제&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rwqkJ/btsKX4DXlWV/hGrK4vpCLdjQ4CHymcGv90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rwqkJ/btsKX4DXlWV/hGrK4vpCLdjQ4CHymcGv90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rwqkJ/btsKX4DXlWV/hGrK4vpCLdjQ4CHymcGv90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrwqkJ%2FbtsKX4DXlWV%2FhGrK4vpCLdjQ4CHymcGv90%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;898&quot; height=&quot;506&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;506&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;SDKCustomFontName들을 보면 .ttf 확장자까지 파일 이름에 넣었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 bundle에서 resources들의 url을 찾고있는데 뒤에 extension에서 또 &quot;ttf&quot;를 입력해서..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폰트.ttf.ttf를 찾으니 못찾지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;내 사라진 1시간...&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>IOS</category>
      <author>clamp</author>
      <guid isPermaLink="true">https://clamp-coding.tistory.com/526</guid>
      <comments>https://clamp-coding.tistory.com/526#entry526comment</comments>
      <pubDate>Wed, 27 Nov 2024 20:34:58 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] DynamicFramework</title>
      <link>https://clamp-coding.tistory.com/524</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Framework, Library&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Library: Code와 Data가 모인 하나의 모듈&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Framework: Library를 포함하는 하나의 패키지로 캡슐화한 디렉토리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) Framework = Library + Nib + Resources(Image, String, Header)&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;2. DynamicFramework&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 컴파일된 바이너리 파일이 stack의 Bundle영역에 존재하여 어플리케이션 코드에서 스택 영역을 참조하여 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 동적으로 연결되므로 전체를 빌드하지 않아도 프레임워크를 사용 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Dynamic Framework는 공유메모리를 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 DynamicFramework를 사용하는 앱 들이 있다면 해당 코드는 메모리에 한 번만 로드되어 여러 앱이 공유&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DynamicFramework는 앱 실행중에 시스템이 라이브러리를 로드&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;Dynamic Linker(dyld)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- DynamicFramework가 앱 실행중에 동적으로 연결되게 하는 시스템 컴포넌트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;동적으로 연결이란 뭘까?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&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;1. 실행파일(excutable File)로딩: 앱 실행시 iOS는 Executable File을 메모리에 로드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Dynamic Framework의 로드 과정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 프레임워크 경로 탐색&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 파일에 기록된 DynamicFramework의 경로를 탐색, 경로는 주로 @rpath(run time search path), @executable_path 등의 동적 위치 정보를 기반으로 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 메모리 로드&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dyld는 필요한 dynamicFramework를 시스템에서 읽어 메모리의 Heap 영역에 로드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) 실행파일과 DynamicFramework 간의 함수, 변수 등의 참조를 연결&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4) 최적화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-dyld는 동적 연결 속도를 높이기 위해 기존에 캐싱된 연결 정보(OpCache)나 Prebinding 정보를 사용&lt;/p&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;blockquote data-ke-style=&quot;style2&quot;&gt;ex) Alamofire&lt;br /&gt;&lt;br /&gt;앱 실행 &amp;gt; dyld가 실행파일(Executable File)에서 Alamofire를 사용하는 것을 확인 &amp;gt; Alamofire Framework가 디스크에서 읽혀서 메모리에 로드 &amp;gt; dyld가 Alamofire의 api를 실행파일과 연결 &amp;gt; 앱이 호출시 메모리에 로드된 Alamofire 호출&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DynamicFramework가 포함된 앱을 앱스토어에 올리면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- AppBundle에 포함 &quot;AppBundle/Frameworks&quot; 경로에 위치(DrivedData)에서 확인 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 앱을 빌드할 때 사용하는 Dynamic Framework를 XCode가 확인하고 이를 앱 번들 내부에 복사.&lt;/p&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_1732540138994&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;MyApp.app/
 ㄴFrameworks/
 	ㄴMyDynamicframework.framework
    	ㄴOtherDynamicFramework.framework&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;DynamicFramework를 AppBundle에 포함시키기 위한 XCode 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. LinkBinary with Libraries&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- XCode에서 사용하는 Dynamic Framework를 프로젝트의 Link Binary with Libraries에 추가해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. EmbedFrameworks 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Build Phase &amp;gt; Embed Frameworks에 해당 Framework를 추가해야합니다. XCode가 앱 번들에 Framework를 복사하고 런타임에 로드할 수 있도록 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. CodeSigning 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 앱 스토에 제출하려면 모든 DynamicFramework는 앱의 코드에 서명 정책에 따라 서명되어야 합니다&lt;/p&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;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;??? DynamicFramework도 AppBundle에 포함되는데 왜 App의 크기가 덜 증가하거나 효율적인지 궁금했습니다&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;공유메모리 사용&lt;br /&gt;DynamicFramework는 런타임에 iOS의 &quot;공유메모리&quot;를 활용합니다&lt;br /&gt;예를 들어 A앱, B앱이 모두 동일한 SomeFrameowrk.dylib를 사용한다면 두 앱은 메모리에서 같은 프레임워크를 공유합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;??? SomeFramework.dylib를 공유한다면 Framework의 변수가 변경되면 A, B 모두 영향?&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;DynamicFramework의 메모리 공유와 데이터 독립성&lt;br /&gt;1. 공유되는 부분: 코드&lt;br /&gt;- Dynamic Framework의 코드(함수, 메서드)는 읽기 전용으로 공유됩니다.&lt;br /&gt;&lt;br /&gt;2. 독립적인 부분: 데이터&lt;br /&gt;- Dynamic Framework에서 사용하는 **데이터(전역변수, 정적변수, 객체)는 앱 마다 독립적으로 할당됩니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;a href=&quot;https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/Framework.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/Framework.html&lt;/a&gt;&lt;/p&gt;</description>
      <category>IOS</category>
      <author>clamp</author>
      <guid isPermaLink="true">https://clamp-coding.tistory.com/524</guid>
      <comments>https://clamp-coding.tistory.com/524#entry524comment</comments>
      <pubDate>Mon, 25 Nov 2024 22:24:42 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] Decorator 패턴을 활용한 에러 처리</title>
      <link>https://clamp-coding.tistory.com/523</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;h2 data-ke-size=&quot;size26&quot;&gt;데코레이터 패턴&lt;/h2&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;Wrapper 패턴이라고도 불립니다.&lt;/p&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;Wrapper내부에 Wrapper&lt;/b&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;393&quot; data-origin-height=&quot;474&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxjHsa/btsKCLLeou4/GmQsQqkGu3YmhwhJYanSKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxjHsa/btsKCLLeou4/GmQsQqkGu3YmhwhJYanSKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxjHsa/btsKCLLeou4/GmQsQqkGu3YmhwhJYanSKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxjHsa%2FbtsKCLLeou4%2FGmQsQqkGu3YmhwhJYanSKK%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;419&quot; data-origin-width=&quot;393&quot; data-origin-height=&quot;474&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;이 인형 하나가 Wrapper가 되고 그 Wrapper가 또 Wrapper를 가지고 있는 구조입니다. 각자의 Wrapper는 각자의 역할을 담당합니다.&lt;/p&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;b&gt;데코레이터 패턴의 특징&lt;/b&gt;은 두 가지입니다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. &quot;동적&quot;으로 런타임에 기능을 변경하고 추가할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 기존 객체를 건들지 않는 방식으로 기능을 추가할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 핵심 기능을 담당하는 Component Interface(Protocol)&lt;/p&gt;
&lt;pre id=&quot;code_1731235917700&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public protocol ErrorHandler {
    func handleError(error: Error)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 이를 채택해서 구현하는 ConcreteComponent 타입을 구현합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1731236015191&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class ConcreteErrorHandler: ErrorHandler {
    
    func handleError(error: Error) {
        print(error)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Error를 프린트하는 Concrete 컴포넌트를 작성했습니다.&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 data-ke-size=&quot;size18&quot;&gt;3. 데코레이터 구현&lt;/p&gt;
&lt;pre id=&quot;code_1731236188674&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class UpdateUIErrorHandlerDecorator: ErrorHandler {
    
    private var wrapped: ErrorHandler
    private var updateUI: (() -&amp;gt; Void)
    
    init(
        wrapped: ErrorHandler,
        updateUI: @escaping (() -&amp;gt; ())
    ) {
        self.wrapped = wrapped
        self.updateUI = updateUI
    }
    
    func handleError(error: Error) {
        wrapped.handleError(error: error)
        DispatchQueue.main.async { [weak self] in
            self?.updateUI()
        }
    }
}&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;이제 Concrete를 꾸며줄 데코레이터들을 추가하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 데코레이터들은 Wrapper들을 소유합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데코레이터를 보면 wrapped라는 Component Interface인 ErrorHandler라는 타입을 소유합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자에서는 updateUI라는 클로저를 받는데 이 데코레이터를 사용하는 뷰 혹은 뷰모델에서 업데이트할 UI를 정의해주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 뷰 모델에서 통신에 실패하고 서버에서 받은 에러를 뷰에 보여줘야 할 경우 이를 어떻게 사용할 수 있을까요&lt;/p&gt;
&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_1731236633071&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;open class ViewModel: ObservableObject {
    @Published var isAdShowing: Bool = true
    @Published var isShowingError: Bool = false
    @Published var errorMessage: String = &quot;&quot;
    
    private var errorHandler: ErrorHandler?
    
    init() {
        let concreteErrorHandler: ErrorHandler = ConcreateErrorHandler()
        let updateUIDecorator: ErrorHandler = UpdateUIErrorHandlerDecorator(
            wrapped: concreteErrorHandler,
            updateUI: { [weak self] in
                self?.isAdShowing = false
            }
        )
        self.errorHandler = updateUIDecorator
    }
}&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;근데 만약 런타임에 에러 처리를 바꿔야 한다면? UI업데이트가 사라져야 한다면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 시점에 errorHandler를 변경해주면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1731236791404&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 어떤 시점에 
        let concreteErrorHandler: ErrorHandler = ConcreateErrorHandler()
        self.errorHandler = concreteErrorHandler&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 타입의 에러를 처리하는 데코레이터, 에러 발생 로그를 서버로 보내는 데코레이터 등&amp;nbsp; 필요한 기능을 &lt;b&gt;조합&lt;/b&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 data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Solid 원칙중 하나인 단일 책임 원칙 SRP(Single Responsibility Principle)을 준수하게 됩니다.&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;br /&gt;혹시 틀린점이나 부족한 점이 있으면 알려주시면 감사하겠습니다.&lt;/p&gt;</description>
      <category>IOS</category>
      <author>clamp</author>
      <guid isPermaLink="true">https://clamp-coding.tistory.com/523</guid>
      <comments>https://clamp-coding.tistory.com/523#entry523comment</comments>
      <pubDate>Sun, 10 Nov 2024 20:13:19 +0900</pubDate>
    </item>
    <item>
      <title>[SwiftUI] - 자식뷰에 데이터를 어떻게 전달해야할까 그리고 뷰 갱신이 이루어 질까? with. MVVM</title>
      <link>https://clamp-coding.tistory.com/522</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;UIKit, RxSwift, ReactorKit, RxFlow, SwiftUI, Combine, Asycn Await, MVVM, MVC등 많은 기술을 거쳐왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에는 SwiftUI와 MVVM을 주로 적용시켜왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UIKit을 사용 할 때에는 MVVM-InOut패턴과 RXSwift을 적용해 어느정도의 데이터 흐름을 파악하기 용이했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Input을 정의해 이벤트가 어디서 시작되는지 알 수 있었고 스트림을 통해 방출되는 데이터는 Output을 활용해 뷰가 구독하고 갱신을 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 SwiftUI로 넘어오며 @Binding을 활용해 데이터의 양방향 흐름이 가능해졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모뷰의 모델이 갖고있는 데이터를 자식뷰에 전달하면 자식뷰가 이 데이터를 변경할 시 부모뷰의 모델이 변경됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Oct-28-2024 21-57-26.gif&quot; data-origin-width=&quot;414&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R3nmw/btsKnnCXBmS/RIJRiI89NfkYBJk8OZd580/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R3nmw/btsKnnCXBmS/RIJRiI89NfkYBJk8OZd580/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R3nmw/btsKnnCXBmS/RIJRiI89NfkYBJk8OZd580/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/R3nmw/btsKnnCXBmS/RIJRiI89NfkYBJk8OZd580/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;414&quot; height=&quot;440&quot; data-filename=&quot;Oct-28-2024 21-57-26.gif&quot; data-origin-width=&quot;414&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1730120310750&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class BindingObservableModel: ObservableObject {
    @Published var text: String = &quot;&quot;
    
    func addText() {
        text += &quot;\(text.count)&quot;
    }
}

struct BindingTEST: View {
    
    @ObservedObject var viewModel = BindingObservableModel()
    
    var body: some View {
        Button(
            action: {
                viewModel.addText()
            },
            label: {
                Text(&quot;ParentView Button Text: \(viewModel.text)&quot;)
            }
        )
        
        Spacer()
            .frame(height: 50)
        
        BindingTESTSubView(text: $viewModel.text)
    }
}

struct BindingTESTSubView: View {
    
    @Binding var text: String
    
    var body: some View {
        Button(
            action: {
                deleteText()
            },
            label: {
                Text(&quot;SubView Button Text: \(text)&quot;)
            }
        )
    }
    
    func deleteText() {
        text.removeLast()
    }
}&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;Binding은 양방향 데이터 흐름이 가능하기 때문에 부모뷰에서 전달받은 text: Binding&amp;lt;String&amp;gt;을 변경해도 부모가 관리하는 Data도 함께 변경됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 Binding이 아닌 일반 text로 넘겼을 때는 어떨까요&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;598&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dej3eq/btsKlYj6gQl/AioQWyQRJhwpGQci80IC1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dej3eq/btsKlYj6gQl/AioQWyQRJhwpGQci80IC1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dej3eq/btsKlYj6gQl/AioQWyQRJhwpGQci80IC1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdej3eq%2FbtsKlYj6gQl%2FAioQWyQRJhwpGQci80IC1K%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;494&quot; height=&quot;289&quot; data-origin-width=&quot;598&quot; data-origin-height=&quot;350&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 불가능 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SwiftUI의 상태 관리 방법 @State, @Binding, @ObservedObject등을 통해 상태관리&lt;/b&gt;를 해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 만약 자식뷰가 어떠한 로직을 개별적으로 담당해서 수행해야 할 때는 어떻게 해야할까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셀이 있는데 각 셀마다 상당히 복잡한 로직을 갖고있고, 각 셀이 담당하게 하려면?&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;1. 뷰모델에 Published를 만들고 부모에서 데이터가 변경되었을 때 자식도 동일한 데이터를 유지하기 위해 바인딩을 한다?&lt;/h3&gt;
&lt;pre id=&quot;code_1730121510745&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class BindingTESTSubViewModel: ObservableObject {
    
    @Published var text: String
    private var cancellables = Set&amp;lt;AnyCancellable&amp;gt;()
    
    init(text: Binding&amp;lt;String&amp;gt;) {
        
        self.text = text.wrappedValue
        
        self.$text
            .dropFirst()
            .sink(receiveValue: {
                text.wrappedValue = $0
            })
            .store(in: &amp;amp;cancellables)
    }
    
    func deleteText() {
        text.removeLast()
    }
}

struct BindingTESTSubView: View {
    
    @ObservedObject var viewModel: BindingTESTSubViewModel
    
    var body: some View {
        Button(
            action: {
                viewModel.deleteText()
            },
            label: {
                Text(&quot;SubView Button Text: \(viewModel.text)&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;p data-ke-size=&quot;size16&quot;&gt;만약 부모뷰에서 데이터가 변경된 경우 SubView의 ViewModel이 ObservedObject이므로&amp;nbsp; 다시그려질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식뷰에서 데이터가 변경된 경우 자식뷰의 published변수를 생성자에 전달받은 Binding에 바인딩 시켜놓았으니 부모뷰의 데이터가 변경됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 구조는 깊이가 조금만 깊어져도 데이터의 변경이 어디서 이뤄지는지 파악하기 힘들기 때문에 선호하지 않습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;** 만약 이 경우 자식뷰의 ViewModel이 StateObject라면 다시 그려지지 않습니다. &lt;b&gt;StateObject는 자신의 뷰가 생명주기를 담당하기 때문&lt;/b&gt;입니다. &lt;b&gt;ObservedObject라면 부모뷰에서 전달받은 데이터가 변경된다면 재 호출&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;2. 자식뷰의 뷰모델이 Binding을 사용한다.&lt;/h3&gt;
&lt;pre id=&quot;code_1730121983948&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class BindingTESTSubViewModel: ObservableObject {
    @Binding var text: String
    
    init(text: Binding&amp;lt;String&amp;gt;) {
        self._text = text
    }
    
    func deleteText() {
        text.removeLast()
    }
}

struct BindingTESTSubView: View {
    
    @ObservedObject var viewModel: BindingTESTSubViewModel
    
    var body: some View {
        Button(
            action: {
                viewModel.deleteText()
            },
            label: {
                Text(&quot;SubView Button Text: \(viewModel.text)&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;자식뷰의 뷰모델이 Binding자체를 받아 양방향 바인딩이 가능하고 ObservedOjbect이기 때문입니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 StateObject라면 어떻게 될까요&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Oct-28-2024 22-29-18.gif&quot; data-origin-width=&quot;414&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRa0jC/btsKmoieT7v/zSgSLoKeisNk75OBQ8Seek/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRa0jC/btsKmoieT7v/zSgSLoKeisNk75OBQ8Seek/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRa0jC/btsKmoieT7v/zSgSLoKeisNk75OBQ8Seek/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bRa0jC/btsKmoieT7v/zSgSLoKeisNk75OBQ8Seek/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;414&quot; height=&quot;440&quot; data-filename=&quot;Oct-28-2024 22-29-18.gif&quot; data-origin-width=&quot;414&quot; data-origin-height=&quot;440&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;b&gt;뷰는 Published가 변경되면 이를 감지하고 다시그립니다.&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;ObservedOjbect일 때 그려질 수 있었던 이유는 ViewModel의 데이터가 변경되고 업데이트가 되고 뷰가 다시 로드되서&lt;/b&gt;이지 ObservableObject의 @Binding을 바라보고 있는 View는 다시 그려지지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Published가 변경되어야 ObservableObject의 objectWillChange가 이벤트를 발생시켜 뷰를 다시그립니다.&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;b&gt; SwiftUI는 양방향 바인딩&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;여기서 SwiftUI와 MVVM의 문제점을 느꼈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;뷰와 뷰모델의 구조가 복잡해질수록&amp;nbsp;언제 어디서 어느 데이터가 변경되는지 파악하기 매우 힘듭니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 TCA를 학습하던중 의문점이 생겼습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰 갱신이 필요한 데이터라면 지금까지 그냥 Binding으로 전달했는데 단방향 아키텍처와 어울리지 않다는 생각에 View 업데이트가 이루어 지는 방식을 다시 고민했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 많은 블로그가 State업데이트를 View가 감지하는 방식이라고 Binding을 많이설명합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 공식문서에서는 Binding을 아래와 같이 설명합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1d1f; text-align: left;&quot; data-v-535cd656=&quot;&quot; data-v-5bf7a092=&quot;&quot; data-v-611b62f7=&quot;&quot; data-v-15d7e6ba=&quot;&quot;&gt;A property wrapper type that can read and write a value owned by a source of truth.&lt;/div&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;소유된 값을 읽고 쓸 수 있게&lt;/b&gt; 해주는 친구입니다. 뷰 갱신을 위해 필요한게 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 코드를 봅시다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1730126206469&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct ParentView: View {
    @State private var count = 0
    
    var body: some View {
        VStack {
            Button(&quot;Increment&quot;) {
                count += 1
            }
            ChildView(count: count)
        }
    }
}

struct ChildView: View {
    let count: Int
    
    var body: some View {
        VStack {
            Text(&quot;Count: \(count)&quot;)
            Text(&quot;Fixed Text&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ㅁㅁㅁ.gif&quot; data-origin-width=&quot;414&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bajqDj/btsKmKL8laJ/lzD3qVEiqBsinbPKtCZAkK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bajqDj/btsKmKL8laJ/lzD3qVEiqBsinbPKtCZAkK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bajqDj/btsKmKL8laJ/lzD3qVEiqBsinbPKtCZAkK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bajqDj/btsKmKL8laJ/lzD3qVEiqBsinbPKtCZAkK/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;414&quot; height=&quot;440&quot; data-filename=&quot;ㅁㅁㅁ.gif&quot; data-origin-width=&quot;414&quot; data-origin-height=&quot;440&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;Binding이 아니어도 뷰 갱신은 잘 이루어집니다.&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_1730126334991&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct ChildView: View {
    let count: Int
    
    init(count: Int) {
        self.count = count
        print(&quot;ChildView init with count: \(count)&quot;)
    }
    
    var body: some View {
        VStack {
            Text(&quot;Count: \(count)&quot;)
                .onAppear {
                    print(&quot;Text(\&quot;Count: \\(count)\&quot;) appeared with count: \(count)&quot;)
                }
            Text(&quot;Fixed Text&quot;)
                .onAppear {
                    print(&quot;Text(\&quot;Fixed Text\&quot;) appeared&quot;)
                }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;123.gif&quot; data-origin-width=&quot;414&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/K0Uoz/btsKnq7hPeG/eGY1aTI95cVsl1eebc1fIK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/K0Uoz/btsKnq7hPeG/eGY1aTI95cVsl1eebc1fIK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/K0Uoz/btsKnq7hPeG/eGY1aTI95cVsl1eebc1fIK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/K0Uoz/btsKnq7hPeG/eGY1aTI95cVsl1eebc1fIK/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;414&quot; height=&quot;176&quot; data-filename=&quot;123.gif&quot; data-origin-width=&quot;414&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;ChildView는 Init이 호출되고 각 UI컴포넌트들의 ONAppear가 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 count가 증가할 때 마다 &lt;b&gt;Init이 계속 호출&lt;/b&gt;되지만 &lt;b&gt;Init이 호출된 후 ONAppear가 다시 호출되진 않습니다.&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;ChildView가 부모뷰로 부터 값을 전달 받는 경우 그 값이 변경되면 ChildView 전체가 다시렌더링 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 body가 다시 렌더링 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;이 방식은 자식 뷰가 부모 뷰로부터 받는 값이 변경될 때 자식뷰에서 뷰 전체가 새로 그려지는 것처럼 동작하지만 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;그래서 count가 변경되면 ChildView가 다시 랜더링될 때 ChildView의 body 전체 재 실행되며 각 뷰 들이 다시 평가됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 SwiftUI내부 최적화로 변화가 없는 부분은 실제 화면 렌더링에선 영향을 받지 않습니다. 즉&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1730126603848&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Text(&quot;Count: \(count)&quot;) // count 변경 시 평가가 이루어지고 값이 변경되었다면 Text가 다시 렌더링됩니다.
Text(&quot;Fixed Text&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;p data-ke-size=&quot;size16&quot;&gt;즉, SwiftUI는 모든 하위 뷰의 &lt;span&gt;body&lt;/span&gt;를 다시 호출하지만, &lt;b&gt;뷰 상태에 변경이 없다고 판단되면 이를 화면에 다시 그리지 않는 방식으로 성능을 최적화&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;b&gt;부모 뷰의&amp;nbsp;모델에서 Published 혹은 State로 관리 할 경우 내부적으로 StateObject를 사용하지 않는다면 최 하위 자식뷰 까지 갱신이 이루어집니다.&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;</description>
      <category>SwiftUI+TCA+Combine</category>
      <author>clamp</author>
      <guid isPermaLink="true">https://clamp-coding.tistory.com/522</guid>
      <comments>https://clamp-coding.tistory.com/522#entry522comment</comments>
      <pubDate>Mon, 28 Oct 2024 23:24:03 +0900</pubDate>
    </item>
    <item>
      <title>[iOS Swift ] AES-256 암호화</title>
      <link>https://clamp-coding.tistory.com/521</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;회사 프로젝트에서 AES-256암호화로 웹과 데이터를 주고받아야 하는 일이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;서버: App으로 광고 전달&lt;br /&gt;&lt;br /&gt;App: 특정 조건에 해당하는 경우 사용자 정보를 암호화하고 내부 규정에 맞춰 웹으로 이동시키는 작업&lt;br /&gt;이 경우 웹으로 이동 시 사용자 정보를 함께 전달해야 하므로 AES-256으로 암호화 하고 서버는 웹으로 전달받은 내용을 복호화 하여 데이터 처리&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 AES암호화는 양방향 암호화입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;단방향 암호화&lt;/b&gt;&lt;/span&gt;: 한 번 암호화하면 원래 데이터로 되돌릴 수 없는 암호화 방식입니다. 주로 해시함수를 사용하여 데이터를 특정 길이의 고정된 데이터로 변환합니다 대표적으로 SHA-256기법이 있습니다. 복호화가 불가능하다는 특징이 있으며 주로 비밀번호 저장, 파일 무결성 체크에 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;양방향 암호화&lt;/b&gt;&lt;/span&gt;: 암호화된 데이터를 필요할 때 복구하여 사용할 수 있는 경우에 유용합니다. 키의 보안성이 아주 중요하며 키가 유출되면 암호화된 데이터도 쉽게 복호화 할 수 있습니다. AES-256이 있습니다.&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;AES-256의 특징&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &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;2. &lt;b&gt;256비트 키 사용&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- AES알고리즘은 128, 192, 256비트의 키 길이를 사용할 수 있습니다. 이 중 AES256은 가장 긴 키 길이를 사용하여 보안 강도가 가장 높습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. &lt;b&gt;고속성과 보안성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 블록 암호 방식중에서 상대적으로 빠른 속도를 제공하며 256비트 키 길이는 현재까지 알려진 모든 공격 기법에 대해 안전한 수준이라고 합니다.&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AES256은 &lt;b&gt;블록 암호화 방식&lt;/b&gt;을 사용하여 데이터를 암호화 합니다. 즉 데이터를 128비트(16바이트)크기의 블록 단위로 나누어 각 블록을 암호화합니다.&lt;/p&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;b&gt;키 생성&lt;/b&gt;: 256비트의 비밀키를 생성합니다&lt;/p&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;b&gt;암호화 과정&lt;/b&gt;: AES256 암호화는 10, 12, 14라운드로 반복되며 각 라운드는 4가지 연산으로 구성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;&lt;b&gt;SubBytes&lt;/b&gt;: 각 바이트를 &lt;b&gt;S-box&lt;/b&gt;라고 불리는 표를 통해 대체하여 비선형 변환을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;&lt;b&gt;ShiftRows&lt;/b&gt;: 행을 기준으로 각 바이트를 &lt;b&gt;좌측으로 이동&lt;/b&gt;하여 데이터를 재배열합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;&lt;b&gt;MixColumns&lt;/b&gt;: 열을 기준으로 행렬 곱셈을 수행하여 &lt;b&gt;행 간 상호작용&lt;/b&gt;을 증가시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;&lt;b&gt;AddRoundKey&lt;/b&gt;: 라운드 키와 XOR 연산을 수행하여 블록 데이터를 섞습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 복호화 과정: 복호화는 암호화의 역순으로 수행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;모드의 종류&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;1.&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;b&gt;ECB (Electronic Codebook Mode)&lt;/b&gt;:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;동일한 평문 블록이 동일한 암호문 블록으로 암호화되므로, &lt;b&gt;패턴 노출&lt;/b&gt;의 위험이 큽니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;데이터 양이 작을 때나 성능이 중요한 경우에 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;2.&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;b&gt;CBC (Cipher Block Chaining Mode)&lt;/b&gt;:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;각 블록을 암호화하기 전에 &lt;b&gt;이전 블록의 암호문&lt;/b&gt;과 XOR 연산을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;*초기화 벡터(IV)가 필요하며, 평문의 변화가 암호문 전체에 영향을 미치므로 보안성이 더 높습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;3.&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;b&gt;CFB (Cipher Feedback Mode)&lt;/b&gt;:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;스트림 암호처럼 동작하며, 실시간 데이터 암호화에 적합합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;4.&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;b&gt;OFB (Output Feedback Mode)&lt;/b&gt;:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;비트 오류가 전체 암호문에 영향을 주지 않도록 설계되었으며, 스트림 암호와 유사한 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;5.&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;b&gt;GCM (Galois/Counter Mode)&lt;/b&gt;:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;&lt;b&gt;데이터 무결성&lt;/b&gt;을 제공하는 인증 모드입니다. 빠른 속도와 높은 보안성을 제공하여 많이 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;사용시 유의할 점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;b&gt;키 관리&lt;/b&gt;: AES256의 보안 강도는 키의 안전한 관리에 따라 결정됩니다. 키가 유출되면 암호화의 의미가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. &lt;b&gt;초기화 벡터(IV)&lt;/b&gt;: CBC모드와 같은 경우 IV가 필요합니다. IV는 키와 함께 안전하게 관리되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. &lt;b&gt;패딩(padding)&lt;/b&gt;: AES256은 고정 블록 크기(16바이트)로 암호화 하므로 평분의 길이가 16배수가 아닌 경우 패딩을 추가해야합니다.&lt;/p&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;AES256은 현재로서는 가장 안전하고 널리 사용되는 대칭키 암호화 방식이라고 합니다. 데이터의 기밀성을 보장하는데 매우 효과적이지만 키 관리와 초기화 벡터의 적절한 사용이 중요합니다.&lt;/p&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;pre id=&quot;code_1728309119415&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import CryptoSwift

public class AES256Cryption {
    
    private static let SECRET_KEY = &quot;15647894536123156489496543213153&quot;
    private static let IV = &quot;asdfasvzxcvasdff&quot;
    
    static func encrypt(string: String) throws -&amp;gt; String {
        guard !string.isEmpty else { return &quot;&quot; }
        
        return try getAESObject().encrypt(string.bytes).toBase64()
    }
    
    static func decrypt(string: String) throws -&amp;gt; String {
        guard !string.isEmpty else { return &quot;&quot; }
        
        let data = Data(base64Encoded: string)
        let decrypted = try getAESObject().decrypt([UInt8](data!))
        
        return String(bytes: decrypted, encoding: .utf8) ?? &quot;&quot;
    }
    
    private static func getAESObject() throws -&amp;gt; AES {
        let keyDecodes: Array&amp;lt;UInt8&amp;gt; = Array(SECRET_KEY.utf8)
        let ivDecodes: Array&amp;lt;UInt8&amp;gt; = Array(IV.utf8)
        let aesObject = try AES(key: keyDecodes, blockMode: CBC(iv: ivDecodes), padding: .pkcs5)
        
        return aesObject
    }
}&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;AES256으로 &quot;Hello, world!&quot;을 암호화 한 결과입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;304&quot; data-origin-height=&quot;64&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMVhWF/btsJYHBiAyQ/1toGAnPNuUkzIrpnE0lY5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMVhWF/btsJYHBiAyQ/1toGAnPNuUkzIrpnE0lY5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMVhWF/btsJYHBiAyQ/1toGAnPNuUkzIrpnE0lY5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMVhWF%2FbtsJYHBiAyQ%2F1toGAnPNuUkzIrpnE0lY5K%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;304&quot; height=&quot;64&quot; data-origin-width=&quot;304&quot; data-origin-height=&quot;64&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;CryptoSwift&lt;br /&gt;&lt;a href=&quot;https://github.com/krzyzanowskim/CryptoSwift&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/krzyzanowskim/CryptoSwift&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1728309210460&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - krzyzanowskim/CryptoSwift: CryptoSwift is a growing collection of standard and secure cryptographic algorithms implemen&quot; data-og-description=&quot;CryptoSwift is a growing collection of standard and secure cryptographic algorithms implemented in Swift - krzyzanowskim/CryptoSwift&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/krzyzanowskim/CryptoSwift&quot; data-og-url=&quot;https://github.com/krzyzanowskim/CryptoSwift&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/tpRpW/hyXd7Aqnp6/J1hwGUHKCvajuzUVg8JC61/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/rH1PQ/hyXd6IfYpS/TkykIHLxeh55Xe0dW2tqbk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/krzyzanowskim/CryptoSwift&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/krzyzanowskim/CryptoSwift&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/tpRpW/hyXd7Aqnp6/J1hwGUHKCvajuzUVg8JC61/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/rH1PQ/hyXd6IfYpS/TkykIHLxeh55Xe0dW2tqbk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;GitHub - krzyzanowskim/CryptoSwift: CryptoSwift is a growing collection of standard and secure cryptographic algorithms implemen&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;CryptoSwift is a growing collection of standard and secure cryptographic algorithms implemented in Swift - krzyzanowskim/CryptoSwift&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>IOS</category>
      <author>clamp</author>
      <guid isPermaLink="true">https://clamp-coding.tistory.com/521</guid>
      <comments>https://clamp-coding.tistory.com/521#entry521comment</comments>
      <pubDate>Mon, 7 Oct 2024 22:58:13 +0900</pubDate>
    </item>
    <item>
      <title>[SwiftUI] ForEach가 View를 업데이트 하는 방법과 @StateObject를 사용할 때 문제점(그리고 그 뒷 이야기..)</title>
      <link>https://clamp-coding.tistory.com/520</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;SwiftUI를 사용할 때 ForEach를 자주 사용합니다.&lt;/p&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;List를 사용할 수 있지만 List를 사용하는 방법보단&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ForEach + LazyVStack + ForEach를 사용하는 방법이 더욱 다양한 기능을 적용시키기 좋아서 후자를 선택해 사용하고있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ForEach와 StateObject를 사용할 때에는 몇 가지 문제점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 ForEach는 View의 id를 기준으로 뷰를 구분하고 업데이트합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 @Published 프로퍼티가 바뀌게되어도 뷰를 업데이트 하긴 하죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 @StateObject를 사용해 ForEach를 사용한다고 가정할 경우 뷰 업데이트에 대한 문제가 생길 수 있습니다.&lt;/p&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_1719540436717&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct Message: Identifiable {
    var id: AnyHashable = UUID().uuidString
    var content: String
    var didRead: Bool = false
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 message를 구분하기 위해, 또는 ForEach를 사용하기위해, 또는 원하는 message로 스크롤 해야하는 기능이 필요할 때 등등 다양한 상황을 대비하기위해 각 message에 id를 부여할겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 우리는 forEach를 어떻게 사용할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719540758965&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ForEach(viewModel.messages, id: \.id) { message in
        ChatBubble(item: message)                    
}&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;LazyVStack, ScrollView는 생략했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatBubble의 구조에서 문제가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719540852385&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct ChatBubble: View {
    
    @StateObject var viewModel: ChatBubbleViewModel
    
    init(message: Message) {
        self._viewModel = StateObject(wrappedValue: ChatBubbleViewModel(item: message))
    }
    
    var body: some View {
        HStack {
            Text(&quot;\(viewModel.message.content)&quot;)
            
            RoundedRectangle(cornerRadius: 30)
                .frame(width: 30, height: 30)
                .foregroundStyle(viewModel.message.didRead ? Color.green : Color.black)
        }
    }
}&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_1719541173476&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class ChatBubbleViewModel: NSObject, ObservableObject {
    @Published var message: Message
    
    init(item: Message) {
        self.message = item
    }
}&lt;/code&gt;&lt;/pre&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;최상위 ForEach에서 id를 사용한다.&lt;/li&gt;
&lt;li&gt;ChatBubble의 ViewModel이 @StateObject이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Why?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번 이유가 왜 문제인지 생각해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@ObservedObject로 선언하면 해당 프로퍼티는 외부에서 주입&lt;/b&gt;받습니다. 그럼 viewModel이 지속적으로 생성되어 새로운 프로퍼티가 할당되고 할당된 프로퍼티의 &lt;b&gt;데이터에 맞게 UI가 업데이트&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;하지만 들어가는 채팅 버블이 다양하고 이 버블은 스크롤되어 메모리에 올라가고 / 내려가고를 반복합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채팅앱은 ForEach의 하나의 row에 다양한  Bubble이 들어갈 수 있습니다. 예를 들어 동영상, 사진, 링크, 텍스트, 이모티콘, 위치, 파일 등등등...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 채팅앱의 특성상 다양한 데이터를 보여줘야하고 ObservedObject로 사용하면 지속적으로 뷰모델을 생성해서 주입하게되면 성능저하가 발생하더군요, 또한 특정 bubble에서는 뷰의 상태가 변할때마다 화면이 깜빡거리는 이슈가 발생했습니다.(뷰가 새로그려지는 시점이라고 생각합니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 @StateObject를 사용하기로 했습니다.&lt;/p&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;@StateObject는 내부에서 뷰모델을 생성하고 메모리공간을 유지&lt;/b&gt;합니다. 매번 새로운 데이터와 함께 뷰모델을 외부에서 주입받지 않습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 2번 문제로 인해 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 문제 &quot;최상위 ForEach에서 id를 사용한다.&quot; 가 문제가 되는 이유는 다음과 같습니다.&lt;/p&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;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;ForEach는 id를 기준으로 view를 업데이트합니다.&amp;nbsp; ==&amp;gt;&amp;gt; id값이 변하지 않으면 View를 업데이트 하지 않는다.&lt;/span&gt;&lt;/b&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;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 내부에 didRead값이 변하더라도 id가 변하지 않으면 뷰를 다시그리지 않습니다.&lt;/p&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_1719541811297&quot; class=&quot;groovy&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;Button(action: {
      viewModel.messages[0].didRead = true
                        
    }, label: {
      Text(&quot;updateFirstItem&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;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&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;updateFirstItem을 클릭하면 0번 index의 didRead를 true로 바꿉니다.&lt;/p&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;b&gt;* 참고로 스유로 채팅앱을 개발할 때 스크롤뷰를 뒤집어서 사용하는 경우가 많습니다. 스크롤뷰가 뒤집어져있어 &quot;0입니다&quot;가 0번인덱스 라는걸 알아주세요&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;해결방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결한 제 아이디어는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2326; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;message를 구분할 Id와 View에서 사용할 Id를 구분한다.&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swift에는 Hashable라는 프로토콜이 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 hashvalue를 제공하고 다양한 값으로 hash함수를 사용할 수 있는 기능을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 Hash함수란&amp;nbsp; N =&amp;gt; 해시함수 =&amp;gt; N에 대한 고유한값 이 나오게 되는 함수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719542056295&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct Message: Identifiable, Hashable {    
    func hash(into hasher: inout Hasher) {
        hasher.combine(messageId)
        hasher.combine(content)
        hasher.combine(didRead)
    }
    
    var id: AnyHashable {
        return hashValue
    }
    
    var messageId: AnyHashable = UUID().uuidString
    var content: String
    var didRead: Bool = false
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Message를 구분할 id와 View에서 사용할 id를 구분합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hash는 상태에 따라 view가 변경되어야 하는 값을 넣습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) isSending, isDownloaded 등등.. 다양한 값이 들어갈 수 있을거에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나라도 변경되면 View의 id값이 변경되고 @StateObject라도 forEach는 id의 변경을 감지하고 View를 다시그리게됩니다.&lt;/p&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;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&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;&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;ChatBubbleViewModel을 사용하지 않고 ChatRoomViewModel을 사용하도록 변경했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatBubble은 ChatRoom 내부의 뷰 컴포넌트이고 각자의 ViewModel이 필요하지 않다고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StateObject와 ObservedObject, SwiftUI의 View 갱신 방법등에 대해 공부하는 내용이라고 생각해 주시면 감사하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터의 id를 View의 id로 설정하는 방법은 좋은 방법이 아닙니다.&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;메시지는 ChatRoomViewModel이 관리하며 하위 뷰 들은 Binding을 받아서 데이터를 보여주는 역할을 하고 UI로직만을 처리하는 방식이 올바른 방법이라 생각됩니다.&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>SwiftUI+TCA+Combine</category>
      <author>clamp</author>
      <guid isPermaLink="true">https://clamp-coding.tistory.com/520</guid>
      <comments>https://clamp-coding.tistory.com/520#entry520comment</comments>
      <pubDate>Fri, 28 Jun 2024 11:41:11 +0900</pubDate>
    </item>
    <item>
      <title>[SwiftUI] SwiftUI는 뷰를 어떻게 구분하고 업데이트 하는가.(Identity, Lifetime)</title>
      <link>https://clamp-coding.tistory.com/519</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI는 뷰를 어떻게 구분하고 업데이트할까요? 또 Identity, Lifetime등 용어는 무엇일까요?&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;동등성(equality)와 동일성(Identity)입니다.&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;동등성(Equality)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;157&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHAZxN/btsGFf9M21B/bMW98kpzhMSVU7eLGJylA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHAZxN/btsGFf9M21B/bMW98kpzhMSVU7eLGJylA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHAZxN/btsGFf9M21B/bMW98kpzhMSVU7eLGJylA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHAZxN%2FbtsGFf9M21B%2FbMW98kpzhMSVU7eLGJylA1%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;529&quot; height=&quot;157&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;157&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;Equatable로 비교하면 이 둘은 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 &lt;b&gt;상태&lt;/b&gt;만을 비교하면 이 둘은 같다고 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;한 clamp가 있다고 가정해봅시다.&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;610&quot; data-origin-height=&quot;181&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1AcVZ/btsGFyVAcdh/cyPp9GhBRZhXGWRvZqc8i0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1AcVZ/btsGFyVAcdh/cyPp9GhBRZhXGWRvZqc8i0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1AcVZ/btsGFyVAcdh/cyPp9GhBRZhXGWRvZqc8i0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1AcVZ%2FbtsGFyVAcdh%2FcyPp9GhBRZhXGWRvZqc8i0%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;610&quot; height=&quot;181&quot; data-origin-width=&quot;610&quot; data-origin-height=&quot;181&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;우리는 이름도 같고, 나이도 같기 때문에 같은 clamp라고 생각 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Equatable로 비교하면 다르다고 나옵니다. 상태가 다르니까요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 우리가 저 둘을 동일한 clamp라고 생각하려면, 즉 같은 clamp로 &lt;b&gt;&quot;식별&quot;&lt;/b&gt;하려면 무언가가 필요합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 주민등록번호가 될 수 있겠지만 코드에서 사용할 순 없기 때문에 무언가 &lt;b&gt;식별자&lt;/b&gt;가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 Identifiable 프로토콜이 탄생하게 되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;203&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tqTfV/btsGCvzayWb/8qb7qhkwb5qZMwT29W9IYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tqTfV/btsGCvzayWb/8qb7qhkwb5qZMwT29W9IYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tqTfV/btsGCvzayWb/8qb7qhkwb5qZMwT29W9IYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtqTfV%2FbtsGCvzayWb%2F8qb7qhkwb5qZMwT29W9IYk%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;670&quot; height=&quot;203&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;203&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;이렇게 되면 good, bad 모두 같은 사람이라고 판단할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우 이 둘의 identity(동일성)이 같다고 할 수 있네요.&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;blockquote data-ke-style=&quot;style3&quot;&gt;UIKit은 클래스 타입이기 때문에 뷰를 할당하므로 얻는 포인터가 그 뷰의 명시적 ID가 될 수 있습니다.&lt;br /&gt;SwiftUI는 value타입이기 떄문에 이런 값을 지속적으로 사용할 수 없습니다.&lt;br /&gt;그래서 Identity가 등장하게 되었으며 데이터 및 업데이트 주기에 영향을 미칩니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI는 값타입입니다. 값타입은&lt;b&gt; 복사에 의해 값이 전달되므로 원본을 변경하지 않고 각각의 복사본이 독립적으로 존재&lt;/b&gt;하게되죠. 변수에 값을 할당하거나 함수에 인자로 전달할 때 원본 값이 아닌 &lt;b&gt;독립된 복사본이 생성&lt;/b&gt;됩니다. 이는 각 복사본이 메모리 상에 다른 위치에 존재한다는 뜻이며 이러한 특성때문에 해당 뷰를 지속적으로 추적하기 힘듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 SwiftUI가 내부적으로 뷰의 메모리 주소를 ID로 사용한다면 뷰가 재생성 될 때 마다 새로운 id가 할당됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI는 &lt;b&gt;뷰의 상태와 속성의 변화를 감지하고 변화된 부분만을 효율적으로 업데이트&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;뷰의 새로운 상태와 이 전 상태를 비교하게 되는데 이 때 id가 다르다면 상태 비교가 불가능하게 됩니다.&amp;nbsp;&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;View Identity&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;명시적(Explicit) Identity&lt;/li&gt;
&lt;li&gt;구조적(Structural) Identity&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 뷰는 어떻게 id를 갖게 될까요&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;명시적(Explicit) Identity&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 직접 id를 설정해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 사소한 뷰 하나하나 id를 설정하는건 매우 비효율적이죠.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tfxsR/btsGCZGZyPh/5IkfeIyZoSd85BhqWpssJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tfxsR/btsGCZGZyPh/5IkfeIyZoSd85BhqWpssJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tfxsR/btsGCZGZyPh/5IkfeIyZoSd85BhqWpssJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtfxsR%2FbtsGCZGZyPh%2F5IkfeIyZoSd85BhqWpssJk%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;503&quot; height=&quot;196&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;196&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;h3 data-ke-size=&quot;size23&quot;&gt;구조적(Structural)Identity&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;266&quot; data-origin-height=&quot;153&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0ROo5/btsGG5FhRg1/KsUJZiBJkVaNKACErjZgmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0ROo5/btsGG5FhRg1/KsUJZiBJkVaNKACErjZgmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0ROo5/btsGG5FhRg1/KsUJZiBJkVaNKACErjZgmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0ROo5%2FbtsGG5FhRg1%2FKsUJZiBJkVaNKACErjZgmK%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;266&quot; height=&quot;153&quot; data-origin-width=&quot;266&quot; data-origin-height=&quot;153&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;SwiftUI는 &lt;b&gt;뷰 계층구조&lt;/b&gt;를 사용해 암시적 Identity를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 전반에 걸쳐 구조적 ID를 생성하므로 모든 뷰의 id를 생성할 필요는 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 뷰에서 if조건문을 사용하는 경우 상황에 따라 다른 뷰를 보이도록 사용하는데, SwiftUI는 정적인 if문 같은 경우 뷰 계층 구조에서 서로 &lt;b&gt;다른 브랜치&lt;/b&gt;를 만들며 각각 고유한 Id를 가지게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713244959387&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;some View =
    _ConditionalContent&amp;lt;
        EmptyView, 		// true
        UserListView	// false
&amp;gt;&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;이렇게 변환됩니다. View는 true, false에 대한 제네릭 타입을 받게됩니다. 그러므로 각 상황에 따라 다른 뷰라는걸 보장하며 이 보장을 통해 내부적으로 각각 다른 id를 부여합니다.&lt;/p&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;LifeTime 뷰와 데이터의 수명&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;View Value Lifetime&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;411&quot; data-origin-height=&quot;232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FXOxf/btsGFh01XQ7/ImPxFcuUs04MSSwmeZtdH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FXOxf/btsGFh01XQ7/ImPxFcuUs04MSSwmeZtdH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FXOxf/btsGFh01XQ7/ImPxFcuUs04MSSwmeZtdH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFXOxf%2FbtsGFh01XQ7%2FImPxFcuUs04MSSwmeZtdH1%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;411&quot; height=&quot;232&quot; data-origin-width=&quot;411&quot; data-origin-height=&quot;232&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;상위 뷰에서 SomeView를 호출한 상태입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 description = &quot;행복하다&quot; 라는 정보를 갖는 메모리를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 어떤 인터랙션을 통해 description = &quot;배고프다&quot; 로 변경되면 어떤 일이 일어날까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 공간에 description = &quot;배고프다&quot;에 대한 값을 세팅합니다. 그리고 &lt;b&gt;이전 값과 현재값을 변경한 후 변경되었다면 이전 값을 해제&lt;/b&gt;하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;View Lifetime&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SomeView의 id는 view가 init되고 onAppear가 불렸을 시 SwiftUi는 SomeView에 id를 할당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 흘러 새로운 value들이 업데이트 되어도 id는 동일합니다. 외부에 의해 변경되거나 해당 뷰가 제거되었을 때 id의 수명은 종료됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;837&quot; data-origin-height=&quot;174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d3SUPx/btsGFztDpYk/veudazkDITGPAlbTDnH8hk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d3SUPx/btsGFztDpYk/veudazkDITGPAlbTDnH8hk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d3SUPx/btsGFztDpYk/veudazkDITGPAlbTDnH8hk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd3SUPx%2FbtsGFztDpYk%2FveudazkDITGPAlbTDnH8hk%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;837&quot; height=&quot;174&quot; data-origin-width=&quot;837&quot; data-origin-height=&quot;174&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;h4 data-ke-size=&quot;size20&quot;&gt;State Lifetime&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwfitUI의 @State, @StateObject를 뷰에서 확인하면 해당 데이터는 &lt;b&gt;뷰의 lifetime동안 유지&lt;/b&gt;해야 한다는 것을 압니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰의 수명은 뷰의 id수명과 연관있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;State로 생성된 저장공간은 뷰의 id 수명과 지속되는 저장소입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 onAppear가 불렸을 때 State 변수를 메모리에 할당하고 id가 변경되지 않는다면 데이터가 변경되어도 동일한 공간을 사용합니다. view의 body가 다시 호출되어도 동일한 공간을 사용합니다.&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;266&quot; data-origin-height=&quot;153&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0ROo5/btsGG5FhRg1/KsUJZiBJkVaNKACErjZgmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0ROo5/btsGG5FhRg1/KsUJZiBJkVaNKACErjZgmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0ROo5/btsGG5FhRg1/KsUJZiBJkVaNKACErjZgmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0ROo5%2FbtsGG5FhRg1%2FKsUJZiBJkVaNKACErjZgmK%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;266&quot; height=&quot;153&quot; data-origin-width=&quot;266&quot; data-origin-height=&quot;153&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 EmptyView와 UserListView는&lt;b&gt; 다른 브랜치로 정의되고 id도 다를것이고&lt;/b&gt;, 조건이 true인 뷰에 State를 영구적인 저장 공간에 할당할 것입니다. false인 곳은 기존과 다른 id이기 때문에 사용하던 공간이 해제되겠습니다.&lt;/p&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;그렇다면 적절한 id를 사용하는 방법은 무엇일까요?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;명시적 identity를 사용하는 경우 안정적이고 고유한 id를 사용해야합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 name, number와 같은 프로퍼티는 &lt;b&gt;중복&lt;/b&gt;될 수 있습니다. id와 같은 &lt;u&gt;&lt;b&gt;고유한 요소&lt;/b&gt;&lt;/u&gt;를 사용해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;b&gt;변하지 않아야 합니다&lt;/b&gt;. id가 계속 변하지 않고 안정적이면 성능에 도움을 줍니다. 영구적으로 변하지 않을 id를 선택해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;구조적 identity를 사용하는 Dependent Code를 권장합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;if 조건문은 뷰 계층 구조에서 다른 브랜치를 사용하기 때문에 다른 id를 갖게됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 두 가지 상태를 표현하고 싶다면 &lt;b&gt;가능한 브랜치를 나누기보단 modifier를 이용&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;442&quot; data-origin-height=&quot;113&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uk56Z/btsGCW4DOVX/mFmrjuaiBVuC9QNHKpbe1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uk56Z/btsGCW4DOVX/mFmrjuaiBVuC9QNHKpbe1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uk56Z/btsGCW4DOVX/mFmrjuaiBVuC9QNHKpbe1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fuk56Z%2FbtsGCW4DOVX%2FmFmrjuaiBVuC9QNHKpbe1K%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;442&quot; height=&quot;113&quot; data-origin-width=&quot;442&quot; data-origin-height=&quot;113&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;</description>
      <category>SwiftUI+TCA+Combine</category>
      <author>clamp</author>
      <guid isPermaLink="true">https://clamp-coding.tistory.com/519</guid>
      <comments>https://clamp-coding.tistory.com/519#entry519comment</comments>
      <pubDate>Tue, 16 Apr 2024 14:53:15 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] Alamofire 네트워크 레이어 설계2. 토큰관리(Interceptor)</title>
      <link>https://clamp-coding.tistory.com/518</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Alamofire에서 토큰 갱신, 에러 처리등을 효율적으로 하기위해 제공해주는 Interceptor라는 프로토콜이 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Interceptor는 서버에 요청을 보내기 전에, 중간에 가로채서 어떠한 작업을 수행할 수 있게 해주고 요청의 결과로 Error가 발생한 경우 요청을 다시 할 것인지를 처리할 수 있게 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- adapt(): API호출 전에 URLRequest를 가로채서 어떤 작업을 할 수 있도록 해주는 메서드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- retry(): API호출의 결과로 Error가 발생한 경우 요청을 다시 할 것인지 처리하도록 해주는 메서드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Alamofire가 제공하는 RequestIntereptor의 본문은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;977&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MZJYc/btsGwfI1RhA/aFZhRKkrXFKF4iAYwiir2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MZJYc/btsGwfI1RhA/aFZhRKkrXFKF4iAYwiir2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MZJYc/btsGwfI1RhA/aFZhRKkrXFKF4iAYwiir2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMZJYc%2FbtsGwfI1RhA%2FaFZhRKkrXFKF4iAYwiir2K%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;977&quot; height=&quot;260&quot; data-origin-width=&quot;977&quot; data-origin-height=&quot;260&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;496&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dy8UEi/btsGurwOqeK/WwaTlqIPnqHkFmO4v6w1p0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dy8UEi/btsGurwOqeK/WwaTlqIPnqHkFmO4v6w1p0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dy8UEi/btsGurwOqeK/WwaTlqIPnqHkFmO4v6w1p0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdy8UEi%2FbtsGurwOqeK%2FWwaTlqIPnqHkFmO4v6w1p0%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;397&quot; height=&quot;434&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;496&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 토큰을 활용해 서버와 통신하는 앱이라면 adapt() 메서드 내에서 토큰을 넣어줄 수 있을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 예시입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1712723973209&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result&amp;lt;URLRequest, Error&amp;gt;) -&amp;gt; Void) {
        guard urlRequest.url?.absoluteString.hasPrefix(&quot;https://api.agify.io&quot;) == true,
              let accessToken = KeychainServiceImpl.shared.accessToken else {
                  completion(.success(urlRequest))
                  return
              }

        var urlRequest = urlRequest
        urlRequest.setValue(&quot;Bearer &quot; + accessToken, forHTTPHeaderField: &quot;Authorization&quot;)
        completion(.success(urlRequest))
    }&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;또한 401 에러가 발생한다면 토큰을 refresh시켜야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1712724109924&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -&amp;gt; Void) {
        guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 else {
            completion(.doNotRetryWithError(error))
            return
        }

        RefreshTokenAPI.refreshToken { result in
            switch result {
            case .success(let accessToken):
                KeychainService.shared.accessToken = accessToken
                completion(.retry)
            case .failure(let error):
                completion(.doNotRetryWithError(error))
            }
        }
    }&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;만약 401에러가 아니라면 retry하지 않고 401에러라면 토큰을 refresh 한 후 재 요청을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;refresh를 실행 하였는데도 실패했다면 로그아웃, 데이터 삭제 등 작업을 실행할 수 있을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 만들어준 Interceptor 객체를 활용할 땐 아래와 같이 활용하면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XoFUc/btsGuK3XAA6/XkXOFo4XGHSBa6bKm219m1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XoFUc/btsGuK3XAA6/XkXOFo4XGHSBa6bKm219m1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XoFUc/btsGuK3XAA6/XkXOFo4XGHSBa6bKm219m1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXoFUc%2FbtsGuK3XAA6%2FXkXOFo4XGHSBa6bKm219m1%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;738&quot; height=&quot;176&quot; data-origin-width=&quot;738&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;request의 파라미터인 interceptor에 interceptor를 넣어주면 됩니다.&lt;/p&gt;</description>
      <category>IOS</category>
      <author>clamp</author>
      <guid isPermaLink="true">https://clamp-coding.tistory.com/518</guid>
      <comments>https://clamp-coding.tistory.com/518#entry518comment</comments>
      <pubDate>Wed, 10 Apr 2024 13:45:43 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] 커스텀 액티비티 인디케이터 제작 feat.  RxSwift</title>
      <link>https://clamp-coding.tistory.com/517</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;커스텀&amp;nbsp;액티비티&amp;nbsp;인디케이터&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;/p&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;이럴 때 ActivityIndicator는 사용자에게 프로그램이 정확히 작업중이라는것을 알 수 있게 해줍니다.&lt;/p&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;앞서 해당 프로젝트에서는 Alamofire와 Rxswift를 활용하였습니다.&lt;/p&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;1. 우선 액티비티 인디케이터를 관리할 클래스를 생성하였습니다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 클래스의 이름은 Loading입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 모듈과 Loading 클래스가 속해있는 Core모듈은 서로 분리되어 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;757&quot; data-origin-height=&quot;143&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/esh6zb/btsF5TNxGWV/v7t7GdR8P93j4O5yxts9xk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/esh6zb/btsF5TNxGWV/v7t7GdR8P93j4O5yxts9xk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/esh6zb/btsF5TNxGWV/v7t7GdR8P93j4O5yxts9xk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fesh6zb%2FbtsF5TNxGWV%2Fv7t7GdR8P93j4O5yxts9xk%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;757&quot; height=&quot;143&quot; data-origin-width=&quot;757&quot; data-origin-height=&quot;143&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;backgroundColor는 액티비티 인디케이터가 회전중일 경우 배경 색을 약간 흐려지게 만들어 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;h4 data-ke-size=&quot;size20&quot;&gt;2. 액티비티 인디케이터를 정의합니다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 Loading 클래스 내부에 존재합니다. 저는 이미지를 사용하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;622&quot; data-origin-height=&quot;414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGNYQm/btsF5DKZrwO/RQfP1DxvuT4KyTYW6ZHZG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGNYQm/btsF5DKZrwO/RQfP1DxvuT4KyTYW6ZHZG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGNYQm/btsF5DKZrwO/RQfP1DxvuT4KyTYW6ZHZG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGNYQm%2FbtsF5DKZrwO%2FRQfP1DxvuT4KyTYW6ZHZG1%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;622&quot; height=&quot;414&quot; data-origin-width=&quot;622&quot; data-origin-height=&quot;414&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 UIView객체를 생성하고, 화면의 크기를 가져옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 인디케이터의 가로와 세로 크기를 화면 넓이의 4/1로 설정하고, 인디케이터가 화면의 중앙에 위치하도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 side, x, y 변수에 저장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;view의 배경색을 설정하고 이미지를 가져와 이미지뷰에 설정하고, 생성한 view에 추가합니다.&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;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 액티비티 인디케이터를 화면에 띄웁니다.&lt;/h4&gt;
&lt;pre id=&quot;code_1711507075561&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    public static func start(isFullScreen: Bool = true, stopTouch: Bool = true) {
        DispatchQueue.main.async {
            if let window: UIWindow = UIApplication.shared.windows.filter({$0.isKeyWindow}).first {
                
                var found: Bool = false
                
                for subViews in window.subviews {
                    if subViews.tag == 1 {
                        found = true
                    }
                }
                
                if !found {
                    
                    if isFullScreen {
                        let screen: CGRect = UIScreen.main.bounds
                        
                        indicator.frame = CGRect(x: 0, y: 0, width: screen.width, height: screen.height)
                        
                        for subview in indicator.subviews {
                            let x = (screen.width / 2) - (subview.frame.width / 2)
                            let y = (screen.height / 2) - (subview.frame.height / 2)
                            subview.frame = CGRect(x: x, y: y, width: subview.frame.width, height: subview.frame.height)
                        }
                    } else {
                        let screen: CGRect = UIScreen.main.bounds
                        let side = screen.width / 4
                        let x = (screen.width / 2) - (side / 2)
                        let y = (screen.height / 2) - (side / 2)
                        indicator.frame = CGRect(x: x, y: y, width: side, height: side)
                        
                        for subview in indicator.subviews {
                            subview.frame = CGRect(x: side / 4, y: side / 4, width: side / 2, height: side / 2)
                            
                        }
                    }
                    
                    if(stopTouch) {
                        indicator.isUserInteractionEnabled = true
                    }
                    
                    for subView in indicator.subviews {
                        subView.layer.add(animation, forKey: nil)
                    }
                    
                    window.addSubview(indicator)
                    
                    UIView.animate(withDuration: fade, delay: 0.5, animations: {
                        self.indicator.alpha = 1.0
                    })
                    
                }
                
            }
        }
    }&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;액티비티 인디케이터를 UIWindow에 추가합니다. 특정 ViewController에 추가하는 것이 아닌 Window에 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 하나 파라미터로 stopTouch라는 변수를 받게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파라미터가 true일 경우 indicator.isUserInteractionEnabled = true로 설정해 인디케이터를 가진 뷰가 터치 이벤트를 받도록 함으로써 통신중일 경우엔 유저의 터치 이벤트가 작동하지 않도록 설정할 수 있습니다.&lt;/p&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;3. 액티비티 인디케이터를 정지합니다.&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;474&quot; data-origin-height=&quot;198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DJhbk/btsF8aAKIN0/lJ4GxWZAXw5MxEb3mSgDsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DJhbk/btsF8aAKIN0/lJ4GxWZAXw5MxEb3mSgDsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DJhbk/btsF8aAKIN0/lJ4GxWZAXw5MxEb3mSgDsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDJhbk%2FbtsF8aAKIN0%2FlJ4GxWZAXw5MxEb3mSgDsk%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;474&quot; height=&quot;198&quot; data-origin-width=&quot;474&quot; data-origin-height=&quot;198&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;h4 data-ke-size=&quot;size20&quot;&gt;4. 네트워크 통신과 연결&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인디케이터 작동에 필요한 요소를 전부 작성했으니 네트워크 통신과 연결해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 프로젝트에서는 response가 필요한 모든 http 통신을 다음 메서드로 사용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1711507428670&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public func request&amp;lt;T: Decodable&amp;gt;(_ urlConvertible: URLRequestConvertible) -&amp;gt; Single&amp;lt;T&amp;gt;&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;RxSwift의 Single을 사용합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 네트워크 통신은 Single.create 내부에서 진행됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1711507702945&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public func request&amp;lt;T: Decodable&amp;gt;(_ urlConvertible: URLRequestConvertible) -&amp;gt; Single&amp;lt;T&amp;gt; {
        return Single.create { single in
            
            Loading.start()

            let request = self.session
                .request(urlConvertible,
                         interceptor: self.authInterceptor)
                .validate(statusCode: 200 ..&amp;lt; 300)
                .responseDecodable(of: T.self) { response in
                    /.......
                    
                    Loading.stop()

                }
            return Disposables.create {
                request.cancel()
            }
        }
    }&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;Single.create 내부의 구문을 보시면 네트워크 통신을 시작하기 전에 Loading.start()로 액티비티 인디케이터를 작동시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후 모든 네트워크 통신이 끝난 후 성공, 실패 구분 없이 Loading.stop()으로 인디케이터의 작동을 중지시킵니다.&lt;/p&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;만약 어떠한 API통신을 할 때 액티비티 인디케이터가 작동하지 않아야 한다면 requestNoActi 처럼 메서드를 하나 더 만들어 주면 될 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 커스텀 ActivityIndicator를 제작해본 경험이였습니다.&lt;/p&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;img src=&quot;&quot; width=&quot;250&quot;/&gt;| &lt;image src=&quot;https://github.com/Connect-Crew/TeamOne-iOS/assets/121999075/966565a8-6184-4b0e-8251-5d1e7d31b3ad&quot; width=&quot;250&quot;/&gt;</description>
      <category>IOS</category>
      <author>clamp</author>
      <guid isPermaLink="true">https://clamp-coding.tistory.com/517</guid>
      <comments>https://clamp-coding.tistory.com/517#entry517comment</comments>
      <pubDate>Wed, 27 Mar 2024 12:04:05 +0900</pubDate>
    </item>
    <item>
      <title>SwiftUI TCA - 3. State, Action, Reducer, Effect</title>
      <link>https://clamp-coding.tistory.com/516</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;TCA의 구성을 하나 씩 살펴보겠습니다.&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;앱의 상태 &quot;State&quot;&lt;/h2&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;568&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/np2nK/btsFOfoHaeI/0hBBuyg9iZ02nglzltas9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/np2nK/btsFOfoHaeI/0hBBuyg9iZ02nglzltas9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/np2nK/btsFOfoHaeI/0hBBuyg9iZ02nglzltas9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnp2nK%2FbtsFOfoHaeI%2F0hBBuyg9iZ02nglzltas9k%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;568&quot; height=&quot;290&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;568&quot; data-origin-height=&quot;290&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;State는 Reducer의 현재 상태를 갖는 구조체입니다.&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;예를 들어 카운트를 증, 감 시키는 어플리케이션을 가정합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 카운트를 표시하기위해 State는 다음과같이 count데이터를 소지하고 있을 것 입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1710395009207&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct CounterFeature: Reducer {
    
    struct State: Equatable {
        var count = 0
    }
    
    ...
}&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;여기서 State는 Equatable인데요, 그 이유는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;SwiftUI는 View의 상태가 변경되었을 때 해당 view를 자동으로 업데이트 합니다.&lt;br /&gt;즉 현재 상태와 이 전 상태를 비교하여 불 필요한 렌더링을 방지하는데, Equatable을 적용하여 변화를 빠짐없이 체크하기 위해 Equatable한 Struct를 활용합니다(테스트와도 관련 있다고 합니다).&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상태 변화를 일으키는 모든 동작 &quot;Action&quot;&lt;/h2&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;568&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d1HWfc/btsFOlh209c/OrvIFSdKdkKV1M2MYU5M50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d1HWfc/btsFOlh209c/OrvIFSdKdkKV1M2MYU5M50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d1HWfc/btsFOlh209c/OrvIFSdKdkKV1M2MYU5M50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd1HWfc%2FbtsFOlh209c%2FOrvIFSdKdkKV1M2MYU5M50%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;568&quot; height=&quot;290&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;568&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Action은 디바이스와 User Interaction을 받아오기 위한 타입입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1710395522677&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct CounterFeature: Reducer {
    
    struct State: Equatable {
        var count = 0
    }
    
    enum Action: Equatable {
        case decrementButtonTapped
        case incrementButtonTapped
    }
    
    ...
}&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;이 외에도 알림창을 닫거나, API의 Request를 받거나, 타이머를 작동시키거나 하는 복잡한 행동들도 Action에 추가할 수 있습니다.&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;변경을 처리한다 &quot;Reducer&quot;&lt;/h2&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;568&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rHmQQ/btsFLS2HtoX/AcS8zuh5e1oUmcMGpcLGbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rHmQQ/btsFLS2HtoX/AcS8zuh5e1oUmcMGpcLGbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rHmQQ/btsFLS2HtoX/AcS8zuh5e1oUmcMGpcLGbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrHmQQ%2FbtsFLS2HtoX%2FAcS8zuh5e1oUmcMGpcLGbk%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;568&quot; height=&quot;290&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;568&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reducer는 App의 현재 State를 주어진 Action을 바탕으로 어떻게 다음 상태로 바꿀 것인지 묘사하고 어떤 결과(Effect)가 존재한다면 현재 상태(State)를 함수형으로, 가독성 좋게 작성하기 위해 고안되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1710395672827&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct CounterFeature: Reducer {
    
    struct State: Equatable {
        var count = 0
    }
    
    enum Action: Equatable {
        case decrementButtonTapped
        case incrementButtonTapped
    }
    
    var body: some ReducerOf&amp;lt;Self&amp;gt; {
        Reduce { state, action in
            switch action {
            case .decrementButtonTapped:
                state.count -= 1
                return .none
                
            case .incrementButtonTapped:
                state.count += 1
                return .none
            }
        }
    }
}&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;View에서 Action을 취하면 Action은 Reducer를 통해 State를 변화시킵니다.&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;Effect&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Action에 따른 결과 Effect&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Reducer의 Action이 반환하는 타입 입니다.&lt;/li&gt;
&lt;li&gt;Action을 거친 모든 결과물입니다.&lt;/li&gt;
&lt;li&gt;예상과 다른 결과물을 Side Effect라고 합니다.&lt;/li&gt;
&lt;li&gt;외부 시스템과 상호작용 하는 작업을 나타냅니다. 이를 통해 앱의 State가 변경됩니다.&lt;/li&gt;
&lt;li&gt;State를 직접 변경하는 Action과 달리 비동기 작업을 수행하고 결과를 Action으로 변환하여 State에 반영하게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Effect는 특정 Action을 실행한 후 결과에 따라 새로운 Action을 생성하고 이를 통해 State를 업데이트 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;TCA에서의 Effect는 다음과 같은 기능을 수행합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비동기 작업 관리:&lt;/b&gt; 네트워크 요청, 데이터 로딩, 파일 다운로드 등등..&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Side Effect 분리:&lt;/b&gt; 함수형 프로그래밍 원칙에 따라 Side Effect를 배제합니다. Effect와 SideEffect를 다루는 부분을 명확히 분리하여 코드의 가독성을 높이고 추론력을 높이고 테스트와 디버깅에 용이해집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;취소 및 에러 핸들링:&lt;/b&gt; 비동기 작업의 성공, 실패 및 중단을 관리하는데 사용됩니다. 예를 들어 네트워크 요청중에 발생한 오류를 적절히 처리하고 State를 적절히 업데이트 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;순서 보장:&lt;/b&gt; TCA의 Effect는 순차적으로 실행되며 그 순서가 보장됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;순수함수적인 Effect&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수형 프로그래밍에서 순수함수는 아주 중요한 내용입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;*순수함수: 주어진 입력에 대해 항상 동일한 출력을 return하며 외부 상태를 변경하지 않고 Side Effect가 없는 함수를 의미합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 순수함수로는 SideEffect나 비동기작업을 처리할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Effect는 직접 state를 변경하진 않습니다. 하지만 &lt;b&gt;TCA에서 Effect는 사이드 이펙트와 같은 비동기 작업도 순수함수 방식으로 처리할 수 있도록 설계&lt;/b&gt;되었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 코드를 보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1710398001058&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct CounterFeature: Reducer {
    
    struct State: Equatable {
        var count = 0
        var fact: String?
        var isLoadingFact = false
    }
    
    enum Action: Equatable {
        case decrementButtonTapped
        case incrementButtonTapped
        case factResponse(String)
        case getFactButtonTapped
    }
    
    var body: some ReducerOf&amp;lt;Self&amp;gt; {
        Reduce { state, action in
            switch action {
            case .decrementButtonTapped:
                state.count -= 1
                return .none
                
            case .incrementButtonTapped:
                state.count += 1
                return .none
                
            case .getFactButtonTapped:
                state.fact = nil
                state.isLoadingFact = true
                // [count = state.count] 구문은 클로저 내부에서 state.count를 count로 사용하겠다는 의미입니다.
                return .run { [count = state.count] send in
                    let (data, _) = try await URLSession.shared.data(
                        from: URL(string: &quot;url 주소가 들어갈 장소/\(count)&quot;)!
                    )
                    let fact = String(decoding: data, as: UTF8.self)
                    await send(.factResponse(fact))
                }
                
            case let .factResponse(fact):
                state.fact = fact
                state.isLoadingFact = false
                return .none
            }
        }
    }
}&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;.run, send등은 뒤에서 더 설명하겠습니다. 일단 구조만 보자면 .getFactButtonTapped 액션이 발생하면 외부 통신과 연결된 프로퍼티를 설정하고 외부 통신을 진행한 후 factResponse Action을 send하게 됩니다. 만약 외부 통신의 결과로 Error가 발생하여 Side Effect가 발생하게 되면(디코딩 과정 등) 이 또한 .showErrorToast 같은 액션을 만들어서 처리할 수 있을 것입니다.&lt;/p&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;Effect와 Combine(Rx)의 상관관계&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rx, Combine의 개념에는 Publisher(Observable)와 Subscriber가 있습니다.&lt;/p&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;Publisher:&lt;/b&gt; &lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: left;&quot;&gt;데이터를 생성하고 전달하는 역할을 합니다. 이들은 비동기적으로 여러 값들을 시간에 따라 방출할 수 있으며, 이벤트나 데이터 스트림의 소스 역할을 합니다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Subscriber:&lt;/b&gt; &lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: left;&quot;&gt;Publisher 로부터 방출되는 데이터나 이벤트를 구독하고 받아 처리합니다. 데이터가 방출될 때마다 특정 액션을 취할 수 있도록 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Effect (TCA):&lt;/b&gt; TCA에서는 Effect를 사용하여 앱의 사이드 이펙트를 관리합니다. 이는 네트워크 요청, 타이머, 외부 데이터 피드 등과 같은 비동기 작업을 캡슐화합니다. Effect는 Combine의 Publisher와 유사하게, 작업의 결과를 시간에 따라 방출할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;.run (Effect의 메서드):&lt;/b&gt; Effect 내에서 비동기 작업을 시작하고, 그 결과를 앱의 상태 변화로 연결하는 데 사용됩니다. 이는 Combine에서의 Subscriber가 Publisher를 구독하는 것과 유사한 역할을 하며, RxSwift에서의 Subscription 과정과 비교될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: left;&quot;&gt;Combine과 TCA에서는 데이터나 이벤트의 흐름을 정의하는 &quot;소스(Publisher/Effect)&quot;와 이를 받아 처리하는 &quot;리스너(Subscriber/.run)&quot;의 관계를 통해 비동기 작업을 관리합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: left;&quot;&gt;RxSwift를 사용해본 경험이 있다면, Observable이 데이터를 방출하고 Observer가 이를 구독하는 방식을 이미 이해하고 계실 것입니다. TCA의 Effect와 Combine의 Publisher는 Observable과 유사한 역할을 하며, 데이터 흐름을 관리하고 구독하는 방식에서도 Observer와 Subscriber의 개념을 찾아볼 수 있습니다.&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;h4 data-ke-size=&quot;size20&quot;&gt;Effect와 Action&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Effect와 Action은 각각 다른 역할을 담당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Action은 사용자에게서 발생하는 이벤트를 캡슐화하며 이는 State 변화를 일으키는데 사용됩니다.&lt;/b&gt; 예를 들어 버튼 클릭. 텍스트 입력, 타이머 완료, 네트워크 응답 등의 이벤트가 Action이 됩니다.&lt;/p&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;Effect는 비동기 작업을 처리하고 결과를 다시 Action으로 반환하는 역할을 수행&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Effect는 외부와의 상호작용을 위해 사용&lt;/b&gt;되며 해당 작업이 완료 ==&amp;gt; 적절한 Action을 실행 ==&amp;gt;&amp;nbsp; State를 업데이트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 TCA의 Effect는 Swift의 Combine 프레임워크를 기반으로 작성되었습니다. 비동기 작업을 처리하고 Combine의 Publisher 및 Operator를 활용하여 작업을 조합하고 변환할 수 있습니다.&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;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주요 메서드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;.none&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Effect는 반환해야 하는데 아무 동작도 취하지 않을 때 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 액션에 대해 아무런 비동기 처리도 필요하지 않을 때 사용됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1710399482047&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    var body: some ReducerOf&amp;lt;Self&amp;gt; {
        Reduce { state, action in
            switch action {
            case .decrementButtonTapped:
                state.count -= 1
                return .none
                
            case .incrementButtonTapped:
                state.count += 1
                return .none
                
                ....
            }
      }&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;.send&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파라미터로 Action을 받는 메서드입니다.&lt;/li&gt;
&lt;li&gt;특정 Action 이후 즉시 추가적인 동기 액션이 필요할 때 사용됩니다.&lt;/li&gt;
&lt;li&gt;주로 자식 컴포넌트에서 부모 컴포넌트로 데이터를 전달할 때 사용됩니다.&lt;/li&gt;
&lt;li&gt;액션을 전달하는 동시에 애니메이션을 지정할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 카운터 앱에서 최대치에 도달한 경우 사용자에게 최대치 도달 알림을 전달해야 하는 경우 예를 들어보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1710399754815&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; struct CounterState: Equatable {
    var count: Int = 0
    var isMaxReached: Bool = false
}

enum CounterAction: Equatable {
    case incrementButtonTapped
    case maxCountReached
}

let counterReducer = Reducer&amp;lt;CounterState, CounterAction, Void&amp;gt; { state, action, _ in
    switch action {
    case .incrementButtonTapped:
        state.count += 1
        if state.count == 10 {
            return .send(.maxCountReached)
        }
        return .none
        
    case .maxCountReached:
        state.isMaxReached = true
        return .none
    }
}&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;.run&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기 작업을 래핑하는 메서드입니다.&lt;/li&gt;
&lt;li&gt;인자로 비동기 클로저를 받아서 실행합니다.&lt;/li&gt;
&lt;li&gt;클로저 내부에서 .send를 사용해 액션을 시스템에 전달 할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1710399965765&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//.run method에 들어가는 operation 인자
operation: @escaping @Sendable (_ send: Send&amp;lt;Action&amp;gt;) async throws -&amp;gt; Void&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1710400355234&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct CounterFeature: Reducer {
    
...
    
    var body: some ReducerOf&amp;lt;Self&amp;gt; {
        Reduce { state, action in
            switch action {
          ...
                
            case .getFactButtonTapped:
                state.fact = nil
                state.isLoadingFact = true
                // [count = state.count] 구문은 클로저 내부에서 state.count를 count로 사용하겠다는 의미입니다.
                return .run { [count = state.count] send in
                    let (data, _) = try await URLSession.shared.data(
                        from: URL(string: &quot;url 주소가 들어갈 장소/\(count)&quot;)!
                    )
                    let fact = String(decoding: data, as: UTF8.self)
                    await send(.factResponse(fact))
                }
                
            case let .factResponse(fact):
                state.fact = fact
                state.isLoadingFact = false
                return .none
            }
        }
    }
}&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;.cancellable(id:), cancle(id:)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Effect를 취소할 수 있게 만들어주는 메서드입니다.&lt;/li&gt;
&lt;li&gt;id는 Effect를 식별하는 값 입니다. 문자열 등 해쉬가 되는 값이라면 OK입니다.&lt;/li&gt;
&lt;li&gt;cancelInFlight라는 Bool타입의 인자가 있는데 Default는 false입니다. true라면 같은 id로 실행중인 Effect를 취소할 수 있습니다.&lt;/li&gt;
&lt;li&gt;필요한 타이밍에 cancle한다면 이미 진행중인 Effect를 즉시 취소 후 취소된 결과를 전달합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1710401163497&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct CounterFeature: Reducer {
    
    struct State: Equatable {
        var count = 0
        var isTimerOn = false
    }
    
    enum Action: Equatable {
        case togggleTimerButtonTapped
        case timerTicked
    }
    
    var body: some ReducerOf&amp;lt;Self&amp;gt; {
        Reduce { state, action in
            switch action {
            case .togggleTimerButtonTapped:
                state.isTimerOn.toggle()
                
                if state.isTimerOn {
                    return .run { send in
                        while true {
                            try await Task.sleep(for: .seconds(1))
                            
                            await send(.timerTicked)
                        }
                    }
                    .cancellable(id: &quot;timer&quot;)
                    
                } else {
                    return .cancel(id: &quot;timer&quot;)
                }
                
                
            case .timerTicked:
                state.count += 1
                return .none
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;.merge, .concateate&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;merge&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러개의 Effect들을 동시에 실행 시키는 기능입니다.&lt;/li&gt;
&lt;li&gt;여러개의 이펙트를 인자로 받아서 결과를 병합합니다.&lt;/li&gt;
&lt;li&gt;순서가 보장되지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;concatenate&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;merge와 같지만 순서를 부여합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1710401310244&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct CounterFeature: Reducer {
    struct State: Equatable {}
    enum Action: Equatable {
        case incrementButtonTapped
        case decrementButtonTapped
        case factResponse(String)
        case getFactButtonTapped
	// 숫자를 1 증가시키고 그 번호에 해당하는 사실을 가져오는 기능 추가
	case increaseAndGetFactButtonTapped
    }
    
    var body: some ReducerOf&amp;lt;Self&amp;gt; {
        Reduce { state, action in
            switch action {
            ...
            case .increaseAndGetFactButtonTapped:
                return .concatenate(
                    .send(.incrementButtonTapped),
                    .send(.getFactButtonTapped))
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>SwiftUI+TCA+Combine</category>
      <author>clamp</author>
      <guid isPermaLink="true">https://clamp-coding.tistory.com/516</guid>
      <comments>https://clamp-coding.tistory.com/516#entry516comment</comments>
      <pubDate>Thu, 14 Mar 2024 16:37:15 +0900</pubDate>
    </item>
  </channel>
</rss>