<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>hrniin의 개발 노트</title>
    <link>https://thdbs523.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Tue, 21 Jul 2026 14:21:24 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>hrniin</managingEditor>
    <image>
      <title>hrniin의 개발 노트</title>
      <url>https://tistory1.daumcdn.net/tistory/4348374/attach/29ea26413b29416c8e1770d323051e52</url>
      <link>https://thdbs523.tistory.com</link>
    </image>
    <item>
      <title>2025년 회고 - 인생사 새옹지마</title>
      <link>https://thdbs523.tistory.com/456</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;a href=&quot;https://youtu.be/KsRhciAW5ao?si=xOepuHAd5vftljHy&quot;&gt;https://youtu.be/KsRhciAW5ao?si=xOepuHAd5vftljHy&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;890&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhFiqo/dJMcabQrdhd/CEc2khruK9GQKGksHPkEkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhFiqo/dJMcabQrdhd/CEc2khruK9GQKGksHPkEkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhFiqo/dJMcabQrdhd/CEc2khruK9GQKGksHPkEkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhFiqo%2FdJMcabQrdhd%2FCEc2khruK9GQKGksHPkEkK%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;400&quot; height=&quot;445&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;890&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;올해는 크고 작은 변화들이 쉴틈없이 이어졌던 한 해였다. 매년 새로운 목표를 세웠었지만 2025년에는 잘 지키지 못할 거창하고 상세한 목표보다 하루하루를 잘 살아내는 것을 목표로 했다. 다만 취업이라는 유일한 목표를 세웠는데, 운이 좋게도 목표를 잘 이뤄냈고 아직 새로운 환경에서 정신없이 적응중이다.&lt;/p&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;요즘 글쓰기 능력이 떨어진 것 같아 이번 회고만큼은 지피티의 도움 없이 회고를 써보려 한다. 정제되지 않은 TMI들이 난무하니 주의바란다. ^^&lt;/p&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;1월~9월: 취업 준비 기간&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해는 취업 준비로 정신없이 달려왔다. 첫 직장은 대기업에 가고 싶다는 욕심이 있었기 때문에 초반에는 대기업만 지원했다. 그러나 10%의 서류 합격률을 맛보며 이대로 가다간 올해 안에 취업은커녕 면접조차 제대로 못 볼 것 같다는 압박감이 다가왔다. 또한 나중에 정말 원하는 기업의 면접을 보게 될 때, 면접에 익숙해진 상태가 되고 싶었다. 그 이후로 대충 직무와 지역만 맞으면 무작정 다 지원했다. 한달에 면접을 10번 가까이 볼 정도로 바쁘게 살았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;지치지 않게 도와준 우리집   오디&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오디는 우아한테크코스에서 했던 팀 프로젝트다. 수료 후에도 몇몇 크루들과 프로젝트를 계속 유지했다. 프로젝트를 유지하는 각자의 이유가 있었지만, 나는 오로지 &amp;rdquo;이력서 소재&amp;ldquo;를 위함이었다. View 기반 코드를 Compose로 마이그레이션 하고, 복잡한 코드를 단순화하고, 분산되어 있던 비즈니스 로직을 Usecase로 통일했다. 모든게 취업을 위한 것이었지만, CS/자소서/면접 준비에 지칠 때마다 도망칠 수 있는 안식처이기도 했다. 계속되는 취준에 지쳐 &lt;i&gt;내가 왜 개발자가 되고싶은지&lt;/i&gt; 의문이 들 때면, 코드를 개선하는 과정에서 개발에 다시금 흥미를 느끼곤 했다. 결과적으로도 포트폴리오에 개선 경험을 잘 녹여내어 면접에서도 강점으로 활용할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;내 인생 최악의 시기&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;408&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CTF8n/dJMcagYu9P6/DqFLAk4S9BAnbW9cshAYv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CTF8n/dJMcagYu9P6/DqFLAk4S9BAnbW9cshAYv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CTF8n/dJMcagYu9P6/DqFLAk4S9BAnbW9cshAYv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCTF8n%2FdJMcagYu9P6%2FDqFLAk4S9BAnbW9cshAYv1%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;408&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;408&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 기업에 잘 다니고 있는 지금 얘기하기엔 좀 웃기지만 2025년 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;나에게&lt;/span&gt;&amp;nbsp;꽤나 큰 이슈는 네이버 공채였다. 3개월이라는 긴 시간동안 채용이 진행되기도 했고, 정말 가고 싶은 기업이었기 때문에 최선을 다해 준비했다. 결과적으로는 최종 면접에서 탈락했고, 최선을 다한만큼 탈락했을 때 타격이 컸었다. 온전히 회복하는데까지 1~2일의 시간이 걸린 것 같다.&lt;br /&gt;당시에는 스트레스를 많이 받았기에 &lt;i&gt;'차라리 서류부터 떨어졌으면 기대도 안했을텐데'&lt;/i&gt;라는 생각을 했다. 하지만 나중에 돌이켜보니, 간절함을 가지고 최선을 다하는 과정에서 얻은 것들이 정말 많았다. 이때의 경험이 기반이 되어 다른 면접을 더 수월하게 임할 수 있었고, 국내 최대&lt;span&gt; IT기업의 문턱까지 가보며 개발자로서 자신감이 붙기도 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-9bebc404-aad8-46fb-a25f-7406e2f1e8bf&quot; style=&quot;text-align: left;&quot; 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;lt;한 기업에 최선을 다하고 집중하는 것&amp;gt;와 &amp;lt;정신건강을 위해 한 기업에만 올인하지 않는 것&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;약 10개월 정도 취준을 했다. 취준 기간이 길어지면서 &quot;남들 다 하는 건데 왜 나만 이렇게 어렵지?&quot;(아님)라는 생각이 들며 자존감도 낮아졌다. 좋아하는 우테코 동기들도 만나지 않고, 동기들이 컨퍼런스를 가자고 해도 가지 않았다. 가끔 친구들을 만나도 자신감이 떨어져서 온전히 즐기지 못했다. 미래가 불확실하다는 압박감이 너무나도 크게 다가와서 불면증, 위궤양까지 겪었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;파도 위에서 균형을 찾는 사람&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cG5Bn3/dJMcadm8p5d/lJ55ZgPCzDegV8oMOsEGk1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cG5Bn3/dJMcadm8p5d/lJ55ZgPCzDegV8oMOsEGk1/img.jpg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot; data-is-animation=&quot;false&quot; data-filename=&quot;KakaoTalk_Photo_2026-01-10-16-23-52 005.jpeg&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cG5Bn3/dJMcadm8p5d/lJ55ZgPCzDegV8oMOsEGk1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcG5Bn3%2FdJMcadm8p5d%2FlJ55ZgPCzDegV8oMOsEGk1%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;3024&quot; height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnqlc6/dJMcadm8p5b/PxOSoICUucfsGdHwsjxT40/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnqlc6/dJMcadm8p5b/PxOSoICUucfsGdHwsjxT40/img.jpg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot; data-is-animation=&quot;false&quot; data-filename=&quot;KakaoTalk_Photo_2026-01-10-16-23-53 007.jpeg&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnqlc6/dJMcadm8p5b/PxOSoICUucfsGdHwsjxT40/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbnqlc6%2FdJMcadm8p5b%2FPxOSoICUucfsGdHwsjxT40%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;3024&quot; height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l0Uk6/dJMcac9BzCw/CLpAeBLuxw3TEweabLOkeK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l0Uk6/dJMcac9BzCw/CLpAeBLuxw3TEweabLOkeK/img.jpg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot; data-is-animation=&quot;false&quot; data-filename=&quot;KakaoTalk_Photo_2026-01-10-16-23-53 010.jpeg&quot; style=&quot;width: 63.2558%; margin-right: 10px;&quot; data-widthpercent=&quot;64&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l0Uk6/dJMcac9BzCw/CLpAeBLuxw3TEweabLOkeK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl0Uk6%2FdJMcac9BzCw%2FCLpAeBLuxw3TEweabLOkeK%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;4032&quot; height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgE4em/dJMcadm8p5c/0MElwIZ2xVItRt7xq5fErK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgE4em/dJMcadm8p5c/0MElwIZ2xVItRt7xq5fErK/img.jpg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot; data-is-animation=&quot;false&quot; data-filename=&quot;KakaoTalk_Photo_2026-01-10-16-23-53 009.jpeg&quot; data-widthpercent=&quot;36&quot; style=&quot;width: 35.5814%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgE4em/dJMcadm8p5c/0MElwIZ2xVItRt7xq5fErK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgE4em%2FdJMcadm8p5c%2F0MElwIZ2xVItRt7xq5fErK%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;3024&quot; height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이대로 가다가는 장기적으로 건강한 취준을 할 수 없겠다는 생각이 들어, 의식적으로 &lt;i&gt;나를 위한 하루&lt;/i&gt;를 보내기 시작했다. 하루동안은 노트북을 펼치지 않고, 일찍 일어나 장을 보러 나갔다. 스스로를 위해 좋아하는 음식을 정성스레 만들었고, 방을 깨끗히 청소하며 복잡한 마음도 다잡으려 노력했다. 천천히 다이어리를 쓰면서 내 감정을 들여다봤다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;취준에 몰입하다 보면 시간적/금전적/심리적 여유가 줄어들어서, 좋아하는 취미나 여가를 온전히 즐기지 못할 때가 많다. 그러다 보면 &lt;i&gt;'취업만 하면 하고 싶은 걸 마음껏 할 수 있겠지'&lt;/i&gt;라는 생각을 하게 되고, 자연스레 행복을 미래로 계속 유예하게 된다. 하지만 미래에도 또 다른 어려움이 있을 것이고, 그렇게 미래만 바라보다 보면 지금의 나를 잃어버릴 것 같았다. 의식적으로 나를 돌보는 하루를 보내며, 현재를 온전히 받아들이고 즐기는 사람이 되려 했다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;수많은 실패와 단 한 번의 합격&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmZ8Pa/dJMcacPlTT8/jC9RKIQqeuvgdCKY7eaft1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmZ8Pa/dJMcacPlTT8/jC9RKIQqeuvgdCKY7eaft1/img.jpg&quot; data-origin-width=&quot;2886&quot; data-origin-height=&quot;3848&quot; data-is-animation=&quot;false&quot; data-filename=&quot;KakaoTalk_Photo_2026-01-10-16-23-51 001.jpeg&quot; style=&quot;width: 36.3631%; margin-right: 10px;&quot; data-widthpercent=&quot;36.79&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmZ8Pa/dJMcacPlTT8/jC9RKIQqeuvgdCKY7eaft1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmZ8Pa%2FdJMcacPlTT8%2FjC9RKIQqeuvgdCKY7eaft1%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;2886&quot; height=&quot;3848&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BU8nG/dJMcahC6ZjW/EYzGIij4iri8IB6kif7tdK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BU8nG/dJMcahC6ZjW/EYzGIij4iri8IB6kif7tdK/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;908&quot; data-filename=&quot;KakaoTalk_Photo_2026-01-10-16-23-51 002.jpeg&quot; style=&quot;width: 62.4741%;&quot; data-widthpercent=&quot;63.21&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BU8nG/dJMcahC6ZjW/EYzGIij4iri8IB6kif7tdK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBU8nG%2FdJMcahC6ZjW%2FEYzGIij4iri8IB6kif7tdK%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;1170&quot; height=&quot;908&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴 노력 끝에, 운이 좋게도 내가 원하는 조건의 기업에 합격할 수 있었다. 취준 기간이 길어지다보니 처음엔 믿기지 않았다.. (사실 아직도 가끔 신기하다) 합격하고보니, 밀도 있는 면접을 위해서는 내가 어떤 사람인지 잘 아는게 첫번째라고 느꼈다. 내가 뭘 알고 뭘 모르는지, 왜 이런 생각을 가지고 있는지를 깊이 회고한 후에야 내가 원하는 면접의 흐름을 가져갈 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가끔 &lt;i&gt;'이걸 좀 더 일찍 일었더라면 더 빨리 취업할 수 있었을까?'&lt;/i&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;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-07 오후 11.06.25.png&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;740&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0KxFh/dJMcaf6nrA0/665BTPirOIc7n4K9kcU01K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0KxFh/dJMcaf6nrA0/665BTPirOIc7n4K9kcU01K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0KxFh/dJMcaf6nrA0/665BTPirOIc7n4K9kcU01K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0KxFh%2FdJMcaf6nrA0%2F665BTPirOIc7n4K9kcU01K%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;740&quot; data-filename=&quot;스크린샷 2026-01-07 오후 11.06.25.png&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;740&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 합격 이후 일상 블로그에 회고를 남긴 적이 있다. (회고에 대한 광기 ㄷㄷ) 회고를 쓰기 위해 노트북을 켜자마자 가장 먼저 떠오른 단어가 바로 &lt;i&gt;'새옹지마'&lt;/i&gt;였다. 인생의 행복과 불행은 예측하기 어렵다는 뜻이다. 그렇기에 잠시 뒤처지는 사람이 기죽을 필요도 없고, 앞서가는 사람이 자만할 이유도 없다. 취준 기간 내내 이 말이 머릿속에 오래 남았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;스스로 채워가는 일상들&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mqR5z/dJMcagjUr5I/sAWuFpEjtONOE9Np7TRPYk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mqR5z/dJMcagjUr5I/sAWuFpEjtONOE9Np7TRPYk/img.jpg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot; data-is-animation=&quot;false&quot; data-filename=&quot;KakaoTalk_Photo_2026-01-10-16-30-05 001.jpeg&quot; style=&quot;width: 29.3253%; margin-right: 10px;&quot; data-widthpercent=&quot;29.67&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mqR5z/dJMcagjUr5I/sAWuFpEjtONOE9Np7TRPYk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmqR5z%2FdJMcagjUr5I%2FsAWuFpEjtONOE9Np7TRPYk%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;3024&quot; height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxeDj6/dJMcachvO2I/yU42vyroGYwh1PtO7BYhc0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxeDj6/dJMcachvO2I/yU42vyroGYwh1PtO7BYhc0/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;3520&quot; data-origin-height=&quot;1980&quot; data-filename=&quot;KakaoTalk_Photo_2026-01-10-16-30-05 002.jpeg&quot; style=&quot;width: 69.5119%;&quot; data-widthpercent=&quot;70.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxeDj6/dJMcachvO2I/yU42vyroGYwh1PtO7BYhc0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxeDj6%2FdJMcachvO2I%2FyU42vyroGYwh1PtO7BYhc0%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;3520&quot; height=&quot;1980&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취업 이후 완전히 새로운 일상을 살아가고 있다. 일단 네 번째 자취를 시작했다. 이전에는 학교, 인턴, 부트캠프 때문에 짧게만 자취를 했었는데, 이제는 본가에서 완전히 독립한 느낌이 들어서 왠지 기분이 이상하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취준 기간 동안 잃어버렸던 취미들도 다시 되찾고 있다. 동기들과 밴드 합주를 하거나 공연을 보러 다니고, 난생 처음 필라테스에 등록해서 일주일에 한번씩 공사장 소리도 내보고 있다. (ㅋㅋ;;)&lt;/p&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;i&gt;편안해보여서 경력직인 줄 알았다&lt;/i&gt;는 얘기도 많이 들을 정도로 잘 적응하고 있다. 운이 좋게도 너무 좋은 동료들을 만난 덕분이다. 사실 팀에는 잘 적응했지만 업무에 충분히 적응했는지는 잘 모르겠다. 이 부분은 앞으로의 숙제가 될 것 같다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;좋은 개발자로서의 마인드셋&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 개발자는 어떤 사람일까? 좋은 개발자가 되기 위해서는 어떤 노력을 해야할까? 입사 후 혼자 많이 고민해본 주제다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자신이 알고있는 기술에만 얽매이지 않고, 상황에 따라 유연하게 대처하는 사람&lt;/li&gt;
