<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>스터디룸</title>
    <link>https://8iggy.tistory.com/</link>
    <description>공부했던 것들 복습 및 요약</description>
    <language>ko</language>
    <pubDate>Thu, 16 Apr 2026 20:51:15 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>8iggy</managingEditor>
    <item>
      <title>2년 만의 연말 회고</title>
      <link>https://8iggy.tistory.com/277</link>
      <description>&lt;p&gt;23년 말 치지직 런칭 준비와 겹쳐 회고를 못했는데 이미 지나간 해라 생략하고 24년을 돌아보면서 25년에는 어떻게 해야할지 정리해보려 한다. 24년은 아쉬운 점, 배웠던 점과 앞으로 해야할 점들이 좀 많았다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;- 서비스&lt;/strong&gt;&lt;br&gt;23년 중순부터 23년 말까지 치지직 서비스 런칭을 준비하면서 정신없이 일했는데 이 때 실력도 키우려는 노력을 미처 들어지 못했던 게 24년까지 이어진 듯하다.&lt;br&gt;24년 초 라이브와 영상, 채팅, 광고 등 어마어마하게 큰 피쳐들을 시니어 두 분이서 담당하느라 ui위주로 담당했었고 이전에 했으면 좋겠다고 제안했던 네비게이션 구조를 앱 전체에 적용해야 했다. 이전에는 굳이 할 필요가 없었으나 사수 분들의 배려 덕에 어거지로 미디어 피커에 적용했던 구조가 피쳐 요구사항으로 인해 강제되는 상황이 와버렸다.&lt;br&gt;기술 리서치도 하고 이미 컴포즈-네비게이션 라이브러리를 순정으로 사용하고 있지 않아서 기존 사용하던 스펙과 충돌하지 않게끔 커스텀해서 사용할 수 있도록 하면서 많은 시간을 쏟았다. 라이브러리 내 버그도 발견해서 구글 이슈 트래커에 질문 답변을 주고받으며 버그 수정도 해보고 XML-네비게이션에선 지원하나 컴포즈-네비게이션에서 지원하지 않는 옵션들도 지원하게끔 커스텀하면서 실력을 키울 수 있었다.&lt;br&gt;다만, 내가 작성한 방식이 문제가 발생했을 때 코드 책임범위가 너무 넓어 문제 발생 및 담당자 부재 시 팀원이 트러블슈팅하기에 힘들다는 점, 다른 팀원이 사용하기에 정의해야 할 점들이 너무 많다는 점들을 지적받았다. 취준 때 디자인 패턴이 교조적이라 생각했었는데 진지하게 공부해야 한다는 생각이 들었다. Hilt를 써보려고 도입해봤는데 막상 Hilt없이도 잘 동작해서 머쓱했었는데 좀 더 검증을 철저히 했어야 했다는 생각도 들었다. 프로젝트 구조 상 DB, 네트워크 Repository를 ViewModel 생성자 등에 주입하는 구조가 아니고 따로 싱글톤으로 관리하고 있어서 Hilt 라이브러리의 효용성이 그리 크지 않았는데 새로운 시도를 해야 한다는 강박에 그랬던 것 같다.&lt;br&gt;안드로이드 네비게이션 동작 시 &lt;code&gt;luanchSingleTop&lt;/code&gt;, &lt;code&gt;popUpTo&lt;/code&gt;, &lt;code&gt;inclusive&lt;/code&gt;, &lt;code&gt;saveState&lt;/code&gt;, &lt;code&gt;restoreState&lt;/code&gt; 등 플래그를 조합하면서 다양한 케이스의 이동에 대응하는데 사수와 리뷰하면서 저 플래그의 조합에 따라서 어떻게 동작이 바뀌는지 단편적으로밖에 대답을 드리지 못했던 기억이 난다. 해당 부분이 기존 액티비티 구조와 플래그가 다르게 동작하는 부분이 있어 최대한 기존 프로젝트 migration 진행 시 side effect이 발생하지 않도록 철저하게 모든 케이스를 고려했어야 했는데 그러질 못했다. 사수와 함께 &lt;code&gt;navigate&lt;/code&gt; 관련 함수 커스텀을 진행하면서 접근법, 방법론 등을 많이 배웠다.&lt;br&gt;여름 배포에는 타 부서 간 협업이 엄청나게 많았던 피쳐를 맡았었는데 사내 슬랙에 동시다발적으로 채널 멘션되니 도저히 집중할 수가 없었다. 카톡 안읽은 메세지를 지워야만 직성이 풀리는 성격 때문이었을까. 막상 안드로이드 담당자로 미친듯이 소환되는데 신속하게 대답을 드릴 수가 없어서 답답하고 죄송스러웠다. 아이폰 담당자 분이 나보다 훨씬 경력이 있으신 분이라 먼저 답변해주셔서 슬쩍 얹어간 적도 많았다. 앞으로 광고던 채팅이던 라이브던 어느 하나를 맡더라도 타 부서 간 협업이 엄청나게 이뤄지는 피쳐들이라 걱정이 된다.&lt;br&gt;추가로 공유가 제대로 이뤄지지 않는 것 같다는 피드백도 있었다. 사내 코드 관련 작업을 하다가 수정한 내용이나 기능 관련 배포된 코드가 있었는데 코드 모양새가 파악하기 힘들다던지 side effect이 발생할 여지가 있는 코드가 있으면 한 번쯤은 팀원에게 조언을 구해야 했다는 내용이었다. 왜 그랬나 생각해보니 그간 실수하고 도움을 자주 받으면서 알게 모르게 좀 위축되었던 것 같다. 막바지 배포 준비로 급할 때 발견해서 조치하려면 더 스트레스 상황으로 돌아올테니 앞으로는 코드 모양새에 짜침이 느껴지면 일단 물어봐야겠다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;- 부업&lt;/strong&gt;&lt;br&gt;사실상 24년에는 부업을 거의 하지 못했다. 이미 런칭해서 운영 중인 합격왕 서비스도 기능 개발에 리소스를 많이 쏟을 수 없었고 연말에는 내가 해야할 부분들 팀장 선배가 담당해서 처리해줬다. 다른 부업 활동도 간단한 유틸 앱 2개, 추석 연휴에 급하게 작업해서 게임 관련 앱 하나 런칭한 게 거의 전부였다. 해야할 일들을 명확하게 정리하고 최대한 쪼개서 주말에 1시간만이라도 봤다면 더 많이 할 수 있었겠지만 되돌아보면 아예 진이 빠져서 주말에는 늦잠과 멍 때리기만 하느라 시간을 보냈어서 의미없는 후회같다. 내년에는 간단한 서버 작업 및 아이폰 앱 개발도 gpt 도움을 받고 실습하면서 개발 범위를 넓혀보려 한다. 현업 수준의 코드를 요구하진 않으니 해볼 수 있는 도전이지 않을까&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;- 투자&lt;/strong&gt;&lt;br&gt;부업에는 시간을 투자할 수 없으니 핸드폰으로 해볼 수 있는 주식을 조금 들여다 봤다. 기존 중장기 채권으로 구성된 포트폴리오 리밸런싱, 드러켄 밀러가 투자했던 종목 3개월 늦게 추격매수했을 때 수익 검증, 성장주&amp;amp;방어주 조합으로 장기 분할 매수할 포트폴리오 찾기 등을 했었다. 연초에 수행했던 리밸런싱의 모양새가 조금 이상해지고 드러켄 밀러 추격매수 검증하면서 완전히 짬통 포트폴리오였는데 너무 많은 시간을 들이는 것 같아 전부 정리하고 장기적으로 은퇴시점까지 가져갈 수 있는 포트폴리오를 찾는데 집중했다. 그 결과, 어느정도 고정된 비율의 내 투자관에 맞는 포트폴리오를 찾아가고 있다. 내년에는 월급에서 투자금액 비중을 확대할 예정인데 회사, 부업과 운동 위주로 시간을 할애할 생각이라 마침 주식에 신경을 덜 쓸 수 있게끔 정리되고 있어 다행이라 생각한다. 만약 시간이 난다면 gpt로 서버 공부하면서 주식 종목 및 포트폴리오 백테스팅 성능 비교분석 앱 하나 정도 만들어볼까 고민 중이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;- 생활&lt;/strong&gt;&lt;br&gt;그닥 좋은 수준은 아니었다. 일과 생활이 무너지면서 4-5월과 7-8월 개발 기간에는 몇 주간 새벽 2-3시 취침, 오전 8-9시 기상 유지하기도 하고 잘 풀리지 않을 땐 새벽 4-5시까지 작업하고 11시에 일어나는 생활을 했었다. 스스로 조절하지 못하는 생활이 이어지면서 연말에 이르러 심할 때는 1주 정도 출근 후 키보드에 손을 올렸는데 스스로 놀랄 정도로 코드를 작성할 수 없던 기간도 있었다. 주말 최대한 생각하지 않으려 노력하면서 쉬며 컨디션을 되돌린 결과 무사히 배포를 마쳤는데 생각했던 공수산정 기간을 많이 초과했던 것 같고 코드 퀄리티도 좋지 못했던 것 같아 스스로에게 부끄러웠다. 올해 들어서 이런 경우를 경험하다보니 자기관리도 업무의 연장선이라는 생각이 들었다. 내년에는 이 부분을 좀 더 제대로 신경써서 관리할 예정이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;- 총평&lt;/strong&gt;&lt;br&gt;치지직으로 시작해서 치지직으로 끝난 한 해였다. 돌이켜보면 나도 뒤지게 바쁜 것 같은데 주변 직원 분들은 진짜 저걸 감당할 수 있나 싶은 수준이라 앓는 소리도 조심스러웠다.(&lt;del&gt;그래도 서로 출근하는 네트워킹 데이 땐 일하기 싫다고 노래를 불렀더랬다&lt;/del&gt;) 다행히 급한 기능들은 무사히 배포된 것 같아 바빴던 기간이 무의미하진 않았어서 다행이라 생각한다. 내년에는 바쁘다는 생각에 매몰되지 않고 휴식을 취해야 할 때를 구분하며 내가 놓치고 있는 게 무엇인지 확인하면서 더 나은 한 해를 보내려 한다.&lt;/p&gt;</description>
      <category>기록/뻘글</category>
      <author>8iggy</author>
      <guid isPermaLink="true">https://8iggy.tistory.com/277</guid>
      <comments>https://8iggy.tistory.com/277#entry277comment</comments>
      <pubDate>Thu, 2 Jan 2025 09:00:38 +0900</pubDate>
    </item>
    <item>
      <title>2022년 회고</title>
      <link>https://8iggy.tistory.com/276</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;으레 많은 개발자들이 연말갬성을 뭉치고 모아 회고글을 쓰듯 나역시 23년 각오를 다지기 위해 뻘글을 써보고자 한다. &lt;br /&gt;&lt;br /&gt;2022년은&amp;nbsp;경찰공무원에서&amp;nbsp;개발자로&amp;nbsp;이직에&amp;nbsp;성공한&amp;nbsp;뒤&amp;nbsp;맞는&amp;nbsp;첫&amp;nbsp;해여서&amp;nbsp;내게&amp;nbsp;굉장히&amp;nbsp;뜻깊은&amp;nbsp;1년이었다.&amp;nbsp;21년도엔 막연히&amp;nbsp;백엔드&amp;nbsp;개발자가&amp;nbsp;되기&amp;nbsp;위해&amp;nbsp;CS와&amp;nbsp;알고리즘&amp;nbsp;등을&amp;nbsp;공부했지만&amp;nbsp;신입&amp;nbsp;공채직원&amp;nbsp;부서&amp;nbsp;배치&amp;nbsp;과정에서&amp;nbsp;안드로이드&amp;nbsp;부서로&amp;nbsp;배치되었다. &lt;br /&gt;안드로이드를 제대로 공부한 적도 없는데 너무 뜬금없어서 당황했지만 돌이켜보면 프레임워크를 익히지도 않고 뭣 하나 구현한 것도 없는 신입 개발자가 무슨 패기로 백엔드 직군을 지망했는지 어이가 없을 지경이다. &lt;br /&gt;&lt;br /&gt;21년&amp;nbsp;회고는&amp;nbsp;월별로&amp;nbsp;나눠서&amp;nbsp;진행했지만&amp;nbsp;올해는&amp;nbsp;월별이고&amp;nbsp;자시고&amp;nbsp;정신없이&amp;nbsp;몰려오는&amp;nbsp;대로&amp;nbsp;치워내느라&amp;nbsp;섹션&amp;nbsp;별로&amp;nbsp;나눠서&amp;nbsp;정리해야겠다. &lt;br /&gt;&lt;br /&gt;&lt;b&gt;1.&amp;nbsp;안드로이드&amp;nbsp;개발자&lt;/b&gt; &lt;br /&gt;2월까지의&amp;nbsp;공채&amp;nbsp;적응&amp;nbsp;기간을&amp;nbsp;거치고&amp;nbsp;난&amp;nbsp;뒤(&lt;s&gt;세상에 온보딩이 2개월이라니 생각보다 갓기업이었음...&lt;/s&gt;)&amp;nbsp;밴드,&amp;nbsp;카페를&amp;nbsp;개발한&amp;nbsp;Group&amp;amp;&amp;nbsp;CIC에&amp;nbsp;배치를&amp;nbsp;받았다.&amp;nbsp;거기에&amp;nbsp;네이버에서&amp;nbsp;가장&amp;nbsp;최근에&amp;nbsp;생겼다고&amp;nbsp;볼&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;Game부서였다.&amp;nbsp;게임&amp;nbsp;개발하나&amp;nbsp;싶었는데&amp;nbsp;게임&amp;nbsp;커뮤였다.&amp;nbsp;멘토와&amp;nbsp;리더와의&amp;nbsp;면담&amp;nbsp;때&amp;nbsp;최대한&amp;nbsp;어필한&amp;nbsp;점은&amp;nbsp;진짜&amp;nbsp;아무것도&amp;nbsp;모르고&amp;nbsp;백엔드를&amp;nbsp;지망했었다는&amp;nbsp;점이었다.&amp;nbsp;실제로&amp;nbsp;아무것도&amp;nbsp;몰랐으니&amp;nbsp;딱히&amp;nbsp;틀린&amp;nbsp;말은&amp;nbsp;아니었지만...&amp;nbsp;'-`&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;초면에 납작 업드려서 그런지 사수와 리더님이 굉장히 케어를 많이 해주셨다. 덕분에 정말 많은 배려를 받고 기초를 쌓는데 집중할 수 있었다. 그리고 자바로 개발하지 않고 코틀린으로 개발해야 한대서 안드로이드와 함께 코틀린까지 공부를 해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;nbsp; - Kotlin&lt;/b&gt; &lt;br /&gt;&quot;Kotlin&amp;nbsp;in&amp;nbsp;Action&quot;이라는&amp;nbsp;책을&amp;nbsp;추천받았는데&amp;nbsp;JVM까지&amp;nbsp;고려하면서&amp;nbsp;공부하려니&amp;nbsp;진도가&amp;nbsp;너무&amp;nbsp;느렸다.&amp;nbsp;결국&amp;nbsp;람다까지만&amp;nbsp;보고&amp;nbsp;실습&amp;nbsp;프로젝트에&amp;nbsp;들어갔다.&amp;nbsp;ㅠ&amp;nbsp;아직&amp;nbsp;자바도&amp;nbsp;정리&amp;nbsp;못했는데&amp;nbsp;큰일이다.&amp;nbsp;그래도&amp;nbsp;제네릭을&amp;nbsp;제외하곤&amp;nbsp;실제&amp;nbsp;개발에&amp;nbsp;본격적으로&amp;nbsp;쓰이진&amp;nbsp;않는&amp;nbsp;내용이라&amp;nbsp;천천히&amp;nbsp;공부해보려&amp;nbsp;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;생각보다 언어가 직관적이어서 오히려 자바보다 더 코드가 간결해졌다. 앞으로 자바로 쓰인 앱을 건들 자신이 없어졌다. 리서치하면서도 자바 코드로 쓰인 포스트는 자연스레 뒤로 가기를 누르는 나를 발견할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;nbsp; - Android&lt;/b&gt; &lt;br /&gt;안드로이드의&amp;nbsp;기본적인&amp;nbsp;시스템&amp;nbsp;공부를&amp;nbsp;위해&amp;nbsp;&quot;Do&amp;nbsp;it&amp;nbsp;안드로이드&amp;nbsp;프로그래밍&amp;nbsp;with&amp;nbsp;Kotlin&quot;&amp;nbsp;도서를&amp;nbsp;잡고&amp;nbsp;외부&amp;nbsp;스터디를&amp;nbsp;주관해서&amp;nbsp;진행했다.&amp;nbsp;덕분에&amp;nbsp;어느정도&amp;nbsp;감이&amp;nbsp;잡히긴&amp;nbsp;했는데&amp;nbsp;아직&amp;nbsp;브로드캐스트&amp;nbsp;리시버와&amp;nbsp;콘텐츠&amp;nbsp;프로바이더&amp;nbsp;쪽&amp;nbsp;정리가&amp;nbsp;미숙해&amp;nbsp;23년&amp;nbsp;초에&amp;nbsp;정리를&amp;nbsp;진행할까&amp;nbsp;싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사내 안드로이드 입사 동기들끼리도 스터디를 진행해서 Kotlin의 비동기 처리 수단인 Coroutine에 대해서도 어느정도 깊이있게 스터디를 진행했다. 내부 레포에 정리한 자료를 사수님이 보시고 23년 RxKotlin으로 구현된 모듈을 Coroutine으로 migration하자는 목표를 꺼내셨다. 아무래도 큰일 난 것 같다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;대부분의 앱은 XML기반으로 작성된다. 하지만 Game 부서는 신생 서비스여서 사수가 Jetpack Compose를 시작부터 도입했다고 한다. 덕분에 Compose 초기부터 현재까지 발생했던 상당히 많은 버그들과 많은 커스텀 레이아웃들을 볼 수 있었다. 문제는 초기 Compose에서 제공하지 않던 기능을 위해 구현했던 방식이 지금에 와선 best-practice아닌 부분이 있어 리팩토링하겠다고 덤볐다가 머가리가 깨지고 있다... 역시 사수님이 그렇게 작성하신 데에는 다 이유가 있기 마련이다. ㅠㅠㅠ &lt;br /&gt;&lt;br /&gt;&lt;b&gt;2. 사이드 프로젝트&lt;/b&gt;&lt;br /&gt;9월 즈음 지인과 함께 2인 1팀으로 진행을 했다. 그 친구는 기획/마케팅/서버를 담당하고 나는 클라이언트/디자인 담당이 되었다. 업무도 적응 못했는데 뭔 사이드 플젝이냐 생각이 들었지만 또 생각해보면 코드는 많이 작성할수록 좋고 구현하면서 안드로이드 시스템 공부가 되지 않을까 싶어 적극적으로 참여하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;약 3개월 동안 진행한 결과 쉽지 않겠단 생각이 들었지만 역시 더더욱 그렇다. 23년에도 가열차게 제작해봐야 결과지를 받아볼 수 있겠다. 일단은 계속 진행해도 괜찮겠다는 판단이 섰다. &lt;br /&gt;&lt;br /&gt;&lt;b&gt;3.&amp;nbsp;가사노동(?)&lt;/b&gt; &lt;br /&gt;동생놈이&amp;nbsp;공부를&amp;nbsp;개같이&amp;nbsp;못해서&amp;nbsp;부모님으로부터&amp;nbsp;SOS가&amp;nbsp;들어왔다.&amp;nbsp;한창&amp;nbsp;주가를&amp;nbsp;올리기&amp;nbsp;위해&amp;nbsp;굴러야&amp;nbsp;하는&amp;nbsp;입장에서&amp;nbsp;굉장히&amp;nbsp;부담되는&amp;nbsp;요청이지만&amp;nbsp;어쩌겠는가.&amp;nbsp;지금&amp;nbsp;구제하지&amp;nbsp;못하면&amp;nbsp;60년을&amp;nbsp;살아가야&amp;nbsp;할&amp;nbsp;가족구성원&amp;nbsp;하나가&amp;nbsp;제&amp;nbsp;몫을&amp;nbsp;감당하지&amp;nbsp;못해&amp;nbsp;결국&amp;nbsp;내게&amp;nbsp;업보로&amp;nbsp;돌아오겠다는&amp;nbsp;생각이&amp;nbsp;들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래도 최대한 부담을 덜어야겠다는 생각에 머리를 굴린 결과 감독없이 일방적 보고체계만으로 진도를 확인할 수 있도록 절차를 만들었다. 그리고 점검을 위해 주말 하루만을 희생해 매주 테스트를 제작/출제를 했다. &lt;br /&gt;추석부터&amp;nbsp;시작했으니&amp;nbsp;약&amp;nbsp;3개월이&amp;nbsp;지난&amp;nbsp;결과&amp;nbsp;고1&amp;nbsp;3등급&amp;nbsp;수준의&amp;nbsp;지능에서&amp;nbsp;고2&amp;nbsp;3등급&amp;nbsp;수준의&amp;nbsp;지능까지&amp;nbsp;끌어올린&amp;nbsp;느낌이다...&amp;nbsp;국어&amp;nbsp;영어만&amp;nbsp;봐주고&amp;nbsp;있는데&amp;nbsp;1월부터는&amp;nbsp;한국사까지&amp;nbsp;추가해야&amp;nbsp;한다.&amp;nbsp; &lt;br /&gt;&lt;br /&gt;&lt;b&gt;4.&amp;nbsp;운동&lt;/b&gt; &lt;br /&gt;몸을 축내면서 이직 준비를 하느라 이젠 진짜 몸을 움직여야 한다고 신체가 신호를 보내왔다. 마침 이사를 하고 난 뒤 단지 시설에 나?름 갖춰진 헬스장이 있어 가끔 출석도장을 찍었다. 처음에는 &quot;진짜 답도 없네 ㅋㅋ&quot; 싶었는데 몇 달에 걸쳐서 꾸준히 가다보니 생각보다 조금씩 적응하는 게 보여서 재미를 붙일 수 있었다. 2월 즈음부터 다니기 시작했는데 요번에 보니 3개월 좀 넘는 107일로 기록되어 있다. 계산해보면 대충 1주에 약 2일을 운동한 셈이다. 생각보다 출석율이 저조해서 23년에는 200일 언저리로 다닐 수 있도록 해봐야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;프레임이 넓어졌다고 느끼는 게 한국 기준 100사이즈 티셔츠가 맞지 않아서 서양 기준 100이나 한국 기준 105를 입어야 찌셔츠가 되지 않고 공간이 남는 수준이 되었다. 23년 일정이 22년보다 바빠질 예정이지만 게임을 줄여서라도 가야지... &lt;br /&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;총평&lt;/b&gt; &lt;br /&gt;21년은&amp;nbsp;네이버&amp;nbsp;입사가&amp;nbsp;주요&amp;nbsp;이벤트라면&amp;nbsp;22년은&amp;nbsp;신입&amp;nbsp;직원의&amp;nbsp;적응기로&amp;nbsp;요약할&amp;nbsp;수&amp;nbsp;있겠다.&amp;nbsp;그리고&amp;nbsp;생각보다&amp;nbsp;허비한&amp;nbsp;시간이&amp;nbsp;많다&amp;nbsp;생각했는데&amp;nbsp;꽤나&amp;nbsp;꽉&amp;nbsp;차&amp;nbsp;있던&amp;nbsp;한&amp;nbsp;해였다.&amp;nbsp;작년에도&amp;nbsp;제작년보다&amp;nbsp;나아졌다고&amp;nbsp;생각했는데&amp;nbsp;올해도&amp;nbsp;작년보다&amp;nbsp;조금이나마&amp;nbsp;나아진&amp;nbsp;느낌이라&amp;nbsp;기분이&amp;nbsp;나쁘진&amp;nbsp;않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;23년은 대학원 복학이 예정되어 있다. 이직하고 22년 적응하느라 1년 휴학을 했는데 이제 영수증을 받아볼 때가 되었다. 자퇴를 할까도 싶었는데 지금 아니면 석사를 언제 따겠나 싶어서 복학을 결정했다. 딱히 관련없는 보안 전공이라 너무 많은 노력을 쏟을 생각은 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;결국 내년에는 위의 활동과 함께 대학원이라는 과업이 추가되었다. 가을 즈음에 번아웃 비스무리한 증상이 찾아왔었는데 23년에 그런 증상 없이 보내기 위해 다른 과제들을 쳐내는 효율을 올릴 필요가 있어보인다.&lt;/p&gt;</description>
      <category>기록/뻘글</category>
      <author>8iggy</author>
      <guid isPermaLink="true">https://8iggy.tistory.com/276</guid>
      <comments>https://8iggy.tistory.com/276#entry276comment</comments>
      <pubDate>Mon, 2 Jan 2023 00:07:14 +0900</pubDate>
    </item>
    <item>
      <title>Android | Jetpack Compose에서의 상태 관리 - Composable, State Holder, ViewModel</title>
      <link>https://8iggy.tistory.com/275</link>
      <description>&lt;h2&gt;읽기 전&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.&lt;/li&gt;
