<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>관슨의 개발일지</title>
    <link>https://gwanson-dev.tistory.com/</link>
    <description>귀찮음과 불편함을 해결하는 개발자가 되자.</description>
    <language>ko</language>
    <pubDate>Fri, 24 Jul 2026 09:47:32 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>GwanSon</managingEditor>
    <image>
      <title>관슨의 개발일지</title>
      <url>https://tistory1.daumcdn.net/tistory/6960545/attach/79451260dcab4d94a68dec4bad04fd7f</url>
      <link>https://gwanson-dev.tistory.com</link>
    </image>
    <item>
      <title>[iOS] Our Bridge 개발 회고</title>
      <link>https://gwanson-dev.tistory.com/122</link>
      <description>&lt;div&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ptbxO/dJMcac4Oqmi/YzAGdl5KUjcnNx0mYdolF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ptbxO/dJMcac4Oqmi/YzAGdl5KUjcnNx0mYdolF0/img.png&quot; data-origin-width=&quot;1320&quot; data-origin-height=&quot;2868&quot; data-filename=&quot;01-our-bridge.png&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ptbxO/dJMcac4Oqmi/YzAGdl5KUjcnNx0mYdolF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FptbxO%2FdJMcac4Oqmi%2FYzAGdl5KUjcnNx0mYdolF0%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;1320&quot; height=&quot;2868&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b05Gkt/dJMcabx3i0G/MieX1vqaBVPxzV1u9CPvNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b05Gkt/dJMcabx3i0G/MieX1vqaBVPxzV1u9CPvNk/img.png&quot; width=&quot;400&quot; data-is-animation=&quot;false&quot; data-filename=&quot;02-our-bridge.png&quot; data-origin-height=&quot;2868&quot; data-origin-width=&quot;1320&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b05Gkt/dJMcabx3i0G/MieX1vqaBVPxzV1u9CPvNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb05Gkt%2FdJMcabx3i0G%2FMieX1vqaBVPxzV1u9CPvNk%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;1320&quot; height=&quot;2868&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt;Our Bridge는?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Our Bridge는 장거리 커플을 위한 조용한 일상 공유 앱입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;떨어져 있는 두 사람이 서로의 하루, 기분, 일정, 사진을 부담 없이 확인할 수 있도록 만드는 것이 목표였습니다. 메신저처럼 계속 대화를 주고받는 앱이라기보다는, 상대방의 생활 리듬과 마음을 자연스럽게 느낄 수 있는 공간에 가깝게 만들고 싶었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장거리 연애를 하다 보면 &quot;지금 뭐 해?&quot;라는 말을 자주 하게 됩니다. 물론 그 말 자체가 나쁜 것은 아니지만, 매번 물어봐야만 상대의 하루를 알 수 있다는 점이 조금 아쉽게 느껴졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Our Bridge에서는 다음과 같은 경험을 중심으로 잡았습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오늘의 기분과 현재 상태 공유&lt;/li&gt;
&lt;li&gt;서로의 시간과 날씨 확인&lt;/li&gt;
&lt;li&gt;다음 만남 D-day 표시&lt;/li&gt;
&lt;li&gt;공유 캘린더&lt;/li&gt;
&lt;li&gt;서로 챙김 목록&lt;/li&gt;
&lt;li&gt;오늘의 한 장 사진 공유&lt;/li&gt;
&lt;li&gt;연결된 다리 진행도&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발 환경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트는 iOS 앱으로 개발했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Swift&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SwiftUI&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;iOS 18.0+&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CloudKit&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Sign in with Apple&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OpenWeather API&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Firebase 기반 구조도 고려했지만, 최종적으로는 Apple 생태계 안에서 자연스럽게 동작하도록 &lt;code&gt;Sign in with Apple&lt;/code&gt;과 &lt;code&gt;CloudKit&lt;/code&gt;을 중심으로 구성했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;앱의 방향성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Our Bridge를 만들면서 가장 신경 쓴 부분은 &quot;부담 없는 공유&quot;였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기분을 기록하거나 사진을 올리는 기능은 흔한 기능일 수 있습니다. 하지만 커플 앱에서 이 기능들이 너무 무겁게 느껴지면 오히려 사용하기 어려워진다고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 홈 화면도 많은 정보를 한 번에 보여주기보다는, 두 사람이 오늘 어떤 상태인지 조용히 확인할 수 있도록 구성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;홈 화면에는 다음과 같은 카드들이 들어갑니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다음 만남&lt;/li&gt;
&lt;li&gt;연결된 다리&lt;/li&gt;
&lt;li&gt;시간&amp;middot;날씨&lt;/li&gt;
&lt;li&gt;기분 공유&lt;/li&gt;
&lt;li&gt;최근의 순간들&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 기능은 독립적으로 존재하지만, 결국 &quot;오늘 두 사람이 얼마나 이어져 있는지&quot;를 보여주는 방향으로 연결되도록 만들었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;커플 연결 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Our Bridge는 두 사용자가 하나의 커플 공간을 공유해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 초대 코드 기반 연결 방식을 사용했습니다. 한 사용자가 커플 공간을 만들면 초대 코드가 생성되고, 상대방이 해당 코드를 입력해 같은 공간에 참여하는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 CloudKit의 공유 영역을 사용했습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;let share = CKShare(rootRecord: rootRecord)
share[CKShare.SystemFieldKey.title] = &quot;Our Bridge 초대&quot; as CKRecordValue
share.publicPermission = .readWrite&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CloudKit을 사용하면서 좋았던 점은 Apple 계정 기반으로 사용자 인증과 데이터 공유 흐름을 구성할 수 있다는 점이었습니다. 다만 공유 데이터, 개인 데이터, 공개 초대 코드 데이터의 역할을 나누는 부분은 생각보다 신경 쓸 것이 많았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Our Bridge에서는 대략 다음과 같이 역할을 나눴습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개인 사용자 정보: private database&lt;/li&gt;
&lt;li&gt;커플이 함께 보는 데이터: shared database&lt;/li&gt;
&lt;li&gt;초대 코드 조회용 데이터: public database&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 단순히 &quot;커플 데이터니까 같이 저장하면 되겠지&quot;라고 생각했는데, 실제로 구현하다 보니 누가 만든 데이터인지, 누가 접근할 수 있는지, 연결이 끊겼을 때 어떤 데이터를 정리해야 하는지까지 고려해야 했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;연결된 다리 진행도&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Our Bridge에서 개인적으로 마음에 들었던 기능은 &lt;code&gt;연결된 다리&lt;/code&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 앱에 들어와서 기능을 사용하는 것에서 끝나는 것이 아니라, 두 사람이 오늘 얼마나 서로의 하루에 참여했는지를 진행도로 보여주는 기능입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진행도는 다음 요소를 기준으로 계산했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;두 사람의 오늘 기분 공유&lt;/li&gt;
&lt;li&gt;두 사람의 오늘의 한 장&lt;/li&gt;
&lt;li&gt;서로 챙김 완료&lt;/li&gt;
&lt;li&gt;오늘 캘린더 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 기분 공유는 각자 10점, 오늘의 한 장은 각자 15점처럼 점수를 나눠 계산했습니다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;let moodPoints = (hasTodayCheckIn(userId) ? 10 : 0)
    + (hasTodayCheckIn(partnerId) ? 10 : 0)

let photoPoints = (hasTodayPhoto(userId) ? 15 : 0)
    + (hasTodayPhoto(partnerId) ? 15 : 0)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기능은 복잡한 알고리즘보다는 사용자의 행동을 어떻게 감성적인 피드백으로 바꿀지에 더 초점을 맞췄습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진행도에 따라 문구와 이미지도 달라지도록 했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 발판을 기다리는 중&lt;/li&gt;
