<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>베르의 안드로이드</title>
    <link>https://seonghoonc.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 26 Jun 2026 15:22:28 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>베르_최성훈</managingEditor>
    <image>
      <title>베르의 안드로이드</title>
      <url>https://tistory1.daumcdn.net/tistory/5874554/attach/12ee1800d2b74e1282b5eb9bc989e02a</url>
      <link>https://seonghoonc.tistory.com</link>
    </image>
    <item>
      <title>2025년의 이야기</title>
      <link>https://seonghoonc.tistory.com/63</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;어딜가도 연말 분위기가 느껴지는 요즘이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크리스마스가 중요한 행사라고 생각하진 않으면서도 이맘때면 캐롤을 듣곤 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;추운 겨울엔&amp;nbsp;&lt;/span&gt;행사도 약속도 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금이라도 타인의 온기를 느끼고 싶어서일까&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 사람의 올해 이야기에 살아가고 변화하고 있음을 느낀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 비교적 쉽게 변하는 사람인 것 같다. 만나는 사람, 처해진 환경에 따라 많이 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해도 변화가 많이 생겼다. 덜어낸 것도 많고 더한 것도 많다. 이제 그 이야기를 해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;책&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;올해부터 책을 읽기 시작했다. 읽고 싶은 책이 있었다기 보다 책을 읽는게 필요해졌다. AI Agent 의 발전과 릴스, 쇼츠 때문에 뇌가 짧고 불확실한 것, 그리고 그 결과에서 얻는 도파민에 익숙해지고 있다. 문서를 읽는 능력, 비판적으로 사고하는 능력 등이 떨어지는게 느껴졌다. 이를 의도적으로 기피할 필요성이 생겼고 그렇기에 책을 읽기 시작했다. 책은 천천히 차갑게(이성적이게) 사고할 수 있게 만들어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;혼자 하는 것보다 같이 읽고 얘기하면 동기부여가 잘 될 것 같아서 동아리 친구들과 책 세미나를 만들었다. 같은 책을 정해진 분량을 읽고 만나서 수다를 나눈다. 다른 해석도 들어보고 단기 기억을 장기 기억으로 보내는 활동이다. 인간은 무엇인가. 삶에서 중요한 것은 무엇일까. 어떻게 시대의 흐름에서 생존할 수 있을까같은 내용을 얘기를 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;사피엔스&lt;/b&gt;, &lt;b&gt;호모데우스&lt;/b&gt;를 읽었고 지금은 &lt;b&gt;넥서스&lt;/b&gt;를 읽고 있다. 세 개 다 유발하라리의 책이다. 인간을 이렇게 해석할 수 있는게 놀라웠다. 내 삶에 가장 영향을 많이 준 책이 아닐까? 삶의 의미를 빼앗아가면서도 동시에 중요성을 일깨워주는 신기한 책이다. 사피엔스가 돈, 제국, 종교 등 가상의 실제를 만들어 서로 효율적으로 협력하며 이렇게 강력한 힘을 가지게 되었다. 또한 이 힘을 경계하지 않고 잘 사용하지 않으면 해결할 수 없는 재앙이 닥칠 것으로 주장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 책은 진리가 아니다. 틀릴 수 없다고 주장하는 종교, 이데올로기, 문서를 비판하기 때문에 이 책 또한 진리가 될 수 없다. 그 때문에 비판적 개연성을 찾게 되었고 비판적으로 사고할 수 있게 되었다. 이해가 안되던 것들도 이해가 되기 시작했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;함께자라기&lt;/b&gt;도 이 세미나에서 한번 더 읽었었는데 중요한 내용을 상기할 수 있어 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;사내 세미나에서도 책을 몇 권 읽었다. 헬로 스타트업, 상자 밖에 있는 사람, Kotlin Coroutine Deep Dive, 클린 아키텍처, 비폭력대화를 읽었다. 사내 세미나는 보통 일과 연관되어 많이 얘기하는데 원래 일하던 방식과 다른 시각으로 바라보게 되거나 잊었던 것을 상기할 수 있어 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;React Native&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;회사에서 RN 개발을 시작했다. CTO 님의 요청으로 시작했으나 Native 만 딥다이브 해봤던 나에게 좋은 기회였다. 오히려 네이티브를 더 잘 이해해야 가능하다고 들어서 재밌을 것 같았다.&amp;nbsp;이미 돌아가는 센디 안드로이드 앱에 일부만 RN 으로 전환하는 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;brownfield 방식을 사용했다. New Architecture 를 사용했고 Turbo Module 로 브릿징을 구현했다. 그 과정에서 꽤 많은 트러블 슈팅을 경험했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;기존 그래들이 쉽게 해주던 의존성 관리를 벗어나 package.json 에 직접 의존성을 추가하고 버전을 관리혀면서 Okhttp version 이 충돌하고 clean build, code gen 이 안됐다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Android 에서 node 를 못찾고아 패키지 위치 참조도 재조정해주었다. &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;실기기에서 metro 포트 못찾는 문제도 있었고, 난독화 때문에 release 빌드에서 크래시도 발생했으며 ReactNativeHost Deprecated 문제 등 생각보다 많은 트러블 슈팅을 경험했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;완전히 통제 가능한 수준이 되었다고 하면 거짓말일 것이다. AI 의 도움도 많이 받았고 이리 저리 해봐다가 '왜 되지?' 했던 적도 있었다. 그래도 개발 스킬 및 사고의 범위를 확장할 수 있는 좋은 기회였다고 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;관련 내용을 공부해 React Native 가 어떻게 Native 화면을 그리는지를 주제로 동아리 WAP 에서 발표도 했었다. Greenfield 로도 RN 을 해보고 싶어서 Expo 로 사이드 프로젝트 모아동에서 앱을 개발해 출시했다. 부경대 동아리 정보, 모집을 한눈에 보는 서비스인데 잘 됐으면 좋겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Sendy&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;벌써 회사에 온지 1년이 넘었다니 믿겨지지 않는다. 시간이 정말 빠르다. 변하는게 없어서 지루했던 시기도 있었지만 요새는 빨리빨리 무언가를 만들어내고 실험하는게 재밌어졌다. 상반기까지만 해도 개발적으로만 참여하려고 했는데 이제 적응이 됐는지 좀 더 범위를 넓혀서 주도적으로 참여하려고 노력하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이제 좀 개발자로서 어디에 흥미가 있는지 명확해진 것 같다. 누군가는 오픈소스에 기여하고 자동화시키고 하는 것에 흥미가 있지만, 나는 제품에 따른 사용자의 반응이 재밌다. 데이터 분석, UI/UX 쪽에 흥미가 훨씬 큰 것 같아 Product Engineer 를 목표로 나아갈 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;커뮤니티&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;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;Android super init 참여&lt;/li&gt;
&lt;li&gt;GDG Busan 오프보딩&lt;/li&gt;
&lt;li&gt;Google IO Extended Busan&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;올해는 작년에 비해 훨씬 적게 참여했다. 하고싶어서 하는게 아니라고 느껴지는 것들은 덜어내기 시작했다. 덕분에 하고싶은 것에 집중할 수 있었다. 보통 행사가 서울에서 열리니까 못간다는 것은 핑계고 비용과 시간을 들일만큼 흥미, 필요성을 못느끼는 것 같다. 서울에 있었으면 여러 행사에 자주 참여하지 않았을까하는 아쉬움은 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Nextstep Compose&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;스타트업에 몸담고 있어 동료에게 피드백을 많이 받을 수 있는 환경이 아니라는 생각한다. 내가 작성하는 Compose 코드도 점검 받고 어느정도 딥다이브를 해보고 싶어서 NextStep 을 진행했다. 강의를 듣고 미션한 뒤 코드리뷰 받고 개선하는 방식인데 우테코에서 하던 것과 비슷해서 어렵지 않게 해낼 수 있었다. 회사 프로젝트에도 Compose 테스트 코드도 적용해 복잡한 화면의 테스트를 자동화할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;다이어트&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;회사를 다니면서 살이 80kg 까지 쪘었다. 그렇게 8월 쯤 나의 다이어트가 시작되었다. 20대 후반인 것이 크게 동기부여가 되었다. 건강 때문인가? 싶겠지만 그렇지는 않고 곧 20대가 끝나는데 70kg 인 나로도 살아보고 싶어서 빼고 있다. 특별한 약속이 없으면 점심에는 샐러드, 저녁에는 계란 바나나를 먹으면서 지내고 있다. 현재 기준으로 74kg 까지 뺐고 73 ~ 75 왔다갔다 하는 것 같다. 한번도 70kg 까지 빼본적이 없는데 이번엔 꼭 목표까지 빼볼 생각이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리하며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교적 안정적인 한 해를 보낸 것 같다. 덕분에 스스로를 재정비할 수 있는 시간을 가질 수 있었다.&lt;/p&gt;</description>
      <category>회고</category>
      <author>베르_최성훈</author>
      <guid isPermaLink="true">https://seonghoonc.tistory.com/63</guid>
      <comments>https://seonghoonc.tistory.com/63#entry63comment</comments>
      <pubDate>Sun, 21 Dec 2025 15:20:48 +0900</pubDate>
    </item>
    <item>
      <title>추상화 쉽게 설명하기</title>
      <link>https://seonghoonc.tistory.com/62</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;소프트웨어 공학 공부를 하다보면 추상화에 대한 얘기가 반드시, 자주 등장한다. 얼마 전 함께자라기 세미나를 진행하다가 어떻게하면 쉽게 설명할 수 있을까 고민하게 되었고 그러다 좋은 예시가 생각나서 적어보고자한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 가는 학교 처음 보는 교실 처음보는 수학 선생님을 만나 수업을 들은 경험이 있는가? 우리는 곧바로 가르침을 받을 수 있다. 그 선생님이 누구인지, 어떤 사람인지, 얼마나 잘하는지는 내 관심사가 아니다. 의심하지 않고 바로 협력할 수 있다. 내가 아는 선생님(추상화)은 가르침(행위)를 할 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 다른 친구한테 수학을 가르쳐달라고 한 경험을 보자. 처음 보는 친구에게 할 수 있는가? 아니다. 이 친구가 나보다 수학을 잘하는 사람인지(상태) 가르쳐주는걸 할 수 있는 사람인지(행위)에 대해 알아야한다. 즉, 친구에(구현체) 의존적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 교시가 되고 과학시간(요구사항 변경)이다. 과학 선생님이 왔다. 그럼 바로 수업이 가능한가? 당연히 가능하다. 이 사람도 선생님이기 때문이다. 수학 선생님이 은퇴(레거시)하고 새로운 선생님이 왔다. 바로 수업이 가능한가? 당연하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 친구는? 갑자기 모르는 과학문제가 생긴다면? 수학을 가르쳐주던 친구가 전학을 가고 새친구가 온다면? 곧바로 수업이 가능할까? 아니다. 이 친구가 나보다 과학을 잘하는지, 새 친구가 나보다 공부를 잘하는 사람인 지 다시 알아야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 추상화다. 수업이라는 프로그램을 만든다고 생각해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가르침 받는 사람 A 와 가르치는 사람 B가 있을 것이다. 그럼 A 는 B 의 함수를 실행해서 응답을 받고 정보를 습득한다. 가르치는 사람 B 가 이제 은퇴 할 나이(레거시 코드)가 되거나 다른 지식을 가르쳐야 한다(요구사항 변경)면 새로운 C 를 찾아 가르침을 받도록 A 의 코드를 수정해야한다.&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;802&quot; data-origin-height=&quot;474&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brk5IU/btsPDVBCuac/Cf2nNb7H4crxoNGa2tpJa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brk5IU/btsPDVBCuac/Cf2nNb7H4crxoNGa2tpJa1/img.png&quot; data-alt=&quot;화살표는 의존 방향이다. (의존한다는 안다는 것과 같다)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brk5IU/btsPDVBCuac/Cf2nNb7H4crxoNGa2tpJa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbrk5IU%2FbtsPDVBCuac%2FCf2nNb7H4crxoNGa2tpJa1%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;493&quot; height=&quot;291&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;474&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;화살표는 의존 방향이다. (의존한다는 안다는 것과 같다)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 추상화해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가르침(행위)를 가진 사람을 선생님(I)이라고 하자. I 를 구현하는 B&amp;rsquo; 가 존재한다. A가 가르침을 받기 위해서는 B&amp;rsquo; 를 알아야하는가? 그렇지 않다. I 만 알면된다. 즉, 선생님인지만 알면 가르침을 받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B&amp;rsquo; 가 레거시 코드가 되거나 요구사항 변경되면 B&amp;rsquo; 대신 C&amp;rsquo; 로 바꿔끼운다. 그럼 수업은 곧바로 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wVHq8/btsPD0bMPGx/t4PjJiTFH01YHmlv3KlwYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wVHq8/btsPD0bMPGx/t4PjJiTFH01YHmlv3KlwYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wVHq8/btsPD0bMPGx/t4PjJiTFH01YHmlv3KlwYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwVHq8%2FbtsPD0bMPGx%2Ft4PjJiTFH01YHmlv3KlwYk%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;493&quot; height=&quot;327&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;602&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 변경가능한 소프트한 스프트웨어가 만들어진다. 이것이 추상화의 장점이고 의존성 역전이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추상화로 할 수 있는 일부분에 대해서만 알아보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추상화는 엄청나게 새로운 개념이 아니다. 오래전부터 우리는 새로운 걸 찾거나 만들면 이름을 붙인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이런저런 특징을 가지는 것을 &lt;b&gt;&amp;ldquo;슈슈슈슈퍼베르&amp;rdquo;&lt;/b&gt;라고 하자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 다음부터 &lt;b&gt;&amp;ldquo;슈슈슈슈퍼베르&amp;rdquo;&lt;/b&gt;를 보면 당연히 이렇겠지 기대하게되고 이걸 사용해 어떤 문제를 해결하게 될 것이다. 즉, 쉽게 협력할 수 있으며 문제를 유연하게 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;갑자기 유발하라리 사피엔스의 가상의 실제가 떠오른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돈, 제국, 종교처럼 가상의 실제는 서로 효율적인 협력이 가능하게 만들어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추상화, 객체지향 또한, &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;객체가 책임을 가지고 역할에 맞게 동작하여 다른 객체와 유연한 협력이 가능하게 해준다. 비슷한 맥락이 아닐까 싶다.&lt;/span&gt;&lt;/p&gt;</description>
      <author>베르_최성훈</author>
      <guid isPermaLink="true">https://seonghoonc.tistory.com/62</guid>
      <comments>https://seonghoonc.tistory.com/62#entry62comment</comments>
      <pubDate>Fri, 1 Aug 2025 09:24:58 +0900</pubDate>
    </item>
    <item>
      <title>책을 왜 읽어야 할까</title>
      <link>https://seonghoonc.tistory.com/61</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;본래 &lt;span style=&quot;color: #1a5490;&quot;&gt;&lt;b&gt;책&lt;/b&gt;&lt;/span&gt;을 별로 읽지 않았습니다. 책의 중요성을 잘 몰랐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CS나 개발 서적만 좀 읽었던 것 같네요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자로 커리어를 시작하고나서 조금 생각이 바꼈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직업 특성상 세상이 너무 빠르게 변한다는게 피부에 느껴졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빠르게 변하는건 원래 그랬지만 ChatGPT 가 등장하고부터 &lt;span style=&quot;color: #1a5490;&quot;&gt;&lt;b&gt;가속도&lt;/b&gt;&lt;/span&gt;가 점점 붙었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러자 고민이 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어차피 AI 발전이 훨씬 빠를텐데 공부를 많이 하는게 얼마나 큰 의미를 가질까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 지금 공부하는게 몇 년 후 아무 의미 없어지면 어떡하지?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아예 &lt;span style=&quot;color: #1a5490;&quot;&gt;&lt;b&gt;학습하기 힘든 분야&lt;/b&gt;&lt;/span&gt;로 도망가야하는건 아닐까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론, 개발 공부(혹은 전공 공부)가 필요없다는 생각은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 많이 알 수록 AI agent 를 더 잘 사용할 수 있다는 것에 동의합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이드 프로젝트 하는 것과 다르게 업무 도메인은 훨씬 복잡하기도하구요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 세상은 &lt;span style=&quot;color: #1a5490;&quot;&gt;&lt;b&gt;예측 불가능&lt;/b&gt;&lt;/span&gt;하게 변합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세상이 변해도 유의미한 지식이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 에 의해 제 뇌의 &lt;span style=&quot;color: #1a5490;&quot;&gt;&lt;b&gt;비판적 사고 능력&lt;/b&gt;&lt;/span&gt;이 떨어지는 것도 경계하고싶고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;&lt;b&gt;쉬운 지식을 쉽게&lt;/b&gt;&lt;/span&gt; 얻는 세상에서 &lt;span style=&quot;color: #1a5490;&quot;&gt;&lt;b&gt;어려운 지식을 어렵게&lt;/b&gt;&lt;/span&gt;&amp;nbsp;얻는 방법이 뭐가 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 책을 읽기 시작했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같이 하는 것에 동기부여를 많이 받아서 동아리에 책 세미나를 만들어 운영중입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정해진 양만큼 책을 읽고 중요한 문장을 기록합니다. 만나서 해당 내용에 대해 토의합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 세미나 시작한지 세 달 정도 됐는데 이제 이 글을 쓰네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 책으로 읽었던 유발 하라리의&lt;span style=&quot;color: #781b33;&quot;&gt; &lt;b&gt;사피엔스&lt;/b&gt;&lt;/span&gt;가 끝났고, 다음은&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt; 함께자라기&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함께자라기는 이미 반 년 전에 읽었지만 워낙 좋은 책이기도하고 많이 까먹어서 다시 읽고 실천하고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무리 없이 꾸준히 읽는 것을 목표로 하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 책을 다 읽으면 짧게 독후감 남기도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다.&lt;/p&gt;</description>
      <category>책</category>
      <author>베르_최성훈</author>
      <guid isPermaLink="true">https://seonghoonc.tistory.com/61</guid>
      <comments>https://seonghoonc.tistory.com/61#entry61comment</comments>
      <pubDate>Sun, 20 Jul 2025 13:08:21 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 런타임 권한 요청</title>
      <link>https://seonghoonc.tistory.com/59</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Intro&lt;/b&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;b&gt;런타임 권한 요청&lt;/b&gt;에 대해 공부해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;권한 요청 종류&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 안드로이드의 권한 요청에는 두 가지가 있다.&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;install-time, Runtime&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.&amp;nbsp; Install-time permission&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱의 샌드박스 영역을 벗어난 데이터나 작업에 접근할 수 있도록 허용하지만, 사용자의 프라이버시에 제한적인 위험만을 초래하는 데이터(상대적으로 덜 위험한 권한)에 한한다. 예시로 인터넷 권한이 있다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&amp;nbsp;Android 플랫폼은 Linux 사용자 기반 보호 기능을 활용하여 앱 리소스를 식별하고 분리한다. 이러한 방법으로 앱을 서로 분리하고 앱과 시스템을 악성 앱으로부터 보호한다. 이를 위해 Android는 각 Android 애플리케이션에 고유한 사용자 ID(UID)를 할당하여 자체 프로세스에서 실행한다. Android는 UID를 사용하여 커널 수준의 애플리케이션 샌드박스를 설정한다.&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://source.android.com/docs/security/app-sandbox?hl=ko&quot;&gt;https://source.android.com/docs/security/app-sandbox?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1743253097657&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;애플리케이션 샌드박스 &amp;nbsp;|&amp;nbsp; Android Open Source Project&quot; data-og-description=&quot;2025년 3월 27일부터 AOSP를 빌드하고 기여하려면 aosp-main 대신 android-latest-release를 사용하는 것이 좋습니다. 자세한 내용은 AOSP 변경사항을 참고하세요. 이 페이지는 Cloud Translation API를 통해 번역되&quot; data-og-host=&quot;source.android.com&quot; data-og-source-url=&quot;https://source.android.com/docs/security/app-sandbox?hl=ko&quot; data-og-url=&quot;https://source.android.com/docs/security/app-sandbox?hl=ko&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://source.android.com/docs/security/app-sandbox?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://source.android.com/docs/security/app-sandbox?hl=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;애플리케이션 샌드박스 &amp;nbsp;|&amp;nbsp; Android Open Source Project&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;2025년 3월 27일부터 AOSP를 빌드하고 기여하려면 aosp-main 대신 android-latest-release를 사용하는 것이 좋습니다. 자세한 내용은 AOSP 변경사항을 참고하세요. 이 페이지는 Cloud Translation API를 통해 번역되&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;source.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;요약하자면, Android 는 커널 수준에서 샌드박스를 설정하여 앱과 앱을 서로 분리하고 보호한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Runtime permissions&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 개인정보에 관련된 데이터에 접근 권한을 부여한다. 예시로 사진, 알림, 위치 권한 등이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중에서 런타임 권한을 어떻게 요청하는지 좀 더 깊게 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;런타임 권한 요청&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;런타임 권한을 요청하기 위한 기본 원칙은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 권한이 &lt;b&gt;필요한 기능과 상호작용하기 시작할 때&lt;/b&gt; 컨텍스트에 따라 권한을 요청합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용자를 차단하지 않습니다&lt;/b&gt;. 교육용 UI 흐름(예: 권한 요청의 근거를 설명하는 흐름)을 취소하는 옵션을 항상 제공합니다.&lt;/li&gt;