&lt;li&gt;개인적으로 실습하면서 배운 점을 정리한 글입니다,&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.android.com/jetpack/compose/state&quot;&gt;Jetpack Compose State 공식문서&lt;/a&gt;의 내용을 정리합니다. Compose에서 관리할 State가 많아질 때 어떻게 책임을 나눠야하는 지 알아봅니다. 사실상, 대부분의 서비스 앱은 MVVM 패턴으로 구성된다. 이번 포스팅은 Compose를 도입 시 어떻게 MVVM 패턴으로 구현할 수 있는지 정리함을 목적으로 합니다.&lt;/p&gt;
&lt;h2&gt;Compose에서의 상태 관리 - UI, State Holder, ViewModel&lt;/h2&gt;
&lt;p&gt;Compose에서는 UI element의 상태, UI에 표시될 상태 등 다양한 상태를 관리하면서 사용자에게 화면을 출력한다. 간단한 상태 호이스팅은 컴포저블 함수에서 관리할 수 있다. 그러나, 루트 컴포저블에 가까워질 수록 추적할 상태가 많아지거나 컴포저블 함수에서 실행할 비교적 복잡한 로직이 있는 경우 로직과 상태에 대한 책임을 다른 클래스에 위임하는 것이 좋다.&lt;/p&gt;
&lt;p&gt;특히 Compose UI element의 상태를 관리하는 다른 클래스, &lt;strong&gt;상태 홀더(State Holder)&lt;/strong&gt;에 위임하는 것을 권장한다. 아래는 Compose 화면의 상태 관리와 관련된 주요 항목이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;컴포저블(Composable): 간단한 UI element state를 관리&lt;/li&gt;
&lt;li&gt;상태 홀더(State Holder): 복잡한 UI element state를 관리하며 state와 연결된 UI 로직도 보유&lt;/li&gt;
&lt;li&gt;ViewModel: 비즈니스 로직 및 화면 UI state에 대한 액세스 권한을 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGLIR0/btrUAcsktwC/gE8E6mkXdk2OyHrkpdrSk0/img.png&quot; alt=&quot;Android_Compmose_Managing_State_001&quot;&gt;&lt;/p&gt;
&lt;p&gt;위 그림은 Compose의 상태 관리와 관련된 항목과 역할을 요약한 그림이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;컴포저블&lt;/strong&gt;은 복잡성에 따라 0개 이상의 상태 홀더를(일반 객체 혹은 ViewModel) 사용할 수 있다. 상태 홀더를 유연하게 필요에 따라 사용할 수 있다는 의미이다.&lt;/p&gt;
&lt;p&gt;일반적인 &lt;strong&gt;State Holder&lt;/strong&gt;는(영어 버전 공식문서에서는 Plain State Holder로 명명되어 있으며 한국어 번역 상 &amp;quot;일반적인 상태 홀더&amp;quot;라고 작성된 것으로 보인다.) 비즈니스 로직이나 화면 상태에 액세스해야 하는 경우 ViewModel을 사용할 수도 있다. ViewModel은 비즈니스 로직을 담당하고 있으며 UI에 표시할 데이터 또한 보관하고 있기에 상태 홀더가 ViewModel에 접근하는 경우가 있다는 말이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ViewModel&lt;/strong&gt;은 비즈니스 레이어 혹은 데이터 영역을 사용한다. ViewModel은 데이터를 조회하고 저장하기 위해 데이터 영역과 상호작용한다. 이는 비즈니스 로직 수행을 위한 비즈니스 레이어로의 접근과도 동일하게 볼 수 있다.&lt;/p&gt;
&lt;h2&gt;상태 및 로직 유형&lt;/h2&gt;
&lt;h3&gt;상태 유형&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;화면 UI 상태 : 화면에 표시되어야 하는 정보를 담는다. 앱의 데이터를 포함하고 있어 보통 다른 layer들과 연결된다.&lt;/li&gt;
&lt;li&gt;UI element 상태 : UI element의(ex. Composable) 상태를 호이스팅한 결과다. 예를 들어, &lt;code&gt;ScaffoldState&lt;/code&gt;는 &lt;code&gt;Scaffold&lt;/code&gt; 컴포저블의 상태를 처리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;로직 유형&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;UI 로직 : 화면에 상태 변경을 표시하는 방법과 관련이 있다. 예를 들어, 버튼을 사용자가 클릭하면 특정 화면으로 탐색하는 로직이나 목록의 특정 아이템으로 스크롤하는 로직 등이 해당된다.&lt;/li&gt;
&lt;li&gt;비즈니스 로직 : 상태 변경에 따라 진행 여부가 결정되는 작업이다. 결제/환경설정 저장 등이 해당된다. 예를 들어, 유저가 버튼을 클릭하여 뉴스 앱에서 기사를 북마크할 때 비즈니스 로직은 북마크 정보를 파일이나 데이터베이스에 저장한다. 따라서, 해당 로직은 보통 도메인이나 데이터 레이어에 속하며 UI 레이어에 배치되진 않는다. 상태 홀더는 보통 이 로직을 도메인/데이터 레이어가 노출한 함수를 호출함으로써 위임한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;정보 소스로서의 컴포저블&lt;/h2&gt;
&lt;p&gt;상태와 로직이 간단하다면 컴포저블에 UI 로직와 UI element 상태를 사용함이 좋다. ]&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun MyApp() {
    MyTheme {
        val scaffoldState = rememberScaffoldState()
        val coroutineScope = rememberCoroutineScope()

        Scaffold(scaffoldState = scaffoldState) {
            MyContent(
                showSnackbar = { message -&amp;gt;
                    coroutineScope.launch {
                        scaffoldState.snackbarHostState.showSnackbar(message)
                    }
                }
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드는 UI element state인 &lt;code&gt;ScaffoldState&lt;/code&gt;와 &lt;code&gt;CoroutineScope&lt;/code&gt;를 처리하는 임의로 작성된 &lt;code&gt;MyApp&lt;/code&gt; 컴포저블이다. Coroutine을 사용하여 화면에 스낵바 출력에 해당하는 UI 로직을 처리하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ScaffoldState&lt;/code&gt;에 변경될 수 있는 속성이 포함되기에 해당 컴포저블과의 모든 상호작용은 위에 정의한 &lt;code&gt;MyApp&lt;/code&gt; 컴포저블에서 이루어져야 한다. 그렇지 않고 다른 컴포저블에 전달해버리면 해당 컴포저블이 상태를 변경할 수 있기에 단일 정보 소스 원칙에 위배되고 버그 추적에 어려움을 겪는다.&lt;/p&gt;
&lt;h2&gt;정보 소스로서의 State Holder&lt;/h2&gt;
&lt;p&gt;여러 UI element들의 상태와 관련된 복잡한 UI 로직이 있는 컴포저블이라면 책임을 상태 홀더에 위임하는 것이 좋다. 책임을 분리함으로써 격리하여 테스트를 진행할 수 있고 컴포저블의 복잡성 또한 줄어들기 때문이다. 컴포저블은 UI element를 출력하는 데 집중하고 상태 홀더가 UI로직과 UI element 상태를 담당함을 의미한다.&lt;/p&gt;
&lt;p&gt;상태 홀더 구현은 일반 클래스로 하되 컴포지션에 의해 생성되고 기억되기에 Compose 종속 항목을(ex. &lt;code&gt;remember~state&lt;/code&gt;) 사용할 수 있다. 컴포지션에서 기억할 수 있도록 &lt;code&gt;remember&lt;/code&gt;를 사용하여 함수를 작성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// Plain class that manages App&amp;#39;s UI logic and UI elements&amp;#39; state
class MyAppState(
    val scaffoldState: ScaffoldState,
    val navController: NavHostController,
    private val resources: Resources,
    /* ... */
) {
    val bottomBarTabs = /* State */

    // Logic to decide when to show the bottom bar
    val shouldShowBottomBar: Boolean
      get() = /* ... */
    // Navigation logic, which is a type of UI logic
    fun navigateToBottomBarRoute(route: String) { /* ... */ }

    // Show snackbar using Resources
    fun showSnackbar(message: String) { /* ... */ }
}

@Composable
fun rememberMyAppState(
    scaffoldState: ScaffoldState = rememberScaffoldState(),
    navController: NavHostController = rememberNavController(),
    resources: Resources = LocalContext.current.resources,
    /* ... */
) = remember(scaffoldState, navController, resources, /* ... */) {
    MyAppState(scaffoldState, navController, resources, /* ... */)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 UI 로직과 UI element 상태를 보관하던 &lt;code&gt;MyApp&lt;/code&gt;은 UI element 명세에만 집중하고 나머지는 &lt;code&gt;MyAppState&lt;/code&gt;에 위임하였다. UI element state 인스턴스 조회를 위해 &lt;code&gt;rememberMyAppState&lt;/code&gt; 함수를 새롭게 정의하여 &lt;code&gt;MyApp&lt;/code&gt; 컴포저블에서 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun MyApp() {
    MyTheme {
        val myAppState = rememberMyAppState()
        Scaffold(
            scaffoldState = myAppState.scaffoldState,
            bottomBar = {
                if (myAppState.shouldShowBottomBar) {
                    BottomBar(
                        tabs = myAppState.bottomBarTabs,
                        navigateToRoute = {
                            myAppState.navigateToBottomBarRoute(it)
                        }
                    )
                }
            }
        ) {
            NavHost(navController = myAppState.navController, &amp;quot;initial&amp;quot;) { 
                /* ... */ 
            }
        }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;컴포저블이 제어할 UI element가 늘어남에 따라 매번 따로 파라미터를 관리하기가 굉장히 번잡해졌다. 컴포저블의 책임이 늘어남에 따라 State Holder 도입 필요성이 증가한다.&lt;/p&gt;
&lt;h4&gt;참고할 점 - Compose UI element state 복원&lt;/h4&gt;
&lt;p&gt;만약 Activity 혹은 프로세스가 종료된 이후에도 보존하려는 상태를 상태 홀더에 포함하려면 &lt;code&gt;rememberSaveable&lt;/code&gt;을 사용하고 상태에 맞는 커스텀 Saver를 만들면 된다.&lt;/p&gt;
&lt;h2&gt;정보 소스로서의 ViewModel&lt;/h2&gt;
&lt;p&gt;상태 홀더 클래스가 UI 로직과 UI element의 상태를 담당했었다. ViewModel은 다른 특별한 유형의 상태 홀더로서 다음의 작업을 맡는다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;비즈니스/데이터 레이어 등 주로 계층 구조의 다른 레이어에 배치되는 앱의 비즈니스 로직에 대한 액세스 권한 제공&lt;/li&gt;
&lt;li&gt;특정 화면에 표시하기 위한 앱 데이터 준비(화면 UI 상태가 됨)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;참고할 점 - ViewModel의 수명 &amp;gt; Composable의 수명&lt;/h4&gt;
&lt;p&gt;ViewModel은 컴포지션보다 수명이 더 길다. config change가 발생하여 컴포지션이 종료되더라도 유지되기 때문이다. ViewModel의 수명주기는 Compose contents의(Activity or Fragment) 호스트 수명주기나 Navigation Graph의 수명주기를 따를 수 있다. &lt;/p&gt;
&lt;p&gt;따라서, 컴포지션보다 긴 수명주기를 가지고 있기에 컴포지션의 수명과 바인딩된 상태를 참조해선 안된다. 컴포지션의 수명과 바인딩된 상태를 참조하여 장기 지속 참조가 이어지면 메모리 누수가 발생할 수 있기 때문이다.&lt;/p&gt;
&lt;h4&gt;주의할 점 - Composable 변수로 인스턴스 전달&lt;/h4&gt;
&lt;p&gt;ViewModel의 인스턴스를 다른 컴포저블 함수로 전달하면 안된다고 설명한다. 만약 컴포저블 함수에 ViewModel을 전달하면 해당 함수는 ViewModel의 특정 타입과 결합되어 재사용성이 떨어지고 테스트와 preview 동작에 어려움을 겪기 때문이다. 또한, ViewModel 인스턴스를 관리하는 명확한 단일 소스 저장소가 없어진다. ViewModel을 전달하면 여러 컴포저블이 ViewModel의 함수를 호출하고 상태를 수정할 수 있기 때문에 디버깅도 더 어려워진다. 따라서, UDF 권장사항에 따라 필요한 상태만 전달함이 옳다.&lt;/p&gt;
&lt;h4&gt;주의할 점 - ViewModel에서 UI element state 저장&lt;/h4&gt;
&lt;p&gt;ViewModel은 컴포저블 함수가 아니므로 컴포지션의 구성요소가 아니다. 따라서, 컴포저블에서 만든 상태를(&lt;code&gt;remember~State&lt;/code&gt;) ViewModel이 보관해서는 안된다. 별도의 State Holder 클래스를 선언하여 해당 인스턴스를 보관해야 한다.&lt;/p&gt;
&lt;p&gt;다음 코드는 Screen Level Composable에서 사용된 ViewModel의 예시 코드이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;data class ExampleUiState(
    val dataToDisplayOnScreen: List&amp;lt;Example&amp;gt; = emptyList(),
    val userMessages: List&amp;lt;Message&amp;gt; = emptyList(),
    val loading: Boolean = false
)

class ExampleViewModel(
    private val repository: MyRepository,
    private val savedState: SavedStateHandle
) : ViewModel() {

    var uiState by mutableStateOf(ExampleUiState())
        private set

    // Business logic
    fun somethingRelatedToBusinessLogic() { /* ... */ }
}

@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {

    val uiState = viewModel.uiState
    /* ... */

    ExampleReusableComponent(
        someData = uiState.dataToDisplayOnScreen,
        onDoSomething = { viewModel.somethingRelatedToBusinessLogic() }
    )
}

@Composable
fun ExampleReusableComponent(someData: Any, onDoSomething: () -&amp;gt; Unit) {
    /* ... */
    Button(onClick = onDoSomething) {
        Text(&amp;quot;Do something&amp;quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;커스텀 UI 상태 홀더 클래스인 &lt;code&gt;ExampleUiState&lt;/code&gt; 클래스를 정의하였고 &lt;code&gt;ExampleScreen&lt;/code&gt; 컴포저블에서 &lt;code&gt;viewModel()&lt;/code&gt; 함수를 사용하여 ViewModel 인스턴스를 생성한 뒤 데이터를 조회하여 &lt;code&gt;ExampleReusableComponent&lt;/code&gt; 컴포저블의 파라미터로 전달함을 확인할 수 있다. 이렇듯 ViewModel은 사용자에게 보여줄 커다란 데이터를 보관하거나 비즈니스 로직을 담당한다.&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;viewModel()&lt;/code&gt; 함수&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;viewModel()&lt;/code&gt; 함수는 현재 생성된 ViewModel 인스턴스를 불러오거나 새롭게 생성하는 함수이다. 함수 사용을 위해 &lt;code&gt;build.gradle&lt;/code&gt; 파일에 특정 라이브러리 implementation이 필요하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-groovy&quot;&gt;implementation &amp;quot;androidx.lifecycle:lifecycle-viewmodel-compose:$version&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;참고할 점 - 프로세스 재생성 후 ViewModel에서 복원할 항목&lt;/h4&gt;
&lt;p&gt;위 코드의 &lt;code&gt;ExampleViewModel&lt;/code&gt; 클래스 생성자를 보면 &lt;code&gt;savedState&lt;/code&gt; 이름으로 &lt;code&gt;SavedStateHandle&lt;/code&gt; 타입의 변수를 선언했음을 확인할 수 있다. 흔한 경우는 아니지만 프로세스 재생성 후 보존할 상태가 ViewModel에 포함된다면 &lt;code&gt;SavedSateHandle&lt;/code&gt;을 사용하여 상태를 지속하라고 설명한다. 이에 대해서는 기회가 된다면 다뤄볼 예정이다.&lt;/p&gt;
&lt;h2&gt;ViewModel과 State Holder&lt;/h2&gt;
&lt;p&gt;지금까지 Plain State Holder와 ViewModel의 역할을 보면 사뭇 다르다는 점을 발견할 수 있다. State Holder는 UI element state UI 로직(Behavior)에 대한 책임을 갖고, ViewModel은 Screen State(UI State), 비즈니스 로직에 대한 책임을 갖고 있다. 따라서, 두 가지의 상태 홀더를 화면 수준의 컴포저블에서 모두 다룰 수 있다.&lt;/p&gt;
&lt;p&gt;하단의 코드에는 ViewModel이 UI state를 &lt;code&gt;ExampleUiState&lt;/code&gt;로 다루고 State Holder가 UI element state를 &lt;code&gt;ExampleState&lt;/code&gt;로 다루는 동작을 정의하였다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class ExampleState(
    val lazyListState: LazyListState,
    private val resources: Resources,
    private val expandedItems: List&amp;lt;Item&amp;gt; = emptyList()
) {
    fun isExpandedItem(item: Item): Boolean = TODO()
    /* ... */
}

@Composable
fun rememberExampleState(/* ... */): ExampleState { TODO() }

@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {

    val uiState = viewModel.uiState
    val exampleState = rememberExampleState()

    LazyColumn(state = exampleState.lazyListState) {
        items(uiState.dataToDisplayOnScreen) { item -&amp;gt;
            if (exampleState.isExpandedItem(item)) {
                /* ... */
            }
            /* ... */
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ExampleState&lt;/code&gt; 클래스와 &lt;code&gt;rememberExampleState&lt;/code&gt;함수가 갑자기 등장했으나 해당 클래스는 앞서 작성되었던 &lt;code&gt;MyAppState&lt;/code&gt; 클래스와 &lt;code&gt;rememberMyAppState&lt;/code&gt; 함수와 동일한 구조를 갖는다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/jetpack/compose/state?hl=ko#state-and-composition&quot;&gt;Android Developers - 상태 및 Jetpack Compose&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Android</category>
      <category>Android</category>
      <category>compose</category>
      <category>jetpack compose</category>
      <category>state</category>
      <category>state holder</category>
      <category>UI State</category>
      <category>ViewModel</category>
      <category>안드로이드</category>
      <category>컴포즈</category>
      <author>8iggy</author>
      <guid isPermaLink="true">https://8iggy.tistory.com/275</guid>
      <comments>https://8iggy.tistory.com/275#entry275comment</comments>
      <pubDate>Mon, 26 Dec 2022 18:16:52 +0900</pubDate>
    </item>
    <item>
      <title>Android | Jetpack Compose ViewModel에서의 상태</title>
      <link>https://8iggy.tistory.com/274</link>
      <description>&lt;h2&gt;읽기 전&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.&lt;/li&gt;
&lt;li&gt;개인적으로 실습하면서 배운 점을 정리한 글입니다,&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.android.com/codelabs/jetpack-compose-state#11&quot;&gt;Jetpack Compose의 State 코드랩&lt;/a&gt;과 &lt;a href=&quot;https://developer.android.com/jetpack/compose/state&quot;&gt;Jetpack Compose State 공식문서&lt;/a&gt;의 내용을 정리합니다. Compose에서 ViewModel이 UI 상태를 어떻게 다루는 지 알아봅니다.&lt;/p&gt;
&lt;h2&gt;관련 자료&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://8iggy.tistory.com/273&quot;&gt;Android | Jetpack Compose Observable MutableList&lt;/a&gt;에서 이어지는 포스트입니다.&lt;/p&gt;
&lt;h2&gt;Compose에서의 ViewModel&lt;/h2&gt;
&lt;p&gt;ViewModel은 config change 후에도 유지되므로 컴포지션보다 생명주기가 더 길고 Compose 콘텐츠 호스트(Activity, Fragment, Compose Navigation의 경우 Navigation Graph의 Destination)의 수명주기를 따를 수 있다.&lt;/p&gt;
&lt;h4&gt;주의할 점 - Compose 관련 함수&lt;/h4&gt;
&lt;p&gt;ViewModel은 컴포지션의 일부가 아니기에 메모리 누수가 발생할 수 있다. 따라서, 컴포저블에서 만든 상태를(ex. &lt;code&gt;remember~State&lt;/code&gt; 등) 보유해서는 안된다. 특히, &lt;code&gt;remember&lt;/code&gt; 키워드로 생성된 Compose 상태를 ViewModel 인스턴스가 보관할 경우 config change가 발생하여 컴포지션이 종료된 경우 문제가 될 수 있다. Activity가 재생성되면서 기존 &lt;code&gt;remember&lt;/code&gt;한 상태는 컴포지션에서 삭제되었음에도 ViewModel 인스턴스는 기존 상태에 대한 레퍼런스를 들고 있어 의도와는 다르게 동작할 수 있기 때문이다.&lt;/p&gt;
&lt;h2&gt;목록 이전 및 함수 삭제&lt;/h2&gt;
&lt;p&gt;UI 코드에서 생성했던 상태와 목록을 ViewModel로 이전함과 동시에 비즈니스 로직도 ViewModel로 추출해보자.&lt;/p&gt;
&lt;h3&gt;WellnessViewModel.kt 생성&lt;/h3&gt;
&lt;p&gt;목록을 반환하던 &lt;code&gt;getWellnessTasks()&lt;/code&gt;함수를 &lt;code&gt;WellnessViewModel&lt;/code&gt;로 이동시킨다. 이전에 작성하였듯 &lt;code&gt;toMutableStateList&lt;/code&gt;를 사용하여 내부 &lt;code&gt;_tasks&lt;/code&gt; 변수를 정의하고 외부에 노출시킬 &lt;code&gt;tasks&lt;/code&gt;를 정의한다. 이렇게 작성하면 ViewModel 외부에서 수정할 일이 없기에 책임을 집중시킬 수 있다.&lt;/p&gt;
&lt;p&gt;아이템 제거하던 코드도 ViewModel로 이전하여 내장 remove함수를 호출하는 함수를 작성한다.&lt;/p&gt;
&lt;h4&gt;WellnessViewModel.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class WellnessViewModel : ViewModel() {
    private val _tasks = getWellnessTasks().toMutableStateList()
    val tasks: List&amp;lt;WellnessTask&amp;gt;
      get() = _tasks
    fun remove(item: WellnessTask) {
        _tasks.remove(item)
    }
}

private fun getWellnessTasks() = List(30) { i -&amp;gt; WellnessTask(i, &amp;quot;Task # $i&amp;quot;) }&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;viewModel()&lt;/code&gt; 함수를 호출하여 컴포저블에서 해당 ViewModel에 액세스&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;viewModel()&lt;/code&gt;함수는 추가 라이브러리가 필요하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-groovy&quot;&gt;implementation &amp;quot;androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;wellnessViewModel&lt;/code&gt;을 인스턴스화하고 컴포저블에 적용&lt;/h3&gt;
&lt;h4&gt;WellnessScreen.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WellnessScreen(
  modifier: Modifier = Modifier,
  wellnessViewModel: WellnessViewModel = viewModel()
) {
    Column(modifier = modifier) {
        StatefulCounter()
        WellnessTasksList(
            list = wellnessViewModel.tasks,
            onCloseTask = { 
                task -&amp;gt; wellnessViewModel.remove(task) 
            }
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;viewModel()&lt;/code&gt;은 기존 생성되어 있던 &lt;code&gt;ViewModel&lt;/code&gt;을 반환하거나 지정된 scope에서 새로운 &lt;code&gt;ViewModel&lt;/code&gt;을 생성한다. ViewModel 인스턴스는 scope가 활성화되어 있는 동안 유지된다. 만약 컴포저블이 Activity에서 사용되는 경우 &lt;code&gt;viewmodel()&lt;/code&gt;은 Activity가 finish되거나 프로세스가 종료될 때까지 동일한 인스턴스를 반환한다.&lt;/p&gt;
&lt;p&gt;이로써 UI 표시될 데이터와 비즈니스 로직이 포함된 ViewModel과 화면이 서로 연결되었다. &lt;/p&gt;
&lt;h4&gt;참고할 점 - Composable에서의 ViewModel 사용&lt;/h4&gt;
&lt;p&gt;ViewModel은 Navigation Graph의 Activity나 Fragment 혹은 이런 호스트에서 호출되는 Root 컴포저블 근처에서 사용하는 것이 좋다. 그리고 앞서 주의점으로 설명하였듯이 ViewModel을 다른 컴포저블로 전달하지 않고 필요한 데이터나 로직을 실행하는 함수만 매개변수로 전달함을 잊어셔는 안된다.&lt;/p&gt;
&lt;h2&gt;목록 아이템 선택 상태 이전&lt;/h2&gt;
&lt;p&gt;마지막으로 선택된 상태와 관련된 로직을 ViewModel로 이전해보자. 해당 작업이 완료되면 모든 데이터와 상태가 ViewModel에서 관리되므로 코드가 더 간단해지며 테스트도 용이해진다.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;WellnessTask&lt;/code&gt; 모델 클래스 수정&lt;/h3&gt;
&lt;p&gt;앞서 데이터 클래스로 정의했던 &lt;code&gt;WellnessTask&lt;/code&gt; 클래스에 선택된 상태를 저장할 수 있도록 변수를 선언하고 기본값을 false로 지정한다.&lt;/p&gt;
&lt;h4&gt;WellnessTask.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;data class WellnessTask(val id: Int, val label: String, var checked: Boolean = false)&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ViewModel에 목록 아이템 선택 상태 수정 함수 정의&lt;/h3&gt;
&lt;p&gt;목록 아이템 선택 여부를 저장하도록 수정하였으니 ViewModel에서 선택된 상태를 새로운 값으로 수정하는 함수를 구현한다.&lt;/p&gt;
&lt;h4&gt;WellnessViewModel.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class WellnessViewModel : ViewModel() {
   ...
   fun changeTaskChecked(item: WellnessTask, checked: Boolean) =
       tasks.find { it.id == item.id }?.let { task -&amp;gt;
           task.checked = checked
       }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;새롭게 정의된 함수는 파라미터로 수정할 목록 아이템과 선택 상태를 수정할 값을 받는다. 이후 갖고 있는 아이템 목록에서 일치하는 아이템을 찾고 값을 수정한다.&lt;/p&gt;
&lt;h3&gt;Composable에서 아이템 선택 상태 수정 함수 호출&lt;/h3&gt;
&lt;p&gt;목록 아이템의 선택 상태를 정의하고 선택 상태를 변경하는 함수를 정의했으니 이제 Compose의 목록 아이템에서 함수를 호출하는 코드를 작성해야 한다.&lt;/p&gt;
&lt;h4&gt;WellnessScreen.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WellnessScreen(
    modifier: Modifier = Modifier,
    wellnessViewModel: WellnessViewModel = viewModel()
) {
   Column(modifier = modifier) {
       StatefulCounter()

       WellnessTasksList(
           list = wellnessViewModel.tasks,
           onCheckedTask = { task, checked -&amp;gt;
               wellnessViewModel.changeTaskChecked(task, checked)
           },
           onCloseTask = { task -&amp;gt;
               wellnessViewModel.remove(task)
           }
       )
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;우선 목록 컴포저블의 &lt;code&gt;onCheckedTask&lt;/code&gt; 변수로 ViewModel의 &lt;code&gt;changeTaskChecked&lt;/code&gt; 함수를 호출하는 람다함수를 정의한다.&lt;/p&gt;
&lt;h3&gt;파라미터로 전달한 람다 함수를 목록 아이템에 추가&lt;/h3&gt;
&lt;p&gt;목록 컴포저블에 체크박스 선택 시 동작을 전달했으므로 각 아이템 컴포저블에 필요한 매개변수를 전달하면 된다.&lt;/p&gt;
&lt;h4&gt;WellnessTasksList.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WellnessTasksList(
   list: List&amp;lt;WellnessTask&amp;gt;,
   onCheckedTask: (WellnessTask, Boolean) -&amp;gt; Unit,
   onCloseTask: (WellnessTask) -&amp;gt; Unit,
   modifier: Modifier = Modifier
) {
   LazyColumn(
       modifier = modifier
   ) {
       items(
           items = list,
           key = { task -&amp;gt; task.id }
       ) { task -&amp;gt;
           WellnessTaskItem(
               taskName = task.label,
               checked = task.checked,
               onCheckedChange = { checked -&amp;gt; onCheckedTask(task, checked) },
               onClose = { onCloseTask(task) }
           )
       }
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;목록 컴포저블로부터 전달받은 &lt;code&gt;onCheckedTask&lt;/code&gt; 람다함수에 실제 반영될 파라미터인 &lt;code&gt;task&lt;/code&gt;와 &lt;code&gt;checked&lt;/code&gt; 변수를 입력하여 앞서 정의했던 ViewModel의 함수에 아이템의 정보가 입력되게끔 한다.&lt;/p&gt;
&lt;h3&gt;불필요한 &lt;code&gt;WellnessTaskItem&lt;/code&gt; 코드 정리&lt;/h3&gt;
&lt;p&gt;체크박스 선택 상태를 목록 컴포저블까지 호이스팅했기에 이제 Stateful 컴포저블은 필요치 않다. 따라서, 삭제를 진행한 뒤 Stateless 컴포저블만 남겨둔다.&lt;/p&gt;
&lt;h4&gt;WellnessTaskItem.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WellnessTaskItem(
   taskName: String,
   checked: Boolean,
   onCheckedChange: (Boolean) -&amp;gt; Unit,
   onClose: () -&amp;gt; Unit,
   modifier: Modifier = Modifier
) {
   Row(
       modifier = modifier, verticalAlignment = Alignment.CenterVertically
   ) {
       Text(
           modifier = Modifier
               .weight(1f)
               .padding(start = 16.dp),
           text = taskName
       )
       Checkbox(
           checked = checked,
           onCheckedChange = onCheckedChange
       )
       IconButton(onClick = onClose) {
           Icon(Icons.Filled.Close, contentDescription = &amp;quot;Close&amp;quot;)
       }
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;앱을 실행해보면 아직 체크박스 상태가 제대로 동작하지 않음을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjJ7TC/btrUs8c9lZz/mRqRvpo3DSCne395qeR0F0/img.gif&quot; alt=&quot;Android_Compmose_ViewModel_State_001&quot;&gt;&lt;/p&gt;
&lt;p&gt;의도한 대로 동작하지 않는 이유로는 Compose에서 &lt;code&gt;MutableList&lt;/code&gt;를 위해 추적하는 상태가 아이템 추가 및 삭제와 관련된 변경사항이기 때문이다. 따라서, 삭제는 제대로 작동하지만 추적하도록 지시하지 않았던 아이템의 값 변경사항에 대해서는 인지하지 못하기 때문이다.&lt;/p&gt;
&lt;p&gt;문제를 해결하기 위해 두 가지 방법을 사용할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;데이터 클래스 &lt;code&gt;WellnessTask&lt;/code&gt;를 변경하여 &lt;code&gt;checkedState&lt;/code&gt;의 타입을 &lt;code&gt;Boolean&lt;/code&gt;에서 &lt;code&gt;MutableState&amp;lt;Boolean&amp;gt;&lt;/code&gt;이 되도록 하면 Compose에서 해당 변수 상태의 변경사항을 추적한다.&lt;/li&gt;
&lt;li&gt;변경하려는 항목을 복사하고 목록에서 항목을 삭제한 뒤 변경된 항목을 다시 목록에 추가한다. 그러면 Compose에서는 아이템의 추가 및 삭제와 관련하여 추적하고 있기 때문에 목록 변경사항을 인지한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;두 방법 모두 장단이 존재하며 상황에 따라 취사선택하라고 문서에서 제시한다. 구현된 목록 구조에 따라 요소를 삭제하고 읽는 데 비용이 많이 들 수 있기에 코드랩에서는 잠재적으로 비용이 많이 드는 목록 작업 대신 Compose 직관적인 방법을 택하였다. 따라서 &lt;code&gt;checkedState&lt;/code&gt;를 Observable하게 변경한다.&lt;/p&gt;
&lt;h3&gt;체크박스 선택 여부를 Observable하게 변경&lt;/h3&gt;
&lt;p&gt;변수 객체타입을 바꾸면 다음과 같이 변경할 수 있다.&lt;/p&gt;
&lt;h4&gt;WellnessTask.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;data class WellnessTask(
  val id: Int,
  val label: String,
  val checked: MutableState&amp;lt;Boolean&amp;gt; = mutableStateOf(false)
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Kotlin의 delegated(위임된) 속성을 이용하면 &lt;code&gt;checked&lt;/code&gt; 변수를 좀 더 간단하게 사용할 수 있다.&lt;/p&gt;
&lt;h4&gt;WellnessTask.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class WellnessTask(
    val id: Int,
    val label: String,
    initialChecked: Boolean = false
) {
    var checked by mutableStateOf(initialChecked)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 앱을 실행해보면 체크박스 작동이 정상적으로 이루어짐을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7Wkui/btrUuvSlZ1w/0r9S1kZ0zOYMojukNSJdH0/img.gif&quot; alt=&quot;Android_Compmose_ViewModel_State_002&quot;&gt;&lt;/p&gt;
&lt;h2&gt;참고자료&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/codelabs/jetpack-compose-state?hl=ko#11&quot;&gt;Google Codelab - Jetpack Compose의 상태 #12. ViewModel의 상태&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Android</category>
      <category>Android</category>
      <category>codelab</category>
      <category>compose</category>
      <category>jetpack compose</category>
      <category>kotlin</category>
      <category>state</category>
      <category>ViewModel</category>
      <category>안드로이드</category>
      <category>컴포즈</category>
      <author>8iggy</author>
      <guid isPermaLink="true">https://8iggy.tistory.com/274</guid>
      <comments>https://8iggy.tistory.com/274#entry274comment</comments>
      <pubDate>Sat, 24 Dec 2022 00:18:45 +0900</pubDate>
    </item>
    <item>
      <title>Android | Jetpack Compose Observable MutableList</title>
      <link>https://8iggy.tistory.com/273</link>
      <description>&lt;h2&gt;읽기 전&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.&lt;/li&gt;
&lt;li&gt;개인적으로 실습하면서 배운 점을 정리한 글입니다,&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.android.com/codelabs/jetpack-compose-state#10&quot;&gt;Jetpack Compose의 State 코드랩&lt;/a&gt;과 &lt;a href=&quot;https://developer.android.com/jetpack/compose/state&quot;&gt;Jetpack Compose State 공식문서&lt;/a&gt;의 내용을 정리합니다. Compose에서의 변경 가능한 목록 구현을 위해 어떤 코드를 작성해야 하는지 다룹니다.&lt;/p&gt;
&lt;h2&gt;관련 자료&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://8iggy.tistory.com/272&quot;&gt;Android | Jetpack Compose State 복원, 목록 사용&lt;/a&gt;에서 이어지는 포스트입니다.&lt;/p&gt;
&lt;h2&gt;변경 가능한 목록으로 전환&lt;/h2&gt;
&lt;p&gt;이전 포스트에서 목록을 구현하여 체크박스 기능까지 지원하였다. 하지만, 삭제 기능은 현재 빈 람다를 반환하여 동작하지 않았다. 흔히 변경 가능한 리스트를 생각하면 &lt;code&gt;ArrayList&amp;lt;T&amp;gt;&lt;/code&gt;나 &lt;code&gt;mutableListOf()&lt;/code&gt;를 사용한다. 그러나, Compose에서는 이들을 사용한다고 해서 목록에 변동이 생길 경우 UI의 리컴포지션을 예약한다고 Compose에 알리지 않는다.&lt;/p&gt;
&lt;p&gt;목록 변동을 Compose가 관찰할 수 있도록 &lt;code&gt;MutableList&lt;/code&gt; 인스턴스를 만들어야 한다. 이를 위해 안드로이드에서는 &lt;code&gt;toMutableStateList()&lt;/code&gt; 확장함수를 제공한다. 이를 통해 관찰 가능한 &lt;code&gt;MutableList&lt;/code&gt;를 만들 수 있다.&lt;/p&gt;
&lt;p&gt;Compose UI에서 상태를 관찰하기 위해선 &lt;code&gt;State&amp;lt;T&amp;gt;&lt;/code&gt;로 매핑해야 한다고 설명했었다. 마찬가지로 &lt;code&gt;mutableStateOf&lt;/code&gt; 함수는 &lt;code&gt;MutableState&amp;lt;T&amp;gt;&lt;/code&gt; 타입의 객체를 반환하며 &lt;code&gt;mutableStateListOf&lt;/code&gt;와 &lt;code&gt;toMutableStateList&lt;/code&gt; 함수는 &lt;code&gt;SnapshotStateList&amp;lt;T&amp;gt;&lt;/code&gt; 타입의 객체를 반환하며 해당 타입의 객체를 문서에서  &lt;code&gt;Observable MutableList&lt;/code&gt;라 설명한다. &lt;/p&gt;
&lt;h3&gt;Task 리스트를 Observable MutableList로 전환&lt;/h3&gt;
&lt;h4&gt;WellnessScreen.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   Column(modifier = modifier) {
       StatefulCounter()

       val list = remember { getWellnessTasks().toMutableStateList() }
       WellnessTasksList(list = list, onCloseTask = { task -&amp;gt; list.remove(task) })
   }
}
// copy from WellnessTaskList.kt
private fun getWellnessTasks() = List(30) { i -&amp;gt; WellnessTask(i, &amp;quot;Task # $i&amp;quot;) }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;StatefulCounter&lt;/code&gt; 컴포저블이 상단에 위치하고 list 변수에 &lt;code&gt;remember&lt;/code&gt;를 사용하여 &lt;code&gt;MutableStateList&lt;/code&gt; 타입의 객체를 담았다.&lt;/p&gt;
&lt;h4&gt;주의할 점&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;mutableStateListOf&lt;/code&gt; API를 사용하여 목록을 만들 수 있으나 이를 사용하면 예기치 않은 리컴포지션이 발생할 수 있음을 숙지해야 한다. 잦은 리컴포지션의 발생은 UI 성능 저하를 유발할 수 있기 때문에 민감하게 받아들여야 한다.&lt;/p&gt;
&lt;p&gt;만약 아래와 같이 &lt;code&gt;MutableStateList&lt;/code&gt;를 &lt;code&gt;remember&lt;/code&gt;하는 변수 선언을 한 뒤 해당 상태 객체에 데이터를 담는 코드를 작성했다고 해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// Don&amp;#39;t do this!
val list = remember { mutableStateListOf&amp;lt;WellnessTask&amp;gt;() }
list.addAll(getWellnessTasks())&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 경우 리스트에 아이템이 담길 때마다 상태가 변경된 것으로 인식하기 때문에 매번 리컴포지션이 발생한다. 현재는 초기 값을 설정할 수 있으므로 한꺼번에 구성해서 &lt;code&gt;remember&lt;/code&gt;하는 것이 불필요한 리컴포지션을 줄일 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// Do this instead. Don&amp;#39;t need to copy
val list = remember {
mutableStateListOf&amp;lt;WellnessTask&amp;gt;().apply { addAll(getWellnessTasks()) }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;보통 데이터를 연속적으로 요청하지 않고 일정 부분을 받기에 객체에 담은 뒤 &lt;code&gt;remember&lt;/code&gt;하는 코드를 작성하면 이후에 해당 State 객체에 item을 담는 코드를 작성하지 않아 리컴포지션을 줄였다.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;WellnessTaskList&lt;/code&gt; 컴포저블 함수 수정&lt;/h3&gt;
&lt;p&gt;이전 포스트에서 &lt;code&gt;WellnessTaskList&lt;/code&gt; 컴포저블의 목록을 기본값으로 설정했었다. 이를 삭제하고 외부에서 받게끔 작성한 뒤 목록이 Screen 레벨로 호이스팅되었기에 &lt;code&gt;WellnessTask&lt;/code&gt; 아이템 삭제를 담당하는 &lt;code&gt;onCloseTask&lt;/code&gt; 람다 파라미터를 새롭게 정의한다. 그리고 &lt;code&gt;onCloseTask&lt;/code&gt;를 &lt;code&gt;WellnessTaskItem&lt;/code&gt;에 전달하여 각 item의 우측 삭제 버튼 클릭 시 실행하도록 수정한다.&lt;/p&gt;
&lt;p&gt;추가로 Mutable한 리스트를 Compose에 출력하는 동안 데이터가 변경될 때 추가 조치가 필요하다. 목록 출력을 위해 사용했던 &lt;code&gt;items&lt;/code&gt; 함수에 &lt;code&gt;key&lt;/code&gt; 매개변수를 추가로 작성해야 한다. 만약 &lt;code&gt;key&lt;/code&gt;를 추가하지 않으면 기본값으로 목록에 있는 항목의 위치를 기준으로 키가 지정된다. 따라서, 데이터 변동은 위치를 변경함을 의미하고 이는 기억된 상태를 잃는다는 뜻이다. 항목의 위치를 기준올 설정된 기본 키 값에서 별도의 &lt;code&gt;id&lt;/code&gt; 값으로 지정하면 이 문제를 해결할 수 있다.&lt;/p&gt;
&lt;p&gt;앞서 &lt;code&gt;Wellnesstask&lt;/code&gt; 데이터 클래스를 정의할 때 &lt;code&gt;id&lt;/code&gt; 변수를 정의했었다. 해당 변수를 &lt;code&gt;items&lt;/code&gt;의 &lt;code&gt;key&lt;/code&gt; 변수로 넣어주자.&lt;/p&gt;
&lt;h4&gt;WellnessTaskList.kt - WellnessTaskList&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WellnessTasksList(
   list: List&amp;lt;WellnessTask&amp;gt;,
   onCloseTask: (WellnessTask) -&amp;gt; Unit,
   modifier: Modifier = Modifier
) {
   LazyColumn(modifier = modifier) {
       items(
           items = list,
           key = { task -&amp;gt; task.id }
       ) { task -&amp;gt;
           WellnessTaskItem(taskName = task.label, onClose = { onCloseTask(task) })
       }
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이로써 목록 컴포저블은 외부에서 아이템 삭제 시 실행할 람다 파라미터를 받고 각 아이템에 전달하는 역할을 수행한다.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;onClose&lt;/code&gt;람다 함수를 Stateful 컴포저블로 옮기고 Stateless 컴포저블에서 호출&lt;/h3&gt;
&lt;h4&gt;WellnessTaskItem.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WellnessTaskItem(
   taskName: String, onClose: () -&amp;gt; Unit, modifier: Modifier = Modifier
) {
   var checkedState by rememberSaveable { mutableStateOf(false) }

   WellnessTaskItem(
       taskName = taskName,
       checked = checkedState,
       onCheckedChange = { newValue -&amp;gt; checkedState = newValue },
       onClose = onClose,
       modifier = modifier,
   )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;WellnessScreen&lt;/code&gt;에서 삭제 시 동작할 람다 함수를 &lt;code&gt;onClose&lt;/code&gt;로 전달하였고 목록 컴포저블을 거쳐 Stateless 컴포저블까지 전달 후 실행하게끔 작성하였다. 이제 목록에서 아이템을 삭제할 수 있다.&lt;/p&gt;
&lt;p&gt;각 목록 아이템의 우측 X를 클릭하면 이벤트가 상태를 소유한 목록까지 이동하므로 목록에서 항목이 삭제되며 Compose는 화면을 재구성한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Q7Hno/btrTZy4bsFd/6fOXL3IUsFBAjLxMyEQV9k/img.png&quot; alt=&quot;Android_Compmose_MutableList_001&quot;&gt;&lt;/p&gt;
&lt;p&gt;그림에 따라 상태는 하단으로 전파되고 아이템을 삭제하겠다는 이벤트는 하단 컴포저블로부터 상단까지 상승하는 단방향 흐름을 볼 수 있다.&lt;/p&gt;
&lt;h2&gt;목록 사용 시 주의점&lt;/h2&gt;
&lt;p&gt;앞서 코드에서 &lt;code&gt;remember&lt;/code&gt;를 사용하여 목록을 저장한 바 있다. 그렇다면 컴포지션 종료 후 복원 간 유지를 위해 &lt;code&gt;rememberSaveable&lt;/code&gt;을 사용하여 보관할 수 있을까에 대한 의문이 든다. 바로 적용해보면 런타임 에러가 발생하며 이는 &lt;a href=&quot;https://8iggy.tistory.com/272&quot;&gt;Android | Jetpack Compose State 복원, 목록 사용&lt;/a&gt;에 작성하였듯이 맞춤 Saver를 제공하지 않았기 때문이다. 그리고  &lt;code&gt;rememberSaveable&lt;/code&gt;은 &lt;code&gt;Bundle&lt;/code&gt;객체에 데이터를 보관 후 복원 절차를 수행한다. &lt;code&gt;Bundle&lt;/code&gt; 객체에는 가급적 텍스트, 부울 등 기본 자료형 저장을 권장하며 용량 제한은 50KB정도이다. 따라서, 직렬화/역직렬화가 필요한 데이터 구조나 대량의 데이터를 저장하는 데 &lt;code&gt;rememberSaveable&lt;/code&gt;을 사용하는 것은 부적합하다.&lt;/p&gt;
&lt;p&gt;따라서 목록 등 대량의 데이터 저장과 앱 상태 홀더의 역할을 수행하는  &lt;code&gt;ViewModel&lt;/code&gt;을 도입해야 한다.&lt;/p&gt;
&lt;h2&gt;참고자료&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/codelabs/jetpack-compose-state?hl=ko#10&quot;&gt;Google Codelab - Jetpack Compose의 상태 #11. 관찰 가능한 MutableList&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://8iggy.tistory.com/272&quot;&gt;Android | Jetpack Compose State 복원, 목록 사용&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Android</category>
      <category>Android</category>
      <category>codelab</category>
      <category>compose</category>
      <category>jetpack compose</category>
      <category>kotlin</category>
      <category>Observable MutableList</category>
      <category>안드로이드</category>
      <category>컴포즈</category>
      <author>8iggy</author>
      <guid isPermaLink="true">https://8iggy.tistory.com/273</guid>
      <comments>https://8iggy.tistory.com/273#entry273comment</comments>
      <pubDate>Mon, 19 Dec 2022 18:40:38 +0900</pubDate>
    </item>
    <item>
      <title>Android | Jetpack Compose State 복원, 목록 사용</title>
      <link>https://8iggy.tistory.com/272</link>
      <description>&lt;h2&gt;읽기 전&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.&lt;/li&gt;
&lt;li&gt;개인적으로 실습하면서 배운 점을 정리한 글입니다,&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.android.com/codelabs/jetpack-compose-state#7&quot;&gt;Jetpack Compose의 State 코드랩&lt;/a&gt;과 &lt;a href=&quot;https://developer.android.com/jetpack/compose/state&quot;&gt;Jetpack Compose State 공식문서&lt;/a&gt;의 내용을 정리합니다. Compose에서 컴포지션 종료와 재생성 간 상태를 어떻게 보관하는지를 Compose에서의 목록 사용과 연계해서 알아봅니다.&lt;/p&gt;
&lt;h2&gt;관련 자료&lt;/h2&gt;
&lt;p&gt;Compose State 복원은 &lt;a href=&quot;https://8iggy.tistory.com/270&quot;&gt;Android | Jetpack Compose Remember State&lt;/a&gt;에서 이어집니다.&lt;/p&gt;
&lt;p&gt;Compose 목록 사용은 &lt;a href=&quot;https://8iggy.tistory.com/271&quot;&gt;Android | Jetpack Compose State Hoisting&lt;/a&gt;에서 이어집니다.&lt;/p&gt;
&lt;h2&gt;액티비티 재구성에 따른 상태 변화&lt;/h2&gt;
&lt;p&gt;Compose 상태가 바뀌면 리컴포지션이 발생하면서 변화된 상태를 반영했었다. 그리고 변경된 상태를 리컴포지션에도 유지하기 위해 &lt;code&gt;remember&lt;/code&gt; API를 사용하여 저장한다. 그렇다면, 화면 회전이나 언어 변경, 라이트/다크 모드 전환과 같이 Config Change로 인해 실행 중인 Activity가 Android 시스템에 의해 닫혔다가 다시 생성되는 경우엔 어떻게 될까?&lt;/p&gt;
&lt;p&gt;앱을 실행한 뒤 &lt;code&gt;Add one&lt;/code&gt; 버튼을 눌러 &lt;code&gt;count&lt;/code&gt; 변수를 0에서 1로 바꾼 뒤 기기를 회전시켜 화면을 세로모드에서 가로모드로 바꿔보자.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJY5B5/btrTzlpjySq/EqRUM9p09GZkxq624rFGhk/img.gif&quot; alt=&quot;Android_Compose_State_Restore_001&quot;&gt;&lt;/p&gt;
&lt;p&gt;Activity는 Config Change(구성 변경, 이 경우는 방향) 후 재생성되므로 저장된 상태를 삭제된다. 따라서, &lt;code&gt;count&lt;/code&gt; 변수는 다시 초기값인 0으로 되돌아가며 카운터가 사라진다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;remember&lt;/code&gt;를 사용하면 리컴포지션 간 상태를 유지할 수 있지만 Config Change로 인한 &lt;strong&gt;구성 변경 간에는 유지되지 않는다.&lt;/strong&gt; 이를 위한 새로운 &lt;code&gt;rememberSaveable&lt;/code&gt;이라는 API를 사용해야 한다.&lt;/p&gt;
&lt;h2&gt;상태 복원에 쓰이는 &lt;code&gt;rememberSaveable&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;rememberSaveable&lt;/code&gt;은 &lt;code&gt;Bundle&lt;/code&gt;에 저장할 수 있는 모든 값을 자동으로 저장한다. 다른 값의 경우 커스텀 Saver 객체를 전달할 수 있다.&lt;/p&gt;
&lt;h3&gt;Bundle에 저장할 수 없는 항목 저장&lt;/h3&gt;
&lt;h4&gt;Parcelize 주석 추가&lt;/h4&gt;
&lt;p&gt;가장 간단한 방법으로 &lt;code&gt;@Parcelize&lt;/code&gt; 주석을 추가하는 것이다. 그러면 객체가 parceable이 되며 번들로 제공된다. 다음의 코드는 City 데이터 클래스를 parceable로 만들어 상태에 저장한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Parcelize
data class City(val name: String, val country: String) : Parceable

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable {
        mutableStateOf(City(&amp;quot;Madrid&amp;quot;, &amp;quot;Spain&amp;quot;))
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;MapSaver API 사용&lt;/h4&gt;
&lt;p&gt;key-value 집합으로 객체를 변환하는 규칙을 정의하여 Bundle에 저장할 수 있는 값 집합으로 만들 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;data class City(val name: String, val country: String)

val CitySaver = run {
    val nameKey = &amp;quot;Name&amp;quot;
    val countryKey = &amp;quot;Country&amp;quot;
    mapSaver(
        save = { mapOf(nameKey to it.name, countryKey to it.country) },
        restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
}

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City(&amp;quot;Madrid&amp;quot;, &amp;quot;Spain&amp;quot;))
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;ListSaver API 사용&lt;/h4&gt;
&lt;p&gt;list 형태로도 stateSaver를 만들 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;data class City(val name: String, val country: String)

val CitySaver = listSaver&amp;lt;City, Any&amp;gt;(
    save = { listOf(it.name, it.country) },
    restore = { City(it[0] as String, it[1] as String) }
)

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City(&amp;quot;Madrid&amp;quot;, &amp;quot;Spain&amp;quot;))
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;rememberSaveable&lt;/code&gt; 적용&lt;/h3&gt;
&lt;p&gt;기존 &lt;code&gt;WaterCounter&lt;/code&gt;에서 &lt;code&gt;remember&lt;/code&gt;를 &lt;code&gt;rememberSaveable&lt;/code&gt;로 바꿔보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       //var count by remember { mutableStateOf(0) }
       var count by rememberSaveable { mutableStateOf(0) }

       if (count &amp;gt; 0) {
           Text(&amp;quot;You&amp;#39;ve had $count glasses.&amp;quot;)
       }
       Button(
           onClick = { count++ },
           enabled = count &amp;lt; 10,
           Modifier.padding(top = 8.dp)) {
           Text(&amp;quot;Add one&amp;quot;)
       }
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Z8LGC/btrTyECJOq4/YjJadUkIRYMpEQg1DFaWCk/img.gif&quot; alt=&quot;Android_Compose_State_Restore_002&quot;&gt;&lt;/p&gt;
&lt;p&gt;결국 앱의 UX 상황에 맞춰서 &lt;code&gt;remember&lt;/code&gt;를 사용할 것인지 &lt;code&gt;rememberSaveable&lt;/code&gt;을 사용할 것인지 결정해야 한다.&lt;/p&gt;
&lt;h2&gt;Compose에서의 목록 사용&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://8iggy.tistory.com/271&quot;&gt;Android | Jetpack Compose State Hoisting&lt;/a&gt;에 이어 목록 사용을 위해 앱에 두 가지 작업을 추가한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;작업을 완료로 표시하기 위해 목록 항목 선택 기능&lt;/li&gt;
&lt;li&gt;완료할 필요 없는 작업을 목록에서 삭제 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;목록 item 정의&lt;/h3&gt;
&lt;p&gt;목록에 보여줄 아이템을 정의해야 한다. 이전에 &lt;a href=&quot;https://8iggy.tistory.com/270&quot;&gt;Android | Jetpack Compose Remember State&lt;/a&gt;에서 정의했던 &lt;code&gt;WellnessTaskItem&lt;/code&gt;을 재사용하여 체크박스와 닫기 버튼을 추가해주자.&lt;/p&gt;
&lt;h4&gt;WellnessTaskItem.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WellnessTaskItem(
    taskName: String,
    checked: Boolean,
    onCheckedChange: (Boolean) -&amp;gt; Unit,
    onClose: () -&amp;gt; Unit,
    modifier: Modifier = Modifier
) {
    Row(
        modifier = modifier, verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 16.dp),
            text = taskName
        )
        Checkbox(
            checked = checked,
            onCheckedChange = onCheckedChange
        )
        IconButton(onClick = onClose) {
            Icon(Icons.Filled.Close, contentDescription = &amp;quot;Close&amp;quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Checkbox&lt;/code&gt; 컴포저블과 &lt;code&gt;IconButton&lt;/code&gt; 컴포저블을 추가하여 체크와 닫기 기능을 지원할 수 있도록 UI를 구성하였고 컴포저블 파라미터로 &lt;code&gt;checked&lt;/code&gt;, &lt;code&gt;onCheckedChange&lt;/code&gt;, &lt;code&gt;onClose&lt;/code&gt;를 입력하여 &lt;code&gt;Stateless&lt;/code&gt; 컴포저블이 될 수 있도록 한다.&lt;/p&gt;
&lt;h3&gt;목록 item의 상태 정의&lt;/h3&gt;
&lt;p&gt;목록 item을 &lt;strong&gt;stateless&lt;/strong&gt;하게 정의했으므로 외부에서 상태를 주입해야 한다. &lt;code&gt;checkedStated&lt;/code&gt;를 정의하는 동일한 이름의 &lt;code&gt;Stateful&lt;/code&gt; 컴포저블을 우선 정의해둔다.&lt;/p&gt;
&lt;h4&gt;WellnessTaskItem.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WellnessTaskItem(taskName: String, modifier: Modifier = Modifier) {
   var checkedState by remember { mutableStateOf(false) }

   WellnessTaskItem(
       taskName = taskName,
       checked = checkedState,
       onCheckedChange = { newValue -&amp;gt; checkedState = newValue },
       onClose = {}, // we will implement this later!
       modifier = modifier,
   )
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Task의 내용을 담을 데이터 클래스 정의&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;WellnessTask.kt&lt;/code&gt;파일을 만들어 ID와 라벨이 포함된 &lt;code&gt;Task&lt;/code&gt; 모델을 데이터 클래스로 정의한다.&lt;/p&gt;
&lt;h4&gt;WellnessTask.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;data class WellnessTask(val id: Int, val label: String)&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Task 목록을 반환할 함수 추가 정의&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;WellnessTasksList.kt&lt;/code&gt;라는 파일을 만들어 더미 데이터를 반환하는 함수를 정의한다.&lt;/p&gt;
&lt;h4&gt;WellnessTasksList.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;private fun getWellnessTasks() = List(30) { i -&amp;gt;
    WellnessTask(i, &amp;quot;Task # $i&amp;quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가짜 데이터를 반환하고 있지만 실제 서비스에서는 데이터 영역에서 조회하거나 네트워크를 통해 데이터를 받아서 전달한다.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;WellnessTasksList.kt&lt;/code&gt;에서 목록 정의&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;WellnessTasksList.kt&lt;/code&gt;에서 목록을 만드는 컴포저블 함수를 정의한다. &lt;code&gt;LazyColumn&lt;/code&gt;으로 목록을 구현하고 삽입할 item으로 앞서 정의했던 &lt;code&gt;WellnessTaskItem&lt;/code&gt;을 활용하면 되겠다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WellnessTasksList(
    modifier: Modifier = Modifier,
    list: List&amp;lt;WellnessTask&amp;gt; = remember { getWellnessTasks() }
) {
    LazyColumn(
        modifier = modifier
    ) {
        items(list) { task -&amp;gt;
            WellnessTaskItem(taskName = task.label)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;목록을 &lt;code&gt;WellnessScreen&lt;/code&gt;에 추가&lt;/h3&gt;
&lt;p&gt;Task들을 보여줄 목록을 정의했으므로 화면에 출력하기 위해 &lt;code&gt;WellnessTaskList&lt;/code&gt; 컴포저블을 &lt;code&gt;WellnessScreen&lt;/code&gt;에 추가한다.&lt;/p&gt;
&lt;h4&gt;WellnessScreen.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   Column(modifier = modifier) {
       StatefulCounter()
       WellnessTasksList()
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 앱을 실행해보면 체크박스 선택이 가능해진다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Jz7hQ/btrTy33wiIo/zBvYDVbt1pxGJ5OpgxzXR0/img.gif&quot; alt=&quot;Android_Compose_State_Restore_003&quot;&gt;&lt;/p&gt;
&lt;p&gt;다만 삭제하는 로직은 빈 람다함수를 파라미터로 넣었기 때문에 아직 동작하지 않는다. 그리고 &lt;code&gt;WellnessTaskItem&lt;/code&gt;의 상태로 &lt;code&gt;remember&lt;/code&gt; API를 사용했었는데 이와 관련해서 어떤 부작용이 있는지 확인해보자.&lt;/p&gt;
&lt;h2&gt;LazyList에서의 상태 복원&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;checkedState&lt;/code&gt;는 각 &lt;code&gt;WellnessTaskItem&lt;/code&gt; 컴포저블에 독립적으로 속한다. &lt;code&gt;checkedState&lt;/code&gt;가 변경되면 &lt;code&gt;WellnessTaskItem&lt;/code&gt;의 해당 인스턴스만 재구성되며 &lt;code&gt;LazyColumn&lt;/code&gt;의 모든 &lt;code&gt;WellnessTaskItem&lt;/code&gt; 인스턴스에 리컴포지션이 트리거되진 않는다. 현대 &lt;code&gt;WellnessTaskItem&lt;/code&gt; 코드를 보면 &lt;code&gt;remember&lt;/code&gt; API를 사용하여 &lt;code&gt;checkedState&lt;/code&gt; 변수를 정의하였다. 앱을 켜고 다음의 절차를 따라해보면 의도와는 다르게 동작함을 확인할 수 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;해당 목록 상단의 item 체크박스를 선택&lt;/li&gt;
&lt;li&gt;선택한 item들이 화면 밖으로 나가도록 스크롤&lt;/li&gt;
&lt;li&gt;앞서 선택한 item이 화면 안으로 들어오도록 스크롤&lt;/li&gt;
&lt;li&gt;선택했던 item들의 체크박스가 해제되어 있음&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;remember의 문제점으로 컴포지션에서 호출되지 않으면 기억된 상태를 삭제한다는 점이 있었다. &lt;code&gt;LazyColumn&lt;/code&gt; 컴포저블의 경우 스크롤하면서 항목을 지나치면 해당 item에 해당 컴포지션을 완전히 종료하기 때문에 UI 컴포지션에서 삭제되어 더 이상 항목이 표시되지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cV6GdJ/btrTz0ruDnF/bulLP3WKAxK1ku7Fu717Hk/img.gif&quot; alt=&quot;Android_Compose_State_Restore_004&quot;&gt;&lt;/p&gt;
&lt;p&gt;이 문제를 해결하기 위해선 `rememberSaveable을 사용하면 된다. 저장된 값은 저장된 인스턴스 상태 메커니즘을 통해 Activity나 프로세스 재생성 시에도 유지되기 때문에 컴포지션이 종료될 때도 유지된다.&lt;/p&gt;
&lt;p&gt;Statefull하게 정의한 &lt;code&gt;WellnessTaskItem&lt;/code&gt;의 코드에서 &lt;code&gt;remember&lt;/code&gt;을 &lt;code&gt;rememberSaveable&lt;/code&gt;로 바꿔보자.&lt;/p&gt;
&lt;h4&gt;WellnessTaskItem.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WellnessTaskItem(taskName: String, modifier: Modifier = Modifier) {
   var checkedState by rememberSaveable { mutableStateOf(false) }

   WellnessTaskItem(
       taskName = taskName,
       checked = checkedState,
       onCheckedChange = { newValue -&amp;gt; checkedState = newValue },
       onClose = {}, // we will implement this later!
       modifier = modifier,
   )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;코드를 수정한 뒤 앱을 실행해보면 체크 상태가 유지되고 있음을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1gjJV/btrTtk6R5lt/tUZ2lPyurxpKDKk1N16GKK/img.gif&quot; alt=&quot;Android_Compose_State_Restore_005&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Compose의 일반적 패턴&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;LazyColumn&lt;/code&gt;의 내부 구현을 보면 &lt;code&gt;state&lt;/code&gt;를 다음과 같이 정의해두고 있다.&lt;/p&gt;
&lt;h4&gt;LazyDsl.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun LazyColumn(
    modifier: Modifier = Modifier,
    state: LazyListState = rememberLazyListState(),
    ...
) {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;내부 상태 정의를 위해 &lt;code&gt;LazyListState&lt;/code&gt; 타입을 반환하는 &lt;code&gt;rememberLazyListState()&lt;/code&gt;함수를 호출함을 확인할 수 있다. &lt;code&gt;rememberLazyListState&lt;/code&gt; 함수의 내부 구현을 더 들어가보자.&lt;/p&gt;
&lt;h4&gt;LazyListSTate.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun rememberLazyListState(
    initialFirstVisibleItemIndex: Int = 0,
    initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
    return rememberSaveable(saver = LazyListState.Saver) {
        LazyListState(
            initialFirstVisibleItemIndex,
            initialFirstVisibleItemScrollOffset
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;rememberLazyListState&lt;/code&gt; 컴포저블 함수는 &lt;code&gt;rememberSaveable&lt;/code&gt;을 사용하여 목록의 초기 상태를 구현한다. 그렇기 때문에 Activity가 재생성 되더라도 스크롤 상태는 어떠한 조치를 취하지 않아도 유지된다.&lt;/p&gt;
&lt;h2&gt;참고자료&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/codelabs/jetpack-compose-state?hl=ko#7&quot;&gt;Google Codelab - Jetpack Compose의 상태 #8. Compose에서 상태 복원&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/codelabs/jetpack-compose-state?hl=ko#9&quot;&gt;Google Codelab - Jetpack Compose의 상태 #10. 목록 사용&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/jetpack/compose/state&quot;&gt;Android Developers - 상태 및 Jetpack Compose&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Android</category>
      <category>Android</category>
      <category>codelab</category>
      <category>compose</category>
      <category>jetpack compose</category>
      <category>LazyColumn</category>
      <category>rememberSaveable</category>
      <category>state</category>
      <category>안드로이드</category>
      <category>컴포즈</category>
      <category>코드랩</category>
      <author>8iggy</author>
      <guid isPermaLink="true">https://8iggy.tistory.com/272</guid>
      <comments>https://8iggy.tistory.com/272#entry272comment</comments>
      <pubDate>Tue, 13 Dec 2022 14:48:41 +0900</pubDate>
    </item>
    <item>
      <title>Android | Jetpack Compose State Hoisting</title>
      <link>https://8iggy.tistory.com/271</link>
      <description>&lt;h2&gt;읽기 전&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.&lt;/li&gt;
&lt;li&gt;개인적으로 실습하면서 배운 점을 정리한 글입니다,&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.android.com/codelabs/jetpack-compose-state#8&quot;&gt;Jetpack Compose의 State 코드랩&lt;/a&gt;과 &lt;a href=&quot;https://developer.android.com/jetpack/compose/state&quot;&gt;Jetpack Compose State 공식문서&lt;/a&gt;의 내용을 정리합니다. Stateful과 Stateless 컴포저블의 차이를 확인하고 상태를 상단으로 끌어올리는 &lt;code&gt;Hoisting&lt;/code&gt;에 대해 알아봅니다.&lt;/p&gt;
&lt;h2&gt;관련 자료&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://8iggy.tistory.com/270&quot;&gt;Android | Jetpack Compose Remember State&lt;/a&gt;에서 이어지는 포스트입니다.&lt;/p&gt;
&lt;h2&gt;Hoisting(호이스팅)이란?&lt;/h2&gt;
&lt;p&gt;주로 제시하는 설명은 &amp;quot;변수의 선언과 초기화를 분리한 후 선언 부분만 코드의 최상단으로 옮기는 행위&amp;quot;라고 한다. 왜 이러한 과정을 수행하는지 알아보자.&lt;/p&gt;
&lt;h2&gt;상태 호이스팅&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;remember&lt;/code&gt;를 사용하여 객체를 저장하는 컴포저블은 내부 상태를 포함하고 있어 &lt;code&gt;Stateful&lt;/code&gt;하다고 볼 수 있다. 이는 호출하는 곳에서 상태를 제어할 필요가 없고 상태를 직접 관리하지 않아도 내부적으로 상태를 사용할 수 있는 상황에서 유용하다. 그러나, 내부 상태를 갖는 컴포저블은 &lt;strong&gt;재사용 가능성이 적고 테스트하기가 더 어렵다&lt;/strong&gt;. 그래서 상태를 바깥 호출하는 위치에서 선언함으로써 호출된 컴포저블 내부에서는 주입된 상태를 다루는 방식 즉, &lt;code&gt;Stateless&lt;/code&gt;한 컴포저블을 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;Compose에서의 상태 호이스팅은 컴포저블을 &lt;code&gt;Stateful&lt;/code&gt;에서 &lt;code&gt;Stateless&lt;/code&gt;로 만들기 위해 상태를 컴포저블의 호출자로 옮기는 패턴을 의미한다. 일반적으로 상태 관련된 변수를 다음 두 개의 매개변수로 바꿈으로써 이루어진다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;value: T&lt;/code&gt; - 컴포저블이 다룰 상태 값&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onValueChange: (T) -&amp;gt; Unit&lt;/code&gt; - 상태의 값을 변경하도록 요청하는 이벤트이며 T는 컴포저블에 제안할 새로운 값이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이렇듯 상태가 내려가고(UI에 표시할 상태가 컴포저블로 내려감) 이벤트가 올라가는(하위 컴포저블에서 발생된 이벤트가 상위 컴포저블로 올라감) 패턴을 단방향 데이터 흐름(UDF)라고 하고 상태 호이스팅은 해당 아키텍쳐를 Compose에서 구현하는 방법이라고 설명한다.&lt;/p&gt;
&lt;p&gt;상태를 호이스팅함으로써 갖는 속성으론 다음이 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;단일 소스 저장소 : 상태를 복제하는 대신 옮겼기에 소스 저장소가 하나만 존재하여 버그 방지에 도움&lt;/li&gt;
&lt;li&gt;공유 가능 : 호이스팅한 상태를 여러 컴포저블과 공유할 수 있음&lt;/li&gt;
&lt;li&gt;캡슐화됨 : &lt;code&gt;Stateful&lt;/code&gt;한 컴포저블만 상태를 수정할 수 있음&lt;/li&gt;
&lt;li&gt;가로채기 가능 : &lt;code&gt;Stateless&lt;/code&gt; 컴포저블의 호출자는 상태를 변경하기 전 이벤트를 무시할 것인지 반영할 것인지 결정가능&lt;/li&gt;
&lt;li&gt;분리됨 : &lt;code&gt;Stateless&lt;/code&gt; 컴포저블에서 다루는 상태를 어디에나 저장할 수 있게 된다. (ex. ViewModel)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;code&gt;Stateful&lt;/code&gt;과 &lt;code&gt;Stateless&lt;/code&gt; 비교&lt;/h2&gt;
&lt;p&gt;컴포저블 함수에서 모든 상태를 외부로 추출하여 만들어진 컴포저블 함수는 &lt;code&gt;Stateless&lt;/code&gt;하다고 볼 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Stateless&lt;/code&gt; 컴포저블 : 상태를 소유하지 않는 컴포저블로 새로운 상태를 갖거나 정의하거나 수정하지 않음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Statefull&lt;/code&gt; 컴포저블 : 시간이 지남에 따라 변하는 상태를 갖는 컴포저블&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;모든 세부 컴포저블이 &lt;code&gt;Stateless&lt;/code&gt;할 수는 없지만 가급적 적게 상태를 갖게끔 설계하여 상태를 호이스팅함이 바람직하다고 한다.&lt;/p&gt;
&lt;p&gt;기존 &lt;code&gt;WaterCounter&lt;/code&gt; 컴포저블을 리팩토링하여 &lt;code&gt;StatefulCounter&lt;/code&gt;와 &lt;code&gt;StatelessCounter&lt;/code&gt;로 구분해보자.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;StatelessCounter&lt;/code&gt; 컴포저블은 &lt;code&gt;count&lt;/code&gt;를 표시하고 &lt;code&gt;count&lt;/code&gt;를 늘릴 때의 함수를 호출한다. 앞서 작성하였듯이 &lt;code&gt;value&lt;/code&gt;와 &lt;code&gt;onValueChange&lt;/code&gt;를 정의해야 하므로 상태 변수인 &lt;code&gt;count&lt;/code&gt;와 그 값을 증가시키는 &lt;code&gt;onIncrement&lt;/code&gt; 람다 함수를 매개변수로 받도록 선언한다.&lt;/p&gt;
&lt;h4&gt;WaterCounter.kt - StatelessCounter&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun StatelessCounter(count: Int, onIncrement: () -&amp;gt; Unit, modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       if (count &amp;gt; 0) {
           Text(&amp;quot;You&amp;#39;ve had $count glasses.&amp;quot;)
       }
       Button(onClick = onIncrement, Modifier.padding(top = 8.dp), enabled = count &amp;lt; 10) {
           Text(&amp;quot;Add one&amp;quot;)
       }
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;StatefulCounter&lt;/code&gt; 컴포저블은 상태를 소유하므로 &lt;code&gt;count&lt;/code&gt;의 상태를 갖고 &lt;code&gt;StatelessCounter&lt;/code&gt;컴포저블을 호출할 때 해당 상태를 매개변수로 입력하며 수정합니다.&lt;/p&gt;
&lt;h4&gt;WaterCounter.kt - StatefulCounter&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun StatefulCounter(modifier: Modifier = Modifier) {
   var count by rememberSaveable { mutableStateOf(0) }
   StatelessCounter(count, { count++ }, modifier)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 기존 &lt;code&gt;WaterCounter&lt;/code&gt;를 두 개의 컴포저블인 &lt;code&gt;StatefulCounter&lt;/code&gt;와 &lt;code&gt;StatelessCounter&lt;/code&gt;로 분리함으로써 상태를 성공적으로 호이스팅하였다. 이제 &lt;code&gt;WaterCounter&lt;/code&gt;를 호출하던 코드를 수정하면 되겠다.&lt;/p&gt;
&lt;h4&gt;WellnessScreen.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   StatefulCounter(modifier)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;호이스팅 시 주의할 점&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;상태는 적어도 해당 상태를 사용하는 모든 커포저블의 가장 낮은 공통 상위 Element로 끌어올려야 한다. (읽기)&lt;/li&gt;
&lt;li&gt;상태는 최소한 변경될 수 있는 가장 높은 수준으로 끌어올려야 한다. (쓰기)&lt;/li&gt;
&lt;li&gt;두 상태가 동일한 이벤트에 대한 응답으로 변경되는 경우 두 상태는 동일한 수준으로 끌어올려야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;위 규칙에 따라 상태를 적절한 수준으로 끌어올리지 않으면 단방향 데이터 흐름을 구현하지 못할 수 있다.&lt;/p&gt;
&lt;p&gt;현재 상태를 호이스팅하느라 두 개의 컴포저블로 분리하였는데, 이 경우 몇가지 이점이 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Stateless&lt;/code&gt; 컴포저블 재사용&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;만약 마신 물과 함께 주스의 잔 개수도 함께 계산하려면 &lt;code&gt;WaterCounter&lt;/code&gt;와 유사한 &lt;code&gt;JuiceCounter&lt;/code&gt;를 생성하지 않고도 각각 &lt;code&gt;waterCount&lt;/code&gt;와 &lt;code&gt;JuiceCount&lt;/code&gt; 변수를 기억하고 &lt;code&gt;StatlessCounter&lt;/code&gt; 컴포저블 함수를 사용하여 두 개의 독립 상태를 출력할 수 있다.&lt;/p&gt;
&lt;h4&gt;WaterCounter.kt - StatefulCounter&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun StatefulCounter() {
    var waterCount by remember { mutableStateOf(0) }
    var juiceCount by remember { mutableStateOf(0) }

    StatelessCounter(waterCount, { waterCount++ })
    StatelessCounter(juiceCount, { juiceCount++ })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;앱을 실행하면 컴포지션 구조는 아래 그림과 같이 &lt;code&gt;waterCount&lt;/code&gt;를 표시하는 &lt;code&gt;StatelessCounter&lt;/code&gt;와 &lt;code&gt;juiceCount&lt;/code&gt;를 표시하는 &lt;code&gt;StatelessCounter&lt;/code&gt; 두 개의 컴포저블이 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SQ7KH/btrTa5tBVkl/v4F08Rt02K5XSQevmuCGl1/img.png&quot; alt=&quot;Android_Compose_State_Hoisting_001&quot;&gt;&lt;/p&gt;
&lt;p&gt;만약 &lt;code&gt;juiceCount&lt;/code&gt;가 수정되면 &lt;code&gt;StatefulCounter&lt;/code&gt;가 재구성되며 리컴포지션 중에 Compose가 &lt;code&gt;juiceCount&lt;/code&gt;만 변경됨을 인지하고 해당 상태를 사용하는 컴포저블 함수에 대해서만 리컴포지션을 트리거한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2fZEm/btrTa58d1JZ/019sUMtf0z66h3urA5SK8K/img.png&quot; alt=&quot;Android_Compose_State_Hoisting_002&quot;&gt;&lt;/p&gt;
&lt;p&gt;사용자가 &lt;code&gt;juiceCount&lt;/code&gt;를 늘리면 &lt;code&gt;StatefulCounter&lt;/code&gt;가 재구성되고 &lt;code&gt;juiceCount&lt;/code&gt;를 읽는 &lt;code&gt;StatelessCounter&lt;/code&gt;도 재구성된다. 그러나, &lt;code&gt;waterCount&lt;/code&gt;를 읽는 &lt;code&gt;StatelessCounter&lt;/code&gt;는 재구성되지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I7COZ/btrS7uBrPZX/fIY2nRXzEk53vSQt2YSqk0/img.png&quot; alt=&quot;Android_Compose_State_Hoisting_003&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;code&gt;Stateful&lt;/code&gt; 컴포저블 함수는 여러 컴포저블에 동일한 상태를 제공할 수 있음&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun StatefulCounter() {
   var count by remember { mutableStateOf(0) }

   StatelessCounter(count, { count++ })
   AnotherStatelessMethod(count, { count *= 2 })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;임시로 두 컴포저블이 하나의 상태롤 바라보게끔 작성하였다. 이 경우 &lt;code&gt;count&lt;/code&gt; 변수가 &lt;code&gt;StatelessCounter&lt;/code&gt;나 &lt;code&gt;AnotherStatelessMethod&lt;/code&gt; 함수에 의해 변경되면 두 컴포저블이 하나의 공통 상태를 관찰하고 있기 때문에 모든 항목이 재구성된다.&lt;/p&gt;
&lt;p&gt;호이스팅된 상태는 공유할 수 있으므로 리컴포지션을 방지하고 재사용성을 높이기 위해 컴포저블에 필요한 상태만 전달함이 좋다. 리컴포지션이 자주 발생하면 앱의 성능에 안좋은 영향이 발생할 수 있기 때문에 필요한 경우에만 매개변수로 상태를 입력해야 한다.&lt;/p&gt;
&lt;h2&gt;참고자료&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/codelabs/jetpack-compose-state#8&quot;&gt;Google Codelab - Jetpack Compose의 상태 #9 상태 호이스팅&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/jetpack/compose/state&quot;&gt;Android Developers - 상태 및 Jetpack Compose&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Android</category>
      <category>Android</category>
      <category>codelab</category>
      <category>compose</category>
      <category>hoisting</category>
      <category>jetpack compose</category>
      <category>state</category>
      <category>State Hoisting</category>
      <category>상태</category>
      <category>안드로이드</category>
      <category>컴포즈</category>
      <category>호이스팅</category>
      <author>8iggy</author>
      <guid isPermaLink="true">https://8iggy.tistory.com/271</guid>
      <comments>https://8iggy.tistory.com/271#entry271comment</comments>
      <pubDate>Thu, 8 Dec 2022 16:22:42 +0900</pubDate>
    </item>
    <item>
      <title>Android | Jetpack Compose Remember State</title>
      <link>https://8iggy.tistory.com/270</link>
      <description>&lt;h2&gt;읽기 전&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.&lt;/li&gt;
&lt;li&gt;개인적으로 실습하면서 배운 점을 정리한 글입니다,&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.android.com/codelabs/jetpack-compose-state#2&quot;&gt;Jetpack Compose의 State 코드랩&lt;/a&gt;과 &lt;a href=&quot;https://developer.android.com/jetpack/compose/state&quot;&gt;Jetpack Compose State 공식문서&lt;/a&gt;의 내용을 정리합니다. Compose가 상태를 추적하여 UI를 변경하게끔 코드를 작성하고 해당 상태를 저장합니다. Recomposition이 발생하여 Compose가 재생성되며 기존 상태를 복원하는 코드를 작성합니다.&lt;/p&gt;
&lt;h2&gt;관련 자료&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://8iggy.tistory.com/269&quot;&gt;Android | Jetpack Compose State &amp;amp; Event&lt;/a&gt;에서 이어지는 포스트입니다.&lt;/p&gt;
&lt;h2&gt;Compose State 추적&lt;/h2&gt;
&lt;p&gt;상태가 변경되면 영향을 받는 Composable 함수를 새 상태로 재실행하여 새로운 업데이트 UI가 생성되며 이를 Recomposition이 발생한다고 표현한다. 매번 모든 Composable 코드를 재실행할 수는 없기에 영향을 받지 않는 요소들은 건너뛰고 개별 Composable에 필요한 데이터만을 확인하여 업데이트 한다. 그렇기에 Compose가 UI 업데이트를 위해 어떤 상태를 추적해야 하는지 알아야 한다. 그래야 해당 상태가 업데이트 될 때 Recomposition을 예약할 수 있기 때문이다.&lt;/p&gt;
&lt;p&gt;Compose에는 특정 상태를 읽는 Composable의 Recomposition을 예약할 수 있는 특정 상태 추적 시스템이 있다. 이를 통해 Compose가 전체 UI를 변경하지 않고 일부 Composable만 재구성할 수 있다. 해당 작업은 &lt;strong&gt;쓰기(상태 변경)&lt;/strong&gt;뿐만 아니라 상태에 대한 &lt;strong&gt;읽기&lt;/strong&gt;도 추적하여 실행된다.&lt;/p&gt;
&lt;p&gt;Compose의 &lt;code&gt;State&lt;/code&gt;와 &lt;code&gt;MutableState&lt;/code&gt;를 사용하여 Compose에서 상태를 관찰할 수 있다. 상태 &lt;code&gt;value&lt;/code&gt; 속성을 읽는 각 Composable을 추적하고 해당 &lt;code&gt;value&lt;/code&gt;가 변경되면 Recomposition을 트리거한다. &lt;code&gt;mutableStateOf&lt;/code&gt;함수를 사용하여 관찰 가능한 &lt;code&gt;MutableState&lt;/code&gt;를 만들 수도 있다. 해당 함수는 initial 값을 매개변수로 받아 State 객체에 래핑하여 상태의 &lt;code&gt;value&lt;/code&gt; 값을 관찰 가능한 상태로 만든다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;count&lt;/code&gt; 변수가 0을 초기값으로 갖도록 &lt;code&gt;mutableStateOf&lt;/code&gt;함수를 적용하면 &lt;code&gt;mutableStateOf&lt;/code&gt;함수는 &lt;code&gt;MutableState&lt;/code&gt;타입을 반환한다. 따라서, &lt;code&gt;value&lt;/code&gt;를 업데이트하면 상태가 업데이트되며 Compose는 &lt;code&gt;value&lt;/code&gt;를 읽는 Composable 함수에 Recomposition을 트리거한다.&lt;/p&gt;
&lt;h4&gt;WaterCounter.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
      // Changes to count are now tracked by Compose
       val count: MutableState&amp;lt;Int&amp;gt; = mutableStateOf(0)

       Text(&amp;quot;You&amp;#39;ve had ${count.value} glasses.&amp;quot;)
        Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
           Text(&amp;quot;Add one&amp;quot;)
       }
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;count&lt;/code&gt;가 변경되면 &lt;code&gt;count&lt;/code&gt;의 &lt;code&gt;value&lt;/code&gt;를 읽는 Composable함수의 Recomposition이 예약되므로 &lt;code&gt;WaterCounter&lt;/code&gt; Composable 함수는 버튼을 클릭할 때마다 재구성된다. 그러나, 앱을 실행해보면 아무 일도 발생하지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vchXh/btrS5yDlXLE/S9Vxyn5Ge1dQwigOffS6pk/img.gif&quot; alt=&quot;Android_Remember_State_001&quot;&gt;&lt;/p&gt;
&lt;p&gt;Recomposition 예약은 버튼 클릭 리스너로 &lt;code&gt;count&lt;/code&gt; 변수의 &lt;code&gt;value&lt;/code&gt;를 증가시켰는데 왜 텍스트에 변화가 발생하지 않을까?&lt;/p&gt;
&lt;p&gt;그 이유는 &lt;code&gt;WaterCounter&lt;/code&gt;가 재구성되면서 count 변수에 다시 &lt;code&gt;mutableStateOf(0)&lt;/code&gt; 재할당으로 인해 0으로 초기화되었기 때문이다. 따라서, Recomposition이 발생하더라도 해당 변수의 값을 유지시킬 방법이 필요하다.&lt;/p&gt;
&lt;h2&gt;Compose State 저장&lt;/h2&gt;
&lt;p&gt;Compose에서는 Composable Inline 함수인 &lt;code&gt;remember&lt;/code&gt; API를 지원한다. &lt;code&gt;remember&lt;/code&gt;로 계산된 값은 Initial Composition 중에 Composition에 저장되고 저장된 값은 Recomposition 발생 시 유지된다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;remember&lt;/code&gt;로 &lt;code&gt;mutableStateOf&lt;/code&gt;를 감싸서 변수에 할당하면 다음과 같이 사용할 수 있다.&lt;/p&gt;
&lt;h4&gt;WaterCounter.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        val count: MutableState&amp;lt;Int&amp;gt; = remember { mutableStateOf(0) }
        Text(&amp;quot;You&amp;#39;ve had ${count.value} glasses.&amp;quot;)
        Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
            Text(&amp;quot;Add one&amp;quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다만, 이렇게 할당을 해버리면 매번 &lt;code&gt;value&lt;/code&gt;속성을 찾을 때마다 직접 getter 호출을 해야 한다. Kotlin에서 지원하는 Delegated Properties(위임된 속성)을 사용하여 &lt;code&gt;value&lt;/code&gt;속성 호출을 간소화할 수 있다.&lt;/p&gt;
&lt;h4&gt;WaterCounter.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       var count by remember { mutableStateOf(0) }

       Text(&amp;quot;You&amp;#39;ve had $count glasses.&amp;quot;)
       Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
           Text(&amp;quot;Add one&amp;quot;)
       }
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;by 키워드를 사용하여 속성을 위임한 뒤 버튼 클릭 시 데이터가 변경되므로 &lt;code&gt;var&lt;/code&gt; 키워드로 변수를 정의한다. 앱을 실행해보면 아래와 같이 동작한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GUiEa/btrS3qe0hcy/gFSmd2nI49kOE9C1DvPI10/img.gif&quot; alt=&quot;Android_Remember_State_002&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Compose 밖에서 상태 저장&lt;/h3&gt;
&lt;p&gt;앞선 예제에선 Compose에서 상태를 저장한 뒤 읽었는데 크기가 비교적 큰 데이터는 UI 코드로 저장하기엔 부담이 있다. 따라서, ViewModel 등으로 분리하여 데이터를 보관하는데 해당 데이터가 변경될 때 인지하여 UI를 변경해야 하는 상황이 필요하다. 일반적으로 LiveData, StateFlow, Flow, Observable(RxJava) 등 관찰 가능한 유형을 사용하여 상태를 앱에 저장할 수 있다. 이들은 단순히 &lt;strong&gt;관찰 가능한&lt;/strong&gt; 데이터일 뿐, Compose가 해당 데이터 객체를 관찰하고 상태가 변경될 때 자동으로 재구성되도록 하기 위해선 State로 매핑해야 한다.&lt;/p&gt;
&lt;h4&gt;예시코드 - LiveData&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class ExampleViewModel: ViewModel() {
    private val _exampleData: MutableLiveData&amp;lt;Tasks&amp;gt; = MutableLiveData()
    val exampleData: LiveData&amp;lt;Tasks&amp;gt; = _data
}

@Composable
fun ExampleComposable(
    exampleViewModel: ExampleViewModel = viewModel()
) {
    val exampleData by exampleViewModel.exampleData.observeAsState()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;observeAsState&lt;/code&gt;를 사용하여 State로 매핑하였다. 이제 Compose는 &lt;code&gt;ExampleViewModel&lt;/code&gt;에 선언된 &lt;code&gt;LiveData&lt;/code&gt; 객체가 갱신되면 상태 변화로 인지하여 Recomposition을 수행할 것이다.&lt;/p&gt;
&lt;h2&gt;저장된 상태 기반 UI&lt;/h2&gt;
&lt;p&gt;Compose는 선언한 UI 프레임워크이므로 상태 변경 시 UI 구성요소를 삭제하거나 공개 상태를 변경하지 않고 특정 상태의 조건에서 UI가 어떻게 존재하는 지 설명한다. Recomposition 발생 이후 UI가 업데이트된 결과로 Composable이 Composition을 수행하거나 종료할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bI5ZKe/btrS3ouKdRQ/x39KE20BLCHES2kC7WC11K/img.png&quot; alt=&quot;Android_Remember_State_003&quot;&gt;&lt;/p&gt;
&lt;p&gt;즉, 위 그림에 따라 UI를 설명하는 &amp;quot;Composition&amp;quot;이 존재하고 Composable이 Composition으로 진입한 뒤 현재 상태에 따라 n회 Recompose된다. 이후 조건에 맞지 않으면 Composition에서 사라져 종료된다. 결국 Composable 함수가 Initial Composition이나 Recomposition에서 호출되는 경우에만 Composition에 Composable 함수가 위치하고 호출되지 않는다면 Composable 함수는 더 이상 Composition에 존재하지 않음을 의미한다.&lt;/p&gt;
&lt;h3&gt;상태에 따른 UI 출력&lt;/h3&gt;
&lt;p&gt;버튼 클릭 시 텍스트 값이 1씩 증가하도록 코드를 작성하였다. 만약 &lt;code&gt;count&lt;/code&gt; 변수의 &lt;code&gt;value&lt;/code&gt;가 0보다 클 때만 텍스트를 보여주려 한다면 다음과 같이 코드를 작성할 수 있겠다.&lt;/p&gt;
&lt;h4&gt;WaterCounter.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       var count by remember { mutableStateOf(0) }

       if (count &amp;gt; 0) {
           // This text is present if the button has been clicked
           // at least once; absent otherwise
           Text(&amp;quot;You&amp;#39;ve had $count glasses.&amp;quot;)
       }
       Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
           Text(&amp;quot;Add one&amp;quot;)
       }
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Layout Inspector로 Composition의 트리를 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biqBt8/btrS6h16bA6/oQSQyaFY5WkMCKsiuZgnWk/img.png&quot; alt=&quot;Android_Remember_State_004&quot;&gt;&lt;/p&gt;
&lt;p&gt;앱을 실행해보면 아직 &lt;code&gt;count&lt;/code&gt;가 0이므로 Text 컴포저블이 배치되지 않았다. 이제 에뮬레이터의 버튼을 누르면 다음의 과정을 거쳐 텍스트가 생성된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;count&lt;/code&gt;의 &lt;code&gt;value&lt;/code&gt; 값이 1증가하면서 상태가 변경됨&lt;/li&gt;
&lt;li&gt;Recomposition 예약&lt;/li&gt;
&lt;li&gt;화면이 새로운 Element로 Recompose됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/80jBc/btrS5y4pkRI/kmA5gppByqzG0x21pWW3kk/img.png&quot; alt=&quot;Android_Remember_State_005&quot;&gt;&lt;/p&gt;
&lt;p&gt;다시 Layout Inspector로 Element트리를 확인하면 Text 컴포저블도 추가되었음을 확인할 수 있다.&lt;/p&gt;
&lt;h3&gt;상태는 특정 순간에 UI에 표시되는 요소를 결정&lt;/h3&gt;
&lt;p&gt;만약 &lt;code&gt;count&lt;/code&gt; 변수의 값이 10이 될 때까지 버튼을 활성화하고 그 이후로는 사용할 수 없도록 만들고 싶다면 버튼 컴포저블의 enabled 매개변수를 사용하면 된다.&lt;/p&gt;
&lt;h4&gt;WaterCounter.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       var count by remember { mutableStateOf(0) }

       if (count &amp;gt; 0) {
           Text(&amp;quot;You&amp;#39;ve had $count glasses.&amp;quot;)
       }
       Button(
           onClick = { count++ },
           enabled = count &amp;lt; 10,
           Modifier.padding(top = 8.dp)) {
           Text(&amp;quot;Add one&amp;quot;)
       }
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2hvVA/btrS6gvq8Yq/uBhFFuak2O1V19qj1mP6sK/img.gif&quot; alt=&quot;Android_Remember_State_006&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Composition의 Remember&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;remember&lt;/code&gt;는 컴포지션에 객체를 저장한 뒤, &lt;code&gt;remember&lt;/code&gt;가 호출되는 소스 위치가 리컴포지션 중에 재호출되지 않으면 객체를 삭제한다. 해당 문장이 어떤 뜻인지 시각적인 확인을 위해 추가로 수행할 동작을 아래와 같이 정의한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;사용자가 물을 한 잔 이상 마셨을 때 사용자가 할 Wellness 작업을 표시하고 닫을 수 있도록 제공&lt;/li&gt;
&lt;li&gt;사용자가 마신 물을 0으로 초기화하는 기능 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;WellnessTaskItem.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WellnessTaskItem(
    taskName: String,
    onClose: () -&amp;gt; Unit,
    modifier: Modifier = Modifier
) {
    Row(
        modifier = modifier, verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            modifier = Modifier.weight(1f).padding(start = 16.dp),
            text = taskName
        )
        IconButton(onClick = onClose) {
            Icon(Icons.Filled.Close, contentDescription = &amp;quot;Close&amp;quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;표시할 task 이름과 닫기 버튼 클릭 시 수행할 람다 함수를 파라미터로 받는 Composable 함수를 정의하였다.&lt;/p&gt;
&lt;p&gt;이제 &lt;code&gt;count&lt;/code&gt;가 0보다 크면 &lt;code&gt;WellnessTaskItem&lt;/code&gt;이 표시되도록 &lt;code&gt;WaterCounter&lt;/code&gt;를 수정한다. 또한, 닫기 기능을 지원하기로 했으므로 &lt;code&gt;showTask&lt;/code&gt;가 true인 경우 &lt;code&gt;WellnessTaskItem&lt;/code&gt;을 표시하도록 새로운 if 문을 추가하고 &lt;code&gt;remember&lt;/code&gt; API를 사용하여 리컴포지션이 발생하더라도 값이 유지되도록 한다. &lt;code&gt;showTask&lt;/code&gt; 변수를 선언했으므로 닫기 클릭 시 &lt;code&gt;showTask&lt;/code&gt; 변수를 false로 변경하도록 이벤트를 파라미터로 넘겨준다.&lt;/p&gt;
&lt;p&gt;마지막으로 &lt;code&gt;count&lt;/code&gt;변수를 0으로 초기화해주는 버튼을 추가해서 UI를 초기 상태로 되돌리는 기능을 제공한다.&lt;/p&gt;
&lt;h4&gt;WaterCounter.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       var count by remember { mutableStateOf(0) }
       if (count &amp;gt; 0) {
           var showTask by remember { mutableStateOf(true) }
           if (showTask) {
               WellnessTaskItem(
                   onClose = { showTask = false },
                   taskName = &amp;quot;Have you taken your 15 minute walk today?&amp;quot;
               )
           }
           Text(&amp;quot;You&amp;#39;ve had $count glasses.&amp;quot;)
       }

       Row(Modifier.padding(top = 8.dp)) {
           Button(onClick = { count++ }, enabled = count &amp;lt; 10) {
               Text(&amp;quot;Add one&amp;quot;)
           }
           Button(onClick = { count = 0 }, Modifier.padding(start = 8.dp)) {
               Text(&amp;quot;Clear water count&amp;quot;)
           }
       }
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;앱을 실행하면 아래 그림과 같이 화면이 구성된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s5yo5/btrS5Qp84iX/3afkHdSuJZ7xt4LmR3mjFk/img.png&quot; alt=&quot;Android_Remember_State_007&quot;&gt;&lt;/p&gt;
&lt;p&gt;Element 트리는 우측 그림과 같이 표현되는데 &lt;code&gt;count&lt;/code&gt;와 &lt;code&gt;showTask&lt;/code&gt;는 현재 기억된 값이고 &lt;code&gt;count&lt;/code&gt;가 0인 상태이므로 버튼이 담긴 Row 컴포저블만 컴포지션에 포함된다.&lt;/p&gt;
&lt;p&gt;이후 앱에서 &lt;code&gt;Add one&lt;/code&gt; 버튼을 클릭해보면 &lt;code&gt;count&lt;/code&gt;가 증가하여 리컴포지션이 발생하고 &lt;code&gt;WellnessTaskItem&lt;/code&gt;과 물의 양을 카운트해주는 &lt;code&gt;Text&lt;/code&gt; 컴포저블이 모두 표시된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MREor/btrS5g30gzv/kEMkkD0YqTAp7OicUyPAa1/img.png&quot; alt=&quot;Android_Remember_State_008&quot;&gt;&lt;/p&gt;
&lt;p&gt;새로 출력된 &lt;code&gt;WellnessTaskItem&lt;/code&gt;의 X 버튼을 누르면 &lt;code&gt;showTask&lt;/code&gt; 변수 상태에 변화가 생겼으므로 다른 리컴포지션이 발생하고 &lt;code&gt;WellnessTaskItem&lt;/code&gt;은 더 이상 표시되지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kCFhm/btrS3o9jWGU/deo1REKSZQrc8xYKuGwmYk/img.png&quot; alt=&quot;Android_Remember_State_009&quot;&gt;&lt;/p&gt;
&lt;p&gt;다시 &lt;code&gt;Add one&lt;/code&gt; 버튼을 클릭하여 사용자가 마신 물의 양을 증가시키면 리컴포지션이 발생한다. 다만, &lt;code&gt;showTask&lt;/code&gt; 변수는 리컴포지션이 발생하여도 false임을 기억하고 있기에 &lt;code&gt;WellnessTaskItem&lt;/code&gt;이 출력되지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8MnUy/btrS1ezhDFY/d1dAEJJzYeFF8E0yQpxvbk/img.png&quot; alt=&quot;Android_Remember_State_010&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Clear water count&lt;/code&gt;버튼을 클릭하여 &lt;code&gt;count&lt;/code&gt;를 0으로 만들면 리컴포지션이 발생한다. &lt;code&gt;count&lt;/code&gt;가 0이 되었으므로 이후 &lt;code&gt;count&lt;/code&gt;를 표시하는 Text 컴포저블과 &lt;code&gt;WellnessTaskItem&lt;/code&gt;과 관련된 모든 코드를 호출하지 않고 컴포지션을 종료한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOXhss/btrS1QkEvgs/siKfI5dxDBNnUuPQCGxMU0/img.png&quot; alt=&quot;Android_Remember_State_011&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;showTask&lt;/code&gt;를 &lt;code&gt;remember&lt;/code&gt;하는 코드가 호출되지 않았으므로 앞서 설명했듯이 &lt;code&gt;showTask&lt;/code&gt;를 저장하는 객체가 컴포지션에서 삭제되고 첫 번째 단계로 복귀한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cio0Az/btrS5NttQOX/suUL6Wm59WUDshIK5oFnpk/img.png&quot; alt=&quot;Android_Remember_State_012&quot;&gt;&lt;/p&gt;
&lt;p&gt;다시 &lt;code&gt;Add one&lt;/code&gt; 버튼을 눌러 &lt;code&gt;count&lt;/code&gt;를 0에서 1로 만들면 리컴포지션이 발생한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYSsql/btrS5z90AJJ/zsKytPn9tp8B7xlwvGgnKK/img.png&quot; alt=&quot;Android_Remember_State_013&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;WellnessTaskItem&lt;/code&gt; 컴포저블이 다시 출력됨을 확인할 수 있다. &lt;code&gt;showTask&lt;/code&gt;를 &lt;code&gt;remember&lt;/code&gt;하는 코드가 호출되지 않아 &lt;code&gt;showTask&lt;/code&gt;를 저장하는 객체가 컴포지션에서 삭제되었기 때문이다.&lt;/p&gt;
&lt;p&gt;만약 &lt;code&gt;showTask&lt;/code&gt; 변수의 값을 저장하는 코드의 위치를 바꾸면 리컴포지션 발생 시 어떤 차이가 있는지 확인해보자.&lt;/p&gt;
&lt;h3&gt;상태 코드 선언 위치에 따른 차이&lt;/h3&gt;
&lt;h4&gt;WaterCounter.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       var count by remember { mutableStateOf(0) }
       var showTask by remember { mutableStateOf(true) }
       if (count &amp;gt; 0) {
           //var showTask by remember { mutableStateOf(true) }
           if (showTask) {
               WellnessTaskItem(
                   onClose = { showTask = false },
                   taskName = &amp;quot;Have you taken your 15 minute walk today?&amp;quot;
               )
           }
           Text(&amp;quot;You&amp;#39;ve had $count glasses.&amp;quot;)
       }

       Row(Modifier.padding(top = 8.dp)) {
           Button(onClick = { count++ }, enabled = count &amp;lt; 10) {
               Text(&amp;quot;Add one&amp;quot;)
           }
           Button(onClick = { count = 0 }, Modifier.padding(start = 8.dp)) {
               Text(&amp;quot;Clear water count&amp;quot;)
           }
       }
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;showTask&lt;/code&gt; 변수의 상태 저장 코드를 &lt;code&gt;count&lt;/code&gt; 변수 값 확인 코드 이전에 작성하였다. 이전 코드는 &lt;code&gt;remember&lt;/code&gt; 코드가 &lt;code&gt;count&lt;/code&gt; 변수 분기문 다음에 작성되어 있어 아래와 같이 동작했다.&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;showTask&lt;/code&gt;변수를 &lt;code&gt;remember&lt;/code&gt;하는 코드가 분기문 안에 있을 때 동작 흐름&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;count&lt;/code&gt; 변수가 0 -&amp;gt; 1로 전환될 시 &lt;code&gt;showTask&lt;/code&gt; 변수를 컴포지션에 넣으면서 &lt;code&gt;WellnessTaskItem&lt;/code&gt; 출력&lt;/li&gt;
&lt;li&gt;닫기 버튼 클릭하여 &lt;code&gt;showTask&lt;/code&gt; 값 false로 전환&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count&lt;/code&gt; 변수 0으로 초기화 시 &lt;code&gt;showTask&lt;/code&gt; 변수를 &lt;code&gt;remember&lt;/code&gt;하는 위치까지 도달하지 않아 컴포지션에서 삭제&lt;/li&gt;
&lt;li&gt;다시 &lt;code&gt;count&lt;/code&gt; 변수를 0 -&amp;gt; 1로 전환 시 이전 &lt;code&gt;showTask&lt;/code&gt; 변수의 값이 삭제되어 &lt;code&gt;WellnessTaskItem&lt;/code&gt; 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이제 위 코드대로 &lt;code&gt;showTask&lt;/code&gt; 변수를 &lt;code&gt;remember&lt;/code&gt;하는 코드를 &lt;code&gt;count&lt;/code&gt; 변수 분기문 바깥으로 빼고 실행해보면 동작 과정이 사뭇 달라진다.&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;showTask&lt;/code&gt;변수를 &lt;code&gt;remember&lt;/code&gt;하는 코드가 분기문 밖에 있을 때 동작 흐름&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;count&lt;/code&gt; 변수와 &lt;code&gt;showTask&lt;/code&gt; 변수를 각각 0과 true로 초기화하여 &lt;code&gt;remember&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count&lt;/code&gt; 변수가 0 -&amp;gt; 1로 전환될 시 &lt;code&gt;showTask&lt;/code&gt; 변수가 이미 true이므로 &lt;code&gt;WellnessTaskItem&lt;/code&gt; 출력&lt;/li&gt;
&lt;li&gt;닫기 버튼 클릭하여 &lt;code&gt;showTask&lt;/code&gt; 값 false로 전환&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count&lt;/code&gt; 변수 0으로 초기화 시 showTask 변수를 &lt;code&gt;remember&lt;/code&gt;하는 코드가 &lt;code&gt;count&lt;/code&gt; 분기 밖이어서 도달됨&lt;/li&gt;
&lt;li&gt;다시 &lt;code&gt;count&lt;/code&gt; 변수를 0 -&amp;gt; 1로 전환 시 이전 &lt;code&gt;showTask&lt;/code&gt; 변수의 값이 존재하기에 &lt;code&gt;WellnessTaskItem&lt;/code&gt;이 출력되지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;흔히 개발을 하다보면 상태 관련 코드는 가급적 함수 상단에 몰아 넣는 경우가 많다. 그게 아니더라도 딱히 코드 선언 위치에 대해 고려하지 않는다. 그러나, 리컴포지션 시 도달하지 못한 &lt;code&gt;remember&lt;/code&gt; 객체가 삭제된다는 점을 참고하면 Compose 사용 시 코드의 위치도 염두에 두어야 한다.&lt;/p&gt;
&lt;h2&gt;참고자료&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/codelabs/jetpack-compose-state#4&quot;&gt;Google Codelab - Jetpack Compose의 상태 #5 구성 가능한 함수의 메모리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/codelabs/jetpack-compose-state#5&quot;&gt;Google Codelab - Jetpack Compose의 상태 #6 상태 기반 UI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/codelabs/jetpack-compose-state#6&quot;&gt;Google Codelab - Jetpack Compose의 상태 #7 컴포지션의 Remember&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/jetpack/compose/state&quot;&gt;Android Developers - 상태 및 Jetpack Compose&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Android</category>
      <category>Android</category>
      <category>codelab</category>
      <category>compose</category>
      <category>jetpack compose</category>
      <category>Remember</category>
      <category>state</category>
      <category>안드로이드</category>
      <category>컴포즈</category>
      <author>8iggy</author>
      <guid isPermaLink="true">https://8iggy.tistory.com/270</guid>
      <comments>https://8iggy.tistory.com/270#entry270comment</comments>
      <pubDate>Wed, 7 Dec 2022 21:18:17 +0900</pubDate>
    </item>
    <item>
      <title>Android | Jetpack Compose State &amp;amp; Event</title>
      <link>https://8iggy.tistory.com/269</link>
      <description>&lt;h2&gt;읽기 전&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.&lt;/li&gt;
&lt;li&gt;개인적으로 실습하면서 배운 점을 정리한 글입니다,&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.android.com/codelabs/jetpack-compose-state#2&quot;&gt;Jetpack Compose의 State 코드랩&lt;/a&gt;과 &lt;a href=&quot;https://developer.android.com/jetpack/compose/state&quot;&gt;Jetpack Compose State 공식문서&lt;/a&gt;의 내용을 정리합니다. UI를 결정짓는 State와 State 변경을 위해 트리거되는 Event에 대해 정의합니다.&lt;/p&gt;
&lt;h3&gt;Composition 관련 용어&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;컴포지션(Composition): Jetpack Compose가 Composable 함수를 실행할 때 빌드한 UI&lt;/li&gt;
&lt;li&gt;초기 첨포지션(Initial Composition): 처음 Composable을 실행하여 Composition 생성&lt;/li&gt;
&lt;li&gt;리컴포지션(Recomposition): 상태 변경 시 Composition을 업데이트하기 위해 Composable을 다시 실행하는 과정&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;State란?&lt;/h2&gt;
&lt;p&gt;시간이 지남에 따라 변할 수 있는 값으로 광범위하게 정의되어 있다. Room DB 데이터부터 클래스 변수까지 포괄하기 때문이다. 요약하자면, 상태에 따라 특정 시점에 앱 UI에 표시되는 항목이 결정된다고 이해하면 된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;State 예시&lt;ul&gt;
&lt;li&gt;채팅앱 최근 수신 메세지&lt;/li&gt;
&lt;li&gt;사용자의 프로필 사진&lt;/li&gt;
&lt;li&gt;목록의 스크롤 위치&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;State &amp;amp; Event &amp;amp; Composition&lt;/h2&gt;
&lt;p&gt;Compose는 선언형이므로 Compose 업데이트를 위해서는 새로운 인수로 동일 컴포저블을 호출해야 한다. 그리고 해당 &amp;quot;인수&amp;quot;는 UI 상태를 표현한다. 상태가 업데이트되면 재구성(Recomposition)이 발생한다.&lt;/p&gt;
&lt;p&gt;우선 특정 카운트를 표시하는 Composable 함수를 생성하고 Activity에 설정한다.&lt;/p&gt;
&lt;h4&gt;WaterCounter.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   val count = 0
   Text(
       text = &amp;quot;You&amp;#39;ve had $count glasses.&amp;quot;,
       modifier = modifier.padding(16.dp)
   )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;몇 잔의 물을 마셨는지 출력하는 텍스트 컴포저블을 정의하였다.&lt;/p&gt;
&lt;h4&gt;WellnessScreen.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   WaterCounter(modifier)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;얼마나 마셨는지 출력하는 WaterCounter 컴포저블 함수를 출력하는 Screen 함수이다.&lt;/p&gt;
&lt;h4&gt;MainActivity.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           // 프로젝트 이름에 따라 변경되는 Theme
           BasicStateCodelabTheme {
               // A surface container using the &amp;#39;background&amp;#39; color from the theme
               Surface(
                   modifier = Modifier.fillMaxSize(),
                   color = MaterialTheme.colors.background
               ) {
                   WellnessScreen()
               }
           }
       }
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;실행결과&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nxuQr/btrSIRj21sv/3XCKujLpaEJZtaHfDzhlSk/img.png&quot; alt=&quot;Android_Jetpack_Compose_State_Event_001&quot;&gt;&lt;/p&gt;
&lt;p&gt;물을 얼마나 마셨는지 표시하는 WaterCounter 함수는 &lt;code&gt;count&lt;/code&gt; 변수의 상태에 따라 결정된다. 버튼을 정의해서 &lt;code&gt;count&lt;/code&gt; 변수를 증가시켜 보자.&lt;/p&gt;
&lt;h3&gt;Compose Event&lt;/h3&gt;
&lt;p&gt;상태의 업데이트는 이벤트에 대한 응답 기반으로 이루어진다. 흔히 버튼 누르기 등 UI 요소와 사용자 간의 상호작용이나 네트워크 응답 등으로 트리거된다. 이에 공식문서는 다음과 같이 상태와 이벤트를 표현한다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;상태는 &lt;strong&gt;존재&lt;/strong&gt;하고 이벤트는 &lt;strong&gt;발생&lt;/strong&gt;한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Android는 다음 그림으로 표현되는 핵심 UI 업데이트 루프를 갖는다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdoCf7/btrSI9SiobD/vXBb5q5mdyvpVQTZy7ERZk/img.png&quot; alt=&quot;Android_Jetpack_Compose_State Event_002&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이벤트: 사용자 및 기타 요인에 의해 발생&lt;/li&gt;
&lt;li&gt;상태 업데이트: 이벤트 핸들러가 UI에서 사용하는 상태를 변경&lt;/li&gt;
&lt;li&gt;상태 표시: 새로운 상태를 표시하도록 UI가 업데이트 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;사용자가 상호작용할 수 있도록 버튼을 정의하여 &lt;code&gt;count&lt;/code&gt; 변수가 증가게끔 설정한다.&lt;/p&gt;
&lt;h4&gt;WaterCounter.kt&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       var count = 0
       Text(&amp;quot;You&amp;#39;ve had $count glasses.&amp;quot;)
       Button(
           onClick = { count++ },
           Modifier.padding(top = 8.dp)
       ) {
           Text(&amp;quot;Add one&amp;quot;)
       }
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;사용자가 이벤트를 발생시키도록 버튼을 정의하였고 클릭 시 상태인 &lt;code&gt;count&lt;/code&gt; 변수를 증가시켰으나 아무 일도 일어나지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cT2iE0/btrSKvAiizO/IclUibNgFhqW4CORNEFEk1/img.png&quot; alt=&quot;Android_Jetpack_Compose_State Event_003&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;count&lt;/code&gt; 변수에 다른 값이 설정되어도 Compose에서 해당 변수의 값 변경을 &amp;quot;상태 변경&amp;quot;으로 감지하지 않았기에 UI 업데이트(컴포저블 함수의 재구성 즉, Recomposition)가 일어나지 않는다. 상태가 변경될 때 Compose에 화면을 다시 그려야 한다고 알리지 않았기 때문이다.&lt;/p&gt;
&lt;h2&gt;참고자료&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/codelabs/jetpack-compose-state#2&quot;&gt;Google Codelab - Jetpack Compose의 상태 #3 Compose에서의 상태&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/codelabs/jetpack-compose-state#3&quot;&gt;Google Codelab - Jetpack Compose의 상태 #4 Compose에서의 이벤트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/jetpack/compose/state&quot;&gt;Android Developers - 상태 및 Jetpack Compose&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Android</category>
      <category>Android</category>
      <category>codelab</category>
      <category>compose</category>
      <category>event</category>
      <category>Jetpack</category>
      <category>jetpack compose</category>
      <category>state</category>
      <category>안드로이드</category>
      <category>컴포즈</category>
      <author>8iggy</author>
      <guid isPermaLink="true">https://8iggy.tistory.com/269</guid>
      <comments>https://8iggy.tistory.com/269#entry269comment</comments>
      <pubDate>Mon, 5 Dec 2022 00:35:31 +0900</pubDate>
    </item>
    <item>
      <title>2023 KAKAO BLIND RECRUITMENT 코딩테스트 후기</title>
      <link>https://8iggy.tistory.com/268</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;9월 24일 토요일에 치뤄졌다. 오후에 14시부터 19시까지 5시간 동안 진행되었다. 작년에 LINE 코테와 중복된 날째로 진행된 것과는 달리 다른 날에 열렸다. 총 7문제로 난이도는 역시 카카오는 카카오. 이미 합격해서 간절함이 없어서일까 3솔로 마무리했다. 예상 합격컷은 4솔&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;1번은 문제를 저수준으로 치환할 수 있으면 바로 풀리는 문제였다. 데이터를 억지로 주어진 대로만 가공하려 했다면 꽤나 많은 시간을 소요했을 것이다. 2번은 그?리디 느낌이 나는 반복문 문제였다. 1번과 2번을 해결하면서 느낀 점은 더 이상 12번에서 개날먹 문제를 던져주지는 않는다는 점이다. 이전까지는 말 그대로 지문을 읽으면 슉 해결되었다면 지금은 지문을 읽고 이해한 뒤 나름대로의 해석을 해야 코드를 작성할 수 있다. 확실히 고급 문제에서 난이도를 올리기보단 초급 문제를 조금 어렵게 출제하여 컷을 조금 높히겠다는 느낌이 들었다. 3번은 완전탐색인데 python유저가 아니면 꽤나 애를 먹었겠다 싶은 문제가 나왔다. 필자도 문제를 3차원 배열로 치환해서 생각하느라 조금 헤맸다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&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: #000000;&quot;&gt;4번부터 사실상 카카오 코테의 메인디쉬인데 이미 직장에 다니고 있는 중이라서 그런가 피지컬이 떨어져 제대로 먹지도 못하고 테스트를 종료했다. 문제 유형은 늘 그렇듯이 dp, 트리 등 고급 알고리즘 기법 위주로 나왔다. 4번은 포화 이진트리라는 점과 중위탐색이라는 점을 참고해서 실마리가 비교적 빨리 잡히나 싶었는데 테스트케이스만 통과하고 본 케이스는 통과되지 못했다. 아마 문제에 대해 제대로 이해하지 못한 결과였으리라. 사실 난이도가 너무 확 올라가서 흥미도 식고 실력도 조금 떨어진 게 체감이 되었다. 가끔은 문제를 풀어주긴 해야겠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&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: #000000;&quot;&gt;체감상 123번은 반드시 해결을 해야만 했고 4번을 해결하거나 5번의 정확성 파트를 해결해야 2차 코테 응시 기회가 주어지지 않을까 싶다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;이번 네이버 하반기 공채 코테도 꽤나 어려운 거 같던데 아무래도 자료구조 구현이나 기본 알고리즘은 외워서 적용하면 풀리는 문제들이 나오지 않아서인지 점점 난이도가 올라가는 느낌이다. 다만, 코테에서 문해력의 중요성이 더 올라가는 흐름은 환영할 만한 일이다. 이제 코테 준비하려면 책도 읽어둬야 하는 시대가 올지도 모르겠다 ㅋㅋ&lt;/span&gt;&lt;/p&gt;</description>
      <category>기록/코딩테스트</category>
      <author>8iggy</author>
      <guid isPermaLink="true">https://8iggy.tistory.com/268</guid>
      <comments>https://8iggy.tistory.com/268#entry268comment</comments>
      <pubDate>Tue, 27 Sep 2022 19:09:20 +0900</pubDate>
    </item>
  </channel>
</rss>