<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>ao 지식 탄광</title>
    <link>https://ao3921.tistory.com/</link>
    <description>지식 찾아 채광하기</description>
    <language>ko</language>
    <pubDate>Mon, 6 Apr 2026 10:20:16 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>ao</managingEditor>
    <image>
      <title>ao 지식 탄광</title>
      <url>https://tistory1.daumcdn.net/tistory/8619548/attach/0f5db15037d54ee49b03cbe09f536e29</url>
      <link>https://ao3921.tistory.com</link>
    </image>
    <item>
      <title>크로스 플랫폼 프레임워크 비교, Flutter vs React Native</title>
      <link>https://ao3921.tistory.com/5</link>
      <description>&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;React Native의 New Architecture(Fabric, TurboModules, JSI)에 대한 설명을 2026년 기준으로 보강, 기존에 &quot;Bridge를 통해서 네이티브 기능 사용&quot;이라고만 적혀있던 부분이 지금은 구조가 많이 바뀜&lt;/li&gt;
&lt;li&gt;Hermes 엔진에 기본 JS 엔진이 된 점 추가, Flutter 쪽은 Impeller가 Skia를 대체한 현재 상황을 반영&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;699&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEuCCT/dJMcacvQkqd/7N8NcwKJohgrRtiUT5YDV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEuCCT/dJMcacvQkqd/7N8NcwKJohgrRtiUT5YDV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEuCCT/dJMcacvQkqd/7N8NcwKJohgrRtiUT5YDV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEuCCT%2FdJMcacvQkqd%2F7N8NcwKJohgrRtiUT5YDV1%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;1152&quot; height=&quot;699&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;699&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 코드베이스로 Android와 iOS 앱을 동시에 만들 수 있다면? 네이티브 언어(Kotlin, Swift)를 각각 쓰지 않아도 된다면? 이게 크로스 플랫폼 프레임워크가 해결하려는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 가장 많이 쓰이는 두 프레임워크, &lt;b&gt;Flutter&lt;/b&gt;와 &lt;b&gt;React Native&lt;/b&gt;를 비교한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;다만 일반적인 비즈니스 앱 수준에서는 사용자가 차이를 느끼기 어렵고, 오히려 프레임워크 쪽이 네이티브보다 나은 부분도 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Flutter&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;http://flutter.dev&quot;&gt;flutter.dev&lt;/a&gt; &amp;middot; &lt;a href=&quot;http://github.com/flutter&quot;&gt;github.com/flutter&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google이 만든 프레임워크로, &lt;b&gt;Dart&lt;/b&gt; 언어를 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 특징은 자체 렌더링 엔진(Skia &amp;rarr; Impeller)을 가지고 있다는 점이다. 플랫폼의 네이티브 UI 컴포넌트를 안 쓰고 모든 픽셀을 직접 그린다. 덕분에 어떤 플랫폼에서든 똑같은 UI가 나오고, 복잡한 애니메이션에서도 안정적인 프레임을 유지한다.&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;b&gt;Dart 언어&lt;/b&gt; &amp;mdash; Java랑 문법이 비슷해서 진입 장벽이 낮은 편이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Hot Reload / Hot Restart&lt;/b&gt; &amp;mdash; 코드 고치면 바로 확인할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;위젯 기반 선언적 UI&lt;/b&gt; &amp;mdash; 모든 UI 요소가 위젯이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;플랫폼 지원 범위가 넓음&lt;/b&gt; &amp;mdash; Android, iOS 말고도 Web, Windows, macOS, Linux까지 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MethodChannel&lt;/b&gt; &amp;mdash; 네이티브(Kotlin/Swift) 코드와 통신해서 플랫폼 고유 기능을 쓸 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공식 패키지가 많음&lt;/b&gt; &amp;mdash; Google이 직접 관리하는 패키지가 꽤 있고, &lt;a href=&quot;http://pub.dev&quot;&gt;pub.dev&lt;/a&gt;에 45,000개 넘는 패키지가 올라와 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Dart DevTools&lt;/b&gt; &amp;mdash; 성능 프로파일링, 위젯 리빌드 추적 등 디버깅 도구가 잘 되어 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOT 컴파일로 네이티브 ARM 코드로 변환되기 때문에 앱 시작 속도와 런타임 성능도 좋다. 커스텀 디자인이 중요하거나, 브랜드 고유의 룩앤필을 살려야 하는 앱에서 특히 힘을 발휘한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React Native&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;http://reactnative.dev&quot;&gt;reactnative.dev&lt;/a&gt; &amp;middot; &lt;a href=&quot;http://github.com/facebook/react-native&quot;&gt;github.com/facebook/react-native&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Meta(구 Facebook)가 만든 프레임워크로, &lt;b&gt;JavaScript / TypeScript&lt;/b&gt;를 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter와 다르게 플랫폼의 실제 네이티브 UI 컴포넌트를 사용한다. iOS의 바운스 물리, Android의 리플 이펙트 같은 것들이 별도 구현 없이 자연스럽게 동작한다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2026년 기준으로 New Architecture가 기본값이 되면서 성능이 많이 올라왔다.&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;Fabric Renderer&lt;/b&gt; &amp;mdash; 동기적, 동시성(concurrent) UI 업데이트가 가능해졌다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TurboModules&lt;/b&gt; &amp;mdash; 네이티브 모듈을 lazy-loaded로 접근할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JSI&lt;/b&gt; &amp;mdash; 예전의 비동기 Bridge가 사라지고, JS와 네이티브 코드 간 통신 지연이 크게 줄었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외 특징은 이렇다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;React 문법 거의 그대로&lt;/b&gt; &amp;mdash; 웹에서 React 쓰던 사람이라면 빠르게 적응할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드 푸시 (OTA 업데이트)&lt;/b&gt; &amp;mdash; Microsoft CodePush로 스토어 심사 없이 앱을 업데이트할 수 있다. Flutter에는 없는 기능이라 이게 필요한 프로젝트라면 큰 차별점이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;npm 생태계&lt;/b&gt; &amp;mdash; 서드파티 라이브러리가 압도적으로 많다. 다만 공식 패키지는 Flutter보다 적다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Flexbox 스타일링&lt;/b&gt; &amp;mdash; CSS를 직접 못 쓰고, Flexbox 기반 스타일 객체를 사용한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Hermes 엔진&lt;/b&gt; &amp;mdash; 기본 JS 엔진으로, 시작 시간과 메모리 사용을 최적화한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;선언적 UI &amp;amp; 컴포넌트 기반&lt;/b&gt; &amp;mdash; React의 설계 철학을 그대로 이어받았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;React vs React Native 문법 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React (웹):&lt;/p&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;&amp;lt;div style={{ backgroundColor: 'blue', padding: '10px' }}&amp;gt;Hello&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Native (모바일):&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;View style={{ backgroundColor: 'blue', padding: 10 }}&amp;gt;
  &amp;lt;Text&amp;gt;Hello&amp;lt;/Text&amp;gt;