&lt;li&gt;겪은 어려움이나 실수를 숨기지 않고 빠르게 공유하는 사람&lt;/li&gt;
&lt;li&gt;제품 전반에 대한 이해를 가지고 놓친 부분을 빠르게 짚어내는 사람&lt;/li&gt;
&lt;/ul&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2026년에 이루고 싶은 것들&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&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;h4 data-ke-size=&quot;size20&quot;&gt;개발 컨퍼런스 발표&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 개발자로서 욕심이 크다. 내가 멋지다고 생각하는 개발자들이 하는 일들은 다 해보고 싶다. 그 중 하나가 컨퍼런스에서 발표하기다. 발표를 통해 내가 멋지고 대단한 사람이라는 걸 알리고 싶다. 내가 정말 멋진 사람이 아니더라도, 발표를 하다보면 멋진 사람이 되어있을 것이라 믿는다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;나에게 관대해지기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 욕심이 큰 만큼, 나에 대한 기준이 높다. 스스로에게 당근보다는 채찍을 더 많이 주는 편이다. 입사 한 달 차에 팀원들과 커피챗을 하며 모두가 칭찬을 해주었는데, 감사함과 동시에 &quot;내가 정말 그런가? 아직 부족한데..&quot;라는 생각이 들었다. 부족하더라도 나에게 당근을 주는 습관을 들여보자. 토스는 빠르게 흘러가는 만큼 완벽보다는 속도를 중요시 여기고, 실수하더라도 빠르게 공유하고 해결하면 된다.&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;h4 data-ke-size=&quot;size20&quot;&gt;나만의 강점 찾기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두가 각자만의 강점이 있다. 개발 역량뿐만 아니라 커뮤니케이션, 꼼꼼함, 사용자 경험 등등.. 그런데 아직은 내 강점이 어떤 건지 잘 모르겠다. 내가 일하는 방식을 조금 더 잘 이해하고, 그 안에서 내 강점을 살려서 더 잘 일할 수 있는 방식을 찾아가보고 싶다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;AI 잘 활용하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 주변에 AI를 잘 활용하는 동료들이 많다. 반면에 나는 개발 시에 로직이 헷갈릴 때 가볍게 참고하는 정도다. (아직은 코드를 완전히 이해하며 업무하고 싶다는 욕심도 있다) 팀원들과 AI를 주제로 얘기를 나눌때가 종종 있는데, 그럴 때마다 나는 잘 활용하지 않는다는 대답을 했고 왠지 모르게 시대에 뒤처져있다는 생각이 들어 부끄러웠다. 이제는 AI에 대한 거리감을 줄이고 업무 효율을 높이기 위해 더 적극적으로 활용해보려고 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;체력 증진&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;야근이 잦은 만큼 체력 관리가 필수라는 것을 느끼고 있다. 아직은 주 1회 정도로 가볍게 운동하고 있지만, 앞으로는 나와 잘 맞는 운동 패턴을 찾아 습관화해야겠다. 표준까진 아니더라도 표준에 근접할 때까지 근육량을 늘리는게 목표다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기술 블로그 업로드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직은 기술 부채를 해결한다거나 복잡한 아키텍처를 개선하는 등의 거창한 일을 해보지는 않았다. 남들이 다 할 수 있는 피쳐 개발이 아닌, 조금 더 복잡한 문제를 해결해 보고 그 과정을 기술 블로그로 담아보고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기록하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 업무를 했는지, 업무하며 배운 제품 정책, 사소한 인사이트까지 꾸준히 기록하고 싶다. 바쁘게 살다보면 어떻게 하루가 흘러갔는지 모를때가 있다. 아무런 긴장감 없이 기계처럼 이슈를 쳐내기 보다는 바쁘더라도 학습을 놓지않는 사람이 되고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;나이먹기 싫다고 투정하긴 했지만 사실 27살이 되는 2026년이 기대된다. 처음으로 개발자로서 온전히 채워가는 한 해가 될테고 그 과정 속에서 나만의 색깔을 찾아갈 것이다. 내가 바랐던 안정적인 삶 속에서 원하는 방향대로 나아가는 삶이 너무 즐겁다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 내 기대만큼 멋진 삶이 아닐 수 있다. 하지만 어려움이 와도 결국 이겨낼 것이고 그 속에서 깨닫는게 분명 있을 것이다. 예측대로 흘러가지 않기에 재밌는 인생이라는 걸 느끼는 요즘이다. 평탄하진&lt;span&gt; &lt;/span&gt;않더라도&lt;span&gt; &lt;/span&gt;어려움&lt;span&gt; &lt;/span&gt;속에서&lt;span&gt; &lt;/span&gt;많이&lt;span&gt; &lt;/span&gt;성장하는&lt;span&gt; &lt;/span&gt;해가&lt;span&gt; &lt;/span&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-end=&quot;2229&quot; data-start=&quot;2185&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2229&quot; data-start=&quot;2185&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회고</category>
      <author>hrniin</author>
      <guid isPermaLink="true">https://thdbs523.tistory.com/456</guid>
      <comments>https://thdbs523.tistory.com/456#entry456comment</comments>
      <pubDate>Sat, 10 Jan 2026 16:36:00 +0900</pubDate>
    </item>
    <item>
      <title>토스 NEXT 2025 Android Developer 합격 후기</title>
      <link>https://thdbs523.tistory.com/455</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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;사진자료_토스_경력_3년_이하_개발자_대상_2025_토스_NEXT_개발자_챌린지_접수_시작.png&quot; data-origin-width=&quot;2400&quot; data-origin-height=&quot;1628&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kJ2IC/btsQLNPTRwO/DR3fZBcr0Dip7OlWBlnh01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kJ2IC/btsQLNPTRwO/DR3fZBcr0Dip7OlWBlnh01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kJ2IC/btsQLNPTRwO/DR3fZBcr0Dip7OlWBlnh01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkJ2IC%2FbtsQLNPTRwO%2FDR3fZBcr0Dip7OlWBlnh01%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;475&quot; data-filename=&quot;사진자료_토스_경력_3년_이하_개발자_대상_2025_토스_NEXT_개발자_챌린지_접수_시작.png&quot; data-origin-width=&quot;2400&quot; data-origin-height=&quot;1628&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약 1년 째 이어지던 취업 준비 끝에, 드디어 토스에 합격하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8월 초에 NEXT 개발자 챌린지에 지원했고, 9월 말에 오퍼 레터를 받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 NEXT 후기가 거의 없어서 ㅎ 채용 과정에서의 준비 과정과 후기를 정리해 보려 한다.&lt;/p&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;과제 테스트 &amp;gt; 온라인 과제 테스트 &amp;gt; 직무 인터뷰 &amp;gt; 문화적합성 인터뷰 &amp;gt; 레퍼런스 체크 및 처우 협의 &amp;gt; 입사일 조율 및 입사&lt;/span&gt;&lt;br /&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;h3 data-ke-size=&quot;size23&quot;&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;나는 초기 커리어를 단단히 쌓고, 다양한 경험을 통해 시야를 넓히고 싶다는 목표를 가지고 있었기에 토스에 지원하게 되었다.&lt;/p&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;b&gt;이력서&lt;/b&gt;는 pdf 4장 분량이었고, 아래와 같은 사항들을 간략히 정리했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간단한 자기소개 - 관심 분야, 강점 등&lt;/li&gt;