&lt;li&gt;다리 놓기 시작&lt;/li&gt;
&lt;li&gt;서로에게 가는 중&lt;/li&gt;
&lt;li&gt;절반을 건너왔어요&lt;/li&gt;
&lt;li&gt;거의 이어졌어요&lt;/li&gt;
&lt;li&gt;오늘의 다리 완성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 기능이지만 앱의 이름인 Our Bridge와 가장 잘 맞는 기능이라고 생각합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로컬 캐시 적용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CloudKit 기반 앱을 만들면서 가장 아쉬웠던 부분은 네트워크 상태나 응답 속도에 따라 화면이 비어 보일 수 있다는 점이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커플 앱은 자주 열어보는 앱에 가깝기 때문에, 매번 데이터를 새로 불러오느라 기다리게 만들고 싶지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 커플 데이터 일부는 로컬에 캐시하도록 구성했습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;enum LocalCoupleDataCache {
    private static func key(for userId: String) -&amp;gt; String {
        &quot;longdy.cachedCoupleData.\(userId)&quot;
    }

    static func load(userId: String) -&amp;gt; CachedCoupleSnapshot? {
        guard let data = UserDefaults.standard.data(forKey: key(for: userId)) else {
            return nil
        }
        return try? JSONDecoder().decode(CachedCoupleSnapshot.self, from: data)
    }

    static func save(_ snapshot: CachedCoupleSnapshot) {
        guard let data = try? JSONEncoder().encode(snapshot) else { return }
        UserDefaults.standard.set(data, forKey: key(for: snapshot.userId))
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 거창한 캐시 시스템을 만들지는 않았습니다. 현재 필요한 것은 &quot;앱을 다시 켰을 때 마지막 데이터를 빠르게 보여주는 것&quot;이었기 때문에 &lt;code&gt;UserDefaults&lt;/code&gt;와 &lt;code&gt;Codable&lt;/code&gt;을 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추후 데이터가 더 커지거나 캐시 정책이 복잡해진다면 파일 저장이나 별도 저장소를 고려할 수 있겠지만, 지금 단계에서는 이 정도가 가장 단순하고 충분하다고 판단했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;화면 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Our Bridge는 크게 네 개의 탭으로 구성했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;홈&lt;/li&gt;
&lt;li&gt;캘린더&lt;/li&gt;
&lt;li&gt;챙김&lt;/li&gt;
&lt;li&gt;오늘의 한 장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;홈에서는 두 사람의 현재 상태를 빠르게 볼 수 있도록 했습니다. 캘린더에서는 서로의 일정과 다음 만남을 관리할 수 있고, 챙김에서는 상대방을 위해 기억하고 싶은 일을 등록할 수 있습니다. 오늘의 한 장에서는 하루에 한 장씩 사진을 공유할 수 있도록 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 기능을 많이 넣기보다는, 장거리 커플이 실제로 자주 확인할 만한 것들만 남기려고 했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발하면서 배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트를 진행하면서 가장 크게 느낀 점은, 앱의 기능보다 데이터 흐름을 먼저 잘 잡는 것이 중요하다는 점이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 커플 앱처럼 두 명의 사용자가 같은 데이터를 바라보는 구조에서는 단순 CRUD보다 고려할 부분이 많았습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초대 코드는 누가 만들 수 있는지&lt;/li&gt;
&lt;li&gt;본인이 만든 초대를 본인이 수락하지 못하게 해야 하는지&lt;/li&gt;
&lt;li&gt;이미 연결된 사용자가 다른 초대를 입력하면 어떻게 처리할지&lt;/li&gt;
&lt;li&gt;커플 연결을 끊으면 어떤 데이터를 유지하고 어떤 데이터를 지울지&lt;/li&gt;
&lt;li&gt;공유 데이터 접근 권한이 사라졌을 때 어떻게 복구할지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 부분들은 화면을 만드는 것보다 더 많은 고민이 필요했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 SwiftUI로 화면을 구성하면서 상태 관리의 중요성도 다시 느꼈습니다. 홈, 캘린더, 챙김, 사진 기능이 서로 독립되어 보이지만 실제로는 하나의 커플 상태를 공유하기 때문에, AppViewModel을 중심으로 데이터를 관리하도록 구성했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Our Bridge는 아직 개선하고 싶은 부분이 많은 프로젝트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이번 프로젝트를 통해 단순히 화면을 만드는 것보다, 사용자가 어떤 상황에서 앱을 열고 어떤 감정을 느낄지를 생각하면서 개발하는 경험을 할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 CloudKit 공유 구조, Apple 로그인, 로컬 캐시, SwiftUI 상태 관리 등을 실제 앱 흐름 안에서 다뤄볼 수 있어서 많이 배웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로는 알림 경험, 사진 공유 UX, 오프라인 상태 처리 등을 더 다듬어보고 싶습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장거리 커플이 매번 긴 대화를 하지 않아도, 서로의 하루를 조금 더 자연스럽게 느낄 수 있는 앱이 되었으면 좋겠습니다.&lt;/p&gt;</description>
      <category>iOS</category>
      <author>GwanSon</author>
      <guid isPermaLink="true">https://gwanson-dev.tistory.com/122</guid>
      <comments>https://gwanson-dev.tistory.com/122#entry122comment</comments>
      <pubDate>Thu, 9 Jul 2026 12:07:11 +0900</pubDate>
    </item>
    <item>
      <title>[swiftUI] SwiftUI에서 UIImagePickerController 사용하기</title>
      <link>https://gwanson-dev.tistory.com/121</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서: &lt;a href=&quot;https://developer.apple.com/documentation/uikit/uiimagepickercontroller&quot;&gt;https://developer.apple.com/documentation/uikit/uiimagepickercontroller&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;UIImagePickerController&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UIImagePickerController는 iOS 앱 내에서 사진이나 동영상을 촬영하거나, 기기의 사진 라이브러리에서 이미지를 선택할 수 있도록 시스템 기본 UI를 제공하는 뷰 컨트롤러입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주요 모드(Source Type)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 컨트롤러의 화면과 역할은 표시하기 전에 설정하는 소스 타입(Source Type)에 따라 달라집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;.camera&lt;/code&gt;: 새로운 사진이나 동영상을 촬영할 수 있는 카메라 인터페이스를 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.photoLibrary&lt;/code&gt;/&lt;code&gt;.savedPhotoAlbum&lt;/code&gt;: 기기에 저장된 기존 사진과 동영상을 고를 수 있는 앨범 인터페이스를 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기본 사용 방법&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;지원 여부 확인: &lt;code&gt;isSourceTypeAvailable&lt;/code&gt; 메서드를 사용해 현재 기기에서 카메라나 사진첩 접근이 가능한지 먼저 확인합니다.&lt;/li&gt;
&lt;li&gt;미디어 타입 설정: 사진만 고르게 할 것인지 동영상도 고르게 할 것인지 &lt;code&gt;mediaTypes&lt;/code&gt; 속성을 통해 설정합니다.&lt;/li&gt;
&lt;li&gt;화면 띄우기(Present):
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;iPhone: 모달 방식으로 띄움&lt;/li&gt;
&lt;li&gt;iPad: 카메라는 전체화면을 사용할 수 있지만 사진첩 앨범을 띄울 때는 반드시 팝업 형태로 띄워야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;결과 처리(Delegate): 사용자가 사진을 선택하거나 취소 버튼을 눌렀을 때의 동작을 처리하기 위해 &lt;code&gt;UIImagePickerControllerDelegate&lt;/code&gt;를 설정하여 결과값을 넘겨받습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주의사항&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로 세로(Portrait) 모드만 지원합니다.&lt;/li&gt;
&lt;li&gt;기본 UI를 사용하고 싶지 않다면 해당 클래스 대신 &lt;code&gt;AVFoundation&lt;/code&gt;이나 &lt;code&gt;PhotoKit&lt;/code&gt; 프레임워크를 사용해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;데모 앱&lt;/h1&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;import SwiftUI
import UIKit
import UniformTypeIdentifiers

struct ContentView: View {
    // 선택하거나 촬영한 이미지를 SwiftUI 화면에 표시하기 위한 상태값입니다.
    @State private var selectedImage: UIImage?

    // 하나의 UIImagePickerController로 카메라와 사진첩을 번갈아 열기 위해 sourceType을 저장합니다.
    @State private var pickerSource: UIImagePickerController.SourceType = .photoLibrary
    @State private var isShowingPicker = false

    // 시뮬레이터처럼 카메라를 사용할 수 없는 환경을 안내하기 위한 alert 상태입니다.
    @State private var alertMessage = &quot;&quot;
    @State private var isShowingAlert = false

    var body: some View {
        NavigationStack {
            VStack(spacing: 24) {
                imagePreview

                VStack(spacing: 12) {
                    Button {
                        presentPicker(for: .camera)
                    } label: {
                        Label(&quot;사진 촬영&quot;, systemImage: &quot;camera&quot;)
                            .frame(maxWidth: .infinity)
                    }
                    .buttonStyle(.borderedProminent)

                    Button {
                        presentPicker(for: .photoLibrary)
                    } label: {
                        Label(&quot;사진첩에서 선택&quot;, systemImage: &quot;photo.on.rectangle&quot;)
                            .frame(maxWidth: .infinity)
                    }
                    .buttonStyle(.bordered)
                }
            }
            .padding()
            .navigationTitle(&quot;UIImagePickerController Demo&quot;)
            .toolbarTitleDisplayMode(.inline)
        }
        .sheet(isPresented: $isShowingPicker) {
            ImagePicker(sourceType: pickerSource, selectedImage: $selectedImage)
                .ignoresSafeArea()
        }
        .alert(&quot;사용할 수 없음&quot;, isPresented: $isShowingAlert) {
            Button(&quot;확인&quot;, role: .cancel) {}
        } message: {
            Text(alertMessage)
        }
    }

    private var imagePreview: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 8)
                .fill(.secondary.opacity(0.12))

            if let selectedImage {
                Image(uiImage: selectedImage)
                    .resizable()
                    .scaledToFit()
                    .clipShape(RoundedRectangle(cornerRadius: 8))
            } else {
                ContentUnavailableView(
                    &quot;선택된 이미지 없음&quot;,
                    systemImage: &quot;photo&quot;,
                    description: Text(&quot;사진을 촬영하거나 사진첩에서 이미지를 선택하세요.&quot;)
                )
            }
        }
        .frame(maxWidth: .infinity)
        .aspectRatio(1, contentMode: .fit)
    }

    private func presentPicker(
        for sourceType: UIImagePickerController.SourceType
    ) {
        // UIImagePickerController는 기기마다 지원하는 sourceType이 다를 수 있으므로 먼저 확인합니다.
        guard UIImagePickerController.isSourceTypeAvailable(sourceType) else {
            alertMessage = sourceType == .camera
            ? &quot;이 기기에서는 카메라를 사용할 수 없습니다.&quot;
            : &quot;이 기기에서는 사진첩을 사용할 수 없습니다.&quot;
            isShowingAlert = true
            return
        }

        pickerSource = sourceType
        isShowingPicker = true
    }
}