&amp;lt;/View&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML 태그 대신 &lt;code&gt;View&lt;/code&gt;, &lt;code&gt;Text&lt;/code&gt; 같은 네이티브 컴포넌트를 쓰고, 스타일 값에 단위(&lt;code&gt;px&lt;/code&gt; 등)를 안 붙인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JS/React 경험이 있는 팀이 빠르게 모바일 앱을 내야 할 때 유리하고, OTA 업데이트가 필요하다면 사실상 선택지가 좁아진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비교 표&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;Flutter&lt;/th&gt;
&lt;th&gt;React Native&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;개발사&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;Meta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;언어&lt;/td&gt;
&lt;td&gt;Dart&lt;/td&gt;
&lt;td&gt;JavaScript / TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;렌더링&lt;/td&gt;
&lt;td&gt;자체 엔진 (Impeller)&lt;/td&gt;
&lt;td&gt;네이티브 컴포넌트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;렌더링 성능&lt;/td&gt;
&lt;td&gt;복잡한 애니메이션, 커스텀 UI에서 우세&lt;/td&gt;
&lt;td&gt;메모리 효율, 바이너리 크기에서 우세&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;지원 플랫폼&lt;/td&gt;
&lt;td&gt;Android, iOS, Web, Windows, macOS, Linux&lt;/td&gt;
&lt;td&gt;Android, iOS, tvOS, macOS, Windows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;생태계&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;http://pub.dev&quot;&gt;pub.dev&lt;/a&gt; 45,000+&lt;/td&gt;
&lt;td&gt;npm 전체 활용 가능&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;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OTA 업데이트&lt;/td&gt;
&lt;td&gt;미지원&lt;/td&gt;
&lt;td&gt;CodePush 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;네이티브 연동&lt;/td&gt;
&lt;td&gt;MethodChannel&lt;/td&gt;
&lt;td&gt;JSI / TurboModules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;학습 곡선&lt;/td&gt;
&lt;td&gt;Dart를 새로 배워야 함&lt;/td&gt;
&lt;td&gt;JS/React 경험자에게 유리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;디버깅&lt;/td&gt;
&lt;td&gt;Dart DevTools&lt;/td&gt;
&lt;td&gt;React Native Debugger&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 뭘 쓸까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Flutter가 맞을 때&lt;/b&gt; &amp;mdash; 커스텀 UI가 핵심이거나, 모바일+데스크톱+웹을 하나의 코드로 커버하고 싶거나, 플랫폼 간 UI 일관성이 중요할 때.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;React Native가 맞을 때&lt;/b&gt; &amp;mdash; 팀에 JS/React 경험자가 많거나, 네이티브 룩앤필이 중요하거나, OTA 업데이트가 필수이거나, 기존 React 웹 앱과 코드를 공유하고 싶을 때.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정답은 없다. 프로젝트 요구사항, 팀의 기술 스택, 유지보수 전략에 따라 다르다.&lt;br /&gt;개인적으로, Flutter를 좋아하는데 개발자 편의성이 뛰어나고 원하는 구조를 적용하기 쉬워서 Flutter를 선호한다. 물론 Flutter를 이전부터 해오고 있었기 때문에 Flutter를 더 선호하긴 한다 ㅎㅎ&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;</description>
      <category>모바일</category>
      <category>Flutter</category>
      <category>react</category>
      <category>rn</category>
      <category>플러터</category>
      <author>ao</author>
      <guid isPermaLink="true">https://ao3921.tistory.com/5</guid>
      <comments>https://ao3921.tistory.com/5#entry5comment</comments>
      <pubDate>Wed, 1 Apr 2026 08:28:42 +0900</pubDate>
    </item>
    <item>
      <title>FCM Notification/Silent Push 알림 처리</title>
      <link>https://ao3921.tistory.com/4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 작성했던 문서들이 있는데 블로그를 만든 김에 정리&amp;amp;최신화해서 새 글을 올리려고 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1490&quot; data-origin-height=&quot;860&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csaBX4/dJMcagkFuIP/Wktdzz3OF2kkj4gR4LXWh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csaBX4/dJMcagkFuIP/Wktdzz3OF2kkj4gR4LXWh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csaBX4/dJMcagkFuIP/Wktdzz3OF2kkj4gR4LXWh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsaBX4%2FdJMcagkFuIP%2FWktdzz3OF2kkj4gR4LXWh0%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;1490&quot; height=&quot;860&quot; data-origin-width=&quot;1490&quot; data-origin-height=&quot;860&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;운영 중인 Android 앱을 Flutter로 포팅하는 프로젝트에서 푸쉬 알림을 구현해야 했다. 문제는 기존 앱들이 이미 동작하고 있었기 때문에 서버 쪽 API 수정을 최소화해야 했다는 점이다. 그래서 기존 API가 보내는 Silent Push 방식을 그대로 살리면서 Flutter 측에서 대응하는 방향으로 진행했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;FCM이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FCM(Firebase Cloud Messaging)은 모바일 환경에서 푸쉬 알림을 구현하거나, 서버에서 클라이언트로 데이터를 전송할 때 쓰는 서비스다. Android, iOS, Web을 모두 지원하고 적용례가 많아서 모바일 푸쉬 구현의 사실상 표준이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FCM의 푸쉬는 크게 두 가지로 나눠다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Silent Push (Data only notification)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;푸쉬 데이터에 &lt;code&gt;notification&lt;/code&gt; 필드가 없는 경우다. OS가 알아서 팝업을 띄워주지 않기 때문에 클라이언트에서 직접 알림 출력 시점과 내용을 제어해야 한다. 팝업 없이 데이터만 조용히 전달할 수도 있어서 &amp;ldquo;Silent&amp;rdquo;이라고 부른다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Notification Push&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;notification&lt;/code&gt; 필드가 포함된 일반적인 알림이다. OS가 알아서 팝업을 처리해주기 때문에 클라이언트에서 별도로 할 일이 없다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 Silent Push였나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트의 서버 API는 이미 다른 앱들과 공유되고 있었고, 그 API가 보내는 푸쉬에는 &lt;code&gt;notification&lt;/code&gt; 필드가 없었다. 즉, 애초부터 Silent Push로 설계되어 있었던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 고치면 기존 앱에 영향이 갈 수 있으니, Flutter 쪽에서 Silent Push를 받아서 직접 알림을 띄우는 방식으로 가닥을 잡았다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Flutter에서 Silent Push 알림 출력하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://pub.dev/packages/flutter_local_notifications&quot;&gt;flutter_local_notifications&lt;/a&gt; 패키지를 사용했다. Silent Push로 받은 데이터를 이 패키지로 로컬 알림으로 출력하면, Notification Push와 동일한 모습으로 푸쉬가 뜨는 걸 확인할 수 있었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;알림 출력 코드&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;await flutterLocalNotificationsPlugin.show(
  int.parse(pushData['transId'] ?? '0') + DateTime.now().second,
  '${pushData['title'] ?? ''}',
  pushData['content'] ?? '',
  details,
  payload: jsonEncode(pushData),
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;details&lt;/code&gt;에는 Android/iOS 각각의 알림 채널 정보가 들어간다. 한 가지 주의할 점이 있는데, Android 알림 채널의 &lt;code&gt;importance&lt;/code&gt;와 &lt;code&gt;priority&lt;/code&gt;를 &lt;b&gt;High 이상&lt;/b&gt;으로 설정해야 포그라운드에서도 알림이 제대로 출력된다. 이걸 몰라서 처음에 알림이 안 뜨는 문제로 시간을 날렸던 기억이 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/develop/ui/views/notifications?hl=ko&quot;&gt;Android 알림 문서&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;알림 터치 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알림을 터치했을 때의 동작은 &lt;code&gt;onDidReceiveNotificationResponse&lt;/code&gt;로 처리한다. &lt;code&gt;show()&lt;/code&gt; 할 때 넣어둔 &lt;code&gt;payload&lt;/code&gt;를 여기서 꺺내 쓸 수 있다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;await flutterLocalNotificationsPlugin.initialize(
  initializationSettings,
  onDidReceiveNotificationResponse:
      (NotificationResponse notificationResponse) async {
    switch (notificationResponse.notificationResponseType) {
      case NotificationResponseType.selectedNotification:
        Map&amp;lt;String, dynamic&amp;gt; tempJson =
            jsonDecode(notificationResponse.payload!);

        if (tempJson['pushType'] == 'back') {
          // 인트로 화면으로 이동 후 푸쉬 데이터 전달
          Provider.of&amp;lt;IntroProv&amp;gt;(navKey.currentContext!, listen: false).clear();
          await Navigator.pushAndRemoveUntil(
            navKey.currentContext!,
            MaterialPageRoute(
              builder: (context) =&amp;gt; IntroScreen(
                argumentMap: tempJson,
              ),
            ),
            (Route&amp;lt;dynamic&amp;gt; route) =&amp;gt; false,
          );
        } else {
          // 홈 화면에서 푸쉬 데이터 처리
          Provider.of&amp;lt;HomeProv&amp;gt;(navKey.currentContext!, listen: false)
              .onTapPush(pushArgument: PushArgument.fromJson(tempJson));
        }
        break;

      case NotificationResponseType.selectedNotificationAction:
        // 액션 버튼이 있는 푸쉬 처리
        break;
    }
  },
  onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;pushType&lt;/code&gt;에 따라 인트로 화면으로 보낼지, 홈 화면에서 처리할지 분기하는 구조다. 실제 프로젝트에서는 푸쉬 타입이 더 다양했지만 핵심 로직은 이렇다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Notification Push는 어떻게 다르나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;notification&lt;/code&gt; 필드가 포함된 알림은 위의 과정이 필요 없다. 서버 &amp;rarr; FCM &amp;rarr; 클라이언트 흐름에서 OS가 직접 팝업을 처리해주기 때문이다. Android는 transport layer에서, iOS는 APNs(Apple Push Notification service)에서 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 알림을 터치했을 때의 동작은 직접 구현해야 한다. 이 경우에는 &lt;code&gt;FirebaseMessaging.onMessageOpenedApp.listen()&lt;/code&gt;을 쓴다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Silent Push&lt;/b&gt;: 클라이언트가 직접 알림을 만들어야 한다 &amp;rarr; &lt;code&gt;flutter_local_notifications&lt;/code&gt;로 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Notification Push&lt;/b&gt;: OS가 알아서 처리해준다 &amp;rarr; 터치 동작만 &lt;code&gt;onMessageOpenedApp&lt;/code&gt;으로 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 운영 중인 앱과 API를 공유하는 상황이라면, 서버를 건드리기보다 클라이언트에서 맞춰주는 게 현실적일 때가 많다. Silent Push가 딱 그 상황이었다.&lt;/p&gt;</description>
      <category>모바일</category>
      <author>ao</author>
      <guid isPermaLink="true">https://ao3921.tistory.com/4</guid>
      <comments>https://ao3921.tistory.com/4#entry4comment</comments>
      <pubDate>Wed, 1 Apr 2026 08:15:36 +0900</pubDate>
    </item>
    <item>
      <title>안드로이드 컴포즈 dp, sp 단위</title>
      <link>https://ao3921.tistory.com/3</link>
      <description>&lt;p&gt;플러터에서는 딱히 단위가 필요 없었다. 위젯의 크기, 텍스트의 크기는 논리 픽셀(Logical Pixel, lp)로 결정되었다. 그런데, 안드로이드 네이티브로 돌아오니, dp, sp 단위가 생겼다. 거기에다 em 단위도 있다.&lt;/p&gt;
&lt;h2&gt;dp와 sp는 어떻게 다르며, 왜  필요할까?&lt;/h2&gt;
&lt;h3&gt;dp&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;density-independent pixel&lt;/strong&gt; - 화면 밀도에 독립적인 단위로, UI 레이아웃의 기본 단위이다 160dpi 화면에서 1dp = 1px.&lt;/p&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;dp는 어떠한 화면에서도 일정한 크기를 가질 수 있도록 해주는 유연한 단위이다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;그렇기에, dp 단위는 개발자가 다양한 크기의 화면에 대응하는 레이아웃을 쉽게 만들 수 있도록 돕는 단위라 할 수 있다.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;sp&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;scale-independent pixel&lt;/strong&gt; - dp와 동일하지만 시스템 글꼴 크기 설정(접근성)이 추가로 반영된다. 텍스트 크기의 기본 단위.&lt;/p&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;dp와 동일하게 작동하나, 폰트에 쓰인다. fontScale이 1.0일때 sp는 dp와 동일하다. 안드로이드 시스템에서 폰트의 크기는 시스템 글꼴 크기 설정에 따라 계산된다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;그렇기에, sp 단위는 개발자가 사용자가 설정한 시스템 글꼴 크기 설정대로 폰트 크기를 변형할 수 있게 하는 단위라 할 수 있다.&lt;/p&gt;
&lt;h3&gt;em&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;폰트 크기를 기준으로 한 상대적인 배수 단위이다. &lt;code&gt;1.em&lt;/code&gt;은 현재 폰트 사이즈의 100%와 같다.&lt;/li&gt;
&lt;li&gt;자간, 행간, 줄 높이 등, 폰트 크기와 연관 있는 곳에서 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;Box(
  contentAlignment = Alignment.Center,
  modifier = Modifier
      .size(width = 29.dp, height = 24.dp)
      .clip(RoundedCornerShape(12.dp)),
  )

Text(
    &amp;quot;content&amp;quot;,
    overflow = TextOverflow.Ellipsis,
    maxLines = 2,
    fontSize = 14.sp,
        color = Colors.ContentText,
)&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;하나로 사용할 순 없을까?&lt;/h2&gt;
&lt;p&gt;Flutter에서는 실수에 단위를 붙이지 않고 실수 그대로 사용한다. 위젯 레이아웃 크기에서도 동일하게 단위를 사용하지 않고 텍스트 위젯에서도 단위를 사용하지 않는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;Container(
    width: 300,
    height:280,
    )

    Text(
        &amp;quot;텍스트&amp;quot;,
        style: TextStyle(fontSize: 18)
    )&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;개발편의성 측면에서 안드로이드에서도 단위를 사용하지 않고 하나로 사용할 수 있지 않을까? sp는 dp에 TextScale 값을 곱하여 계산하니까, 텍스트 관련한 컴포넌트에서 따로 처리를 하면 될 것도 같다.&lt;/p&gt;
&lt;p&gt;Text 컴포넌트 내부에서 &lt;code&gt;dp&lt;/code&gt; 값을 받되, 시스템의 폰트 스케일 팩터를 자동으로 곱해주면 될 것도 같다. 실제로 Flutter는 &lt;code&gt;TextScaler&lt;/code&gt;를 통해 위젯 레벨에서 이 처리를 하고 있고, 개발자는 단위를 구분할 필요 없이 logical pixel 하나만 사용한다.&lt;/p&gt;
&lt;p&gt;이 점에서 Android Compose와 Flutter는 다른 접근 방법을 취했다. &lt;strong&gt;Compose&lt;/strong&gt;는 .sp라는 단위를 사용하게 해 &lt;strong&gt;“텍스트가 아닌 곳에서도 폰트 스케일을 고려하여 크기를 지정할 수 있다”.&lt;/strong&gt; &lt;/p&gt;
&lt;p&gt; 텍스트가 아닌 텍스트 옆에 붙는 아이콘 크기, 텍스트를 감싸는 패딩, 텍스트 기반 버튼의 최소 높이 등, 이 크기들은 글꼴 크기가 커질 때 같이 커져야 자연스럽다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flutter&lt;/strong&gt;에서는 같은 방식으로 텍스트가 아닌 곳에 글꼴 크기를 설정하려면 &lt;code&gt;MediaQuery.textScaleFactorOf&lt;/code&gt; 를 가져와서 수동으로 곱해주어야 한다. 단위가 하나이기 때문에 텍스트와 관련 없는 위젯에서는 편리하겠지만, 텍스트 크기 연동이 필요하면 수작업이 늘어날 것이다.&lt;/p&gt;
&lt;p&gt;안드로이드 컴포즈의 설계 방침은 이해 되나 이게 최선일까 하고 궁금하다. Flutter를 만들때에도 나처럼 sp, dp를 구분하는게 귀찮은 마음에 lp 단위를 만들어서 적용하지 않았을까? 다음에 텍스트 컴포넌트를 만들 일이 있으면 더 편하게 단위를 지정할 방법을 찾아봐야겠다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.android.com/design/ui/mobile/guides/layout-and-content/grids-and-units&quot;&gt;안드로이드 공식 문서 - 그리드 및 단위&lt;/a&gt;&lt;/p&gt;</description>
      <category>안드로이드</category>
      <category>dp</category>
      <category>SP</category>
      <category>안드로이드</category>
      <author>ao</author>
      <guid isPermaLink="true">https://ao3921.tistory.com/3</guid>
      <comments>https://ao3921.tistory.com/3#entry3comment</comments>
      <pubDate>Mon, 30 Mar 2026 17:16:24 +0900</pubDate>
    </item>
    <item>
      <title>우테코 안드 8기 &amp;quot;칸반 보드 태스크 - 컴포즈 기초&amp;quot; 리뷰 정리 2차</title>
      <link>https://ao3921.tistory.com/2</link>
      <description>&lt;p&gt;1차 리뷰에 이은 2차 리뷰, 3주? 전 내용인데 3주 전의 나와 지금의 나를 비교하니 많이 배우긴 한 것 같다는 생각이 든다. 이 당시에는 컴포넌트를 어디까지 분리해야 하는지 기준을 세우지 못했고 컴포넌트에 너무 많은 역할과 책임을 부여했었다. 물론, 지금이라고 완벽한 건 아니지만 어느 정도 기준은 세웠기 때문에 그 기준에 따르고 있다. 기억에 따르면 다음 리뷰에서 컴포넌트 분리 기준을 얘기할 것 같다.&lt;/p&gt;
&lt;p&gt;PR: woowacourse/compose-kanban-board-task#1&lt;/p&gt;
&lt;p&gt;리뷰어: ㅋㄹ&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. FlowRow 후속 답변&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;KanbanBoard.kt&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Q: 잘 찾아보셨습니다. 질문주신 부분은 제가 답변드리지 않아도 미션을 진행해나가면서 스스로 답을 찾으실 수 있을 것 같아요. &lt;code&gt;Column&lt;/code&gt;이라는 컴포저블 함수가 있지만, &lt;code&gt;LazyColumn&lt;/code&gt;이 또 있는 이유가 무엇일까요? &lt;code&gt;Lazy&lt;/code&gt;는 왜 붙었을지 생각하며 차이점을 비교해보셔도 도움이 될 것 같아요.&lt;/p&gt;
&lt;p&gt;이 질문은 답변을 잊어버린 것 같다;; 지금이라도 답변해보자면, LazyColumn은 한번에 모두 로딩하지 않는다. 그래서 &lt;code&gt;Lazy&lt;/code&gt;라 호칭한다고 생각한다. &amp;quot;게으른 칼럼&amp;quot;이기에 기존의 Column처럼 모든 요소를 한번에 로딩하는 게 아니라 필요할 때마다 로딩을 통해 요소를 가져온다. 보통, 구매 내역, 상품 목록 등이 이런 구조를 가지고 있다.&lt;/p&gt;
&lt;p&gt;이렇게, 많은 요소를 가지고 있는 칼럼을 한번에 로딩하려면 로딩하는 시간도 길 것이고 많은 컴포넌트를 한 번에 보여줘야하니까 성능적 문제도 생길 수 있다. 이 문제들을 LazyColumn을 통해 해결 할 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;2. emptyList() vs listOf() / tagList 기본값&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;KanbanBoard.kt&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Q: 이전 코멘트에 이어지는 내용인데, 내부 값이 없는 리스트를 만들 때는 &lt;code&gt;listOf()&lt;/code&gt;보다는 &lt;code&gt;emptyList()&lt;/code&gt;를 쓰는 것이 직관적으로 느껴지는데 어떻게 생각하시나요? 그리고 &lt;code&gt;tagList&lt;/code&gt;만 기본 값을 지정해주는 이유는 무엇인가요?&lt;/li&gt;
&lt;li&gt;A: 내부 값이 없는 리스트를 만들 때에도 똑같이 listOf()를 사용한 이유는, emptyList()랑 동일한 기능을 하기 때문에 그대로 사용하였습니다. 그런데, 생각해보니 emptyList()를 사용하는 게 말씀하신대로 다른 사람이 보기에 더 이해가 쉬울 것 같습니다. 이전에 KanbanBoardCard에 @Preview가 설정되어 있을 때 tagList 이외에도 기본값을 설정해주었었는데 @Preview를 옮기면서 지우는 과정에 누락되었던 것 같습니다. 지우도록 하겠습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;3. 시스템 폰트 크기 설정 관련 후속&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;KanbanBoard.kt&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Q: 시스템 폰트 크기 설정에 따라 글씨 크기가 바뀌지 않아야 하는 이유가 무엇인가요? 사용자들은 앱에서도 자신이 사용하는 일반적인 글씨 크기를 원할겁니다. 이를 무시하고 고정된 크기로 보여준다면 사용에 이질감을 느끼지 않을까요?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이것도 답변을 잊었다. 지금은 어떤지 모르겠지만, 지인의 지인이 당근 개발자였는데 시스템 폰트 크기 설정을 고려하지 않기 위해 폰트 크기를 고정해놓고 UI를 짠다고 했다. 나도 현업에 있을 때 시스템 폰트 크기 설정 때문에 레이아웃이 어긋나서 문제가 되었던 적이 있는데 그 이후로 폰트 배율을 고정해놓고 작업했던 기억이 난다. 물론, 사용자는 설정한 시스템 폰트 크기대로 나오고 레이아웃도 그에 맞춰서 변형되는게 베스트 케이스겠지만, 그 당시에는 시간이 부족해서 그렇게 작업했었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. Preview만으로 개발한 것에 대해&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;README.md&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Q: 현재 앱을 실행하면 기본 화면인 Click Me만 보이고 있습니다. 단순히 Preview만을 보면서 개발하셨던건가요?&lt;/li&gt;
&lt;li&gt;A: 맞습니다. Preview만 보면서 개발하였습니다. 실제로 실행가능하여야 한다는 언급이 없어서 그대로 놓아두었습니다. 2단계 제출 때 수정하여 실행해서도 보일 수 있도록 만들겠습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;5. 함수 책임 분리&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;KanbanBoard.kt&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Q: 하나의 함수가 너무 많은 일을 하고 있다고 느껴지진 않나요? 적절한 책임을 부여해서 함수를 나눠본다면 어떤 장점이 있을까요?&lt;/li&gt;
&lt;li&gt;A: KanbanBoardCard 안에 제목, 내용, 태그 로우(CustomChip), 수평구분선, 담당자 정보 로우(아이콘, 담당자명)를 포함하고 있어 작은 함수는 아니라고 생각합니다. 만약 여기에서 더 분리한다면 태그 로우와 담당자 정보 로우를 분리하겠습니다.&lt;ul&gt;
&lt;li&gt;여기에서 의문이 드는 점: 제목과 내용 같은 경우에는 하나의 Text 컴포넌트로 표시하고 있습니다. 이렇게 하나의 자식을 가진 요소도 분리하는 게 맞을까요? 저라면 제목이나 내용 정도는 놓을 것 같습니다.&lt;/li&gt;
&lt;li&gt;적절한 책임을 부여해서 함수를 나누면 &lt;strong&gt;재사용성&lt;/strong&gt;이 생기고 &lt;strong&gt;변경범위가 줄어들&lt;/strong&gt; 것 같습니다. 다른 컴포넌트를 생성했을 때 재사용할 수 있을 것이고 변경범위 축소는 KanbanBoardCard 함수를 전부 읽지 않고 해당 컴포넌트만 확인하고 수정하면 되어서 편의성이 올라갈 거라 생각합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;6. Composable의 관심사 / 태그 제한 로직&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;KanbanBoard.kt&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Q: 태그는 5개까지 글자 수는 5글자까지 라는 제한을 함수 내부에서 직접 진행하고 있는데요. Composable의 관심사는 어디까지라고 생각하시나요? 화면에 따라 태그를 다르게 보여줘야 한다면, 글자 수를 변경해야 한다면 어떻게 대응할 수 있을까요? 변화를 가장 줄이면서 대응하는 방법은 어떤 것들이 있을까요?&lt;/li&gt;
&lt;li&gt;A: 아래와 같이 정리했습니다.&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Composable의 관심사&lt;/strong&gt;: UI 처리에만 한정해야 한다고 생각합니다. 즉, 레이아웃, 스타일링 등이라고 생각합니다. 코드에서 5글자 제한을 substring을 통해서 처리하고 있는데 이러한 처리는 컴포저블의 관심사 밖이라고 생각합니다. KanbanBoardCardData 처럼 도메인 모델에 분리하는 게 맞다고 생각합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;화면별 대응&lt;/strong&gt;: 도메인 모델에 글자 수 제한 로직을 분리하고 글자 수 제한을 매개변수로 받아 출력되는 글자 수를 바꾸도록.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;변화 최소화&lt;/strong&gt;: 분리라고 생각합니다. 컴포넌트 함수가 너무 커서 분리하였고, 이 분리를 통해서 변경범위를 줄일 수 있으리라고 생각합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;7. KanbanCardData와 KanbanBoardCard 파라미터 일치&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;KanbanCardData.kt&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Q: &lt;code&gt;KanbanBoardCard&lt;/code&gt; 함수가 받는 파라미터들과 정확히 일치하네요.&lt;/li&gt;
&lt;li&gt;A: 맞습니다. 어떻게 하면 좀 더 깔끔해 보일지 고민하다가 KanbanCardData로 변경하였습니다. 혹시 이와 같은 경우에는 굳이 class로 분리하지 않는 게 맞을까요?&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;8. data class 선택 이유&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;KanbanCardData.kt&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Q: data class를 선택한 이유는 무엇인가요?&lt;/li&gt;
&lt;li&gt;A: 단순하게 매개변수를 묶는 용도로 사용하고 있기 때문에 데이터 구조화와 전달에 최적화된 클래스인 data class를 사용하는 게 맞다고 생각했습니다. 현 시점에서는 데이터 구조화 이외에도 비즈니스 로직을 담고 있기 때문에 data class가 아닌 class로 수정했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;9. Preview 가시성&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;KanbanBoard.kt&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Q: Preview는 외부에서 사용되나요? 외부에서 사용하지 않는 친구들의 가시성은 어떻게 해줘야할까요?&lt;/li&gt;
&lt;li&gt;A: 외부에서 사용하지 않고 해당 범위에서만 사용하기 때문에 private 가시성을 붙이는 게 맞다고 생각이 듭니다. private나 public을 잘 사용하지 않다보니 아직 손에 익지 않아 빠뜨렸네요. 유념하겠습니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>우테코</category>
      <category>8기</category>
      <category>안드로이드</category>
      <category>우아한테크코스</category>
      <category>우테코</category>
      <author>ao</author>
      <guid isPermaLink="true">https://ao3921.tistory.com/2</guid>
      <comments>https://ao3921.tistory.com/2#entry2comment</comments>
      <pubDate>Mon, 30 Mar 2026 15:09:22 +0900</pubDate>
    </item>
    <item>
      <title>우테코 안드 8기 &amp;quot;칸반 보드 태스크 - 컴포즈 기초&amp;quot; 리뷰 정리 1차</title>
      <link>https://ao3921.tistory.com/1</link>
      <description>&lt;p&gt;블로그 글 작성을 더 이상은 늦춰서는 안되겠다는 생각이 들어서 지금이라도 시작하려고 한다. 일단은 쉬운 주제부터 시작!&lt;/p&gt;
&lt;p&gt;첫 미션에다 처음으로 PR 리뷰를 받았는데 신선한 경험이었다. 나름 2~3년 개발을 했는데도 PR 자체가 처음이었다. 현업에 있을 때에는 PR 리뷰랄것도 없이 코드 리딩정도만 진행했는데, 이런 과정 자체가 굉장히 충격적이었다. 한편으론, 개발을 헛 했다는 기분도 든다. 그래도, 우테코에서 이러한 선진적인 방법을 배우고 싶은 것이었으니 굉장히 만족한다.&lt;/p&gt;
&lt;p&gt;PR: woowacourse/compose-kanban-board-task#1&lt;/p&gt;
&lt;p&gt;리뷰어: ㅋㄹ&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. ktlint와 테스트 확인 습관&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;README.md&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Q: 항상 제출하기 전 ktlint와 테스트를 확인해보는 습관을 들이면 좋을 것 같습니다.&lt;/li&gt;
&lt;li&gt;A: 코드 정렬만 하고 있었는데 그 생각을 못했네요. 주의하겠습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;2. 커밋 분리 기준&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;README.md&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Q: README 작성 잘해주셨습니다. 커밋 로그를 보니 기능의 구현과 README 수정을 다른 커밋에 진행했던데 그렇게 진행한 이유가 있을까요? 과연 두 커밋을 나누는 것이 의미가 있는 것일지 고민해보고 알려주세요.&lt;/li&gt;
&lt;li&gt;A: 말씀을 들어보니 굳이 둘로 나누지 않아도 괜찮았겠다 생각이 드네요. 두 커밋으로 나눈 이유는 커밋 컨벤션에 따라 feat와 docs를 생각해서 나누었습니다. 기능 구현은 feat, README는 docs에서 커밋하고 싶었습니다. 지금 고민해보니, readme 수정이라고 해도 단순히 체크리스트 체크만 하는 것이어서 둘로 나누지 않아도 괜찮을 것 같네요! 앞으로의 구현에 반영토록 하겠습니다!&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;3. FlowRow 외에 아이템 배치 방법&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;KanbanBoard.kt&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Q: FlowRow 외에 비슷한 방식으로 아이템을 배치하는 방법은 무엇이 있을지 찾아보고 간단하게 비교해서 알려주시겠어요?&lt;/li&gt;
&lt;li&gt;A: FlowRow처럼 수평으로 배치하는 컴포넌트는 Row, FlowRow, LazyRow가 있었습니다.&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Row&lt;/strong&gt; - 컴포넌트를 수평 배치하는 가장 간단한 컴포넌트&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FlowRow&lt;/strong&gt; - 컴포넌트를 수평 배치하되 설정한 너비를 넘어가면 자동으로 다음 줄로 넘어가는 컴포넌트&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LazyRow&lt;/strong&gt; - 화면에 보이는 항목만 렌더링하고 수평 스크롤하면 새 항목을 그리고 벗어난 항목은 해제하는 컴포넌트&lt;/li&gt;
&lt;li&gt;현재 요구사항으로는 칩(태그)이 카드의 너비를 넘어갔을 때 다음 줄로 표시되어야 했기에 FlowRow의 사용이 적절하다고 생각합니다. 그러나 FlowRow의 경우 화면 너비를 계산해야 하고 자식 위젯의 크기를 고려해야 하기에 해당 요구사항처럼 필요한 곳 이외에서 사용하면 오버킬이라고 생각합니다.&lt;/li&gt;
&lt;li&gt;추가로 궁금한 점: LazyColumn이 많은 수의 자식을 가지고 있고 화면에 그려지지 않았지만 그려질 항목이 많다면 이는 성능저하의 요소가 될 수 있을까요?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;4. listOf() 내부 동작&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;KanbanBoard.kt&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Q: &lt;code&gt;listOf()&lt;/code&gt; 함수는 내부적으로 어떻게 리스트를 만들어주고 있나요? 0단계에서 학습하셨으리라 믿고 질문드려봅니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A: listOf의 실제 구현상 코드는 아래와 같습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;  public fun &amp;lt;T&amp;gt; listOf(vararg elements: T): List&amp;lt;T&amp;gt; =
      if (elements.size &amp;gt; 0) elements.asList() else emptyList()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  listOf가 하나 이상의 매개변수를 전달 받았을 때에는 vararg로 받은 elements는 함수 내부에서 Array로 취급되고 Array를 asList하여 리스트로 만드는 것이었습니다. listOf가 매개변수를 전달 받지 않았을 때에는 emptyList()를 통해 EmptyList를 반환하는데 여기에서 EmptyList가 &lt;code&gt;List&amp;lt;Nothing&amp;gt;&lt;/code&gt;이라는 게 신기했습니다. 비어있는 ArrayList를 반환하는 줄 알았는데 이렇게 최적화하는 게 신기했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;5. shape vs Modifier.clip&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;KanbanBoard.kt&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Q: &lt;code&gt;shape&lt;/code&gt;를 주지 않고 &lt;code&gt;Modifier.clip&lt;/code&gt;으로도 비슷한 결과를 도출할 수 있습니다. 둘은 어떤 차이가 있는지 알아보고 알려주세요.&lt;/li&gt;
&lt;li&gt;A: 프리뷰의 배경화면을 검은색으로 설정했는데 &lt;code&gt;Modifier.background&lt;/code&gt;에 shape를 사용했을 때는 검은색이 보였는데 &lt;code&gt;Modifier.clip&lt;/code&gt;을 사용하니까 검은색이 보이지 않았다. 알고보니 Modifier에서 순서를 조정하면 되는 일이었다. 백그라운드 설정 이전에 클립 해줘야 제대로 잘린다!&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Modifier.background&lt;/code&gt;는 내부에 있는 콘텐츠나 터치 영역은 그대로 사각형&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Modifier.clip&lt;/code&gt;은 그 이후 체인 전체에 클리핑을 적용. 배경, 콘텐츠, 이미지, 터치 영역까지 모두 해당 shape으로 자름&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;clip이 &lt;code&gt;Modifier.background&lt;/code&gt;보다 더 강력하다고 느꼈습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;6. padding과 background 순서&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;KanbanBoard.kt&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Q: &lt;code&gt;padding&lt;/code&gt;과 &lt;code&gt;background&lt;/code&gt;의 위치가 바뀌면 어떤 일이 일어나는지, 그리고 왜 그런일이 일어나는지 확인해보고 알려주세요.&lt;/li&gt;
&lt;li&gt;A: 이전에 주로 했던 플러터와 비교를 통해 통찰을 얻을 수 있었습니다. 컴포즈에서 모디파이어 체이닝 순서가 무분별하다고 생각했는데 플러터에서도 똑같이 위젯을 감싸면서 순서를 적용했다는 사실을 깨달으니 연상이 쉽게 되었습니다.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;background&lt;/code&gt; → &lt;code&gt;padding&lt;/code&gt; 순서: &amp;quot;카드에 배경색을 설정하고 그 안에 여백을 적용하기&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;padding&lt;/code&gt; → &lt;code&gt;background&lt;/code&gt; 순서: &amp;quot;여백을 적용한 카드 내부에 배경색을 칠한다&amp;quot; → 여백이 미리 적용된 카드에 흰색 여백이 생기고 중간에만 배경색이 적용됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;7. sp와 dp의 차이&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;KanbanBoard.kt&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Q: &lt;code&gt;sp&lt;/code&gt;와 &lt;code&gt;dp&lt;/code&gt;는 어떻게 다른가요? &lt;code&gt;dp&lt;/code&gt;를 &lt;code&gt;fontSize&lt;/code&gt;로 사용하면 어떤 일이 일어나나요?&lt;/li&gt;
&lt;li&gt;A: 아래와 같이 정리했습니다.&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;dp&lt;/strong&gt; (density independent pixel): 화면 기준 픽셀. 같은 기기라면 항상 같은 크기.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;sp&lt;/strong&gt; (scale independent pixel): 가변 글꼴 기준 픽셀. 시스템 글꼴 설정에 따라 1sp당 픽셀 수가 달라짐.&lt;/li&gt;
&lt;li&gt;Text 뷰에 fontSize를 dp로 설정했더니 &lt;code&gt;Argument type mismatch: actual type is &amp;#39;Dp&amp;#39;, but &amp;#39;TextUnit&amp;#39; was expected.&lt;/code&gt; 오류 발생. 애초에 설계가 dp로 폰트 크기를 받지 않겠다는 오류라고 생각.&lt;/li&gt;
&lt;li&gt;기본적으로 폰트 관련 컴포넌트에서는 sp, 이외에는 dp로 사용하면 된다는 것을 알았습니다.&lt;/li&gt;
&lt;li&gt;추가 질문: 만약 시스템 폰트 크기 설정에 따른 폰트 크기 변화에 구애하지 않고 싶을 때에는 어떻게 하면 되는지 궁금합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;8. PreviewParameter 활용&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;KanbanBoard.kt&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Q: Preview 잘 구성해주셨습니다. 확인하고 싶은 텍스트 값들이 달라질 때마다 Preview 함수를 계속 만들어야 할지 고민해보면 좋겠어요. 하드코딩 하지 않고 같은/다른 내용을 보여줄 수 있도록 하는 방법을 찾아봅시다. &lt;code&gt;PreviewParameter&lt;/code&gt; 키워드를 통해 고민해보시면 좋겠습니다.&lt;/li&gt;
&lt;li&gt;A: 실제로 코드로 구현해봤는데 컴포넌트를 줄일 수 있는 점에서 좋다고 생각합니다. 한편으론 Preview 말고도 &lt;code&gt;@PreviewFontScale&lt;/code&gt;, &lt;code&gt;@PreviewScreenSizes&lt;/code&gt; 등 여러가지 Annotation이 있다는 사실을 알 수 있었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;9. 색상만 다른 같은 UI&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;KanbanBoard.kt&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Q: 색상만 다르고 정확히 같은 UI를 구성해야 하는 상황이 오면 어떻게 될까요? 여기서 생기는 문제를 어떻게 피해볼 수 있을까요?&lt;/li&gt;
&lt;li&gt;A: 저라면 색상 값을 매개 변수로 빼겠습니다. 그런데, 색상 값을 매개변수로 빼려고 했는데 아예 Modifier를 매개변수로 받고 색상을 조정하는 건 어떨까 싶었습니다. 그런데, 알고보니 코틀린에서는 Modifier은 기본적으로 바깥으로 빼는 게 맞았습니다. 그렇지만, 호출하는 쪽에서 여백이나 크기를 조절할 수 있게 Modifier를 열어두는 것이었지만 색상 같은 자식의 위젯은 따로 빼는 게 맞다고 생각했습니다. 마침, 이 부분으로 오늘 또 수업이 있어서 좋은 공부가 되었던 것 같습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이번 리뷰에서는 기본적인 주제들에 대해서 많이 질문을 받았다. 컴포즈 기초 지식이나 커밋 방법 등을 점검 받을 수 있었다. 한편으론, DP와 SP를 왜 나누었는지에 대해서 잘 이해가 가지 않는다. 플러터에서는 논리적 픽셀 입력으로 단위도 쓰지 않고 정수로만 크기를 정할 수 있었는데, 컴포즈에서는 .dp, .sp를 붙여야하는 번거로운 방법을 택하고 있다. 또한, 텍스트만 sp 단위를 쓴다면 크기 단위는 정수로 통일하고 텍스트만 따로 처리하면 되지 않을까~ 싶기도 하고.. 이 주제로 새로운 글을 써보는 것도 좋다고 생각이 든다.&lt;/p&gt;</description>
      <category>우테코</category>
      <category>8기</category>
      <category>안드로이드</category>
      <category>우아한테크코스</category>
      <category>우테코</category>
      <author>ao</author>
      <guid isPermaLink="true">https://ao3921.tistory.com/1</guid>
      <comments>https://ao3921.tistory.com/1#entry1comment</comments>
      <pubDate>Mon, 30 Mar 2026 14:46:56 +0900</pubDate>
    </item>
  </channel>
</rss>