&lt;li&gt;사용자가 기능에 &lt;b&gt;필요한 권한을 거부하거나 취소하면 권한이 필요한 기능을 사용 중지&lt;/b&gt;하는 등의 방법으로 앱의 성능을 단계적으로 저하시켜 사용자가 &lt;b&gt;앱을 계속 사용할 수 있도록&lt;/b&gt; 합니다.&lt;/li&gt;
&lt;li&gt;시스템 동작을 가정하지 않습니다. 예를 들어 여러 권한이 동일한&lt;span&gt;&amp;nbsp;&lt;/span&gt;권한 그룹에 표시된다고 가정하지 마세요. 권한 그룹은 앱이 밀접하게 관련된 여러 권한을 요청할 때 시스템에서 사용자에게 표시하는 시스템 대화상자의 수를 최소화하는 데만 도움이 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한이 필요한 기능을 사용할 때 권한을 요청해야하고, 권한을 거부하더라도 앱을 계속 사용할 수 있게 해야한다. 마지막 원칙은 잘 이해가 되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 원칙에 대해 이해가 잘 되지 않았는데 아래 글을 읽어보길 추천 받았다. 혹시 이해가 안되는 독자가 있다면 추천한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #66666e; text-align: left;&quot;&gt;&lt;a href=&quot;https://developer.android.com/guide/topics/permissions/overview#groups&quot;&gt;https://developer.android.com/guide/topics/permissions/overview#groups&lt;/a&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #66666e; text-align: left;&quot;&gt;- 권한 그룹은 연관있는 권한(e.g. 메세지 전송 권한, 수신 권한)을 카테고리화해서, 사용자에게 필요한 권한 목록을 한 UI에 담아서 보여준다. 하지만 권한 그룹이 예고없이 변경될 가능성이 있어 특정 권한이 특정 그룹에 속할 것이라고 함부로 가정하면 안된다. 따라서 시스템의 권한 요청 로직을 개발자가 완전히 제어할 수 없다는 것을 의미한다&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1743252783641&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Android에서의 권한 &amp;nbsp;|&amp;nbsp; Privacy &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;이 페이지는 Cloud Translation API를 통해 번역되었습니다. Android에서의 권한 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 앱 권한은 다음 항목에 대한 액세스&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/guide/topics/permissions/overview#groups&quot; data-og-url=&quot;https://developer.android.com/guide/topics/permissions/overview?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/L5rG3/hyYvqNjhTU/gfdpyEPvFA18hIdJZK7EuK/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676&quot;&gt;&lt;a href=&quot;https://developer.android.com/guide/topics/permissions/overview#groups&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/guide/topics/permissions/overview#groups&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/L5rG3/hyYvqNjhTU/gfdpyEPvFA18hIdJZK7EuK/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Android에서의 권한 &amp;nbsp;|&amp;nbsp; Privacy &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 페이지는 Cloud Translation API를 통해 번역되었습니다. Android에서의 권한 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 앱 권한은 다음 항목에 대한 액세스&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;런타임 권한 요청 워크플로&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한 요청이 정말 필요한지 판단한 후에 다음과 같은 단계로 권한을 요청할 수 있다. 권한 요청 워크플로는 8 단계로 이루어져있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vIOlf/btsMS5AJm0T/wxqQuP08CrCBPNCnJ3gtgk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vIOlf/btsMS5AJm0T/wxqQuP08CrCBPNCnJ3gtgk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vIOlf/btsMS5AJm0T/wxqQuP08CrCBPNCnJ3gtgk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvIOlf%2FbtsMS5AJm0T%2FwxqQuP08CrCBPNCnJ3gtgk%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;691&quot; height=&quot;363&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;672&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;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;앱의 &lt;b&gt;매니페스트에 권한을 선언&lt;/b&gt;한다.&lt;/li&gt;
&lt;li&gt;앱의 특정 작업이&lt;b&gt; 알맞은 런타임 권한과 연결되도록 앱의 UX를 설계&lt;/b&gt;한다. 앱이 &lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: left;&quot;&gt;비공개 사용자 데이터(Private User Data)&lt;/span&gt;에 액세스하도록 권한을 부여해야 할 수 있는 작업을 사용자에게 알린다.&lt;/li&gt;
&lt;li&gt;사용자가 권한이 필요한 작업을 실행할 때까지 기다린다. 실행하면 런타임 권한을 요청한다.&lt;/li&gt;
&lt;li&gt;사용자가 &lt;b&gt;이미 앱에 필요한&lt;span&gt;&amp;nbsp;&lt;/span&gt;런타임 권한을 부여했는지 확인&lt;/b&gt;한다. &lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: left;&quot;&gt;런타임 권한이 필요한 작업을 실행할 때마다 권한이 있는지 확인해야 한다.&lt;/span&gt;&lt;br /&gt;&lt;b&gt;권한 부여 O &lt;/b&gt;-&amp;gt; 비공개 사용자 데이터에 액세스할 수 있다. &lt;br /&gt;&lt;b&gt;권한 부여 X &lt;/b&gt;-&amp;gt;&amp;nbsp; 다음 단계로 이동&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;사용자에게 앱에서 근거를 표시해야하는지 확인한다.&amp;nbsp;&lt;br /&gt;&lt;b&gt;설명이 필요 O&lt;/b&gt; -&amp;gt; 근거를 UI 요소로 표시한다. &lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;앱이 액세스하려는 데이터가 무엇인지, 런타임 권한을 부여하면 앱이 사용자에게 제공할 수 있는 이점이 무엇인지 명확하게 설명해야 한다.&lt;/span&gt;&lt;br /&gt;&lt;b&gt;설명이 필요 X&lt;/b&gt; -&amp;gt; 다음 단계로 이동&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;앱에서 비공개 사용자 데이터에 액세스하는 데 필요한&lt;span&gt; 런타임 권한을 요청한다.&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;런타임 권한 부여를 선택했는지 또는 거부를 선택했는지 사용자의 응답을 확인한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;권한 허용 O &lt;/b&gt;-&amp;gt; 비공개 사용자 데이터에 액세스할 수 있다. &lt;br /&gt;&lt;b&gt;권한 허용 X &lt;/b&gt;-&amp;gt;&amp;nbsp;사용자에게 기능을 제공하도록&lt;span&gt; 앱 환경의 성능을 단계적으로 저하한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;권한 요청 및 작업 실행&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 권한이 필요한 작업일 때 권한 요청하는 과정을 코드로 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Activity 또는 Fragment 초기화 로직에서 다음과 같이 ActivityResultCallback 을 정의한다. 시스템 다이얼로그로 사용자에게 권한 요청을 했을 때 응답에 따라 어떻게 처리할지 정의할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1742790196250&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val requestPermissionLauncher =
    registerForActivityResult(RequestPermission()
    ) { isGranted: Boolean -&amp;gt;
        if (isGranted) {
           // 권한을 부여했으니 기능 실행
        } else {
            // 권한을 거부하였기 때문에 기능을 사용하지 못함을 사용자에게 알린다.
            // 동시에 거부했다고해서 바로 설정 화면으로 넘기거나 하지말라. 사용자의 결정을 존중하라.
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 권한이 필요한 기능을 사용할 때 권한 요청 로직을 수행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1742790873653&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;when {
    ContextCompat.checkSelfPermission(
            context,
            Manifest.permission.REQUESTED_PERMISSION
            ) == PackageManager.PERMISSION_GRANTED -&amp;gt; {
         // 권한이 이미 있음.
    }
    ActivityCompat.shouldShowRequestPermissionRationale(
            this, Manifest.permission.REQUESTED_PERMISSION) -&amp;gt; {
        // 권한이 이 기능에 왜 필요한지, 어떤 권한이 필요한지 자세히 설명한다. 
        // 권한을 거부했을 때 사용할 수 없는 기능을 설명한다.
        // 이 UI에는 사용자가 권한을 부여하지 않고도 앱을 계속 사용할 수 있도록
        // &amp;ldquo;취소&amp;rdquo; 또는 &amp;ldquo;괜찮아요&amp;rdquo; 버튼을 포함한다.
        showInContextUI(...)
    }
    else -&amp;gt; {
        // 권한을 요청한다. 다시 묻지 않음 상태면 실행되지 않는다.
        requestPermissionLauncher.launch(
                Manifest.permission.REQUESTED_PERMISSION)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;requestPermissions(), onRequestPermissionsResult() 를 사용해 직접 권한을 요청할 수도 있다고 공식문서에서 설명되어있지만 현재 Deprecated 되었다.&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;1280&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3wHzq/btsM2oy1MnI/KuRX5dIL9iBMK4bkmwaaTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3wHzq/btsM2oy1MnI/KuRX5dIL9iBMK4bkmwaaTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3wHzq/btsM2oy1MnI/KuRX5dIL9iBMK4bkmwaaTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3wHzq%2FbtsM2oy1MnI%2FKuRX5dIL9iBMK4bkmwaaTK%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;776&quot; height=&quot;232&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;382&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;이 메서드는 ActivityResultContract를 통해 타입 안전성이 강화된 Activity Result API를 사용하기 위해 더 이상 권장되지 않는다. android.activity.result.contract 패키지에서 제공하는 사전 구성된 여러 인텐트 계약을 사용할 수 있으며, ActivityResultContracts는 테스트를 위한 훅(hook)을 제공하고, 프래그먼트와 독립적인 별도의 테스트 가능한 클래스에서 결과를 받을 수 있도록 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;권한 거부 처리&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;사용자가 권한을 거부하면 거부에 따른 영향을 이해하도록 지원해야한다. &lt;/span&gt;&lt;span&gt;권한이 없어서 작동하지 않는 기능을 사용자가 인식하도록 해야한다. 권장 사항은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;- &lt;b&gt;사용자의 주의를 유도한다.&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;앱에 필요한 권한이 없어서 기능이 제한된 앱 UI의 특정 부분을 강조표시한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: left;&quot;&gt;- &lt;/span&gt;&lt;/span&gt;&lt;b&gt;자세히 설명한다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;일반적인 메시지는 표시하지 않아야한다. 대신 앱에 필요한 권한이 없어서 어떤 기능을 사용할 수 없는지 명확히 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;UI 를 차단하지 않는다. &lt;/b&gt;앱을 계속 사용하는 것을 막는 전체 화면 경고 메시지를 표시하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;동시에 앱은 권한을 거부하겠다는 사용자의 결정을 존중해야 한다. Android 11(API 수준 30)부터 사용자가 앱이 기기에 설치된 전체 기간 동안 특정 권한에 관해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;거부&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;&lt;b&gt;를 두 번 이상 탭하면 앱에서 그 권한을 다시 요청하는 경우 사용자에게 시스템 권한 대화상자가 표시되지 않는다.&lt;/b&gt;이러한 사용자의 작업은 '다시 묻지 않음'을 의미한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;Outro&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;오늘은 런타임 권한 요청에 대해 간단하게 알아보았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;권한이 필요한 기능이 많은데 권한을 어떻게하면 잘 얻을 수 있을지 고민해봐야겠다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android</category>
      <author>베르_최성훈</author>
      <guid isPermaLink="true">https://seonghoonc.tistory.com/59</guid>
      <comments>https://seonghoonc.tistory.com/59#entry59comment</comments>
      <pubDate>Sun, 23 Mar 2025 23:40:36 +0900</pubDate>
    </item>
    <item>
      <title>[Jetpack Compose] 공식문서 읽기 Side-effects in Compose Part 2</title>
      <link>https://seonghoonc.tistory.com/58</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은&lt;b&gt; &quot;[Jetpack Compose] 공식문서 읽기 Side-effects in Compose Part 1&quot;&amp;nbsp;&lt;/b&gt;에 이어진 글입니다. 오늘도 공식문서를 읽어보며 Jetpack Compose 의 SideEffect 에 대해 공부해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://seonghoonc.tistory.com/56&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://seonghoonc.tistory.com/56&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740232932651&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Jetpack Compose] 공식문서 읽기 Side-effects in Compose Part 1&quot; data-og-description=&quot;들어가며&amp;nbsp;LaunchedEffect, DisposableEffect 를 많이 사용하는데 제대로 알고 사용하자는 의미로 공식문서를 읽어보며 SideEffect 에 대해 공부해보겠습니다. 제 생각과 의역이 들어가기 때문에 정확한 정&quot; data-og-host=&quot;seonghoonc.tistory.com&quot; data-og-source-url=&quot;https://seonghoonc.tistory.com/56&quot; data-og-url=&quot;https://seonghoonc.tistory.com/56&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/zqPCN/hyYjt9UrzT/P3F2wikh2LyJNQS7GnIyC1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/ptPXM/hyYfWeRqAL/kZAcoGy0DYcfxkzMGc9aF1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://seonghoonc.tistory.com/56&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://seonghoonc.tistory.com/56&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/zqPCN/hyYjt9UrzT/P3F2wikh2LyJNQS7GnIyC1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/ptPXM/hyYfWeRqAL/kZAcoGy0DYcfxkzMGc9aF1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Jetpack Compose] 공식문서 읽기 Side-effects in Compose Part 1&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가며&amp;nbsp;LaunchedEffect, DisposableEffect 를 많이 사용하는데 제대로 알고 사용하자는 의미로 공식문서를 읽어보며 SideEffect 에 대해 공부해보겠습니다. 제 생각과 의역이 들어가기 때문에 정확한 정&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;seonghoonc.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;State%20and%20effect%20use%20cases-1&quot; style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;State and effect use cases&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. &lt;b&gt;SideEffect&lt;/b&gt;: publish Compose state to non-Compose code&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;compose state 를 compose 가 관리되지 않는 객체에 공유하고 싶다면 SideEffect Composable 을 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;recomposition 이 성공된 후에 실행됩니다. 반면에, composable 내부에 effect 를 직접 작성하여 recomposition 이 성공하기 전에 Effect 를 실행해서는 안됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 analytics 가 이후 발생하는 모든 analytics 이벤트에 커스텀 메타데이터를 전달하여 사용자들을 세분화 할 수 있습니다. 즉, SideEffect 를 사용해 현재 사용자의 타입을 analytics 라이브러리에 전달할 수 있는 것이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1740232868720&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        FirebaseAnalytics()
    }

    // 컴포지션이 성공할 때마다 Analytics 를 새 userType으로 업데이트
    SideEffect {
        analytics.setUserProperty(&quot;userType&quot;, user.userType)
    }
    return analytics
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. &lt;b&gt;produceState&lt;/b&gt;: convert non-Compose state into Compose state&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;produceState 는 Composition 에 Coroutine Scope 를 launch 해 반환된 State 값을 전달할 수 있습니다. 이를 사용해 Compose 외부의 상태를 Compose 상태로 변환할 수 있으며 Flow, LiveData, RxJava 와 같이 구독 기반의 상태를 Composition 으로 가져올 때 유용하죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;producer 는 produceState 가 Composition 되었을 때만 실행되면, Composition 이 끝나면 취소됩니다. 또한 같은 값이 setValue 되면 recomposition 을 트리거하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;produceState 는 코루틴을 생성하지만, non-suspending Data Source 도 관찰할 수 있습니다. 만약 구독을 해제하고 싶다면 awaitDispose 함수를 사용하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 예시는 produceState 를 사용해 네트워크에서 이미지를 로드합니다. loadNetworkImage Composable 함수는 다른 Composable 함수가 사용할 수 있는 State 를&amp;nbsp; 반환하죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1740234358417&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun loadNetworkImage(
    url: String,
    imageRepository: ImageRepository = ImageRepository()
): State&amp;lt;Result&amp;lt;Image&amp;gt;&amp;gt; {

    // 초기값 Result.Loading 으로 생성된다.
    // url 혹은 imageRepository 가 변경되면 실행중인 producer 가 취소되고 다시 실행된다.
    return produceState&amp;lt;Result&amp;lt;Image&amp;gt;&amp;gt;(initialValue = Result.Loading, url, imageRepository) {
        val image = imageRepository.load(url)

        // State 를 Error 나 Success 로 업데이트한다. 
        // 다른 composable 이 state 를 보고 있다면 recomposition 이 트리거 될 것이다.
        value = if (image == null) {
            Result.Error
        } else {
            Result.Success(image)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주의할 점은 returnType 이 있는 Composable 은 일반 함수와 같이 소문자로 시작해야한다는 것입니다. 다른 Composable 이 대문자로 시작하는 것과 차이가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;7.&lt;b&gt;&lt;span&gt; derivedStateOf&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;: convert one or multiple state objects into another state&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose 에서 관찰중인 state 나 composable input 이 변경되면 매번 Recomposition 이 발생합니다. 이는 UI 가 실제 업데이트 해야되는 것보다 자주 일어날 수도 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;recompose 가 필요한 시점보다 더 자주 상태가 변경된다면 derivedStateOf 를 사용해야합니다. 예를 들어 스크롤 위치처럼 자주 변하는 값이 있을 때, Composable 이 특정 위치 이상을 넘었을 때만 Recomposition 하면 되는 경우가 있습니다. 오직 update 가 필요할 때만 관찰되는 state 를 만들 수 있습니다. kotlin Flow 의 distinctUntilChanged&lt;a style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; href=&quot;https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/distinct-until-changed.html#:%7E:text=Returns%20flow%20where%20all%20subsequent,a%20StateFlow%20has%20no%20effect.&quot;&gt;()&lt;/a&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;그러나, 공식문서에 따르면 derivedStateOf 는 더 expensive 하다고 표현됩니다. 일반적으로 state 를 관찰하는 것보다 비용이 많이 든다는 뜻이겠죠. 꼭 필요할때만 사용하는게 좋다고합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 올바른 사용 방법&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1740235092537&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
// messages 가 변경되면 MessageList 가 재구성되겠죠.
// derivedStateOf 는 이 재구성이랑은 상관이 없습니다(영향을 주지 않습니다). 
fun MessageList(messages: List&amp;lt;Message&amp;gt;) {
    Box {
        val listState = rememberLazyListState()

        LazyColumn(state = listState) {
            // ...
        }

        // 첫번째로 보이는 item 의 index 가 0 이상이면 버튼을 표시합니다.
        // 하지만 1, 2, 3, 4, ... 그 이후로 계속 재구성되겠죠.
        // 이를 최소화하기 위해 derivedStateOf 를 사용했습니다.
        val showButton by remember {
            derivedStateOf {
                listState.firstVisibleItemIndex &amp;gt; 0
            }
        }

        AnimatedVisibility(visible = showButton) {
            ScrollToTopButton()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 잘못된 사용 방법&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1740235650453&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 이렇게 사용해서는 안됩니다.
var firstName by remember { mutableStateOf(&quot;&quot;) }
var lastName by remember { mutableStateOf(&quot;&quot;) }

val fullNameBad by remember { derivedStateOf { &quot;$firstName $lastName&quot; } } // This is bad!!!
val fullNameCorrect = &quot;$firstName $lastName&quot; // This is correct&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 firstName 혹은 lastName 이 변경될 때마다 무조건 Recomposition 이 일어나야합니다. 이런 경우엔 derivedStateOf 가 오히려 성능 저하를 일으킬 수 있습니다. 그러므로 필요할 때만 사용해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8. &lt;b&gt;snapshotFlow&lt;/b&gt; : convert Compose's State into Flows&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;snapshotFlow 는 compose State 를 cold Flow 로 바꿔줍니다. snapshotFlow는 수집(collect)할 때&amp;nbsp; 블록을 실행하며, 그 안에서 읽은 State 객체의 결과를 발행(emit)합니다. 만약 snapshotFlow 내에서 State 객체 중 하나가 변경되면, 새 값이 이전에 발행된 값과 다를 경우 Flow 는 collector 에게 새로운 값을 emit 합니다. 이 동작은 Flow.distinctUntilChanged와 유사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 예제는 사용자가 리스트에서 첫 번째 항목을 넘어서 스크롤할 때 이를 분석(analytics)에 기록하는 부작용(side effect)을 보여줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1740236148156&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -&amp;gt; index &amp;gt; 0 }
        .distinctUntilChanged()
        .filter { it == true }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Restarting effects&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose 의 LaunchedEffect&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;produceState&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;, or&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;DisposableEffect 같은 effect 들은 여러 key 인자들을 받는데, 변경될 때마다 실행중인 effect 가 cancel 되고 새로운 effect 가 시작됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1740236367443&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 특성 때문에 올바르게 사용되지 않는다면 버그나 비효율을 야기할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 만큼 발생하지 않는다&amp;nbsp; -&amp;gt; 버그&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요 이상으로 발생한다 -&amp;gt; 비효율&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로, Effect 코드 블록에서 사용되는 mutable 변수와 immutable 변수는 Effect Composable 의 매개변수로 추가되어야 합니다. 이 외에도, Effect 를 강제로 다시 시작하기 위해 추가 매개변수를 넣을 수 있습니다. 만약 변수의 변경이 Effect 를 다시 시작하게 해서는 안 된다면, 해당 변수를 rememberUpdatedState로 감싸야 합니다. 만약 그 변수가 키 없이 remember로 감싸져서 한 번도 변경되지 않는다면, 그 변수를 Effect 의 키로 전달할 필요가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시에서는 onState, onStop 이 rememberUpdatedState 때문에 변경되지 않기 때문에 DiposableEffect 의 key 로 전달할 필요가 없습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1740236775891&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun HomeScreen(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onStart: () -&amp;gt; Unit, // Send the 'started' analytics event
    onStop: () -&amp;gt; Unit // Send the 'stopped' analytics event
) {
    // These values never change in Composition
    val currentOnStart by rememberUpdatedState(onStart)
    val currentOnStop by rememberUpdatedState(onStop)

    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event -&amp;gt;
            /* ... */
        }

        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 lifecycleOwner를 DisposableEffect 의 매개변수로 전달하지 않고 변경된다면, HomeScreen은 재구성되지만 DisposableEffect는 폐기되거나 재시작되지 않습니다. 이로 인해 이후로 잘못된 lifecycleOwner가 사용되어 문제가 발생하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Constants as keys&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;true 같은 effect key 를 사용하면 Lifecycle 을 따르도록 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리하며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 &lt;b&gt;SideEffect, &lt;b&gt;produceState, &lt;b&gt;&lt;span&gt;derivedStateOf, &lt;b&gt;snapshotFlow &lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;span&gt;에 대해 공부해보았습니다. 오늘 공부한 것들은 한 번에 이해되지 않아 여러번 읽어보았네요. 조금 더 효율적으로 작성할 수 있는지 고민해보면서 써야겠습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Jetpack Compose</category>
      <author>베르_최성훈</author>
      <guid isPermaLink="true">https://seonghoonc.tistory.com/58</guid>
      <comments>https://seonghoonc.tistory.com/58#entry58comment</comments>
      <pubDate>Sun, 23 Feb 2025 00:13:42 +0900</pubDate>
    </item>
    <item>
      <title>[Jetpack Compose] Testing in Jetpack Compose</title>
      <link>https://seonghoonc.tistory.com/57</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;들어가기 전에&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;테스트에 관련된 글을 쓰는게 정말 오랜만인 것 같네요. 저는 JUnit4, JUnit5, Kotest 를 사용한 단위 테스트, Robolectic 을 사용한 통합 테스트, Espresso 를 사용한 UI 테스트를 작성한 경험이 있습니다. Compose Test 는 최근에 NextStep 학습 테스트로 배우는 Compose 강의를 듣기 시작하면서 접하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI 테스트는 여러 버전에서, 다양한 단말기가 문제 없이 의도한 대로 동작하는지 쉽게 테스트 할 수 있습니다. 에러를 쉽게 찾거나 앱의 퀄리티 향상에 도움이 되죠.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Key Concepts&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose 코드 테스트의 핵심 개념은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://developer.android.com/develop/ui/compose/testing/semantics&quot;&gt;Semantics&lt;/a&gt;&lt;/b&gt;: &lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: left;&quot;&gt;Compose 테스트는&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: left;&quot;&gt;UI 의 각 부분에 각각 의미를 부여하는 &lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: left;&quot;&gt;semantics 와 상호작용합니다. &lt;/span&gt;&lt;/span&gt;semantics 는&amp;nbsp; UI 계층 구조와 함께 생성됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://developer.android.com/develop/ui/compose/testing/apis&quot;&gt;Testing APIs&lt;/a&gt;&lt;/b&gt;: Compose 는 요소를 찾고(finders), 상태를 검증(Assertion)하며, 버튼 클릭 같은 액션(Actions)을 수행하는 Test API 를 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://developer.android.com/develop/ui/compose/testing/synchronization&quot;&gt;Synchronization&lt;/a&gt;&lt;/b&gt;: 기본적으로 Compose 테스트는 UI와 자동으로 동기화되어, 어설션을 수행하거나 행동을 실행하기 전에 UI가 idle 상태가 될 때까지 기다립니다.UI가 idle 상태라는 것은 모든 pending 작업이나 애니메이션, recomposition 등이 완료되어 더 이상 변경 사항이 없는 상태를 의미합니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://developer.android.com/develop/ui/compose/testing/interoperability&quot;&gt;Interoperability&lt;/a&gt;&lt;/b&gt;: 하이브리드 앱에서 Compose 와 View 기반 요소 모두와 상호작용 할 수 있습니다. 또한 다른 테스트 프레임워크와 통합해서 사용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 중에서 오늘은 Test API 에 대해 좀 더 알아보고 실제로 적용해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt;Compose Testing API&lt;/b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 봤듯 Compose test 에서 UI 요소와 상호작용할 수 있는 방법은 세 가지가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #555555; text-align: left;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Finders : &lt;/b&gt;하나 이상의 요소를 선택해 Assertion 을 만들거나 작업을 실행할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Assertion&lt;/b&gt;: 요소가 존재하는지 혹은 어떤 속성을 가지는지 확인하는데 사용됩니다. 예를 들어 &quot;환영합니다&quot; 라는 텍스트가 존재하는지 확인할 수 있겠죠.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Actions&lt;/b&gt;: user event 를 요소에 주입합니다. 클릭하거나 제스처를 취하는 등의 시뮬레이션이 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 세 가지를 사용하면 &quot;+&quot; 라는 텍스트를 가진 버튼을 찾고(Finders) 버튼을 클릭한 뒤(Actions) count 가 1이 되는지 확인(Assertion)할 수 있겠죠? 그런 방식으로 사용할 수 있습니다. 공식 영상에서 보여준 계산기 예시처럼 1, +, 2, = 을 순서대로 클릭하고 결과가 3이 나오는지 확인할 수도 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;비밀번호 입력 창 만들기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 어떻게 사용하는지 확인해봅시다. 간단한 요구사항을 만들고 기능이 잘 동작하는지 테스트해보겠습니다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;비밀번호 입력 창을 만든다.&lt;br /&gt;- [ ] 비밀번호는 6자이다.&lt;br /&gt;- [ ] 비밀번호는 숫자만 입력 가능하다. 문자 입력을 제한하지는 않는다.&lt;br /&gt;- [ ] 입력한 비밀번호는 보이지 않아야한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;TextField 로 비밀번호 입력 받기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 메테리얼 TextField 를 사용해 만들어보았습니다. 먼저 기본적으로 입력 가능하게 만들어 주었습니다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739104078485&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun PasswordTextField(modifier: Modifier = Modifier) {
    var password by remember { mutableStateOf(&quot;&quot;) }

    TextField(
        value = password,
        label = { Text(&quot;비밀번호 입력&quot;) },
        onValueChange = { value -&amp;gt; password = value},
        modifier = modifier
            .fillMaxWidth()
            .padding(16.dp)
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Video_2025-02-09-20-58-35.gif&quot; data-origin-width=&quot;334&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ErIoS/btsMbFvCBeG/6SOrnjmAD7Zbrsk7sLHHZ1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ErIoS/btsMbFvCBeG/6SOrnjmAD7Zbrsk7sLHHZ1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ErIoS/btsMbFvCBeG/6SOrnjmAD7Zbrsk7sLHHZ1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/ErIoS/btsMbFvCBeG/6SOrnjmAD7Zbrsk7sLHHZ1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;299&quot; height=&quot;645&quot; data-filename=&quot;KakaoTalk_Video_2025-02-09-20-58-35.gif&quot; data-origin-width=&quot;334&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;비밀번호 가리기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항을 만족할 수 있게 테스트를 먼저 작성해볼까요?&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;- [ ] 입력한 비밀번호는 보이지 않아야한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호는 입력해도 화면에 표시되지 않아야겠죠? 그럼 다음과 같이 테스트를 작성할 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;UI 테스트는 안드로이드 환경에서 동작하기 때문에 AndroidTest 패키지에 만들어주어야 합니다!&lt;/b&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1739104009341&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class PasswordTextFieldTest {
    // JUnit Rule 로 컴포즈 테스트 환경 구성
    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun 비밀번호를_입력해도_비밀번호가_보이지_않는다() {
        val password = &quot;123456&quot;

        // 테스트 중 렌더링 할 Compose UI
        composeTestRule.setContent {
            MaterialTheme {
                PasswordTextField()
            }
        }

        composeTestRule
            .onNodeWithText(&quot;비밀번호 입력&quot;) // 텍스트로 비밀번호 UI 노드를 찾는다.
            .performTextInput(password) // 비밀번호를 입력한다.

        composeTestRule
            .onNodeWithText(password)
            .assertDoesNotExist() // 비밀번호가 보이는지 확인한다.
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 실행해보면 바로... 실패! PasswordTextField 에 비밀번호 입력이 보이지 않도록하는 코드를&lt;span style=&quot;background-color: #ffffff; color: #66666e; text-align: left;&quot;&gt;&amp;nbsp;&lt;/span&gt;않았기 때문이죠.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1260&quot; data-origin-height=&quot;166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dkQ1ol/btsMbKKyevf/2w640OhxiwpBfA2utHqAlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dkQ1ol/btsMbKKyevf/2w640OhxiwpBfA2utHqAlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dkQ1ol/btsMbKKyevf/2w640OhxiwpBfA2utHqAlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdkQ1ol%2FbtsMbKKyevf%2F2w640OhxiwpBfA2utHqAlK%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;700&quot; height=&quot;92&quot; data-origin-width=&quot;1260&quot; data-origin-height=&quot;166&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;visualTransformation 인자로 PasswordVisualTransformation 을 객체를 전달하고 다시 실행시켜보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739104134894&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun PasswordTextField(modifier: Modifier = Modifier) {
    var password by remember { mutableStateOf(&quot;&quot;) }

    TextField(
        value = password,
        label = { Text(&quot;비밀번호 입력&quot;) },
        onValueChange = { value -&amp;gt; password = value },
        visualTransformation = PasswordVisualTransformation(), // 추가
        modifier = modifier
            .fillMaxWidth()
            .padding(16.dp)
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과.. 성공! 이런식으로 Composable 함수를 테스트 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1274&quot; data-origin-height=&quot;184&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ytB0W/btsMa9c5nC2/otWuIYsaZQOF6XUZKwuHbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ytB0W/btsMa9c5nC2/otWuIYsaZQOF6XUZKwuHbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ytB0W/btsMa9c5nC2/otWuIYsaZQOF6XUZKwuHbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FytB0W%2FbtsMa9c5nC2%2FotWuIYsaZQOF6XUZKwuHbK%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;541&quot; height=&quot;78&quot; data-origin-width=&quot;1274&quot; data-origin-height=&quot;184&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;비밀번호 유효성 검사하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;남은 두 가지 요구사항에 대해서도 테스트를 해봅시다!&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;- [ ] 비밀번호는 6자이다.&lt;br /&gt;- [ ] 비밀번호는 숫자만 입력 가능하다. 문자 입력을 제한하지는 않는다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트는 다음 세 가지에 대해 작성하였습니다. 실제 코드라면 범위에 걸리는 것들에 대해 조금 더 많은 경우의 수를 테스트 해야겠지만 줄였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 6자리가 아닌 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 문자가 포함된 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 올바르게 입력된 경우&lt;/p&gt;
&lt;pre id=&quot;code_1739104206979&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
    fun 비밀번호_길이가_6자리가_아니면_에러_메시지가_출력된다() {
        val invalidPassword = &quot;12345&quot; // 5자리로, 6자리가 아님

        composeTestRule.setContent {
            MaterialTheme {
                PasswordTextField()
            }
        }

        // 비밀번호 입력란을 찾아 잘못된 비밀번호를 입력
        composeTestRule
            .onNodeWithText(&quot;비밀번호 입력&quot;)
            .performTextInput(invalidPassword)

        // 유효하지 않으므로 에러 메시지가 표시되어야 한다.
        composeTestRule
            .onNodeWithText(&quot;비밀번호는 6자리 숫자를 입력해야 합니다.&quot;)
            .assertExists()
    }

    @Test
    fun 비밀번호에_숫자외_문자가_포함되면_에러_메시지가_출력된다() {
        val invalidPassword = &quot;12345a&quot; // 6자리이지만 숫자 외 문자가 포함됨

        composeTestRule.setContent {
            MaterialTheme {
                PasswordTextField()
            }
        }

        // 비밀번호 입력란을 찾아 잘못된 비밀번호를 입력
        composeTestRule
            .onNodeWithText(&quot;비밀번호 입력&quot;)
            .performTextInput(invalidPassword)

        // 에러 메시지가 나타나야 한다.
        composeTestRule
            .onNodeWithText(&quot;비밀번호는 6자리 숫자를 입력해야 합니다.&quot;)
            .assertExists()
    }

    @Test
    fun 올바른_비밀번호_입력시_에러_메시지가_보이지_않는다() {
        val validPassword = &quot;123456&quot; // 6자리 숫자

        composeTestRule.setContent {
            MaterialTheme {
                PasswordTextField()
            }
        }

        // 올바른 비밀번호를 입력한다.
        composeTestRule
            .onNodeWithText(&quot;비밀번호 입력&quot;)
            .performTextInput(validPassword)

        // 에러 메시지가 없어야 한다.
        composeTestRule
            .onNodeWithText(&quot;비밀번호는 6자리 숫자를 입력해야 합니다.&quot;)
            .assertDoesNotExist()
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 입력되면 password 를 검증하고 유효하지 않은 경우에 ErrorMessage 를 보여줬습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739104443368&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun PasswordTextField(modifier: Modifier = Modifier) {
    var password by remember { mutableStateOf(&quot;&quot;) }
    var passwordError by remember { mutableStateOf(&quot;&quot;) }

    TextField(
        value = password,
        label = { Text(&quot;비밀번호 입력&quot;) },
        onValueChange = { value -&amp;gt;
            password = value
            val isValid = password.length == 6 &amp;amp;&amp;amp; password.all { it.isDigit() }
            passwordError = if (!isValid) &quot;비밀번호는 6자리 숫자를 입력해야 합니다.&quot; else &quot;&quot;
        },
        visualTransformation = PasswordVisualTransformation(), // 추가
        isError = passwordError.isNotBlank(),
        supportingText = { Text(passwordError) },
        modifier = modifier
            .fillMaxWidth()
            .padding(16.dp)
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트가 모두 성공하는 것을 확인할 수 있습니다:)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9TH3y/btsMbbu4OyS/G9ZjqoEAKMkZlVvk9Apdh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9TH3y/btsMbbu4OyS/G9ZjqoEAKMkZlVvk9Apdh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9TH3y/btsMbbu4OyS/G9ZjqoEAKMkZlVvk9Apdh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9TH3y%2FbtsMbbu4OyS%2FG9ZjqoEAKMkZlVvk9Apdh0%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;655&quot; height=&quot;166&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;318&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;UI 테스트의 WOW 포인트&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;굉장히 빠른 속도로 테스트가 실행되는 것을 확인할 수 있습니다. 기존 기능이 제대로 동작하는지, 새로운 기능에 문제 없는지 쉽고 빠르게 확인할 수 있어 매력적이었습니다. 끝!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Feb-09-2025 21-45-48.gif&quot; data-origin-width=&quot;1136&quot; data-origin-height=&quot;888&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZCqJ2/btsMcZGBjeg/lD2SAJihCZ53Z6RSy2R9i1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZCqJ2/btsMcZGBjeg/lD2SAJihCZ53Z6RSy2R9i1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZCqJ2/btsMcZGBjeg/lD2SAJihCZ53Z6RSy2R9i1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cZCqJ2/btsMcZGBjeg/lD2SAJihCZ53Z6RSy2R9i1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;726&quot; height=&quot;568&quot; data-filename=&quot;Feb-09-2025 21-45-48.gif&quot; data-origin-width=&quot;1136&quot; data-origin-height=&quot;888&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;출처&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/develop/ui/compose/testing&quot;&gt;https://developer.android.com/develop/ui/compose/testing&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/develop/ui/compose/testing/apis&quot;&gt;https://developer.android.com/develop/ui/compose/testing/apis&lt;/a&gt;&lt;/p&gt;</description>
      <category>Jetpack Compose</category>
      <author>베르_최성훈</author>
      <guid isPermaLink="true">https://seonghoonc.tistory.com/57</guid>
      <comments>https://seonghoonc.tistory.com/57#entry57comment</comments>
      <pubDate>Sun, 9 Feb 2025 21:50:34 +0900</pubDate>
    </item>
    <item>
      <title>[Jetpack Compose] 공식문서 읽기 Side-effects in Compose Part 1</title>
      <link>https://seonghoonc.tistory.com/56</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;LaunchedEffect, DisposableEffect 를 많이 사용하는데 제대로 알고 사용하자는 의미로 공식문서를 읽어보며 SideEffect 에 대해 공부해보겠습니다. 제 생각과 의역이 들어가기 때문에 &lt;span style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot;&gt;정확한 정보를 얻고 싶다면&lt;/span&gt; 본문을 읽는걸 추천드립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/develop/ui/compose/side-effects&quot;&gt;https://developer.android.com/develop/ui/compose/side-effects&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;side-effects 는 composable function 범위 밖에서 발생한 앱의 상태 변화입니다. s&lt;span style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot;&gt;ide-Effects는 예상하지 못한 리컴포지션, 리컴포지션 간 순서 변경, 리컴포지션 취소로 이어질 수 있습니다.&lt;/span&gt; 때문에&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;side-effects 는&amp;nbsp;&lt;/span&gt;이상적으로 없어야합니다. 하지만 스낵바를 보여주거나 화면을 이동하는 이벤트 등 side-effects 가 필요한 경우도 있습니다. 이 경우 이벤트가 컴포저블의 lifecycle 에 맞춰서 실행되어야합니다. 그때 사용해야하는 것이 Effect Api 입니다. 이 글에서 Jetpack compose 의 side-effects API 들에 대해 공부해봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;State and effect use cases&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;nbsp;An &lt;b&gt;effect&lt;/b&gt;&amp;nbsp;is a composable function that doesn't emit UI and causes side effects to run when a composition completes.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;문서에 따르면 &quot;effect 는 Composable 함수이지만 UI 를 emit 하지않는다. 컴포지션이 완료될 때 side effects 를 실행한다.&quot; 라고 얘기합니다. 화면을 그리는 것이 아니라 Side effects 를 실행하기 위한 함수라고 이해하면 될 것 같습니다. Compose 에서 effect 는 남용되기 쉽습니다. &lt;span style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot;&gt;특히 effect 함수를 사용할 때는&lt;/span&gt; Unidirectional data flow (단방향 데이터 흐름) 이 깨지지 않도록 주의해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;참고: 컴포즈에서 반응형 UI 는 비동기로 발생하며 콜백 대신 코루틴을 이용합니다.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. LaunchedEffect&lt;/b&gt; : &lt;span&gt;run suspend functions in the scope of a composable&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;먼저 가장 많이 보는 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;LaunchedEffect&lt;/b&gt;&lt;/span&gt; 입니다. &lt;b&gt;Composable scope 내에서 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;전체 생명주기에 걸쳐&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;suspend 함수를 실행&lt;/b&gt;하려면 &lt;span style=&quot;color: #000000;&quot;&gt;LaunchedEffect&lt;/span&gt; 를 사용해야합니다. &lt;span style=&quot;color: #000000;&quot;&gt;LaunchedEffect&lt;/span&gt; 가 컴포지션을 시작하면 LaunchedEffect 의 매개변수로 전달된 coroutine block 이 실행되며 LaunchedEffect 의 composition 이 끝나면 coroutine block 이 cancel 됩니다. 만약 LaunchedEffect 가 전달받은 key 가 변경되면 이미 실행중인 coroutine 은 cancel 되고 새로운 coroutine 이 다시 실행됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1737869611412&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var pulseRateMs by remember { mutableStateOf(3000L) }
val alpha = remember { Animatable(1f) }
LaunchedEffect(pulseRateMs) { // pulseRateMs 의 값이 변경되면 코루틴을 취소하고 변경된 값으로 다시 실행됨
    while (isActive) {
        delay(pulseRateMs) // pulseRateMs 시간마다 alpha 가 -&amp;gt; 0 -&amp;gt; 1
        alpha.animateTo(0f)
        alpha.animateTo(1f)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;코드를 보면 pulseRateMs 가 있는데 이 값을 LaunchedEffect key 로 전달했기 때문에 이 값이 바뀌면 LaunchedEffect 코루틴이 취소되고 다시 실행됩니다. 초기에 3초(3000)에 한 번씩 alpha 가 변하다가 pulseRateMs 가 1000 이 된다면 1초에 한번씩 alpha 가 변경 되는 것이죠. 이는 Composable 생명주기가 끝날 때까지 반복됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. rememberCoroutineScope&lt;/b&gt; : &lt;span&gt;obtain a composition-aware scope to launch a coroutine outside a composable&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;LaunchedEffect 는 Composable 함수라 다른 composable 함수 내에서만 실행할 수 있습니다. Composable 외부에 있지만 Composition 종료 후 자동으로 취소되게 하려면 rememberCoroutineScope 를 사용해야합니다.&lt;span style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;또한 코루틴 생명주기를 수동으로 관리해야할 때 사용할 수 있습니다.&amp;nbsp;&lt;span style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot;&gt;버튼 클릭 같은 이벤트 핸들링을 할 때 유용하게 사용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #000000; text-align: left;&quot; data-copy-event-label=&quot;android_compose_side_effects_remembercoroutinescope-SideEffectsSnippets&quot; data-region-tag=&quot;android_compose_side_effects_remembercoroutinescope&quot; data-github-path=&quot;android/snippets/compose/snippets/src/main/java/com/example/compose/snippets/sideeffects/SideEffectsSnippets.kt&quot; data-code-snippet=&quot;true&quot; data-scope=&quot;android_compose_side_effects_remembercoroutinescope&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun MoviesScreen(snackbarHostState: SnackbarHostState) {

    // MoviesScreen Composable 함수의 생명주기에 바인딩된 CoroutineScope 구성
    val scope = rememberCoroutineScope()

    Scaffold(
        snackbarHost = {
            SnackbarHost(hostState = snackbarHostState)
        }
    ) { contentPadding -&amp;gt;
        Column(Modifier.padding(contentPadding)) {
            Button(
                onClick = {
                    // showSnackbar 하기 위해 새로운 코루틴 생성
                    scope.launch {
                        snackbarHostState.showSnackbar(&quot;Something happened!&quot;)
                    }
                }
            ) {
                Text(&quot;Press me&quot;)
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. rememberUpdatedState:&lt;/b&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;reference a value in an effect that shouldn't restart if the value changes&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;LaunchedEffect 는 key 매개변수 중 하나가 변경되면 다시 시작합니다. 그러나 경우에 따라서 값이 변경되더라도 다시 시작하지 않게 하고 싶을 수 있습니다. 이 경우 &lt;b&gt;rememberUpdatedState&lt;/b&gt; 를 사용할 수 있습니다. 이 방법은 비용이 많이 들거나 다시 시작해서는 안되는 작업에 사용할 때 유용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;예를 들어 시간이 지나면 사라지는 LandingScreen 이 있을 때, 컴포지션 이후 onTimeout 값이 변경되어 리컴포지션 되더라도 &quot;일정시간 후에 대기 시간 경과 알림&quot; 이라는 효과는 다시 시작해서는 안됩니다. 그렇다고 onTimeout 이 최초의 onTimeout 으로 실행되면 문제가 생길 수 있겠죠. 최신의 onTimeout 을 가리키게 하려면 rememberUpdatedState 를 사용하면 됩니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;android_compose_side_effects_rememberupdatedstate-code-sample&quot; class=&quot;kotlin&quot; style=&quot;color: #000000; text-align: left;&quot; data-copy-event-label=&quot;android_compose_side_effects_rememberupdatedstate-SideEffectsSnippets&quot; data-region-tag=&quot;android_compose_side_effects_rememberupdatedstate&quot; data-github-path=&quot;android/snippets/compose/snippets/src/main/java/com/example/compose/snippets/sideeffects/SideEffectsSnippets.kt&quot; data-code-snippet=&quot;true&quot; data-scope=&quot;android_compose_side_effects_rememberupdatedstate&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun LandingScreen(onTimeout: () -&amp;gt; Unit) {

    // LandingScreen 이 리컴포지션 되면 항상 최근의 onTimeout function 을 가리킵니다.
    val currentOnTimeout by rememberUpdatedState(onTimeout)

    // 재실행하지 않고 다시 delay 할 필요없이 최신의 onTimeout 을 실행할 수 있다는 의미입니다.
    LaunchedEffect(true) {
        delay(SplashWaitTimeMillis)
        currentOnTimeout()
    }

    /* Landing screen content */
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;effect 를 호출 시점 생명주기 와 동일하게 생성하려면 변하지 않는 상수(Unit or true) 를 매개변수로 전달하면 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.  DisposableEffect: &lt;/b&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;effects that require cleanup&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;key 가 변경되거나 Composition 종료 후 정리해야하는 sideEffect 의 경우 &lt;b&gt;DisposableEffect&lt;/b&gt; 를 사용해야합니다. &lt;b&gt;DisposableEffect&amp;nbsp;&lt;/b&gt;key가 변경되면 컴포저블이 현재 효과를 dispose (삭제, 정리) 하고 effect 를 다시 호출하여 reset 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어, LifecycleObserver 를 사용해 생명주기에 맞는 이벤트를 보내고 싶다면 &lt;b&gt;DisposableEffect &lt;/b&gt;를 사용해 관찰자를 등록, 등록 취소할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;android_compose_side_effects_disposableeffect-code-sample&quot; class=&quot;kotlin&quot; style=&quot;color: #000000; text-align: left;&quot; data-copy-event-label=&quot;android_compose_side_effects_disposableeffect-SideEffectsSnippets&quot; data-region-tag=&quot;android_compose_side_effects_disposableeffect&quot; data-github-path=&quot;android/snippets/compose/snippets/src/main/java/com/example/compose/snippets/sideeffects/SideEffectsSnippets.kt&quot; data-code-snippet=&quot;true&quot; data-scope=&quot;android_compose_side_effects_disposableeffect&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun HomeScreen(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onStart: () -&amp;gt; Unit, // Started 이벤트 전송
    onStop: () -&amp;gt; Unit // stoppted 이벤트 전송
) {
    // 새로운 값이 전달되면 최신 값을 가리킨다.
    val currentOnStart by rememberUpdatedState(onStart)
    val currentOnStop by rememberUpdatedState(onStop)

    // lifecycleOwner 값이 변경되면 effect 를 정리하고 리셋한다.
    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event -&amp;gt;
        	// onStart 라면 onStart 이벤트 전송
            if (event == Lifecycle.Event.ON_START) {
        	// onStop 이라면 onStop 이벤트 전송
            } else if (event == Lifecycle.Event.ON_STOP) {
                currentOnStop()
            }
        }

        // lifecycle 옵저버 add
        lifecycleOwner.lifecycle.addObserver(observer)

        // Composition 이 종료될 때 observer 를 제거한다. (dispose)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }

    /* Home screen content */
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리하며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;오늘은 LaunchedEffect, rememberCoroutineScope, rememberUpdatedState, DisposableEffect 에 대해 학습해보았습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 공식문서 읽기, 생각 남기기 활동이지만 모르고 사용하던 것들을 제대로 이해할 수 있는 시간을 가질 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서 남은&amp;nbsp;&lt;span&gt;SideEffect, &lt;/span&gt;&lt;span&gt;produceState, &lt;/span&gt;&lt;span&gt;derivedStateOf, &lt;/span&gt;&lt;span&gt;snapshotFlow 에 대해 알아보겠습니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Jetpack Compose</category>
      <author>베르_최성훈</author>
      <guid isPermaLink="true">https://seonghoonc.tistory.com/56</guid>
      <comments>https://seonghoonc.tistory.com/56#entry56comment</comments>
      <pubDate>Sun, 26 Jan 2025 16:30:01 +0900</pubDate>
    </item>
    <item>
      <title>Nested Scrollable List in Android Views (RecyclerView 를 사용한 중첩 스크롤 방식)</title>
      <link>https://seonghoonc.tistory.com/55</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Intro&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목록이 중첩된&lt;span&gt;&amp;nbsp;&lt;/span&gt;복잡한 디자인을 개발해야할 때가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 상품 목록에서 최근 본 상품을 상단에 보여준다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 상품 중간에 광고를 넣는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 판매자의 다른 상품들을 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우 복잡한 화면을 어떻게 구현할지 고민해 봐야합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 View System 에서는 어떻게 개발해야하는지 알아봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024 Google I/O Extended in Busan 에서 발표한 글을 토대로 정리한 내용입니다.&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;1402&quot; data-origin-height=&quot;788&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dBSZDO/btsLEgo0fNa/Nw5jU4D2MhLVVkATKhdPYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dBSZDO/btsLEgo0fNa/Nw5jU4D2MhLVVkATKhdPYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dBSZDO/btsLEgo0fNa/Nw5jU4D2MhLVVkATKhdPYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdBSZDO%2FbtsLEgo0fNa%2FNw5jU4D2MhLVVkATKhdPYk%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;511&quot; height=&quot;287&quot; data-origin-width=&quot;1402&quot; data-origin-height=&quot;788&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 앱을 만들면서 이해해봅시다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;GDG Busan 의 &lt;/span&gt;앱을 한 번 만들어 보겠습니다. 아래와 같이 디자인하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;858&quot; data-origin-height=&quot;754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SdCQu/btsLDVZIwzB/06ufSYFkdxzmSkm1uCT1RK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SdCQu/btsLDVZIwzB/06ufSYFkdxzmSkm1uCT1RK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SdCQu/btsLDVZIwzB/06ufSYFkdxzmSkm1uCT1RK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSdCQu%2FbtsLDVZIwzB%2F06ufSYFkdxzmSkm1uCT1RK%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;437&quot; height=&quot;384&quot; data-origin-width=&quot;858&quot; data-origin-height=&quot;754&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 화면은 다음과 같은 순서로 구성되어있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 최 상단 배너&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 운영진 목록 (Organizers)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 이벤트 목록 (Past Events)&lt;/p&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 View System 의 RecyclerView 를 사용해 만들어봅시다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;RecyclerView 란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/develop/ui/views/layout/recyclerview&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.android.com/develop/ui/views/layout/recyclerview&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;Android View System 에서 정적, 동적 목록을 그리기 위해 사용하며 findViewById 의 Cost 를 줄이기 위해 ListView 와 달리 ViewHolder 패턴을 강제합니다. LayoutManager 를 사용해 Linear, Grid, StaggeredGrid 형태의 뷰를 쉽게 구현할 수 있으며 Animation 전용 클래스도 제공합니다.&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;926&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/edzvfM/btsLEJLcIya/vjvySZBeDO4MxVcDRcqk6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/edzvfM/btsLEJLcIya/vjvySZBeDO4MxVcDRcqk6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/edzvfM/btsLEJLcIya/vjvySZBeDO4MxVcDRcqk6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FedzvfM%2FbtsLEJLcIya%2FvjvySZBeDO4MxVcDRcqk6K%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;488&quot; height=&quot;329&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 구현 방식은 다음과 같습니다. RecyclerView.ViewHolder 를 상속하는 ViewHolder 를 구현하고 Adapter 에 해당 ViewHolder 타입을 전달합니다. 그 후 RecyclerView 의 adapter 로 set 하면 됩니다.&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;1238&quot; data-origin-height=&quot;164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qvIJ3/btsLCHVw5Wf/4ZkOHfPBXqV9aiXoVIDPo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qvIJ3/btsLCHVw5Wf/4ZkOHfPBXqV9aiXoVIDPo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qvIJ3/btsLCHVw5Wf/4ZkOHfPBXqV9aiXoVIDPo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqvIJ3%2FbtsLCHVw5Wf%2F4ZkOHfPBXqV9aiXoVIDPo0%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;566&quot; height=&quot;75&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;164&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;- Adapter 와 ViewHolder&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1998&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nsd5P/btsLJCSMIvZ/vxuFhGVkFTbob2cKRHYC00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nsd5P/btsLJCSMIvZ/vxuFhGVkFTbob2cKRHYC00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nsd5P/btsLJCSMIvZ/vxuFhGVkFTbob2cKRHYC00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnsd5P%2FbtsLJCSMIvZ%2FvxuFhGVkFTbob2cKRHYC00%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;634&quot; height=&quot;283&quot; data-origin-width=&quot;1998&quot; data-origin-height=&quot;892&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;- View 구조&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1576&quot; data-origin-height=&quot;910&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GhqOC/btsLJC6hSzR/OHfkvmgVVUTb943PyKEZa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GhqOC/btsLJC6hSzR/OHfkvmgVVUTb943PyKEZa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GhqOC/btsLJC6hSzR/OHfkvmgVVUTb943PyKEZa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGhqOC%2FbtsLJC6hSzR%2FOHfkvmgVVUTb943PyKEZa1%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;664&quot; height=&quot;383&quot; data-origin-width=&quot;1576&quot; data-origin-height=&quot;910&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;스크롤 가능한 목록&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 리사이클러뷰를 사용해 간단하게 접근해 봅시다. 우리는 전체 화면을 스크롤하면서 동시에 목록도 스크롤해야합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ScrollView 와 RecyclerView 를 같이 쓰면 되겠네요!&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;1276&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEJfjL/btsLDNnfSGD/LSpRkH8KEn6EJEbC8DJZQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEJfjL/btsLDNnfSGD/LSpRkH8KEn6EJEbC8DJZQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEJfjL/btsLDNnfSGD/LSpRkH8KEn6EJEbC8DJZQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEJfjL%2FbtsLDNnfSGD%2FLSpRkH8KEn6EJEbC8DJZQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;527&quot; height=&quot;163&quot; data-origin-width=&quot;1276&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그러나 예상과 다르게 동작하는 것을 확인할 수 있습니다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;스크롤 뷰, 리사이클러뷰의 스크롤 방향이 겹치는 &lt;/span&gt;&lt;/span&gt;세로 스크롤은 리사이클러뷰 스크롤과 전체 스크롤이 따로 노는 것을 확인할 수 있습니다. 가로 스크롤은 잘 되네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Video_2024-07-07-17-09-04 (1).gif&quot; data-origin-width=&quot;146&quot; data-origin-height=&quot;318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dkTSaG/btsLIAn6LCJ/Ks3IpFnP1YG0OMI55vMri0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dkTSaG/btsLIAn6LCJ/Ks3IpFnP1YG0OMI55vMri0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dkTSaG/btsLIAn6LCJ/Ks3IpFnP1YG0OMI55vMri0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/dkTSaG/btsLIAn6LCJ/Ks3IpFnP1YG0OMI55vMri0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;243&quot; height=&quot;529&quot; data-filename=&quot;KakaoTalk_Video_2024-07-07-17-09-04 (1).gif&quot; data-origin-width=&quot;146&quot; data-origin-height=&quot;318&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 우리는 여러가지 방법을 사용할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. NestedScrollView&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Using Multi ViewType&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. ConcatAdapter&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. Epoxy,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. Motion Layout&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. NestedScrollView&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중에 가장 쉬운 것 하나만 알면 되지 않을까요? NestedScrollView 를 한 번 사용해봅시다. 변경은 매우매우매우 간단합니다. ScrollView 를 NestedScrollView 로 바꿔주기만 하면 됩니다.&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;1538&quot; data-origin-height=&quot;912&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bglRQr/btsLJVq2lFO/kTmudAh7ffBoCZDzPiUVCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bglRQr/btsLJVq2lFO/kTmudAh7ffBoCZDzPiUVCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bglRQr/btsLJVq2lFO/kTmudAh7ffBoCZDzPiUVCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbglRQr%2FbtsLJVq2lFO%2FkTmudAh7ffBoCZDzPiUVCK%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;825&quot; height=&quot;489&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;912&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 우리가 원하는 대로 잘 동작하는 것을 확인할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Video_2024-07-07-17-20-05.gif&quot; data-origin-width=&quot;146&quot; data-origin-height=&quot;324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LIq2A/btsLJKQz6JA/vcuKk68oHPo9lzlx5HNF7K/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LIq2A/btsLJKQz6JA/vcuKk68oHPo9lzlx5HNF7K/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LIq2A/btsLJKQz6JA/vcuKk68oHPo9lzlx5HNF7K/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/LIq2A/btsLJKQz6JA/vcuKk68oHPo9lzlx5HNF7K/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;234&quot; height=&quot;519&quot; data-filename=&quot;KakaoTalk_Video_2024-07-07-17-20-05.gif&quot; data-origin-width=&quot;146&quot; data-origin-height=&quot;324&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 끝! ... 일 까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;AndroidStudio 의 Layout Inspector 를 확인해보면 문제점을 발견할 수 있습니다. 스크롤 방향이 다른 vertical RecyclerView 는 재사용이 잘 되지만 방향이 겹친 Past Event 는 뷰가 재사용되지 않고 미리 다 그려버리는 것을 확인할 수 있습니다. NestedScrollView 는 매우 간단하지만 리사이클러뷰의 가장 큰 장점을 없애 버리죠. &amp;nbsp;WrapContent 높이를 사용하면, 모든 자식 View들이 한 번에 Measure &amp;amp; Layout 되어야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면 기록 2024-07-08 오전 11.04.48 (1).gif&quot; data-origin-width=&quot;1272&quot; data-origin-height=&quot;1318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXPvzU/btsLJvsCf5U/DKGxRi6rILpaoucEelYgE0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXPvzU/btsLJvsCf5U/DKGxRi6rILpaoucEelYgE0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXPvzU/btsLJvsCf5U/DKGxRi6rILpaoucEelYgE0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bXPvzU/btsLJvsCf5U/DKGxRi6rILpaoucEelYgE0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;659&quot; height=&quot;683&quot; data-filename=&quot;화면 기록 2024-07-08 오전 11.04.48 (1).gif&quot; data-origin-width=&quot;1272&quot; data-origin-height=&quot;1318&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;아이템 개수만큼 View 인스턴스를 모두 미리 생성하므로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;성능 저하&lt;/b&gt;가 일어납니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #202124;&quot;&gt;구현이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #202124;&quot;&gt;매우 간단하기 때문에 아이템 개수가 적다면 충분히 고려해볼 수 있습니다만&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;아이템 개수가 많아질 가능성이 있다면 다른 방식으로 만들어야 합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. Using Multi ViewType&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그럼 재사용 가능하게 만드는 방법은 없을까요? 앞에서 소개한 Multi-ViewType 방식을 사용해 봅시다. RecyclerView 는 ViewType 에 따라 다른 ViewHolder 를 사용하도록 할 수 있습니다. 이를 이용하면 ViewHolder 안에 RecyclerView 를 넣을 수도 있죠.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1166&quot; data-origin-height=&quot;1118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLkBG8/btsLId1dNZa/7w3vk0BbuDgLtzRhdjPfyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLkBG8/btsLId1dNZa/7w3vk0BbuDgLtzRhdjPfyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLkBG8/btsLId1dNZa/7w3vk0BbuDgLtzRhdjPfyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLkBG8%2FbtsLId1dNZa%2F7w3vk0BbuDgLtzRhdjPfyK%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;426&quot; height=&quot;408&quot; data-origin-width=&quot;1166&quot; data-origin-height=&quot;1118&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면을 네 부분으로 나누어 뷰타입으로 지정해봅시다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1422&quot; data-origin-height=&quot;1018&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEsizq/btsLJEpt5oV/4Ia463WjkZaKN1UxgHMV1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEsizq/btsLJEpt5oV/4Ia463WjkZaKN1UxgHMV1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEsizq/btsLJEpt5oV/4Ia463WjkZaKN1UxgHMV1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEsizq%2FbtsLJEpt5oV%2F4Ia463WjkZaKN1UxgHMV1k%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;565&quot; height=&quot;404&quot; data-origin-width=&quot;1422&quot; data-origin-height=&quot;1018&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;2172&quot; data-origin-height=&quot;826&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tcfWE/btsLHQZwhyz/Ub2fPfy46x4c9Kj59TCOVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tcfWE/btsLHQZwhyz/Ub2fPfy46x4c9Kj59TCOVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tcfWE/btsLHQZwhyz/Ub2fPfy46x4c9Kj59TCOVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtcfWE%2FbtsLHQZwhyz%2FUb2fPfy46x4c9Kj59TCOVK%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;455&quot; height=&quot;173&quot; data-origin-width=&quot;2172&quot; data-origin-height=&quot;826&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 아이템 형태에 따라 필요한 데이터 형태의 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Sealed interface&lt;span&gt; 를&lt;/span&gt;&lt;/span&gt;&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;GdgBusanViewItem&lt;span&gt; 이름으로&lt;/span&gt;&lt;/span&gt;&amp;nbsp;정의하였습니다. GdgBusanRecyclerView 는 4가지 뷰타입을 재사용하며 화면에 그리고 그 중 OrganizersViewHolder 는 그 내부에 RecyclerView 를 그립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면 기록 2024-07-08 오전 11.01.34 (1).gif&quot; data-origin-width=&quot;1260&quot; data-origin-height=&quot;1336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxaDfb/btsLJKQAuvo/kKQU1rrjEtBfPWcNcfyDK1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxaDfb/btsLJKQAuvo/kKQU1rrjEtBfPWcNcfyDK1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxaDfb/btsLJKQAuvo/kKQU1rrjEtBfPWcNcfyDK1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cxaDfb/btsLJKQAuvo/kKQU1rrjEtBfPWcNcfyDK1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;530&quot; height=&quot;562&quot; data-filename=&quot;화면 기록 2024-07-08 오전 11.01.34 (1).gif&quot; data-origin-width=&quot;1260&quot; data-origin-height=&quot;1336&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #202124;&quot;&gt;&amp;nbsp;재사용이 가능해 리사이클러뷰의 장점이 그대로 적용됩니다. 또한,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #202124;&quot;&gt;Layout&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #202124;&quot;&gt;파일을 수정하지 않아도 아이템만 추가해주면&lt;/span&gt;&lt;span style=&quot;color: #202124;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;동일한&lt;/span&gt;&lt;span style=&quot;color: #202124;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;뷰를 몇 번이든 띄울 수 있습니다. 그러나 ViewTpye 이 많아질수록&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #202124;&quot;&gt;구현이 복잡하고 시간이 오래 걸립니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. ConcatAdapter&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RecyclerView 1.2.0 부터 추가된 ConcatAdapter 를 사용해 구현하는 방법도 있습니다. ConcatAdapter 는 여러 Adapter 를 하나의 Adpater 로 합쳐서 구현할 수 있게 도와줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;1136&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o6akx/btsLHNO0uNz/dWjKUYmF2qh4jmpC87F1iK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o6akx/btsLHNO0uNz/dWjKUYmF2qh4jmpC87F1iK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o6akx/btsLHNO0uNz/dWjKUYmF2qh4jmpC87F1iK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo6akx%2FbtsLHNO0uNz%2FdWjKUYmF2qh4jmpC87F1iK%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;368&quot; height=&quot;364&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;1136&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #202124;&quot;&gt;&amp;nbsp;재사용 가능하며&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #202124;&quot;&gt;Multi View Type&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #202124;&quot;&gt;보다는 비교적 간단합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #202124;&quot;&gt;그러나 아이템이 하나여도 어댑터로 구현해야&lt;/span&gt;&lt;span style=&quot;color: #202124;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;합니다. ConcatAdapter 의 생성자에 ConcatAdapter.Config 객체를 넘겨서 옵션을 제어할 수 있습니다. 옵션에는 ConcatAdapter.Config 는 isolateViewTypes 와 stableIdMode 가 있습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #202124;&quot;&gt;&amp;nbsp;isolateViewTypes 는 어댑터 간에 동일한 뷰홀더를 공유할 것인지 설정하는 옵션입니다. &lt;span style=&quot;color: #202124; text-align: start;&quot;&gt;isolateViewTypes 가 true 이면 어댑터간에 풀을 공유하지 않습니다. false 이면 뷰홀더를 공유하게 되지만 서로 다른 뷰홀더가 동일한 뷰타입을 사용하지 않도록 주의해야합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #202124;&quot;&gt;&lt;span style=&quot;color: #202124; text-align: start;&quot;&gt;stableIdMode 는 3가지가 존재합니다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #202124;&quot;&gt;&lt;span style=&quot;color: #202124; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;-&amp;nbsp;NO_STABLE_IDS: 기본으로 지정되는 값. concatAdapter 에 추가되는 어댑터의 stable id 를 무시한다.&amp;nbsp;&lt;br /&gt;- ISOLATED_STABLE_IDS:&amp;nbsp;concatAdapter가 stable id 가 지정된&amp;nbsp;어댑터를 요구한다. 서로 독립적인 id 풀을 가지고 동일한 id 를 리턴할 수 있다.&lt;br /&gt;- SHARED_STABLE_IDS:&amp;nbsp;concatAdapter가 stable id 가 지정된&amp;nbsp;어댑터를 요구한다. 서로 id 풀을 공유하며 서로 다른 id 를 가지도록 보장해야한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 옵션들에 대해 이해가 부족한 상태로 작업하면 원하지 않는 결과를 얻을 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConcatAdapter 에 대해 더 자세한 내용은 잘 정리된 다음 글들을 추천합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;a href=&quot;https://medium.com/androiddevelopers/merge-adapters-sequentially-with-mergeadapter-294d2942127a&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://medium.com/androiddevelopers/merge-adapters-sequentially-with-mergeadapter-294d2942127a&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/hongbeomi-dev/concatadapter-deep-dive-6aa79750f81e&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://medium.com/hongbeomi-dev/concatadapter-deep-dive-6aa79750f81e&lt;/a&gt;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. Epoxy&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Airbnb에서 만든 오픈소스 라이브러리로, &lt;b&gt;RecyclerView&lt;/b&gt; 기반으로 다양한 UI를 쉽게 구성하도록 돕습니다. 주로 아래와 같은 이유로 많이 사용됩니다. 데이터 바인딩, DiffUtil, 여러 ViewType 처리 등을 Epoxy가 자동화/간소화해줍니다. &lt;b&gt;서로 다른 UI&lt;/b&gt;를 한 화면에서 쉽게 구성 가능하며, &lt;span&gt;EpoxyModel&lt;/span&gt; 단위로 뷰를 정의하기 때문에 직관적입니다. 그러나 별도의 &lt;b&gt;라이브러리를 도입&lt;/b&gt;해야 하며, 팀원 모두가 사용법을 숙지해야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. MotionLayout&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;MotionLayout 은 앱에서 모션 및 위젯 애니메이션을 관리하는 layout type 입니다. MotionLayout 과 RecyclerView 를 같이 사용해서스크롤 위치에 맞춰 상단 배너와 리사이클러뷰를 collapsing 한다면 동일한 화면을 구현할 수 있습니다. 그러나 더 복잡한 화면이면 한계가 있을 것으로 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Outro&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;중첩 스크롤 가능한 RecyclerView? 그냥 Compose Lazy Layout 의 item, items 쓰면 해결되는거 아닌가요?&amp;nbsp; 구현만 보자면 그렇습니다.&amp;nbsp;하지만 이미 리사이클러뷰, 리스트뷰로 수많은 복잡한 코드가 작성되어있죠. 일부만 수정하거나 컴포즈로 마이그레이션하거나 혹은 컴포즈를 아직 도입하지 않는 환경에서 개발하게 된다면 View system 에서 어떻게 개발할 수 있는지 알아야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 우린 이것들 중 무엇을 사용해야할까요? 재사용할 수 없는 NestedScrollView 는 쓰면 안되는걸까요? 이것 또한 정답은 없습니다. 당장 빨리 릴리즈 해야하는데 성능이 중요하지 않을 수도 있습니다. 혹은 아이템 개수가 작아서 재사용이 필요없을 수도 있고요. 구현 방법을 이해하고 장단점을 숙지하고 있다가 본인의 상황에 맞춰서 사용하길 바랍니다.&lt;/p&gt;</description>
      <category>Android</category>
      <author>베르_최성훈</author>
      <guid isPermaLink="true">https://seonghoonc.tistory.com/55</guid>
      <comments>https://seonghoonc.tistory.com/55#entry55comment</comments>
      <pubDate>Thu, 9 Jan 2025 22:58:36 +0900</pubDate>
    </item>
    <item>
      <title>2025학년도 안드로이드 탐구 영역 후기</title>
      <link>https://seonghoonc.tistory.com/54</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GDG Android 에서 주최한 시험인 안드로이드 탐구영역에 응시했습니다!&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저도 GDG 의 일원으로 온오프라인 행사를 주최하고 있지만&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;GDG Android 분들은 어떻게 이런 신박한 기획을 생각하고 추진하셨을까 정말 대단하다고 생각됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://android-exam25.gdg.kr/&quot;&gt;https://android-exam25.gdg.kr/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1734166212664&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;2025학년도 안드로이드 탐구영역&quot; data-og-description=&quot;1. 모집 개요 목적: 안드로이드 개발에 대한 학생들의 열정을 키우고, 실력을 평가하여 미래 IT 인재를 발굴합니다. 대상: 안드로이드 개발에 관심 있는 전국의 현&amp;middot;신입 누구나 일정: 접수 기간: 2&quot; data-og-host=&quot;android-exam25.gdg.kr&quot; data-og-source-url=&quot;https://android-exam25.gdg.kr/&quot; data-og-url=&quot;https://android-exam25.gdg.kr/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://android-exam25.gdg.kr/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://android-exam25.gdg.kr/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;2025학년도 안드로이드 탐구영역&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. 모집 개요 목적: 안드로이드 개발에 대한 학생들의 열정을 키우고, 실력을 평가하여 미래 IT 인재를 발굴합니다. 대상: 안드로이드 개발에 관심 있는 전국의 현&amp;middot;신입 누구나 일정: 접수 기간: 2&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;android-exam25.gdg.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;시험 준비&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 토요일마다 종종 모각코를 하러 갑니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;12월 7일 답안 제출 마감날에도 모각코를 하러 갔죠. 다른 분이 코딩하고 있을 때 혼자 문제를 풀어보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당일 서면 파스쿠찌에서 찍은 사진입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1324&quot; data-origin-height=&quot;1272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNyLL1/btsLiLDzGqd/2EDAIb45Zq4Lk41DZ5LRFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNyLL1/btsLiLDzGqd/2EDAIb45Zq4Lk41DZ5LRFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNyLL1/btsLiLDzGqd/2EDAIb45Zq4Lk41DZ5LRFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNyLL1%2FbtsLiLDzGqd%2F2EDAIb45Zq4Lk41DZ5LRFk%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;619&quot; height=&quot;595&quot; data-origin-width=&quot;1324&quot; data-origin-height=&quot;1272&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오랜만에 이런 시험지를 보니까 수능 준비할 때 생각나더군요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험 문제는 다음과 같이 구성됐습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;공통: Android&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;선택: Android Library, Kotlin Coroutines, Jetpack Compose&lt;/p&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 Library 를 선택했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴 공부 많이 했지만 아직도 어렵다고 생각하고, 컴포즈도 사용은 하지만 아직 깊이 알지 못하구요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그나마 Android Library 는 많이 맞출 수 있지 않을까 생각했죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험 시작하고 첫 페이지는 쉬웠습니다. 바로바로 체크하고 넘어갔던 것 같네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 다음 장부터 당황했습니다. 생각보다 많이 어렵더라구요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 유형은 수능과 비슷합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 5개 중에 옳은 것, 옳지 않은 것을 선택&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- ㄱ, ㄴ, ㄷ 중 옳은 것만을 고른 것은?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한, 두 개가 꼭 헷갈리더라구요. 정말 아닌 것들은 다 X 치고 남은 것 중에 찍었습니다..ㅋㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;가장 인상 깊었던 문제&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 인상 깊었던 문제는 14번이었습니다.&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;1312&quot; data-origin-height=&quot;1234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mlXVZ/btsLiJTgDJT/jTknMKuSvi6yenHovGdz7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mlXVZ/btsLiJTgDJT/jTknMKuSvi6yenHovGdz7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mlXVZ/btsLiJTgDJT/jTknMKuSvi6yenHovGdz7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmlXVZ%2FbtsLiJTgDJT%2FjTknMKuSvi6yenHovGdz7K%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;591&quot; height=&quot;556&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;1234&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보자마자 너무 웃겨서 바로 사진 찍었습니다ㅋㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 취미로 시를 쓰는데 뭔가 반갑기도 하고 재밌었습니다ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제 만들어주신 출제위원분께 감사합니다:)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;가장 어려웠던 문제&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 풀기 힘들었던 문제는 13 문제입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4대 컴포넌트 중 Activity, Service, Broadcast Receiver 에 관한 문제였는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;각 컴포넌트의 특징으로 ㄱ, ㄴ, ㄷ 을 특정해야하고 또 그걸로 보기에서 옳은 것을 골라야하더군요.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;882&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b47u4i/btsLiUf0q72/kI22toybltAKdZ7VxzfoVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b47u4i/btsLiUf0q72/kI22toybltAKdZ7VxzfoVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b47u4i/btsLiUf0q72/kI22toybltAKdZ7VxzfoVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb47u4i%2FbtsLiUf0q72%2FkI22toybltAKdZ7VxzfoVK%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;664&quot; height=&quot;882&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;882&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;Service, Broadcast Receiver&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 오랜만에 봐서 ㄱ, ㄴ, ㄷ 특정하는 것도 어려웠는데 &lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;보기에 있는 것들도 쉽지 않더라구요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;특징을 정리해보면&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;ㄱ : UI 스레드에서 동작한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;ㄴ : 개발자가 직접 생성할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;ㄷ : 사용자가 보이지 않는 환경에서 동작한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4대 컴포넌트는 기본적으로 UI 스레드에서 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 생성할 수 있는 것은 BroadCastReceiver 밖에 없고 나머진 Intent 로 요청하는 형태입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자에게 보이는 환경은 화면, UI를 담당하는 Activity 밖에 없죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 A 는 Activity, B는 Service, C는 Broadcast Receiver 네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보기를 정리해보면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄱ: Activity 는 동일 클래스의 객체가 여러개 존재할 수 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ : Activity 와 Service 간에 Intent 나 bindService 로 데이터를 전달할 수 잇고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄷ : 4대 컴포넌트는 기본적으로 UI 스레드에서 시작하니 O 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답은 ㄱ, ㄴ, ㄷ 인&amp;nbsp; 5 번이네요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 틀렸습니다ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;퇴실하면서&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방금 점수 메기고 왔는데 점수가 처참하군요 하하...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 생각보다 점수가 낮은데 모르는걸 많이 알게되서 좋다고 생각합니다:)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 코루틴, 컴포즈 문제를 아직 못풀었습니다. 핑계지만... 연말이라 많이 바쁘군요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새해 목표로 문제 풀고 이 문제들의 오답노트를 작성해보면 재밌지 않을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행사 주최의 관점에서, 안드로이드 주니어 관점에서, 매우 유의미한 활동이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 동기부여와 피드백을 받아가네요:)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GDG Android 분들, 출제 위원 분들 모두 감사드립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오랜만에 시험 치니까 조금 피곤해졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 저에게 보상을 주러 갔습니다. 방어가 또 제철이니까요:)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1324&quot; data-origin-height=&quot;1204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDXC2Y/btsLjbu7rPe/26TBkNDGM1dJ3tQkkcN1wK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDXC2Y/btsLjbu7rPe/26TBkNDGM1dJ3tQkkcN1wK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDXC2Y/btsLjbu7rPe/26TBkNDGM1dJ3tQkkcN1wK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDXC2Y%2FbtsLjbu7rPe%2F26TBkNDGM1dJ3tQkkcN1wK%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;581&quot; height=&quot;528&quot; data-origin-width=&quot;1324&quot; data-origin-height=&quot;1204&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회고</category>
      <author>베르_최성훈</author>
      <guid isPermaLink="true">https://seonghoonc.tistory.com/54</guid>
      <comments>https://seonghoonc.tistory.com/54#entry54comment</comments>
      <pubDate>Sat, 14 Dec 2024 17:51:17 +0900</pubDate>
    </item>
    <item>
      <title>조금 일찍하는 2024년 회고</title>
      <link>https://seonghoonc.tistory.com/53</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;들어가기 전에&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저에게 있어 2024년은 1월 1일부터 시작하지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우아한테크코스가 끝나는 2023년 11월 말부터 시간은 다르게 흘렀기 때문이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에, 아직 2024년이 한달 남은 현 시점에 1년 회고를 작성하고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024년은 제게 &lt;b&gt;취업&lt;/b&gt;이라는 큰 산이 있었기 때문에 조금은 특별한 1년이었을지 모르겠네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2023년 12월 우아한테크코스 안드로이드 과정 그 10개월의 긴 기간이 지나고 부산으로 돌아오게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;br /&gt;&lt;br /&gt;페스타고, 그 두 번째 시작&lt;br /&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페스타고는 우아한테크코스에서 시작한 대학교 축제 티케팅 &amp;amp; 라인업 검색 애플리케이션입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;569&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4ueR7/btsLbRQzEul/X2o2vqjEOYSksmD8RHPUxK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4ueR7/btsLbRQzEul/X2o2vqjEOYSksmD8RHPUxK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4ueR7/btsLbRQzEul/X2o2vqjEOYSksmD8RHPUxK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4ueR7%2FbtsLbRQzEul%2FX2o2vqjEOYSksmD8RHPUxK%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;608&quot; height=&quot;270&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;569&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우아한테크코스가 끝나고 페스타고 팀원 중에 취업한 사람도 있었고 그렇지 못한 사람도 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페스타고 팀원들과 함께하면서 얻은 것이 너무 많았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 끝나고 계속 함께하고 싶었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 팀원들은 기존 페스타고에서 사용자 유치에 어려움을 겪었고 세 가지 문제를 해결해야 했습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;iOS 앱 없음, 디자이너 없음, 사용자 유치가 어려움&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 페스타고는 기획을 수정, iOS 개발자, 디자이너를 모집했고 Android 3, iOS 1, Design 2, Server 4 총 10명의 팀을 결성해 우테코가 끝나고 추가로 4~5개월 정도 더 협업하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 결국에...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페스타고는 iOS, Android 둘 다 릴리즈 되었고 아직까지 운영중입니다. (기능 개발은 하지 않습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 200명 이상 누적 사용자를 유치했고 그 과정에서 선택, 강제 업데이트나 사용자 경험 개선, 어려운 디자인 대응 등 많이 노력했던 기억이 나네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저에게 페스타고는 굉장히 큰 행운이었고 값진 경험이었습니다. 무엇보다 1년 동안 함께 해준 팀원분들께 너무 감사합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직장 다니면서 혹은 다른 직무로 일하면서도 사이드로 꾸준히 작업해주셨던 노력 절대 잊지 않겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;a href=&quot;https://github.com/woowacourse-teams/2023-festa-go?tab=readme-ov-file&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/woowacourse-teams/2023-festa-go?tab=readme-ov-file&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;br /&gt;&lt;br /&gt;학교로 돌아가서&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;저는 서울에서 부산으로 내려가기 싫었습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현업자들은 대부분 수도권에 있고 유명한 컨퍼런스, 연합 동아리 해커톤 등 모두 수도권 중심으로 개최됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우테코에서는 잡담이 경쟁력이라고 가만히 있어도 정보가 흘러들어왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스타 개발자, 실력있는 개발자를 만나기 쉬운 환경이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그에 비해 과거 부산에서의 저는 이렇게 좋은 개발 인프라를 누리지 못했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기에 부산에 내려온 저는 서울과 비슷한 환경을 만들기 위해 노력했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 부경대학교 개발동아리 WAP 의 개발 환경을 개선하기 위해 노력했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겨울, 여름 방학 때 10시 데일리 미팅을 모집했고 개발 세미나를 열었습니다.&lt;br /&gt;현직자 특강으로 우아한테크코스 박재성(Jason) 코치님을 부경대에 초청했습니다. (킹갓...)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우왑코스 를 만들어 우테코처럼 미션, 코드리뷰 기반 학습 (코틀린, 안드로이드) 멘토링을 해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영자들께 개모임(우테코 테코톡 비슷), 배포 필수, 부스 운영 등의 발표 방식도 제안했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와비라는 팀 프로젝트를 기획, 운영하면서 필수 과제, 페어프로그래밍, 스프린트 등 함께 성장할 수 있게 도왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학생들을 많이 만나는건 저에게 좋은 복습이 되었습니다. 좋은 동기부여였구요.&lt;br /&gt;하지만 뭔가 부족했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저에게 있어 가장 큰&lt;b&gt; 동기부여&lt;/b&gt;는 &lt;b&gt;멋진 사람&lt;/b&gt;을 만나는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멋진 사람?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 잘하는 사람인가? 돈 많이 버는 사람인가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확실한건 제가 멋있어 하는 사람들은 그런 사람들이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 멋진 사람은 누구냐...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저에게&amp;nbsp;&lt;b&gt;새로움이라는 자극을 줄 수 있는 사람&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 삶의 방식에서, 다른 견해에서, 스스로 성장하면서 자신만의 철학을 가지게 된 사람들을 만나면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 짧은 만남에서도 새로움을 전해받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 받은 새로움은 일년이 지나도 빛을 잃지 않는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 사람이 되는 것이&lt;b&gt; 제 꿈&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;br /&gt;&lt;br /&gt;GDG 부산과 GDG&lt;/b&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;b&gt;GDG 부산&lt;/b&gt;에 들어가게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GDG 커뮤니티는 제게 다양한 개발자, 멋진 사람들을 만나게 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스스로 그런 기회(행사)를 만들기도 하죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GDG 서밋, 커뮤니티 나잇 등 &lt;b&gt;GDG Organizer&lt;/b&gt; 들을 위한 행사에 가고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Google I/O, Devfest&lt;/b&gt; 에 참여, 운영하면서 다양한 멋진 사람을 만날 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 다른 맥락인데 드로이드나이츠 행사 스탭, 라이트닝 토크 연사도 했었네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부산 개발자 인프라를 위해 노력해주시는&lt;b&gt; GDG Busan Organizer&lt;/b&gt; 분들 감사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제게 도움을 주신 다른 &lt;b&gt;GDG&lt;/b&gt; 분들께도 감사의 인사를 전합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1411&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r7BOH/btsLbu2l6oH/hk9vsexgTSwX4O5r5uEH81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r7BOH/btsLbu2l6oH/hk9vsexgTSwX4O5r5uEH81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r7BOH/btsLbu2l6oH/hk9vsexgTSwX4O5r5uEH81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr7BOH%2FbtsLbu2l6oH%2Fhk9vsexgTSwX4O5r5uEH81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;375&quot; height=&quot;281&quot; data-origin-width=&quot;1411&quot; data-origin-height=&quot;1058&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;안드로이드 그리고 방황&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 취업, 이직 어렵다고 많이 얘기하지만 남들보다 열심히, 잘하면 된다고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지원 자체를 못할 줄은 몰랐죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대기업은 채용 문을 닫은 상태고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스타트업 시장에서 신입 채용은 거의 없으며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작디 작은 모바일 시장은 Flutter, RN 과 함께 그 작은 파이를 나눠먹고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 기업 3년차 공고까지 할 수 있는 지원은 다 했지만 서류 합격조차 거의 없었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 우테코에서 많은 것을 배웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발, 소프트 스킬, 문화 등 이상적인 배움은 이런게 아닐까하고 느낄 정도였으니까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어 생태계에 선한 영향력을 주고 싶어졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나,&amp;nbsp;생태계에 들어가지도 못하면 무슨 의미가 있는건지 의문을 가졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋아하고, 열심히하고, 잘하는 일을 시작도 못하게 될까봐 두려웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 분들께 상담을 받았고 그 중 인상 깊었던 말은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 개발자이기 이전에 Kotlin 개발자고 컴퓨터공학 전공자라는 말이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향, 디자인패턴, Kotlin 등에는 자신있었기 때문에 백엔드를 해보기로 마음 먹었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의 및 스터디로 Java, Spring, RealMySQL 을 공부하고 혹시 자격증이 필요할까봐 SQLD 도 땄습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 언어, 프레임워크 등의 지식보다 더 중요한 것을 얻었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 개발이 다 비슷비슷하다는 것.&amp;nbsp;좁은 시야가 더 넓어졌다는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깊게 공부해보진 못했지만 덕분에 두려움을 이겨낼 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1996&quot; data-origin-height=&quot;464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zFqjO/btsLani94gE/ke05Zf1XCfSzFwcJS8QmZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zFqjO/btsLani94gE/ke05Zf1XCfSzFwcJS8QmZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zFqjO/btsLani94gE/ke05Zf1XCfSzFwcJS8QmZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzFqjO%2FbtsLani94gE%2Fke05Zf1XCfSzFwcJS8QmZk%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;598&quot; height=&quot;139&quot; data-origin-width=&quot;1996&quot; data-origin-height=&quot;464&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;br /&gt;&lt;br /&gt;서울시 지하철 시 공모전&lt;/b&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;이 취미를 가진지도 3년이 지났는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해는 특별히 공모전에 당선되어 서울시 지하철역에 설치됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 &lt;b&gt;소화기&lt;/b&gt;라는 시가 &lt;b&gt;&lt;/b&gt;광화문, 왕십리, 미아, 버티고개역에 설치됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공모전 당선 문자를 받은게 가장 기분 안좋고 방황하던 시기여서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취미이지만 하나를 이뤄냈다는게, 인정 받았다는게 기뻤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근엔 링크드인에 관련 글을 썼는데 좋아요 300 가, 조회수가 23,000 회를 넘었네요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 분들이 응원하고 공감해주셔서 좋았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.linkedin.com/feed/update/urn:li:activity:7265891802182737921/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.linkedin.com/feed/update/urn:li:activity:7265891802182737921/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;br /&gt;&lt;br /&gt;안드로이드 외주&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;기획, 디자인, 개발 모든 것을 해야했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 재택이고 우테코 크루들과 다시 한 번 프로젝트를 할 수 있어서 지원하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 개발자가 도메인을 이해하는 것이 얼마나 중요한가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Jetpack Compose 와 친해지기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 마감 기한과 코드 품질 사이에서 개발하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크게 이 세 가지를 얻어갈 수 있었네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;br /&gt;&lt;br /&gt;면접과 취업&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;다만 부산에서 안드로이드 개발자를 뽑는다는 것이 매력적으로 다가왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모집 공고에 적힌 기술 스택, 문화 등이 제가 했던 것들과 비슷해서 잘 맞겠다 생각했고 바로 지원했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상 질문을 뽑는 등의 면접 대비는 하지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 한 모든 활동에는 이유가 있었고 목적이 있었으니까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기억이 안날까봐 제 블로그를 다시 읽어보기는 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접을 할 때는 별로 떨리지 않았습니다. 그냥 인생 얘기하는 것 같았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 이력, 기술에 대해 새로운 사람들과 얘기하는게 재밌었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(자세한 면접 과정은 생략하겠습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 면접이 끝나고 나서 긴장되고 불안하기 시작했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정도면 답변 잘한거 아닌가 생각도 들지만&lt;br /&gt;아쉬운 답변을 했던게 생각났습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기대가 큰 만큼 불합격 문자를 받을 용기가 없었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접 기간에 연휴가 많아서 조금 길어졌는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹여나 오늘 연락올까봐 핸드폰만 보고 있었고 나중엔 배가 아파서 소화도 잘 안되더군요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠이 많은 편이라 잠이 안온적이 잘 없는데 잠도 잘 안오고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;친구와 카페에서 코딩하고 있었는데 갑자기 합격 전화를 받았고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가족과 응원해준 사람들에게 합격했다고 연락드렸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합격 전에 생각했을 때는 너무 기뻐서 소리를 지르거나 힘들었던 감정이 복받쳐 울 줄 알았는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딱히 그런건 없었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드디어 시작을 하는구나.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇 개월간 했던 부정적인 생각이 사라질 정도로만 기뻤습니다.&lt;br /&gt;&lt;br /&gt;&lt;a href=&quot;https://sendy.ai/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://sendy.ai/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1733663069965&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;센디 - 용달 화물 서비스&quot; data-og-description=&quot;화물운송이 간편해집니다. 용달차 부르기, 용달이사, 복잡한 물류센터 입고까지 센디는 운송이 필요한 모두를 위해 만들어진 서비스입니다.&quot; data-og-host=&quot;sendy.ai&quot; data-og-source-url=&quot;https://sendy.ai/&quot; data-og-url=&quot;https://sendy.ai/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cF7B1v/hyXKj7Jg5V/0ODJedjYd1K4qMgRnPWbp0/img.png?width=850&amp;amp;height=400&amp;amp;face=434_237_471_278,https://scrap.kakaocdn.net/dn/cHuEPV/hyXGGwMYRz/KCtvlO51ojrCjoWogkaTek/img.png?width=850&amp;amp;height=400&amp;amp;face=434_237_471_278,https://scrap.kakaocdn.net/dn/dguVxG/hyXGzLdsgz/Vvj0LfYcyhWgTgiT5JAXC0/img.png?width=1220&amp;amp;height=1388&amp;amp;face=0_0_1220_1388&quot;&gt;&lt;a href=&quot;https://sendy.ai/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://sendy.ai/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cF7B1v/hyXKj7Jg5V/0ODJedjYd1K4qMgRnPWbp0/img.png?width=850&amp;amp;height=400&amp;amp;face=434_237_471_278,https://scrap.kakaocdn.net/dn/cHuEPV/hyXGGwMYRz/KCtvlO51ojrCjoWogkaTek/img.png?width=850&amp;amp;height=400&amp;amp;face=434_237_471_278,https://scrap.kakaocdn.net/dn/dguVxG/hyXGzLdsgz/Vvj0LfYcyhWgTgiT5JAXC0/img.png?width=1220&amp;amp;height=1388&amp;amp;face=0_0_1220_1388');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;센디 - 용달 화물 서비스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;화물운송이 간편해집니다. 용달차 부르기, 용달이사, 복잡한 물류센터 입고까지 센디는 운송이 필요한 모두를 위해 만들어진 서비스입니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;sendy.ai&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;br /&gt;&lt;br /&gt;2024년을 마무리하며&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;12월은 만나지 못한 사람들, 밀린 드라마와 영화, 게임 한 판으로 보낼 생각입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 2024년에 도움을 주신 많은 분들과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힘들 때 가까이서 함께해주신 분들께 감사의 인사를 전합니다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2025년에는&amp;nbsp;&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사와 함께 성장할 수 있지 않을까&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;부산의 개발자 생태계에 선한 영향력을 줄 수 있지 않을까&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;나 혼자 잘하는 것보다 함께 잘하는게 더 어렵고 재밌을 것 같네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 마무리가 좋을까 생각하다가 2023년 회고를 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우아한테크코스 수료식에서 크루들에게 전했던 글이 있더군요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;nbsp;우테코 10개월, 누군가는 가장 힘든 시간이었다고 하지만 저에게는 가장 행복했던 꿈만 같은 시간이었습니다. 우테코 들어와서 한 연극, 크루들과의 회식, 함께 간 일본여행, 코건의 공연과 드로이드 나이츠 그리고 대망의 엠티까지&amp;hellip; 많고 과분한 선물을 주셔서 감사합니다. 여러분께 드릴 두 가지 부탁이 있습니다. 좋은 일이 생긴다면, 그 동안 노력한 것에 대한 보답이라고 생각하고 충분히 즐겨주세요. 나쁜 일이 생긴다면, 나란 사람은 왜 이럴까 자책하지 마시고 지금 상황이 안좋아서 그런거라고 생각해주세요. 그만한 자격을 갖춘 사람들이라 믿어 의심치 않습니다. 갑작스런 연락에 커피 한 잔, 좋은 일에 밥 한 그릇, 힘든 일에 술 한 잔, 살 수 있는 사람이 되도록 노력하겠습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;2025년에도 똑같이&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;갑작스런 연락에 커피 한 잔&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;좋은 일에 밥 한 그릇&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;힘든 일에 술 한 잔&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;살 수 있는 사람이 되도록 노력하겠습니다.&lt;/p&gt;</description>
      <category>회고</category>
      <category>회고</category>
      <author>베르_최성훈</author>
      <guid isPermaLink="true">https://seonghoonc.tistory.com/53</guid>
      <comments>https://seonghoonc.tistory.com/53#entry53comment</comments>
      <pubDate>Sat, 7 Dec 2024 22:41:27 +0900</pubDate>
    </item>
  </channel>
</rss>