// UIKit의 UIImagePickerController를 SwiftUI에서 sheet로 띄우기 위한 래퍼입니다.
struct ImagePicker: UIViewControllerRepresentable {
    let sourceType: UIImagePickerController.SourceType
    @Binding var selectedImage: UIImage?
    @Environment(\.dismiss) private var dismiss

    func makeUIViewController(context: Context) -&amp;gt; UIImagePickerController {
        let picker = UIImagePickerController()
        picker.sourceType = sourceType
        // 이미지 선택/촬영만 다루도록 mediaTypes를 이미지로 제한합니다.
        picker.mediaTypes = [UTType.image.identifier]
        picker.allowsEditing = false
        picker.delegate = context.coordinator
        return picker
    }

    func updateUIViewController(
        _ uiViewController: UIImagePickerController,
        context: Context
    ) {
    }

    func makeCoordinator() -&amp;gt; Coordinator {
        Coordinator(selectedImage: $selectedImage, dismiss: dismiss)
    }

    final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
        @Binding private var selectedImage: UIImage?
        private let dismiss: DismissAction

        init(selectedImage: Binding&amp;lt;UIImage?&amp;gt;, dismiss: DismissAction) {
            _selectedImage = selectedImage
            self.dismiss = dismiss
        }

        func imagePickerController(
            _ picker: UIImagePickerController,
            didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]
        ) {
            // 선택 결과에서 원본 이미지를 꺼내 SwiftUI 상태값에 전달합니다.
            selectedImage = info[.originalImage] as? UIImage
            dismiss()
        }

        func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
            dismiss()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI에서는 &lt;code&gt;UIImagePickerController&lt;/code&gt;를 직접 사용할 수 없기 때문에 &lt;code&gt;UIViewControllerRepresentable&lt;/code&gt;로 감싸서 사용했습니다. 버튼을 누르면 카메라 또는 사진첩을 &lt;code&gt;sourceType&lt;/code&gt;으로 지정해 picker를 띄우고, 선택이 끝나면 delegate 메서드에서 &lt;code&gt;originalImage&lt;/code&gt;를 가져와 SwiftUI의 &lt;code&gt;@State&lt;/code&gt;값에 저장합니다. 저장된 이미지는 &lt;code&gt;Image(uiImage:)&lt;/code&gt;로 화면에 표시합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스크린샷&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;2622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UydCq/dJMb99NcbTc/sBWXXmWIiqp2FdMg0uqnek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UydCq/dJMb99NcbTc/sBWXXmWIiqp2FdMg0uqnek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UydCq/dJMb99NcbTc/sBWXXmWIiqp2FdMg0uqnek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUydCq%2FdJMb99NcbTc%2FsBWXXmWIiqp2FdMg0uqnek%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;500&quot; height=&quot;1087&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;2622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAy193/dJMcahdrvEP/bgKJUO71hxauYwqIMENJL0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAy193/dJMcahdrvEP/bgKJUO71hxauYwqIMENJL0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAy193/dJMcahdrvEP/bgKJUO71hxauYwqIMENJL0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAy193%2FdJMcahdrvEP%2FbgKJUO71hxauYwqIMENJL0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;1087&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;2622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xfhHB/dJMb99NcbTh/O2gGmjizH9YTiCr56C0XIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xfhHB/dJMb99NcbTh/O2gGmjizH9YTiCr56C0XIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xfhHB/dJMb99NcbTh/O2gGmjizH9YTiCr56C0XIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxfhHB%2FdJMb99NcbTh%2FO2gGmjizH9YTiCr56C0XIk%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;500&quot; height=&quot;1087&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;2622&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>swiftUI</category>
      <category>Swift</category>
      <category>SwiftUI</category>
      <category>UIImagePickerController</category>
      <author>GwanSon</author>
      <guid isPermaLink="true">https://gwanson-dev.tistory.com/121</guid>
      <comments>https://gwanson-dev.tistory.com/121#entry121comment</comments>
      <pubDate>Thu, 21 May 2026 14:52:30 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] cached_network_image에 대한 짧막한 설명</title>
      <link>https://gwanson-dev.tistory.com/119</link>
      <description>&lt;h2&gt;cached_network_image란?&lt;/h2&gt;
&lt;p&gt;cached_network_image는 인터넷 서버(URL)에 있는 이미지를 불러와서 화면에 보여주고, 스마트폰의 로컬 저장소(캐시)에 자동으로 임시 저장해 주는 Flutter의 필수 UI 패키지입니다. Flutter의 기본 위젯인 &lt;code&gt;Image.network()&lt;/code&gt;는 이미지를 불러올 수는 있지만, 앱을 껐다 켜거나 화면을 다시 그릴 때마다 매번 인터넷에서 새로 다운로드를 받습니다. 이 패키지를 사용하면 데이터 낭비를 막고, 로딩 속도를 획기적으로 올리며, 로딩 중 스피너나 실패 시 에러 화면까지 처리할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;설치&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;dependencies:
    flutter:
        sdk: flutter
    cached_network_image: ^3.3.1&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;사용&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &amp;#39;package:flutter/material.dart&amp;#39;;
import &amp;#39;package:cached_network_image/cached_network_image.dart&amp;#39;;

class ProfileImageWidget extends StatelessWidget {
    final String imageUrl;

    const ProfileImageWidget({Key? key, required this.imageUrl}) : super(key: key);