&lt;li&gt;경력, 학력, 외부 경험, 자격증&lt;/li&gt;
&lt;li&gt;사용 가능한 기술 스택&lt;/li&gt;
&lt;li&gt;프로젝트 - 소개, 기술 스택, 기여한 부분&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;는 pdf 23장 분량이었다.&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;이력서는 기본사항을 위주로 작성했다면, 포트폴리오에는 &quot;어떻게 문제를 해결해나가는 사람인지&quot;에 집중했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 기술은 어느정도까지 알고 있는지, 프로젝트에서 어떤 문제를 어떻게 해결했는지, 우아한테크코스를 통해 어떤 걸 배웠는지, 내 협업 방식은 어떤지 등등..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연한 얘기를 길게 늘어놓기보다는, 나만의 얘기를 만들어나가려 노력했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;그렇지만 대부분의 IT기업이 그렇듯, 개발자는 스펙보다 실력이 훨씬 중요하다고 생각한다.&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8월 7일까지 서류 접수를 받고, 8월 9일에 바로 과제 테스트가 진행되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래머스에서 2시간 동안 객관식 34문제, 서술형 7문제를 풀어야 했다.&lt;/p&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;준비 방법&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;면접 대비용으로 안드로이드 개념들을 정리해둔 노션 페이지가 있는데, 테스트 전날 가볍게 훑어보았다.&lt;/p&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;후기&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;객관식은 안드로이드 심화적인 개념 위주의 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전히 처음 보는 개념의 문제가 3~4개 정도 있었는데, 문제를 꼼꼼히 읽으면서 답을 유추해나갔고 대부분 맞혔다고 생각한다.&lt;/p&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;대부분 무난하게 작성할 수 있었지만, 1~2문제는 고민을 좀 하게 되었다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-23 오후 10.19.22.png&quot; data-origin-width=&quot;1076&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGX9si/btsQK0I00zk/7rXDyIRNSthRZWqUTz937K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGX9si/btsQK0I00zk/7rXDyIRNSthRZWqUTz937K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGX9si/btsQK0I00zk/7rXDyIRNSthRZWqUTz937K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGX9si%2FbtsQK0I00zk%2F7rXDyIRNSthRZWqUTz937K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;127&quot; data-filename=&quot;스크린샷 2025-09-23 오후 10.19.22.png&quot; data-origin-width=&quot;1076&quot; data-origin-height=&quot;274&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;최선을 다해 테스트를 응시한 결과, 테스트 3일 뒤인 8월 12일에 합격 메일을 받을 수 있었다!&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;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;unnamed.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bm0AyN/btsQK3Z3HTR/97QODQLF2SErEKGmwPh3V0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bm0AyN/btsQK3Z3HTR/97QODQLF2SErEKGmwPh3V0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bm0AyN/btsQK3Z3HTR/97QODQLF2SErEKGmwPh3V0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbm0AyN%2FbtsQK3Z3HTR%2F97QODQLF2SErEKGmwPh3V0%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;263&quot; data-filename=&quot;unnamed.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직무 인터뷰는 9월 초에 온라인으로 진행되었다. (지원자마다 일정은 상이)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 직군은 인터뷰 전에 사전 과제를 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사전 과제를 60분 동안 구현한 뒤, 짧은 휴식 시간을 가지고 약 60분 동안 인터뷰가 진행되었다.&lt;/p&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;준비 방법&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;또한 토스 채용 홈페이지에 게시된 인터뷰 관련 아티클이 많은 도움이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(아티클 링크: &lt;a href=&quot;https://toss.im/career/article/server_developer_interview&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://toss.im/career/article/server_developer_interview&lt;/a&gt;,&amp;nbsp;&lt;a href=&quot;https://toss.im/career/article/next-25-frontend&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://toss.im/career/article/next-25-frontend&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;이렇게 면접 방식을 살펴본 결과, 공통적으로 강조되는 점이 하나 있다. 바로 &quot;왜?&quot;에 집중한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 프로젝트에 어떤 기술을 사용했다고 했을 때, 합리적인 근거와 맥락에 기반하여 선택했는지를 묻는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 면접 준비를 하는 과정에서도 &quot;왜?&quot;에 집중하며 예상 질문을 뽑았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제출한 이력서와 포트폴리오를 보면서, 사용한 기술이나 문제 해결 방식에 대해 &quot;왜 이런 판단을 내렸는지&quot;를 꼼꼼히 되짚고 정리하는 시간을 가졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에는 정리했던 내용들을 계속 들여다보며 면접에서 자연스럽게 얘기할 수 있도록 연습했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;처음 요구사항을 받았을 때는 엄청 당황했지만, 침착하게 다시 읽어보니 감이 잡혔다.&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시간 안에 프로젝트 셋팅부터 구현까지 완료하기엔 시간이 매우 부족했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드가 정리되지 않은 상태로 약 60% 정도 구현을 마치고 과제를 제출하게 되었다.&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;b&gt;면접 후기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 개발자 세 분과 함께 진행되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접 초반에는 과제를 기반으로 진행되고, 후반에는 이력서/포트폴리오를 기반으로 진행되었다.&lt;/p&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;i&gt;과제 기반 면접&lt;/i&gt;에서는 과제 코드를 함께 보면서 &quot;어떤 의도로 이 코드를 작성했는지&quot;를 위주로 말씀드렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 면접관님과 함께 의견을 주고받으며 과제 코드를 실시간으로 개선해나갔는데, 이 과정이 굉장히 재미있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소에 자신이 작성하는 코드에 대해 깊이 이해하는 것이 중요하다고 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;이력서 기반 면접&lt;/i&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;&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;기술 질문에 대한 대답을 하면 &quot;너무 잘 대답해 주셨네요~&quot;라고 ㅋㅋㅋ 해주셔서&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;/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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-23 오후 11.05.38.png&quot; data-origin-width=&quot;1012&quot; data-origin-height=&quot;216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QlzyA/btsQJ4kY6p8/OvFSKE8KMu5RbGnXeS5yt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QlzyA/btsQJ4kY6p8/OvFSKE8KMu5RbGnXeS5yt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QlzyA/btsQJ4kY6p8/OvFSKE8KMu5RbGnXeS5yt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQlzyA%2FbtsQJ4kY6p8%2FOvFSKE8KMu5RbGnXeS5yt1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;107&quot; data-filename=&quot;스크린샷 2025-09-23 오후 11.05.38.png&quot; data-origin-width=&quot;1012&quot; data-origin-height=&quot;216&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접을 보고 딱 일주일 후 합격 메일을 받을 수 있었다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문화적합성 인터뷰&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;unnamed.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDvNQV/btsQR4kajXH/LpIHkzYysuWBsgmN2SZjg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDvNQV/btsQR4kajXH/LpIHkzYysuWBsgmN2SZjg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDvNQV/btsQR4kajXH/LpIHkzYysuWBsgmN2SZjg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDvNQV%2FbtsQR4kajXH%2FLpIHkzYysuWBsgmN2SZjg1%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;263&quot; data-filename=&quot;unnamed.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9월 중순 온라인으로 약 1시간 동안 진행되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;준비 과정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 토스 홈페이지의 문화 관련 글과 토스 유튜브의 문화 관련 영상을 하나도 빠짐없이 시청했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 면접에 도움될 만한 것들을 따로 정리해두었다.&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;여러 소재가 있다면 예상치 못한 질문에서도 그 소재들을 기반으로 유연하게 답변을 구성할 수 있을 것이라 생각했고, 실제로도 그랬다.&lt;/p&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;b&gt;후기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HRBP님과 1:1로 면접이 진행되었다.&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;이런저런 얘기를 나누며 회고라는 키워드가 나왔고, &quot;오늘 면접에 대해 회고해보자면?&quot;이라는 질문을 하셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 &quot;긴장하지 않으려 노력했는데 긴장한게 면접에서 티가 난 것 같아 아쉽다&quot;라는 답변을 드렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접관님께서 &quot;그동안 토스에 여러번 도전해온 만큼 간절한 기회인데 긴장하는 것이 당연하다, 혜민님 내면에 있는 얘기를 듣고 싶어서 긴장하지 말라는 얘기를 여러번 했다, 너무 신경쓰지 말라&quot;라고 말씀해주셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 그동안 봤던 면접들 중 가장 편안하게 나를 드러낼 수 있었다. 이날 면접에서 가장 기억에 남는 순간이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;합격이어도 납득이 갔고 불합격이어도 납득이 갔다.&lt;/p&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 alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1508&quot; data-origin-height=&quot;1071&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eiLi6S/btsQMEkD3te/V97FXrUYndQJYZyXY6pC4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eiLi6S/btsQMEkD3te/V97FXrUYndQJYZyXY6pC4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eiLi6S/btsQMEkD3te/V97FXrUYndQJYZyXY6pC4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeiLi6S%2FbtsQMEkD3te%2FV97FXrUYndQJYZyXY6pC4k%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;1508&quot; height=&quot;1071&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1508&quot; data-origin-height=&quot;1071&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드디어 다른 후기에서만 보던 READY FOR TOSS를 만나게 되었다!&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;&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;/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;그동안 내가 추구해왔던 마인드셋을 잃지않고 꾸준히 노력한다면, 충분히 잘 할 수 있을거라 믿는다.&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;</description>
      <category>회고</category>
      <author>hrniin</author>
      <guid isPermaLink="true">https://thdbs523.tistory.com/455</guid>
      <comments>https://thdbs523.tistory.com/455#entry455comment</comments>
      <pubDate>Mon, 29 Sep 2025 00:13:29 +0900</pubDate>
    </item>
    <item>
      <title>코루틴 취소와 OkHttp 네트워크 요청 연동하기</title>
      <link>https://thdbs523.tistory.com/454</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;h3 data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘은 Retrofit, Room 같은 라이브러리에서 내부적으로 코루틴을 지원하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 앞에 suspend 키워드만 붙이면, 라이브러리가 알아서 처리해주기 때문에 개발자가 따로 신경쓸 필요가 없다.&lt;/p&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;그래서 직접 저수준의 코드를 구현하는 게 아니라면, 평소에 자주 접하게 되는 코루틴 API는 suspend, viewModelScope, Dispatchers 정도에 그친다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 최근 이미지 라이브러리를 만들다가 예상치 못한 오류를 만났고, 이를 해결하는 과정에서 suspendCancellableCoroutine을 사용하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴을 처음 학습할 때 다뤘던 API지만, 잘 사용하지 않다보니 어떤 상황에 활용해야 하는지 잊고 있었다.&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;h3 data-ke-size=&quot;size23&quot;&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755010904698&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    override suspend fun intercept(chain: Interceptor.Chain): Bitmap {
        val request = Request.Builder()
            .url(chain.url)
            .build()
        val call = okHttpClient.newCall(request)
        val response = call.execute()

        if (!response.isSuccessful) {
            val errorBody = response.body?.string() ?: &quot;No error body&quot;
            error(&quot;이미지를 불러올 수 없습니다. code=${response.code}, message=${response.message}, body=$errorBody&quot;)
        }

        val byteArray = response.body?.bytes() ?: error(&quot;응답이 비어있습니다.&quot;)
        val bitmap = byteArray.decodeSampledBitmap(chain.imageRequest.size)
        if (logging) Log.d(&quot;ImageLoader&quot;, &quot;Network Image Load: ${chain.url}&quot;)
        return bitmap
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OkHttp를 통해 네트워크 요청을 보내고, 응답을 받아 반환하는 간단한 함수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 intercept 함수는 suspend 함수로 선언되어 있다.&lt;/p&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;760&quot; data-origin-height=&quot;196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0biGA/btsPzDIPwAO/ltFmKkuuMOd8EHNS96QyWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0biGA/btsPzDIPwAO/ltFmKkuuMOd8EHNS96QyWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0biGA/btsPzDIPwAO/ltFmKkuuMOd8EHNS96QyWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0biGA%2FbtsPzDIPwAO%2FltFmKkuuMOd8EHNS96QyWk%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;760&quot; height=&quot;196&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;196&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 intercept 함수를 호출한 코루틴이 취소되어도, 네트워크 응답은 계속 진행된다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 OkHttp의 execute() 함수가 코루틴과 무관한 일반 함수이기 때문이다.&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;즉, intercept 함수 내부에서 코루틴이 취소되었다는 것을 감지하고 네트워크 요청을 취소해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 필요한 것이 바로 suspendCancellableCoroutine이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;suspendCancellableCoroutine의 역할&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-07-26 오후 7.02.52.png&quot; data-origin-width=&quot;2012&quot; data-origin-height=&quot;84&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGjRXL/btsPAMLrOEr/ISWO2zideubcHBJcrnXKfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGjRXL/btsPAMLrOEr/ISWO2zideubcHBJcrnXKfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGjRXL/btsPAMLrOEr/ISWO2zideubcHBJcrnXKfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGjRXL%2FbtsPAMLrOEr%2FISWO2zideubcHBJcrnXKfk%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;2012&quot; height=&quot;84&quot; data-filename=&quot;스크린샷 2025-07-26 오후 7.02.52.png&quot; data-origin-width=&quot;2012&quot; data-origin-height=&quot;84&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;suspendCancellableCoroutine을 호출하면 코루틴이 즉시 중단된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 적절한 시점에 resume() 또는 resumeWithException()을 호출하여 코루틴을 다시 재개한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;resume(): 인자로 전달한 값으로 코루틴을 재개&lt;/li&gt;
&lt;li&gt;resumeWithException(): 예외를 던지면서 코루틴을 재개&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;또한 block 람다의 인자로 CancellableContinuation을 받고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 활용해 코루틴 취소와 관련된 필요한 작업을 수행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 API들을 제공한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;invokeOnCacellation(): 코루틴이 취소될 때 수행할 작업을 지정한다.&lt;/li&gt;
&lt;li&gt;cancel(): 코루틴을 취소한다.&lt;/li&gt;
&lt;li&gt;isCancelled: 코루틴이 취소되었는지 확인한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;suspendCancellableCoroutine 사용 시 주의할 점&lt;/h3&gt;
&lt;pre id=&quot;code_1755010920384&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;suspend fun main() {
    val result = fetchData()
}

suspend fun fetchData() = suspendCancellableCoroutine&amp;lt;String&amp;gt; { continuation -&amp;gt;
    println(&quot;데이터 요청 시작&quot;)
    // ...

    if (shouldCancel) {
        println(&quot;작업 중단&quot;)
        return@suspendCancellableCoroutine
    }

    continuation.resume(&quot;서버에서 받은 데이터&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;resume()이나 resumeWithException()을 호출하지 않으면, 이 함수를 호출한 코루틴은 영원히 대기 상태로 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;suspendCancellableCoroutine은 호출 즉시 코루틴을 중단시키기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 return을 호출했기 때문에 fetchData() 함수는 종료되지만, 코루틴이 재개되지 않아 main() 함수의 코루틴은 계속 대기하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 방지하기 위해 suspendCancellableCoroutine 내부에서는 early return을 최대한 지양해야 한다.&lt;/p&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 intercept 함수로 돌아와서, 네트워크 요청을 취소 가능한 형태로 수정해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755010930602&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    override suspend fun intercept(chain: Interceptor.Chain): Bitmap =
        suspendCancellableCoroutine { continuation -&amp;gt;
            val request = Request.Builder()
                .url(chain.url)
                .build()
            val call = okHttpClient.newCall(request)
            val response = call.execute()

            continuation.invokeOnCancellation {
                call.cancel()
            }

            if (!response.isSuccessful) {
                val errorBody = response.body?.string() ?: &quot;No error body&quot;
                val exception = IllegalStateException(&quot;이미지를 불러올 수 없습니다. code=${response.code}, message=${response.message}, body=$errorBody&quot;)
                continuation.resumeWithException(exception)
            }

            val byteArray = response.body?.bytes()
            if (byteArray == null) {
                continuation.resumeWithException(IllegalStateException(&quot;응답이 비어있습니다.&quot;))
            } else {
                val bitmap = byteArray.decodeSampledBitmap(chain.imageRequest.size)
                if (logging) Log.d(&quot;ImageLoader&quot;, &quot;Network Image Load: ${chain.url}&quot;)
                continuation.resume(bitmap)
            }
        }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CancellableContinuation의 invokeOnCacellation()을 사용해서, 코루틴이 취소되면 네트워크 요청도 함께 취소되도록 구현했다.&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;h3 data-ke-size=&quot;size23&quot;&gt;콜백 기반 API와 함께 사용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 intercept 함수는 항상 IO 디스패처에서 호출하도록 설계되어 있어 execute를 사용했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 메인 디스패처에서도 안전하게 호출하려면 enqueue를 사용할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 경우에도 suspendCancellableCoroutine을 활용하면, 콜백 기반 API를 깔끔하게 코루틴 스타일로 감쌀 수 있다.&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;아래는 intercept 함수의 execute를 enqueue로 변환한 코드다.&lt;/p&gt;
&lt;pre id=&quot;code_1755010941878&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    override suspend fun intercept(chain: Interceptor.Chain): Bitmap =
        suspendCancellableCoroutine { continuation -&amp;gt;
            val request = Request.Builder()
                .url(chain.url)
                .build()
            val call = okHttpClient.newCall(request)

            continuation.invokeOnCancellation {
                call.cancel()
            }

            val callback = object : Callback {
                override fun onFailure(
                    call: Call,
                    e: IOException,
                ) {
                    continuation.resumeWithException(e)
                }

                override fun onResponse(
                    call: Call,
                    response: Response,
                ) {
                    if (!response.isSuccessful) {
                        val exception = IllegalStateException(&quot;이미지를 불러올 수 없습니다. code=${response.code}, message=${response.message}&quot;)
                        continuation.resumeWithException(exception)
                        return
                    }

                    val byteArray = response.body?.bytes()
                        ?: return continuation.resumeWithException(IllegalStateException(&quot;응답이 비어있습니다.&quot;))

                    val bitmap = byteArray.decodeSampledBitmap(chain.imageRequest.size)
                    if (logging) Log.d(&quot;ImageLoader&quot;, &quot;Network Image Load: ${chain.url}&quot;)
                    continuation.resume(bitmap)
                }
            }
            call.enqueue(callback)
        }&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;suspendCancellableCoroutine은 특히 콜백 기반 API를 코루틴 형태로 변환하고 싶을 때 유용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콜백이 단일 값을 반환하면 suspendCancellableCoroutine을, 스트림처럼 여러 값을 반환하면 callbackFlow를 사용한다.&lt;/p&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;suspendCoroutine vs suspendCancellableCoroutine&lt;/h3&gt;
&lt;p data-end=&quot;5112&quot; data-start=&quot;5065&quot; data-ke-size=&quot;size16&quot;&gt;suspendCancellableCoroutine을 학습하다보면 suspendCoroutine이 항상 함께 언급된다.&lt;/p&gt;
&lt;p data-end=&quot;5856&quot; data-start=&quot;5773&quot; data-ke-size=&quot;size16&quot;&gt;함수명에서 알 수 있듯 suspendCoroutine은 취소를 감지할 수 없는 suspendCancellableCoroutine과 유사하다.&lt;/p&gt;
&lt;p data-end=&quot;5112&quot; data-start=&quot;5065&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;5357&quot; data-start=&quot;5114&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;suspendCoroutine&lt;/td&gt;
&lt;td&gt;suspendCancellableCoroutine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;5288&quot; data-start=&quot;5228&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;5246&quot; data-start=&quot;5228&quot;&gt;Continuation 타입&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;5261&quot; data-start=&quot;5246&quot;&gt;Continuation&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;5288&quot; data-start=&quot;5261&quot;&gt;CancellableContinuation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;5309&quot; data-start=&quot;5289&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;5297&quot; data-start=&quot;5289&quot;&gt;취소 감지&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;5303&quot; data-start=&quot;5297&quot;&gt;불가능&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;5309&quot; data-start=&quot;5303&quot;&gt;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;5357&quot; data-start=&quot;5310&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;5318&quot; data-start=&quot;5310&quot;&gt;사용 목적&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;5335&quot; data-start=&quot;5318&quot;&gt;취소 처리 없는 단순 래핑&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;5357&quot; data-start=&quot;5335&quot;&gt;취소 시 리소스 해제가 필요한 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;두 함수 모두 코루틴을 일시 중단하고, 이후 수동으로 재개한다는 점은 같다.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;5481&quot; data-start=&quot;5359&quot; data-ke-size=&quot;size16&quot;&gt;suspendCoroutine은 취소 신호를 무시하기 때문에, 리소스 정리 없이 영원히 중단 상태로 남을 수 있다.&lt;br /&gt;반면 suspendCancellableCoroutine은 취소 처리가 가능해 훨씬 안전하다.&lt;/p&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;suspendCancellableCoroutine은 콜백 기반 API를 코루틴으로 감싸고, 코루틴 취소 시 별도의 처리가 필요할 때 사용한다.&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;424&quot; data-start=&quot;389&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;424&quot; data-start=&quot;389&quot; data-ke-size=&quot;size16&quot;&gt;기본적인 개념을 확실히 이해하고 있어야겠다.&lt;br /&gt;기초가 탄탄해야 복잡한 문제도 능숙하게 해결할 수 있다는 걸 다시금 깨달았다..!&lt;/p&gt;
&lt;p data-end=&quot;424&quot; data-start=&quot;389&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;424&quot; data-start=&quot;389&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>kotlin/coroutines</category>
      <author>hrniin</author>
      <guid isPermaLink="true">https://thdbs523.tistory.com/454</guid>
      <comments>https://thdbs523.tistory.com/454#entry454comment</comments>
      <pubDate>Wed, 13 Aug 2025 00:01:22 +0900</pubDate>
    </item>
    <item>
      <title>Fragment에서 ViewCompositionStrategy를 사용하는 이유</title>
      <link>https://thdbs523.tistory.com/453</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;오디&lt;/span&gt;&lt;/b&gt;라는 프로젝트에서 점진적으로 Compose 마이그레이션을 진행중이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 Compose를 사용한게 아니기 때문에, 화면 전환에는 Compose Navigation이 아닌 Intent나 FragmentManager 방식을 사용하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면 전환은 기존에 사용하던 방식을 사용하고, 각각의 Activity/Fragment에서 컴포저블 함수를 호출하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 LoginActivity라면 LoginScreen을 호출하고, MeetingsActivity라면 MeetingsScreen을 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때, Fragment에서는 ViewCompositionStrategy와 함께 컴포저블 함수를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;State 유지와 메모리 누수를 방지하기 위해&quot; ViewCompositionStrategy를 사용하는 것은 알고 있었지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;막상 어떤 문제가 발생하는지, 그리고 ViewCompositionStrategy은 어떤 역할을 하는지 제대로 알지 못하고 사용했다.&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;h3 data-ke-size=&quot;size23&quot;&gt;Activity에서 Compose 사용하기&lt;/h3&gt;
&lt;pre id=&quot;code_1751782271458&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class XXXActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            enableEdgeToEdge()
            OdyTheme {
                XXXScreen()
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 Activity에서는 위처럼 간단히 setContent 블록 내에서 컴포저블 함수를 호출할 수 있다.&lt;/p&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;Fragment에서 Compose 사용하기 - 좋지 못한 예시&lt;/h3&gt;
&lt;pre id=&quot;code_1748792228128&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class XXXFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View {
        return ComposeView(requireContext()).apply {
            setContent {
                OdyTheme {
                    XXXScreen()
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 Fragment에서 Activity와 유사하게 컴포저블 함수를 호출하면 메모리 누수가 발생한다.&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;Fragment에서 Compose 사용하기 - 좋은 예시&lt;/h3&gt;
&lt;pre id=&quot;code_1748792308690&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class XXXFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View {
        return ComposeView(requireContext()).apply {
            setViewCompositionStrategy(
                ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed,
            )
            setContent {
                OdyTheme {
                    XXXScreen()
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하면 위처럼 사용하면 안전하게 컴포저블 함수를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;setContent 뿐만 아니라 글 초기에 말했던 ViewCompositionStrategy도 설정해야 한다.&lt;/p&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;ViewCompositionStrategy를 설정하지 않으면 발생하는 문제&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1748956605996&quot; class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class XXXFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View {
        return ComposeView(requireContext()).apply {
            setContent {
                OdyTheme {
                    XXXScreen()
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 아까 좋지 못한 예시로 보여준 코드의 문제점을 자세히 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 컴포지션은 Window에 detach될 때 dispose된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Window에서 detach된다는 것은 여러 경우가 있다. 곧 뷰가 소멸되거나, 컨테이너가 뷰에서 벗어나는 경우 등이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉, Fragment View가 소멸되는 타이밍과 Fragment의 컴포지션이 제거되는 타이밍이 다르다.&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;그렇다면 &quot;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Window에 detach되는 것&quot;은&amp;nbsp;&lt;/span&gt;Fragment의 어떤 생명주기에서 발생될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글링을 열심히 해본 결과 명확한 답을 찾을 수 없었다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Window에 detach되는 것과 Activity에 detach되는 것은 다르기 때문에, onDetach() 이전에 컴포지션이 제거될 것이라 생각한다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어쨌든 Fragment에서 뷰가 소멸되는 onDestroyView()과는 완전히 대응되지 않아서 이러한 문제가 발생한다는 것은 알겠다!&lt;/p&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;Fragment View가 소멸되지 않았는데 Fragment의 컴포지션이 제거된다면?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View는 남아있는데 컴포지션이 제거되어 처음부터 다시 컴포지션이 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 기존 컴포지션에서 사용되고 있던 State들은 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;기존 State들이 유실되어 부자연스러운 UI가 나타날 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 LazyColumn의 스크롤 상태가 보존되지 못하고 갑자기 화면 가장 상단으로 스크롤이 이동할 수도 있을 것이다.&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;또는 Fragment View는 소멸되었는데 컴포지션은 살아있다면, 메모리 누수로 이어질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ViewCompositionStrategy의 역할&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Fragment에서 ViewCompositionStrategy 없이 컴포즈를 사용하면 어떤 문제가 발생하는지 이해했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 ViewCompositionStrategy는 어떤 역할을 하길래 이러한 문제를 해결해 주는지 알아보자.&lt;/p&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;ViewCompositionStrategy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;ViewCompositionStrategy는 &lt;b&gt;컴포지션을 언제 제거해야 하는지 정의하는 것&lt;/b&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;Fragment 컴포지션이 제거되는 시점이 Fragmen View의 소멸 시점과 달라서 생기는 문제라면,&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;컴포지션이 제거되는 시점을 코드 상으로 커스텀하면 되는 것이다!&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;ViewCompositionStrategy 종류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;현재는 ViewCompositionStrategy에 4가지 종류가 존재한다.&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;ViewCompositionStrategy에 대한 글이 아니므로 간단히만 살펴보려고 한다.&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DisposeOnDetachedFromWindow
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;뷰가 Window에서 detach되면 컴포지션이 제거된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DisposeOnDetachedFromWindowOrReleasedFromPool
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;뷰가 Window에서 detach되면 컴포지션이 제거된다.&lt;/li&gt;
&lt;li&gt;Pooling Container의 뷰가 Window에서 detach되거나, 풀이 가득찬 경우 컴포지션이 제거된다.&lt;/li&gt;
&lt;li&gt;(뷰가 Pooling Container에 포함되지 않는다면 DisposeOnDetachedFromWindow와 동일)&lt;/li&gt;
&lt;li&gt;더 자세히 알고 싶다면 &lt;a href=&quot;https://medium.com/androiddevelopers/jetpack-compose-interop-using-compose-in-a-recyclerview-569c7ec7a583&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크 참고&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DisposeOnLifecycleDestroyed
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인자로 들어온 Lifecycle이 소멸되면 컴포지션이 제거된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DisposeOnViewTreeLifecycleDestroyed
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연결된 뷰에서 가져온 ViewTreeLifecycleOwner가 소멸되면 컴포지션이 제거된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;ViewCompositionStrategy의 기본값&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-02 오후 10.47.22.png&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;1090&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2mlcV/btsOmIDX8kq/vVnWwSBJQQdlPKzJSU8DVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2mlcV/btsOmIDX8kq/vVnWwSBJQQdlPKzJSU8DVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2mlcV/btsOmIDX8kq/vVnWwSBJQQdlPKzJSU8DVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2mlcV%2FbtsOmIDX8kq%2FvVnWwSBJQQdlPKzJSU8DVk%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;1546&quot; height=&quot;1090&quot; data-filename=&quot;스크린샷 2025-06-02 오후 10.47.22.png&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;1090&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 모든 컴포지션의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;ViewCompositionStrategy 기본값은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;DisposeOnDetachedFromWindowOrReleasedFromPool이다.&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;ViewCompositionStrategy.&lt;/span&gt;Default 내부를 확인해보면 단순히&amp;nbsp;DisposeOnDetachedFromWindowOrReleasedFromPool만 반환하고 있다.&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;즉, 별도로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;ViewCompositionStrategy를 설정하지 않으면&lt;span&gt;&amp;nbsp;&lt;/span&gt;Window에서 detach될 때 컴포지션이 제거된다는 것을 알 수 있다.&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;Fragment의 &lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;ViewCompositionStrategy&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1748872118474&quot; class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class XXXFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View {
        return ComposeView(requireContext()).apply {
            setViewCompositionStrategy(
                ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed,
            )
            setContent {
                OdyTheme {
                    XXXScreen()
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;다시 Fragment의 &lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;ViewCompositionStrategy로 돌아와보자. &lt;/span&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 style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;위 코드는&amp;nbsp;&lt;/span&gt;&lt;/span&gt;글 초반에 본 좋은 예시의 코드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네 종류의 &lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;ViewCompositionStrategy 중&amp;nbsp;&lt;/span&gt;DisposeOnViewTreeLifecycleDestroyed를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 명시적으로 &quot;Fragment View가 소멸될 때 컴포지션도 제거해줘&quot;라고 설정했기 때문에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fragment View의 생명주기와 동일한 시점에 컴포지션이 제거된다는 것이 보장되는 것이고, State도 유실되지 않을 수 있는 것이다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748871802582&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class XXXFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View {
        return ComposeView(requireContext()).apply {
            setViewCompositionStrategy(
                ViewCompositionStrategy.DisposeOnLifecycleDestroyed(this@XXXFragment.viewLifecycleOwner)
            )
            setContent {
                OdyTheme {
                    XXXScreen()
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DisposeOnViewTreeLifecycleDestroyed이 아닌 DisposeOnLifecycleDestroyed을 사용할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 명시적으로 인자로 Fragment View의 LifecycleOwner를 전달해주면,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DisposeOnViewTreeLifecycleDestroyed를 사용한 것과 동일하게 Fragment View가 소멸될 때 컴포지션도 제거된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Activity에서는 &lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;ViewCompositionStrategy를 사용하지 않는 이유&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;그런데 왜 Acticity에서는 &lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;ViewCompositionStrategy를 따로 설정하지 않았는지 의문이 들 수도 있다.&lt;/span&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;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;이유는 Fragment와 Activity의 생명주기 관리 방식의 차이 때문이다.&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;821&quot; data-origin-height=&quot;1004&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3NTNB/btsOm83FYBj/x7RuKltDteBn3Mof0gBVe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3NTNB/btsOm83FYBj/x7RuKltDteBn3Mof0gBVe1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3NTNB/btsOm83FYBj/x7RuKltDteBn3Mof0gBVe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3NTNB%2FbtsOm83FYBj%2Fx7RuKltDteBn3Mof0gBVe1%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;400&quot; height=&quot;489&quot; data-origin-width=&quot;821&quot; data-origin-height=&quot;1004&quot;/&gt;&lt;/span&gt;&lt;/figure&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;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;먼저 Fragment의 생명주기를 보자.&lt;/span&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;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;Fragment는 Fragment 자체의 생명주기와 Fragment View의 생명주기가 다르다.&lt;/span&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;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;Fragment 생명주기가 Fragment View의 생명주기 보다 길기 때문에,&lt;/span&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;Fragment는 살아있는데 Fragment View가 여러번 생성/소멸될 수 있다.&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;background-color: #ffffff;&quot;&gt;즉, 뷰가 소멸된 후에도 참조가 남아있다면 메모리에서 해제되지 못하고 메모리 누수로 이어질 수 있기 때문에 주의해야 한다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #202124;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;이러한 관점에서 컴포지션의 생명주기와 &lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;Fragment View 생명주기를 완전히 동일시켜야 안정적이다.&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;513&quot; data-origin-height=&quot;663&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbCSa9/btsOokP32LN/uKqNh2VYquPIXaWTwNPQY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbCSa9/btsOokP32LN/uKqNh2VYquPIXaWTwNPQY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbCSa9/btsOokP32LN/uKqNh2VYquPIXaWTwNPQY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbCSa9%2FbtsOokP32LN%2FuKqNh2VYquPIXaWTwNPQY1%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;400&quot; height=&quot;517&quot; data-origin-width=&quot;513&quot; data-origin-height=&quot;663&quot;/&gt;&lt;/span&gt;&lt;/figure&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;반면에 Activity의 생명주기는 Activity의 생명주기와 Activity View의 생명주기가 동일하다.&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;Activity가 소멸되면 Activity View도 소멸되고 컴포지션 또한 제거된다.&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;즉, 특별한 설정 없어도 &lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;Activity View와 컴포지션은 함께 정리된다.&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;이러한 차이 때문에 Fragment에서는 항상 Activity 보다 추가적인 설정이 필요한 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(데이터 바인딩에서 binding 객체의 참조를 명시적으로 제거한 것처럼..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;이렇게 마음 한구석 찝찝했던 Fragment의 ViewCompositionStrategy에 대해 알아봤다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드의 생산성을 위해 프로젝트를 Compose로 마이그레이션하고 있지만, 이것저것 새롭게 알게 되는 것이 많아 즐거운 마음으로 마이그레이션하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100% 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;현재 마이그레이션 진행 중인 &lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;오디&lt;/span&gt;&lt;/b&gt; 프로젝트의 깃허브 링크를 끝으로 글을 마무리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/woowacourse-teams/2024-ody&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/woowacourse-teams/2024-ody&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1748873042824&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - woowacourse-teams/2024-ody: 더 이상 &amp;quot;너 어디야?&amp;quot;라고 물어보지 마세요. 약속을 더욱 즐겁고 편안&quot; data-og-description=&quot;더 이상 &amp;quot;너 어디야?&amp;quot;라고 물어보지 마세요. 약속을 더욱 즐겁고 편안하게, 여러분의 우정을 더욱 돈독하게 만들어 드릴게요. - woowacourse-teams/2024-ody&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/woowacourse-teams/2024-ody&quot; data-og-url=&quot;https://github.com/woowacourse-teams/2024-ody&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bFSGuY/hyY0uavxpz/97JCjK6PObl6fUGr7DDYBK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/NGNbQ/hyY33I3C8w/zjdvF4L94fkpmfEyfiZeR1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/woowacourse-teams/2024-ody&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/woowacourse-teams/2024-ody&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bFSGuY/hyY0uavxpz/97JCjK6PObl6fUGr7DDYBK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/NGNbQ/hyY33I3C8w/zjdvF4L94fkpmfEyfiZeR1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - woowacourse-teams/2024-ody: 더 이상 &quot;너 어디야?&quot;라고 물어보지 마세요. 약속을 더욱 즐겁고 편안&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;더 이상 &quot;너 어디야?&quot;라고 물어보지 마세요. 약속을 더욱 즐겁고 편안하게, 여러분의 우정을 더욱 돈독하게 만들어 드릴게요. - woowacourse-teams/2024-ody&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/develop/ui/compose/migrate/interoperability-apis/compose-in-views?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.android.com/develop/ui/compose/migrate/interoperability-apis/compose-in-views?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ViewCompositionStrategy&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ViewCompositionStrategy&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/guide/components/activities/activity-lifecycle?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.android.com/guide/components/activities/activity-lifecycle?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/guide/fragments/lifecycle?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.android.com/guide/fragments/lifecycle?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/androiddevelopers/jetpack-compose-interop-using-compose-in-a-recyclerview-569c7ec7a583&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://medium.com/androiddevelopers/jetpack-compose-interop-using-compose-in-a-recyclerview-569c7ec7a583&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;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>kotlin/compose</category>
      <author>hrniin</author>
      <guid isPermaLink="true">https://thdbs523.tistory.com/453</guid>
      <comments>https://thdbs523.tistory.com/453#entry453comment</comments>
      <pubDate>Tue, 3 Jun 2025 22:45:51 +0900</pubDate>
    </item>
    <item>
      <title>Paging3+RecyclerView를 Compose로 마이그레이션하기</title>
      <link>https://thdbs523.tistory.com/452</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;요즘 &lt;span style=&quot;color: #6164c6;&quot;&gt;오디&lt;/span&gt;를 Compose로 마이그레이션하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #6164c6;&quot;&gt;오디&lt;/span&gt;는 우아한테크코스에서 팀 프로젝트로 개발했던 앱인데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시에는 Compose를 모르는 상태였기 때문에 모든 UI가 XML로 구현되어 있다.&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 alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screen_Recording_20250404_164514.gif&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZB2go/btsM8if0zi8/0fSAtlHKN5uql45GiPOGw0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZB2go/btsM8if0zi8/0fSAtlHKN5uql45GiPOGw0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZB2go/btsM8if0zi8/0fSAtlHKN5uql45GiPOGw0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bZB2go/btsM8if0zi8/0fSAtlHKN5uql45GiPOGw0/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;300&quot; height=&quot;667&quot; data-filename=&quot;Screen_Recording_20250404_164514.gif&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #6164c6;&quot;&gt;오디&lt;/span&gt;의 주소 검색 화면은 Paging3을 사용해 무한 스크롤을 구현했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 화면을 마이그레이션하며 &quot;Compose에 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Paging3를 &lt;/span&gt;어떻게 적용할지&quot;에 대해 배운 것들을 정리해 보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 RecyclerView에서 Compose로 마이그레이션하는 방법을 중점적으로 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Paging3와 PagingSource, PagingDataAdapter에 대한 내용을 자세히 다루지 않는다.&lt;/p&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;가장 먼저, RecyclerView와 Compose 각각의 방법에 대해 요약해 보자면 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;RecyclerView의 Paging3 적용 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. PagingSource를 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. PagingDataAdapter를 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. PagingSource에서 가져온 데이터를 collect한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. Adapter의 submitData를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Compose의 Paging3 적용 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. PagingSource를 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;PagingSource에서 가져온 데이터를 State로 변환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. LazyList(LazyColumn, LazyRow...)를 사용해 데이터를 화면에 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Compose는 (당연하게도) Adapter를 정의할 필요가 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PagingSource를 작성하는 것 외에는, 일반 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;*&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PagingSource는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;데이터 소스에서 페이지 별로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&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;가져오는 방법을 정의한 객체다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;PagingDataAdapter는 RecyclerView의 Adapter 역할을 한다. 페이징 데이터를 쉽게 처리할 수 있도록 해준다.&lt;/p&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. Compose Paging3 의존성 추가&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1743765736428&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// libs.versions
androidx-compose-paging = { group = &quot;androidx.paging&quot;, name = &quot;paging-compose&quot;, version.ref = &quot;androidxPaging&quot; }

// build.gradle
implementation(libs.androidx.compose.paging)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose에서 Paging을 사용하기 위해서는 별도의 의존성 추가가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025/04/04 기준 최신 버전은 3.3.6이다.&lt;/p&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;2. 컴포저블에서 Paging 데이터 가져오기&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1743765749427&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val addresses = viewModel.address.collectAsLazyPagingItems()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;collectAsLazyPagingItems 라는 함수를 통해 PagingData를 Compose에서 사용할 수 있는 상태로 변환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743765759356&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
public fun &amp;lt;T : Any&amp;gt; Flow&amp;lt;PagingData&amp;lt;T&amp;gt;&amp;gt;.collectAsLazyPagingItems(
    context: CoroutineContext = EmptyCoroutineContext
): LazyPagingItems&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;collectAsLazyPagingItems의 리시버는 Flow&amp;lt;PagingData&amp;lt;T&amp;gt;&amp;gt;이며, 반환 타입은 LazyPagingItems이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 이 LazyPagingItems 덕분에 페이지 데이터를 LazyList에서 바로 사용할 수 있는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로는 MutableState 타입의 itemSnapshotList가 Flow를 바라보고 있다.&lt;/p&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;3. LazyList로 페이지 데이터 띄우기&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1743765773019&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
private fun AddressList(
    addresses: LazyPagingItems&amp;lt;Address&amp;gt;,
    onClickAddress: (Address) -&amp;gt; Unit,
) {
    LazyColumn(...) {
        items(count = addresses.itemCount) { index -&amp;gt;
            val address = addresses[index] ?: return@items
            AddressItem(address, onClickAddress)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LazyPagingItems와 LazyList를&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;/span&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;보다시피 기존에 구현하던 LazyList 방식과 별반 다를게 없다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LazyPagingItems의 itemCount를 통해 아이템 개수를 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743765783124&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun items(
    count: Int,
    key: ((index: Int) -&amp;gt; Any)? = null,
    contentType: (index: Int) -&amp;gt; Any? = { null },
    itemContent: @Composable LazyItemScope.(index: Int) -&amp;gt; Unit
)
    
inline fun &amp;lt;T&amp;gt; LazyListScope.items(
    items: List&amp;lt;T&amp;gt;,
    noinline key: ((item: T) -&amp;gt; Any)? = null,
    noinline contentType: (item: T) -&amp;gt; Any? = { null },
    crossinline itemContent: @Composable LazyItemScope.(item: T) -&amp;gt; Unit
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 List를 사용했을 때와 차이점은 사용하는 items 함수가 다르다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 첫 번째 함수가 Paging과 함께 사용하는 함수고, 두 번째 함수가 일반적으로 List와 함께 사용하는 함수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 함수의 items 매개변수 타입이 List&amp;lt;T&amp;gt;이므로, Paging에서는 사용할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 items 함수&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LazyListScope 인터페이스 내부에 정의되어 있다.&lt;/li&gt;
&lt;li&gt;아이템의 개수를 전달하고, itemContent에서는 람다의 인자 index를 통해 직접 아이템을 가져온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 items 함수&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LazyListScope의 확장 함수 형태다.&lt;/li&gt;
&lt;li&gt;아이템 리스트를 전달하고, itemContent에서는 람다의 인자가 아이템이므로, 바로 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. LazyList에 key 지정하기&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1743765796798&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
private fun AddressList(
    addresses: LazyPagingItems&amp;lt;Address&amp;gt;,
    onClickAddress: (Address) -&amp;gt; Unit,
) {
    LazyColumn(...) {
        items(
            count = addresses.itemCount,
            key = addresses.itemKey { it.id },
        ) { index -&amp;gt;
            val address = addresses[index] ?: return@items
            AddressItem(address, onClickAddress)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LazyList에서는 리컴포지션을 최적화하기 위해 key를 지정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LazyPagingItems를 사용할 때도 itemKey라는 확장 함수를 통해 key를 지정할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743765809553&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public fun &amp;lt;T : Any&amp;gt; LazyPagingItems&amp;lt;T&amp;gt;.itemKey(
    key: ((item: T) -&amp;gt; Any)? = null
)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;itemKey 람다의 인자 item을 사용해 어떤 값을 key로 사용할 지 지정한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 예제 코드에서는 Address 객체의 id를 key로 사용했다. ( itemKey { it.id } )&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 key가 null이라면(기본값), index가 key가 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5. 동적으로 데이터가 로드되지 않는다면&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LazyList에서는 무조건 LazyPagingItems 타입을 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무한 스크롤의 이점인 지연 로딩이 되지 않을 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743765817500&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun AddressSearchScreen(
    viewModel: AddressSearchViewModel = hiltViewModel(),
) {
    val addresses = viewModel.address.collectAsLazyPagingItems()

    Scaffold(...) {
        AddressSearchContent(
            addresses = addresses.toList()
        )
    }
}

private fun &amp;lt;T : Any&amp;gt; LazyPagingItems&amp;lt;T&amp;gt;.toList(): List&amp;lt;T&amp;gt; {
    val list = mutableListOf&amp;lt;T&amp;gt;()
    repeat(itemCount) {
        val item = get(it) ?: return@repeat
        list.add(item)
    }
    return list
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 위 코드처럼 LazyPagingItems이 아닌 List를 사용했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;굳이굳이 LazyPagingItems의 확장 함수 toList()를 작성해 가면서..;;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743765827610&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
@Preview(showSystemUi = true)
private fun AddressSearchContentPreview() {
    OdyTheme {
        AddressSearchContent(
            addresses = listOf(...),
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 프리뷰를 더 수월하게 작성하기 위함이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LazyPagingItems의 객체를 직접 생성할 수 없기 때문에, 컴포저블의 인자로 LazyPagingItems를 전달할 수 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 데이터를 로드해 오는 부분에 로그를 찍어보니, 한 번에 모든 데이터를 불러오고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, List가 아닌 LazyPagingItems를 사용해야 데이터를 동적으로 불러올 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 생각해 보면 당연하다.. LazyPagingItems 내부의 PagingData에서 스크롤 시 자동으로 새로운 페이지를 불러오고 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 LazyPagingItems를 사용했을 때 프리뷰는 어떻게 작성할까?&lt;/p&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;6. LazyPagingItems 프리뷰 작성하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프리뷰로 확인하려는 컴포저블 함수의 인자로 LazyPagingItems를 전달해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 LazyPagingItems의 객체를 직접 생성할 수는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743765839030&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
@Preview(showSystemUi = true)
private fun AddressSearchContentPreview() {
    val addresses = listOf(...)

    val pagingData = PagingData.from(addresses)
    val lazyPagingItems = flowOf(pagingData).collectAsLazyPagingItems()

    OdyTheme {
        AddressSearchContent(
            addresses = lazyPagingItems,
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 위처럼 프리뷰를 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LazyPagingItems는 PagingData를 Flow 형태로 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, PagingData의 Flow를 생성 후 collectAsLazyPagingItems를 호출하여 LazyPagingItems를 생성하면 된다.&lt;/p&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;7. PagingSource의 LoadState 가져오기&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1743765854931&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class AddressPagingSource(...) : PagingSource&amp;lt;Int, Address&amp;gt;() {
    override suspend fun load(params: LoadParams&amp;lt;Int&amp;gt;): LoadResult&amp;lt;Int, Address&amp;gt; {
        return runCatching {
            val addresses = addressRepository.fetchAddresses(...).getOrThrow()
            LoadResult.Page(...)
        }.getOrElse { throwable -&amp;gt;
            LoadResult.Error(throwable)
        }
    }

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PagingSource에서는 데이터 소스에서 데이터를 가져오다가 오류가 발생한 경우, LoadResult.Error를 반환할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 LoadResult를 UI 계층에서 받아와 에러 핸들링을 해야 하는 경우가 있다. (ex: 토스트, 스낵바, 에러 화면 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743765865235&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;adapter.addLoadStateListener { loadState -&amp;gt;
    when (loadState.append) {
        LoadState.Loading -&amp;gt; {}
        is LoadState.NotLoading -&amp;gt; {}
        is LoadState.Error -&amp;gt; {}
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RecyclerView에서는 PagingDataAdapter의 addLoadStateListener를 사용하여 LoadState를 받아왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743765874402&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun AddressSearchScreen(
    viewModel: AddressSearchViewModel = hiltViewModel(),
) {
    val addresses = viewModel.address.collectAsLazyPagingItems()

    Scaffold(...) { innerPadding -&amp;gt;
        when (val state = addresses.loadState.append) {
            LoadState.Loading -&amp;gt; TODO()
            is LoadState.Error -&amp;gt; TODO()
            is LoadState.NotLoading -&amp;gt; TODO()
        }

        AddressSearchContent(...)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose의 경우 collectAsLazyPagingItems에서 가져온 LazyPagingItems를 통해 loadState에 접근할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743765883619&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class CombinedLoadStates(
    val refresh: LoadState,
    val prepend: LoadState,
    val append: LoadState,
    val source: LoadStates,
    val mediator: LoadStates?
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;addLoadStateListener 와 LazyPagingItems의 loadState는 CombinedLoadStates 타입이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CombinedLoadStates는 LoadState를 가지고 있는 일종의 컬렉션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 핸들링을 위해 refresh, prepend, append를 주로 사용한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;refresh: 데이터를 새로고침하거나 처음 데이터를 로드했을 때의 상태&lt;/li&gt;
&lt;li&gt;prepend: 데이터의 앞쪽에 새로운 데이터를 추가할 때의 상태 (위로 스크롤)&lt;/li&gt;
&lt;li&gt;append: 데이터의 뒤쪽에 새로운 데이터를 추가할 때의 상태 (아래로 스크롤)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포저블 함수 collectAsLazyPagingItems를 사용하여 Flow&amp;lt;PagingData&amp;lt;T&amp;gt;&amp;gt;를 LazyPagingItems&amp;lt;T&amp;gt;로 변환한다.&lt;/li&gt;
&lt;li&gt;LazyPagingItems를 사용해 LazyList에서 페이지 데이터를 화면에 보여준다.&lt;/li&gt;
&lt;li&gt;LazyPagingItems를 사용해 LoadState를 가져올 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;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;&lt;a href=&quot;https://developer.android.com/reference/kotlin/androidx/paging/compose/package-summary&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.android.com/reference/kotlin/androidx/paging/compose/package-summary&lt;/a&gt;&lt;/p&gt;</description>
      <category>kotlin/compose</category>
      <author>hrniin</author>
      <guid isPermaLink="true">https://thdbs523.tistory.com/452</guid>
      <comments>https://thdbs523.tistory.com/452#entry452comment</comments>
      <pubDate>Fri, 4 Apr 2025 20:21:36 +0900</pubDate>
    </item>
    <item>
      <title>Flow, StateFlow, SharedFlow를 편하게 테스트하는 방법 (feat: Turbine)</title>
      <link>https://thdbs523.tistory.com/451</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;그동안 Flow의 특성을 고려하지 않거나 가독성이 좋지 않은 상태로 &quot;돌아가는 테스트 코드&quot;를 작성했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다보니 내가 짠 테스트인데도 지금 다시 보니 어떤 코드인지 제대로 이해하지 못하는 경우가 꽤나 있었다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;그 과정에서 Turbine 라이브러리를 얕게 학습했는데, 오늘은 이와 관련된 포스팅을 하려고 한다.&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;Turbine&amp;nbsp;라이브러리가 무엇인지 간단히 알아보고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Turbine를 사용한 Flow와 StateFlow, SharedFlow 테스트 방법을 각각 소개한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Turbine를 사용하지 않았을 때의 코드와 비교하여 Turbine의 이점을 알아보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;Turbine 라이브러리란?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/cashapp/turbine&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/cashapp/turbine&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1742992350707&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - cashapp/turbine: A testing library for kotlinx.coroutines Flow&quot; data-og-description=&quot;A testing library for kotlinx.coroutines Flow. Contribute to cashapp/turbine development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/cashapp/turbine&quot; data-og-url=&quot;https://github.com/cashapp/turbine&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/kn7OG/hyYxKpTjE0/VPkNXxWCLOnBob4XKkEkPk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bNmua8/hyYvvm8mLp/4h4ZeAR9V9DINtCIXOKLjK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/cashapp/turbine&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/cashapp/turbine&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/kn7OG/hyYxKpTjE0/VPkNXxWCLOnBob4XKkEkPk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bNmua8/hyYvvm8mLp/4h4ZeAR9V9DINtCIXOKLjK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - cashapp/turbine: A testing library for kotlinx.coroutines Flow&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A testing library for kotlinx.coroutines Flow. Contribute to cashapp/turbine development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flow를 편리하게 테스트할 수 있는 서드파티 라이브러리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 공식 문서에서도 소개하고 있을 정도로, 많은 사람들이 사용하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1742992564596&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;flowOf(&quot;one&quot;, &quot;two&quot;).test {
    assertEquals(&quot;one&quot;, awaitItem())
    assertEquals (&quot;two&quot;, awaitItem())  
    awaitComplete()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Turbine 사용법을 가장 간략하게 표현한 코드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 &lt;i&gt;test()&lt;/i&gt; 함수가 무엇인지 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;test 함수&lt;/span&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;public suspend fun &amp;lt;T&amp;gt; Flow&amp;lt;T&amp;gt;.test(
  timeout: Duration? = null,
  name: String? = null,
  validate: suspend TurbineTestContext&amp;lt;T&amp;gt;.() -&amp;gt; Unit,
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flow의 확장 함수 형태인 &lt;i&gt;test()&lt;/i&gt;는 Flow의 값을 편리하게 꺼내오고, 어설션할 수 있는 스코프를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매개변수 &lt;i&gt;validate&lt;/i&gt; 람다의 리시버 TurbineTestContext를 통해&lt;i&gt; awaitItem()&lt;/i&gt;을 호출할 수 있는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;awaitItem, awaitComplete, awaitError 함수&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1742993954393&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface ReceiveTurbine&amp;lt;T&amp;gt; {
    suspend fun awaitItem(): T
    
    suspend fun awaitComplete()

    suspend fun awaitError(): Throwable
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TurbineTestContext는 ReceiveTurbine를 구현하고 있기 때문에, 위와 같은 ReceiveTurbine 함수들을 호출할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 세개의 함수는 &lt;i&gt;test()&lt;/i&gt;의 &lt;i&gt;validate&lt;/i&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;Turbine에서는 Flow가 값을 방출했을 때, Flow가 완료되었을 때, 에러가 발생했을 때를 모두 하나의 Event로 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 위 세개의 함수를 통해 기대하는 Event가 발생할 때까지 기다리다가, Event가 발생했다면 Event를 반환하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 timeout 동안 Event가 발생하지 않았다면, 예외를 던진다.&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;span style=&quot;color: #ef5369;&quot;&gt;awaitXXX 함수의 timeout&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1742994537347&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;app.cash.turbine.TurbineAssertionError: No value produced in 3s&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;test 함수의 timeout의 기본값은 3초이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Flow의 &lt;i&gt;test&lt;/i&gt; 블록 내에서 &lt;i&gt;awaitItem()&lt;/i&gt;를 호출했는데, 3초 동안 아무런 값이 방출되지 않는다면 위와 같은 오류가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1742994626509&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;flowOf(&quot;one&quot;, &quot;two&quot;).test(timeout = Duration.parse(&quot;PT10S&quot;)) {
    assertEquals(&quot;one&quot;, awaitItem())
    assertEquals (&quot;two&quot;, awaitItem())  
    awaitComplete()
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;timeout&lt;/i&gt; 매개변수에 Duration을 지정하여 timeout을 커스텀할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Turbine의 기본적인 함수들을 알아봤으니, Flow를 테스트하는 방법에 대해 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;Flow 테스트하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;Flow 단일 값 테스트 - &lt;span style=&quot;color: #ef5369;&quot;&gt;Turbine 사용 X&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1743228515898&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
fun flowTest1() = runTest {
    // given
    val repository = FakeRepository()
    val useCase = UseCase(repository)

    // when
    val actual = useCase()

    // then
    assertThat(actual.first()).isTrue
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Turbine 없이 Flow 값 하나만을 테스트하려는 경우다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 &lt;i&gt;first()&lt;/i&gt;를 사용해 Flow의 가장 첫 번째 항목을 가져올 수 있다.&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;span style=&quot;color: #ef5369;&quot;&gt;Flow 단일 값 테스트 -&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;Turbine 사용 O&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1743228531762&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
fun flowTest1WithTurbine() = runTest {
    // given
    val repository = FakeRepository()
    val useCase = UseCase(repository)

    // when
    val actual = useCase()

    // then
    actual.test {
        assertThat(awaitItem()).isTrue
        awaitComplete()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Turbine를 사용하여 이전 코드를 테스트한 코드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 테스트인 경우에는 Turbine를 사용했을 때 이점이 두드러지지는 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;awaitItem()&lt;/i&gt;으로 값을 받아오고, &lt;i&gt;awaitComplete()&lt;/i&gt;로 Flow가 끝났다는 이벤트를 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743220044857&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// 두 번째 값을 가져옴
flow.drop(1).first()

// 처음 5개의 값을 가져옴
flow.take(5).toList()

// 값이 하나만 방출된 후 Flow가 닫혔는지 확인
flow.single()

// 연산이 무한하지 않은 Flow에서 값의 개수
flow.count()
flow.count { it == 3 }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;first()&lt;/i&gt;가 아니어도, Flow의 여러 연산들을 통해 Flow의 값을 가져올 수 있다.&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;span style=&quot;color: #ef5369;&quot;&gt;순차적으로 &lt;span style=&quot;text-align: start;&quot;&gt;Flow &lt;/span&gt;여러 값 테스트 - &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;Turbine 사용 X&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;toList()&lt;/i&gt;를 사용해 Flow 값을 한 번에 가져오는 경우, 전체 값을 가져올 때까지 함수가 중단된다는 단점이 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;toList()&lt;/i&gt;를 사용하지 않고도, 순차적으로 Flow에 값을 방출하고 Flow의 값을 확인할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743220070411&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Test
fun flowTest2() = runTest {
    // given
    val dataSource = FakeDataSource()
    val repository = Repository(dataSource)

    val values = mutableListOf&amp;lt;Int&amp;gt;()
    backgroundScope.launch {
        repository.flow().toList(values)
    }

    // when &amp;amp; then
    dataSource.emit(1)
    assertEquals(10, values[0])

    dataSource.emit(2)
    dataSource.emit(3)
    assertEquals(30, values[2])

    assertEquals(3, values.size)
}&lt;/code&gt;&lt;/pre&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;Turbine를 사용하지 않는다면, &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;위 코드처럼 &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;List에 Flow값을 하나씩 저장시키고, List 값을 확인해야 한다.&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: #666666;&quot;&gt;*&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;테스트의 무한 루프를 방지하기 위해 &lt;span style=&quot;color: #666666; text-align: start;&quot;&gt;backgroundScope를 사용했다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;기본적으로 runTest는 자식 코루틴이 끝날 때까지 기다린다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;Repository의 Flow는 완료되지 않기 때문에, &lt;span style=&quot;color: #666666; text-align: start;&quot;&gt;backgroundScope를 사용하지 않으면&amp;nbsp;&lt;/span&gt;&lt;i&gt;toList()&lt;/i&gt;는 영원히 끝나지 않고 테스트도 무한루프를 돌 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666; text-align: start;&quot;&gt;반면 &lt;/span&gt;&lt;span style=&quot;color: #666666; text-align: start;&quot;&gt;backgroundScope로 생성된 코루틴은 테스트가 끝나면 자동으로 취소된다.&lt;/span&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;/span&gt;&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;span style=&quot;color: #ef5369;&quot;&gt;순차적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Flow&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;여러 값 테스트 -&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;Turbine 사용 O&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1743220081006&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Test
fun flowTest2WithTurbine() = runTest {
    // given
    val dataSource = FakeDataSource()
    val repository = Repository(dataSource)
    
    // when &amp;amp; then
    repository.scores().test {
        dataSource.emit(1)
        assertEquals(10, awaitItem())

        dataSource.emit(2)
        awaitItem()

        dataSource.emit(3)
        assertEquals(30, awaitItem())
        awaitComplete()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Turbine를 사용한 코드는 위와 같다.&lt;br /&gt;Flow를 collect하거나 최종 연산자를 호출하지 않아도, &lt;i&gt;test()&lt;/i&gt;를 호출하면&amp;nbsp;내부적으로 Flow를 collect한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;List 없이 &lt;i&gt;awaitItem()&lt;/i&gt;를 호출하여 Flow의 값을 순차적으로 꺼내온다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;List를 생성하고 List 인덱스에 접근하는 코드들이 사라지게 되어 가독성이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;StateFlow 테스트하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;StateFlow 테스트 - Turbine 사용 X&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1743228546007&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
fun stateFlowTest() = runTest {
    // given
    val fakeRepository = FakeRepository()
    val viewModel = ViewModel(fakeRepository)

    // when &amp;amp; then
    assertEquals(0, viewModel.score.value)

    fakeRepository.emit(1)
    assertEquals(1, viewModel.score.value)

    fakeRepository.emit(2)
    fakeRepository.emit(3)
    assertEquals(3, viewModel.score.value)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StateFlow는 Flow와 달리 Hot Flow이기 때문에 누군가 구독하지 않아도 값을 방출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 항상 하나의 값을 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Flow의 &lt;i&gt;first()&lt;/i&gt;, &lt;i&gt;drop()&lt;/i&gt; 등을 사용할 수 있지만, value를 통해 현재의 값을 가져올 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743227694241&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
fun stateFlowTest() = runTest {
    // given
    val fakeRepository = FakeRepository()
    val viewModel = ViewModel(fakeRepository)

    backgroundScope.launch {
        viewModel.score.collect {}
    }

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StateFlow는 Hot Flow지만, &lt;i&gt;Flow.stateIn()&lt;/i&gt;을 통해 Flow를 StateFlow로 변환한 경우에는 꼭 collect하는 코드가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;collect하지 않는다면, Flow가 활성화되지 않아 StateFlow 값도 업데이트되지 않는다.&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;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;StateFlow 테스트 - Turbine 사용 O&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1743222212731&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun stateFlowTestWithTurbine() = runTest {
    val fakeRepository = FakeRepository()
    val viewModel = ViewModel(fakeRepository)

    viewModel.score.test {
        assertEquals(0, awaitItem())

        fakeRepository.emit(1)
        assertEquals(1, awaitItem())

        fakeRepository.emit(2)
        fakeRepository.emit(3)
        assertEquals(3, awaitItem())
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 Turbine를 사용하는 경우, &lt;i&gt;stateIn()&lt;/i&gt;을 사용하고 있더라도 collect하지 않아도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt; test()&lt;/i&gt; 내부적으로 StateFlow를 collect해주고 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;SharedFlow 테스트하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;SharedFlow 테스트 - Turbine 사용 X&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1743222647886&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
fun sharedFlowTest() = runTest {
    // given
    val fakeRepository = FakeRepository()
    val viewModel = ViewModel(fakeRepository)

    // when
    launch {
        viewModel.initialize()
    }

    // then
    assertThat(viewModel.event.first()).isEqualTo(3)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SharedFlow는 StateFlow와 동일하게 Hot Flow이지만, 값을 가지고 있지 않는다.&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;이 경우 SharedFlow의 값을 계속 기다리면서 테스트가 무한 루프를 돌 것이다.&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;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;SharedFlow 테스트 - Turbine 사용 O&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1743222779293&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
fun sharedFlowTestWithTurbine() = runTest {
    // given
    val fakeRepository = FakeRepository()
    val viewModel = ViewModel(fakeRepository)

    // when
    launch {
        viewModel.initialize()
    }

    // then
    viewModel.event.test {
        assertThat(awaitItem()).isEqualTo(3)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SharedFlow의 경우 Turbine를 사용했을 때 큰 차이점은 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 테스트들과 동일하게 &lt;i&gt;test()&lt;/i&gt;와 &lt;i&gt;awaitItem()&lt;/i&gt;을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743224272889&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
fun sharedFlowTestWithTurbine() = runTest {
    // given
    val fakeRepository = FakeRepository()
    val viewModel = ViewModel(fakeRepository)

    // when, then
    viewModel.event.test {
    	viewModel.initialize()
        assertThat(awaitItem()).isEqualTo(3)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 &lt;i&gt;test()&lt;/i&gt; 블록 내부에서 when문을 작성해 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 SharedFlow의 값이 방출된 이후에 어설션을 하더라도, 테스트가 성공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Turbine가 내부적으로 어떻게 동작하길래 &lt;i&gt;test()&lt;/i&gt;와 &lt;i&gt;awaitItem()&lt;/i&gt;만으로 테스트를 성공할 수 있는지는 아직 모르겠다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Flow를 깊이 이해하지 않아도 테스트할 수 있다는 것이&lt;span&gt;&amp;nbsp;&lt;/span&gt;Turbine의 큰 장점일 수도 있겠다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반면 Turbine 사용 없이 테스트를 작성할 때는, Flow의 특성을 아주 잘 이해해야 올바르게 테스트할 수 있다는 느낌을 받았다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트의 테스트 코드 퀄리티를 위해 Turbine를 알아봤지만, 아직 실제 코드에 적용하지는 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트가 MVP 단계인 만큼, 복잡한 테스트가 필요하지 않을 거라 판단했기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Turbine는 Flow를 편리하게 테스트할 수 있도록 도와주지만,&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;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Turbine의 &lt;i&gt;test&lt;/i&gt; 함수 블록 내에서 사용할 수 있는 유용한 함수들을 정리하고 이 글을 마무리 하려고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 Flow의 완료 시점을 알지 못할 때, &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;&lt;i&gt;awaitComplete()&lt;/i&gt; 대신 &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;&lt;i&gt;cancelAndIgnoreRemainingEvents()&lt;/i&gt;를 사용한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;&lt;i&gt;cancelAndConsumeRemainingEvents()&lt;/i&gt;: &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;Flow를 &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;취소하고&lt;/span&gt; 남은 이벤트를 모두 소비한다. (소비한 결과를 List 형태로 반환)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;i&gt;cancelAndIgnoreRemainingEvents()&lt;/i&gt;: Flow를 취소하고 남은 이벤트를 모두 무시한다.&lt;/li&gt;
&lt;li&gt;&lt;i&gt;expectMostRecentItem()&lt;/i&gt;: 가장 최근의 Flow 값을 가져온다.&lt;/li&gt;
&lt;li&gt;&lt;i&gt;cancel()&lt;/i&gt;: Flow를 취소한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;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;a href=&quot;https://jaeryo2357.tistory.com/115&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://jaeryo2357.tistory.com/115&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/kotlin/flow/test?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.android.com/kotlin/flow/test?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/@vh.dev/testing-stateflow-and-sharedflow-with-ease-a-guide-to-seamless-testing-using-the-turbine-library-e0fb4ed2de66&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://medium.com/@vh.dev/testing-stateflow-and-sharedflow-with-ease-a-guide-to-seamless-testing-using-the-turbine-library-e0fb4ed2de66&lt;/a&gt;&lt;/p&gt;</description>
      <category>kotlin/coroutines</category>
      <author>hrniin</author>
      <guid isPermaLink="true">https://thdbs523.tistory.com/451</guid>
      <comments>https://thdbs523.tistory.com/451#entry451comment</comments>
      <pubDate>Sat, 29 Mar 2025 15:07:50 +0900</pubDate>
    </item>
    <item>
      <title>Compose Side Effect 이해하기! (2)</title>
      <link>https://thdbs523.tistory.com/450</link>
      <description>&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-text=&quot;produceState&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1742648861711&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;Compose Side Effect 이해하기! (1)&quot; data-og-description=&quot;Side Effect란?컴포즈에서 Side Effect는 컴포저블 함수의 밖에서 발생하는 앱 상태에 대한 변경사항이다.예를 들어 사용자가 버튼을 클릭하면 다른 화면이 열리거나, 앱이 인터넷이 연결되어 있지 않&quot; data-og-host=&quot;thdbs523.tistory.com&quot; data-og-source-url=&quot;https://thdbs523.tistory.com/449&quot; data-og-url=&quot;https://thdbs523.tistory.com/449&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/pH5sD/hyYrQkMWD1/vIeDEUScLQR6WQMUWTq7w1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/pH5sD/hyYrQkMWD1/vIeDEUScLQR6WQMUWTq7w1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://thdbs523.tistory.com/449&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://thdbs523.tistory.com/449&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/pH5sD/hyYrQkMWD1/vIeDEUScLQR6WQMUWTq7w1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/pH5sD/hyYrQkMWD1/vIeDEUScLQR6WQMUWTq7w1/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;Compose Side Effect 이해하기! (1)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Side Effect란?컴포즈에서 Side Effect는 컴포저블 함수의 밖에서 발생하는 앱 상태에 대한 변경사항이다.예를 들어 사용자가 버튼을 클릭하면 다른 화면이 열리거나, 앱이 인터넷이 연결되어 있지 않&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;thdbs523.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-text=&quot;produceState&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;저번 포스팅에서 Side Effect이 어떤 건지 알아보고,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-text=&quot;produceState&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt; &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;Side Effect와 관련된&lt;/span&gt;&amp;nbsp;컴포저블 함수 LaunchedEffect,&amp;nbsp;DisposableEffect, SideEffect에 대해 알아봤다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-text=&quot;produceState&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;rememberCoroutineScope&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;rememberUpdatedState&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;derivedStateOf&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;produceState&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;collectAsStateWithLifecycle&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;snapshotFlow&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-text=&quot;produceState&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이번엔 State를 더 효율적으로 관리하기 위한 여섯 개의 API를 알아보려고 한다!&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;rememberCoroutineScope(kotlin.Function0)&quot; style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-text=&quot;rememberCoroutineScope&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;rememberCoroutineScope&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;pre id=&quot;code_1742804374327&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
inline fun rememberCoroutineScope(
    crossinline getContext: @DisallowComposableCalls () -&amp;gt; CoroutineContext = {
        EmptyCoroutineContext
    }
): CoroutineScope&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;rememberCoroutineScope는 &lt;u&gt;함수를 호출한 컴포지션의&lt;/u&gt;&lt;/span&gt;&lt;u&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/u&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&lt;u&gt;CoroutineScope를 반환&lt;/u&gt;하는 컴포저블 함수다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;따라서 컴포지션을 종료하면 코루틴 스코프도 취소된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;컴포저블 외부에서 컴포지션의 생명주기와 동일한 코루틴을 실행하려는 경우 주로 사용한다.&lt;/span&gt;&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;h4 style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1a5490; text-align: start;&quot;&gt;rememberCoroutineScope 사용 예시&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;일부 Compose API는 suspend 함수다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;만약 이 Compose API를 컴포저블 함수 바깥에서 호출해야 한다면, LaunchedEffect를 사용할 수도 없을 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;가장 대표적인 예시로 Snackbar가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;val coroutineScope = rememberCoroutineScope()
val snackBarHostState = remember { SnackbarHostState() }

Scaffold(
    snackbarHost = { SnackbarHost(snackBarHostState) },
) { innerPadding -&amp;gt;
    Button(
        onClick = {
            coroutineScope.launch {
                snackBarHostState.showSnackbar(&quot;Button Click&quot;)
            }
        },
    ) {
        Text(text = &quot;Snackbar Button&quot;)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;SnackBarHostState의 showSnackbar 함수는 suspend 함수다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;컴포저블이 아닌 버튼의 onClick 블록에서 실행되기 때문에, LaunchedEffect를 사용할 수 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이때 컴포저블의 생명주기와 동일한 스코프를 통해 코루틴을 생성하면&amp;nbsp;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;showSnackbar를 호출할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;컴포저블이 종료되면 해당 코루틴도 종료되기 때문에 안전하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 id=&quot;launchedeffect-vs-remembercoroutinescope&quot; style=&quot;background-color: #ffffff; color: #4e5256; text-align: start;&quot; data-ke-size=&quot;size20&quot; data-text=&quot;LaunchedEffect vs rememberCoroutineScope&quot;&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;LaunchedEffect vs rememberCoroutineScope&lt;/span&gt;&lt;/h4&gt;
&lt;div id=&quot;steps&quot; style=&quot;color: #4e5256; text-align: start;&quot;&gt;
&lt;div style=&quot;background-color: #ffffff;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;LaunchedEffect와 &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;rememberCoroutineScope 모두 코루틴을 생성하고 suspend 함수를 호출하기 위해 사용한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;그럼 &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;LaunchedEffect를 대신해서 &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;rememberCoroutineScope를 사용할 수 있을까?&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&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 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;LaunchedEffect는 컴포저블 함수이므로 컴포저블 함수 내에서만 호출할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;u&gt;컴포지션과 연관된 Side Effect를 실행하고자 할 때 사용&lt;/u&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;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;rememberCoroutineScope는 컴포저블과 동일한 코루틴 스코프를 생성하기 때문에, 컴포저블 외부에서 중단 함수를 호출할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;u&gt;코루틴 스코프로 생성한 코루틴은 컴포지션 여부와 무관하게 실행&lt;/u&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;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;만약 컴포저블 함수에서 LauncedEffect 대신 scope.launch &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;를 사용했다면, &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;리컴포지션이 발생하여 컴포저블 함수가 호출될 때마다 코루틴이 계속 실행될 것이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt; &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;따라서 리소스를 낭비하게 되며, &lt;/span&gt;LaunchedEffect를 대신해서 &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;rememberCoroutineScope를 사용할 수 없다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&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;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-size: 1.44em; letter-spacing: -1px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;rememberUpdatedState&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun&amp;nbsp;&amp;lt;T&amp;nbsp;:&amp;nbsp;Any?&amp;gt; rememberUpdatedState(newValue:&amp;nbsp;T):&amp;nbsp;State&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;rememberUpdatedState는 &lt;u&gt;항상 최신 값으로 &lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;State를&amp;nbsp;&lt;/span&gt;업데이트&lt;/u&gt;하는 컴포저블 함수다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;pre id=&quot;code_1742652452147&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun &amp;lt;T&amp;gt; rememberUpdatedState(newValue: T): State&amp;lt;T&amp;gt; = remember {
    mutableStateOf(newValue)
}.apply { value = newValue }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;내부 구현도 매우 간단하다.&lt;/span&gt;&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: #333333;&quot;&gt;인자로 들어온 newValue로 MutableState를 생성하고, 인자로 들어온 값이 변경될 때마다 value에 새로운 값을 대입한다.&lt;/span&gt;&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: #333333;&quot;&gt;즉, 항상 최신의 값이 State에 반영된다.&lt;/span&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;size18&quot;&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;rememberUpdatedState 사용 예시&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;먼저 rememberUpdatedState를 사용하지 않았을 때의 예시를 보자.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1742655573834&quot; class=&quot;livescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun LandingScreen(onTimeout: () -&amp;gt; Unit, modifier: Modifier = Modifier) {
&amp;nbsp; &amp;nbsp; Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; LaunchedEffect(onTimeout) {
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; delay(SplashWaitTime)
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; onTimeout()
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }

&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null)
&amp;nbsp; &amp;nbsp; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;위 예시에서는 필요한 데이터를 모두 로드한 후, onTimeout을 실행시켜 랜딩 화면을 종료시킨다. &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;(데이터 로드를 delay로 대체했다)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; 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;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;데이터 로드나 delay와 같은 Side Effect가 실행되는 동안 onTimeout이 변경되면, Side Effect가 끝난 후 최신 상태의 onTimeout이 호출된다는 보장이 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;이를 해결하기 위해 &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;rememberUpdatedState를 사용할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;pre id=&quot;code_1742652452148&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun LandingScreen(onTimeout: () -&amp;gt; Unit, modifier: Modifier = Modifier) {
&amp;nbsp; &amp;nbsp; Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; val currentOnTimeout by rememberUpdatedState(onTimeout)

&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; LaunchedEffect(Unit) {
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; delay(SplashWaitTime)
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; currentOnTimeout()
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }

&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null)
&amp;nbsp; &amp;nbsp; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;p style=&quot;background-color: #ffffff; color: #4e5256; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;rememberUpdatedState를 사용하여 onTimeout을 저장한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #4e5256; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;만약 &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;Side Effect가 실행되는 동안&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;onTimeout이 변경되어도, &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;rememberUpdatedState 내부에서 State를 업데이트한다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #4e5256; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #4e5256; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 &lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;onTimeout이&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;rememberUpdatedState이 아닌 일반 State였다면 어떨까?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #4e5256; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;변경사항이 발생하더라도, value에 직접 최신 값을 대입해준 것이 아니기 때문에 이전 예제와 동일한 문제가 발생할 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #4e5256; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;반면에 &lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;rememberUpdatedState는 &lt;u&gt;값이 변경될 때마다 value에 값을 대입하기 때문에 항상 최신 값&lt;/u&gt;이라는 것을 보장한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #4e5256; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #4e5256; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;rememberUpdatedState는&amp;nbsp;&lt;/span&gt;람다가 Side Effect 동안 계속 유지되거나, 컴포지션으로 계산되는 값을 참조하는 경우에 사용한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #4e5256; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;LaunchedEffect에서 람다를 호출하는 경우 일반적으로 &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;rememberUpdatedState를 사용한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&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 id=&quot;derivedStateOf(kotlin.Function0)&quot; style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-text=&quot;derivedStateOf&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;derivedStateOf&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1742802841180&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@StateFactoryMarker
fun &amp;lt;T : Any?&amp;gt; derivedStateOf(calculation: () -&amp;gt; T): State&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;derivedStateOf는 다른 State로 새로운 State를 계산하고 싶을 때 사용한다.&lt;/span&gt;&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: #333333;&quot;&gt;derivedStateOf의 calculation&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;블록은 블록 내부에서 사용된 State가 변경될 때마다 실행된다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;하지만 컴포저블 함수는 &lt;u&gt;기존 값과 계산 결과값이 다를 때만 리컴포지션&lt;/u&gt;된다.&lt;/span&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;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1a5490; text-align: start;&quot;&gt;derivedStateOf 사용 예시&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1742802841181&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val showButton = listState.firstVisibleItemIndex &amp;gt; 0&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;firstVisibleItemIndex는 현재 화면에서 가장 첫 번째로 보이는 리스트 아이템의 인덱스를 나타낸다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;firstVisibleItemIndex는 스크롤할 때마다 변경되기 때문에, showButton도 그만큼 자주 변경되고, 자주 리컴포지션된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1742802841182&quot; class=&quot;coq&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val showButton by remember {
    derivedStateOf {
        listState.firstVisibleItemIndex &amp;gt; 0
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;하지만&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;derivedStateOf를 사용하면 true나 false로 값이 변경될 때만 리컴포지션된다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, firstVisibleItemIndex가 변경되어도, 실제 showButton의 값이 변경되었을 때만 리컴포지션되어 최적화할 수 있다.&lt;/p&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 id=&quot;produceState(kotlin.Any,kotlin.coroutines.SuspendFunction1)&quot; style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-text=&quot;produceState&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;produceState&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun&amp;nbsp;&amp;lt;T&amp;nbsp;:&amp;nbsp;Any?&amp;gt; produceState(
    initialValue:&amp;nbsp;T,&amp;nbsp;
    producer:&amp;nbsp;suspend&amp;nbsp;ProduceStateScope&amp;lt;T&amp;gt;.() -&amp;gt; Unit
):&amp;nbsp;State&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;produceState를 사용하면 &lt;u&gt;Compose가 아닌 상태를 Compose State로 변환&lt;/u&gt;할 수 있다.&lt;/span&gt;&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: #333333;&quot;&gt;예를 들어&amp;nbsp;Flow,&amp;nbsp;LiveData,&amp;nbsp;RxJava와 같은 Observable 객체를 State로 변환한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;하나의 State를 반환하기 때문에, 동일한 값을 설정하면 리컴포지션이 발생하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;initialValue를 통해 반환된 State의 초기값을 지정한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;producer 블록 내에서는 State의 value를 사용해 State에 값을 지정한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1742651667450&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val currentPerson by
    produceState&amp;lt;Person?&amp;gt;(null, viewModel) {
        val disposable = viewModel.registerPersonObserver { person -&amp;gt; value = person }
        awaitDispose { disposable.dispose() }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;코루틴을 사용하지만, 중단되지 않는 경우에도 produceState를 사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이 때 produceState 블록의 코루틴이 종료되는 경우 (&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;컴포지션 종료, 데이터 변경, 오류 발생)&amp;nbsp;&lt;/span&gt; awaitDispose이 실행된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;awaitDispose를 사용하여 자원을 해제한다.&lt;/span&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;pre class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;@Composable
fun &amp;lt;T&amp;gt; produceState(
    initialValue: T,
    producer: suspend ProduceStateScope&amp;lt;T&amp;gt;.() -&amp;gt; Unit
): State&amp;lt;T&amp;gt; {
    val result = remember { mutableStateOf(initialValue) }
    LaunchedEffect(Unit) {
        ProduceStateScopeImpl(result, coroutineContext).producer()
    }
    return result
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;내부 구현을 보면 MutableState와 LaunchedEffect를 사용하고 있는 것을 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;initialValue를 가진 mutableState를 생성하고, LauncedEffect 내에서 producer를 호출한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;즉,&amp;nbsp;&lt;/span&gt;producer 블록을 실행하는 코루틴은 컴포지션이 시작하면 실행되고, 컴포지션이 종료되면 취소된다.&lt;/span&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;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;produceState에 key 지정하기&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1742649904378&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun &amp;lt;T&amp;gt; produceState(
    initialValue: T,
    vararg keys: Any?,
    producer: suspend ProduceStateScope&amp;lt;T&amp;gt;.() -&amp;gt; Unit
): State&amp;lt;T&amp;gt; {
    val result = remember { mutableStateOf(initialValue) }
    LaunchedEffect(keys = keys) {
        ProduceStateScopeImpl(result, coroutineContext).producer()
    }
    return result
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;LaunchedEffect와 마찬가지로&amp;nbsp;produceState도 여러 key를 지정할 수 있다.&lt;/span&gt;&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: #333333;&quot;&gt;key들 중 한 값이 바뀔 때마다 producer 블록이 호출된다.&lt;/span&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;h4 style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;produceState 사용 예시&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1742054803397&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun DetailsScreen(
    onErrorLoading: () -&amp;gt; Unit,
    modifier: Modifier = Modifier,
    viewModel: DetailsViewModel = viewModel()
) {
    val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) {
        val cityDetailsResult = viewModel.cityDetails
        value = if (cityDetailsResult is Result.Success&amp;lt;ExploreModel&amp;gt;) {
            DetailsUiState(cityDetailsResult.data)
        } else {
            DetailsUiState(throwError = true)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;위 예시에서는 ViewModel의 값을 통해 State&amp;lt;DetailsUiState&amp;gt;로 변환한다.&lt;/span&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;span style=&quot;color: #333333;&quot;&gt;일반적으로 ViewModel에서는 Flow나 LiveData로 UiState를 가지고 있다. 또는&amp;nbsp;State를 직접 가지고 있는 경우도 있다.&lt;/span&gt;&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: #333333;&quot;&gt;보통 결과값에 따라 UiState를 달리하는 로직은 ViewModel에 존재하고, 컴포저블 함수에서는 이를 State로 변환하기만 한다.&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;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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; color: #333333;&quot;&gt;따라서 매핑 로직을 작성하기에 적합한 produceState 함수를 사용할 일이 없다.&lt;/span&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;span style=&quot;color: #333333;&quot;&gt;만약 ViewModel에서 Observable 객체가 아닌 값을 가지고 있거나, 컴포저블 함수에서 직접 데이터를 로드하여 UiState를 매핑해야 하는 경우 사용한다.&lt;/span&gt;&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: #333333;&quot;&gt;Flow나 LiveData를 State로 변환하는 컴포저블 함수도 내부적으로 produceState를 사용하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;collectAsStateWithLifecycle&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1742054803398&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun &amp;lt;T : Any?&amp;gt; Flow&amp;lt;T&amp;gt;.collectAsStateWithLifecycle(
    initialValue: T,
    lifecycle: Lifecycle,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
    context: CoroutineContext = EmptyCoroutineContext
): State&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;collectAsStateWithLifecycle를 사용하면 &lt;/span&gt;&lt;u&gt;Flow를 State로 변환&lt;/u&gt;할 수 있다.&lt;/span&gt;&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: #333333;&quot;&gt;Flow 값이 수집될 때마다 State가 변경되어 리컴포지션 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1742054803398&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun &amp;lt;T&amp;gt; Flow&amp;lt;T&amp;gt;.collectAsStateWithLifecycle(
    initialValue: T,
    lifecycle: Lifecycle,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
    context: CoroutineContext = EmptyCoroutineContext
): State&amp;lt;T&amp;gt; {
    return produceState(initialValue, this, lifecycle, minActiveState, context) {
        lifecycle.repeatOnLifecycle(minActiveState) {
            if (context == EmptyCoroutineContext) {
                this@collectAsStateWithLifecycle.collect { this@produceState.value = it }
            } else withContext(context) {
                this@collectAsStateWithLifecycle.collect { this@produceState.value = it }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;내부적으로는 위에서 설명했던 produceState를 사용하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;lifecycle.repeatOnLifecycle은 Android View와 MVVM을 사용하면서 많이 작성했을 코드다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;즉,&amp;nbsp;repeatOnLifecycle을 통해 생명주기에 따라 Flow를 옵저빙해주고, Flow가 collect될 때마다 새로운 State의 value에 넣어주는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;collectAsState와의 차이&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;1212&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdG4SH/btsMTYaqqKJ/tBsKeDHSvNXPCSQGW6z9sk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdG4SH/btsMTYaqqKJ/tBsKeDHSvNXPCSQGW6z9sk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdG4SH/btsMTYaqqKJ/tBsKeDHSvNXPCSQGW6z9sk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdG4SH%2FbtsMTYaqqKJ%2FtBsKeDHSvNXPCSQGW6z9sk%2Fimg.webp&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;530&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;1212&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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: #333333;&quot;&gt;collectAsState는 &lt;span style=&quot;text-align: start;&quot;&gt;collectAsStateWithLifecycle와 동일하게, &lt;/span&gt;State로 &lt;span style=&quot;text-align: start;&quot;&gt;Flow의&amp;nbsp;&lt;/span&gt;값을 수집하는 함수다.&lt;/span&gt;&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: #333333;&quot;&gt;그렇다면 둘의 차이는 뭘까?&lt;/span&gt;&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: #333333;&quot;&gt;함수명에서 알 수 있듯이, &lt;span style=&quot;text-align: start;&quot;&gt;collectAsStateWithLifecycle는 생명 주기&lt;/span&gt;를 인식한다는 것이다.&lt;/span&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;collectAsStateWithLifecycle의 매개변수 minActiveState는 기본적으로 Lifecycle.State.STARTED이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;현재 화면의 &lt;span style=&quot;text-align: start;&quot;&gt;생명 주기가&lt;/span&gt;&amp;nbsp;최소 STARTED일 때 값을 수집하고, STOP 이후에는 값을 수집하지 않는다.&lt;/span&gt;&lt;/u&gt;&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: #333333;&quot;&gt;즉, 앱이 백그라운드에 있을 때는 값을 수집하지 않기 때문에 불필요한 리소스 낭비를 방지할 수 있다.&lt;/span&gt;&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: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;minActiveState를 사용해서 &lt;/span&gt;값을 수집하거나 중지할 최소 상태를 지정할 수도 있다.&lt;/span&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;pre id=&quot;code_1742801477219&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun &amp;lt;T : R, R&amp;gt; Flow&amp;lt;T&amp;gt;.collectAsState(
    initial: R,
    context: CoroutineContext = EmptyCoroutineContext
): State&amp;lt;R&amp;gt; = produceState(initial, this, context) {
    if (context == EmptyCoroutineContext) {
        collect { value = it }
    } else withContext(context) {
        collect { value = it }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;반면에 collectAsState는 앱이 백그라운드에 있을 때도 계속 값을 수집한다.&lt;/span&gt;&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: #333333;&quot;&gt;그래서 생명 주기를 관리해야 하는 안드로이드가 아닌 다른 플랫폼에서 Compose를 개발할 때 collectAsState를&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;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;snapshotFlow&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun &amp;lt;T&amp;gt; snapshotFlow(block: () -&amp;gt; T): Flow&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;snapshotFlow를 사용하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;State를 Flow로 변환&lt;/u&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블록 내의 State가 변경되면 블록이 실행되고, Flow에 값을 내보낸다,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주의할 점은 새로운 값이 이전에 내보낸 값과 다른 경우에만 Flow에 값을 내보낸다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연속으로 중복된 값을 내보내지 않는다는 점에서는 Flow의 &lt;span style=&quot;color: #242424; text-align: left;&quot;&gt;distinctUntilChanged와 비슷하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;snapshotFlow 사용 예시&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1742804425903&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;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;&lt;span style=&quot;color: #333333;&quot;&gt;위 코드에서 &lt;span style=&quot;text-align: left;&quot;&gt;listState.firstVisibleItemIndex는 Flow로 변환되어 &lt;span style=&quot;text-align: left;&quot;&gt;firstVisibleItemIndex이 변경될 때마다 값을 내보낸다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flow 이점을 활용하여 새로운 값을 계산하고 싶을 때 주로 사용한다.&lt;/p&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;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;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;a style=&quot;color: #666666;&quot; href=&quot;https://medium.com/androiddevelopers/consuming-flows-safely-in-jetpack-compose-cde014d0d5a3&quot;&gt;https://medium.com/androiddevelopers/consuming-flows-safely-in-jetpack-compose-cde014d0d5a3&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;a style=&quot;color: #666666;&quot; href=&quot;https://developer.android.com/develop/ui/compose/side-effects?hl=ko&quot;&gt;https://developer.android.com/develop/ui/compose/side-effects?hl=ko&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;a style=&quot;color: #666666;&quot; href=&quot;https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects?hl=ko&amp;amp;continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fjetpack-compose-for-android-developers-3%3Fhl%3Dko%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fjetpack-compose-advanced-state-side-effects#0&quot;&gt;https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects?hl=ko&amp;amp;continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fjetpack-compose-for-android-developers-3%3Fhl%3Dko%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fjetpack-compose-advanced-state-side-effects#0&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;a style=&quot;color: #666666;&quot; href=&quot;https://tourspace.tistory.com/412&quot;&gt;https://tourspace.tistory.com/412&lt;/a&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;</description>
      <category>kotlin/compose</category>
      <author>hrniin</author>
      <guid isPermaLink="true">https://thdbs523.tistory.com/450</guid>
      <comments>https://thdbs523.tistory.com/450#entry450comment</comments>
      <pubDate>Mon, 24 Mar 2025 17:18:59 +0900</pubDate>
    </item>
    <item>
      <title>Compose Side Effect 이해하기! (1)</title>
      <link>https://thdbs523.tistory.com/449</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;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Side Effect란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포즈에서 Side Effect는 컴포저블 함수의 밖에서 발생하는 앱 상태에 대한 변경사항이다.&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;기본적으로 컴포저블 함수는 Side Effect에 자유로워야 한다.&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;Side Effect를 이해하고 잘 활용하기 위해, 관련된 컴포저블 함수를 알아보려고 한다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Side Effect를 처리하기 위해 아래와 같은 함수들이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LaunchedEffect&lt;/li&gt;
&lt;li&gt;DisposableEffect&lt;/li&gt;
&lt;li&gt;SideEffect&lt;/li&gt;
&lt;li&gt;rememberCoroutineScope&lt;/li&gt;
&lt;li&gt;rememberUpdatedState&lt;/li&gt;
&lt;li&gt;produceState&lt;/li&gt;
&lt;li&gt;derivedStateOf&lt;/li&gt;
&lt;li&gt;snapshotFlow&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&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 style=&quot;color: #333333; text-align: start;&quot;&gt;LaunchedEffect,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;DisposableEffect,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;SideEffect를 각각 알아보고, 비교해 보려고 한다.&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;LaunchedEffect&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1742051282023&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun LaunchedEffect(
    key1: Any?, 
    block: suspend CoroutineScope.() -&amp;gt; Unit
)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 컴포즈를 개발하며 가장 많이 사용하는 LaunchedEffect이다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot;&gt;LaunchedEffect는&amp;nbsp;&lt;/span&gt;컴포저블 안에서 안전하게 suspend 함수를 호출하기 위해 사용하는 컴포저블 함수다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1a5490; font-size: 1.25em; letter-spacing: -1px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;LaunchedEffect의 key&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1742051942726&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun LaunchedEffect(
    key1: Any?,
    block: suspend CoroutineScope.() -&amp;gt; Unit
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot;&gt;LaunchedEffect의 매개변수로 key를 전달할 수 있다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&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: #3a4954; text-align: left;&quot;&gt;key가 변경되면 LaunchedEffect의 내부 블록이 다시 호출된다.&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_1742052262258&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun LaunchedEffect(
    key1: Any?,
    key2: Any?,
    block: suspend CoroutineScope.() -&amp;gt; Unit
)

@Composable
fun LaunchedEffect(
    key1: Any?,
    key2: Any?,
    key3: Any?,
    block: suspend CoroutineScope.() -&amp;gt; Unit
)

@Composable
fun LaunchedEffect(
    vararg keys: Any?,
    block: suspend CoroutineScope.() -&amp;gt; Unit
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 key 매개변수를 가지는 함수들이 오버로딩되어 있기 때문에, 개수 제한 없이 여러 key를 지정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 key 중 하나라도 변경이 일어나면, LaunchedEffect의 내부 블록이 다시 호출된다.&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;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;상수를 key로 사용하기&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1742052329161&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LaunchedEffect(Unit) {
    viewModel.fetchMeetings()
}&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 style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;컴포저블의 수명 주기 동안 한 번만 실행시켜야 하는 로직인 경우,&lt;span style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot;&gt; Unit, 1, false처럼&amp;nbsp;&lt;/span&gt;변하지 않는 상수를 key로 사용한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 ViewModel에서 데이터를 가져오는 함수를 한 번만 호출하기 위해, LaunchedEffect(Unit) { ... }를 사용할 수 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;configuration change에도 컴포저블은 리컴포지션되기 때문에, LaunchedEffect(Unit) 내부 블록이 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Activity의 onCreate()에서 함수를 호출하는 것과 유사하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;configuration change마다 호출하지 않으려면 이러한 함수 호출을 ViewModel 내부에서 하는 것이 좋다.&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;span style=&quot;color: #1a5490;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;LaunchedEffect의&amp;nbsp;&lt;/span&gt;코루틴 생명주기&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot;&gt;LaunchedEffect는 &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot;&gt;suspend 함수를 호출할 수 있는 함수다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot;&gt;그렇다면 suspend 함수가 호출되는 코루틴의 생명주기는 어떻게 될까?&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;먼저 컴포저블이 컴포지션을 시작하면 현재 컴포지션의 CoroutineContext에서 코루틴이 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포저블이 컴포지션을 종료하면 코루틴이 취소된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 LaunchedEffect의 key가 변경되면, 기존 코루틴이 취소되고 새 코루틴이 실행된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;DisposableEffect(kotlin.Any,kotlin.Function1)&quot; style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-text=&quot;DisposableEffect&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;DisposableEffect&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun&amp;nbsp;DisposableEffect(
    key1:&amp;nbsp;Any?,
    effect:&amp;nbsp;DisposableEffectScope.() -&amp;gt; DisposableEffectResult
):&amp;nbsp;Unit&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;DisposableEffect&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;는 key가 변경되면 내부 블록이 다시 실행된다는 점에서 LaunchedEffect와 유사하다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;그런데 DisposableEffect는 컴포저블이 컴포지션을 종료했을 때 자원을 정리해야 하는 경우 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;DisposableEffect의 onDispose&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1742053566390&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class DisposableEffectScope {
    inline fun onDispose(
        crossinline onDisposeEffect: () -&amp;gt; Unit
    ): DisposableEffectResult = object : DisposableEffectResult {
        override fun dispose() {
            onDisposeEffect()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DisposableEffect의 effect 블록은 DisposableEffectScope를 리시버로 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DisposableEffectScope는 유일한 함수로 onDispose를 가지고 있는데, 이 함수는 컴포저블이 컴포지션을 종료했을 때 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, DisposableEffect의 내부 블록에서 onDispose를 호출하여 컴포저블이 종료됐을 때의 로직을 지정해줄 수 있다.&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;span style=&quot;color: #1a5490;&quot;&gt;onDispose 사용 예시&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1742051300627&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;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;예를 들어 LifecycleEventObserver와 함께 DisposableEffect를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lifecycle이 변경될 때마다 실행할 로직을 작성하고, lifecycleOwner에 observer를 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 onDispose 내부에서 removeObserver를 호출하여 컴포저블의 컴포지션이 종료되었을 때 Observer를 제거한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;onDispose에서 removeObserver를 호출하지 않으면, observer가 lifecycleOwner에 계속 남아있게 되어 메모리 누수나 예상치 못한 버그가 발생할 수 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; 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;color: #006dd7;&quot;&gt;SideEffect&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1742054444835&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun SideEffect(
    effect: () -&amp;gt; Unit
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SideEffect는 컴포저블 내부의 상태를 컴포저블 외부와 공유하기 위해 사용하는 컴포저블 함수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SideEffect의 내부 블록 effect는 리컴포지션이 완료될 때마다 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LaunchedEffect와 다르게 내부 블록이 suspend 함수가 아니고, UI 스레드에서 동기적으로 실행된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3a4954; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1742055399594&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()
    }
    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;Compose의 State와 관련 없는 작업을 수행할 때 주로 사용된다.&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;LaunchedEffect vs DisposableEffect vs SideEffect&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;LaunchedEffect&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 시점에만 실행되어야 하는 경우 사용한다.&lt;/li&gt;
&lt;li&gt;key가 변경될 때마다 실행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;DisposableEffect&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자원 해제가 필요한 경우 사용한다.&lt;/li&gt;
&lt;li&gt;key가 변경될 때마다 실행된다.&lt;/li&gt;
&lt;li&gt;onDispose 함수로 자원을 해제할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;SideEffect&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리컴포지션이 완료될 때마다 실행되어야 하는 경우 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>kotlin/compose</category>
      <author>hrniin</author>
      <guid isPermaLink="true">https://thdbs523.tistory.com/449</guid>
      <comments>https://thdbs523.tistory.com/449#entry449comment</comments>
      <pubDate>Thu, 27 Feb 2025 00:21:28 +0900</pubDate>
    </item>
    <item>
      <title>3.3 채널과 플로우 - 핫 데이터 소스와 콜드 데이터 소스</title>
      <link>https://thdbs523.tistory.com/448</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;우리가 사용하는 대부분의 데이터 소스는 핫 스트림 데이터와 콜드 스트림 데이터 두 가지로 구분할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이 두 가지의 차이를 이해하는 것이 중요하다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 39.1856%; height: 57px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 20.4365%; height: 19px;&quot;&gt;&lt;span&gt;핫&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20.9302%; height: 19px;&quot;&gt;&lt;span&gt;콜드&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 20.4365%; height: 19px;&quot;&gt;&lt;span&gt;컬렉션(List, Set)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20.9302%; height: 19px;&quot;&gt;&lt;span&gt;Sequence, Stream&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 20.4365%; height: 19px;&quot;&gt;&lt;span&gt;Channel&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20.9302%; height: 19px;&quot;&gt;&lt;span&gt;FLow, Rx.Java 스트림&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(List, Set과 같은) 컬렉션은 핫이고, Sequence와 자바의 Stream은 콜드다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Channel은 핫이고, Flow와 (Observable, Single과 같은) Rxlava 스트림은 콜드다.&lt;/p&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;핫 vs 콜드&lt;/h3&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;&lt;b&gt;반면에 콜드 데이터 스트림은 게을러서 요청이 있을 때만 작업을 수행하며, 아무것도 저장하지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739442523535&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    val l = buildList {
        repeat(3) {
            add(&quot;User$it&quot;)
            println(&quot;L: Added User&quot;)
        }
    }

    val l2 = l.map {
        println(&quot;L: Processing&quot;)
        &quot;Processed $it&quot;
    }

    val s = sequence {
        repeat(3) {
            yield(&quot;User$it&quot;)
            println(&quot;S: Added User&quot;)
        }
    }

    val s2 = s.map {
        println(&quot;S: Processing&quot;)
        &quot;Processed $it&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;L: Added User&lt;br /&gt;L: Added User&lt;br /&gt;L: Added User&lt;br /&gt;L: Processing&lt;br /&gt;L: Processing&lt;br /&gt;L: Processing&lt;/blockquote&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;즉, 콜드 데이터 스트림은 (Sequence, Stream, Flow)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;무한할 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최소한의 연산만 수행한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;(중간에 생성되는 값들을 저장하지 않기 때문에) 메모리를 적게 사용한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;중간 연산(map, filter 등)은 이전 시퀀스에 새로운 연산을 추가할 뿐이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;최종 연산이 호출되기 전까지는 아무런 연산을 수행하지 않고, &lt;b&gt;최종 연산에서 모든 작업을 실행한다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739442648422&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun m(i: Int): Int {
    print(&quot;m$i &quot;)
    return i * i
}

fun f(i: Int): Boolean {
    print(&quot;f$i &quot;)
    return i &amp;gt;= 10
}

fun main() {
    listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .map { m(it) }
        .find { f(it) }
        .let { print(it) }
    // m m2 m3 m4 m5 m6 m7 m8 mg m10 f1 f4 f9 f16 16

    println()

    sequenceOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .map { m(it) }
        .find { f(it) }
        .let { print(it) }
    // m f1 m2 f4 m3 19 m4 f16 16
}&lt;/code&gt;&lt;/pre&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;위 예제에서 시퀀스의 find는 map에게 첫 번째 원소를 달라고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;map은 sequenceOf에게 첫 번째 원소를 받고, map을 수행한 뒤 find에 넘긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;find는 위에서 받은 원소(1)가 요구 사항에 부합하는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항을 만족하지 못한다면, find는 적절한 원소를 찾을 때까지 계속해서 질의한다.&lt;/p&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;따라서 리스트는 원소의 처리 순서도 다르고, 컬렉션 처리 과정에서 좀더 많은 메모리를 필요로 하며, 더 많은 연산을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739700194875&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun m(i: Int): Int {
    print(&quot;m$i &quot;)
    return i * i
}

fun main() {
    val l = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .map { m(it) } // ml m2 m3 m4 m5 m6 m7 m8 m9 m10

    println(l) // [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
    println(l.find { it &amp;gt; 10 }) // 16
    println(l.find { it &amp;gt; 10 }) // 16
    println(l.find { it &amp;gt; 10 }) // 16

    val s = sequenceOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .map { m(it) }

    println(s.toList()) // [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
    println(s.find { it &amp;gt; 10 }) // m1 m2 m3 m4 16
    println(s.find { it &amp;gt; 10 }) // m1 m2 m3 m4 16
    println(s.find { it &amp;gt; 10 }) // ml m2 m3 m4 16
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스트는 원소를 가지고 있는 컬렉션이지만, 시퀀스는 원소를 어떻게 계산할지 정의한 것 뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핫 데이터 스트림은&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;항상 사용 가능한 상태다. (각 연산이 최종 연산이 될 수 있다)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;여러 번 사용되었을 때 매번 결과를 다시 계산할 필요가 없다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핫 채널, 콜드 플로우&lt;/h3&gt;
&lt;pre id=&quot;code_1739442869609&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val channel = produce {
    while (true) {
        val x = computeNextValue()
        send(x)
    }
}

val flow = flow {
    while (true) {
        val x = computeNextValue()
        emit(x)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 코루틴으로 돌아와서, 콜드 데이터 스트림인 플로우를 생성해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플로우를 생성하는 가장 일반적인 방법은 produce 함수와 비슷한 flow를 사용하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739442953921&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun CoroutineScope.makeChannel() = produce {
    println(&quot;Channel started&quot;)
    for (i in 1..3) {
        delay(1000)
        send(i)
    }
}

suspend fun main() = coroutineScope {
    val channel = makeChannel()

    delay(1000)
    println(&quot;Calling channel...&quot;)
    for (value in channel) {
        println(value)
    }

    println(&quot;Consuming again...&quot;)
    for (value in channel) {
        println(value)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Channel started&lt;br /&gt;(1초 후)&lt;br /&gt;Calling channel... &lt;br /&gt;1&lt;br /&gt;(1초 후)&lt;br /&gt;2&lt;br /&gt;(1초 후)&lt;br /&gt;3&lt;br /&gt;(1초 후)&lt;br /&gt;Consuming again...&lt;/blockquote&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;수신자가 얼마나 많은지는 신경쓰지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;별도의 코루틴에서 계산을 곧바로 수행한다.&lt;/b&gt; 따라서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;produce는 CoroutineScope 확장 함수의 코루틴 빌더가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채널은 곧바로 시작되지만, 버퍼의 크기가 0인 랑데뷰인 채널이기 때문에 수신자가 원소를 받을 때까지 중단된다.&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;pre id=&quot;code_1739443092009&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun makeFlow() = flow {
    println(&quot;Flow started&quot;)
    for (i in 1..3) {
        delay(1000)
        emit(i)
    }
}

suspend fun main() = coroutineScope {
    val flow = makeFlow()

    delay(1000)
    println(&quot;Calling flow...&quot;)
    flow.collect { value -&amp;gt; println(value) }
    println(&quot;Consuming again...&quot;)
    flow.collect { value -&amp;gt; println(value) }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;(1초 후)&lt;br /&gt;Calling flow...&lt;br /&gt;Flow started&lt;br /&gt;(1초 후)&lt;br /&gt;1&lt;br /&gt;(1초 후)&lt;br /&gt;2&lt;br /&gt;(1초 후)&lt;br /&gt;3&lt;br /&gt;Consuming again...&lt;br /&gt;Flow started&lt;br /&gt;(1초 후)&lt;br /&gt;1&lt;br /&gt;(1초 후)&lt;br /&gt;2&lt;br /&gt;(1초 후)&lt;br /&gt;3&lt;/blockquote&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;flow는 단지 최종 연산이 호출될 때 원소가 어떻게 생성되어야 하는지 정의한 것에 불과하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 flow 빌더는 코루틴 빌더가 아니고, 어떠한 처리도 하지 않는다. &lt;b&gt;CoroutineScope가 필요하지 않다. &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;flow 빌더는 최종 연산의 스코프에서 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;coroutineScope 같은 코루틴 스코프 함수처럼, 중단 함수의 컨티뉴에이션 객체로부터 스코프를 가지고 온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;요약&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;핫 데이터 소스는 열정적이다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;가능한 빨리 원소를 만들고 저장하며, 원소가 소비되는 것과 무관하게 생성한다.&lt;/li&gt;
&lt;li&gt;예를 들어 컬렉션(List, Set), Channel이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;콜드 데이터 소스는 게으르다.&lt;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;일반적으로 원소를 저장하지 않고, 필요할 때 원소를 생성한다.&lt;/li&gt;
&lt;li&gt;최종 연산에서 값을 생성한다. 중간 과정에서는 무엇을 해야 할지만 정의한 것이다.&lt;/li&gt;
&lt;li&gt;연산은 최소한으로 수행하며, 무한정일 수 있다.&lt;/li&gt;
&lt;li&gt;예를 들어 Sequence, 자바 Stream, Flow, RxJava 스트림&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;(Observable, Single)&lt;/span&gt;이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;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;&lt;a href=&quot;https://www.yes24.com/product/goods/123034354&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.yes24.com/product/goods/123034354&lt;/a&gt;&lt;/p&gt;</description>
      <category>kotlin/coroutines</category>
      <author>hrniin</author>
      <guid isPermaLink="true">https://thdbs523.tistory.com/448</guid>
      <comments>https://thdbs523.tistory.com/448#entry448comment</comments>
      <pubDate>Sun, 16 Feb 2025 19:02:08 +0900</pubDate>
    </item>
    <item>
      <title>3.2 채널과 플로우 - 셀렉트</title>
      <link>https://thdbs523.tistory.com/447</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;코루틴에는 select 함수가 있다. 이 select 함수는&lt;b&gt; 가장 먼저 완료되는 코루틴의 결과를 기다리는 역할을&lt;/b&gt; 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;또한 여러 개의 채널 중 남은 공간이 있는 채널에 데이터를 보내거나, 여러 개의 채널 중 원소가 존재하는 채널로부터 원소를 받을 수도 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;select 함수는 코틀린 코루틴이 출시된 이후부터 사용이 가능했지만, 아직 실험용이다. 즉, API 형태가 바뀔 가능성이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;select를 실제로 사용하는 경우가 드물기 때문에, 안정화될 가능성도 적다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;지연되는 값 선택하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개의 소스에 데이터를 요청한 뒤, 가장 빠른 응답만 얻는 경우를 생각해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 쉬운 방법은 &lt;b&gt;요청을 여러 개의 비동기 프로세스로 시작한 뒤, select 함수 내부에서 값을 기다리는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;select 내부에서는 람다 내 Deferred의 onAwait 함수를 호출한다. 또한 람다 내에서 값을 변환할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739441661687&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;suspend fun requestData1(): String {
    delay(100_000)
    return &quot;Data1&quot;

}

suspend fun requestData2(): String {
    delay(1000)
    return &quot;Data2&quot;
}

val scope = CoroutineScope(SupervisorJob())

suspend fun askMultipleForData(): String {
    val defData1 = scope.async { requestData1() }
    val defData2 = scope.async { requestData2() }

    return select {
        defData1.onAwait { it }
        defData2.onAwait { it }
    }
}

suspend fun main(): Unit = coroutineScope {
    println(askMultipleForData())
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;(1초 후)&lt;br /&gt;Data2&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 비동기 작업이 먼저 완료되면 select 함수도 바로 끝나기 때문에, 하나의 결괏값만 반환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제에서는 외부의 스코프에서 async 코루틴이 시작된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, askMultipleForData를 호출한 코루틴을 취소해도, 외부 스코프에서 시작된 async 코루틴은 취소되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739441726610&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;suspend fun askMultipleForData(): String = coroutineScope {
    select&amp;lt;String&amp;gt; {
        async { requestData1() }.onAwait { it }
        async { requestData2() }.onAwait { it }
    }
}

suspend fun main(): Unit = coroutineScope {
    println(askMultipleForData())
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;(100초 후)&lt;br /&gt;Data2&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;coroutineScope를 사용해서 비동기 작업과 select를 하나의 스코프에서 시작시키는 예제를 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스코프는 모든 자식 코루틴이 끝날 때까지 기다리기 때문에, 1초가 아닌 100초 후에서야 Data2를 결과로 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739441764469&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;suspend fun askMultipleForData(): String = coroutineScope {
    select&amp;lt;String&amp;gt; {
        async { requestData1() }.onAwait { it }
        async { requestData2() }.onAwait { it }
    }.also { coroutineContext.cancelChildren() }
}

suspend fun main(): Unit = coroutineScope {
    println(askMultipleForData())
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;(1초 후)&lt;br /&gt;Data2&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100초 후 Data2를 결과로 받는 문제를 해결하기 위해, 위처럼 코드를 수정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;select가 값을 생성하고 난 후, also 블럭 내부에서 자식 코루틴을 취소시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 async와 select를 사용하면 코루틴끼리 경합하는 상황을 쉽게 구현할 수 있지만, 스코프를 명시적으로 취소해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739441838580&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;suspend fun askMultipleForData(): String = raceOf(
    { requestData1() },
    { requestData2() },
)

suspend fun main(): Unit = coroutineScope {
    println(askMultipleForData())
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;(1초 후)&lt;br /&gt;Data2&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스코프를 명시적으로 취소하는 방식은 약간 복잡하기 때문에, 헬퍼 함수를 정의하거나 raceOf 함수를 지원하는 외부 라이브러리를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추후 raceOf 함수를 직접 구현해 볼 것이다.&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;위 예제에서는 Splitties&lt;/span&gt; 라이브러리의 raceOf를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;채널에서 값 선택하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;select 함수는 채널에서도 사용할 수 있다. select 람다 내에서 사용하는 주요 함수는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;onReceive
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;채널이 값을 가지고 있을 때 선택된다.&lt;/li&gt;
&lt;li&gt;(receive 메서드처럼) 값을 받은 뒤 람다식의 인자로 사용한다.&lt;/li&gt;
&lt;li&gt;select는 람다식의 결괏값을 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;onReceiveCatching
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;채널이 값을 가지고 있거나 닫혔을 때 선택된다.&lt;/li&gt;
&lt;li&gt;값을 나타내거나 채널이 닫혔다는 걸 알려주는 ChannelResult를 받아 람다식의 인자로 사용한다.&lt;/li&gt;
&lt;li&gt;select는 람다식의 결괏값을 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;onSend
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;채널의 버퍼에 공간이 있을 때 선택된다.&lt;/li&gt;
&lt;li&gt;(send 메서드처럼) 채널에 값을 보낸 뒤, 채널의 참조값으로 람다식을 수행한다.&lt;/li&gt;
&lt;li&gt;select는 Unit을 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739441986669&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;suspend fun CoroutineScope.produceString(
    s: String,
    time: Long
) = produce {
    while (true) {
        delay(time)
        send(s)
    }
}

fun main() = runBlocking {
    val fooChannel = produceString(&quot;foo&quot;, 210L)
    val barChannel = produceString(&quot;BAR&quot;, 500L)

    repeat(7) {
        select {
            fooChannel.onReceive {
                println(&quot;From fooChannel: $it&quot;)
            }
            barChannel.onReceive {
                println(&quot;From barChannel: $it&quot;)
            }
        }
    }

    coroutineContext.cancelChildren()
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;From fooChannel: foo&lt;br /&gt;From fooChannel: foo&lt;br /&gt;From barChannel: BAR&lt;br /&gt;From fooChannel: foo&lt;br /&gt;From fooChannel: foo&lt;br /&gt;From barChannel: BAR&lt;br /&gt;From fooChannel: foo&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;select 람다 내부에서는 여러 개의 채널로부터 결괏값을 얻기 위해 onReceive나 onReceiveCatching을 함께 사용한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739442131773&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main(): Unit = runBlocking {
    val c1 = Channel&amp;lt;Char&amp;gt;(capacity = 2)
    val c2 = Channel&amp;lt;Char&amp;gt;(capacity = 2)

    // 값을 보냄
    launch {
        for (c in 'A'..'H') {
            delay(400)
            select&amp;lt;Unit&amp;gt; {
                c1.onSend(c) { println(&quot;Sent $c to 1&quot;) }
                c2.onSend(c) { println(&quot;Sent $c to 2&quot;) }
            }
        }
    }

    // 값을 받음
    launch {
        while (true) {
            delay(1000)
            val c = select&amp;lt;String&amp;gt; {
                c1.onReceive { &quot;$it from 1&quot; }
                c2.onReceive { &quot;$it from 2&quot; }
            }
            println(&quot;Received $c&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Sent A to 1&lt;br /&gt;Sent B to 1&lt;br /&gt;Received A from 1&lt;br /&gt;Sent C to 1&lt;br /&gt;Sent D to 2&lt;br /&gt;Received B from 1&lt;br /&gt;Sent E to 1&lt;br /&gt;Sent F to 2&lt;br /&gt;Received C from 1&lt;br /&gt;Sent G to 1&lt;br /&gt;Received E from 1&lt;br /&gt;Sent H to 1&lt;br /&gt;Received G from 1&lt;br /&gt;Received H from 1&lt;br /&gt;Received D from 2&lt;br /&gt;Received F from 2&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;select 함수에서 onSend를 호출하면 버퍼에 공간이 있는 채널에 원소를 보내는 용도로 사용할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;요약&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;select는 가장 먼저 완료되는 코루틴의 결괏값을 기다릴 때나, 여러 개의 채널 중 전송 또는 수신 가능한 채널을 선택할 때 유용하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 채널과 함께 사용하지만, async 코루틴의 경합을 구현하기 위해 사용할 수도 있다.&lt;/p&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;출처&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.yes24.com/product/goods/123034354&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.yes24.com/product/goods/123034354&lt;/a&gt;&lt;/p&gt;</description>
      <category>kotlin/coroutines</category>
      <author>hrniin</author>
      <guid isPermaLink="true">https://thdbs523.tistory.com/447</guid>
      <comments>https://thdbs523.tistory.com/447#entry447comment</comments>
      <pubDate>Sun, 16 Feb 2025 18:23:48 +0900</pubDate>
    </item>
  </channel>
</rss>