<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>iOS 개발자 꿈나무</title>
    <link>https://yanni13.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Tue, 14 Jul 2026 08:12:42 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>yanni13</managingEditor>
    <image>
      <title>iOS 개발자 꿈나무</title>
      <url>https://tistory1.daumcdn.net/tistory/6657677/attach/b3ad240b1d774012ac80ecf5548d146e</url>
      <link>https://yanni13.tistory.com</link>
    </image>
    <item>
      <title>[Swift] Swift Concurrency(2) - GCD vs Swift Concurrency</title>
      <link>https://yanni13.tistory.com/81</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1776700980076&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Swift] Swift Concurrency(1) - 동시성&quot; data-og-description=&quot;오늘은 Swift Concurrency에 대해 본격적으로 알아보기 전,동시성 문제가 왜 발생하고 어떤 문제가 발생하는지 먼저 파악해보고자 한다! (제가 공부하기 위해 작성한 것이므로 틀린 정보가 있다면 알&quot; data-og-host=&quot;yanni13.tistory.com&quot; data-og-source-url=&quot;https://yanni13.tistory.com/80&quot; data-og-url=&quot;https://yanni13.tistory.com/80&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bEH5W9/dJMb8Xkhsot/pOtShiXukqIT2GpMucKzM1/img.png?width=783&amp;amp;height=272&amp;amp;face=0_0_783_272,https://scrap.kakaocdn.net/dn/bP42ZC/dJMb8XR7Mfo/KtI0Zh7Q2GborE4ZbXhBrk/img.png?width=783&amp;amp;height=272&amp;amp;face=0_0_783_272,https://scrap.kakaocdn.net/dn/bscNVO/dJMb8Xkhsos/LSegUgKOwZRYkHTLL5eFh0/img.png?width=1420&amp;amp;height=392&amp;amp;face=0_0_1420_392&quot;&gt;&lt;a href=&quot;https://yanni13.tistory.com/80&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://yanni13.tistory.com/80&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bEH5W9/dJMb8Xkhsot/pOtShiXukqIT2GpMucKzM1/img.png?width=783&amp;amp;height=272&amp;amp;face=0_0_783_272,https://scrap.kakaocdn.net/dn/bP42ZC/dJMb8XR7Mfo/KtI0Zh7Q2GborE4ZbXhBrk/img.png?width=783&amp;amp;height=272&amp;amp;face=0_0_783_272,https://scrap.kakaocdn.net/dn/bscNVO/dJMb8Xkhsos/LSegUgKOwZRYkHTLL5eFh0/img.png?width=1420&amp;amp;height=392&amp;amp;face=0_0_1420_392');&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;[Swift] Swift Concurrency(1) - 동시성&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;오늘은 Swift Concurrency에 대해 본격적으로 알아보기 전,동시성 문제가 왜 발생하고 어떤 문제가 발생하는지 먼저 파악해보고자 한다! (제가 공부하기 위해 작성한 것이므로 틀린 정보가 있다면 알&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;yanni13.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이전 포스팅에서는 Swift Concurrency의 기본 개념이 되는 동시성에 대해서 다뤄보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;오늘은 Swift Concurrency가 어떤 문제를 해결하고자 했는지에 대해서 조금 더 깊게 다뤄보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그 근간이 되는 GCD도 같이 살펴보고 , Swift Concurrency의 종류와 GCD는 어떤 차이점을 가지고 있는지 다뤄보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #9d9d9d; text-align: start;&quot;&gt;(제가 공부하기 위해 작성한 것이므로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #9d9d9d; text-align: start;&quot;&gt;틀린&lt;span&gt;&amp;nbsp;&lt;/span&gt;정보가&lt;span&gt;&amp;nbsp;&lt;/span&gt;있다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;알려주세요!)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✏️ Swift Concurrency&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Swift 5.5부터 도입된 비동기 프로그래밍을 위한 새로운 프레임워크&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Swift Concurrency는 Swift 5.5에 새로 나온 프레임워크로, GCD(Grand Central Dispatch)와 OperationQueue를 대체하거나 보완하며 &lt;b&gt;더 직관적이고 안전한 비동기 코드 작성&lt;/b&gt;을 가능하게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;GCD의 비동기 예시를 통해 동작방식을 한번 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1776787317698&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func gcd() {
	...
        URLSession.shared.dataTask(with: request) { data, response, error in

            DispatchQueue.main.async {

                guard let data else {
                    completion(.failure(.unknown))
                    return
                }

                guard error == nil else {
                    completion(.failure(.invalidResponse))
                    return
                }

                guard let response = response as? HTTPURLResponse,
                    response.statusCode == 200
                else {
                    completion(.failure(.invalidResponse))
                    return
                }

                guard let image = UIImage(data: data) else {
                    completion(.failure(.invalidImage))
                    return
                }

                completion(.success(image))

            }

        }.resume()
}&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;GCD는 DispatchQueue를 통해 작업을 조율하며 모든 걸 &lt;b&gt;클로저로 해결하는 특성&lt;/b&gt;을 가지고 있다. 성공한 데이터든 오류든 응답값이든 모든 걸 클로저로 전달한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 코드의 문제점은 코드 실행 흐름이 뒤죽박죽 섞이게 되어 실행흐름을 알기 어렵고 혹은 까먹거나 놓쳐서 핸들러 실행이 안 되는 경우가 발생할 수 있다는 것이다. 또한 중첩된 클로저가 발생 가능하여 가독성이 떨어지며 여전히 동시성 이슈가 발생할 수 있는 환경이기 때문에 개발자가 lock이나 serial queue로 수동 관리해주어야 한다는 특성이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;결국 GCD로는 런타임시점에 오류를 발생시키기 때문에 실행하기전까지는 알 수 없고 , 이를 &lt;b&gt;컴파일 타임에 파악하고자 나오게 된게 Swift Concurrency&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;그래서 Swift Concurrency는 비동기 작업을 동기 코드처럼 작성하여 가독성과 유지보수성을 높이고 스레드 관리와 동시성 문제를 줄일 수 있도록 하는 목적&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;size18&quot;&gt;Swift Concurrency가 해결하고자 하는 목적은 크게 보면 아래 4가지의 항목으로 정리해 볼 수 있다.&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;안전성&lt;/b&gt;: Actor를 사용하여 데이터 경쟁을 방지하고 throws로 에러 처리를 통합한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;효율성&lt;/b&gt;: Continuation 기반으로 스레드 관리를 최적화하여 Thread Explosion을 감소시킬 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유연성&lt;/b&gt;: TaskGroup과 AsyncStream으로 복잡한 작업을 처리할 수 있다.&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;size18&quot;&gt;해당 개념들은 아래 차차 설명해보겠다! 지금은 이런 게 있구나 정도만 알고 글을 이해하는데 초점을 맞추면 좋을 거 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Thread Explosion&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;스레드의 관점에서 Swift Concurrency를 바라보면 GCD는 Thread Explosion 문제가 존재하지만 Swift Concurrency에서는 actor를 사용해 스레드를 재활용하며 스레드의 개수를 코어의 개수와 동일하게 제한하도록 하여 이 문제를 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;조금 더 풀어서 말해보자면 스레드는 디바이스마다 존재하는 갯수가 한정적이다. 예를 들어 맥북 에어와 맥북 프로의 CPU 코어에 차이가 나는 것처럼 디바이스마다 존재하는 스레드의 개수가 한정적인데 GCD는 이 한정된 스레드를 계속 쪼개서 늘리기 때문에 8 코어에서 스레드의 개수가 100개가 되거나 하는 상황이 발생할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;주로 이런 상황은 시스템에서 작업이 I/O 대기나 락 등으로 block되면 기존 스레드가 멈춘 상태를 피하기 위해서 추가 스레드를 생성하는데 이때 작업량이 많거나 블록이 자주 발생하면 스레드의 수가 급격하게 증가된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 문제를 &lt;b&gt;Thread Explosion&lt;/b&gt; 문제라고 칭한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;context switching 관점에서도 100개의 스레드보다 8개의 스레드가 스케줄링 오버헤드가 덜 발생하게 될 것이다. 또한 스레드가 많아지게 될 경우 CPU가 이를 관리하고 스위칭하는데 더 많은 시간이 소요되고 스레드 수가 코어수를 초과하기 때문에 시스템 부화가 커지는 문제가 있다.&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; Context Switching&lt;/b&gt;&lt;br /&gt;: CPU가 한 스레드에서 다른 스레드로 실행흐름을 전환하는 과정이다.&lt;br /&gt;&lt;br /&gt;스레드의 수가 많을수록 다른 스레드로 전환하려는 과정이 자주 일어나기 때문에 성능이 저하되고, GCD에서는 Thread Explosion이 발생하면 컨텍스트 스위칭 빈도가 증가해 시스템 효율이 떨어지게 된다.&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;size18&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #333333; text-align: left;&quot;&gt;한마디로 정리하자면 GCD queue의 단점을 극복하기 위해서 Swift Concurrency가 나오게 되었고 thread간 context switching이 없고 continuation으로 구성된 모델로 변경한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;즉 &lt;span style=&quot;color: #ee2323;&quot;&gt;최대로 실행되는 스레드 갯수와 CPU 코어 개수와 동일하도록 만든다는 것이 핵심&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;  그걸 어떻게 하는데?&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;우선 위 목표를 달성하기 위해 스레드의 불필요한 점유를 낮추고 재사용성을 높이는 방법을 사용하고자 했다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;GCD의 경우 동기 작업에 의해 비동기 작업의 스레드가 block 된 경우 해당 스레드는 메모리를 점유하곤 있지만 아무 일도 일어나지 않는데 &lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;Swift Concurrency는 작업이 완료되기를 기다리는 동안 스레드를 block 시키지 않고 스레드의 제어권을 시스템에 넘기도록 한다.&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;&lt;span&gt;이때 비동기 함수가 일시 중단되는 지점이 발생하는데 이를 &lt;b&gt;Suspension Point&lt;/b&gt;라고 불리며 swift Concurrency에서는 &lt;b&gt;await&lt;/b&gt; 키워드가 &lt;b&gt;suspension point가 되어 특정 작업이 끝날 때 까지 스레드가 다른 작업을 처리&lt;/b&gt;하도록 한다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776791532368&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func fetchData() async -&amp;gt; Data {
    let data = await networkRequest() // Suspension Point: 여기서 중단
    return data
}&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;비동기 작업이 필요할 때 함수는 suspended 상태로 전환되며 이 상태에서 스레드는 멈추지 않고 다른 작업을 처리할 수 있게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이때 중단된 작업의 상태는 Continuation 객체로 저장되기 때문에 스레드가 달라도 일관된 환경에서 처리할 수 있도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt; Continuation이란?&lt;/b&gt;&lt;br /&gt;: 중단된 작업의 상태를 다시 돌려받았을때 어디서부터 실행할지를 알아야 하는데 함수의 경우 heap의 영역의 async frame이 어디서 재개할지를 기록하고,&amp;nbsp; 중단된 지점에서 실행이 재개할 수 있도록 Continuation이 실행시키는 것이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;따라서 block 대신 suspension을 사용하기 때문에 추가 스레드를 생성할 필요가 없어서 &lt;b&gt;스레드의 효율성이 높아진다.&lt;/b&gt; 또한 스레드 자체가 바뀌지 않고 &lt;b&gt;Continuation&lt;/b&gt;호출로 작업을 재개하므로 &lt;b&gt;context switching이 감소&lt;/b&gt;되며 스레드 수를 코어 수에 맞게 유지하기 때문에&lt;b&gt; thread explosion까지 방지할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이제 swift concurrency의 종류에 대해서 하나씩 살펴보자!&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;1️⃣ async await&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;비동기 함수를 정의하고 호출 시 결과를 기다리는 키워드&lt;/b&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앞서 맨앞에 소개했던 예시를 참고해서 GCD의 completion handler와 비교했을 때 async await을 적용하면 코드가 아래와 같이 간결해질 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1776792590318&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func fetchAsyncAwait() async throws -&amp;gt; UIImage {
    let request = URLRequest(
        url: ImageNetworkManager.url,
        cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
        timeoutInterval: 5
    )
    print(&quot;1&quot;)
    let (data, response) = try await URLSession.shared.data(for: request)

    print(&quot;2&quot;)
    guard let response = response as? HTTPURLResponse,
        response.statusCode == 200
    else {
        throw ErrorCode.invalidResponse
    }
    print(&quot;3&quot;)
    guard let image = UIImage(data: data) else {
        throw ErrorCode.invalidImage
    }
    print(&quot;4&quot;)
    return image
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;GCD에서는 클로저 기반이였기 때문에 호출해서 사용할 때도 모든 성공, 에러에 대한 케이스를 switch문으로 사용했었어야 했다.&lt;/p&gt;
&lt;pre id=&quot;code_1776795065953&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// GCD
ImageNetworkManager.shared.fetchThumbnailURLSession { response in
    switch response {
    case .success(let success):
        self.firstImageView.image = success
    case .failure(let failure):
        self.firstImageView.image = UIImage(systemName: &quot;star.fill&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;size18&quot;&gt;반면 async await을 사용해서는 아래와 같이 간결하게 사용함으로써 지나친 콜백을 피할 수 있고 실행흐름을 파악하기 간편하다.&lt;/p&gt;
&lt;pre id=&quot;code_1776795114203&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// async/await
let image = try await ImageNetworkManager.shared.fetchAsyncAwait()
firstImageView.image = image

let image2 = try await ImageNetworkManager.shared.fetchAsyncAwait()
secondImageView.image = image2

let image3 = try await ImageNetworkManager.shared.fetchAsyncAwait()
thirdImageView.image = image3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2️⃣ &lt;/b&gt;&lt;b&gt;Task&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;동기와 비동기 코드를 연결하는 브릿지 역할&lt;/b&gt;로 Task를 통해 비동기 코드를 시작할 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Task는 Swift Concurrency에서 비동기 작업을 실행하는 단위이며 GCD의 DispatchQueue.async 대신 Task를 사용하여 간단하게 비동기를 사용할 수 있도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아래 예시를 통해 확인해보면 Task 안에서 비동기 구문이 이루어지고 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1776792842285&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Task {
    print(&quot;a&quot;)
    let image = try await ImageNetworkManager.shared.fetchAsyncAwait()
    print(&quot;b&quot;)
    firstImageView.image = image
    print(&quot;c&quot;)

    let image2 = try await ImageNetworkManager.shared.fetchAsyncAwait()
    secondImageView.image = image2

    let image3 = try await ImageNetworkManager.shared.fetchAsyncAwait()
    thirdImageView.image = image3
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;주로 순차적으로 실행됨을 보장&lt;/b&gt;하고 끝나는 시점이 궁금할 때 Task를 사용하게 된다. 이 방법의 문제점은 결국 순차적으로 실행되기 때문에 이미지 한번 받아올 때 5초가 걸린다면 총 15초가 걸리게 된다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그럼 하나의 이미지를 받아올 때마다 하나의 Task 안에 구성하여 총 3개의 Task를 만들면 되지 않을까? 하는 생각이 들 수 있는데&amp;nbsp;&lt;b&gt;Task를 여러개 사용하는 것은 병렬처리 방식으로 사용&lt;/b&gt;하게 되는 것이며 끝나는 시점이 궁금하지 않고 순차적으로 실행되지 않아도 될 때 여러 개의 task를 사용해 볼 수 있다.&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; 그럼 DispatchGroup처럼 여러 비동기 메서드를 동시에 실행시키고 UI 업데이트를 한번에한 번에 못하는지? 혹은 그 결과를 한 번에 받을 순 없을까?&lt;/b&gt;&lt;br /&gt;: 그때 병렬처리 방식인 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;async let&lt;/b&gt;&lt;/span&gt;과 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;TaskGroup&lt;/b&gt;&lt;/span&gt;을 사용할 수 있다. 둘 다 같은 병렬처리 방식이지만 조금의 차이점이 존재한다.&lt;/blockquote&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;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;3️⃣ Async let&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;갯수가 정해져 있는 비동기 작업&lt;/span&gt;을 병렬로 실행할 때&lt;/b&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1776793362118&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func fetchAsyncLet() async throws -&amp;gt; [UIImage] {
    async let image1 = ImageNetworkManager.shared.fetchAsyncAwait()
    async let image2 = ImageNetworkManager.shared.fetchAsyncAwait()
    async let image3 = ImageNetworkManager.shared.fetchAsyncAwait()

    let result = try await [image1,image2, image3]
    return result
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 코드는 async let으로 구현된 코드 예시이다. 선언시점에 image1, image2, image3가 동시에 시작되며 3개 중 가장 느린 것 기준으로 await가 대기되며 3개가 모두 끝나야 result에 할당되는 방식으로 실행 흐름이 흘러간다.&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;위와 비슷한 개념을 가진 GCD의 DispatchGroup (enter/leave)에 대해서도 함께 비교해보자!&lt;/p&gt;
&lt;pre id=&quot;code_1776794116386&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let group = DispatchGroup()
var images: [UIImage] = []
var fetchError: Error?

group.enter()
ImageNetworkManager.shared.fetchCompletion { img, err in
    if let img { images.append(img) }
    fetchError = err
    group.leave()
}
// enter/leave 3번 반복...

group.notify(queue: .main) {
    // fetchError 별도 체크
    // images 순서 보장 안됨
    // race condition 위험
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;GCD로 구현했을 때는 group.enter()로 카운트를 올리고 콜백이 완료되면 group.leave()를 통해 카운트를 내리는 방식으로 동작한다. 그래서 총 3번의 enter와 leave를 실행해주어야 하며 제일 큰 문제는 이 콜백들이 어느 스레드에서 실행될지 보장되지 않기 때문에 race condition이 발생할 수 있다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;순서가 보장되지 않기 때문에 결과 또한 네트워크 응답이 먼저 오는 순서대로 추가되어 실행 순서가 섞이게 될 수 있다. 모든 요청이 끝난 뒤에 group.notify가 호출되긴 하지만 이 시점에서 개발자가 에러 여부와 결과 순서를 직접 검증해야 하기 때문에 번거롭다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이를 비교해 봤을 때 async let은 컴파일러와 런타임이 &lt;b&gt;실행순서 자체를 보장해 주고&lt;/b&gt;&lt;b&gt; 컴파일 시점에 나타나는 에러를 확인할 수 있다&lt;/b&gt;는 점에서 GCD와의 차이점이 발생한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;4️⃣ TaskGroup&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;동적으로 변경될 수 있는 비동기 작업을 병렬로 실행할 때&lt;span style=&quot;color: #ee2323;&quot;&gt; (비동기 작업 개수가 매번 달라지는 경우)&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776793337335&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func fetchTaskGroup() async throws -&amp;gt; [UIImage] {
    return try await withThrowingTaskGroup(of: UIImage.self) { group in

        for item in 1...10 {
            group.addTask {
                try await ImageNetworkManager.shared.fetchAsyncAwait()
            }
        }

        var resultImage: [UIImage] = []

        for try await item in group {
            resultImage.append(item)
        }

        return resultImage
    }
}&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;이처럼 for문을 통해 몇 개의 데이터가 들어올지 알 수 없는 상태에서는 TaskGroup을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;async let은 결국 변수를 선언하는 것이기 때문에 변수 이름이 반드시 존재해야 하고 await 시점에 그 변수를 호출해야 사용할 수 있다. 그래서 개수가 정해져있는 비동기 작업에서 사용하기 편하며, TaskGroup은 반대로&amp;nbsp; 런타임에 동적으로 task를 추가하는 방식으로 구성되어 있기 때문에 비동기 작업의 갯수가 정해져 있지 않은 상태에서 사용해 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앞서 구현된 게 결국 데이터를 가져와서 화면에 띄워주는 역할을 하는데 이건 UIUpdate가 아닌지, GCD에서는 DispatchQueue.main.async를 통해서 실행해 줬는데 이걸 해줘야 하는 건 아닌지 의문이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 Swift Concurrency의 MainActor에 대한 개념을 알고 가면 이 궁금증을 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;MainActor에 관해서는 이전 포스팅에 다뤄본 적이 있기 때문에 해당 포스팅에서는 짧게 다루고 가겠다.&lt;/p&gt;
&lt;figure id=&quot;og_1776793580031&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[iOS] @MainActor와 DispatchQueue의 차이&quot; data-og-description=&quot;iOS로 개발을 어느 정도 해보았다면 DispatchQueue에 대해서 잘 알고 있을 것이다.&amp;nbsp;@MainActor이랑 DispatchQueue모두 메인 스레드에서 작업을 하도록 하는 건데,&amp;nbsp; 둘의 개념을 간단하게 살펴보고&amp;nbsp;차이를 &quot; data-og-host=&quot;yanni13.tistory.com&quot; data-og-source-url=&quot;https://yanni13.tistory.com/51&quot; data-og-url=&quot;https://yanni13.tistory.com/51&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/5fc9Z/dJMb9lMdJhn/aN2PQmpgNCEHqHW92SIfGk/img.png?width=800&amp;amp;height=320&amp;amp;face=0_0_800_320,https://scrap.kakaocdn.net/dn/00RGU/dJMb9kmeYW2/cDQwhAZGAAbM8C8PicqYM0/img.png?width=800&amp;amp;height=320&amp;amp;face=0_0_800_320,https://scrap.kakaocdn.net/dn/mYdHC/dJMb9b3UhKe/g7vKtScXk1buGbLGlNqja0/img.png?width=1704&amp;amp;height=654&amp;amp;face=0_0_1704_654&quot;&gt;&lt;a href=&quot;https://yanni13.tistory.com/51&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://yanni13.tistory.com/51&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/5fc9Z/dJMb9lMdJhn/aN2PQmpgNCEHqHW92SIfGk/img.png?width=800&amp;amp;height=320&amp;amp;face=0_0_800_320,https://scrap.kakaocdn.net/dn/00RGU/dJMb9kmeYW2/cDQwhAZGAAbM8C8PicqYM0/img.png?width=800&amp;amp;height=320&amp;amp;face=0_0_800_320,https://scrap.kakaocdn.net/dn/mYdHC/dJMb9b3UhKe/g7vKtScXk1buGbLGlNqja0/img.png?width=1704&amp;amp;height=654&amp;amp;face=0_0_1704_654');&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;[iOS] @MainActor와 DispatchQueue의 차이&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;iOS로 개발을 어느 정도 해보았다면 DispatchQueue에 대해서 잘 알고 있을 것이다.&amp;nbsp;@MainActor이랑 DispatchQueue모두 메인 스레드에서 작업을 하도록 하는 건데,&amp;nbsp; 둘의 개념을 간단하게 살펴보고&amp;nbsp;차이를&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;yanni13.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;MainActor&lt;/b&gt;란 결국 &lt;b&gt;DispatchQueue.main.async { }&lt;/b&gt;를 swift concurrency에 맵핑되는 개념이고 swift 5.5 이후부터 ViewController에 MainActor이 내부적으로 선언되어 있는 것을 확인할 수 있다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;0.png&quot; data-origin-width=&quot;1140&quot; data-origin-height=&quot;188&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FuDiv/dJMcafGlmOG/johkXCFX2FgcyIEBiSCEnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FuDiv/dJMcafGlmOG/johkXCFX2FgcyIEBiSCEnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FuDiv/dJMcafGlmOG/johkXCFX2FgcyIEBiSCEnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFuDiv%2FdJMcafGlmOG%2FjohkXCFX2FgcyIEBiSCEnK%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;799&quot; height=&quot;132&quot; data-filename=&quot;0.png&quot; data-origin-width=&quot;1140&quot; data-origin-height=&quot;188&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 위 코드에서 DispatchQueue.main.async로 따로 UI 코드를 작성해주지 않아도 mainactor가 내부적으로 선언되어 있기 때문에 오류가 발생하지 않았던 것이다. 이 &lt;b&gt;mainactor는 스레드의 제어권을 넘기기 때문에 다시 global로 보내거나 main으로 보내는 상황들을 따로 고려하지 않아도 내부적으로 알아서 스레드를 변경해 주고,&lt;/b&gt; actor를 통해서 여러 스레드가 특정 자원에 접근하지 못하도록 설정할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  Actor&lt;/b&gt;&lt;br /&gt;&lt;b&gt;: swift concurrency에서 date race를 방지하기 위한 동시성 모델이며 한 번에 하나의 작업만 접근할 수 있도록 보장한다.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;즉, Actor를 잘 사용하면 동시성 문제를 고려하지 않아도 되고 동시성 이슈와 같이 컴파일 타임에 문제가 될만한 코드를 알려준다.&amp;nbsp;&lt;/blockquote&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;b&gt;Swift Concurrency vs GCD&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;GCD에서 기존에 처리했던 동시성이슈나 UI 업데이트 방식, 클로저 기반 실행 흐름을 Swift Concurrency에서 나온 개념과 맵핑해 표로 정리해 보았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-21 오전 1.08.34.png&quot; data-origin-width=&quot;1326&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpDzxj/dJMcaf0AR5Y/sNJVzNVA8RGrHRlKVbJL30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpDzxj/dJMcaf0AR5Y/sNJVzNVA8RGrHRlKVbJL30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpDzxj/dJMcaf0AR5Y/sNJVzNVA8RGrHRlKVbJL30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpDzxj%2FdJMcaf0AR5Y%2FsNJVzNVA8RGrHRlKVbJL30%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;1326&quot; height=&quot;508&quot; data-filename=&quot;스크린샷 2026-04-21 오전 1.08.34.png&quot; data-origin-width=&quot;1326&quot; data-origin-height=&quot;508&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;size18&quot;&gt;스레드 관점의 차이점을 보자면 아래 표로 정리해 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-22 오전 1.54.31.png&quot; data-origin-width=&quot;1542&quot; data-origin-height=&quot;570&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YP03W/dJMcahjSI2g/GPY7Qg3mUDzNQyMeZTjmq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YP03W/dJMcahjSI2g/GPY7Qg3mUDzNQyMeZTjmq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YP03W/dJMcahjSI2g/GPY7Qg3mUDzNQyMeZTjmq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYP03W%2FdJMcahjSI2g%2FGPY7Qg3mUDzNQyMeZTjmq0%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;1542&quot; height=&quot;570&quot; data-filename=&quot;스크린샷 2026-04-22 오전 1.54.31.png&quot; data-origin-width=&quot;1542&quot; data-origin-height=&quot;570&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://engineering.linecorp.com/ko/blog/about-swift-concurrency&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://engineering.linecorp.com/ko/blog/about-swift-concurrency&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://engineering.linecorp.com/ko/blog/about-swift-concurrency-performance&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://engineering.linecorp.com/ko/blog/about-swift-concurrency-performance&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ios-development.tistory.com/1289&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ios-development.tistory.com/1289&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ios-adventure-with-aphelios.tistory.com/23&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ios-adventure-with-aphelios.tistory.com/23&lt;/a&gt;&lt;/p&gt;</description>
      <category>iOS/Swift</category>
      <category>GCD</category>
      <category>Swift Concurrency</category>
      <author>yanni13</author>
      <guid isPermaLink="true">https://yanni13.tistory.com/81</guid>
      <comments>https://yanni13.tistory.com/81#entry81comment</comments>
      <pubDate>Wed, 22 Apr 2026 03:15:00 +0900</pubDate>
    </item>
    <item>
      <title>[Swift] Swift Concurrency(1) - 동시성</title>
      <link>https://yanni13.tistory.com/80</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;오늘은 Swift Concurrency에 대해 본격적으로 알아보기 전,&lt;/h4&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;동시성 문제가 왜 발생하고 어떤 문제가 발생하는지 먼저 파악해보고자 한다!&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #9d9d9d; text-align: start;&quot;&gt;(제가 공부하기 위해 작성한 것이므로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #9d9d9d; text-align: start;&quot;&gt;틀린&lt;span&gt;&amp;nbsp;&lt;/span&gt;정보가&lt;span&gt;&amp;nbsp;&lt;/span&gt;있다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;알려주세요!)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✏️ 동시성이란?&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;여러 작업을 논리적으로 동시에 실행하는 것처럼 보이게 하는 것&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;동시성&lt;/b&gt;이란 &lt;b&gt;실제로 짧은 시간 간격으로 작업 간 전환&lt;/b&gt;하며 실행되어 여러 작업을 논리적으로 동시에 실행하는 것처럼 보이게 만드는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;병렬성&lt;/b&gt;은 &lt;b&gt;물리적으로 동시에 여러 작업을 수행&lt;/b&gt;하는 건데 동시성은 컨텍스트 스위칭을 통해서 처리되기 때문에 하나의 물리적 스레드에서도 여러 스레드가 작업하고 있는 것처럼 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;iOS에서는 동시성을 크게 &lt;b&gt;GCD&lt;/b&gt;의 DispatchQueue와 &lt;b&gt;Swift Concurrency&lt;/b&gt;의 async/await 을 통해 구현할 수 있다.&lt;/p&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;b&gt;⚪️ GCD&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;GCD에서의 사례를 통해 한번 살펴보자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776693161420&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func gcd() {
    print(&quot;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&quot;, nickname)

    DispatchQueue.global().async {
        self.nickname = &quot;칙촉&quot;
    }

    DispatchQueue.global().async {
        self.nickname = &quot;카스타드&quot;
    }

    print(&quot;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&quot;, nickname)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;해당 코드는 DispatchGroup으로 작성했을 때 문제가 발생할 수 있는 코드이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;783&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9Y34y/dJMcabYd8wr/dGgrpCGIPJz40Kt2YvYjxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9Y34y/dJMcabYd8wr/dGgrpCGIPJz40Kt2YvYjxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9Y34y/dJMcabYd8wr/dGgrpCGIPJz40Kt2YvYjxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9Y34y%2FdJMcabYd8wr%2FdGgrpCGIPJz40Kt2YvYjxK%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;783&quot; height=&quot;272&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;783&quot; data-origin-height=&quot;272&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;size18&quot;&gt;보라색 에러를 잘 살펴보면 Data race 어쩌구 하는데, &lt;b&gt;공유 자원에 대한 동시 접근&lt;/b&gt;으로 인해 동시성 문제가 발생하는 상황이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;nickname이라는 공유 자원에 대해 두개의 스레드가 같은 데이터를 수정&lt;/b&gt;하려고 하고 있기 때문에 보라색 에러로 Thread Sanitizer가 감지되어 런타임 에러를 내뱉고 있다.&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;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그리고 스레드는 프로세스 내에서 실행되는 흐름의 단위이며 같은 프로세스 내의 스레드들은 메모리 공간을 공유하고 있다. 그래야 하나의 앱을 개발하고 있을 때 각각의 스레드들이 그걸 공유하고 있어야 코드를 짤 때 여러 곳에서 접근 가능하고 구현하기 쉽기 때문에!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;문제는 이 스레드가 동시에 같은 변수를 접근하려고 할 때 이 동시성 문제가 발생하게 된다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;현재 같은 상황에서도 nickname이라는 변수에 두개의 스레드가 접근하기 위해 문제가 발생하는데, 동시에 변수가 접근할 수 있다 보니 &lt;b&gt;순서에 대한 보장이 이루어지지 않는다.&lt;/b&gt; 그래서 순서에 대한 보장이 되어 있지 않다보니 스레드들끼리 경쟁하고 점유하려고 하는 특성 때문에 Data Race가 발생하고 있는 상황이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt; 동시성 이슈의 원인&lt;br /&gt;: 정리해보면 동시성 이슈는 아래와 같은 상태에서 이루어진다.&lt;br /&gt;&lt;br /&gt;1. 공유 자원에 대한 동시 접근&lt;br /&gt;2. 작업 실행 순서의 비결정성&lt;br /&gt;3. 리소스 경쟁 상태&lt;br /&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  동시성 문제의 종류&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;동시성 이슈의 원인으로 발생하는 동시성 종류에 대해 알아보자.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Race Condition&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Dead Lock&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Starvation&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Priority Inversion (우선순위 역전 현상)&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1️⃣ Race Condition&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;여러 스레드가 공유 자원에 동시에 접근해서 결괏값에 영향을 주거나 연산 결과가 실행 순서에 따라 달라지는 현상&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아까 위에 코드로 예시를 든 경우이다. 이 경우 공유 자원에 대한 동시 접근으로 인해 순서가 꼬여서 Race Condition이 발생하는 경우이다.&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;b&gt; 해결방법&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;보통 이 경우 상호배제와 임계 영역을 설정해서 해결할 수 있다. 아까 위 예시를 통해 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;nickname이라는 공유 자원을 임계 영역으로 정하여 하나의 스레드만 접근할 수 있도록 만들어 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이걸 구현 단계에서 &lt;b&gt;Semaphore&lt;/b&gt;와&amp;nbsp;&lt;b&gt;Mutex&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;&lt;b&gt;Mutex&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776696884273&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    let nicknameLock = NSLock()

    ...
    
    func mutex() {
        // lock이 걸리는 순간 다른데서 접근하지 못함
        nicknameLock.lock()
        print(&quot;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&quot;, nickname)
        nicknameLock.unlock()
        
        DispatchQueue.global().async {
            self.nicknameLock.lock()
            self.nickname = &quot;칙촉&quot;
            self.nicknameLock.unlock()
        }
        
        DispatchQueue.global().async {
            self.nicknameLock.lock()
            self.nickname = &quot;카스타드&quot;
            self.nicknameLock.unlock()
        }
        self.nicknameLock.lock()
        print(&quot;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&quot;, nickname)
        self.nicknameLock.unlock()
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Mutex의 경우 NSLock을 통해 lock을 걸면 해제되기 전까지 다른 데서 접근하지 못한다.&lt;/b&gt; 스레드가 임계 영역에 진입하기 전에 mutex lock을 시도하고, lock 이 되었다면 임계 영역이 실행된다. (= 동시에 접근하지 못하도록 공유자원을 막아버린다.) 이후 작업이 완료되면 unlock을 해서 다른 스레드에서 접근이 가능해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;NSLock은 lock과 unlock 하는 두가지의 상태밖에 존재하지 않기 때문에 &lt;b&gt;bool 타입 처럼 동작&lt;/b&gt;한다. 그래서&lt;span style=&quot;color: #ee2323;&quot;&gt; &lt;b&gt;2진 상태를 가지는 특성&lt;/b&gt;&lt;/span&gt;이 있고, lock을 획득한 스레드만 해제할 수 있기 때문에 잠금을 획득한 스레드만 해제할 수 있다. 즉 &lt;b&gt;한 번에 하나의 스레드만 접근 가능&lt;/b&gt;하기 때문에&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;소유권을 가지게 된다&lt;/b&gt;&lt;/span&gt;는 특성이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt; 발생할 수 있는 문제&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;dispatchQueue.global()이 200개 정도라고 가정했을 때 다 lock을 걸어버린 경우 그 200개의 스레드들이 공유자원에 접근하려고 lock을 획득할 때까지 계속 접근하고 관찰하려고 할 것이다.&lt;br /&gt;이 현상을&lt;b&gt;&amp;nbsp;Spin Lock&amp;nbsp;&lt;/b&gt;이라고 부른다.&lt;br /&gt;&lt;br /&gt;Spin Lock은 이렇게 경쟁이 심한 상태에서 &lt;b&gt;starvation (기아 현상)&lt;/b&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;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Semaphore&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776698023074&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let nicknameDispatchSemaphore = DispatchSemaphore(value: 1)
// value가 가지는 의미는 공유자원에 접근할 수 있는 스레드의 갯수를 칭함
// = 공유자원에 접근할 수 있는 스레드의 갯수를 Integer타입으로 정의
...

func semaphore() {
    nicknameDispatchSemaphore.wait()
    print(&quot;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&quot;, nickname)
    nicknameDispatchSemaphore.signal()
    
    DispatchQueue.global().async {
        self.nicknameDispatchSemaphore.wait()
        self.nickname = &quot;칙촉&quot;
        self.nicknameDispatchSemaphore.signal()
    }
    
    DispatchQueue.global().async {
        self.nicknameDispatchSemaphore.wait()
        self.nickname = &quot;카스타드&quot;
        self.nicknameDispatchSemaphore.signal()
    }
    nicknameDispatchSemaphore.wait()
    print(&quot;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&quot;, nickname)
    nicknameDispatchSemaphore.signal()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 경우는 Integer / Binary Semaphore로 해결해 본 상태이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Integer / Binary Semaphore는 value를 사용해서 공유자원에 접근할 수 있는 스레드의 개수를 semaphore의 binary 변수로 관리하는 기법이며 mutex와 유사하지만 &lt;b&gt;binary Semaphore는 소유권 개념이 없어서 한 스레드가 획득하고 다른 스레드가 해제를 할 수 있다.&amp;nbsp;&lt;/b&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;mutex&lt;/b&gt; : lock을 획득한 스레드가 해제할 수 있음 -&amp;gt; 소유권 O&lt;/li&gt;
&lt;li&gt;&lt;b&gt;semaphore&lt;/b&gt; : 한 스레드가 획득하고 다른 스레드가 해제할 수 있음 -&amp;gt; 소유권 X&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  value의 갯수를 늘려서 공유자원에 접근하는 개수를 늘리게 되면 또 똑같은 문제 현상이 발생하지 않을까?&lt;/b&gt;&lt;br /&gt;: 공유자원에 접근해서 값을 바꿀때 즉 수정할 때 문제가 되는 거지 읽어오는 건 문제가 되지 않기 때문에 value의 값을 2~3 정도로 설정하여 동시에 접근할 수 있도록 사용할 수 있다.&lt;br /&gt;이 경우 소유권이 있냐 없냐에 따라 다르게 나타날 수 있는데 결론적으로 보면 value의 값을 늘리는 게 &lt;b&gt;data race가 발생할 수 있는 조건&lt;/b&gt;이 발생하게 된다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;즉 근본적인 race condition을 해결하지는 못하여 이를 Binary Semaphore를 통해 방지할 수 있다.&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;mutex와 semaphore 코드에서 문제가 발생하지 않는 것을 확인하기 위해서는 &lt;b&gt;edit scheme -&amp;gt; Diagnostics -&amp;gt; Main Thread Checker&lt;/b&gt;이랑 &lt;b&gt;Thread Performance Checker를 활성화&lt;/b&gt;해주면 보라색 오류가 뜬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;근데 위 semaphore와 Mutex로 해결한 방법으로 코드를 실행해 보면 보라색 에러가 사라진 걸 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-21 오전 12.14.56.png&quot; data-origin-width=&quot;1420&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tiEEa/dJMcaju9Usi/i0FTgFCdmHoge5Rqj9Hdbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tiEEa/dJMcaju9Usi/i0FTgFCdmHoge5Rqj9Hdbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tiEEa/dJMcaju9Usi/i0FTgFCdmHoge5Rqj9Hdbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtiEEa%2FdJMcaju9Usi%2Fi0FTgFCdmHoge5Rqj9Hdbk%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;1420&quot; height=&quot;392&quot; data-filename=&quot;스크린샷 2026-04-21 오전 12.14.56.png&quot; data-origin-width=&quot;1420&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2️⃣ Dead Lock&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;두 개 이상의 스레드가 서로 점유한 자원을 기다리며 영원히 진행되지 못하는 상태&lt;br /&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;이 데드락은 왜 발생하게 되는걸까?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;데드락이 발생하기 위한 조건은 아래와 같다.&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;상호배제&lt;/b&gt;: 공유 자원에 하나의 스레드만 접근이 가능해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;점유대기&lt;/b&gt;: 자원을 하나 보유한 상태에서 다른 스레드에 할당된 자원을 점유하기 위해 대기하는 스레드가 존재해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비선점&lt;/b&gt;: 점유 중인 자원을 강제로 뺏을 수 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;순환대기&lt;/b&gt;: 대기하는 스레드들이 순환 형태로 자원을 대기하고 있어야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;예시로 들어보면, nickname이라는 공유 자원에 하나의 스레드만 접근이 가능한 상황에서 다른 스레드가 nickname이라는 공유자원을 점유하기 위해 대기하고 있는데, 이걸 또 뺏을 순 없기 때문에 여러 스레드들이 순환 형태로 자원을 대기하면서 영영 기다리는 상태가 되어버리는 것을 의미한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 해결방법&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;데드락을 해결하기 위한 방법으로는 4가지의 방법이 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;예방&lt;/b&gt;: 데드락의 필요조건 중 하나 이상을 제거하는 방법, 데드락이 절대 발생하지 않지만, 리소스가 많이 든다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;회피&lt;/b&gt;: 시스템의 상태를 지속적으로 모니터링하여 데드락 가능성이 있는지 예측하고, 안전하지 않다고 판단되면 자원 할당 요청을 거부&lt;/li&gt;
&lt;li&gt;&lt;b&gt;탐지 및 복구&lt;/b&gt;: 데드락이 발생하도록 허용한 후 데드락 발생 여부를 주기적으로 탐지하여 복구하는 방법&lt;/li&gt;
&lt;li&gt;&lt;b&gt;무시&lt;/b&gt;: 데드락 발생 가능성을 인지하고는 있지만 특별한 조치를 취하지 않는 방법&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;데드락이 발생하는지 잘 모르기도 하고, 이를 개선하기 위한 많은 리소스와 비용이 필요하기 때문에 주로 데드락이 발생할 때 무시 전략을 선택하기도 한다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;무시 전략은 앱을 잘 사용하다가 갑자기 앱이 멈춰서 사용자가 앱을 백그라운드로 넘겨 강제종료 한 후 다시 사용한다거나 하는 방법들이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3️⃣ Starvation&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;특정 스레드가 필요한 자원을 계속 할당받지 못하는 상태로 무한정 대기 상태에 빠지는 상황&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;데드락과의 차이점은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;데드락은 시스템 전체가 멈추는&lt;/b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;반면&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;기아 상태&lt;/b&gt;는 &lt;b&gt;어떤 스레드 하나만 멈추게 되어 시스템은 계속 진행되지만 특정 스레드만 진행되지 않는 상태&lt;/b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4️⃣ Priority Inversion (우선순위 역전 현상)&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;b&gt;Swift Concurrency&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;결국 앞서 말한 동시성 이슈는 gcd로는 컴파일 시점에서 알기가 어렵다는 한계가 있다.&amp;nbsp;Edit Scheme에서 따로 설정을 해줘야 보라색 오류로 그나마 눈에 보이는데, 이것마저 설정을 하지 않는다면 런타임 시점에 앱에 크래시가 발생하고 터져야 동시성 이슈를 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그럼 이걸 &lt;b&gt;컴파일 시점에 동시성 이슈가 발생할만한 포인트를 알 수 없을까&lt;/b&gt; 고민하다가 이런 목적으로 등장하게 된 기술이 &lt;b&gt;Swift Concurrency&lt;/b&gt;이다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Swift Concurrency를 사용하면 컴파일 시점에 동시성 문제를 미리 발견할 수 있도록 한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Swift Concurrency에 본격적으로 들어가기 전 Swift Concurrency가 결국 해결하고자 하는 문제는 무엇인지, 동시성 문제부터 gcd를 사용해서 동시성 문제를 해결할 수 있는 방법 등에 대해서 공부해 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;다음 편에서는 swift Concurrency에 대해서 더 자세하게 알아보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>iOS/Swift</category>
      <category>GCD</category>
      <category>Swift Concurrency</category>
      <category>동시성</category>
      <author>yanni13</author>
      <guid isPermaLink="true">https://yanni13.tistory.com/80</guid>
      <comments>https://yanni13.tistory.com/80#entry80comment</comments>
      <pubDate>Tue, 21 Apr 2026 01:01:43 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] WidgetKit 알아보기 (+ AppGroup)</title>
      <link>https://yanni13.tistory.com/79</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✏️ WidgetKit&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;WidgetKit은 &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;사용자가 홈 화면 및 잠금화면에서 손쉽게 정보를 확인할 수 있는 &lt;/span&gt;&lt;b&gt;위젯을 구현할 때 주로 사용되는 프레임워크&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;WidgetKit은 기본적으로 iOS, iPadOS, macOS 멀티 플랫폼 지원을 위해 &lt;b&gt;SwiftUI로 구성&lt;/b&gt;되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;들어가기 앞서 위젯이 언제 처음 나오게 되었는지 한번 살펴보자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위젯의 역사는 아래 표를 통해 확인할 수 있다. iOS14부터 widgetkit을 사용할 수 있었고 점차 live activity와 같은 기능들이 추가된 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-08 오후 3.55.19.png&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmJqzP/dJMcag59Vbv/BjsZpCIYoN6f7F58QMDiuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmJqzP/dJMcag59Vbv/BjsZpCIYoN6f7F58QMDiuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmJqzP/dJMcag59Vbv/BjsZpCIYoN6f7F58QMDiuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmJqzP%2FdJMcag59Vbv%2FBjsZpCIYoN6f7F58QMDiuK%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;608&quot; height=&quot;210&quot; data-filename=&quot;스크린샷 2026-04-08 오후 3.55.19.png&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;WWDC 2020에 따르면 위젯의 정의는 크게 4가지의 요소로 구분된다고 한다.&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;Kind&lt;/b&gt; : 위젯이 가지는 식별자이며 하나의 extension이 여러 kind를 지원할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Configuration&lt;/b&gt;: Static Configuration vs Intent Configuration&lt;/li&gt;
&lt;li&gt;&lt;b&gt;supportedFailies&lt;/b&gt;: small, medium, large 크기의 위젯 지원&lt;/li&gt;
&lt;li&gt;&lt;b&gt;placeholder&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;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;오늘은 위젯을 설정하는 방법에 대한 Configuration에 대한 것과 WidgetKit의 핵심 구성 요소들, 전체 플로우에 대해서 알아보도록 하겠다!&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;b&gt;1️⃣ Configuration&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Configuration에는 Static Configuration과 Intent Configuration이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-07 오후 4.49.10.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;2024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLYidx/dJMcabcFgfV/qyUDjwbJ6I4KkDmK9bDLiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLYidx/dJMcabcFgfV/qyUDjwbJ6I4KkDmK9bDLiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLYidx/dJMcabcFgfV/qyUDjwbJ6I4KkDmK9bDLiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLYidx%2FdJMcabcFgfV%2FqyUDjwbJ6I4KkDmK9bDLiK%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;717&quot; height=&quot;480&quot; data-filename=&quot;스크린샷 2026-04-07 오후 4.49.10.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;2024&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;size18&quot;&gt;Target에 widgetkit을 추가할 때 Include Configuration App Intent 활성화 여부에 따라 Static Configuration으로 설정될지, Intent Configuration로 설정될지를 결정할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;활성화 O -&amp;gt; &lt;b&gt;&lt;b&gt;Intent Configuration&lt;/b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;활성화 X -&amp;gt; &lt;b&gt;Static Configuration&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;size18&quot;&gt;&lt;b&gt;&lt;b&gt;Intent Configuration&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1775634163870&quot; class=&quot;reasonml&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;struct ExampleWidget: Widget {
    let kind: String = &quot;ExampleWidget&quot;

    var body: some WidgetConfiguration {
        AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
            ExampleWidgetEntryView(entry: entry)
                .containerBackground(.fill.tertiary, for: .widget)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Static Configuration&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1775634240693&quot; class=&quot;reasonml&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;struct SampleWidget: Widget {
    let kind: String = &quot;SampleWidget&quot;

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            if #available(iOS 17.0, *) {
                SampleWidgetEntryView(entry: entry)
                    .containerBackground(.fill.tertiary, for: .widget)
            } else {
                SampleWidgetEntryView(entry: entry)
                    .padding()
                    .background()
            }
        }
        .configurationDisplayName(&quot;My Widget&quot;)
        .description(&quot;This is an example widget.&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;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;두 코드 간에 어떤 차이점이 있는지 살펴보자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Static Configuration&lt;/b&gt;은 말 그대로 고정된 방식이다. 사용자가 위젯에 대한 설정을 변경할 수 있는 옵션이 없기 때문에 &lt;b&gt;edit 시 위젯 편집 항목이 나타나지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Simulator Screenshot - iPhone 17 - 2026-04-07 at 16.57.02.png&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;2622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mFd5Y/dJMcahjH0pC/ZWtFrSxNOeEm7ueLjJKq7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mFd5Y/dJMcahjH0pC/ZWtFrSxNOeEm7ueLjJKq7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mFd5Y/dJMcahjH0pC/ZWtFrSxNOeEm7ueLjJKq7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmFd5Y%2FdJMcahjH0pC%2FZWtFrSxNOeEm7ueLjJKq7K%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;353&quot; height=&quot;767&quot; data-filename=&quot;Simulator Screenshot - iPhone 17 - 2026-04-07 at 16.57.02.png&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;2622&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;size18&quot;&gt;반면 &lt;b&gt;Intent Configuration&lt;/b&gt;은 위젯 편집 기능을 통해 사용자가 여러 Intent 값을 수정할 수 있도록 위젯을 구성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Simulator Screenshot - iPhone 17 - 2026-04-07 at 16.57.46.png&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;2622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgG7G9/dJMcacvVlgz/Pz6pTxaMpG3uP60vqNi6g0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgG7G9/dJMcacvVlgz/Pz6pTxaMpG3uP60vqNi6g0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgG7G9/dJMcacvVlgz/Pz6pTxaMpG3uP60vqNi6g0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgG7G9%2FdJMcacvVlgz%2FPz6pTxaMpG3uP60vqNi6g0%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;337&quot; height=&quot;733&quot; data-filename=&quot;Simulator Screenshot - iPhone 17 - 2026-04-07 at 16.57.46.png&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;2622&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;size18&quot;&gt;Edit시 위젯 편집 항목이 나타나며 iOS 17 이후부터는 IntentConfiguration이 AppIntentConfiguration으로 변경되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;IntentConfiguration 방식은 아이폰 기본 앱인 날씨 위젯을 생각해 보면 더 이해하기 쉬울 거 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Adobe Express - ScreenRecording_04-08-2026 16-11-29_1.gif&quot; data-origin-width=&quot;222&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zXalR/dJMcacbB4RY/uo0nMbHF7PAmG6RQOOfmgk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zXalR/dJMcacbB4RY/uo0nMbHF7PAmG6RQOOfmgk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zXalR/dJMcacbB4RY/uo0nMbHF7PAmG6RQOOfmgk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/zXalR/dJMcacbB4RY/uo0nMbHF7PAmG6RQOOfmgk/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;222&quot; height=&quot;480&quot; data-filename=&quot;Adobe Express - ScreenRecording_04-08-2026 16-11-29_1.gif&quot; data-origin-width=&quot;222&quot; data-origin-height=&quot;480&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;size18&quot;&gt;앱 안으로 들어가 별도의 설정을 하지 않아도 롱탭 제스처를 통해서 위젯 편집으로 위치 변경이 가능하다는 특징이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;(그만큼 구현은 Static Configuration보단 조금 더 복잡하다!)&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2️⃣ Provider / Entry / EntryView / Widget&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위젯은 크게 &lt;b&gt;&lt;span data-token-index=&quot;1&quot;&gt;Provider &amp;rarr; Entry &amp;rarr; EntryView &amp;rarr; Widget&lt;/span&gt; &lt;/b&gt;4가지의 struct로 구성되어 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1775632684136&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. Provider에서 사용자가 설정한 시간에 맞춰 위젯을 업데이트 할 수 있도록 함.
2. 위젯에 필요한 데이터들은 Entry가 제공!
3. Entry를 통해 EntryView를 구성함 EntryView는 UI를 담당하는 역할
4. EntryView를 통해 widget을 최종적으로 구성하게 됨&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;전체적인 위젯 플로우의 흐름을 보아도&lt;span&gt;&amp;nbsp;&lt;/span&gt;Provider &amp;rarr; Entry &amp;rarr; EntryView &amp;rarr; Widget&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;순으로 엮여있다는 것을 확인할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;각 역할에 대해서 하나하나 살펴보자!&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;Provider&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;위젯의 디스플레이를 업데이트할 시기를 widgetkit에게 알려주는 타입&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;사용자가 홈 화면에 머무르는 시간은 길지 않기 때문에 짧은 시간 안에 위젯을 봐야 하는데, 위젯에 로딩이 길면 이탈을 하기 쉽고 결국 좋지 않은 형태의 위젯이 되어버린다. 그래서 미리 위젯 뷰를 그리고 있다가 시간에 맞춰 뷰를 업데이트하고 TimelineEntry 배열을 통해 특정 시간에 위젯을 업데이트할 수 있도록 하기 위한 역할이다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;프로젝트 파일 Target에 widgetkit을 추가하고 나면 provider에 아래 세 가지 함수가 자체적으로 생성된 채로 파일이 만들어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1775633121221&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 위젯 최초 렌더링
func placeholder(in context: Context) -&amp;gt; SimpleEntry {
    SimpleEntry(date: Date(), configuration: ConfigurationAppIntent())
}

// 위젯 갤러리 미리보기 화면
func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -&amp;gt; SimpleEntry {
    SimpleEntry(date: Date(), configuration: configuration)
}

// 위젯 상태 변경 시점
func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -&amp;gt; Timeline&amp;lt;SimpleEntry&amp;gt; {
    var entries: [SimpleEntry] = []

    // Generate a timeline consisting of five entries an hour apart, starting from the current date.
    let currentDate = Date()
    for hourOffset in 0 ..&amp;lt; 5 {
        let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
        let entry = SimpleEntry(date: entryDate, configuration: configuration)
        entries.append(entry)
    }

    return Timeline(entries: entries, policy: .atEnd)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;즉 이 세가지 함수는 필수적으로 구현되어야 하는 함수이다!&lt;/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;placeholder&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;widgetkit이 최초로 위젯을 렌더링 할 때 사용되며 스켈레톤 뷰로 렌더링된다. 사용자가 잠금화면에서 민감한 정보를 숨기도록 선택한 경우 잠금 해제 전까지 widget을 placeholder로 숨길 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;snapshot&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위젯을 최초로 추가할 때, 위젯 갤러리에서 위젯 미리 보기 화면이 등장하는데 위젯을 구성하는 데이터가 네트워크 통신을 통해 가져오거나 계산하는데 몇 초 이상 걸릴 경우 대비해 mock 데이터를 이용해 빠르게 위젯을 그릴 수 있도록 설정할 때 getSnapShot을 사용함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;timeline&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;widget상태가 변경될 미래 시간이 포함된 timelineEntry 배열과 timeline 정책을 포함하고 있는 Timeline을 반환하며 &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;b&gt;특정 시간에 어떤 데이터를 표시할지를 정의&lt;/b&gt;한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Entry&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;TimelineEntry Protocol을 통해 위젯을 구성하는데 필요한 데이터를 가지고 있으며 &lt;b&gt;특정 시간에 어떤 데이터를 표시할지를 정의&lt;/b&gt;한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1775633847438&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: ConfigurationAppIntent
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;EntryView&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Entry를 이용해서 위젯에 나타날 뷰를 그려줌&lt;/b&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1775633859521&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct ExampleWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        VStack {
            Text(&quot;ExampleWidget&quot;)
            Text(entry.date, style: .time)

            Text(entry.configuration.favoriteEmoji)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;EntryView는 단순히 entry를 매개변수로 가지는 SwiftUI View이기 때문에 원하는 UI를 자유롭게 구성할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Widget&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;최종적으로 Widget Configuration을 구성&lt;/b&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1775633881455&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct ExampleWidget: Widget {
    let kind: String = &quot;ExampleWidget&quot; // 위젯의 고유한 문자열

    var body: some WidgetConfiguration {
        AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
            ExampleWidgetEntryView(entry: entry)
                .containerBackground(.fill.tertiary, for: .widget)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;StaticConfiguration, AppIntentConfiguration 등에서는 EntryView를 최종적으로 반환하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이렇게 WidgetKit을 사용해서 뷰를 띄우는 방법을 공유해 보았다. 하지만 우리가 프로젝트 개발에 있어서 중요한 건 앱 자체에 있는 데이터를 결국 위젯이 표시하게 하는 건데 이건 데이터 동기화도 함께 고려해보아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그때 고려해볼 수 있는 게 AppGroup이다!&lt;/p&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;&lt;b&gt;  AppGroup&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;AppGroup을 통해 App과 AppExtension 간 데이터를 공유할 수 있음&lt;/blockquote&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;appgroup.png&quot; data-origin-width=&quot;605&quot; data-origin-height=&quot;277&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUfqng/dJMcaaETrNH/gKMc3rUh2UXk1I2KJnRvck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUfqng/dJMcaaETrNH/gKMc3rUh2UXk1I2KJnRvck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUfqng/dJMcaaETrNH/gKMc3rUh2UXk1I2KJnRvck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUfqng%2FdJMcaaETrNH%2FgKMc3rUh2UXk1I2KJnRvck%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;605&quot; height=&quot;277&quot; data-filename=&quot;appgroup.png&quot; data-origin-width=&quot;605&quot; data-origin-height=&quot;277&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;size18&quot;&gt;WidgetKit을 target에 추가하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;앱 target이랑 widget target이 가지는 샌드박스는 &lt;b&gt;서로 다른 샌드박스를 가리키기 때문에&lt;/b&gt; 자동으로 데이터 동기화가 이루어지지 않는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;즉, Extension의 bundle은 Container App 번들에 포함되어 잇지만 두 가지는 각각의 Container를 가지고 있기 때문에 App과 App Extension사이에는 데이터가 공유되지 않는 특성이 있다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;이때 AppGroup은 동일한 앱 그룹 식별자를 가진 앱들이 동일한 컨테이너에 접근하여 데이터를 공유할 수 있도록 해준다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;앱 target이랑 widgetkit target이랑 UserDefaults를 공유하고 싶을 때도 App과 App Extension에서 저장되는 위치가 각각 다르기 때문에 App에서 사용하던 UserDefaults를 App Extension에서 사용할 수 없다. &lt;b&gt;&lt;span data-token-index=&quot;1&quot;&gt;그래서 UserDefaults도 shared container를 만들어서 처리해야 한다!&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p 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;b&gt;1. TARGETS -&amp;gt; Signing &amp;amp; Capabilities -&amp;gt; Capability에서 AppGroup 추가&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-08 오후 5.35.02.png&quot; data-origin-width=&quot;1586&quot; data-origin-height=&quot;1216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pyi8F/dJMcacCHqoB/KQjvN71FfNATtmuTu2TYCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pyi8F/dJMcacCHqoB/KQjvN71FfNATtmuTu2TYCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pyi8F/dJMcacCHqoB/KQjvN71FfNATtmuTu2TYCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpyi8F%2FdJMcacCHqoB%2FKQjvN71FfNATtmuTu2TYCk%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;520&quot; height=&quot;399&quot; data-filename=&quot;스크린샷 2026-04-08 오후 5.35.02.png&quot; data-origin-width=&quot;1586&quot; data-origin-height=&quot;1216&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 앱 Target이랑 widgetTarget이랑 동일한 group 네이밍으로 설정해 주기!&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-08 오후 5.36.28.png&quot; data-origin-width=&quot;1506&quot; data-origin-height=&quot;742&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQahW5/dJMcagkLAGj/Qft2lZFRoeukzrwtw9JKYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQahW5/dJMcagkLAGj/Qft2lZFRoeukzrwtw9JKYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQahW5/dJMcagkLAGj/Qft2lZFRoeukzrwtw9JKYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQahW5%2FdJMcagkLAGj%2FQft2lZFRoeukzrwtw9JKYk%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;650&quot; height=&quot;320&quot; data-filename=&quot;스크린샷 2026-04-08 오후 5.36.28.png&quot; data-origin-width=&quot;1506&quot; data-origin-height=&quot;742&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-08 오후 5.36.32.png&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;746&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OzNW5/dJMcagkLAFJ/YFNqrIBBCrj8H1kIGQTA01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OzNW5/dJMcagkLAFJ/YFNqrIBBCrj8H1kIGQTA01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OzNW5/dJMcagkLAFJ/YFNqrIBBCrj8H1kIGQTA01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOzNW5%2FdJMcagkLAFJ%2FYFNqrIBBCrj8H1kIGQTA01%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;651&quot; height=&quot;323&quot; data-filename=&quot;스크린샷 2026-04-08 오후 5.36.32.png&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;746&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그리고 아래코드와 같이 UserDefaults 인스턴스에서 AppGroup을 사용하면 된다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1775637849975&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; private let appGroupID = &quot;group.yan.widgetkitTest&quot;

...
 UserDefaults(suiteName: appGroupID)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;이때 UserDefaults.standard가 아닌 suiteName을 사용해주어야 한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;UserDefaults.standard는 앱 타겟 전용 컨테이너에만 저장되기 때문에 공유 저장소를 가지는 의미인 &lt;b&gt;suiteName&lt;/b&gt;을 사용해서 이 데이터는 공유 저장소임을 명시하는 것이다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;파일 경로에서도 확인해 볼 수 있다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;UserDefaults.standard에서는 아래 경로로 저장되는 반면&lt;/p&gt;
&lt;pre id=&quot;code_1775638271310&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/Library/Preferences/&amp;lt;bundle-id&amp;gt;.plist&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;UserDefaults.suiteName은 아래 groupId를 통해서 저장되기 때문에 동일한 groupId를 가지는 모든 컨테이너에서 접근할 수 있다!&lt;/p&gt;
&lt;pre id=&quot;code_1775638311144&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/Library/Group Containers/&amp;lt;group-id&amp;gt;/Library/Preferences/&amp;lt;group-id&amp;gt;.plist&lt;/code&gt;&lt;/pre&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;b&gt;  고려해봐야 하는 점&lt;/b&gt;&lt;/h2&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;Widgetkit에도 아래 한계점이 존재한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1️⃣ Link&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;첫 번째로 small widget 인 경우 보여줘야 하는 정보들이 최소화되어야 하기 때문에 인터렉션의 제약이 크다. WidgetKit은 기본적으로 전체 영역을 클릭하면 딥링크로 이동되는 구조인데&amp;nbsp;예를 들어 여러 개의 링크가 small widget에 표시된다면 터치 영역이 충돌해서 사실상 동작에 오류가 발생하거나 일부 링크에 대해서는 동작하지 않을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2️⃣ 메모리 30MB 제한&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Widgetkit에서는 메모리가 30MB로 제한된다. 따라서 네트워크 통신 로직이나 이미지 로딩과 같이 무거운 작업들이 위젯에서 이루어진다면 런타임 크래시가 발생할 가능성이 높다. 따라서 위젯기능을 활용할 때는 용량뿐만 아니라 메모리 최적화까지 함께 고려해서 설계해 볼 수 있을 거 같다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3️⃣ Refresh (40~70회)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;apple에서는 하루 refresh 횟수 가이드를 40~70회 정도를 기준으로 잡고 있다. 시스템이 자체적으로 업데이트 시점을 관리하기 때문에 개발자가 정확한 refresh 타이밍을 제어하는 건 어렵다. 업데이트 빈도도 제한적이기 때문에 실시간 데이터 반영이 필요한 서비스에는 적합하지 않을 수 있다. 주식과 같은 실시간 변동이 필요한 앱은 오히려 live activity를 활용해 보는 것도 방법일 거 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4️⃣ 동작방식&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Widgetkit은 lifeCycle이 아니라 Timeline기반으로 동작한다. 따라서 개발자가 원하는 시점에 로직을 실행하거나 데이터를 갱신하는 것이 어렵다. 시스템이 정한 시점에 getTimeline이 호출되고 그때 준비된 데이터를 기반으로 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;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;&lt;b&gt;실행결과&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;실제 프로젝트에서 widgetkit을 small, medium, large로 구현해 보았다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;해당 코드는 이 &lt;a title=&quot;레포지토리&quot; href=&quot;https://github.com/yanni13/ColorWalk/tree/main/ColorWalk/ColorWalkWidget&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;레포지토리&lt;/a&gt;를 참고해서 보면 될 거 같다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;위젯.gif&quot; data-origin-width=&quot;295&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lzGbZ/dJMcafsCacp/hgjmCooW7skcDzmJz8QjCk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lzGbZ/dJMcafsCacp/hgjmCooW7skcDzmJz8QjCk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lzGbZ/dJMcafsCacp/hgjmCooW7skcDzmJz8QjCk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/lzGbZ/dJMcafsCacp/hgjmCooW7skcDzmJz8QjCk/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;295&quot; height=&quot;640&quot; data-filename=&quot;위젯.gif&quot; data-origin-width=&quot;295&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;잘 작동되는 것을 확인해 볼 수 있다~&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/kr/videos/play/wwdc2020/10028/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/kr/videos/play/wwdc2020/10028/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@tygerhwang/%EC%99%84%EB%A3%8CSwift-WidgetKit-%EC%9D%B4%EB%9E%80&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@tygerhwang/%EC%99%84%EB%A3%8CSwift-WidgetKit-%EC%9D%B4%EB%9E%80&lt;/a&gt;&lt;/p&gt;</description>
      <category>iOS/SwiftUI</category>
      <category>AppGroup</category>
      <category>WidgetKit</category>
      <author>yanni13</author>
      <guid isPermaLink="true">https://yanni13.tistory.com/79</guid>
      <comments>https://yanni13.tistory.com/79#entry79comment</comments>
      <pubDate>Wed, 8 Apr 2026 18:08:51 +0900</pubDate>
    </item>
    <item>
      <title>[CS] Coordinator 패턴은 왜 사용하는 것일까?</title>
      <link>https://yanni13.tistory.com/78</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✏️ Coordinator 패턴&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Coordinator 패턴은&lt;b&gt; ViewController에서 수행하던 화면 전환에 대한 로직을 Coordinator를 통해 분리한다는 컨셉&lt;/b&gt;을 가지고 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;나는 개발하다가 ViewController에서 navigate를 할 때 이때껏 큰 불편함을 느끼지 못했었는데, 프로젝트 규모가 6개월을 넘어갈 정도로 커지게 되면서 화면 전환 로직을 더 이상 ViewController가 가지고 있는 게 맞는지, 화면이동 간의 중복코드가 너무 많아지는 문제점들이 있어서 Coordinator 패턴에 대한 관심을 가지게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;navigation 하는 코드 그거 얼마나 된다고 추가적인 학습 비용을 감수하더라도 coordaintor 패턴을 써야 하는지 의문이 들 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(나 조차 그랬으니 . .)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 이런 의문점들을 풀어보고자 포스팅을 작성해보려고 한다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #9d9d9d; text-align: start;&quot;&gt;(제가 공부하기 위해 작성한 것이므로 &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #9d9d9d; text-align: start;&quot;&gt;틀린 정보가 있다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;알려주세요!)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;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;b&gt;&lt;br /&gt;도대체 왜 사용하는 것일까?&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;UIKit 기준으로 모든 ViewController에서 화면 전환이 이루어진다.&lt;/p&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;b&gt;기존 코드의 문제점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;TableView가 있는 화면인 경우 didSelectRowAt 안에서 특정 셀을 클릭했을 때 A -&amp;gt; B로 넘어가는 로직이 작성되고, 또 다른 뷰에서도 계속 비슷한 형식으로 ViewController안에 화면 전환 로직이 작성될 텐데 &lt;u&gt;앱의 규모가 커져서 관리해야 하는 화면 갯수가 많아지게 되거나&lt;/u&gt; &lt;u&gt;하나의 뷰 컨트롤러 안에서 여러 가지 뷰를 제어해야 하는 상황&lt;/u&gt;이라면 뷰컨트롤러는 UI를 그리는 역할뿐만 아니라 어떤 뷰를 보여줄지 관리까지 해야 하는 역할을 담당하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;극단적인 예시를 들어보자면 VC안에 있는 tableView에서 로그인 유무에 따라 다른 뷰를 보여주고자 하면 VC안에서 아래와 같이 분기처리를 해야 하는 로직이 담겨있을 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1774271883542&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

    if AuthManager.shared.isLoggedIn {
        let detailVC = DetailViewController()
        navigationController?.pushViewController(detailVC, animated: true)
    } else {
        let loginVC = LoginViewController()
        navigationController?.pushViewController(loginVC, animated: true)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;MVVM 패턴을 사용해서 viewModel이 분기처리를 하는 방식으로 해결해 볼 수 있지만, 해당 방법의 한계점은 결국 뷰모델이 로그인여부를 확인해서 뷰에 output을 주게 되면 &lt;b&gt;뷰컨트롤러에서 이동을 시키는 건 동일&lt;/b&gt;하다.&lt;/p&gt;
&lt;pre id=&quot;code_1774272357125&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if isLoggedIn {
    let detailVC = DetailViewController()
    navigationController?.pushViewController(detailVC, animated: true)
} else {
    let loginVC = LoginViewController()
    navigationController?.pushViewController(loginVC, animated: true)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;반면 Coordinator를 사용하게 된다면 뷰모델에서 로그인 여부 로직을 확인 후, VC에서는 어떤 뷰로 이동할 건지는 관심 없고 coordinator에게 신호만 전달해 주면 각 화면의 Coordaintor가 계층적으로 뷰의 이동을 관리할 수 있기 때문에 View Controller는 뷰를 그리고, Model 혹은 ViewModel과 상호작용 하여 어떤 뷰를 표시해 줄지를 결정하도록 &lt;b&gt;SRP (단일 책임 원칙)을 가질 수 있게 된다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;즉 VC에서 navigation으로 이동해 주는 방식은 navigation 로직이 VC마다 존재하기 때문에 분산되어 있는 반면 Coordinator를 사용하면 AppCoordinator에서 내려주는 구조이기 때문에 Coordaintor가 하나의 전체 흐름을 관리하는 중앙집중 방식으로 진행된다는 차이점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;따라서 &lt;b&gt;ViewController는 navigation을 직접 수행하지 않고, 이벤트만 전달하며 실제 화면 전환(push/present)은 Coordinator가 담당&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SceneDelegate에서 B화면으로 진입하는 과정까지를 그림으로 그려보았을 때 차이점을 더 확실하게 알 수 있었다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;VC에서 navigation을 처리하는 과정&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-03-23 오후 11.35.01.png&quot; data-origin-width=&quot;492&quot; data-origin-height=&quot;602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VTmew/dJMcahX4L4a/aHCSR4jrVfdvEt6ham3gHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VTmew/dJMcahX4L4a/aHCSR4jrVfdvEt6ham3gHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VTmew/dJMcahX4L4a/aHCSR4jrVfdvEt6ham3gHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVTmew%2FdJMcahX4L4a%2FaHCSR4jrVfdvEt6ham3gHK%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;302&quot; height=&quot;370&quot; data-filename=&quot;스크린샷 2026-03-23 오후 11.35.01.png&quot; data-origin-width=&quot;492&quot; data-origin-height=&quot;602&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;size18&quot;&gt;B ViewController로 가기까지의 과정을 보면 B ViewController로 이동해야 한다는 사실을 A ViewController에서 결정하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;따라서 기존 방식의 문제점은 B화면의 흐름 로직이 A화면에서 다루고 있으니 둘 사이의 결합도가 매우 강하다는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Coordinator를 사용해서 처리하는 과정&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-03-23 오후 11.41.33.png&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8SkCS/dJMcaf61iV8/AVkaBRxtRviQARBqkxGXNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8SkCS/dJMcaf61iV8/AVkaBRxtRviQARBqkxGXNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8SkCS/dJMcaf61iV8/AVkaBRxtRviQARBqkxGXNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8SkCS%2FdJMcaf61iV8%2FAVkaBRxtRviQARBqkxGXNK%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;502&quot; height=&quot;437&quot; data-filename=&quot;스크린샷 2026-03-23 오후 11.41.33.png&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;반면 Coordinator 패턴을 사용하면 도메인별로 A화면의 Coordinator와 B화면 전환에 대한 Coordinator가 각각 생기기 때문에 B 화면에 대한 결정권은 A Coordinator에게 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;따라서 A ViewController는 이동한다는 신호만 트리거 시켜주면 되기 때문에 다음 VC가 어딘지 신경 쓰지 않아도 되고 이를 통해&lt;span&gt;&amp;nbsp;&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;size18&quot;&gt;수정할 때도 용이한데, 갑자기 기획이 변경돼서 A ViewController에서 C&amp;nbsp; ViewController로 이동해야 하는 경우 A ViewController 로직을 수정하는 게 아닌 A Coordinator 부분을 수정하면 되기 때문에 각 계층 간의 역할과 책임도 분리되게 된다는 장점이 있다.&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;한마디로 정리하자면 Coordinator 패턴은 navigation 코드를 분리하는 것뿐만 아니라 앱의 화면 흐름(Flow)을 ViewController 밖에서 통제함으로써 ViewController 간 결합도를 낮추고, 흐름 변경을 유연하게 만들 수 있기 때문에 사용하는 것이다!&lt;/b&gt;&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;참고&lt;/p&gt;
&lt;figure id=&quot;og_1774275806137&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 - kudoleh/iOS-Clean-Architecture-MVVM: Template iOS app using Clean Architecture and MVVM. Includes DIContainer, FlowCoor&quot; data-og-description=&quot;Template iOS app using Clean Architecture and MVVM. Includes DIContainer, FlowCoordinator, DTO, Response Caching and one of the views in SwiftUI - kudoleh/iOS-Clean-Architecture-MVVM&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/kudoleh/iOS-Clean-Architecture-MVVM&quot; data-og-url=&quot;https://github.com/kudoleh/iOS-Clean-Architecture-MVVM&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/6Y1C0/dJMb8T9YUCg/kcvOVKIRm7IFhNYmNP1RFK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/wL4r6/dJMb8UHOA8G/TOzqjmWDgIyrQiYJ1WO3D0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bhPpSl/dJMb8TB82BK/mQKXvuTvSEonfu8MR2G2c1/img.png?width=1474&amp;amp;height=780&amp;amp;face=0_0_1474_780&quot;&gt;&lt;a href=&quot;https://github.com/kudoleh/iOS-Clean-Architecture-MVVM&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/kudoleh/iOS-Clean-Architecture-MVVM&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/6Y1C0/dJMb8T9YUCg/kcvOVKIRm7IFhNYmNP1RFK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/wL4r6/dJMb8UHOA8G/TOzqjmWDgIyrQiYJ1WO3D0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bhPpSl/dJMb8TB82BK/mQKXvuTvSEonfu8MR2G2c1/img.png?width=1474&amp;amp;height=780&amp;amp;face=0_0_1474_780');&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 - kudoleh/iOS-Clean-Architecture-MVVM: Template iOS app using Clean Architecture and MVVM. Includes DIContainer, FlowCoor&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Template iOS app using Clean Architecture and MVVM. Includes DIContainer, FlowCoordinator, DTO, Response Caching and one of the views in SwiftUI - kudoleh/iOS-Clean-Architecture-MVVM&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;
&lt;figure id=&quot;og_1774277487980&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Clean Architecture and MVVM on iOS&quot; data-og-description=&quot;When we develop software it is important to not only use design patterns, but also architectural patterns. There are many different&amp;hellip;&quot; data-og-host=&quot;tech.olx.com&quot; data-og-source-url=&quot;https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3&quot; data-og-url=&quot;https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/DGXBH/dJMb9aKEy3I/vIwbR6TQx2VKTsDUypFS31/img.jpg?width=1200&amp;amp;height=1146&amp;amp;face=0_0_1200_1146&quot;&gt;&lt;a href=&quot;https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/DGXBH/dJMb9aKEy3I/vIwbR6TQx2VKTsDUypFS31/img.jpg?width=1200&amp;amp;height=1146&amp;amp;face=0_0_1200_1146');&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;Clean Architecture and MVVM on iOS&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;When we develop software it is important to not only use design patterns, but also architectural patterns. There are many different&amp;hellip;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;tech.olx.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://zeddios.medium.com/coordinator-pattern-bf4a1bc46930&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://zeddios.medium.com/coordinator-pattern-bf4a1bc46930&lt;/a&gt;&lt;/p&gt;</description>
      <category>CS</category>
      <category>Coordinator</category>
      <category>디자인패턴</category>
      <author>yanni13</author>
      <guid isPermaLink="true">https://yanni13.tistory.com/78</guid>
      <comments>https://yanni13.tistory.com/78#entry78comment</comments>
      <pubDate>Tue, 24 Mar 2026 00:44:40 +0900</pubDate>
    </item>
    <item>
      <title>[RxSwift] RxSwift 기본 개념 파헤치기 (Subscribe, Bind, Subject, Relay, Drive)</title>
      <link>https://yanni13.tistory.com/76</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;오늘은 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;&lt;span style=&quot;background-color: #ffffff; color: #9d9d9d; text-align: start;&quot;&gt;(제가 공부하기 위해 작성한 것이므로 여러 개념들이 나올 수 있으며 틀린 정보가 있다면 알려주세요!)&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;RxSwift의 기본 컨셉이 되는 Observable, Observer, Dispose에 대한 이야기는 이전에 작성해둔 블로그도 있기 때문에 오늘은 프로퍼티에 대해서 더 자세히 알아보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1️⃣ Subscribe&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;RxSwift는 기본적으로 이벤트를 전달하는 &lt;b&gt;Observable&lt;/b&gt;과 이벤트를 처리하는 &lt;b&gt;Observer&lt;/b&gt;로 구성되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;(Observable과 Observer에 대한건 이전 포스팅에도 있기 때문에 이번 포스팅에서는 짧게 다루고 넘어가게보겠다!)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Observable은 이벤트를 전달하는 입장이고 Observer는 그 이벤트를 계속해서 받는 입장이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;예를 들어 버튼을 클릭했을때 버튼을 클릭했다는 신호 혹은 셀을 클릭하거나 검색어를 입력했을때 이벤트를 계속해서 전달하고 있는게 Observable이고 그 이벤트를 받아서 레이블을 표시하고 화면전환을 통해 검색 결과를 보여주는 역할을 Observer가 하게 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이때 계속해서 observable이 전달하는 이벤트를 Observer에게 전달해줄 무언가가 필요한데 둘 사이를 연결해주는 역할이 바로 &lt;b&gt;Subscribe&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-filename=&quot;스크린샷 2026-02-24 오전 1.32.54.png&quot; data-origin-width=&quot;1810&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKclc9/dJMcadueLAJ/ITfVIydJt6vNbR02nYXav0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKclc9/dJMcadueLAJ/ITfVIydJt6vNbR02nYXav0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKclc9/dJMcadueLAJ/ITfVIydJt6vNbR02nYXav0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKclc9%2FdJMcadueLAJ%2FITfVIydJt6vNbR02nYXav0%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;710&quot; height=&quot;90&quot; data-filename=&quot;스크린샷 2026-02-24 오전 1.32.54.png&quot; data-origin-width=&quot;1810&quot; data-origin-height=&quot;230&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;subscribe는 &lt;u&gt;onNext, onComplete, OnError, onDisposed&lt;/u&gt; 4개에 대한 이벤트를 처리할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1773301605542&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Observable
	.just(&quot;중복확인&quot;)
        .subscribe { value in  // OnNext
            print(&quot;next: \(value)&quot;)
            self.navigationItem.title = value
            self.nextButton.setTitle(value, for: .normal)
        } onError: { error in
            print(&quot;error: \(error)&quot;)
        } onCompleted: {
            print(&quot;completed&quot;)
        } onDisposed: {
            print(&quot;disposed&quot;)
        }
        .disposed(by: disposeBag)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;onNext&lt;/b&gt;: subscribe는 next 이벤트를 통해서 이벤트를 방출하는 특성을 가지고 있다. ( onNext는 Trailing Closure의 특성을 가지고 있어서 첫 번째 파라미터인 onNext 라벨이 생략된 것이다!)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;onError: &lt;/b&gt;Observable이 이벤트가 실행중 Lifecycle 중 오류가 발생한다면 error 이벤트를 방출&lt;/li&gt;
&lt;li&gt;&lt;b&gt;onCompleted: &lt;/b&gt;정상적으로 모든 이벤트가 완료된다면 completed를 Notification 으로 안내&lt;/li&gt;
&lt;li&gt;&lt;b&gt;onDisposed: &lt;/b&gt;deinit 되는 시점이나 Sequence가 종료되는 시점에 disposed가 실행&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;size18&quot;&gt;error 또는 completed를 전달받으면 해당 Sequence가 종료되고, 종료된 이후에는 더 이상 이벤트가 발생하지 않는다. Observable은 내부에 상태를 가지는데 한 번 종료 상태로 넘어가면, 어떤 값을 넣어도 다시 Active 상태로 돌아오지 않기 때문에&amp;nbsp;Observable에 대한 재사용이 불가능하게 된다.&lt;/p&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;subscribe는 당연하게도 이벤트를 전달하는 역할이기 때문에 observable이나 observer가 하나라도 없다면 존재할 수 없게 된다.&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;label.rx.text 에는 subscribe 가 되지 않는 이유?&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;라벨은 무한한 이벤트이기 때문에 오류가 절대 발생하지 않기 때문이다. TV에 빗대어서 설명해보자면 Observable은 전기를 보내고 있는데 label.rx.text는 단순히 TV에 어떤 화면을 보여줄지 표시하는 역할이다. &lt;br /&gt;&lt;br /&gt;&lt;b&gt;즉 성립될 수 없는 이유는 subscribe 는 데이터를 주면 연결해주겠다는 명령인데 label은 데이터를 주는 애가 아니라 받는 애인 Observer이기 때문에 subscribe가 되지 않는다.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;이외에도 label을 표시하는게 실패할 일이 없기 때문에 에러가 발생하지 않고 onCompleted도 이벤트가 완벽하게 끝이 났을 경우에 실행되는 건데 &lt;b&gt;label은 무한한 이벤트이며 끝난다는 개념이 없기 때문에 실행되지 않는다.&amp;nbsp;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;disposed 측면에서 한번 살펴보자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Observable에서 next 이벤트에 대한 emit이 끝나면, &lt;u&gt;completed &amp;rarr; disposed&lt;/u&gt;로 Sequence가 정상적으로 종료되고, error가 와도 error 이벤트에 대해 방출이 되면 &lt;u&gt;&lt;span&gt;&amp;nbsp;error&lt;/span&gt;&amp;nbsp;&amp;rarr; disposed&lt;/u&gt;로 Sequence가 정상적으로 종료된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 무한한 이벤트를 가지는 버튼이나 라벨과 같이 무한히 emit이 될 수 있는 상황에서는, 버튼을 100번 누르든 1000번 누르든 사용자가 앱을 끄기 전까진 이벤트가 계속 실행될 수 있기 때문에 disposed 또한 되지 않는다.&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;그래서 disposeBag을 통해 강제 종료 해주는 시점이 필요한 것이다!&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2️⃣ Bind&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;bind는 subscribe랑 동일하게 Observable와 Observer를 연결해주는 역할&lt;/b&gt;을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Observable과 Observer를 연결하도록 구독해서 계속 관찰하고 있다가 값이 변경됨을 감지하면 다음 이벤트를 어떻게 처리할 지 명시해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앞서 말한 버튼과 같은 무한한 이벤트에서는 onCompleted가 실행될 일도, onError가 실행될 일도 없기 때문에 클로저에 의미없는 print를 작성하거나 비워둘 빠에는 사용하지 않는 편이 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;따라서 우리는 subscribe에서 4가지 이벤트 클로저에서 onNext만 사용해야지!라고 할수도 있지만 그마저 &lt;b&gt;불필요한 리소스나 공간을 차지하지 않도록 하기 위해 사용하는 것이 bind&lt;/b&gt;이다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-24 오전 1.32.08.png&quot; data-origin-width=&quot;1042&quot; data-origin-height=&quot;238&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PlWEb/dJMcahKfALr/jQnE7LWHiQdFmVR0ay2iUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PlWEb/dJMcahKfALr/jQnE7LWHiQdFmVR0ay2iUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PlWEb/dJMcahKfALr/jQnE7LWHiQdFmVR0ay2iUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPlWEb%2FdJMcahKfALr%2FjQnE7LWHiQdFmVR0ay2iUK%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;139&quot; data-filename=&quot;스크린샷 2026-02-24 오전 1.32.08.png&quot; data-origin-width=&quot;1042&quot; data-origin-height=&quot;238&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;size18&quot;&gt;bind는 매개변수가 &lt;b&gt;onNext에 대한 이벤트&lt;/b&gt;밖에 처리할 수 없다.&lt;/p&gt;
&lt;pre id=&quot;code_1771864429985&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.bind { value in
    print(&quot;실시간 검색&quot;, value)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;따라서 bind를 사용해서 ui코드도 아래와 같이 구성해볼 수 있게 된다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771864587217&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;placeholder
      .bind(to: emailTextField.rx.placeholder)
      .disposed(by: disposeBag)&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;프로퍼티 안에 placeholder 색상을 직접 지정할 수 있지만 rx를 더 적극적으로 사용해보고자 했을땐 위의 방법으로 구독형식을 사용해서 viewDidLoad안에서 사용해볼 수도 있을거 같다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-03-12 오후 5.34.04.png&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/whlrN/dJMcaadslkd/mmg3czh3Y7f88gSWkM9P7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/whlrN/dJMcaadslkd/mmg3czh3Y7f88gSWkM9P7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/whlrN/dJMcaadslkd/mmg3czh3Y7f88gSWkM9P7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwhlrN%2FdJMcaadslkd%2Fmmg3czh3Y7f88gSWkM9P7K%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;532&quot; height=&quot;173&quot; data-filename=&quot;스크린샷 2026-03-12 오후 5.34.04.png&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;306&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-03-12 오후 5.35.23.png&quot; data-origin-width=&quot;1572&quot; data-origin-height=&quot;538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zP8et/dJMcaiCwn1y/gUI42GTEBg2NHp1NIOfRXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zP8et/dJMcaiCwn1y/gUI42GTEBg2NHp1NIOfRXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zP8et/dJMcaiCwn1y/gUI42GTEBg2NHp1NIOfRXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzP8et%2FdJMcaiCwn1y%2FgUI42GTEBg2NHp1NIOfRXK%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;769&quot; height=&quot;263&quot; data-filename=&quot;스크린샷 2026-03-12 오후 5.35.23.png&quot; data-origin-width=&quot;1572&quot; data-origin-height=&quot;538&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;size18&quot;&gt;실제로 RxSwift 라이브러리를 다운받아서 코드를 살펴보았을때도 프로퍼티에 직접 값을 할당하는 방식이 아닌 스트림을 통해 상태를 주입하는 방식으로 테스트 코드를 작성하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;RxSwift를 사용해보고자 할 때 코드의 통일성을 위해서 뷰까지 이렇게 구현해볼 수도 있겠지만, 내부로직에서 가질 수 있는 특징은 view의 초기화 시점에서도 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;구독 형태&lt;/b&gt;&lt;/span&gt;는 viewDidLoad가 호출될 때 마다 &lt;b&gt;구독이 생성&lt;/b&gt;되고 Observable이 값을 방출할 때 마다 반응을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그리고 특정 시점에 deinit이 되는 방식이지만, &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;프로퍼티로 값에 접근&lt;/span&gt;&lt;/b&gt;하는 방식은 &lt;b&gt;viewDidLoad시점에 뷰에 딱 한번 로드되어 아무 변화가 일어나지 않기 때문에 생명주기 특성이 달라질 수 있다는 특징&lt;/b&gt;이 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;즉 값이 동적으로 자주 변할 때 구독을 사용하면 좋을거 같고, 고정값이 들어가야 한다 하면 직접 할당 방식이 자원을 해제 하는 관점에서 더 적은 비용이 들 수 있을거 같다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3️⃣ Subject&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;이벤트 전달과 처리를 둘 다 할 수 있음&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앞에서 subscribe와 bind는 &lt;b&gt;Observable 타입&lt;/b&gt;이기 때문에 항상 이벤트 전달만 할 수 있고 그에 대한 처리는 할 수 없었다. 하지만 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;subject는 이벤트 전달과 처리를 둘 다 할 수 있다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;subject타입으로 사용할 때는 다음 이벤트를 어떻게 수행해라고 할 때 onNext를 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Subject의 종류로는 아래와 같이 4가지의 종류가 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt; (그중에서 오늘은 제일 많이 사용하는 BehaviorSubject와 PublishSubject에 대해서 알아보고자 한다!)&lt;/span&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;BehaviorSubject : &lt;span style=&quot;color: #000000;&quot;&gt;생성될 때 초기값이 필수&lt;/span&gt;&lt;/b&gt;이기 때문에 subscribe 이전에 방출한 이벤트가 없다면 뷰를 미리 채워두기용으로 적절하다.&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PublishSubject: 초기값이 없는 빈 상태&lt;/b&gt;로 시작한다. subscribe 이후 시점에서 방출되는 이벤트를 처리할 수 있는 특성을 가졌기 때문에 구독 이전에 방출된 이벤트들은 적용되지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ReplaySubject&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AsyncSubject&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 style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;아래 코드로 살펴보자!&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771864748367&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private let name = BehaviorSubject(value: &quot;홍길동&quot;)

...
validationButton
    .rx
    .tap
    .bind(with: self) { owner, _ in
        let list = [&quot;홍길동&quot;, &quot;김철수&quot;, &quot;김영희&quot;]

        let random = list.randomElement()!
        // 다음 이벤트에 대한 처리를 onNext로 실행
        owner.name.on(.next(random))
        owner.name.onNext(random)

        owner.emailTextField.text = random
    }
    .disposed(by: disposeBag)&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;BehaviorSubject를 생성할 때는 초기값을 반드시 작성해줘야 한다. &lt;span style=&quot;font-size: 1.12em; 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;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;BehaviorSubject는 Subscribe 이전에 방출한 이벤트가 있다면 가장 최근에 전달된 이벤트 하나를 전달받을 수 있으며 subscribe 이전에 방출한 이벤트가 없다면 초기값을 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;보통 UIKit에서는 어떤 특정 값을 넣어줄 때 &lt;u&gt;owner.name = random&lt;/u&gt; 이런식으로 구성해볼 수도 있지만 rx에서는 등위연산자라는 개념이 따로 존재하지 않기 때문에 &lt;b&gt;onNext 이벤트를 통해서 값을 할당&lt;/b&gt;해줘야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;이렇게 하게 되면 테이블뷰나 컬렉션 뷰와 같이 데이터가 변경되었을때도 reloadData를 하지 않아도 자동으로 뷰가 업데이트 되기 때문에 데이터가 많이 변경되야 하는 상황에서는 적절하게 사용해볼 수 있을 거 같다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;Observable의 특성상 complete가 발생하면 무조건 dispose가 실행되는데 이때 observable과 observer 사이의 연결은 dispose로 인해 끊겼기 때문에 구독이 끊긴 것이므로 이후의 이벤트는 실행될 수 없다. 그래서 구독하기 전의 이벤트 또한 받지 못한다!&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1773305842581&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 상위뷰에 넘겨줄 신호
let buttonTap = PublishSubject&amp;lt;Void&amp;gt;()
...

 moreCommunityLabel.rx.tap
        .subscribe(onNext: { [weak self] in
            print(&quot;셀 내부: 버튼 눌림 신호 발생&quot;)
            self?.buttonTap.onNext(())
        })
        .disposed(by: disposeBag)&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;PublishSubject 같은 경우 초기값을 작성해주지 않아도 되기 때문에 개발할 때 보통 특정 버튼의 탭이 눌린다는 신호를 전달해야 하거나, 텍스트필드에 값이 들어왔다는 &lt;b&gt;신호를 보낼때&lt;/b&gt; 주로 사용해볼 수 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;subscribe 이후 시점부터 방출되는 이벤트를 처리할 수 있기 때문에 subscribe 이전에 방출된 이벤트들은 무시된다는 특성이 있다.&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; PublishSubject는 구독 이전의 값이 무시되는 이유?&lt;/b&gt;&lt;br /&gt;: 버퍼관점에서 살펴보자! PublishSubject는 초기값을 넣지 않아도 되고, BehaviorSubject는 초기값을 반드시 할당해야 사용할 수 있었다. PublishSubject는 버퍼 크기가 0이기 때문에 구독 이전의 이벤트를 무시하고 신호만 전달해주는 역할을 한다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;반면 BehaviorSubject는 버퍼 크기가 1을 가지기 때문에 초기값을 반드시 할당해줌으로써 버퍼를 미리 채워두고 있다! BehaviorSubject는 현재 상태값을 다루는게 목적이기 때문에 구독 시점에 마지막 값을 알아두고 있어야 한다. 그래서 구독시 마지막 이벤트를 전달할 수 있는 것!!&lt;/blockquote&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;4️⃣ Relay&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Subject를 내부에서 wrapping해서 replay를 제공해주게 된 형태&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Observable 타입에서 subscribe의 매개변수를 줄이기 위해 bind가 등장한 것처럼 &lt;b&gt;subject에서도 똑같은 개념으로 여러개의 메서드를 제외하고 onNext 메서드만 남긴게 Relay&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;Relay 또한 subject와 유사한 특성을 가지는 3가지가 존재한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PublishRelay&lt;/li&gt;
&lt;li&gt;BehaviorRelay&lt;/li&gt;
&lt;li&gt;ReplayRelay&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;subject와 마찬가지로 초기값이 있으면 BehaviorRelay, 초기값이 없다면 PublishRelay을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-03-12 오후 6.24.02.png&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwn5f8/dJMcacWB1M2/VqxW8VcaWi8p8oVvSedbb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwn5f8/dJMcacWB1M2/VqxW8VcaWi8p8oVvSedbb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwn5f8/dJMcacWB1M2/VqxW8VcaWi8p8oVvSedbb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbwn5f8%2FdJMcacWB1M2%2FVqxW8VcaWi8p8oVvSedbb0%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;631&quot; height=&quot;438&quot; data-filename=&quot;스크린샷 2026-03-12 오후 6.24.02.png&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;796&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;size18&quot;&gt;subject와 Relay의 가장 큰 차이점으로 &lt;b&gt;relay는 completed와 error에 대한 이벤트를 받지 못한다.&lt;/b&gt; 이 말은 즉 무한한 이벤트에 대한 처리방식과 일부 같으며 onNext에 대한 이벤트밖에 받지 못하기 때문에 disposed가 되기전까지는 subscribe가 해제되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;그래서 직접 disposed가 호출되는 deinit 시점에 처리를 해줘야 한다!&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Relay에서는 next 이벤트를 &lt;b&gt;accept&lt;/b&gt;이라는 키워드로 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1771865354053&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private let date = BehaviorRelay(value: Date())
...
birthDayPicker.rx.date
    .bind(with: self) { owner, date in
        print(date)
        owner.date.accept(date)
    }
    .disposed(by: disposeBag)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Rxswift에서 데이터를 취급하는 객체들이 어떤 이벤트를 방출할 수 있는지 아래 표를 통해 확인해볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-24 오전 1.49.50.png&quot; data-origin-width=&quot;942&quot; data-origin-height=&quot;366&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8NKzS/dJMcacWrdAG/XzqKmQZfGpkkNZxmL7pgKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8NKzS/dJMcacWrdAG/XzqKmQZfGpkkNZxmL7pgKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8NKzS/dJMcacWrdAG/XzqKmQZfGpkkNZxmL7pgKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8NKzS%2FdJMcacWrdAG%2FXzqKmQZfGpkkNZxmL7pgKk%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;505&quot; height=&quot;196&quot; data-filename=&quot;스크린샷 2026-02-24 오전 1.49.50.png&quot; data-origin-width=&quot;942&quot; data-origin-height=&quot;366&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5️⃣ Drive&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;메인 스레드에서의 실행을 보장&lt;/b&gt;해준다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;bind는 하나의 구독을 공유하는 것이 아니라 각각 구독을 하는 방식이였다. 즉 스트림이 공유되고 있지 않다는 것을 의미하고 &lt;b&gt;스트림이 공유&lt;/b&gt;될 수 있도록 drive를 통해서 수정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;네트워크 통신 같은 백그라운드에서 동작할 가능성이 있는 &lt;b&gt;Observable의 데이터 흐름을 메인스레드에서 동작할 수 있도록 변경해주는 역할이 Drive&lt;/b&gt;이다!&lt;/p&gt;
&lt;pre id=&quot;code_1773306676415&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;output.errorMessage
    .drive(with: self) { owner, message in
        guard !message.isEmpty else { return }

        owner.showAlert(title: &quot;Error&quot;, message: message.description) {
            print(&quot;[ClassItemDetailVC]: \(message.description)&quot;)
        }
    }
    .disposed(by: disposeBag)&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;나의 경우 뷰모델에서 errorMessage를 받아오면 뷰에 표시되도록 drive를 사용해서 alert을 띄워주는 방식을 구현했다.&lt;/p&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; bind vs drive&lt;/b&gt;&lt;br /&gt;: 가장 큰 차이점은 drive는 메인스레드에서 실행되는게 보장된다는 특성이 있지만 이외에도 스트림 관점에서 살펴보자.&lt;br /&gt;&lt;br /&gt;bind는 subscribe를 편하게 사용하기 위한 wrapper이기 때문에 하나의 observable에 여러개의 bind를 걸게 되면 구독할 때 마다 새로운 스트림이 생기게 된다&lt;br /&gt;&lt;br /&gt;반면 drive는 애초에 생성 목적이 여러개의 스트림을 공유하도록 만들어졌기 때문에 하나의 observable에 여러개의 drive를 걸어도 스트림이 한개만 생성된다&lt;br /&gt;&lt;br /&gt;따라서 drive로 바꾸는 과정을 거치게 되면 네트워크 통신이나 버튼 액션을 한번만 실행시키게 되기 때문에 효율적으로 개선해볼 수 있게 될 거 같다.&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;size18&quot;&gt;그래서 subject는 bind와 주로 많이 사용되며 relay는 driver와 자주 사용되곤 한다!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/ReactiveX/RxSwift&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/ReactiveX/RxSwift&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>iOS/UIKit</category>
      <category>bind</category>
      <category>drive</category>
      <category>Relay</category>
      <category>rxswift</category>
      <category>subject</category>
      <category>subscribe</category>
      <author>yanni13</author>
      <guid isPermaLink="true">https://yanni13.tistory.com/76</guid>
      <comments>https://yanni13.tistory.com/76#entry76comment</comments>
      <pubDate>Tue, 24 Feb 2026 01:52:08 +0900</pubDate>
    </item>
    <item>
      <title>[UIKit] View Drawing Cycle</title>
      <link>https://yanni13.tistory.com/75</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;오늘은 테이블뷰를 공부하던 중 imageView가 동그랗게 잘리지 않는 문제의 원인을 파악해 보기 위해&lt;/h4&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;View Drawing Cycle에 대해서 공부해보려고 한다.&lt;/h4&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #9d9d9d; text-align: start;&quot;&gt;(제가 공부하기 위해 작성한 것이므로 여러 개념들이 나올 수 있으며 틀린 내용이 있다면 알려주세요!)&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  문제원인&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1769354239481&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;self.profileImageView.layer.cornerRadius = self.profileImageView.frame.width / 2&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로 나눠서 원의 형태로 이미지 뷰가 만들어지게 구현을 했지만 막상 빌드해 보면 아래 사진처럼 뭔가 마름모 모양의 형태로 구현이 되었다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-26 오전 12.20.02.png&quot; data-origin-width=&quot;296&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sd9ps/dJMcacPsvzg/MeKIrjKmdrKxtpZdahcQQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sd9ps/dJMcacPsvzg/MeKIrjKmdrKxtpZdahcQQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sd9ps/dJMcacPsvzg/MeKIrjKmdrKxtpZdahcQQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsd9ps%2FdJMcacPsvzg%2FMeKIrjKmdrKxtpZdahcQQK%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;198&quot; height=&quot;256&quot; data-filename=&quot;스크린샷 2026-01-26 오전 12.20.02.png&quot; data-origin-width=&quot;296&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;근데 스크롤을 하다보면 또 정상적으로 원의 형태가 나오게 되어 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;실행 시점에 문제가 있는거 같아 DispatchQueue.main 안에 해당 코드를 넣어줬더니 아래와 같이 이미지뷰가 동그랗게 잘렸다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-26 오전 12.22.30.png&quot; data-origin-width=&quot;270&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8ALOQ/dJMcahb8xbC/oaPpyBTZtMoFeaB18k8J61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8ALOQ/dJMcahb8xbC/oaPpyBTZtMoFeaB18k8J61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8ALOQ/dJMcahb8xbC/oaPpyBTZtMoFeaB18k8J61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8ALOQ%2FdJMcahb8xbC%2FoaPpyBTZtMoFeaB18k8J61%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;174&quot; height=&quot;253&quot; data-filename=&quot;스크린샷 2026-01-26 오전 12.22.30.png&quot; data-origin-width=&quot;270&quot; data-origin-height=&quot;392&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;size18&quot;&gt;그래서 View에서 프레임이 어느 시점에 결정되는지 고민해 보고자 view Drawing Cycle에 대한 개념을 학습하게 되었다.&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✏️ View Drawing Cycle&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;뷰가 load되거나 reload 되면서 화면에 그려질 때 생기는 사이클을 의미&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;UIKit에서는 뷰를 그릴 때 아래와 같은 순서로 실행된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;constraint&lt;/b&gt; - 제약 조건 잡고&lt;/li&gt;
&lt;li&gt;&lt;b&gt;layout&lt;/b&gt; - 레이아웃을 잡고&lt;/li&gt;
&lt;li&gt;&lt;b&gt;draw&lt;/b&gt; - 뷰를 화면에 그린다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;스토리보드에서 오토레이아웃을 잡을 때를 생각해 보면 constraints를 먼저 결정하면 그 안에 frame 크기가 내부적으로 결정되고 그다음에 뷰가 화면에 그려진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이때 초기화 시점에서는 전체 사이클이 한번 실행되지만 다음 시점에서부터는 뷰가 그려질 때 이 사이클대로 전부 실행되는게 아니라 온디맨드 방식으로 사용자가 필요할 때 필요한 시점에만 실행하게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #333333; text-align: left;&quot;&gt;만약 온디맨드 방식이 아니라 매번 이 사이클을 돌아야 하는 구조라면 ui가 변경되거나 다른 뷰를 왔다갔다 할 때마다 constraint부터 다시 계산하느라 엄청 성능이 떨어지고 비효율적일 것이다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt; &lt;b&gt;온디맨드 방식이란?&lt;/b&gt;&lt;br /&gt;: 필요한 시점에 요청이 있을때만 실행되도록 하여 불필요한 계산을 줄이고 성능을 최적화하도록 설계된 방식이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;코드로 살펴보자!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;각 단계에서 어떤 메서드가 호출되는지 확인해보고자 간단한 코드를 작성해 보았다.&lt;/p&gt;
&lt;pre id=&quot;code_1769355516916&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class TopView: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        print(&quot;TopView - \(#function)&quot;)
    }
    
    required init?(coder: NSCoder) {
        fatalError(&quot;init(coder:) has not been implemented&quot;)
    }
    
    // 1. Constraints 변경 단계
    override func updateConstraints() {
        super.updateConstraints()
        print(&quot;TopView - \(#function)&quot;)

    }
    override func invalidateIntrinsicContentSize() {
        super.invalidateIntrinsicContentSize()
    }
    // 2. layout 단계
    override func updateProperties() {
        super.updateProperties()
        print(&quot;TopView - \(#function)&quot;)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        self.backgroundColor = .lightGray
        self.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
        print(&quot;TopView - \(#function)&quot;)

    }
    
    // 3. draw 단계
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        print(&quot;TopView - \(#function) rect: \(rect)&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;스크린샷 2026-01-26 오전 12.39.10.png&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;770&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mgCyC/dJMcaiPCadf/g2SYUVX2zTC6blLIEUbqYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mgCyC/dJMcaiPCadf/g2SYUVX2zTC6blLIEUbqYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mgCyC/dJMcaiPCadf/g2SYUVX2zTC6blLIEUbqYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmgCyC%2FdJMcaiPCadf%2Fg2SYUVX2zTC6blLIEUbqYk%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;449&quot; height=&quot;409&quot; data-filename=&quot;스크린샷 2026-01-26 오전 12.39.10.png&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;770&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;size18&quot;&gt;view drawing cycle에서 실행되는 주요 메서드는 5가지 종류가 있다.&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;updateConstraints&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;invalidateIntrinsicContentSize&lt;/li&gt;
&lt;li&gt;updateProperties&lt;/li&gt;
&lt;li&gt;&lt;b&gt;layoutSubviews&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;draw&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-26 오후 9.34.49.png&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;376&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/W1s4G/dJMcaiIRvlo/OgTD1mRFSlT7ZOWOHimHEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/W1s4G/dJMcaiIRvlo/OgTD1mRFSlT7ZOWOHimHEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/W1s4G/dJMcaiIRvlo/OgTD1mRFSlT7ZOWOHimHEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FW1s4G%2FdJMcaiIRvlo%2FOgTD1mRFSlT7ZOWOHimHEK%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;519&quot; height=&quot;161&quot; data-filename=&quot;스크린샷 2026-01-26 오후 9.34.49.png&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;376&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;원래 4가지 정도였지만, iOS 26.0 버전부터 updateProperties가 추가되었다. 이 updateProperties는 view drawing cycle의 일부는 아니며 layout ~ draw 이전의 상태를 정리하기 위한 독립적인 프로퍼티 업데이트 단계로 추가되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;여기서 단계별로 크게 살펴보면 제약 조건이 변경되었을 때 실행되는 &lt;b&gt;updateConstraints&lt;/b&gt;가 있고, intrinsic size를 기반으로 프레임이 변경되거나 제약조건에 대한 재계산 결과등 레이아웃이 변경됐을 때 실행되는 &lt;b&gt;layoutSubviews&lt;/b&gt;, 뷰가 화면에 그려지는 과정으로 시각적인 결과가 변경될 때 &lt;b&gt;draw&lt;/b&gt;메서드가 주로 사용되며 호출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아주 간단한 코드 구현으로 버튼을 눌렀을 때 간단한 title 정도와 색상만 변경되도록 구성하였고 이 코드를 통해서 어느 시점에 실행되는지 파악해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;simulator_screenshot_F2A1B27D-68B9-4867-9BA9-6950479457F4.png&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;2622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpMglT/dJMcaiIQ4nw/1QbA75DnT3XM7YmqVk0re0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpMglT/dJMcaiIQ4nw/1QbA75DnT3XM7YmqVk0re0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpMglT/dJMcaiIQ4nw/1QbA75DnT3XM7YmqVk0re0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpMglT%2FdJMcaiIQ4nw%2F1QbA75DnT3XM7YmqVk0re0%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;316&quot; height=&quot;687&quot; data-filename=&quot;simulator_screenshot_F2A1B27D-68B9-4867-9BA9-6950479457F4.png&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;2622&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;uikit은 뷰가 그려질 때 constraint -&amp;gt; layout -&amp;gt; draw 순으로 실행하기 때문에 초기화 시점에서 setNeedsUpdateConstraints, setNeedsLayout, setNeedsDisplay와 같은 메서드로 다음 사이클에서 실행을 해달라고 &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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-26 오후 8.57.03.png&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NcxtN/dJMcaac2gY1/AZMEOftxyX6EZTH9ISXRb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NcxtN/dJMcaac2gY1/AZMEOftxyX6EZTH9ISXRb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NcxtN/dJMcaac2gY1/AZMEOftxyX6EZTH9ISXRb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNcxtN%2FdJMcaac2gY1%2FAZMEOftxyX6EZTH9ISXRb1%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;477&quot; height=&quot;150&quot; data-filename=&quot;스크린샷 2026-01-26 오후 8.57.03.png&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;226&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;실제로 ViewController에서 제약 조건을 설정하고 constraint 변경 버튼을 눌렀을 때 &lt;b&gt;topView.setNeedsUpdateConstraints()&lt;/b&gt; 메서드를 통해 constraint 계산이 필요할 수 있음을 알려줌으로써 updateConstraints가 실행되고 있는 걸 로그에서 확인해 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;반면 frame과 bounds에 대한 변경은 이루어지지 않았으므로 layout 단계들은 실행되지 않는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이게 바로 온디맨드 구조로 인해 변경된 사항에 한해서 적절한 메서드를 호출하게 되는 것이다.&lt;/p&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;여기서 다음 cycle에서 실행해 달라고 예약하는 방식으로 동작되는 메서드가 있다면 즉시 실행해달라고 하는 메서드들도 존재하지 않을까? 에 대한 궁금증이 생기게 되었다.&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;&lt;b&gt;다음 순서에 실행되도록 예약을 하지 않고 바로 실행해달라고 요청하려면 어떻게 해야 할까?&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그때 사용하는 게 &lt;b&gt;layoutIfNeeded()&lt;/b&gt; 메서드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1769356170283&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@IBAction func clickedFrameButton(_ sender: UIButton) {
        
        print(#function, &quot;layout 강제 업데이트&quot;)
        frameButton.setTitle(&quot;라벨ㅇㅇ 변경&quot;, for: .normal)

        UIView.animate(withDuration: 1.5) {
            self.view.layoutIfNeeded()
        }
    }&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;layoutIfNeeded()를 호출하면 다음 차례로 대기를 미루지 말고 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;즉시 실행해 달라고 요청&lt;/b&gt;&lt;/span&gt;한다. &lt;br /&gt;보통의 경우 다음 사이클에 실행되어도 문제가 없지만, 변화되는 과정이 보여야 하는 애니메이션이나, 계산 로직과 같은 변경 과정이 사용자에게 보여야 하는 경우에는 다음 사이클을 기다리게 되면 눈에 띄지 않게 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그 차이를 애니메이션을 통해 확인해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp; &amp;zwj;♀️ 즉시 호출 (layoutIfNeeded 사용)&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Simulator Screen Recording - iPhone 17 - 2026-01-26 at 00.51.53.gif&quot; data-origin-width=&quot;295&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5scew/dJMcaiIQ4vY/WMk90mDk4KoAalVvS0kqok/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5scew/dJMcaiIQ4vY/WMk90mDk4KoAalVvS0kqok/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5scew/dJMcaiIQ4vY/WMk90mDk4KoAalVvS0kqok/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/5scew/dJMcaiIQ4vY/WMk90mDk4KoAalVvS0kqok/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;295&quot; height=&quot;640&quot; data-filename=&quot;Simulator Screen Recording - iPhone 17 - 2026-01-26 at 00.51.53.gif&quot; data-origin-width=&quot;295&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이렇게 약간의 애니메이션과 함께 label이 변경되는 걸 확인할 수 있다.&lt;/p&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt; &amp;zwj;♀️ 즉시 호출 x (layoutIfNeeded 미사용)&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Simulator Screen Recording - iPhone 17 - 2026-01-26 at 00.53.48.gif&quot; data-origin-width=&quot;295&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2YCSD/dJMcaaYoMpj/al32xrzIXOeQRmTskt53g1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2YCSD/dJMcaaYoMpj/al32xrzIXOeQRmTskt53g1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2YCSD/dJMcaaYoMpj/al32xrzIXOeQRmTskt53g1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/b2YCSD/dJMcaaYoMpj/al32xrzIXOeQRmTskt53g1/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;295&quot; height=&quot;640&quot; data-filename=&quot;Simulator Screen Recording - iPhone 17 - 2026-01-26 at 00.53.48.gif&quot; data-origin-width=&quot;295&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;똑같이 라벨이 변경되긴 하지만 애니메이션 적용 없이 라벨만 갈아 끼듯 변경된다는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;그럼 한 단계의 사이클로 구성된다 했으니까 제약 조건이 추가되거나 제거되었을 경우 updateConstraints부터 draw 메서드까지 전부 실행되는 걸까?&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;그렇지 않다!&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;제약조건이 추가되거나 제거되어도 layout단계에서 frame이 결정되는 영역에서 변화가 존재하지 않으면 Constraints 단계만 실행된다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;예를 들어 라벨을 변경했다고 가정했을 때 라벨이 변하는 지점은 layout이 변하는 것이기 때문에 제약조건에 영향을 미치지 않아 constraint 관련 메서드는 실행되지 않는다. 여기까지는 아까 설명한 온디맨드 구조를 이해했다면 이해가 될 것이다!&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;주의할 점은&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;draw단계에서는 CoreAnimation에서 관리되기 때문에 항상 이벤트가 끝난 뒤 한 번에 렌더링 하도록&lt;/b&gt;&lt;/span&gt; 설계되어 있어서 다음 drawing cycle에서 실행된다. 즉 draw 단계는 즉시 실행되지 않게 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문제 해결&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그럼 다시 처음 문제로 돌아가서 imageView를 동그랗게 자르고 싶을 때 dispatchqueue를 사용해야 하는 이유가 무엇인지에 대해 살펴보자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;처음에는 frame의 크기가 layoutSubViews에서 결정되니까 layout부터 draw까지 순차적으로 실행될 건데, dispatchqueue를 적용해주지 않아도 순차적으로 실행되는 게 아닐까?라고 생각했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 애초에 오류가 났던 이유는 frame의 크기가 결정되지 않은 시점에서 접근했기 때문에 적용이 되지 않은 것이었다. 스크롤 하면 할수록 정상적으로 보였던 것은 테이블뷰 셀이 재사용되면서 layout이 다시 수행되면서 frame의 크기가 결정되었기 때문의 원의 형태가 유지되었던 것이였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;DispatchQueue를 사용해서 시점을 미루는 것도 임시 해결방편이었고 결론적으로는 layoutSubViews에서 이미지 크기를 자르는 코드를 실행하는 방법으로도 해결해 볼 수 있다는 것을 알게 되었다.&lt;/p&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>iOS/UIKit</category>
      <category>View Drawing Cycle</category>
      <author>yanni13</author>
      <guid isPermaLink="true">https://yanni13.tistory.com/75</guid>
      <comments>https://yanni13.tistory.com/75#entry75comment</comments>
      <pubDate>Mon, 26 Jan 2026 22:02:11 +0900</pubDate>
    </item>
    <item>
      <title>[UIKit] awakeFromNib vs prepareForReuse</title>
      <link>https://yanni13.tistory.com/74</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;TableView에서 커스텀 셀을 다룰 때 awakeFromNib, init, prepareForReuse의 역할이 비슷해서 언제 써야 하는지 헷갈릴 때가 많았다. 따라서 오늘은 해당 개념들을 다시 정리하고 왜 이런 구조가 필요한지 분석해보고자 한다.&lt;/p&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: #9d9d9d; text-align: start;&quot;&gt;(제가 공부하기 위해 작성한 것이므로 여러 개념들이 나올 수 있으며 틀린 내용이 있다면 알려주세요!)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✏️ 재사용 메커니즘&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;TableView에서 1000개의 데이터를 보여주는 리스트가 있다고 가정했을 때 이 1000개의 셀이 모두 메모리에 생성되는 방식일까?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Simulator Screenshot - iPhone 17 Pro - 2026-01-16 at 01.44.58.png&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;2622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0faUD/dJMcaf6pqAT/SaTPJl4nhDrOWohMNHAqnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0faUD/dJMcaf6pqAT/SaTPJl4nhDrOWohMNHAqnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0faUD/dJMcaf6pqAT/SaTPJl4nhDrOWohMNHAqnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0faUD%2FdJMcaf6pqAT%2FSaTPJl4nhDrOWohMNHAqnK%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;355&quot; height=&quot;772&quot; data-filename=&quot;Simulator Screenshot - iPhone 17 Pro - 2026-01-16 at 01.44.58.png&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;2622&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;디자인에 따라 다르겠지만 아이폰 설정 창의 셀 크기를 참고해보면 화면에 보이는 셀은 10개 정도의 셀인데, 나머지 셀을 미리 만들어두는 건 메모리 낭비를 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;심지어 몇만개의 셀이 존재할 수도 있는데 스크롤할 때마다 새로운 셀을 생성하게 된다면 성능 저하로 인해 앱이 강제종료될 가능성도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;따라서 UIKit은 &lt;b&gt;화면에서 나타나지 않는 셀을 버리는게 아니라 재사용 큐에 보관했다가 이 셀들을 컨베이어 벨트처럼 재사용하는 구조&lt;/b&gt;로 활용하고 있다. 이게&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; 재사용 메커니즘&lt;/b&gt;&lt;/span&gt;이다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-16 오전 1.54.40.png&quot; data-origin-width=&quot;868&quot; data-origin-height=&quot;370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Zy9zq/dJMcaa46Bh9/swff4MXenXgwUnjEnRrlfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Zy9zq/dJMcaa46Bh9/swff4MXenXgwUnjEnRrlfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Zy9zq/dJMcaa46Bh9/swff4MXenXgwUnjEnRrlfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZy9zq%2FdJMcaa46Bh9%2Fswff4MXenXgwUnjEnRrlfK%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;827&quot; height=&quot;353&quot; data-filename=&quot;스크린샷 2026-01-16 오전 1.54.40.png&quot; data-origin-width=&quot;868&quot; data-origin-height=&quot;370&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;size18&quot;&gt;전체 흐름을 보자면 최초로 화면에 셀이 8개 정도 표시되고 있을 때&amp;nbsp;사용자가 스크롤하면 최초 셀은 위로 넘어간다. 이때 없어지는 게 아니라&lt;b&gt; 재사용 큐에 보관&lt;/b&gt;이 되어 있고 다시 화면 아래에서 해당 큐에 보관이 되어 있던 셀들이 데이터만 교체한 상태로 다시 등장하게 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이때 화면에 8~9개의 셀이 표시되더라도 사용자가 스크롤 했을 경우를 대비해 재사용 가능한 셀이 화면에 표시되는 셀 9개 + a로 추가적으로 셀 몇 개가 더 생성된다. 해당 포스팅에서는 13개의 셀이 재사용된다고 가정해 보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그러면 1000개의 셀을 만들지 않아도 13개 정도의 셀만으로도 무한스크롤이 아무런 성능 저하 없이도 가능하게 된다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;우선 스토리보드 기반으로 테이블뷰에 재사용 가능한 셀을 만들려면 XIB 파일을 선택해야 한다.&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;b&gt;1️⃣ XIB란?&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;&amp;nbsp;XML Interface Builder의 약자이며 Xcode에서 제공하는 XML 기반의 유저 인터페이스 파일&lt;/b&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-15 오전 12.04.24.png&quot; data-origin-width=&quot;1420&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M9Sum/dJMcabXeS7L/tapOhIa9NfxIeqSLI4hRO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M9Sum/dJMcabXeS7L/tapOhIa9NfxIeqSLI4hRO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M9Sum/dJMcabXeS7L/tapOhIa9NfxIeqSLI4hRO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM9Sum%2FdJMcabXeS7L%2FtapOhIa9NfxIeqSLI4hRO1%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;514&quot; height=&quot;253&quot; data-filename=&quot;스크린샷 2026-01-15 오전 12.04.24.png&quot; data-origin-width=&quot;1420&quot; data-origin-height=&quot;700&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-15 오전 12.05.14.png&quot; data-origin-width=&quot;2470&quot; data-origin-height=&quot;1104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ff0Ae/dJMcaiaZExU/nRTRkdgEyOHxwVG2AxaEWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ff0Ae/dJMcaiaZExU/nRTRkdgEyOHxwVG2AxaEWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ff0Ae/dJMcaiaZExU/nRTRkdgEyOHxwVG2AxaEWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFf0Ae%2FdJMcaiaZExU%2FnRTRkdgEyOHxwVG2AxaEWk%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;770&quot; height=&quot;344&quot; data-filename=&quot;스크린샷 2026-01-15 오전 12.05.14.png&quot; data-origin-width=&quot;2470&quot; data-origin-height=&quot;1104&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;size18&quot;&gt;커스텀 셀을 만들고자 할 때 XIB 파일을 선택하면 awakeFromNib이라는 메서드가 자동으로 코드에 추가된 형태로 파일이 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;분명 XIB 파일이 실행되어야 하는데 왜 awakeFromXib가 아니라 awakeFromNib 일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  XIB와 NIB의 차이&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;NIB과 XIB의 차이점에 대해서 먼저 이해를 해보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;XIB는 앞서 말했듯이 xml interface Builder의 축약어이며 NIB은 Next Interface Builder의 약자이다. 둘 다 모두 유저 인터페이스를 정의하기 위한 파일이지만 저장되는 방식이 조금 다르다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;XIB&lt;/b&gt;는 개발자가 직접 생성하고 편집할 수 있는 &lt;b&gt;XML 기반 설계 파일&lt;/b&gt;이지만 &lt;b&gt;NIB은 바이너리 형태로 저장&lt;/b&gt;되는 파일이다. 따라서 Nib파일은 사람이 직접 수정할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;컴퓨터는 이진수를 이해하기 때문에 이 XML기반 형태의 파일을 직접 사용하기엔 효율적이지 않다. 그래서 XIB 파일이 빌드되거나 앱을 배포할 때는 바이너리 형태인 Nib 파일로 변환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;즉 iOS 앱은 데이터를 가져올 때 Xib를 직접 로딩하는 게 아니라 nib 파일을 로딩하기 때문에 메서드 이름도 awakeFromNib이다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1768404383390&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    
    // SearchTableViewCell
    override func awakeFromNib() {
        super.awakeFromNib()
    }
    
    ...
    
    // -TableViewController        
    override func viewDidLoad() {
        super.viewDidLoad()
        // 테이블 뷰 컨트롤러에 셀이 없고, XIB로 셀 구성된 걸 가져올 때 필요한 코드
        tableView.register(UINib(nibName: &quot;SearchTableViewCell&quot;, bundle: nil), forCellReuseIdentifier: &quot;SearchTableViewCell&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;size18&quot;&gt;해당 코드를 실제 테이블뷰에서 어떻게 사용되는지 확인해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;XIB로 셀을 구성할 때는 테이블 뷰에서 해당 셀을 등록하는 과정이 필요하다. 이때 UINib 타입의 코드를 사용해서 XIB 파일이 빌드 시점에 NIB 파일로 변환되고 테이블 뷰에서 객체를 가져올 때 nib 파일에 있는 identifier를 가져오겠다고 선언한다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그럼 awakeFromNib 메서드가 어떻게 동작하고 어떤 역할을 하는걸까?&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;b&gt;2️⃣ awakeFromNib&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;nib파일을 불러올 때 최초 한번 호출되는 초기 설정 단계&lt;/b&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1768403175638&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func prepareForReuse() {
        
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;awakeFromNib은 NIB파일로 저장된 정보들을 읽어서 실제 객체들을 메모리에 생성하고 서로 연결할 때 호출되는 메서드이며 내부적으로 init(coder: )가 호출되고 내부 속성이 초기화된다. 이후에 awakeFromNib이 실행되어 nib 파일 내의 다른 객체들과 모든 연결이 outlet으로 설정되었다는 것을 보장하면서 셀 초기화 작업을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt; &amp;nbsp; awakeFromNib도 결국 셀이 생성되기 전에 초기화를 시켜주는 역할이면 init이랑 비슷한데 왜 init을 사용하지 않고 awakeFromNib을 사용할까?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그건 &lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;둘 다 초기화된다는 점은 같지만&amp;nbsp;&lt;/span&gt;&lt;b&gt;실행 시점이 다르기 때문이다.&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #333333; text-align: left;&quot;&gt;init은 메모리에 객체를 생성해서 객체가 존재할 수 있는 최소 조건을 만드는데 awakeFromNib은 nib을 불러오고 IBOutlet 연결이 된 상태에서 마무리 단계로 생성되기 때문에 &lt;b&gt;객체가 처음 생성될 때 딱 한 번만 호출&lt;/b&gt;되고 스크롤로 셀이 재사용될 때는 재사용 메커니즘에 따라 awakeFromNib이 더 이상 호출되지 않는다. 위의 예제로 보면 13개의 셀에서만 awakeFromNib이 실행된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt; 그럼&amp;nbsp;코드베이스로&amp;nbsp;셀을&amp;nbsp;작성할&amp;nbsp;때는&amp;nbsp;Xib파일을&amp;nbsp;사용하지&amp;nbsp;않는데&amp;nbsp;awakeFromNib이&amp;nbsp;필요할까?&lt;/b&gt;&lt;br /&gt;:&amp;nbsp;정답은&amp;nbsp;아니다!&lt;br /&gt;xib파일이 빌드 시점에 nib파일로 변환된다고 했는데 awakeFromNib은 바이너리 형태인 nib파일에 저장된 걸 꺼내보는 시점에서 생성된다. 하지만 코드베이스 형태에선 XIB와 NIB이 존재하지 않게 되기 때문에 awakeFromNib은 호출되지도 않고 필요하지도 않다. &lt;br /&gt;&lt;br /&gt;따라서 이런 코드베이스 UI에서는 객체 설정과 UI 설정 모두&lt;b&gt; init 단계에서 처리&lt;/b&gt;한다.&lt;/blockquote&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;b&gt;3️⃣ prepareForReuse&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;셀이 재사용되기 전에 반복 호출되는 초기화 단계&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앞서 설명했듯이 awakeFromNib은 셀이 화면에서 보이는 만큼 실행되고 특정 시점에서는 더 이상 실행되지 않는다.&amp;nbsp;13개의 셀에서만 awakeFromNib이 실행된다면 이미지 다운로드처럼 비동기 작업이 필요한 시점에서는 셀 데이터가 꼬이지 않도록 어떻게 제어할 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그때 사용하는 게 &lt;b&gt;prepareForReuse&lt;/b&gt;이다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1768406185775&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;override func prepareForReuse() {
    self.prepareForReuse()
    
    // 셀 초기화
    titleLabel.text = nil
    imageView.image = 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;size18&quot;&gt;셀이 100개가 있든 1000개가 있든 계속해서 셀이 재사용될 때마다 항상 prepareForReuse가 호출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 보통 초기화 코드를 prepareForReuse에 작성하여 셀이 재사용되기 전에 이전 데이터를 정리하고 깔끔한 상태로 재사용될 수 있도록 구성이 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  Configure 과의 차이점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테이블 뷰나 컬렉션 뷰에 대해서 공부를 많이 해보았다면 Configure과의 차이점이 궁금할 수 있다. Configure는 개발자들이 셀에 데이터를 주입하고 UI를 정의하기 위한 메서드로 주로 활용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1768497816922&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    // CityCollectionViewCell
    func configure(_ row: City) {
        cityNameLabel.text = row.city_name
        subNameLabel.text = row.city_explain
        cityImage.setImage(url: row.city_image)
    }
    ...
    
    // CityViewController
        func collectionView(
        _ collectionView: UICollectionView,
        cellForItemAt indexPath: IndexPath
    ) -&amp;gt; UICollectionViewCell {
        let cell =
            collectionView.dequeueReusableCell(
                withReuseIdentifier: CityCollectionViewCell.identifier,
                for: indexPath
            ) as! CityCollectionViewCell

        cell.backgroundColor = .white
        let row = filterInfoCity[indexPath.item]
        cell.configure(row)

        return cell
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;코드를 통해서 살펴보자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;configure를 사용하는 이유는 결국 외부 데이터를 셀 안에 전달해주고자 할 때 사용된다. 따라서 ViewController 안에 셀을 지정해주는 코드에 cell.configure() 를 통해서 데이터를 주입할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;물론 cellForRowAt 메서드에서 UI 속성을 다 지정하고 데이터를 넘겨줄 수 있지만 Configure를 통해 코드를 분리함으로써 CityViewController는 데이터를 선택만 하고 안에 내용이 어떻게 보이는지에 대해서는 모르기 때문에 조금 더 역할과 책임에 대해 나눠지게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 어떻게 보면 configure도 결국 셀이 세팅되기 전에 UI를 그리고 데이터를 표시해 주는 함수고, prepareForReuse도 셀이 재사용되기 전에 초기화를 해주는 메서드이기 때문에 역할이 혼동되기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;configure&lt;/b&gt;는 결국 cellForRowAt에서 데이터를 주입받아서 표시해 주기 때문에 보통 &lt;b&gt;셀을 완성하는 단계에서 실행&lt;/b&gt;된다. 반면 prepareForReuse는 셀이 재사용될 때 다음 재사용될 때를 대비해서 셀을 초기화해주는 역할이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;따라서 셀이 재사용될 때 prepareForReuse로 초기화 된 셀을 configure가 덮어씌우는 구조! 라고 생각하면 이해하기 쉬울 거 같다.&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;br /&gt;&lt;b&gt;항상 prepareForReuse가 실행되고 configure가 실행되는 것은 아니다!&lt;/b&gt; (실행시점이 고정된 게 아님!)&lt;br /&gt;&lt;br /&gt;처음에 빌드를 하고 나서는 셀이 스크롤되지 않은 상태이기 때문에 prepareForReuse가 호출되지 않고 configure가 호출된다. 그러다가 사용자가 스크롤을 하면 셀이 재사용 큐에 들어가게 되는데 이 시점부터는 prepareForReuse가 실행되고 나서 configure가 실행되게 된다!&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;</description>
      <category>iOS/UIKit</category>
      <category>awakeFromNib</category>
      <category>prepareForReuse</category>
      <category>tableView</category>
      <category>xib</category>
      <author>yanni13</author>
      <guid isPermaLink="true">https://yanni13.tistory.com/74</guid>
      <comments>https://yanni13.tistory.com/74#entry74comment</comments>
      <pubDate>Fri, 16 Jan 2026 02:48:29 +0900</pubDate>
    </item>
    <item>
      <title>[swift] Swift Concurrency - nonisolated</title>
      <link>https://yanni13.tistory.com/73</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;라이브러리를 배포하기 위해 기존 앱 프로젝트의 코드를 Swift Package로 옮긴 뒤 빌드했을 때 컴파일 에러가 발생하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-13 오후 10.23.49.png&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;546&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cB7JUX/dJMcahXgOwd/wfyhAAL9EGb9VaQUgcrmZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cB7JUX/dJMcahXgOwd/wfyhAAL9EGb9VaQUgcrmZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cB7JUX/dJMcahXgOwd/wfyhAAL9EGb9VaQUgcrmZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcB7JUX%2FdJMcahXgOwd%2FwfyhAAL9EGb9VaQUgcrmZk%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;714&quot; height=&quot;259&quot; data-filename=&quot;스크린샷 2025-12-13 오후 10.23.49.png&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;546&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;size18&quot;&gt;컴파일러가 두 가지 제안을 줬는데 MainActor를 호출하던가 nonisolated 키워드를 사용하라고 제시했다. 하지만 AVFoundation과 같이 카메라나 미디어 처리는 주로 백그라운드에서 작업되는 것이라고 알고 있는데 MainActor를 호출하는 게 맞는 건가? 싶다가 &lt;b&gt;nonisolated&lt;/b&gt;를 사용해보고자 하였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;따라서 오늘은 swift Concurrency의 일부인 &lt;b&gt;nonisolated&lt;/b&gt; 키워드에 대해서 알아보겠다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;nonisolated와 nonisolated(unsafe)는 어떤 점이 다른지, 어떤 원리로 실행되는지 궁금해서 공부해보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;(제가 공부하기 위해 작성한 것이므로 여러 개념들이 나올 수 있으며 트러블 슈팅에 관한 이야기가 포함되어 있습니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-end=&quot;3043&quot; data-start=&quot;3040&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;⚔️ 문제의 원인을 찾아서..&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱 타깃에서는 빌드도 잘되고 시뮬레이터로 실행도 잘되던 코드를 Swift Package로 그대로 옮겼는데 컴파일 에러가 발생한 이유에 대해서 본질적인 문제의 원인이 궁금해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-15 오전 12.57.44.png&quot; data-origin-width=&quot;1178&quot; data-origin-height=&quot;1040&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b16Num/dJMcagcZCaG/Nd5iKkkpr15t4ahkNOxMiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b16Num/dJMcagcZCaG/Nd5iKkkpr15t4ahkNOxMiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b16Num/dJMcagcZCaG/Nd5iKkkpr15t4ahkNOxMiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb16Num%2FdJMcagcZCaG%2FNd5iKkkpr15t4ahkNOxMiK%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;509&quot; height=&quot;449&quot; data-filename=&quot;스크린샷 2025-12-15 오전 12.57.44.png&quot; data-origin-width=&quot;1178&quot; data-origin-height=&quot;1040&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;size18&quot;&gt;코드 내부를 살펴보면 UIKit의 AppDelegate와 SwiftUI의 App 프로토콜안에서 @MainActor가 이미 내부적으로 호출되고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 암묵적으로 @MainActor나 동시성 로직을 따로 관리해주지 않아도 허용되는 범위가 넓지만, 패키지로 분리하는 순간 어떤 스레드에서 호출될지 모르기 때문에 호출 스레드를 통제할 수 없어 동시성 규칙이 더 엄격하게 적용되고 오류가 난 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;제일 처음 스크린샷을 보면 알겠지만 컴파일러 딴에서 오류가 난 이유는 &lt;b&gt;data race&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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✏️ Data Race&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;여러 쓰레드가 동일한 메모리 영역에 접근하면서 읽기/쓰기 순서가 보장되지 않아 데이터의 값이 변하는 상황&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;동기화에서는 여러 쓰레드나 프로세스가 공유자원에 동시에 접근하려고 할 때 Date Race가 발생하게 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(학부생 때 컴퓨터 구조를 공부하던 때가 떠오른다  )&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;Swift Concurrency 관점에서 Data Race를 보면 서로 다른 task나 actor가 동일한 변경가능한 상태(mutable state)에 접근할 때 그 접근 순서를 컴파일러가 증명할 수 없다면 컴파일 에러가 나게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1765863313630&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var result = 0

func A() {
    result += 1
}

Task {
    A()
}

Task {
    A()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;위코드를 보면 result는 전역변수로 선언되어 있기 때문에 언제든지 변경 가능한 mutable 상태를 지닌다. 각각 독립적인 Task내부에서 A함수를 호출하고 있는데 접근 순서가 보장되지 않기 때문에 해당 코드는 swift 딴에서 해당 코드의 안전함을 증명할 수 없어 컴파일 에러가 발생한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;이 문제를 해결하기 위해 swift에서 도입한 개념이 &lt;b&gt;Actor&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✏️ Actor&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;자신이 소유한 값이 바뀔 수 있는 mutable state에 대해 동시에 접근하지 못하도록 보장하는 것&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1;&quot; href=&quot;https://developer.apple.com/documentation/swift/actor&quot;&gt;공식문서&lt;/a&gt;&lt;/b&gt;에 따르면 actor는 기본적으로 serial executor를 사용하여 작업을 순차적으로 처리하는데 필요시 특정 serial executor를 사용하도록 설정할 수도 있고 task executor를 사용하여 기본 task와 actor의 스케줄링에도 영향을 줄 수 있다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 말은 Actor는&lt;b&gt;&amp;nbsp;serial executor로 인해 순차 실행되면서 data race가 발생하지 않도록 접근 순서를 보장&lt;/b&gt;한다는 것이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앞서 설명했던 코드를 다시 가져와 actor를 적용해 보면 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1765863577631&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;actor ResultActor {
    private var result = 0
    
    func A() {
        result += 1
    }
}

let resultActor = ResultActor()

Task { 
    await resultActor.A()
}

Task {
    await resultActor.A()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;actor안에 result 변수와 함수 A가 포함되어 있는데 &lt;b&gt;actor-isolated 때문에 actor 밖에서는 result와 함수 A를 직접 호출할 수가 없게 된다.&lt;/b&gt; actor 외부에서 result나 A함수를 호출하기 위해서는 &lt;b&gt;await 키워드&lt;/b&gt;를 사용해서 이 작업은 ResultActor 큐에 맡길 테니 기다렸다가 실행해 줘~라고 표시함으로써 data race가 발생하지 않고 result의 값이 안전하게 수정될 수 있는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;b&gt; actor 격리 규칙(actor-isolated)&lt;/b&gt;이란?&lt;br /&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;nbsp;actor가 가진 데이터를 동시에 여러 곳에서 수정하지 못하게 항상 actor 안에서 한 번에 하나씩만 접근하도록 하는 규칙이다.&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;실행 흐름 관점에서 보면 actor의 executor는 선입선출(FIFO)을 보장하고 한 시점의 하나의 actor-isolated 함수만 실행하는 게 원칙이기 때문에 첫 번째 Task에서 A를 호출하면 해당 결과를 반환하고, 두 번째 Task를 실행하여 해당 결과를 반환되어 순서가 보장되니 값이 변경될 일도 극히 드물다!&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(Task는 동시에 시작되지만 actor 내부에서 실행될 때는 순차적으로 실행된다.)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;그럼 nonisolated는 언제 쓰이는 것일까? Actor 인스턴스는 기본적으로 isolated self를 가지는데, nonisolated가 이걸 끄는 것이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✏️ nonisolated&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;actor의 격리에 의존하지 않음을 컴파일러에 명시하는 키워드&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Actor 격리 규칙 덕분에 Actor 내부에서 값이 바뀔 수 있는 부분에는 별도의 lock 없이도 안전하지만 모든 멤버가 항상 Actor의 상태를 읽거나 수정하는 것은 아니다. 예를 들어 Actor 내부에 존재하는 계산 로직이 포함된 함수나 immutable을 반환하는 함수는 굳이 순차적으로 실행될 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이때 &lt;b&gt;nonisolated는 Actor 내부에서 await 없이도 호출 가능하다는 점&lt;/b&gt;이다. nonisolated로 선언된 메서드나 프로퍼티는 actor에서 보장하는 변경가능한 상태에 접근할 수 없기 때문에 data race의 가능성 자체가 없어지게 되어 await없이도 안전하게 호출할 수 있는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;한마디로 정리하자면 해당 키워드를 사용함으로써 &lt;b&gt;actor의 보호를 받지 않는 대신 mutable state에 접근을 금지함으로써 어디서 호출되든 안전하다는걸 컴파일러가 증명할 수 있게 하는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아래 코드를 통해 이해해 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1765820891778&quot; class=&quot;swift&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;actor A {
    nonisolated func B() -&amp;gt; Int {
        ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;이런 코드가 존재한다고 가정했을 때 nonisolated키워드가 명시된 B함수는 actor 타입에 속해있지만, actor가 관리하는 상태와 실행 순서를 보호하기 위한 격리 규칙의 대상에서 제외되어 다른 곳에서도 await 키워드 없이 B함수를 호출할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;b&gt; nonisolated(unsafe)&lt;/b&gt;는 무엇일까?&lt;br /&gt;nonisolated(unsafe)는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;현재 격리 주체인 actor이 존재하지만 actor의 격리 범위를 벗어나고 싶을 때 사용하는 키워드&lt;/b&gt;다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;예를 들어 전역 변수나 정적 변수는 어디서든, 어떤 스레드/actor/task든 동시에 접근가능하기 때문에 Read, Write 순서가 보장되지 않아 값이 마음대로 바뀔 수 있다. 하지만 전역변수나 정적 변수이여도 실제로 개발자가 수동으로 NSLock을 잘해놓은 코드일수도 있고 순차 큐등 동기화를 잘 해놓은 코드일 수도 있다. swift가 그 내부까지는 확인할 수 없기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;컴파일러에게 내가 책임질 테니까 막지 마!&quot;라고&lt;/b&gt; 얘기하는 것이라고 이해하면 쉬울 거 같다!&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;즉 nonisolated(unsafe)는 swift가 안전하다고 인정한 것이 아니라 모른 척 배제해 달라고 요청&lt;/b&gt;하는 것이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;두 개념의 차이를 정리하자면 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;nonisolated&lt;/b&gt;&lt;/span&gt; 키워드는 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;안전하다고 컴파일러에서 보장해 주지만,&lt;/span&gt;&lt;/b&gt; &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;nonsiolated(unsafe)&lt;/b&gt;&lt;/span&gt;는&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; 개발자가 안전하다고 컴파일러에게 주장&lt;/b&gt;&lt;/span&gt;하는 것이다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-15 오전 12.50.35.png&quot; data-origin-width=&quot;1264&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k3cpI/dJMcaiV7OrS/L7BVPaKR8vN5B1mk12Dkm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k3cpI/dJMcaiV7OrS/L7BVPaKR8vN5B1mk12Dkm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k3cpI/dJMcaiV7OrS/L7BVPaKR8vN5B1mk12Dkm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk3cpI%2FdJMcaiV7OrS%2FL7BVPaKR8vN5B1mk12Dkm1%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;671&quot; height=&quot;248&quot; data-filename=&quot;스크린샷 2025-12-15 오전 12.50.35.png&quot; data-origin-width=&quot;1264&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-15 오전 12.50.05.png&quot; data-origin-width=&quot;1268&quot; data-origin-height=&quot;204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HtKJ6/dJMcac2F64m/waYac2bSpoqLEe0OE8al8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HtKJ6/dJMcac2F64m/waYac2bSpoqLEe0OE8al8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HtKJ6/dJMcac2F64m/waYac2bSpoqLEe0OE8al8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHtKJ6%2FdJMcac2F64m%2FwaYac2bSpoqLEe0OE8al8K%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;674&quot; height=&quot;108&quot; data-filename=&quot;스크린샷 2025-12-15 오전 12.50.05.png&quot; data-origin-width=&quot;1268&quot; data-origin-height=&quot;204&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;size18&quot;&gt;이렇게 nonisolated 키워드를 메서드에 호출시키고 변수에 선언해 줬더니 더 이상 컴파일 에러가 발생하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만&lt;b&gt; 항상 nonisolated를 사용하는 게 정답은 아니다. &lt;/b&gt;특히 &lt;b&gt;nonisolated(unsafe)&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;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;✏️ &lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;&amp;nbsp;Strict concurrency와 Swift 6&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #333333; text-align: left;&quot;&gt;swift 6에서는 xcode에서 build setting에 strict concurrency checking을 활성화하면 동시성 환경에서 date race가 발생할 가능성이 있는 곳에서 에러나 경고를 표시한다. (거의 기본으로 세팅이 되어 있는 것으로 알고 있다.)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #333333; text-align: left;&quot;&gt;swift 6 이전까지는 strict concurrency가 적용되지 않아 컴파일은 되지만 시뮬레이터로 돌렸을 때 갑자기 앱이 강제 종료되는 등 런타임 크래시가 발생한 적이 꽤 많았지만, &lt;span style=&quot;background-color: #fcfcfc; color: #333333; text-align: left;&quot;&gt;이제는 컴파일러가 Actor 격리 규칙을 100% 컴파일 타임에 강제하기 때문에 런타임 크래시까지 갈 것도 없이 컴파일 딴에서 컴파일 에러가 발생하도록 보안을 강화했다고 한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;(이유도 모르게 앱이 터지기 전에 미리 막아주는 건 꽤 좋은 거 같다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이번 블로그를 통해 nonisolated에 대해서 살펴보고 actor에 대한 개념도 살펴보았다. 최근 공부했던 것 중에서 가장 어렵게 느껴졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;다른 블로그에서 정리된 내용이 많긴 하지만 여러 사전 지식을 알지 못하면 이해가 가지 않은 블로그들도 많았고, 하나부터 다 공부하자 하니 너무 막막했다!!! 그래서 stackoverflow랑 여러 공식문서를 참고하면서 이해했는데 concurrency는 언제나 어려운 거 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이번 블로그는 나처럼 여러 개념을 몰라도 한 번에 이해하기 쉽도록 작성하는 것이 목표였는데 달성한 지는 모르겠다 ㅎ ㅎ.!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.avanderlee.com/swift/thread-sanitizer-data-races/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.avanderlee.com/swift/thread-sanitizer-data-races/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.avanderlee.com/swift/actors/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.avanderlee.com/swift/actors/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.avanderlee.com/swift/nonisolated-isolated/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.avanderlee.com/swift/nonisolated-isolated/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/swiftlang/swift-evolution/blob/main/proposals/0313-actor-isolation-control.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/swiftlang/swift-evolution/blob/main/proposals/0313-actor-isolation-control.md&lt;/a&gt;&lt;/p&gt;</description>
      <category>iOS/Swift</category>
      <category>actor</category>
      <category>nonisolated</category>
      <category>Swift Concurrency</category>
      <author>yanni13</author>
      <guid isPermaLink="true">https://yanni13.tistory.com/73</guid>
      <comments>https://yanni13.tistory.com/73#entry73comment</comments>
      <pubDate>Tue, 16 Dec 2025 15:23:05 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] SwiftUI에서 Localization 적용하기</title>
      <link>https://yanni13.tistory.com/72</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✏️ Localization이란?&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;앱을 다양한 언어와 지역 환경에 맞게 번역하고 조정하는 과정&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Localization은 앱을 언어와&amp;nbsp;문화권에 맞게 &lt;b&gt;텍스트나, 날짜, 숫자, 형&lt;/b&gt;식등을 현지화 하는 과정이다. 한국어를 다른 언어로 번역하는 것 뿐만 아니라 각 문화권에서 익숙한 표현 방식까지 반영한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;예를 들어 우리나라는 &lt;b&gt;년/월/일&lt;/b&gt; 표기라면 영어권은 &lt;b&gt;월/일/년&lt;/b&gt;을 사용하므로 다국어지원을 설정했을때 날짜 표기 방식도 함께 반영된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;영어는 복수형이나 문장 구조에 따라 표현이 달라지는 것들도 있기 때문에, Localization을 통해 문장이 어색해지는 걸 방지하고 네이티브처럼 자연스러운 문장을 제공할 수 있도록 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이번에 Apple Developer Academy에서 하는 쇼케이스를 앞두고 애플 본사에서도 직원이 나오고 영어로 앱을 설명해야 하는 상황이 생겨 다국어지원을 적용할 필요를 느끼게 되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&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;b&gt; 사용방법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;1. 프로젝트에 String Catalog 파일을 추가하기&lt;/b&gt;&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;String Catalog란?&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;String Catalog는 코드에서 &lt;b&gt;현지화 가능한 모든 문자열을 자동으로 추적하고 번역내용을 한곳에 보관&lt;/b&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1157&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEwpyj/dJMcagKJJVh/KFRytf2SmgVCnmkJzMr220/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEwpyj/dJMcagKJJVh/KFRytf2SmgVCnmkJzMr220/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEwpyj/dJMcagKJJVh/KFRytf2SmgVCnmkJzMr220/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEwpyj%2FdJMcagKJJVh%2FKFRytf2SmgVCnmkJzMr220%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;1157&quot; height=&quot;768&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1157&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-03 오전 10.53.08.png&quot; data-origin-width=&quot;1446&quot; data-origin-height=&quot;998&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blTEP8/dJMcagcVknZ/7lmzsMKcELHSlhM4SlLBp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blTEP8/dJMcagcVknZ/7lmzsMKcELHSlhM4SlLBp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blTEP8/dJMcagcVknZ/7lmzsMKcELHSlhM4SlLBp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblTEP8%2FdJMcagcVknZ%2F7lmzsMKcELHSlhM4SlLBp0%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;634&quot; height=&quot;438&quot; data-filename=&quot;스크린샷 2025-12-03 오전 10.53.08.png&quot; data-origin-width=&quot;1446&quot; data-origin-height=&quot;998&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;파일이름은 기본값인&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Localizable&lt;/span&gt;.strings&lt;/b&gt;으로 설정해주어야 Xcode가 파일이름을 인식해서 로컬라이제이션을 적용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;나는 Resources폴더 하위에 String Catalog 파일을 추가하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;2. Project에서 설정에서 언어추가&amp;nbsp;&lt;/b&gt;&lt;/blockquote&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;Project -&amp;gt; Info -&amp;gt; +&lt;/b&gt; 버튼을 눌러서 Localizations에 English/Korean 를 추가해주자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-21 오전 11.42.58.png&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;872&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6ZhQx/dJMcafyeQTb/uNLO69RrqLrZWRyd4vDWnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6ZhQx/dJMcafyeQTb/uNLO69RrqLrZWRyd4vDWnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6ZhQx/dJMcafyeQTb/uNLO69RrqLrZWRyd4vDWnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6ZhQx%2FdJMcafyeQTb%2FuNLO69RrqLrZWRyd4vDWnK%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;1632&quot; height=&quot;872&quot; data-filename=&quot;스크린샷 2025-11-21 오전 11.42.58.png&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;872&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;size18&quot;&gt;나의 경우 Default Language가 English로 되어 있어서 Korean을 추가해주었다. (Set Default를 버튼을 눌러서 Default Language를 변경할 수 있다.)&lt;/p&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;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;3. 그 후 시뮬레이터를 빌드하면 자동으로 Catalog에 추가된다.&lt;br /&gt;&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;시뮬레이터로 앱을 실행하면 Button이나 Text에 사용된 문자열이 자동으로 String Catalog에 추가된다. 하지만 컴포넌트로 분리되어 있거나 자동 감지가 어려운 문자열은 누락될 수 있으니 꼭 한번 전체 플로우로 테스트해서 확인해보는 것이 중요하다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;없다면 + 버튼을 누르고 직접 추가해주면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-21 오후 12.08.23.png&quot; data-origin-width=&quot;2672&quot; data-origin-height=&quot;2128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sTspI/dJMcafkH9Rp/1TUFiloDN6nG0EKCCfSNwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sTspI/dJMcafkH9Rp/1TUFiloDN6nG0EKCCfSNwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sTspI/dJMcafkH9Rp/1TUFiloDN6nG0EKCCfSNwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsTspI%2FdJMcafkH9Rp%2F1TUFiloDN6nG0EKCCfSNwK%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;2672&quot; height=&quot;2128&quot; data-filename=&quot;스크린샷 2025-11-21 오후 12.08.23.png&quot; data-origin-width=&quot;2672&quot; data-origin-height=&quot;2128&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;해당 문서를 구성요소로 &lt;b&gt;key, Default Language, Korean, Comment&lt;/b&gt; 가 있다. 이 항목들에 대해서 자세히 살펴보자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;Key&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문자열 리소스를 식별하는 ID값이다. Localization &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;사용 자체는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;Key = Value&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;형식으로 사용되며 해당 키에 맞는 string 값을 가져오는 구조이다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Default Language&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로젝트의 기본값으로 설정된 언어에서 해당 key가 어떤 문자열인지 정의한다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Korean&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 key값에 대한 한국어 번역 문자열이다. 현재 프로젝트의 경우 key값이 한글 text로 설정되어 있어 따로 설정해주지 않았다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Comment&lt;/b&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;이다. 같은 단어라도 상황에 따라 다른 번역이 필요할 수 있고 특정 문자열이 앱이름인지 실제 단어인지 모를수도 있다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이외에도 Int 값은 %lld, String 값이 들어가는 텍스트라면 %@를 사용하여 키 값과 동일하게 텍스트로 정의할 수 있다. 이것까지 빌드시점에서 자동으로 감지된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;추가로 SwiftUI에서는 다국어지원을 하게될 경우 String 값이 아닌 &lt;span style=&quot;color: #333333; text-align: center;&quot;&gt;&lt;b&gt;LocalizedStringKey&lt;/b&gt; 타입을 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333; text-align: center;&quot;&gt;그래서 컴포넌트와 같이 String Catalog에서 자동으로 감지하지 못할 경우 타입을 직접 LocalizedStringKey으로 바꿔서 사용해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-03 오전 10.59.42.png&quot; data-origin-width=&quot;1464&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bi1GEw/dJMcabQc2W2/05dZvSooCE2Zz8FNV2KvGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bi1GEw/dJMcabQc2W2/05dZvSooCE2Zz8FNV2KvGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bi1GEw/dJMcabQc2W2/05dZvSooCE2Zz8FNV2KvGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbi1GEw%2FdJMcabQc2W2%2F05dZvSooCE2Zz8FNV2KvGK%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;733&quot; height=&quot;220&quot; data-filename=&quot;스크린샷 2025-12-03 오전 10.59.42.png&quot; data-origin-width=&quot;1464&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;&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;&lt;b&gt;4. Edit Scheme에서 언어별 테스트&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-03 오전 10.42.10.png&quot; data-origin-width=&quot;1086&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o1CZO/dJMcafd0CJY/IG0Yqaz8koS3pkHy8NWDkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o1CZO/dJMcafd0CJY/IG0Yqaz8koS3pkHy8NWDkk/img.png&quot; data-alt=&quot;출처: https://developer.apple.com/documentation/xcode/localizing-and-varying-text-with-a-string-catalog&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o1CZO/dJMcafd0CJY/IG0Yqaz8koS3pkHy8NWDkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo1CZO%2FdJMcafd0CJY%2FIG0Yqaz8koS3pkHy8NWDkk%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;723&quot; height=&quot;386&quot; data-filename=&quot;스크린샷 2025-12-03 오전 10.42.10.png&quot; data-origin-width=&quot;1086&quot; data-origin-height=&quot;580&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://developer.apple.com/documentation/xcode/localizing-and-varying-text-with-a-string-catalog&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Edit Scheme에서 테스트를 해볼 때 &lt;b&gt;App Language&lt;/b&gt;와 &lt;b&gt;App Region&lt;/b&gt;을 해당 언어와 국가로 변경해주면 빌드 했을때 다국어 모드가 실행된 채로 시작된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;나는 영어권 테스트를 위해 App Language를 english로 설정하고 App Region을 영국으로 설정하여 빌드하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;시뮬레이터가 아닌 Preview로도 확인하고 싶다고 하면 프리뷰를 호출하는 부분에 environment를 통해 언어ID를 넣어 지정하여 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1764726297097&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;     static var previews: some View {
        ContentView()
                .environment(\.locale, .init(identifier: &quot;en&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  WWDC 2025&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;WWDC 2025에도 localization 관련 영상이 나왔는데 주요 내용만 짧게 살펴보자!&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;1.&amp;nbsp; Localizations 내보내기/가져오기&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-03 오전 11.38.57.png&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sgaIH/dJMcajgmdNa/JSshNSUkehdRqvnmBk8AQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sgaIH/dJMcajgmdNa/JSshNSUkehdRqvnmBk8AQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sgaIH/dJMcajgmdNa/JSshNSUkehdRqvnmBk8AQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsgaIH%2FdJMcajgmdNa%2FJSshNSUkehdRqvnmBk8AQ0%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;548&quot; height=&quot;337&quot; data-filename=&quot;스크린샷 2025-12-03 오전 11.38.57.png&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;556&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Product -&amp;gt; Export Localizations&lt;/b&gt;를 누르면 String catalog에서 지금까지 번역된 내용과 아직 번역되지 않은 영어 문자열이 포함된 local catalog 파일을 생성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;해당 패키지에 업계 표준 XLIFF 파일이 들어있기 때문에 번역 업체가 쉽게 작업할 수 있으며 번역이 끝나면 완성된 현지화 카탈로그 파일을 보내주면&lt;b&gt; Product -&amp;gt; import Localizations&lt;/b&gt;을 눌러서 다시 가져올 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 문서를 import 하기만 하면 String Catalog 안에 번역 내용이 자동으로 다 채워져서 &lt;b&gt;100%의 상태를 가질 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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. Comment&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;Key에 대한 주석이 없으면 문자열의 용도를 파악하기 어렵다. 해당 key에 대한 value가 탭 막대인지, 버튼인지에 대해 구분이 안되기 때문에 comment에 단어/문장에 대한 문맥 설명을 적어주는 것이 중요하다. 주변 인터페이스 ui가 어떤 것이 있는지도 적어주는 것이 좋다고 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;추가로 Xcode 26에서는 온디바이스 모델을 사용해 코드를 분석하고 자동으로 주석을 작성한다고 하며 문자열 값과 키를 분리해두면 문자열이 바뀌어도 코드를 수정할 필요가 없다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-03 오후 12.03.12.png&quot; data-origin-width=&quot;1176&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y7twa/dJMcad1vJm8/a14HOOKmgZq2r8ujoMa7JK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y7twa/dJMcad1vJm8/a14HOOKmgZq2r8ujoMa7JK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y7twa/dJMcad1vJm8/a14HOOKmgZq2r8ujoMa7JK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy7twa%2FdJMcad1vJm8%2Fa14HOOKmgZq2r8ujoMa7JK%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;708&quot; height=&quot;107&quot; data-filename=&quot;스크린샷 2025-12-03 오후 12.03.12.png&quot; data-origin-width=&quot;1176&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(지금 다국어 적용해놓은 프로젝트에서는 key값이 한국어 문장/단어로 되어 있는데 추가적으로 수정할 필요가 있어보인다)&lt;/p&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-filename=&quot;스크린샷 2025-12-03 오후 12.08.01.png&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;618&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pNZgP/dJMcabvUtgt/gaJHPWmnD8kzE1UA4W1sI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pNZgP/dJMcabvUtgt/gaJHPWmnD8kzE1UA4W1sI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pNZgP/dJMcabvUtgt/gaJHPWmnD8kzE1UA4W1sI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpNZgP%2FdJMcabvUtgt%2FgaJHPWmnD8kzE1UA4W1sI1%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;531&quot; height=&quot;405&quot; data-filename=&quot;스크린샷 2025-12-03 오후 12.08.01.png&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;618&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;size18&quot;&gt;프로젝트 규모가 커지게 됐을때 여러 개발자가 함께 작업하면 프레임워크/패키지 단위로 구성되는데 이때 각각 독립된 String Catalog를 가진다. 즉 런타임에서 문자열을 어디서 찾을지 알려주는 bundle 매개변수가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;WWDC 2025에서는 올해 번들 매크로가 추가되어 현재 타겟의 리소스를 포함하는 번들을 참조할 수 있고 OS 버전이 낮아도 자동으로 처리되어 높은 호환성을 보여준다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;⚔️ 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;오늘은 SwiftUI 프로젝트에 Localization을 적용해보며 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Localization이 어떤건지, 어떤 흐름으로 xcode에서 동작하는지 직접 경험해볼 수 있었다. 확실히 여러 언어와 지역을 지원하면 더 많은 사용자가 앱을 사용할 수 있고, 서비스의 확장성과 접근성 측면에서 많은 사용자를 유치할 수 있다는 점이 큰 메리트라고 느꼈다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/xcode/localization&quot;&gt;https://developer.apple.com/documentation/xcode/localization&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/videos/play/wwdc2025/225/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/videos/play/wwdc2025/225/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/xcode/localizing-and-varying-text-with-a-string-catalog&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/documentation/xcode/localizing-and-varying-text-with-a-string-catalog&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/xcode/adding-support-for-languages-and-regions&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/documentation/xcode/adding-support-for-languages-and-regions&lt;/a&gt;&lt;/p&gt;</description>
      <category>iOS</category>
      <category>localization</category>
      <author>yanni13</author>
      <guid isPermaLink="true">https://yanni13.tistory.com/72</guid>
      <comments>https://yanni13.tistory.com/72#entry72comment</comments>
      <pubDate>Wed, 3 Dec 2025 14:56:30 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] Swift Format 적용기 + husky 배포까지</title>
      <link>https://yanni13.tistory.com/71</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;팀프로젝트에서 SwiftLint를 사용하긴 하지만 막상 바쁜 개발과 마감기한 때문에 경고를 무시한 채 코드를 붙이는 경우가 꽤 흔하다. swiftLint가 팀 전체 코드 스타일을 맞춰주기 위해 어느 정도 가이드를 제공해주긴 하지만 결국 개발자가 직접 수정하지 않으면 경고는 계속해서 남아있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 차라리 코드 포맷팅을 자동으로 보정해주는 툴을 사용하면 다른 기능 구현하는데 더 효과적이지 않을까?라는 고민을 하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;lint가 발생시키는 경고를 최소화하고 코드 스타일 맞추는 시간보다 다른 생산적인 작업에 집중 할 수 있도록 환경을 설정하고 싶었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이럴 때 유용한 도구가 swiftFormat이다. 자동화된 포맷팅을 통해 빌드를 하기만 하면 팀 컨벤션을 자연스럽게 맞춰지고, 개발자가 직접 수정해야 한다는 부담도 줄어들게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;따라서 오늘은 swiftFormat 설치부터 프로젝트 적용, 그리고 Husky를 활용한 팀 배포까지 정리해보려고 한다.&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;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✏️ Swift Format&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;swift 코드를 자동으로 정리해 주는 포맷터&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앞서 말했듯 swiftLint와의 차이는 존재한다. swftLint는 팀 컨벤션에 맞춰서 수정할 수 있으며 경고와 에러를 띄워주긴 하지만 반영은 순전히 개발자의 몫이기 때문에 컨벤션이 지켜지지 않는 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;반면 swift Format은 개발자가 반영하지 않아도 특정 시점(빌드 혹은 커밋...)에서 자동으로 코드가 수정되면서 바뀐다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;장단점도 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;장점으로는 팀에 개발자가 많을수록 코드 스타일이 제각각 달라서 swift Format을 통해 자동으로 코드 스타일을 맞춰주게 된다면 팀원이 실수로 스타일을 어기는 일도 없고 일관된 코드 스타일로 인해 가독성도 더 향상될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;반면 단점으로는 빌드시점에서 swift Format을 적용하게 된다면 swift Format이 변경한 코드까지 변경사항이 추적되어서 불필요한 변경사항이 많이 생기게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 지금 개발 상황을 따져보면 SwiftLint을 적용하곤 있지만 약 400개의 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;lint 경고가 쌓여 있는 상황이라,&lt;/span&gt; 이 400개에 해당하는 오류를 하나하나 수동으로 고치기엔 공수가 많이 든다고 생각하였다. &lt;b&gt;개발 규모가 커지고 파일이 많아질수록 개발자 한 명 한 명의 코드 스타일이 다 달라서 코드 가독성도 떨어지는 문제가 발생&lt;/b&gt;했기 때문에 &lt;b&gt;swift Format을 통해 자동으로 일관된 스타일을 적용해 줄 수 있는 도구가 필요&lt;/b&gt;하다고 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;그럼 직접 적용해 보자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1️⃣ 방법 1: SwiftFormat for Xcode + 단축어(Shortcuts) 설정&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;로컬 환경에서만 동작하는 포맷팅 자동화&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;macOS에 일정 버전 이상이라면 SwiftFormat이 자동으로 설치되어 있다. 해당 방법은 로컬 환경에서만 동작하기 때문에 팀원 모두가 개별로 설정해야 한다는 번거로움이 있는 반면 원하는 시점에만 Format을 돌릴 수 있도록 하여 개인 개발 환경에 최적화되어 있다고 할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;직접 설정해 주기&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;설정 -&amp;gt; 확장 프로그램 검색 -&amp;gt; 로그인 항목 및 확장 프로그램&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;우선 설정에 들어간 후 돋보기를 통해 확장 프로그램을 검색하여 로그인 항목 및 확장 프로그램에 들어가 주자.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-11 오전 10.40.10.png&quot; data-origin-width=&quot;1670&quot; data-origin-height=&quot;1164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A6icI/dJMcafyazMT/Os44Ce9YXWbjjkAjR0ZkK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A6icI/dJMcafyazMT/Os44Ce9YXWbjjkAjR0ZkK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A6icI/dJMcafyazMT/Os44Ce9YXWbjjkAjR0ZkK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA6icI%2FdJMcafyazMT%2FOs44Ce9YXWbjjkAjR0ZkK0%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;1670&quot; height=&quot;1164&quot; data-filename=&quot;스크린샷 2025-11-11 오전 10.40.10.png&quot; data-origin-width=&quot;1670&quot; data-origin-height=&quot;1164&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;macOS를 업데이트시켜 주면 swift Format이 자동으로 맥북에 깔려있다! 여기서 swiftFormat for Xcode를 누르면 아래와 같은 사진으로 넘어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-11 오전 10.40.14.png&quot; data-origin-width=&quot;1670&quot; data-origin-height=&quot;1258&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d6QIKR/dJMb99Lui1j/ks3Xbcf82diNNeWDYhwek0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d6QIKR/dJMb99Lui1j/ks3Xbcf82diNNeWDYhwek0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d6QIKR/dJMb99Lui1j/ks3Xbcf82diNNeWDYhwek0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd6QIKR%2FdJMb99Lui1j%2Fks3Xbcf82diNNeWDYhwek0%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;1670&quot; height=&quot;1258&quot; data-filename=&quot;스크린샷 2025-11-11 오전 10.40.14.png&quot; data-origin-width=&quot;1670&quot; data-origin-height=&quot;1258&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;토글을 활성화해주면 xcode와 연동 완료!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-11 오전 10.40.46.png&quot; data-origin-width=&quot;1836&quot; data-origin-height=&quot;1128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ACSHZ/dJMcafyazNd/2HoEDUV6ZfGhRejC0dFdh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ACSHZ/dJMcafyazNd/2HoEDUV6ZfGhRejC0dFdh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ACSHZ/dJMcafyazNd/2HoEDUV6ZfGhRejC0dFdh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FACSHZ%2FdJMcafyazNd%2F2HoEDUV6ZfGhRejC0dFdh0%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;1836&quot; height=&quot;1128&quot; data-filename=&quot;스크린샷 2025-11-11 오전 10.40.46.png&quot; data-origin-width=&quot;1836&quot; data-origin-height=&quot;1128&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그다음 Xcode를 켜서 Setting을 들어간 후 ShortCuts에 들어가 swiftFormat에 한하여 단축어를 설정하면 해당 단축어만 누르면 swift Format이 자동으로 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-11 오전 10.43.03.png&quot; data-origin-width=&quot;1836&quot; data-origin-height=&quot;1128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpqRHa/dJMcafStoVC/4sWKpGkXLGgEE06WErYKz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpqRHa/dJMcafStoVC/4sWKpGkXLGgEE06WErYKz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpqRHa/dJMcafStoVC/4sWKpGkXLGgEE06WErYKz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpqRHa%2FdJMcafStoVC%2F4sWKpGkXLGgEE06WErYKz0%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;1836&quot; height=&quot;1128&quot; data-filename=&quot;스크린샷 2025-11-11 오전 10.43.03.png&quot; data-origin-width=&quot;1836&quot; data-origin-height=&quot;1128&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;size18&quot;&gt;단축어 설정하면 xcode에서 별다른 설정을 해주지 않아도 swift Format이 자동으로 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;겹치지 않는 단축어를 설정해서 매번 swift Format을 돌릴 수도 있고, runscript에 따로 설정해서 빌드 시점에 swift Format을 자동 적용할 수 있도록 하는 방법도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;나는 빌드 시점에서 하려고 하였으나 단축어가 겹치는 바람에 따로 run script에 설정을 해줘야 했다. 생각해 보니 이전 프로젝트에서 빌드 시점마다 swift format이 적용되도록 설정했을 때 시뮬레이터를 한번 돌릴 때마다 느리게 빌드가 되기도 하고, 빌드할 때마다 swift Format이 자동으로 적용한 코드들이 커밋에 계속 적용된다는 점이 비효율적이라고 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;따라서 이번 프로젝트에서는 커밋시점에서 swift format을 자동 적용해 주도록 hook을 따로 만들어서 빌드 속도와, 불필요한 변경이 계속 기록에 남는 것을 방지하고자 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2️⃣ 방법 2: Hook 적용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;프로젝트 루트 디렉토리로 가서 아래 명령어를 실행해 주면 hook 관련 파일이 생성된다.&lt;/p&gt;
&lt;pre id=&quot;code_1763796051526&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ touch .git/hooks/pre-commit
$ chmod +x .git/hooks/pre-commit\&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그다음 pre-commit 파일을 열어서 아래와 같이 복붙 해주면 된다!&lt;/p&gt;
&lt;pre id=&quot;code_1763797527976&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# pre-commit 여는 명령어
open -a TextEdit .git/hooks/pre-commit&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;여기서 2번 CONFIG_FILE과 PROJECT_SOURCE_DIR을 경로와 폴더구조에 맞춰서 작성해 주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1762863389910&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash


# --- 1. swift-format 절대 경로 자동 설정 및 유효성 확인 ---

# 'which swift-format' 명령의 출력을 SWIFT_FORMAT 변수에 저장
SWIFT_FORMAT=&quot;/opt/homebrew/bin/swift-format&quot;

# which 명령이 실패했거나 (종료 코드 $?가 0이 아님) 파일이 존재하지 않는 경우 확인
if [ $? -ne 0 ] || [ ! -f &quot;$SWIFT_FORMAT&quot; ]; then
  echo &quot;❌ Error: swift-format executable not found.&quot; &amp;gt;&amp;amp;2
  echo &quot;Please ensure swift-format is installed via Homebrew and accessible in your PATH.&quot; &amp;gt;&amp;amp;2
  exit 1
fi

echo &quot;✅ swift-format found at: $SWIFT_FORMAT&quot; &amp;gt;&amp;amp;2

# --- 2. 설정 파일 경로 및 대상 폴더 설정 ---
CONFIG_FILE=&quot;{FormatFile}&quot;
PROJECT_SOURCE_DIR=&quot;{Project Folder}&quot;

# 설정 파일이 없으면 경고 후 종료
if [ -n &quot;$CONFIG_FILE&quot; ] &amp;amp;&amp;amp; [ ! -f &quot;$CONFIG_FILE&quot; ]; then
  echo &quot;⚠️ Warning: swift-format configuration file ($CONFIG_FILE) not found.&quot; &amp;gt;&amp;amp;2
fi


# --- 3. 변경된 Swift 파일 목록 가져오기 및 포맷팅 실행 ---

# git diff --cached --name-only: Staging Area에 있는 파일 목록만 가져옵니다.
# grep '\.swift$': 목록 중 .swift 확장자로 끝나는 파일만 필터링합니다.
CHANGED_SWIFT_FILES=$(git diff --cached --name-only | grep '\.swift$')

if [ -n &quot;$CHANGED_SWIFT_FILES&quot; ]; then
  echo &quot;✨ Scanning and running swift-format on staged files...&quot; &amp;gt;&amp;amp;2
  
  # 포맷팅할 파일을 임시 배열에 저장합니다.
  FILES_TO_FORMAT=()
  for FILE in $CHANGED_SWIFT_FILES; do
    # 파일 경로가 지정된 소스 디렉토리로 시작하는지 확인합니다.
    # RecipeNote/FileName.swift 처럼 경로가 시작되어야 합니다.
    if [[ &quot;$FILE&quot; == &quot;$PROJECT_SOURCE_DIR&quot;* ]]; then
      FILES_TO_FORMAT+=(&quot;$FILE&quot;)
    fi
  done
  
  if [ ${#FILES_TO_FORMAT[@]} -gt 0 ]; then
    echo &quot;  Formatting ${#FILES_TO_FORMAT[@]} files...&quot; &amp;gt;&amp;amp;2

    # SWIFT_FORMAT 변수를 사용하여 절대 경로로 툴 실행
    if [ -n &quot;$CONFIG_FILE&quot; ]; then
      &quot;$SWIFT_FORMAT&quot; format --in-place --configuration &quot;$CONFIG_FILE&quot; &quot;${FILES_TO_FORMAT[@]}&quot;
    else
      &quot;$SWIFT_FORMAT&quot; format --in-place &quot;${FILES_TO_FORMAT[@]}&quot;
    fi

    # 포맷팅으로 인해 내용이 변경된 파일을 다시 Staging Area에 추가합니다.
    git add &quot;${FILES_TO_FORMAT[@]}&quot;

    echo &quot;✅ Swift-format complete. Changes re-staged.&quot; &amp;gt;&amp;amp;2
  else
    echo &quot;☑️ No relevant Swift files found in $PROJECT_SOURCE_DIR for formatting.&quot; &amp;gt;&amp;amp;2
  fi
else
  echo &quot;☑️ No Swift files staged. Skipping format.&quot; &amp;gt;&amp;amp;2
fi

echo &quot;✅ pre-commit hook finished successfully.&quot; &amp;gt;&amp;amp;2
exit 0 # 훅 성공 (커밋 진행)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 이 스크립트를 그대로 개발 중인 브랜치에 올리게 된다면 팀원마다 Homebrew로 swiftFormat을 설치하고 동일한 hook 파일을 모두 직접 붙여 넣어야 한다는 번거로움이 남아있었다. 이런 수동 세팅이 불필요한 시간을 잡아먹기도 하고, 환경차이로 인한 오류가 발생할 가능성이 있다고 판단하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;따라서 &lt;b&gt;husky&lt;/b&gt;를 도입해 프로젝트 내부에서&lt;b&gt; Git hook을 자동으로 배포&lt;/b&gt;하도록 구성하고자 하였다. 그럼 다른 팀원들은 npm install 만 실행하면 자동으로 필요한 hook이 적용되기 때문에 가장 적은 시간으로 팀 전체에 동일한 개발 환경을 세팅할 수 있을 것이라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✏️ husky 배포&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;git Hooks를 적용하게 해주는 라이브러리이다. 즉 hook 관리 도구&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;husky는 .git/hooks 폴더 안에 직접 스크립트를 작성해야 하는 번거로움을 줄여주고 프로젝트 내부에서 .husky/에 hook을 선언적으로 관리할 수 있도록 도와준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;즉 husky를 사용하면 스크립트를 레포지토리 내부에서 관리할 수 있기 때문에 팀원들이 스크립트를 따로 받아서 로컬에 넣어줘야 하는 번거로움을 줄여줄 수 있다. 레포지토리를 클론하고 npm install만 실행하면 끝!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그럼 husky 배포를 어떻게 하는지 알아보자~&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Husky는 Node 생태계 기반 라이브러리이기 때문에&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;사용하려면 Node.js와 NPM이 필요하다.&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;따라서 설치여부를 먼저 확인해 주자&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1762831675698&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$node -v
$npm -v&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;설치되어 있다는 걸 확인하면 husky를 프로젝트에 설치하고, npm 설치가 되어 있지 않은 경우 아래 명령어를 통해 설치해 주자.&lt;/p&gt;
&lt;pre id=&quot;code_1762831756614&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 문제 없을 경우
$npm install husky --save-dev

# npm설치가 안되어 있을 경우 설치 
$npm install husky
$npm husky init&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;프로젝트에 husky 설치가 완료되었다면 필요한 파일들이 기본적으로 생성될 수 있도록 초기화 과정을 거쳐줘야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1762831766047&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$npx husky init&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이제 설치가 다 끝났다! 패키지를 들어가서 보면 husky가 잘 설치되어 있는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-11 오후 12.29.40.png&quot; data-origin-width=&quot;542&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eDaukm/dJMcaihmaki/c54fy5LpjJvUDCoRF3Jxu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eDaukm/dJMcaihmaki/c54fy5LpjJvUDCoRF3Jxu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eDaukm/dJMcaihmaki/c54fy5LpjJvUDCoRF3Jxu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeDaukm%2FdJMcaihmaki%2Fc54fy5LpjJvUDCoRF3Jxu1%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;331&quot; height=&quot;270&quot; data-filename=&quot;스크린샷 2025-11-11 오후 12.29.40.png&quot; data-origin-width=&quot;542&quot; data-origin-height=&quot;442&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후 팀원에게는 pull 혹은 clone 받은 후 아래 명령어만 입력하면 팀 전체가 같은 hook 환경을 공유할 수 있게 되기 때문에 동일하게 커밋 시점에서 swift Format이 적용할 수 있게 된다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1762832741115&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$brew install swiftformat
$npm install&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/nicklockwood/SwiftFormat#using-cocoapods&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/nicklockwood/SwiftFormat#using-cocoapods&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/@yeahg.dev/swift-format%EC%9C%BC%EB%A1%9C-xcode-cursor%EC%97%90%EC%84%9C-%EC%9E%90%EB%8F%99-%ED%8F%AC%EB%A7%B7-%EC%84%A4%EC%A0%95%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-78a91196f250&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://medium.com/@yeahg.dev/swift-format%EC%9C%BC%EB%A1%9C-xcode-cursor%EC%97%90%EC%84%9C-%EC%9E%90%EB%8F%99-%ED%8F%AC%EB%A7%B7-%EC%84%A4%EC%A0%95%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-78a91196f250&lt;/a&gt;&lt;/p&gt;</description>
      <category>iOS</category>
      <category>Husky</category>
      <category>Swift Format</category>
      <author>yanni13</author>
      <guid isPermaLink="true">https://yanni13.tistory.com/71</guid>
      <comments>https://yanni13.tistory.com/71#entry71comment</comments>
      <pubDate>Sat, 22 Nov 2025 16:54:30 +0900</pubDate>
    </item>
  </channel>
</rss>