    @override
    Widget build(BuildContext context) {
        return CachedNetworkImage(
            imageUrl: imageUrl, // 1. 네트워크 이미지 URL 주소

            // 2. 이미지를 다운로드하는 동안 보여줄 위젯 (로딩 스피너)
            placeholder: (context, url) =&amp;gt; const Center(
                child: CircularProgressIndicator(),
            ),

            // 3. 주소가 잘못되었거나 네트워크 에러로 로딩 실패 시 보여줄 위젯
            errorWidget: (context, url, error) =&amp;gt; const Center(
                child: Icon(Icons.error, color: Colors.red),
            ),

            // 4. (선택) 이미지 크기 맞춤 설정
            fit: BoxFit.cover,
            width: 100,
            height: 100,
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Swift와 비교&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flutter(&lt;code&gt;cached_network_image&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Swift(&lt;code&gt;KingFisher&lt;/code&gt;, &lt;code&gt;SDWebImage&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;기본 &lt;code&gt;Image.network&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Data(contentsOf: url)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;캐싱 없이 매번 통신해서 이미지를 그리는 원시적인 방법&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CachedNetworkImage&lt;/code&gt; 위젯&lt;/td&gt;
&lt;td&gt;&lt;code&gt;KFImage(url)&lt;/code&gt;/&lt;code&gt;UIImageView.kf.setImage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;URL을 주면 알아서 캐시 확인 후 그려주는 뷰&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;디스크(Disk)/메모리 캐싱&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ImageCache.default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;내부적으로 이미지를 RAM과 저장소(앱 샌드박스)에 임시 저장하는 동작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;placeholder&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;placeholder&lt;/code&gt; 파라미터&lt;/td&gt;
&lt;td&gt;이미지를 받아오는 동안 임시로 보여주는 회색 박스나 스피너&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;errorWidget&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;failureImage&lt;/code&gt;/&lt;code&gt;catch&lt;/code&gt; 핸들러&lt;/td&gt;
&lt;td&gt;주소가 깨지거나 다운로드 실패 시 보여줄 기본 이미지(Fallback)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SwiftUI 기본 내장&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AsyncImage(url:)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;최근 iOS에 기본 내장되었지만 여전히 세밀한 캐싱은 KingFisher를 씀&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;사용 흐름&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;위젯 렌더링 &amp;amp; URL 요청 : 화면에 &lt;code&gt;CachedNetworkImage&lt;/code&gt;가 등장하면 패키지는 전달받은 URL 문자열을 키값으로 사용합니다.&lt;/li&gt;
&lt;li&gt;캐시 유무 확인 : 서버에 요청을 보내기 전에, 스마트폰의 메모리나 저장소에 똑같은 URL로 저장해 둔 이미지 파일이 있는지 검사합니다.&lt;/li&gt;
&lt;li&gt;로딩 UI 표시(placeholder) : 캐시에 없다면 인터넷에서 받아와야 하므로, 다운로드가 완료될 때까지 placeholder 위젯을 화면에 보여줍니다.&lt;/li&gt;
&lt;li&gt;다운로드 및 캐시 저장 : 서버에서 이미지 수신이 완료되면, 나중에 또 쓸 수 있도록 기기에 임시 파일을 저장해 두고 메모리에 올려놓습니다.&lt;/li&gt;
&lt;li&gt;완료 및 에러 핸들링 : 정상이면 이미지를 화면에 띄우고(&lt;code&gt;fit&lt;/code&gt; 옵션에 따라 크기 조절), 통신이 끊겼거나 주소가 이상하면 &lt;code&gt;errorWidget&lt;/code&gt;을 띄워 앱이 튕기거나 멈추는 것을 방지합니다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Flutter</category>
      <category>cached_network_image</category>
      <category>Flutter</category>
      <author>GwanSon</author>
      <guid isPermaLink="true">https://gwanson-dev.tistory.com/119</guid>
      <comments>https://gwanson-dev.tistory.com/119#entry119comment</comments>
      <pubDate>Wed, 11 Mar 2026 16:32:37 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] dio 패키지에 대한 짧막한 설명</title>
      <link>https://gwanson-dev.tistory.com/118</link>
      <description>&lt;h2&gt;dio란?&lt;/h2&gt;
&lt;p&gt;dio는 Flutter와 Dart 환경에서 가장 널리 사용되는 HTTP 네트워킹 패키지입니다. 기본적인 웹 요철뿐만 아니라, 인터셉터(Interceptor), 파라미터 글로벌 설정(BaseURL 등), 파일 다운로드/업로드, 타임아웃 처리, 요청 취소 등 복잡한 네트워크 작업을 매우 적은 코드로 쉽게 처리할 수 있게 해주는 통신 라이브러리입니다.&lt;/p&gt;
&lt;h2&gt;기존 &lt;code&gt;http&lt;/code&gt; 패키지와의 차이점&lt;/h2&gt;
&lt;p&gt;Flutter에서 API 통신을 할 때 보통 기본 &lt;code&gt;http&lt;/code&gt; 패키지로 시작하지만, 앱이 커지면 거의 대부분 &lt;code&gt;dio&lt;/code&gt;로 넘어갑니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;기본 &lt;code&gt;http&lt;/code&gt;패키지&lt;/th&gt;
&lt;th&gt;&lt;code&gt;dio&lt;/code&gt;패키지&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;복잡도&lt;/td&gt;
&lt;td&gt;가볍고 단순한 통신에 적합&lt;/td&gt;
&lt;td&gt;기능이 많고 엔터프라이즈급 앱에 적합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON 파싱&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jsonDecode()&lt;/code&gt;로 직접 변환해야 함&lt;/td&gt;
&lt;td&gt;응답을 자동으로 JSON₩ 변환해줌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인터셉터(Interceptor)&lt;/td&gt;
&lt;td&gt;직접 구현해야 해서 매우 번거로움&lt;/td&gt;
&lt;td&gt;기본 내장(매 요청/응답 지점마다 가로채기 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;공통 설정(BaseURL 등)&lt;/td&gt;
&lt;td&gt;매번 수동으로 주소를 합쳐서 호출해야 함&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BaseOptions&lt;/code&gt;에 등록해두면 알아서 붙음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;파일 업로드(FormData)&lt;/td&gt;
&lt;td&gt;복잡한 코드를 직접 작성해야 함&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FormData.fromMap()&lt;/code&gt;으로 한줄 컷 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;요청 취소 및 타임아웃&lt;/td&gt;
&lt;td&gt;구현이 까다롭거나 제한적임&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CancelToken&lt;/code&gt;및 속성 제공으로 매우 쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;요약: &lt;code&gt;http&lt;/code&gt;패키지는 순정 기본 도구라면, &lt;code&gt;dio&lt;/code&gt;는 자동화 옵션이 잔뜩 달린 풀옵션 도구입니다. 토큰을 헤더에 넣어서 관리하는 로그인/인증 로직이 있다면 무조건 dio를 쓰는 것이 정신건강에 좋습니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;설치&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;dependencies:
    flutter:
        sdk: flutter
    dio: ^5.4.3&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;사용&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;전역 설정 및 인터셉터 추가(보통 싱글톤이나 Provider로 관리)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &amp;#39;package:dio/dio.dart&amp;#39;;

// 1. 기본 설정 (BaseOptions)
final options = BaseOptions(
    baseUrl: &amp;#39;https://api.example.com&amp;#39;,
    connectTimeout: const Duration(seconds: 5), // 5초동안 응답 없으면 에러
    receiveTimeout: const Duration(seconds: 3),
);

final dio = Dio(options)

// 2. 인터셉터 추가 (모든 통신 전에 토큰 검사/에러 로그 출력 등)
dio.interceptors.add(
    InterceptorsWrapper(
        onRequest: (options, handler) {
            // 모든 API 요청을 보내기 직전에 실행됨 (예: 헤더에 토큰 넣기)
            options.headers[&amp;#39;Authorization&amp;#39;] = &amp;#39;Bearer 유저토큰1234&amp;#39;;
            return handler.next(options);
        },
        onResponse: (response, handler) {
            // 서버에서 정상 응답이 왔을 때
            return handler.next(response);
        },
        onError: (DioException e, handler) {
            // 에러가 났을 때 (예: 토큰 만료 시 자동 재발급 로직 실행)
            return handler.next(e);
        },
    ),
);&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;API 요청하기(기본 호출 &amp;amp; 자동 JSON 파싱)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// GET 요청 (http와 달리 jsonDecoder를 안 써도 이미 Map으로 들어옴)
final response = await dio.get(&amp;#39;/users/1&amp;#39;);
print(response.data[&amp;#39;name&amp;#39;]);

// POST 요청 (Body 데이터를 직관적으로 보냄)
final postResponse = await dio.post(
  &amp;#39;/login&amp;#39;,
  data: {&amp;#39;id&amp;#39;: &amp;#39;user1&amp;#39;, &amp;#39;password&amp;#39;: &amp;#39;123&amp;#39;},
);&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Swift와 비교&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flutter(dio)&lt;/th&gt;
&lt;th&gt;Swift(Alamofire/URLSession)&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;기본 &lt;code&gt;http&lt;/code&gt;패키지&lt;/td&gt;
&lt;td&gt;&lt;code&gt;URLSession&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;언어에서 기본 제공하는 순정 네트워크 클래스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dio&lt;/code&gt;패키지&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Alamofire&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;보일러플레이트를 줄여주는 강력한 서드파티 라이브러리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BaseOptions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;URLSessionConfiguration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;타임아웃, 중복 헤더, BaseURL 등 전역 환경 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Interceptor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RequestInterceptor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;통신을 쏘기 직전이나 받기 직전에 코드를 가로채는 역할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FormData&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MultipartFormData&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이미지 다운로드/업로드 등 큰 파일을 보낼 때 쓰는 방식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;자동 JSON 파싱&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Codable&lt;/code&gt; 프로토콜 기반 디코딩&lt;/td&gt;
&lt;td&gt;응답으로 온 데이터를 별도 변환 없이 바로 딕셔너리로 쓸 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;사용 흐름&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;dio&lt;/code&gt; 인스턴스 생성 : 앱 생명주기 내내 재사용할 전역 &lt;code&gt;dio&lt;/code&gt; 객체를 만듭니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BaseOptions&lt;/code&gt; 부여 : 앱 서버의 고정된 루트 도메인(&lt;code&gt;baseUrl&lt;/code&gt;)이나 공통 타임아웃 제한 시간을 설정하여 매번 적는 수고를 덜어줍니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Interceptor&lt;/code&gt; 장착 :&lt;ul&gt;
&lt;li&gt;로그인 토큰 주입 : &lt;code&gt;onRequest&lt;/code&gt;에서 늘 헤더에 최신 토큰을 꽂아줍니다.&lt;/li&gt;
&lt;li&gt;토큰 만료 재발급 : &lt;code&gt;onError&lt;/code&gt;에서 서버가 401(인증 에러)을 내려보내면, 토큰을 갱신하고 실패했던 통신을 다시 진행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;API 호출 방식(Read/Write) : API 명세서에 맞춰 &lt;code&gt;dio.get()&lt;/code&gt;, &lt;code&gt;dio.post()&lt;/code&gt;, &lt;code&gt;dio.patch()&lt;/code&gt;, &lt;code&gt;dio.delete()&lt;/code&gt; 메서드를 호출합니다.&lt;/li&gt;
&lt;li&gt;모델 매치(JSON -&amp;gt; Dart Object) : 응답받은 &lt;code&gt;response.data&lt;/code&gt;(보통 &lt;code&gt;Map&amp;lt;String, dynamic&amp;gt;&lt;/code&gt;)코드를 모델 클래스의 &lt;code&gt;fromJson()&lt;/code&gt; 공장에 넣어서 다트 객체로 바꿔 화면에 뿌려줍니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;간단히 요약하자면 Flutter의 &lt;code&gt;dio&lt;/code&gt;패키지는 Swift에서 &lt;code&gt;URLSession&lt;/code&gt;을 쓰기 귀찮아서 &lt;code&gt;Alamofire&lt;/code&gt;를 사용하는거구나 라고 이해하시면 됩니다.&lt;/p&gt;</description>
      <category>Flutter</category>
      <category>Dio</category>
      <category>Flutter</category>
      <author>GwanSon</author>
      <guid isPermaLink="true">https://gwanson-dev.tistory.com/118</guid>
      <comments>https://gwanson-dev.tistory.com/118#entry118comment</comments>
      <pubDate>Wed, 11 Mar 2026 12:32:25 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] go_router 패키지에 대한 짧막한 설명</title>
      <link>https://gwanson-dev.tistory.com/117</link>
      <description>&lt;h2&gt;go_router란?&lt;/h2&gt;
&lt;p&gt;go_router는 Flutter 공식에서 관리하고 강력하게 추천하는 딥링크 및 선언적 라우팅(Navigator 2.0) 패키지입니다. 기존 Flutter의 기본 라우팅(&lt;code&gt;Navigator.push&lt;/code&gt;) 방식은 화면이 복잡해질수록 관리하기 어렵고 딥링크(웹 URL 또는 외부 링크로 특정 화면 들어가기) 처리가 매우 까다로웠습니다. go_router는 화면 경로를 URL(예: &lt;code&gt;/home/details/1&lt;/code&gt;) 형식으로 직관적으로 관리하며, 권한 트래픽(로그인 안 한 유저 로그인 창으로 보내기 리다이렉트 기능) 제어나 중첩 라우팅(Nested Routing)을 매우 쉽게 구현할 수 있도록 도와줍니다.&lt;/p&gt;
&lt;h2&gt;설치&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;dependencies:
    fluuter:
        sdk: flutter
    go_router: ^13.2.0&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;사용&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;라우터 설정 (보통 별도의 파일 &lt;code&gt;router.dart&lt;/code&gt;에 분리합니다.)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &amp;#39;package:flutter/material.dart&amp;#39;;
import &amp;#39;package:go_router/go_router.dart&amp;#39;;

// 1. 라우터 인스턴스 생성
final GoRouter routerConfig = GoRouter(
    initialLocation: &amp;#39;/&amp;#39;, // 앱 켰을 때 가장 먼저 보여줄 경로
    routes: [
        GoRoute(
            path: &amp;#39;/&amp;#39;,
            builder: (context, state) =&amp;gt; const HomeScreen(),
        ),
        GoRoute(
            path: &amp;#39;/detail/:id&amp;#39;, // 동적 파라미터 받기 가능
            builder: (context, state) {
                final id = state.pathParameters[&amp;#39;id&amp;#39;]; // 전달받은 데이터 꺼내기
                return DetaulScreen(id: id);
            },
        ),
    ],
);&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;앱(최상단)에 라우터 연결하기 (&lt;code&gt;main.dart&lt;/code&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// 기존 MaterialApp 대신 MaterualApp.router 사용
class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
        return MaterialApp.router(
            routerConfig: routerConfig, // 위에서 만든 라우터 주입
            title: &amp;#39;Go Router 예제&amp;#39;
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;화면 이동하기 (Navigation) &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// 1. 스택에 화면 쌓기 (일반적인 이동, 뒤로 가기 생성됨)
context.push(&amp;#39;detail/123&amp;#39;);
// 2. 해당 경로로 아예 이동해버리기 (주로 웹이나 딥링크 기반 이동)
context.go(&amp;#39;/detail/123&amp;#39;);
// 3. 뒤로 가기
context.pop();&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Swift와 비교&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flutter(go_router)&lt;/th&gt;
&lt;th&gt;Swift(SwiftUI/UIKit)&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GoRouter&lt;/code&gt;인스턴스&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NavigationStack&lt;/code&gt;최상위 세팅/Coordinator 패턴&lt;/td&gt;
&lt;td&gt;앱 전체의 이동 가능한 화면(경로)들의 지도를 그리는 곳&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;path:&amp;#39;/detail&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;navigationDestination(for:)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;특정 화면과 URL 경로를 매핑(연결)하는 라우트 정의&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;context.push()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NavigationLink&lt;/code&gt;/&lt;code&gt;pushViewController&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;뷰 계층 맨 위에 새로운 화면을 얹음(일반적인 네비게이션)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;context.go()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;딥링크 처리 로직/Root View 교체&lt;/td&gt;
&lt;td&gt;선언된 URL 트리 구조대로 강제로 이동(스택을 새로 구성하기도 함)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;context.pop()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dismiss&lt;/code&gt;/&lt;code&gt;popViewController&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 화면을 닫고 이전 화면(아래 계층)으로 돌아감&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redirect&lt;/code&gt; 옵션&lt;/td&gt;
&lt;td&gt;SwiftUI &lt;code&gt;onAppear&lt;/code&gt; 분기처리/Auth Guard&lt;/td&gt;
&lt;td&gt;로그인 여부에 따라 진입을 차단하고 다른 화면으로 튕기게 하는 기능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;핵심 동작&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;GoRouter&lt;/code&gt;인스턴스 선언 : 앱 내의 모든 화면을 &lt;code&gt;path&lt;/code&gt;(문자열 URL 형태)와 짝지어 &lt;code&gt;routes&lt;/code&gt;배열 안에 정의합니다. 보통 라우터 파일(&lt;code&gt;router.dart&lt;/code&gt; 등)을 따로 빼서 관리합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MaterialApp.router&lt;/code&gt; 등록 : 앱의 최상위 위젯을 &lt;code&gt;MaterialApp&lt;/code&gt;에서 &lt;code&gt;MaterialApp.router&lt;/code&gt;로 변경하고 만든 라우터를 집어넣습니다.&lt;/li&gt;
&lt;li&gt;네비게이션 액션(&lt;code&gt;push&lt;/code&gt; vs &lt;code&gt;go&lt;/code&gt;) :&lt;ul&gt;
&lt;li&gt;&lt;code&gt;push()&lt;/code&gt; : 기존 화면을 놔두고 그 위에 새 화면을 쌓을 때 사용합니다. 모바일 앱에서 흔하게 쓰는 &amp;#39;상세 화면 들어가기&amp;#39; 입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;go()&lt;/code&gt; : 특정 주소로 이동할 때 사용합니다. 만약 중첩된 라우터(부모-자식 경로) 구조라면 go_router가 알아서 뒤로 가기 버튼 스택까지 계산해 생성해줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;파라미터 전달 : URL에 &lt;code&gt;:id&lt;/code&gt;형태로 변수를 주입하거나, 데이터 개겣 자체를 &lt;code&gt;extra&lt;/code&gt;파라미터에 담아 다음 화면으로 넘길 수 있습니다.&lt;/li&gt;
&lt;li&gt;리다이렉트 처리 : &lt;code&gt;GoRouter(redirect: (context, state) { ... })&lt;/code&gt; 옵션을 통해, 사용자가 특정 화면에 접근할 때 &amp;quot;현재 로그인 상태인가?&amp;quot;를 확인하여 로그인 창으로 강제 이동시키는 등의 강력한 분기 처리를 구현합니다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Flutter</category>
      <category>Flutter</category>
      <category>go_router</category>
      <author>GwanSon</author>
      <guid isPermaLink="true">https://gwanson-dev.tistory.com/117</guid>
      <comments>https://gwanson-dev.tistory.com/117#entry117comment</comments>
      <pubDate>Tue, 10 Mar 2026 14:32:06 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] Isar 패키지에 대한 짧막한 설명</title>
      <link>https://gwanson-dev.tistory.com/116</link>
      <description>&lt;h2&gt;isar란?&lt;/h2&gt;
&lt;p&gt;isar는 Flutter와 Dart 전용으로 설계된 고속 크로스플랫폼 NoSQL 로컬 데이터베이스입니다. 내부 코어 엔진이 Rust로 작성되어 있어 처리 속도가 압도적으로 빠르며, 모든 작업이 비동기로 처리되어 앱의 UI가 멈추는 현상을 방지합니다. 또한 강력한 타입 안정성을 지원하여 컴파일 시점에 에러를 방지할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;고속 &amp;amp; 비동기 처리 : 내부 코어 엔진이 Rust로 작성되어 있어 속도가 압도적으로 빠릅니다. 게다가 모든 쿼리와 작업이 완전히 비동기로 처리되어 있어 앱의 UI가 버벅거리지 않습니다.&lt;/li&gt;
&lt;li&gt;타입 안정성(Type-safe) : Realm이나 SwiftData처럼, 모델 클래스만 정의해두면 컴파일러가 강력하게 타입을 체크해줍니다.&lt;/li&gt;
&lt;li&gt;반응형 : 쿼리 결과에 &lt;code&gt;.watch()&lt;/code&gt;를 붙이면 데이터가 변경될 때마다 자동으로 스트림(Stream)을 쏴줍니다. 데이터를 직접 갱신할 필요없이 UI가 알아서 바뀝니다.&lt;/li&gt;
&lt;li&gt;코드 제너레이션(Code Generation) : 앱빌드 시점에 모델 구조를 보고 효율적인 내부 코드를 싹 다 자동으로 만들어줍니다.(&lt;code&gt;build_runner&lt;/code&gt; 사용)&lt;/li&gt;
&lt;li&gt;탁월한 검색 기능 : 풀텍스트 검색, 다중 조건 필터링 등이 매우 쉽고 강력합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;설치&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;dependencies:
    flutter:
        sdk: flutter
    isar: ^3.1.0
    isar_flutter_libs: ^3.1.0
    path_provider: ^2.1.1

dev_dependencies:
    build_runner: ^2.4.6
    isar_geneartor: ^3.1.0&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;사용&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;스키마(모델) 정의하기 (&lt;code&gt;user.dart&lt;/code&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &amp;#39;package:isar/isar.dart&amp;#39;;

part &amp;#39;user.g.dart&amp;#39; // 반드시 &amp;#39;파일명.g.dart&amp;#39;로 선언해야 자동 생성이 가능

@Collection()
class User {
    Id id = Isar.autoIncrement; // 자동 증가하는 고유 ID (Primary Key)

    late String name;
    short? age;
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;터미널에서 &lt;code&gt;flutter pub run build_runner build&lt;/code&gt; 명령어를 실행하면 &lt;code&gt;user.g.dart&lt;/code&gt; 파일이 생성됩니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;초기화 및 데이터 저장하기&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &amp;#39;package:path_provider/path_provider.dart&amp;#39;;

// 1. DB 인스턴스 열기
final dir = await getApplicationDocumentsDirectory();
final isar = await Isar.open(
    [UserSchema], // 생성한 스키마 등록
    directory: dir.path,
);

// 2. 새 유저 객체 생성
final newUser = User()
    ..name = &amp;quot;홍길동&amp;quot;
    ..age = 28;

// 3. 쓰기 작업 (반드시 writeTxn 트랜잭션 안에서 실행)
await isar.writeTxn(() async {
    await isar.users.put(newUser);
});

// 4. 데이터 읽기 (전체 가져오기)
final allUsers = await isar.users.where().findAll();&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Swift와 비교&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flutter(isar)&lt;/th&gt;
&lt;th&gt;Swift(CoreData, Realm, SwiftData)&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Collection&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Model&lt;/code&gt;/&lt;code&gt;Object&lt;/code&gt; 서브클래싱/Entity&lt;/td&gt;
&lt;td&gt;DB에 저장될 하나의 테이블(객체) 선언&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Attribute(.unique)&lt;/code&gt;/&lt;code&gt;UUID()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;각 요소를 구분하는 고유 식별자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Isar.open()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ModelContainer&lt;/code&gt;/&lt;code&gt;Realm()&lt;/code&gt;생성&lt;/td&gt;
&lt;td&gt;DB 파일을 열고 연결(인스턴스화)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;isar.writeTxn()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;context.save()&lt;/code&gt;/&lt;code&gt;realm.write&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;데이터를 쓰고/수정/삭제하는 트랜잭션 블록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.watch()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Query&lt;/code&gt;/&lt;code&gt;@ObservedResults&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DB의 데이터 변화를 감지하여 UI 자동 갱신&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;사용 흐름&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;모델 클래스 생성(&lt;code&gt;@Collection&lt;/code&gt;) : 저장할 데이터의 형태를 다트 클래스로 정의합니다.&lt;/li&gt;
&lt;li&gt;코드 제너레이팅(&lt;code&gt;build_runner&lt;/code&gt;) : 명령어를 실행하여 isar가 쿼리 및 파싱에 사용할 보일러플레이트 코드(&lt;code&gt;*.g.dart&lt;/code&gt;)를 자동 생성합니다. (모델이 수정될 때마다 실행 필수)&lt;/li&gt;
&lt;li&gt;DB 인스턴스 초기화 (&lt;code&gt;Isar.open&lt;/code&gt;) : 앱 시작 지점(주로 &lt;code&gt;main()&lt;/code&gt; 내부 또는 상태관리자 초기화 시점)에서 로컬 디렉토리 경로에 DB를 연결합니다.&lt;/li&gt;
&lt;li&gt;비동기 트랜잭션 (&lt;code&gt;writeTxn&lt;/code&gt;) : 데이터의 삽입(&lt;code&gt;put&lt;/code&gt;), 수정(&lt;code&gt;put&lt;/code&gt;), 삭제(&lt;code&gt;delete&lt;/code&gt;) 등 쓰는 작업은 반드시 트랜잭션 블록 내부에서 비동기(&lt;code&gt;await&lt;/code&gt;)로 처리하여 무결성을 유지합니다.&lt;/li&gt;
&lt;li&gt;반응형 스트림 감지 (&lt;code&gt;.watch&lt;/code&gt;) : 쿼리 결과에 &lt;code&gt;.watch()&lt;/code&gt;를 사용하여 데이터 변경이 일어날 때마다 Stream으로 변경 사항을 받아 UI를 자동으로 다시 그립니다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Flutter</category>
      <category>fluuter</category>
      <category>ISAR</category>
      <author>GwanSon</author>
      <guid isPermaLink="true">https://gwanson-dev.tistory.com/116</guid>
      <comments>https://gwanson-dev.tistory.com/116#entry116comment</comments>
      <pubDate>Tue, 10 Mar 2026 13:52:48 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] Riverpod 패키지에 대한 짧막한 설명</title>
      <link>https://gwanson-dev.tistory.com/115</link>
      <description>&lt;h2&gt;Riverpod이란?&lt;/h2&gt;
&lt;p&gt;원래 Flutter에는 &lt;code&gt;Provider&lt;/code&gt;라는 공식 추천 상태 관리 라이브러리가 있습니다. 하지만 &lt;code&gt;Provider&lt;/code&gt;는 위젯 트리(BuildContext)에 강하게 종속되어 있어, 트리가 복잡해지면 에러가 런타임에 발생하는 치명적인 단점이 존재했습니다.&lt;/p&gt;
&lt;p&gt;이를 해결하기 위해 만든 것이 &lt;strong&gt;Riverpod&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;장점 1: 컴파일 타임에 안전합니다. (앱 실행 전에 에러를 잡을 수 있음)&lt;/li&gt;
&lt;li&gt;장점 2: Flutter 위젯 트리(BuildContext)에 의존하지 않고 어디서든 상태를 읽을 수 있습니다.&lt;/li&gt;
&lt;li&gt;장점 3: 캐싱과 비동기(API 통신 등) 처리가 내장되어 있어 매우 강력합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;설치&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;name: my_app_name
environment:
    sdk: ^3.7.0
    flutter: &amp;quot;&amp;gt;=3.0.0&amp;quot;

dependencies:
    flutter:
        sdk: flutter
    flutter_riverpod: ^3.2.1&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;사용&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &amp;#39;package:flutter/material.dart&amp;#39;;  
import &amp;#39;package:flutter_riverpod/flutter_riverpod.dart&amp;#39;;  

// 값을 저장할 &amp;quot;provider&amp;quot;를 생성합니다(여기서는 &amp;quot;Hello world&amp;quot;).  
// provider를 사용하면 노출된 값을 모의(mock)//재정의(override)할 수 있습니다.  
final helloWorldProvider = Provider((_) =&amp;gt; &amp;#39;Hello world&amp;#39;);  

void main() {  
    runApp(  
    // 위젯이 providers를 읽을 수 있게 하려면 전체 애플리케이션을 &amp;quot;ProviderScope&amp;quot; 위젯으로 감싸야 합니다.  
    // 여기에 providers의 상태가 저장됩니다.  
    ProviderScope(  
        child: MyApp(),  
        ),  
    );  
}  

// Riverpod에 의해 노출되는 StatelessWidget 대신 ConsumerWidget을 확장합니다.  
class MyApp extends ConsumerWidget {  
    @override  
    Widget build(BuildContext context, WidgetRef ref) {  
        final String value = ref.watch(helloWorldProvider);  

        return MaterialApp(  
            home: Scaffold(  
                appBar: AppBar(title: const Text(&amp;#39;Example&amp;#39;)),  
                body: Center(  
                    child: Text(value),  
                ),  
            ),  
        );  
    }  
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Swift와 비교&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flutter(Riverpod)&lt;/th&gt;
&lt;th&gt;SwiftUI(Combine)&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;ProviderScope&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@main&lt;/code&gt;안의 최상위 환경 세팅&lt;/td&gt;
&lt;td&gt;상태들을 담아두는 거대한 전역 컨테이너&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@StateObject, ObservableObject&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;상태(데이터)를 정의하고 관리하는 객체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WidgetRef(ref)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@EnvironmentObject&lt;/code&gt;나 객체 접근자&lt;/td&gt;
&lt;td&gt;위젯에서 상태를 읽어오기 위한 다리 역할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ref.watch()&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@ObservedObject&lt;/code&gt;/&lt;code&gt;@Published&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;상태 값이 변하면 UI를 자동으로 다시 그림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ref.read()&lt;/td&gt;
&lt;td&gt;버튼 액션 안에서 객체 메서드 호출&lt;/td&gt;
&lt;td&gt;UI를 다시 그리지 않고 값만 일회성으로 읽거나 함수 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;Riverpod의 3가지 핵심 동작&lt;/h2&gt;
&lt;p&gt;Riverpod에서 데이터를 사용할 때는 &lt;code&gt;ref&lt;/code&gt;라는 객체를 통해 다음 3가지 동작을 합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;ref.watch(provider)&lt;/code&gt; : 데이터를 구독합니다. 값이 바뀌면 화면을 새로고침 합니다. 주로 UI를 그릴 때 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ref.read(provider.notifier).method()&lt;/code&gt; : 데이터를 일회성으로 읽고 맙니다. 값이 바뀌어도 화면을 갱신하지 않습니다. 주로 버튼 클릭 이벤트, API 호출 함수 등을 실행할 때 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ref.listen(provider, (prev, next))&lt;/code&gt; : 데이터가 바뀔 때 UI 트리를 다시 그리는 대신, 특정 액션만 취합니다.(예: 에러가 나면 알림창(SnackBar) 띄우기, 화면 이동하기) iOS의 Combine &lt;code&gt;sink&lt;/code&gt;나 SwiftUI의 &lt;code&gt;.onChange()&lt;/code&gt;와 같습니다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Flutter</category>
      <category>Flutter</category>
      <category>Riverpod</category>
      <author>GwanSon</author>
      <guid isPermaLink="true">https://gwanson-dev.tistory.com/115</guid>
      <comments>https://gwanson-dev.tistory.com/115#entry115comment</comments>
      <pubDate>Tue, 10 Mar 2026 12:55:50 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] Provider</title>
      <link>https://gwanson-dev.tistory.com/114</link>
      <description>&lt;h1&gt;Flutter Provider&lt;/h1&gt;
&lt;h2&gt;Provider란?&lt;/h2&gt;
&lt;p&gt;Flutter 앱에서 여러 위젯 간에 데이터를 공유하고, 상태 변경을 효율적으로 관리할 수 있게 해주는 패키지가 바로 Provider입니다.&lt;/p&gt;
&lt;p&gt;Provider는 Flutter 공식 팀에서 권장하는 상태 관리 솔루션으로, ChangeNotifier를 활용한 간단하면서도 강력한 상태 관리 방식을 제공합니다. MVVM 패턴 구현에 필수적이며, 의존성 주입과 상태 공유를 깔끔하게 처리할 수 있습니다.&lt;/p&gt;
&lt;p&gt;Provider는 Flutter의 상태 관리 패키지로, InheritedWidget을 기반으로 만들어졌습니다. 복잡한 InheritedWidget의 사용을 단순화하고, ChangeNotifier와 결합하여 반응형 상태 관리를 가능하게 합니다.&lt;/p&gt;
&lt;h3&gt;주요 특징&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;직관적이고 사용하기 쉬운 API를 제공합니다.&lt;/li&gt;
&lt;li&gt;필요한 위젯만 리빌드하여 성능 최적화를 진행합니다.&lt;/li&gt;
&lt;li&gt;깔끔한 의존성 주입 패턴을 지원합니다.&lt;/li&gt;
&lt;li&gt;Mock 객체를 쉽게 주입하여 테스트 용이성을 높입니다.&lt;/li&gt;
&lt;li&gt;Flutter 공식에서 권장하는 상태 관리 솔루션입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;왜 Provider를 사용하는가?&lt;/h2&gt;
&lt;h3&gt;1. 기존 방식의 문제점&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// 기존 방식: StatefulWidget의 한계
class CounterWidget extends StatefulWidget {
  @override
  _CounterWidgetState createState() =&amp;gt; _CounterWidgetState();
}

class _CounterWidgetState extends State&amp;lt;CounterWidget&amp;gt; {
  int _counter = 0;

  void _increment() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(&amp;#39;Count: $_counter&amp;#39;),
        ElevatedButton(
          onPressed: _increment,
          child: Text(&amp;#39;Increment&amp;#39;),
        ),
      ],
    );
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식의 문제점:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;상태를 여러 위젯에서 공유하기 어려움&lt;/li&gt;
&lt;li&gt;깊은 위젯 트리에서 상태 전달이 복잡함&lt;/li&gt;
&lt;li&gt;불필요한 위젯 리빌드 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Provider는 기존 방식의 문제점을 해결해줍니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;앱 전체에서 상태를 쉽게 공유할 수 있음&lt;/li&gt;
&lt;li&gt;변경된 부분만 리빌드 가능&lt;/li&gt;
&lt;li&gt;테스트와 유지보수가 쉬움&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;기본 사용법&lt;/h2&gt;
&lt;h3&gt;1. 패키지 설치&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;pubspec.yaml&lt;/code&gt; 파일에 provider 패키지를 추가합니다:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;dependencies:
  flutter:
    sdk: flutter
  provider: ^6.1.5+1&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. ChangeNotifier 생성&lt;/h3&gt;
&lt;p&gt;상태를 관리할 클래스를 생성하고 &lt;code&gt;ChangeNotifier&lt;/code&gt;를 상속받습니다:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &amp;#39;package:flutter/foundation.dart&amp;#39;;

class Counter extends ChangeNotifier {
  int _count = 0;

  int get count =&amp;gt; _count;

  void increment() {
    _count++;
    notifyListeners(); // 상태 변경을 알림
  }

  void decrement() {
    _count--;
    notifyListeners();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Provider 등록&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;main.dart&lt;/code&gt;에서 앱의 최상위에 Provider를 등록합니다:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &amp;#39;package:flutter/material.dart&amp;#39;;
import &amp;#39;package:provider/provider.dart&amp;#39;;
import &amp;#39;counter.dart&amp;#39;;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) =&amp;gt; Counter(),
      child: MaterialApp(
        title: &amp;#39;Provider 예제&amp;#39;,
        home: HomePage(),
      ),
    );
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. Provider 사용&lt;/h3&gt;
&lt;p&gt;위젯에서 Provider를 사용하여 상태에 접근합니다:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(&amp;#39;Provider 예제&amp;#39;),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // Consumer를 사용하여 상태 구독
            Consumer&amp;lt;Counter&amp;gt;(
              builder: (context, counter, child) {
                return Text(
                  &amp;#39;Count: ${counter.count}&amp;#39;,
                  style: TextStyle(fontSize: 24),
                );
              },
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                // Provider를 통해 상태 변경
                Provider.of&amp;lt;Counter&amp;gt;(context, listen: false).increment();
              },
              child: Text(&amp;#39;Increment&amp;#39;),
            ),
            ElevatedButton(
              onPressed: () {
                Provider.of&amp;lt;Counter&amp;gt;(context, listen: false).decrement();
              },
              child: Text(&amp;#39;Decrement&amp;#39;),
            ),
          ],
        ),
      ),
    );
  }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Flutter</category>
      <category>Flutter</category>
      <category>Provider</category>
      <author>GwanSon</author>
      <guid isPermaLink="true">https://gwanson-dev.tistory.com/114</guid>
      <comments>https://gwanson-dev.tistory.com/114#entry114comment</comments>
      <pubDate>Thu, 27 Nov 2025 16:10:03 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] 상위 버전 콤마 제거 이슈 해결 방법</title>
      <link>https://gwanson-dev.tistory.com/113</link>
      <description>&lt;h1&gt;원인&lt;/h1&gt;
&lt;p&gt;Dart 3.7 이상부터 코드 스니펫을 강제로 변경해버렸다고 한다.(업데이트 괜히 했나 싶었음)&lt;/p&gt;
&lt;h1&gt;해결방법&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;analysis_options.yaml&lt;/code&gt;에 해당 코드 작성&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;formatter:
  trailing_commas: preserve&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;콤마 자동 제거 관련 이슈: &lt;a href=&quot;https://github.com/dart-lang/dart_style/issues/1652&quot;&gt;https://github.com/dart-lang/dart_style/issues/1652&lt;/a&gt;&lt;/p&gt;</description>
      <category>Flutter</category>
      <author>GwanSon</author>
      <guid isPermaLink="true">https://gwanson-dev.tistory.com/113</guid>
      <comments>https://gwanson-dev.tistory.com/113#entry113comment</comments>
      <pubDate>Thu, 27 Nov 2025 14:44:37 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] Swift SDK for android 번역문</title>
      <link>https://gwanson-dev.tistory.com/112</link>
      <description>&lt;h1&gt;Android용 Swift SDK 발표&lt;/h1&gt;
&lt;p&gt;Joannis Orlandos&lt;br&gt;2025년 10월 24일&lt;/p&gt;
&lt;p&gt;Swift는 지난 10년 동안 크게 성숙해지며 클라우드 서비스에서 Windows 애플리케이션, 브라우저 앱, 마이크로컨트롤러에 이르기까지 영역을 확장했습니다. Swift는 다양한 종류의 앱과 서비스를 구동하며, 뛰어난 상호 운용성 덕분에 플랫폼 간 코드 공유가 가능합니다.&lt;/p&gt;
&lt;p&gt;Android 워크그룹은 누구나 참여할 수 있는 오픈 그룹으로, Swift를 Android로 확장하는 것을 목표로 합니다. 오늘 우리는 Android용 Swift SDK의 야간(preview) 릴리스를 발표하게 되어 기쁩니다.&lt;/p&gt;
&lt;p&gt;이번 이정표는 수개월 간의 노력과 수년 간 커뮤니티의 풀뿌리 활동 위에 쌓아 올린 결과입니다. 이 SDK를 통해 개발자들은 Swift로 Android 애플리케이션을 개발하기 시작할 수 있으며, 이를 통해 크로스 플랫폼 개발의 새로운 길이 열리고 모바일 생태계 전반의 혁신이 가속화될 것입니다.&lt;/p&gt;
&lt;p&gt;Android용 Swift SDK는 오늘부터 사용할 수 있습니다. Windows 설치 프로그램에 번들되어 제공되며, Linux 또는 macOS에서 사용할 수 있도록 별도로 다운로드할 수도 있습니다.&lt;/p&gt;
&lt;h2&gt;시작하기&lt;/h2&gt;
&lt;p&gt;우리는 Android 기기에서 첫 번째 네이티브 Swift 코드를 설정하는 데 도움이 되는 시작 가이드를 공개했습니다. 또한, Android에서의 엔드 투 엔드 애플리케이션 워크플로를 보여주는 Swift for Android Examples도 함께 제공합니다.&lt;/p&gt;
&lt;p&gt;Android용 Swift SDK를 통해 이제 여러분은 자신의 Swift 패키지를 Android로 포팅하기 시작할 수 있습니다. Swift Package Index에 등록된 패키지의 25% 이상이 이미 Android 빌드를 지원하고 있으며, 커뮤니티 쇼케이스에는 Android 호환 여부가 표시됩니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;swift-java&lt;/code&gt; 프로젝트는 Java와 Swift 간 상호 운용을 가능하게 합니다. 이 프로젝트는 라이브러리이자 코드 생성기 역할을 하여, 안전하고 성능 좋은 바인딩을 자동으로 생성함으로써 양방향으로 Swift와 Java를 통합할 수 있습니다. 비즈니스 로직을 Android로 가져오기 위한 바인딩 생성에 대해 더 알아보려면, 최근 Swift Server Side 밋업에서 Mads Odgaard가 진행한 발표를 확인해 보세요.&lt;/p&gt;
&lt;h2&gt;다음 단계&lt;/h2&gt;
&lt;p&gt;이번 프리뷰 릴리스는 이러한 도구들을 계속 개선할 수 있는 많은 기회를 엽니다. 여러분의 경험, 아이디어, 도구, 앱을 Swift 포럼에서 공유해 주시길 바랍니다. 이 게시물은 관련 토론 스레드와 연결되어 있으며, 새로운 글은 Android 카테고리에 공유할 수 있습니다.&lt;/p&gt;
&lt;p&gt;Android 워크그룹은 현재 검토 중인 비전 문서를 작성하고 있으며, 이는 향후 Android에서의 Swift 작업을 이끌어갈 방향을 제시합니다. 이 비전은 우선 순위 영역을 개괄하고, 생태계 전반의 영향력을 극대화하기 위한 커뮤니티 노력을 안내하게 될 것입니다. 또한 우리는 Android용 Swift SDK의 주요 작업 상태를 추적하는 프로젝트 보드와 공식 CI를 운영하고 있습니다.&lt;/p&gt;
&lt;p&gt;여러분도 우리와 같은 열정을 갖고 있다면, 함께 참여하여 이 생태계를 더욱 발전시켜 주세요!&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;저자&lt;/h2&gt;
&lt;p&gt;Joannis Orlandos&lt;br&gt;Android 워크그룹 의장, 서버 워크그룹 멤버, Hummingbird 유지관리자&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;출처&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;원문: &lt;a href=&quot;https://www.swift.org/blog/nightly-swift-sdk-for-android/&quot;&gt;https://www.swift.org/blog/nightly-swift-sdk-for-android/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;애플이 삼성에게 AI로 뒤쳐지면서 철통같던 폐쇄성을 버리려고 하는 것이 느껴지는 발표인 것 같습니다. 플러터나 리액트 네이티브 같이 하이브리드 애플리케이션 개발이 많이 이루어지고 있어서 다양한 선택지가 될 수도 있을 것 같습니다...만 당장에 사용하기에는 무리가 있어보입니다.&lt;/p&gt;</description>
      <category>iOS</category>
      <category>android</category>
      <category>ios</category>
      <category>SDK</category>
      <category>Swift</category>
      <author>GwanSon</author>
      <guid isPermaLink="true">https://gwanson-dev.tistory.com/112</guid>
      <comments>https://gwanson-dev.tistory.com/112#entry112comment</comments>
      <pubDate>Wed, 29 Oct 2025 15:32:09 +0900</pubDate>
    </item>
  </channel>
</rss>